@blotoutio/providers-google-analytics-4-sdk 1.53.0 → 1.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.cjs.js +174 -8
  2. package/index.js +174 -8
  3. package/index.mjs +174 -8
  4. package/package.json +1 -1
package/index.cjs.js CHANGED
@@ -57,6 +57,40 @@ const upsert = (map, key, update, createDefault) => {
57
57
  return map.set(key, update(currentValue));
58
58
  };
59
59
 
60
+ /**
61
+ * Known ad-network click ID query parameters and the human-readable provider
62
+ * label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
63
+ * and by the analytics API to classify paid-media touch sessions.
64
+ *
65
+ * Keep this list in sync with `defaultParams` in queryParams.ts when new
66
+ * click ID providers are added.
67
+ */
68
+ const CLICK_IDS = [
69
+ { param: 'fbclid', label: 'Facebook' }, // Facebook Ads
70
+ { param: 'gclid', label: 'Google' }, // Google Ads (click)
71
+ { param: 'gbraid', label: 'Google' }, // Google Ads (iOS app)
72
+ { param: 'wbraid', label: 'Google' }, // Google Ads (iOS web)
73
+ { param: 'msclkid', label: 'Bing' }, // Microsoft Bing Ads
74
+ { param: 'ttclid', label: 'TikTok' }, // TikTok Ads
75
+ { param: 'ScCid', label: 'Snapchat' }, // Snapchat Ads
76
+ { param: 'epik', label: 'Pinterest' }, // Pinterest Ads
77
+ { param: 'li_fat_id', label: 'LinkedIn' }, // LinkedIn Ads
78
+ { param: 'twclid', label: 'Twitter' }, // X (Twitter) Ads
79
+ { param: 'rdt_cid', label: 'Reddit' }, // Reddit Ads
80
+ { param: 'aleid', label: 'AppLovin' }, // AppLovin
81
+ { param: 'tabclid', label: 'Taboola' }, // Taboola
82
+ { param: 'obclid', label: 'Outbrain' }, // Outbrain
83
+ { param: 'trybe', label: 'Trybe' }, // Trybe
84
+ { param: '_kx', label: 'Klaviyo' }, // Klaviyo email campaigns
85
+ { param: 'mc_eid', label: 'Mailchimp' }, // Mailchimp email campaigns
86
+ { param: 'ttd_id', label: 'The Trade Desk' }, // The Trade Desk programmatic
87
+ { param: 'evsclid', label: 'EvoSearch' }, // EvoSearch
88
+ { param: 'li_did', label: 'Live Intent' }, // LiveIntent device-level ad
89
+ { param: '_raclid', label: 'Rumble' }, // Rumble Ads
90
+ { param: 'ref_id', label: 'StackAdapt' }, // StackAdapt programmatic
91
+ { param: 'duel_a', label: 'Duel' }, // Duel referral/advocacy
92
+ ];
93
+
60
94
  const isPrimitive = (value) => value === null ||
61
95
  typeof value === 'boolean' ||
62
96
  Number.isFinite(value) ||
@@ -459,6 +493,127 @@ new Set([...isoCountries.keys(), ...usStates.keys()]);
459
493
  */
460
494
  const jsonClone = (json) => tryParse(JSON.stringify(json), undefined);
461
495
 
496
+ /**
497
+ * Exact utm_source normalization (lowercase key → canonical name).
498
+ *
499
+ * Use exact match when the key is short, generic, or must not accidentally
500
+ * absorb variant spellings (e.g. "impact" must not match "impact_radius").
501
+ *
502
+ * Ordering: Paid (search/social → ad networks → affiliates) →
503
+ * Organic (AI/discovery) → Retention (email → SMS → push → post-purchase)
504
+ */
505
+ const UTM_SOURCE_EXACT = {
506
+ // ── Paid – Search & Social ────────────────────────────────────────────────
507
+ google: 'Google',
508
+ adwords: 'Google',
509
+ youtube: 'Google',
510
+ yt: 'Google',
511
+ meta: 'Facebook',
512
+ facebook: 'Facebook',
513
+ instagram: 'Facebook',
514
+ ig: 'Facebook',
515
+ igshopping: 'Facebook',
516
+ threads: 'Facebook',
517
+ twitter: 'Twitter',
518
+ snapchat: 'Snapchat',
519
+ pinterest: 'Pinterest',
520
+ bing: 'Bing',
521
+ microsoft: 'Bing',
522
+ tiktok: 'TikTok',
523
+ // ── Paid – Ad Networks ───────────────────────────────────────────────────
524
+ rtbhouse: 'RTB House',
525
+ applovin: 'AppLovin',
526
+ ttd: 'The Trade Desk',
527
+ amazondsp: 'Amazon DSP',
528
+ axon: 'Axon',
529
+ duel: 'Duel',
530
+ // ── Paid – Affiliates ────────────────────────────────────────────────────
531
+ awin: 'Awin',
532
+ 'affiliate-cj': 'CJ Affiliate',
533
+ impact: 'Impact',
534
+ rakuten: 'Rakuten',
535
+ superfiliate: 'Superfiliate',
536
+ // ── Organic – AI & Discovery ─────────────────────────────────────────────
537
+ perplexity: 'Perplexity',
538
+ chatgpt: 'ChatGPT',
539
+ 'chatgpt.com': 'ChatGPT',
540
+ openai: 'ChatGPT',
541
+ 'copilot.com': 'Microsoft Copilot',
542
+ copilot: 'Microsoft Copilot',
543
+ applenews: 'Apple News',
544
+ whatsapp: 'WhatsApp',
545
+ podcast: 'Podcast',
546
+ // ── Retention – Email ────────────────────────────────────────────────────
547
+ mailchimp: 'Mailchimp',
548
+ omnisend: 'Omnisend',
549
+ iterable: 'Iterable',
550
+ listrak: 'Listrak',
551
+ sailthru: 'Sailthru',
552
+ // ── Retention – Push ─────────────────────────────────────────────────────
553
+ pushowl: 'PushOwl',
554
+ // ── Retention – Post-purchase / Payment ──────────────────────────────────
555
+ narvar: 'Narvar',
556
+ shop_app: 'Shop App',
557
+ salesforce: 'Salesforce',
558
+ yotpo: 'Yotpo',
559
+ };
560
+ /**
561
+ * Partial utm_source normalization (startsWith prefix check, lowercase).
562
+ *
563
+ * Checked after exact match so short exact keys (e.g. "ig", "yt") win first.
564
+ */
565
+ const UTM_SOURCE_PARTIAL = [
566
+ // ── Paid – Social ─────────────────────────────────────────────────────────
567
+ { prefix: 'tiktok', name: 'TikTok' },
568
+ // ── Paid – Ad Networks ───────────────────────────────────────────────────
569
+ { prefix: 'criteo', name: 'Criteo' },
570
+ // ── Retention – Email ────────────────────────────────────────────────────
571
+ { prefix: 'klaviyo', name: 'Klaviyo' },
572
+ // ── Retention – SMS ──────────────────────────────────────────────────────
573
+ { prefix: 'attentive', name: 'Attentive' },
574
+ { prefix: 'postscript', name: 'Postscript' },
575
+ // ── Retention – Post-purchase / Payment ──────────────────────────────────
576
+ { prefix: 'afterpay', name: 'Afterpay' },
577
+ { prefix: 'klarna', name: 'Klarna' },
578
+ ];
579
+ const ORGANIC_SEARCH_ENGINES = [
580
+ { match: 'google', name: 'Google' },
581
+ { match: 'bing', name: 'Bing' },
582
+ { match: 'yahoo', name: 'Yahoo' },
583
+ { match: 'duckduckgo', name: 'DuckDuckGo' },
584
+ { match: 'baidu', name: 'Baidu' },
585
+ { match: 'yandex', name: 'Yandex' },
586
+ { match: 'brave', name: 'Brave' },
587
+ ];
588
+ const KNOWN_REFERRAL_PLATFORMS = [
589
+ { match: 'facebook', name: 'Facebook' },
590
+ { match: 'instagram', name: 'Facebook' },
591
+ { match: 'twitter', name: 'Twitter' },
592
+ { match: 'x.com', name: 'Twitter' },
593
+ { match: 'tiktok', name: 'TikTok' },
594
+ { match: 'pinterest', name: 'Pinterest' },
595
+ { match: 'linkedin', name: 'LinkedIn' },
596
+ { match: 'youtube', name: 'YouTube' },
597
+ { match: 'chatgpt', name: 'ChatGPT' },
598
+ { match: 'claude', name: 'Claude' },
599
+ { match: 'perplexity', name: 'Perplexity' },
600
+ { match: 'shop.app', name: 'Shop App' },
601
+ { match: 'amazon', name: 'Amazon' },
602
+ { match: 'attentive', name: 'Attentive' },
603
+ ];
604
+ const OFFLINE_TOUCH = 'Blotout_Offline';
605
+ new Set([
606
+ ...CLICK_IDS.map((c) => c.label),
607
+ ...Object.values(UTM_SOURCE_EXACT),
608
+ ...UTM_SOURCE_PARTIAL.map((e) => e.name),
609
+ ...ORGANIC_SEARCH_ENGINES.map((e) => `Organic Search - ${e.name}`),
610
+ ...KNOWN_REFERRAL_PLATFORMS.map((e) => `Referral - ${e.name}`),
611
+ 'Referral - Other',
612
+ 'Direct Traffic',
613
+ 'Other',
614
+ OFFLINE_TOUCH,
615
+ ]);
616
+
462
617
  // eslint-disable-next-line @nx/enforce-module-boundaries
463
618
  const getGoogleConsentFromCategories = (categories) => {
464
619
  const analyticsConsent = isCategoryConsented(categories, 'analytics');
@@ -514,7 +669,8 @@ const initGA4 = (ID, advancedConsentMode, consentSkip, consentData, executionCon
514
669
  script.parentNode.insertBefore(element, script);
515
670
  }
516
671
  };
517
- const init = ({ manifest, userId, executionContext, consentData, }) => {
672
+ const init = ({ manifest, userId, executionContext, consentData, pageUrl, }) => {
673
+ var _a;
518
674
  if (!window ||
519
675
  !manifest.variables ||
520
676
  !manifest.variables['measurementId'] ||
@@ -522,19 +678,29 @@ const init = ({ manifest, userId, executionContext, consentData, }) => {
522
678
  return;
523
679
  }
524
680
  window.dataLayer = window.dataLayer || [];
525
- window.gtag = function gtag() {
526
- // eslint-disable-next-line prefer-rest-params
527
- window.dataLayer.push(arguments);
528
- };
681
+ window.gtag =
682
+ window.gtag ||
683
+ function gtag() {
684
+ // eslint-disable-next-line prefer-rest-params
685
+ window.dataLayer.push(arguments);
686
+ };
529
687
  if (!window.google_tag_manager ||
530
688
  !window.google_tag_manager[manifest.variables['measurementId']]) {
531
689
  initGA4(manifest.variables['measurementId'], manifest.variables['advancedConsentMode'], manifest.variables['consentSkip'], consentData, executionContext);
532
690
  }
533
691
  if (window.gtag) {
534
- window.gtag('config', manifest.variables['measurementId'], {
692
+ const config = {
535
693
  user_id: userId,
536
694
  send_page_view: false,
537
- });
695
+ };
696
+ // Override gtag's `dl` param so enhanced-measurement events (scroll, click,
697
+ // etc.) auto-fired by gtag use the top-frame URL instead of the iframe
698
+ // sandbox's document.location.
699
+ const resolvedPageUrl = pageUrl || ((_a = window.location) === null || _a === void 0 ? void 0 : _a.href);
700
+ if (resolvedPageUrl) {
701
+ config['page_location'] = resolvedPageUrl;
702
+ }
703
+ window.gtag('config', manifest.variables['measurementId'], config);
538
704
  }
539
705
  };
540
706
 
@@ -806,7 +972,7 @@ const tag = ({ data, eventName, manifestVariables, eventId, pageUrl, }) => {
806
972
  }
807
973
  return {
808
974
  loaded: isLoaded,
809
- sdkVersion: "1.53.0" ,
975
+ sdkVersion: "1.54.0" ,
810
976
  };
811
977
  };
812
978
 
package/index.js CHANGED
@@ -58,6 +58,40 @@ var ProvidersGoogleAnalytics4Sdk = (function () {
58
58
  return map.set(key, update(currentValue));
59
59
  };
60
60
 
61
+ /**
62
+ * Known ad-network click ID query parameters and the human-readable provider
63
+ * label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
64
+ * and by the analytics API to classify paid-media touch sessions.
65
+ *
66
+ * Keep this list in sync with `defaultParams` in queryParams.ts when new
67
+ * click ID providers are added.
68
+ */
69
+ const CLICK_IDS = [
70
+ { param: 'fbclid', label: 'Facebook' }, // Facebook Ads
71
+ { param: 'gclid', label: 'Google' }, // Google Ads (click)
72
+ { param: 'gbraid', label: 'Google' }, // Google Ads (iOS app)
73
+ { param: 'wbraid', label: 'Google' }, // Google Ads (iOS web)
74
+ { param: 'msclkid', label: 'Bing' }, // Microsoft Bing Ads
75
+ { param: 'ttclid', label: 'TikTok' }, // TikTok Ads
76
+ { param: 'ScCid', label: 'Snapchat' }, // Snapchat Ads
77
+ { param: 'epik', label: 'Pinterest' }, // Pinterest Ads
78
+ { param: 'li_fat_id', label: 'LinkedIn' }, // LinkedIn Ads
79
+ { param: 'twclid', label: 'Twitter' }, // X (Twitter) Ads
80
+ { param: 'rdt_cid', label: 'Reddit' }, // Reddit Ads
81
+ { param: 'aleid', label: 'AppLovin' }, // AppLovin
82
+ { param: 'tabclid', label: 'Taboola' }, // Taboola
83
+ { param: 'obclid', label: 'Outbrain' }, // Outbrain
84
+ { param: 'trybe', label: 'Trybe' }, // Trybe
85
+ { param: '_kx', label: 'Klaviyo' }, // Klaviyo email campaigns
86
+ { param: 'mc_eid', label: 'Mailchimp' }, // Mailchimp email campaigns
87
+ { param: 'ttd_id', label: 'The Trade Desk' }, // The Trade Desk programmatic
88
+ { param: 'evsclid', label: 'EvoSearch' }, // EvoSearch
89
+ { param: 'li_did', label: 'Live Intent' }, // LiveIntent device-level ad
90
+ { param: '_raclid', label: 'Rumble' }, // Rumble Ads
91
+ { param: 'ref_id', label: 'StackAdapt' }, // StackAdapt programmatic
92
+ { param: 'duel_a', label: 'Duel' }, // Duel referral/advocacy
93
+ ];
94
+
61
95
  const isPrimitive = (value) => value === null ||
62
96
  typeof value === 'boolean' ||
63
97
  Number.isFinite(value) ||
@@ -460,6 +494,127 @@ var ProvidersGoogleAnalytics4Sdk = (function () {
460
494
  */
461
495
  const jsonClone = (json) => tryParse(JSON.stringify(json), undefined);
462
496
 
497
+ /**
498
+ * Exact utm_source normalization (lowercase key → canonical name).
499
+ *
500
+ * Use exact match when the key is short, generic, or must not accidentally
501
+ * absorb variant spellings (e.g. "impact" must not match "impact_radius").
502
+ *
503
+ * Ordering: Paid (search/social → ad networks → affiliates) →
504
+ * Organic (AI/discovery) → Retention (email → SMS → push → post-purchase)
505
+ */
506
+ const UTM_SOURCE_EXACT = {
507
+ // ── Paid – Search & Social ────────────────────────────────────────────────
508
+ google: 'Google',
509
+ adwords: 'Google',
510
+ youtube: 'Google',
511
+ yt: 'Google',
512
+ meta: 'Facebook',
513
+ facebook: 'Facebook',
514
+ instagram: 'Facebook',
515
+ ig: 'Facebook',
516
+ igshopping: 'Facebook',
517
+ threads: 'Facebook',
518
+ twitter: 'Twitter',
519
+ snapchat: 'Snapchat',
520
+ pinterest: 'Pinterest',
521
+ bing: 'Bing',
522
+ microsoft: 'Bing',
523
+ tiktok: 'TikTok',
524
+ // ── Paid – Ad Networks ───────────────────────────────────────────────────
525
+ rtbhouse: 'RTB House',
526
+ applovin: 'AppLovin',
527
+ ttd: 'The Trade Desk',
528
+ amazondsp: 'Amazon DSP',
529
+ axon: 'Axon',
530
+ duel: 'Duel',
531
+ // ── Paid – Affiliates ────────────────────────────────────────────────────
532
+ awin: 'Awin',
533
+ 'affiliate-cj': 'CJ Affiliate',
534
+ impact: 'Impact',
535
+ rakuten: 'Rakuten',
536
+ superfiliate: 'Superfiliate',
537
+ // ── Organic – AI & Discovery ─────────────────────────────────────────────
538
+ perplexity: 'Perplexity',
539
+ chatgpt: 'ChatGPT',
540
+ 'chatgpt.com': 'ChatGPT',
541
+ openai: 'ChatGPT',
542
+ 'copilot.com': 'Microsoft Copilot',
543
+ copilot: 'Microsoft Copilot',
544
+ applenews: 'Apple News',
545
+ whatsapp: 'WhatsApp',
546
+ podcast: 'Podcast',
547
+ // ── Retention – Email ────────────────────────────────────────────────────
548
+ mailchimp: 'Mailchimp',
549
+ omnisend: 'Omnisend',
550
+ iterable: 'Iterable',
551
+ listrak: 'Listrak',
552
+ sailthru: 'Sailthru',
553
+ // ── Retention – Push ─────────────────────────────────────────────────────
554
+ pushowl: 'PushOwl',
555
+ // ── Retention – Post-purchase / Payment ──────────────────────────────────
556
+ narvar: 'Narvar',
557
+ shop_app: 'Shop App',
558
+ salesforce: 'Salesforce',
559
+ yotpo: 'Yotpo',
560
+ };
561
+ /**
562
+ * Partial utm_source normalization (startsWith prefix check, lowercase).
563
+ *
564
+ * Checked after exact match so short exact keys (e.g. "ig", "yt") win first.
565
+ */
566
+ const UTM_SOURCE_PARTIAL = [
567
+ // ── Paid – Social ─────────────────────────────────────────────────────────
568
+ { prefix: 'tiktok', name: 'TikTok' },
569
+ // ── Paid – Ad Networks ───────────────────────────────────────────────────
570
+ { prefix: 'criteo', name: 'Criteo' },
571
+ // ── Retention – Email ────────────────────────────────────────────────────
572
+ { prefix: 'klaviyo', name: 'Klaviyo' },
573
+ // ── Retention – SMS ──────────────────────────────────────────────────────
574
+ { prefix: 'attentive', name: 'Attentive' },
575
+ { prefix: 'postscript', name: 'Postscript' },
576
+ // ── Retention – Post-purchase / Payment ──────────────────────────────────
577
+ { prefix: 'afterpay', name: 'Afterpay' },
578
+ { prefix: 'klarna', name: 'Klarna' },
579
+ ];
580
+ const ORGANIC_SEARCH_ENGINES = [
581
+ { match: 'google', name: 'Google' },
582
+ { match: 'bing', name: 'Bing' },
583
+ { match: 'yahoo', name: 'Yahoo' },
584
+ { match: 'duckduckgo', name: 'DuckDuckGo' },
585
+ { match: 'baidu', name: 'Baidu' },
586
+ { match: 'yandex', name: 'Yandex' },
587
+ { match: 'brave', name: 'Brave' },
588
+ ];
589
+ const KNOWN_REFERRAL_PLATFORMS = [
590
+ { match: 'facebook', name: 'Facebook' },
591
+ { match: 'instagram', name: 'Facebook' },
592
+ { match: 'twitter', name: 'Twitter' },
593
+ { match: 'x.com', name: 'Twitter' },
594
+ { match: 'tiktok', name: 'TikTok' },
595
+ { match: 'pinterest', name: 'Pinterest' },
596
+ { match: 'linkedin', name: 'LinkedIn' },
597
+ { match: 'youtube', name: 'YouTube' },
598
+ { match: 'chatgpt', name: 'ChatGPT' },
599
+ { match: 'claude', name: 'Claude' },
600
+ { match: 'perplexity', name: 'Perplexity' },
601
+ { match: 'shop.app', name: 'Shop App' },
602
+ { match: 'amazon', name: 'Amazon' },
603
+ { match: 'attentive', name: 'Attentive' },
604
+ ];
605
+ const OFFLINE_TOUCH = 'Blotout_Offline';
606
+ new Set([
607
+ ...CLICK_IDS.map((c) => c.label),
608
+ ...Object.values(UTM_SOURCE_EXACT),
609
+ ...UTM_SOURCE_PARTIAL.map((e) => e.name),
610
+ ...ORGANIC_SEARCH_ENGINES.map((e) => `Organic Search - ${e.name}`),
611
+ ...KNOWN_REFERRAL_PLATFORMS.map((e) => `Referral - ${e.name}`),
612
+ 'Referral - Other',
613
+ 'Direct Traffic',
614
+ 'Other',
615
+ OFFLINE_TOUCH,
616
+ ]);
617
+
463
618
  // eslint-disable-next-line @nx/enforce-module-boundaries
464
619
  const getGoogleConsentFromCategories = (categories) => {
465
620
  const analyticsConsent = isCategoryConsented(categories, 'analytics');
@@ -515,7 +670,8 @@ var ProvidersGoogleAnalytics4Sdk = (function () {
515
670
  script.parentNode.insertBefore(element, script);
516
671
  }
517
672
  };
518
- const init = ({ manifest, userId, executionContext, consentData, }) => {
673
+ const init = ({ manifest, userId, executionContext, consentData, pageUrl, }) => {
674
+ var _a;
519
675
  if (!window ||
520
676
  !manifest.variables ||
521
677
  !manifest.variables['measurementId'] ||
@@ -523,19 +679,29 @@ var ProvidersGoogleAnalytics4Sdk = (function () {
523
679
  return;
524
680
  }
525
681
  window.dataLayer = window.dataLayer || [];
526
- window.gtag = function gtag() {
527
- // eslint-disable-next-line prefer-rest-params
528
- window.dataLayer.push(arguments);
529
- };
682
+ window.gtag =
683
+ window.gtag ||
684
+ function gtag() {
685
+ // eslint-disable-next-line prefer-rest-params
686
+ window.dataLayer.push(arguments);
687
+ };
530
688
  if (!window.google_tag_manager ||
531
689
  !window.google_tag_manager[manifest.variables['measurementId']]) {
532
690
  initGA4(manifest.variables['measurementId'], manifest.variables['advancedConsentMode'], manifest.variables['consentSkip'], consentData, executionContext);
533
691
  }
534
692
  if (window.gtag) {
535
- window.gtag('config', manifest.variables['measurementId'], {
693
+ const config = {
536
694
  user_id: userId,
537
695
  send_page_view: false,
538
- });
696
+ };
697
+ // Override gtag's `dl` param so enhanced-measurement events (scroll, click,
698
+ // etc.) auto-fired by gtag use the top-frame URL instead of the iframe
699
+ // sandbox's document.location.
700
+ const resolvedPageUrl = pageUrl || ((_a = window.location) === null || _a === void 0 ? void 0 : _a.href);
701
+ if (resolvedPageUrl) {
702
+ config['page_location'] = resolvedPageUrl;
703
+ }
704
+ window.gtag('config', manifest.variables['measurementId'], config);
539
705
  }
540
706
  };
541
707
 
@@ -807,7 +973,7 @@ var ProvidersGoogleAnalytics4Sdk = (function () {
807
973
  }
808
974
  return {
809
975
  loaded: isLoaded,
810
- sdkVersion: "1.53.0" ,
976
+ sdkVersion: "1.54.0" ,
811
977
  };
812
978
  };
813
979
 
package/index.mjs CHANGED
@@ -55,6 +55,40 @@ const upsert = (map, key, update, createDefault) => {
55
55
  return map.set(key, update(currentValue));
56
56
  };
57
57
 
58
+ /**
59
+ * Known ad-network click ID query parameters and the human-readable provider
60
+ * label each one maps to. Used by the CDN worker to resolve `inSessionTouch`
61
+ * and by the analytics API to classify paid-media touch sessions.
62
+ *
63
+ * Keep this list in sync with `defaultParams` in queryParams.ts when new
64
+ * click ID providers are added.
65
+ */
66
+ const CLICK_IDS = [
67
+ { param: 'fbclid', label: 'Facebook' }, // Facebook Ads
68
+ { param: 'gclid', label: 'Google' }, // Google Ads (click)
69
+ { param: 'gbraid', label: 'Google' }, // Google Ads (iOS app)
70
+ { param: 'wbraid', label: 'Google' }, // Google Ads (iOS web)
71
+ { param: 'msclkid', label: 'Bing' }, // Microsoft Bing Ads
72
+ { param: 'ttclid', label: 'TikTok' }, // TikTok Ads
73
+ { param: 'ScCid', label: 'Snapchat' }, // Snapchat Ads
74
+ { param: 'epik', label: 'Pinterest' }, // Pinterest Ads
75
+ { param: 'li_fat_id', label: 'LinkedIn' }, // LinkedIn Ads
76
+ { param: 'twclid', label: 'Twitter' }, // X (Twitter) Ads
77
+ { param: 'rdt_cid', label: 'Reddit' }, // Reddit Ads
78
+ { param: 'aleid', label: 'AppLovin' }, // AppLovin
79
+ { param: 'tabclid', label: 'Taboola' }, // Taboola
80
+ { param: 'obclid', label: 'Outbrain' }, // Outbrain
81
+ { param: 'trybe', label: 'Trybe' }, // Trybe
82
+ { param: '_kx', label: 'Klaviyo' }, // Klaviyo email campaigns
83
+ { param: 'mc_eid', label: 'Mailchimp' }, // Mailchimp email campaigns
84
+ { param: 'ttd_id', label: 'The Trade Desk' }, // The Trade Desk programmatic
85
+ { param: 'evsclid', label: 'EvoSearch' }, // EvoSearch
86
+ { param: 'li_did', label: 'Live Intent' }, // LiveIntent device-level ad
87
+ { param: '_raclid', label: 'Rumble' }, // Rumble Ads
88
+ { param: 'ref_id', label: 'StackAdapt' }, // StackAdapt programmatic
89
+ { param: 'duel_a', label: 'Duel' }, // Duel referral/advocacy
90
+ ];
91
+
58
92
  const isPrimitive = (value) => value === null ||
59
93
  typeof value === 'boolean' ||
60
94
  Number.isFinite(value) ||
@@ -457,6 +491,127 @@ new Set([...isoCountries.keys(), ...usStates.keys()]);
457
491
  */
458
492
  const jsonClone = (json) => tryParse(JSON.stringify(json), undefined);
459
493
 
494
+ /**
495
+ * Exact utm_source normalization (lowercase key → canonical name).
496
+ *
497
+ * Use exact match when the key is short, generic, or must not accidentally
498
+ * absorb variant spellings (e.g. "impact" must not match "impact_radius").
499
+ *
500
+ * Ordering: Paid (search/social → ad networks → affiliates) →
501
+ * Organic (AI/discovery) → Retention (email → SMS → push → post-purchase)
502
+ */
503
+ const UTM_SOURCE_EXACT = {
504
+ // ── Paid – Search & Social ────────────────────────────────────────────────
505
+ google: 'Google',
506
+ adwords: 'Google',
507
+ youtube: 'Google',
508
+ yt: 'Google',
509
+ meta: 'Facebook',
510
+ facebook: 'Facebook',
511
+ instagram: 'Facebook',
512
+ ig: 'Facebook',
513
+ igshopping: 'Facebook',
514
+ threads: 'Facebook',
515
+ twitter: 'Twitter',
516
+ snapchat: 'Snapchat',
517
+ pinterest: 'Pinterest',
518
+ bing: 'Bing',
519
+ microsoft: 'Bing',
520
+ tiktok: 'TikTok',
521
+ // ── Paid – Ad Networks ───────────────────────────────────────────────────
522
+ rtbhouse: 'RTB House',
523
+ applovin: 'AppLovin',
524
+ ttd: 'The Trade Desk',
525
+ amazondsp: 'Amazon DSP',
526
+ axon: 'Axon',
527
+ duel: 'Duel',
528
+ // ── Paid – Affiliates ────────────────────────────────────────────────────
529
+ awin: 'Awin',
530
+ 'affiliate-cj': 'CJ Affiliate',
531
+ impact: 'Impact',
532
+ rakuten: 'Rakuten',
533
+ superfiliate: 'Superfiliate',
534
+ // ── Organic – AI & Discovery ─────────────────────────────────────────────
535
+ perplexity: 'Perplexity',
536
+ chatgpt: 'ChatGPT',
537
+ 'chatgpt.com': 'ChatGPT',
538
+ openai: 'ChatGPT',
539
+ 'copilot.com': 'Microsoft Copilot',
540
+ copilot: 'Microsoft Copilot',
541
+ applenews: 'Apple News',
542
+ whatsapp: 'WhatsApp',
543
+ podcast: 'Podcast',
544
+ // ── Retention – Email ────────────────────────────────────────────────────
545
+ mailchimp: 'Mailchimp',
546
+ omnisend: 'Omnisend',
547
+ iterable: 'Iterable',
548
+ listrak: 'Listrak',
549
+ sailthru: 'Sailthru',
550
+ // ── Retention – Push ─────────────────────────────────────────────────────
551
+ pushowl: 'PushOwl',
552
+ // ── Retention – Post-purchase / Payment ──────────────────────────────────
553
+ narvar: 'Narvar',
554
+ shop_app: 'Shop App',
555
+ salesforce: 'Salesforce',
556
+ yotpo: 'Yotpo',
557
+ };
558
+ /**
559
+ * Partial utm_source normalization (startsWith prefix check, lowercase).
560
+ *
561
+ * Checked after exact match so short exact keys (e.g. "ig", "yt") win first.
562
+ */
563
+ const UTM_SOURCE_PARTIAL = [
564
+ // ── Paid – Social ─────────────────────────────────────────────────────────
565
+ { prefix: 'tiktok', name: 'TikTok' },
566
+ // ── Paid – Ad Networks ───────────────────────────────────────────────────
567
+ { prefix: 'criteo', name: 'Criteo' },
568
+ // ── Retention – Email ────────────────────────────────────────────────────
569
+ { prefix: 'klaviyo', name: 'Klaviyo' },
570
+ // ── Retention – SMS ──────────────────────────────────────────────────────
571
+ { prefix: 'attentive', name: 'Attentive' },
572
+ { prefix: 'postscript', name: 'Postscript' },
573
+ // ── Retention – Post-purchase / Payment ──────────────────────────────────
574
+ { prefix: 'afterpay', name: 'Afterpay' },
575
+ { prefix: 'klarna', name: 'Klarna' },
576
+ ];
577
+ const ORGANIC_SEARCH_ENGINES = [
578
+ { match: 'google', name: 'Google' },
579
+ { match: 'bing', name: 'Bing' },
580
+ { match: 'yahoo', name: 'Yahoo' },
581
+ { match: 'duckduckgo', name: 'DuckDuckGo' },
582
+ { match: 'baidu', name: 'Baidu' },
583
+ { match: 'yandex', name: 'Yandex' },
584
+ { match: 'brave', name: 'Brave' },
585
+ ];
586
+ const KNOWN_REFERRAL_PLATFORMS = [
587
+ { match: 'facebook', name: 'Facebook' },
588
+ { match: 'instagram', name: 'Facebook' },
589
+ { match: 'twitter', name: 'Twitter' },
590
+ { match: 'x.com', name: 'Twitter' },
591
+ { match: 'tiktok', name: 'TikTok' },
592
+ { match: 'pinterest', name: 'Pinterest' },
593
+ { match: 'linkedin', name: 'LinkedIn' },
594
+ { match: 'youtube', name: 'YouTube' },
595
+ { match: 'chatgpt', name: 'ChatGPT' },
596
+ { match: 'claude', name: 'Claude' },
597
+ { match: 'perplexity', name: 'Perplexity' },
598
+ { match: 'shop.app', name: 'Shop App' },
599
+ { match: 'amazon', name: 'Amazon' },
600
+ { match: 'attentive', name: 'Attentive' },
601
+ ];
602
+ const OFFLINE_TOUCH = 'Blotout_Offline';
603
+ new Set([
604
+ ...CLICK_IDS.map((c) => c.label),
605
+ ...Object.values(UTM_SOURCE_EXACT),
606
+ ...UTM_SOURCE_PARTIAL.map((e) => e.name),
607
+ ...ORGANIC_SEARCH_ENGINES.map((e) => `Organic Search - ${e.name}`),
608
+ ...KNOWN_REFERRAL_PLATFORMS.map((e) => `Referral - ${e.name}`),
609
+ 'Referral - Other',
610
+ 'Direct Traffic',
611
+ 'Other',
612
+ OFFLINE_TOUCH,
613
+ ]);
614
+
460
615
  // eslint-disable-next-line @nx/enforce-module-boundaries
461
616
  const getGoogleConsentFromCategories = (categories) => {
462
617
  const analyticsConsent = isCategoryConsented(categories, 'analytics');
@@ -512,7 +667,8 @@ const initGA4 = (ID, advancedConsentMode, consentSkip, consentData, executionCon
512
667
  script.parentNode.insertBefore(element, script);
513
668
  }
514
669
  };
515
- const init = ({ manifest, userId, executionContext, consentData, }) => {
670
+ const init = ({ manifest, userId, executionContext, consentData, pageUrl, }) => {
671
+ var _a;
516
672
  if (!window ||
517
673
  !manifest.variables ||
518
674
  !manifest.variables['measurementId'] ||
@@ -520,19 +676,29 @@ const init = ({ manifest, userId, executionContext, consentData, }) => {
520
676
  return;
521
677
  }
522
678
  window.dataLayer = window.dataLayer || [];
523
- window.gtag = function gtag() {
524
- // eslint-disable-next-line prefer-rest-params
525
- window.dataLayer.push(arguments);
526
- };
679
+ window.gtag =
680
+ window.gtag ||
681
+ function gtag() {
682
+ // eslint-disable-next-line prefer-rest-params
683
+ window.dataLayer.push(arguments);
684
+ };
527
685
  if (!window.google_tag_manager ||
528
686
  !window.google_tag_manager[manifest.variables['measurementId']]) {
529
687
  initGA4(manifest.variables['measurementId'], manifest.variables['advancedConsentMode'], manifest.variables['consentSkip'], consentData, executionContext);
530
688
  }
531
689
  if (window.gtag) {
532
- window.gtag('config', manifest.variables['measurementId'], {
690
+ const config = {
533
691
  user_id: userId,
534
692
  send_page_view: false,
535
- });
693
+ };
694
+ // Override gtag's `dl` param so enhanced-measurement events (scroll, click,
695
+ // etc.) auto-fired by gtag use the top-frame URL instead of the iframe
696
+ // sandbox's document.location.
697
+ const resolvedPageUrl = pageUrl || ((_a = window.location) === null || _a === void 0 ? void 0 : _a.href);
698
+ if (resolvedPageUrl) {
699
+ config['page_location'] = resolvedPageUrl;
700
+ }
701
+ window.gtag('config', manifest.variables['measurementId'], config);
536
702
  }
537
703
  };
538
704
 
@@ -804,7 +970,7 @@ const tag = ({ data, eventName, manifestVariables, eventId, pageUrl, }) => {
804
970
  }
805
971
  return {
806
972
  loaded: isLoaded,
807
- sdkVersion: "1.53.0" ,
973
+ sdkVersion: "1.54.0" ,
808
974
  };
809
975
  };
810
976
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotoutio/providers-google-analytics-4-sdk",
3
- "version": "1.53.0",
3
+ "version": "1.54.0",
4
4
  "description": "Google Analytics 4 Browser SDK for EdgeTag",
5
5
  "author": "Blotout",
6
6
  "license": "MIT",