{"id":291281,"date":"2026-03-26T19:12:01","date_gmt":"2026-03-26T19:12:01","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/utm-tracker-for-elementor\/"},"modified":"2026-06-04T09:29:06","modified_gmt":"2026-06-04T09:29:06","slug":"shulman-utm-attribution-for-elementor","status":"publish","type":"plugin","link":"https:\/\/lin.wordpress.org\/plugins\/shulman-utm-attribution-for-elementor\/","author":21137828,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.10.20","stable_tag":"1.10.20","tested":"7.0","requires":"5.8","requires_php":"7.4","requires_plugins":null,"header_name":"Shulman UTM Attribution for Elementor","header_author":"Ranel Shulman","header_description":"Simple and reliable UTM tracking for Elementor Forms and WooCommerce with First Click and Last Click attribution support.","assets_banners_color":"","last_updated":"2026-06-04 09:29:06","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"","header_author_uri":"","rating":5,"author_block_rating":0,"active_installs":10,"downloads":693,"num_ratings":1,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","changelog"],"tags":{"1.10.10":{"tag":"1.10.10","author":"ranelshulman","date":"2026-05-05 21:43:23"},"1.10.11":{"tag":"1.10.11","author":"ranelshulman","date":"2026-05-07 21:38:08"},"1.10.12":{"tag":"1.10.12","author":"ranelshulman","date":"2026-05-12 08:09:40"},"1.10.13":{"tag":"1.10.13","author":"ranelshulman","date":"2026-05-18 11:16:44"},"1.10.14":{"tag":"1.10.14","author":"ranelshulman","date":"2026-05-18 12:24:26"},"1.10.15":{"tag":"1.10.15","author":"ranelshulman","date":"2026-06-04 07:06:02"},"1.10.16":{"tag":"1.10.16","author":"ranelshulman","date":"2026-06-04 08:07:43"},"1.10.17":{"tag":"1.10.17","author":"ranelshulman","date":"2026-06-04 08:17:07"},"1.10.18":{"tag":"1.10.18","author":"ranelshulman","date":"2026-06-04 08:35:13"},"1.10.19":{"tag":"1.10.19","author":"ranelshulman","date":"2026-06-04 09:15:41"},"1.10.20":{"tag":"1.10.20","author":"ranelshulman","date":"2026-06-04 09:29:06"},"1.10.6":{"tag":"1.10.6","author":"ranelshulman","date":"2026-04-20 19:51:29"},"1.10.7":{"tag":"1.10.7","author":"ranelshulman","date":"2026-04-26 15:31:42"},"1.10.8":{"tag":"1.10.8","author":"ranelshulman","date":"2026-04-30 19:01:58"},"1.10.9":{"tag":"1.10.9","author":"ranelshulman","date":"2026-05-03 08:11:05"},"1.7.12":{"tag":"1.7.12","author":"ranelshulman","date":"2026-03-26 19:21:15"},"1.8.0":{"tag":"1.8.0","author":"ranelshulman","date":"2026-04-14 12:38:36"}},"upgrade_notice":[],"ratings":{"1":0,"2":0,"3":0,"4":0,"5":1},"assets_icons":{"icon-256x256.png":{"filename":"icon-256x256.png","revision":3505913,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":[],"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.10.10","1.10.11","1.10.12","1.10.13","1.10.14","1.10.15","1.10.16","1.10.17","1.10.18","1.10.19","1.10.20","1.10.6","1.10.7","1.10.8","1.10.9","1.7.12","1.8.0"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[9067,76538,335,24188,286],"plugin_category":[36,45],"plugin_contributors":[258723],"plugin_business_model":[],"class_list":["post-291281","plugin","type-plugin","status-publish","hentry","plugin_tags-attribution","plugin_tags-elementor","plugin_tags-leads","plugin_tags-utm","plugin_tags-woocommerce","plugin_category-analytics","plugin_category-ecommerce","plugin_contributors-ranelshulman","plugin_committers-ranelshulman"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/shulman-utm-attribution-for-elementor\/assets\/icon-256x256.png?rev=3505913","icon_2x":"https:\/\/ps.w.org\/shulman-utm-attribution-for-elementor\/assets\/icon-256x256.png?rev=3505913","generated":false},"screenshots":[],"raw_content":"<!--section=description-->\n<p>Tracks attribution data from submitted fields, cookie snapshots, first-click cookies, server-side fallback, and WooCommerce attribution in a defined source-of-truth hierarchy.<\/p>\n\n<p>Features:\n* Elementor lead capture with attribution snapshot\n* WooCommerce order attribution support\n* First-touch and last-touch source tracking\n* Leads dashboard with filters and CSV export\n* Stores WooCommerce customer phone numbers alongside attribution data\n* Backward compatibility for legacy cookies, options, and meta keys<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin ZIP in WordPress admin or copy the plugin folder to \/wp-content\/plugins\/.<\/li>\n<li>Activate the plugin.<\/li>\n<li>Configure settings from the plugin admin screen.<\/li>\n<\/ol>\n\n<!--section=changelog-->\n<h4>1.10.20<\/h4>\n\n<ul>\n<li>Removed the remaining Plugin Check warnings in the leads table by inlining the final count\/items SQL execution path instead of passing intermediate query-string variables to $wpdb methods.<\/li>\n<li>Kept the safe prepared-query flow, source filter compatibility, sorting, pagination, and caching introduced in 1.10.19.<\/li>\n<\/ul>\n\n<h4>1.10.19<\/h4>\n\n<ul>\n<li>Refactored the leads table query builder to use a single prepared-query flow for search, source filters, form filters, sorting, and pagination.<\/li>\n<li>Fixed the malformed SQL \/ placeholder mismatches that triggered Plugin Check prepared SQL errors and unescaped DB parameter warnings in class-leads-table.php.<\/li>\n<li>Added object-cache wrapping for lead count and lead list queries to address direct-query caching guidance in the admin table.<\/li>\n<\/ul>\n\n<h4>1.10.18<\/h4>\n\n<ul>\n<li>Fix: extracted direct\/untracked classification into a private classify_missing_attribution() method. Both build_lead_snapshot() (Elementor lead path) and save_utm_to_order() (WooCommerce path) now call this shared method, ensuring consistent behaviour. The WooCommerce built-in attribution signal (wc_source) is included as a signal on the WooCommerce path and correctly absent on the Elementor lead path.<\/li>\n<li>Fix: attribution confidence resolved by resolve_attribution_dataset() is now preserved through build_lead_snapshot() when meaningful attribution exists. Confidence is only overridden in the direct\/untracked branch.<\/li>\n<li>Fix: extracted a private get_known_referrer_map() method. Both resolve_source_from_referrer() and server_side_fallback() use this single map, eliminating the divergence between the two functions. Added missing platforms from resolve_source_from_referrer(): youtu.be, t.me, telegram.me, reddit, pinterest, snapchat, duckduckgo, baidu, yandex.<\/li>\n<li>Fix: server_side_fallback() now matches referrers against the parsed host only (not strpos() on the full URL). This prevents false positives where a domain like notgoogle.com could have matched the 'google.' pattern.<\/li>\n<li>Fix: removed the incorrect $ute_direct_count variable from leads-page.php (it was computed as total minus tracked, which double-counts both direct and untracked together). All percentage calculations and template references already used the correct $ute_direct_only_count and $ute_untracked_count from dedicated DB queries.<\/li>\n<li>Fix: lead deletion in leads-page.php now performs wp_safe_redirect() + exit after a successful delete. The ?action=delete&amp;lead_id= URL is no longer left in the browser's address bar. The success notice is displayed on the next page load via a short-lived user transient.<\/li>\n<\/ul>\n\n<h4>1.10.17<\/h4>\n\n<ul>\n<li>Fix: replaced single-line phpcs:ignore comments with phpcs:disable \/ phpcs:enable blocks around the two multiline $wpdb-&gt;get_var() \/ $wpdb-&gt;prepare() queries that span multiple lines. The previous inline ignore was placed on the SQL string line but PHPCS was flagging the get_var() and prepare() lines above it \u2014 the disable\/enable block now covers the entire multiline statement correctly.<\/li>\n<\/ul>\n\n<h4>1.10.16<\/h4>\n\n<ul>\n<li>Fix: added phpcs:ignore annotations to all dashboard query lines that reference $source_filter_sql. PHPCS static analysis cannot trace the variable origin and incorrectly flagged it as an unescaped or unprepared DB parameter. The value is derived entirely from hardcoded strings inside build_source_filter_clause() \u2014 no user input reaches the SQL fragment directly.<\/li>\n<li>Fix: updated \"Tested up to\" header in readme.txt from 6.9 to 7.0.<\/li>\n<\/ul>\n\n<h4>1.10.15<\/h4>\n\n<ul>\n<li>Feature: resizable columns on the Attribution Dashboard leads table. Column widths are persisted in localStorage and survive page refresh. A resize handle appears on each column header; drag left\/right to resize. Minimum column width: 60 px. Horizontal scroll is preserved. Sorting, filtering, bulk actions, pagination, CSV export, and column visibility are all unaffected. localStorage access is wrapped in try\/catch so the table remains fully usable when localStorage is unavailable (Private Browsing, restricted environments).<\/li>\n<li>Improvement: Form column in the leads table now displays plain text instead of a clickable filter link. Filtering by form is still available through the Form dropdown above the table.<\/li>\n<li>Feature: paid Meta source normalisation. When utm_medium indicates paid traffic (paid, cpc, paid_social, ppc, display), source aliases fb \/ facebook \/ ig \/ instagram are normalised to the canonical value \"meta\" before storage. Organic Facebook and Instagram traffic is intentionally NOT normalised \u2014 platform-level attribution is preserved for organic sources. Implemented as a single reusable helper normalize_paid_meta_source($source, $medium) called from the attribution processing layer, ensuring consistent output across DB storage, Elementor hidden fields, WooCommerce order meta, webhook payloads, and CSV export.<\/li>\n<li>Improvement: normalisation applied in both required places \u2014 at the end of resolve_attribution_dataset() covering utm_source, last_touch_source, and first_touch_source; and in build_lead_snapshot() when setting first_touch_source from the FC snapshot, using the first-click medium as the paid-intent signal.<\/li>\n<li>Improvement: Attribution Dashboard source filter is backward-compatible with pre-normalisation records. Filtering by \"meta\" returns both new canonical \"meta\" records and legacy paid records stored as fb \/ facebook \/ ig \/ instagram. Legacy paid aliases are merged under a single \"Meta (paid)\" entry in the source dropdown.<\/li>\n<li>Fix: PHPCS\/WPCS \u2014 replaced unreliable inline phpcs:ignore comments with a phpcs:disable \/ phpcs:enable block in enqueue_admin_assets() around the reads of $_GET['page'] and $_GET['post_type']. Added an explanatory comment confirming these are WordPress admin routing parameters, not user-submitted form data, and that nonce verification is not applicable.<\/li>\n<\/ul>\n\n<h4>1.10.14<\/h4>\n\n<ul>\n<li>Fix: corrected inline PHPCS suppression placement for WordPress.Security.NonceVerification.Recommended warnings.<\/li>\n<li>Improvement: moved <code>phpcs:ignore<\/code> comments directly to the relevant <code>$_GET<\/code> access lines so static analysis correctly recognises the intentional nonce verification flow.<\/li>\n<li>No functional plugin behavior changes.<\/li>\n<\/ul>\n\n<h4>1.10.13<\/h4>\n\n<ul>\n<li>Fix: CSV export now correctly sends a downloadable file instead of printing raw CSV text into the admin page.<\/li>\n<li>Fix: Export logic moved from the admin page callback (leads-page.php) to an <code>admin_init<\/code> hook in the main plugin class. This ensures HTTP response headers are set before WordPress outputs any HTML, which is the only reliable way to trigger a file download in the WordPress admin.<\/li>\n<li>Fix: Added <code>ob_end_clean()<\/code> loop before sending CSV headers to flush any output already buffered by WordPress or other plugins.<\/li>\n<li>Fix: UTF-8 BOM is now written via <code>fprintf()<\/code> instead of <code>echo<\/code>, avoiding encoding issues.<\/li>\n<li>Fix: <code>echo \"\u00ef\u00bb\u00bf\"<\/code> (a double-encoded BOM) replaced with the correct <code>\"\\xEF\\xBB\\xBF\"<\/code> BOM byte sequence.<\/li>\n<li>Improvement: leads-page.php now contains an early-return guard for export requests, preventing any HTML from leaking into the response if the admin_init handler is somehow bypassed.<\/li>\n<li>Fix: admin-export-orders.js was an empty IIFE stub \u2014 the WooCommerce orders Export CSV button was unresponsive on click. Added a delegated click handler that builds and submits a POST form to admin-post.php with the correct action and nonce values.<\/li>\n<\/ul>\n\n<h4>1.10.12<\/h4>\n\n<ul>\n<li>Fix: prevent stale \"untracked\" Elementor hidden-field values from conflicting with the final server-side attribution snapshot.<\/li>\n<li>Fix: ensure webhook \/ CRM \/ Google Sheets attribution data stays consistent with the Leads Dashboard resolver output.<\/li>\n<li>Improvement: hidden attribution fields may now replace weaker \"untracked\" values with resolved attribution data such as direct, google, facebook, whatsapp, etc.<\/li>\n<li>Improvement: \"untracked\" is now preserved only for real tracking failures or fully missing attribution signals.<\/li>\n<li>Added: should_overwrite_hidden_attribution_field() helper \u2014 centralises the overwrite decision for attribution field enrichment.<\/li>\n<\/ul>\n\n<h4>1.10.11<\/h4>\n\n<ul>\n<li>Feature: automatic best-effort enrichment of Elementor hidden attribution fields\nbefore all form actions run (Webhook, Email, CRM, Elementor Submissions, etc.).<\/li>\n<li>Enrichment is strictly limited to fields whose Elementor type is \"hidden\".\nVisible form fields are never touched under any condition.<\/li>\n<li>Two enrichment paths:\n\n<ul>\n<li>Known attribution field IDs (utm_source, utm_medium, utm_campaign, utm_content,\nutm_term, gclid, fbclid, referrer, landing_page, conversion_page,\nfirst_touch_source, last_touch_source, confidence, lc_source, lc_medium,\nlc_campaign, fc_source, attribution_source, attribution_medium) are filled\nfrom the central resolver (UTM \u2192 Click ID \u2192 Referrer \u2192 UA \u2192 First Touch \u2192\nDirect \u2192 Untracked).<\/li>\n<li>Custom hidden fields whose ID exactly matches a URL query parameter\n(e.g. utm_platform, camp_name, ad_id, adset_name) are filled from that\nparameter's raw value. No inference or mapping is applied to custom fields.<\/li>\n<\/ul><\/li>\n<li>Generic aliases (source, medium, campaign) removed from resolver-based enrichment\nto prevent conflicts with unrelated form fields.<\/li>\n<li>Existing field values are never overwritten \u2014 only empty recognised fields are enriched.<\/li>\n<li>Strictly non-blocking and fail-safe: if enrichment fails for any reason, form\nsubmission continues normally. No fatal errors, no stopped submissions, no changes\nto required field validation or user-facing fields.<\/li>\n<li>WP_DEBUG mode: logs a short, data-free diagnostic message if enrichment throws.<\/li>\n<\/ul>\n\n<h4>1.10.10<\/h4>\n\n<ul>\n<li>Fix: prevent direct traffic from being incorrectly classified as untracked.<\/li>\n<li>Fix: ensure last_touch_source stays consistent with resolved utm_source.\nPreviously, when utm_source resolved to \"direct\", last_touch_source was incorrectly\nset to \"untracked\" because is_meaningful_attribution_source() excludes \"direct\".\nlast_touch_source now correctly mirrors \"direct\" when that is the resolved state.<\/li>\n<li>Fix: improve webhook \/ Elementor hidden-field data consistency for unattributed traffic.<\/li>\n<li>Improvement: clearer separation between direct and untracked states across resolver,\nlead snapshot, and WooCommerce order meta.<\/li>\n<\/ul>\n\n<h4>1.10.9<\/h4>\n\n<ul>\n<li>Fixed: confidence is now explicit when both utm_source AND utm_medium are present in the URL,\neven when fbclid is also present. fbclid no longer downgrades confidence from explicit to inferred\nwhen full explicit UTM exists. Partial UTM + fbclid correctly stays inferred.<\/li>\n<li>Added: com.google.android.googlequicksearchbox referrer is now classified as google \/ organic.\nThis covers traffic from Android Google app surfaces (Google Discover, Search widget, Lens, Google app).\nPreviously this could fall through to an untracked or referral classification.<\/li>\n<\/ul>\n\n<h4>1.10.8<\/h4>\n\n<p>Explicit UTM \u2192 Click IDs \u2192 Referrer \u2192 User-Agent \u2192 First Touch \u2192 Direct \u2192 Untracked.\n* Improved: fbclid split logic \u2014 fbclid alone now resolves to facebook\/social (organic intent).\n  Only upgrades to facebook\/paid when utm_campaign, utm_content, utm_term, or dynamic ad variables are present.\n* Fixed: utm_medium=social (and any explicit utm_medium) is never overwritten by click ID inference.\n* Improved: direct vs untracked are now clearly separated. \"direct\" means tracking is functioning\n  but no attributable source was found. \"untracked\" is reserved for complete tracking failure\n  (all signals absent: no UTM, no click ID, no referrer, no UA match, no cookie, no first-touch).\n* Added: attribution confidence field (explicit \/ inferred \/ fallback \/ untracked) stored\n  in DB and order meta.\n* Fixed: server_side_fallback no longer returns \"untracked\" for missing\/internal referrers;\n  the caller now decides direct vs untracked based on full signal inventory.\n* Improved: WooCommerce orders column and meta box now distinguish direct vs untracked visually.\n* Improved: Dashboard UTM source filter now lists direct and untracked as separate options.\n* Updated: DB schema version 1.4.0 \u2014 adds confidence column to wp_ute_leads table.\n* Updated: utm-tracker.js \u2014 fbclid click ID split logic, confidence field in cookie snapshot,\n  internal navigation returns \"direct\" not \"untracked\".<\/p>\n\n<h4>1.10.7<\/h4>\n\n<ul>\n<li>Fixed: fbclid normalization no longer overrides an explicitly set utm_medium=social.\nPreviously, any visit with fbclid and utm_medium=social (e.g. an organic Facebook post\ntagged manually) would have its medium silently replaced with paid_social.\nNow only empty or \"none\" medium values are inferred as paid_social from fbclid presence.<\/li>\n<\/ul>\n\n<h4>1.10.6<\/h4>\n\n<ul>\n<li>Fixed remaining Plugin Check SQL issues (prepared statements compliance)<\/li>\n<li>Improved security of database queries.<\/li>\n<li>Cleaned uninstall logic for safer execution.<\/li>\n<\/ul>\n\n<h4>1.10.5<\/h4>\n\n<ul>\n<li>Removed remaining prebuilt SQL variables and inlined prepare() calls.<\/li>\n<li>Reworked admin queries to avoid interpolated SQL variable patterns flagged by review.<\/li>\n<li>Kept phone capture, dashboard display, and CSV export support intact.<\/li>\n<\/ul>\n\n<h4>1.10.4<\/h4>\n\n<ul>\n<li>Fixed admin SQL queries to use prepared statements consistently.<\/li>\n<li>Escaped interpolated table names in dashboard queries.<\/li>\n<li>Added missing translators comments and refreshed readme metadata.<\/li>\n<\/ul>\n\n<h4>1.10.3<\/h4>\n\n<ul>\n<li>Added plugin readme file for compliance.<\/li>\n<li>Improved static analysis compatibility for admin SQL and translations.<\/li>\n<li>Kept lead phone field support in DB, admin table, and CSV export.<\/li>\n<\/ul>\n\n<h4>1.10.2<\/h4>\n\n<ul>\n<li>Added lead phone field support.<\/li>\n<li>Added phone column to leads screen and CSV export.<\/li>\n<\/ul>","raw_excerpt":"Track UTM attribution for Elementor forms and WooCommerce with lead capture, phone storage, and first-touch \/ last-touch reporting.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/291281","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=291281"}],"author":[{"embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/ranelshulman"}],"wp:attachment":[{"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=291281"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=291281"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=291281"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=291281"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=291281"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/lin.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=291281"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}