diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c5274dd..f61d47da6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ **Features** +- Added link click conversion tracking for external and special links (mailto:, tel:, etc.) within popups. Clicks are tracked via analytics beacon and categorized by link type for conversion reporting. - Added [Beaver Builder Forms integration](https://wppopupmaker.com/form-integrations/beaver-builder/) for form submission tracking and conversion analytics. Supports Contact, Subscribe, and Login form modules. - Added [Bit Form integration](https://wppopupmaker.com/form-integrations/bit-form/) for form submission tracking and conversion analytics. - Added [Elementor Pro Forms integration](https://wppopupmaker.com/form-integrations/elementor-forms/) for form submission tracking and conversion analytics with support for targeting specific forms. @@ -15,12 +16,14 @@ **Improvements** +- Improved PID tracking reliability by firing template_redirect at priority 0, ensuring tracking occurs before other plugins that might redirect. - Enhanced all Popup list views with sortable Enabled column and bulk enable/disable actions for easier management of multiple popups. - Block library assets (CSS) loading unnecessarily on all front-end pages. WordPress now automatically loads these styles only when Popup Maker blocks are actually rendered. - Enhanced ad-blocker bypass feature to obfuscate script and style element IDs (in addition to filenames) for improved bypass reliability. IDs now consistently use per site settings using either MD5 hashing or custom prefixes. **Fixes** +- Fixed mailto: and tel: links inside popups being incorrectly modified with tracking parameters, which broke email and phone links. - Fixed Fluent Forms integration fatal error when using double opt-in. Closes #1094. - Fixed Time Delay trigger settings tab displaying blank when switching from Click Trigger advanced tab. Closes #1109. - Fixed trigger modal "Add" button label not displaying due to incorrect i18n function usage. Props to @DAnn2012. diff --git a/assets/js/src/site/plugins/pum-url-tracking.js b/assets/js/src/site/plugins/pum-url-tracking.js index 6f2702e38..f840630cb 100644 --- a/assets/js/src/site/plugins/pum-url-tracking.js +++ b/assets/js/src/site/plugins/pum-url-tracking.js @@ -12,6 +12,7 @@ * * Handles: * - Appending pid (popup ID) to internal links within popups + * - Firing click conversion beacons for external/special links (mailto, tel, etc.) */ window.PUM_URLTracking = { /** @@ -44,6 +45,9 @@ /** * Process all links within a popup to add tracking parameters. * + * Internal links get ?pid= appended (tracked via server redirect). + * External/special links get click handlers for beacon tracking. + * * @param {jQuery} $popup The popup element. * @param {number} pid The popup ID. */ @@ -54,9 +58,8 @@ var $link = $( this ), href = $link.attr( 'href' ); - // Only process internal URLs. if ( self.isInternalUrl( href ) ) { - // Start with base URL parameters. + // Internal URLs: Append PID parameter (tracked via server redirect). var urlParams = { pid: pid }; // Allow extensions to add additional parameters. @@ -71,10 +74,110 @@ var newHref = self.appendParamsToUrl( href, urlParams ); $link.attr( 'href', newHref ); + } else if ( self.shouldTrackClick( href ) ) { + // External/special links: Attach click handler for beacon tracking. + self.attachClickTracking( $link, pid, href ); } } ); }, + /** + * Determine if a link should have click tracking attached. + * + * @param {string} url The URL to check. + * @return {boolean} True if click should be tracked. + */ + shouldTrackClick: function ( url ) { + // Skip empty URLs. + if ( ! url ) { + return false; + } + + // Skip links already tracked via CTA system. + if ( url.indexOf( 'cta=' ) !== -1 ) { + return false; + } + + return true; + }, + + /** + * Get the link type for analytics segmentation. + * + * @param {string} url The URL to categorize. + * @return {string} Link type: 'external', 'mailto', 'tel', or 'other'. + */ + getLinkType: function ( url ) { + if ( url.indexOf( 'mailto:' ) === 0 ) { + return 'mailto'; + } + if ( url.indexOf( 'tel:' ) === 0 ) { + return 'tel'; + } + if ( url.indexOf( 'javascript:' ) === 0 ) { + return 'javascript'; + } + if ( url === '#' || url.indexOf( '#' ) === 0 ) { + return 'anchor'; + } + if ( url.indexOf( 'http' ) === 0 || url.indexOf( '//' ) === 0 ) { + return 'external'; + } + return 'other'; + }, + + /** + * Attach click tracking to a link element. + * + * Fires a conversion beacon when the link is clicked. + * + * @param {jQuery} $link The link element. + * @param {number} pid The popup ID. + * @param {string} href The link URL. + */ + attachClickTracking: function ( $link, pid, href ) { + var self = this; + + // Prevent duplicate handlers. + if ( $link.data( 'pum-click-tracked' ) ) { + return; + } + $link.data( 'pum-click-tracked', true ); + + $link.on( 'click.pum_tracking', function () { + // Only track if analytics is available and enabled. + if ( + ! window.PUM_Analytics || + ! window.pum_vars || + ! window.pum_vars.analytics_enabled + ) { + return; + } + + var data = { + pid: pid, + event: 'conversion', + eventData: { + type: 'link_click', + url: href, + linkType: self.getLinkType( href ), + }, + }; + + // Allow extensions to modify click tracking data. + if ( window.PUM && window.PUM.hooks ) { + data = window.PUM.hooks.applyFilters( + 'popupMaker.popup.linkClickData', + data, + $link + ); + } + + // Fire beacon (sendBeacon queues even during navigation). + window.PUM_Analytics.beacon( data ); + } ); + }, + /** * Check if URL is internal to the current site. * @@ -86,6 +189,14 @@ return false; } + // Skip non-HTTP protocols (mailto:, tel:, javascript:, etc.). + if ( + /^[a-z][a-z0-9+.-]*:/i.test( url ) && + ! /^https?:/i.test( url ) + ) { + return false; + } + // Handle relative URLs. if ( url.indexOf( '/' ) === 0 && url.indexOf( '//' ) !== 0 ) { return true; diff --git a/classes/Controllers/CallToActions.php b/classes/Controllers/CallToActions.php index a5b8fd4fd..01e5a6fe4 100644 --- a/classes/Controllers/CallToActions.php +++ b/classes/Controllers/CallToActions.php @@ -24,7 +24,8 @@ class CallToActions extends Controller { * Initialize cta actions */ public function init() { - add_action( 'template_redirect', [ $this, 'template_redirect' ] ); + // Priority 0 ensures PID tracking fires before other plugins that might redirect. + add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 ); add_action( 'popup_maker/cta_conversion', [ $this, 'track_cta_conversion' ], 10, 2 ); } diff --git a/classes/Plugin/Core.php b/classes/Plugin/Core.php index 06e7212e8..30c3f8c52 100644 --- a/classes/Plugin/Core.php +++ b/classes/Plugin/Core.php @@ -292,6 +292,18 @@ function () { } ); + $this->set( + 'link_click_tracking', + /** + * Get link click tracking service. + * + * @return \PopupMaker\Services\LinkClickTracking + */ + function ( $container ) { + return new \PopupMaker\Services\LinkClickTracking( $container ); + } + ); + do_action( 'popup_maker/register_services', $this ); } @@ -302,6 +314,10 @@ function () { */ protected function init_services() { $license = $this->get( 'license' ); + + // Initialize link click tracking. + $link_click_tracking = $this->get( 'link_click_tracking' ); + $link_click_tracking->init(); } /** diff --git a/classes/Services/LinkClickTracking.php b/classes/Services/LinkClickTracking.php new file mode 100644 index 000000000..882a2c794 --- /dev/null +++ b/classes/Services/LinkClickTracking.php @@ -0,0 +1,232 @@ +increment_site_count(); + + // Increment per-popup count. + $this->increment_popup_count( $popup_id ); + + /** + * Fires after a link click is tracked. + * + * @since X.X.X + * + * @param int $popup_id Popup ID. + * @param array $event_data Link click event data (url, linkType, etc.). + */ + do_action( 'popup_maker/link_click_tracked', $popup_id, $event_data ); + } + + /** + * Increment site-wide link click count. + * + * Uses atomic SQL update to prevent race conditions. + * + * @since X.X.X + * + * @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 ) { + 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 + ) + ); + + wp_cache_delete( self::SITE_COUNT_KEY, 'options' ); + + return (int) get_option( self::SITE_COUNT_KEY, 0 ); + } + + /** + * Increment per-popup link click count. + * + * Uses atomic SQL update to prevent race conditions. + * + * @since X.X.X + * + * @param int $popup_id Popup post ID. + * @return int New count after increment. + */ + protected function increment_popup_count( $popup_id ) { + global $wpdb; + + $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. + $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 + ) + ); + + wp_cache_delete( $popup_id, 'post_meta' ); + + return (int) get_post_meta( $popup_id, self::POPUP_META_KEY, true ); + } + + /** + * Get site-wide link click count. + * + * @since X.X.X + * + * @return int Total link clicks across all popups. + */ + public function get_site_count() { + return (int) get_option( self::SITE_COUNT_KEY, 0 ); + } + + /** + * Get link click count for a specific popup. + * + * @since X.X.X + * + * @param int $popup_id Popup post ID. + * @return int Link clicks 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 link click count. + * + * @since X.X.X + */ + public function reset_site_count() { + delete_option( self::SITE_COUNT_KEY ); + } + + /** + * Reset link click count for a specific popup. + * + * @since X.X.X + * + * @param int $popup_id Popup post ID. + */ + public function reset_popup_count( $popup_id ) { + delete_post_meta( $popup_id, self::POPUP_META_KEY ); + } +}