$args { * Form submission arguments. * * @type int|null $popup_id Popup ID that captured the submission. * @type string|null $form_provider Form plugin name (e.g., 'gravity-forms'). * @type string|null $form_id Form ID from the provider. * @type bool $tracked Whether already tracked by other systems. * } */ public function track_form_conversion( $args ) { // Defensive validation for third-party hook callers. if ( ! is_array( $args ) ) { return; } // Skip if already tracked by another system to prevent duplicates. if ( ! empty( $args['tracked'] ) ) { return; } // Only track submissions that were captured by a popup. if ( empty( $args['popup_id'] ) || ! is_numeric( $args['popup_id'] ) ) { return; } $popup_id = (int) $args['popup_id']; // Verify popup exists before tracking (prevents orphaned meta). $popup = pum_get_popup( $popup_id ); if ( ! pum_is_popup( $popup ) ) { // Log but don't break form submission - tracking is non-critical. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( '[Popup Maker] Skipping form conversion tracking for invalid popup ID: %d', $popup_id ) ); } return; } // Increment site-wide form conversion count. $this->increment_site_count(); // Increment per-popup count. $this->increment_popup_count( $popup_id ); /** * Fires after a form conversion is tracked (non-AJAX). * * @since 1.22.0 * * @param int $popup_id Popup ID. * @param array $args Form submission arguments. */ do_action( 'popup_maker/form_conversion_tracked', $popup_id, $args ); } /** * Track AJAX form conversion from analytics beacon. * * Handles conversions tracked via frontend JS beacon (AJAX submissions). * * @since 1.22.0 * * @param int $popup_id Popup ID from analytics beacon. * @param array $args Additional arguments from beacon. */ public function track_ajax_conversion( $popup_id, $args = [] ) { // Defensive validation for third-party hook callers. if ( ! is_array( $args ) ) { return; } // Extract eventData (matches Pro's pattern). // REST endpoint sanitize_event_data() already decoded JSON to array. $event_data = isset( $args['eventData'] ) ? $args['eventData'] : []; // Only track conversions with explicit form submission metadata. if ( empty( $event_data ) || ! is_array( $event_data ) ) { return; } // Verify this is a form submission event (not CTA or link click). if ( empty( $event_data['type'] ) || 'form_submission' !== $event_data['type'] ) { return; } // Validate popup ID. if ( empty( $popup_id ) || ! is_numeric( $popup_id ) ) { return; } $popup_id = (int) $popup_id; // Verify popup exists before tracking (prevents orphaned meta). $popup = pum_get_popup( $popup_id ); if ( ! pum_is_popup( $popup ) ) { // Log but don't break form submission - tracking is non-critical. if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( sprintf( '[Popup Maker] Skipping AJAX form conversion tracking for invalid popup ID: %d', $popup_id ) ); } return; } // Increment site-wide form conversion count. $this->increment_site_count(); // Increment per-popup count. $this->increment_popup_count( $popup_id ); /** * Fires after an AJAX form conversion is tracked. * * @since 1.22.0 * * @param int $popup_id Popup ID. * @param array $event_data Form submission event data. */ do_action( 'popup_maker/form_conversion_tracked', $popup_id, $event_data ); } /** * Increment site-wide form conversion count. * * Uses atomic SQL update to prevent race conditions when multiple * form submissions occur simultaneously. * * @since 1.22.0 * * @return int New count after increment. */ protected function increment_site_count() { global $wpdb; // Check if option exists; if not, create it with autoload disabled. $exists = $wpdb->get_var( $wpdb->prepare( 'SELECT option_id FROM %i WHERE option_name = %s LIMIT 1', $wpdb->options, self::SITE_COUNT_KEY ) ); if ( ! $exists ) { // Initialize with autoload=no (analytical data doesn't need to load on every request). add_option( self::SITE_COUNT_KEY, 0, '', false ); } // Atomic increment (prevents race condition). $wpdb->query( $wpdb->prepare( 'UPDATE %i SET option_value = option_value + 1 WHERE option_name = %s', $wpdb->options, self::SITE_COUNT_KEY ) ); // Clear cache since we bypassed WordPress's caching layer. wp_cache_delete( self::SITE_COUNT_KEY, 'options' ); // Return updated count. return (int) get_option( self::SITE_COUNT_KEY, 0 ); } /** * Increment per-popup form conversion count. * * Uses atomic SQL update to prevent race conditions when multiple * form submissions occur simultaneously for the same popup. * * @since 1.22.0 * * @param int $popup_id Popup post ID. * @return int New count after increment. */ protected function increment_popup_count( $popup_id ) { global $wpdb; // Check if meta exists; if not, create it. $exists = $wpdb->get_var( $wpdb->prepare( 'SELECT meta_id FROM %i WHERE post_id = %d AND meta_key = %s LIMIT 1', $wpdb->postmeta, $popup_id, self::POPUP_META_KEY ) ); if ( ! $exists ) { add_post_meta( $popup_id, self::POPUP_META_KEY, 0, true ); } // Atomic increment (prevents race condition). $wpdb->query( $wpdb->prepare( 'UPDATE %i SET meta_value = meta_value + 1 WHERE post_id = %d AND meta_key = %s', $wpdb->postmeta, $popup_id, self::POPUP_META_KEY ) ); // Clear cache since we bypassed WordPress's caching layer. wp_cache_delete( $popup_id, 'post_meta' ); // Return updated count. return (int) get_post_meta( $popup_id, self::POPUP_META_KEY, true ); } /** * Get site-wide form conversion count. * * @since 1.22.0 * * @return int Total form conversions across all popups. */ public function get_site_count() { return (int) get_option( self::SITE_COUNT_KEY, 0 ); } /** * Get form conversion count for a specific popup. * * @since 1.22.0 * * @param int $popup_id Popup post ID. * @return int Form conversions for this popup. */ public function get_popup_count( $popup_id ) { return (int) get_post_meta( $popup_id, self::POPUP_META_KEY, true ); } /** * Reset site-wide form conversion count. * * Useful for testing or if data needs to be cleared. * * @since 1.22.0 */ public function reset_site_count() { delete_option( self::SITE_COUNT_KEY ); } /** * Reset form conversion count for a specific popup. * * @since 1.22.0 * * @param int $popup_id Popup post ID. */ public function reset_popup_count( $popup_id ) { delete_post_meta( $popup_id, self::POPUP_META_KEY ); } }