@tagadapay/headless-sdk 1.14.1 → 1.17.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.
- package/dist/core/client.d.ts +12 -0
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +16 -4
- package/dist/core/client.js.map +1 -1
- package/dist/core/http.d.ts +2 -0
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +21 -5
- package/dist/core/http.js.map +1 -1
- package/dist/core/index.d.ts +7 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/modules/checkout.d.ts +5 -1
- package/dist/core/modules/checkout.d.ts.map +1 -1
- package/dist/core/modules/checkout.js +22 -1
- package/dist/core/modules/checkout.js.map +1 -1
- package/dist/core/modules/commerce.d.ts +4 -0
- package/dist/core/modules/commerce.d.ts.map +1 -0
- package/dist/core/modules/commerce.js +5 -0
- package/dist/core/modules/commerce.js.map +1 -0
- package/dist/core/modules/customer.d.ts +47 -1
- package/dist/core/modules/customer.d.ts.map +1 -1
- package/dist/core/modules/customer.js +60 -3
- package/dist/core/modules/customer.js.map +1 -1
- package/dist/core/modules/experiments.d.ts +21 -0
- package/dist/core/modules/experiments.d.ts.map +1 -0
- package/dist/core/modules/experiments.js +85 -0
- package/dist/core/modules/experiments.js.map +1 -0
- package/dist/core/modules/funnel.d.ts +2 -1
- package/dist/core/modules/funnel.d.ts.map +1 -1
- package/dist/core/modules/funnel.js +6 -1
- package/dist/core/modules/funnel.js.map +1 -1
- package/dist/core/modules/lazyCommerce.d.ts +16 -0
- package/dist/core/modules/lazyCommerce.d.ts.map +1 -0
- package/dist/core/modules/lazyCommerce.js +20 -0
- package/dist/core/modules/lazyCommerce.js.map +1 -0
- package/dist/core/modules/offers.d.ts +30 -5
- package/dist/core/modules/offers.d.ts.map +1 -1
- package/dist/core/modules/offers.js +66 -7
- package/dist/core/modules/offers.js.map +1 -1
- package/dist/core/modules/payment.d.ts +19 -1
- package/dist/core/modules/payment.d.ts.map +1 -1
- package/dist/core/modules/payment.js +42 -3
- package/dist/core/modules/payment.js.map +1 -1
- package/dist/core/modules/portal/index.d.ts +102 -0
- package/dist/core/modules/portal/index.d.ts.map +1 -0
- package/dist/core/modules/portal/index.js +166 -0
- package/dist/core/modules/portal/index.js.map +1 -0
- package/dist/core/modules/portal/types.d.ts +617 -0
- package/dist/core/modules/portal/types.d.ts.map +1 -0
- package/dist/core/modules/portal/types.js +11 -0
- package/dist/core/modules/portal/types.js.map +1 -0
- package/dist/core/modules/rx.d.ts +32 -1
- package/dist/core/modules/rx.d.ts.map +1 -1
- package/dist/core/modules/rx.js +17 -1
- package/dist/core/modules/rx.js.map +1 -1
- package/dist/core/modules/track.d.ts +135 -0
- package/dist/core/modules/track.d.ts.map +1 -0
- package/dist/core/modules/track.js +412 -0
- package/dist/core/modules/track.js.map +1 -0
- package/dist/core/types.d.ts +13 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/version.d.ts +4 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +4 -0
- package/dist/core/version.js.map +1 -0
- package/dist/pixels/engine.d.ts +56 -0
- package/dist/pixels/engine.d.ts.map +1 -0
- package/dist/pixels/engine.js +263 -0
- package/dist/pixels/engine.js.map +1 -0
- package/dist/pixels/index.d.ts +10 -0
- package/dist/pixels/index.d.ts.map +1 -0
- package/dist/pixels/index.js +5 -0
- package/dist/pixels/index.js.map +1 -0
- package/dist/pixels/metaEventId.d.ts +15 -0
- package/dist/pixels/metaEventId.d.ts.map +1 -0
- package/dist/pixels/metaEventId.js +17 -0
- package/dist/pixels/metaEventId.js.map +1 -0
- package/dist/pixels/pixelIdentity.d.ts +34 -0
- package/dist/pixels/pixelIdentity.d.ts.map +1 -0
- package/dist/pixels/pixelIdentity.js +65 -0
- package/dist/pixels/pixelIdentity.js.map +1 -0
- package/dist/pixels/pixelMapping.d.ts +172 -0
- package/dist/pixels/pixelMapping.d.ts.map +1 -0
- package/dist/pixels/pixelMapping.js +574 -0
- package/dist/pixels/pixelMapping.js.map +1 -0
- package/dist/pixels/pixelTracker.d.ts +81 -0
- package/dist/pixels/pixelTracker.d.ts.map +1 -0
- package/dist/pixels/pixelTracker.js +797 -0
- package/dist/pixels/pixelTracker.js.map +1 -0
- package/dist/pixels/types.d.ts +103 -0
- package/dist/pixels/types.d.ts.map +1 -0
- package/dist/pixels/types.js +14 -0
- package/dist/pixels/types.js.map +1 -0
- package/dist/react/hooks/useStep.d.ts +7 -0
- package/dist/react/hooks/useStep.d.ts.map +1 -0
- package/dist/react/hooks/useStep.js +14 -0
- package/dist/react/hooks/useStep.js.map +1 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/stripe/index.d.ts +3 -1
- package/dist/stripe/index.d.ts.map +1 -1
- package/dist/stripe/index.js +13 -6
- package/dist/stripe/index.js.map +1 -1
- package/dist/stripe/useStripeExpress.d.ts.map +1 -1
- package/dist/stripe/useStripeExpress.js +15 -1
- package/dist/stripe/useStripeExpress.js.map +1 -1
- package/dist/tagada-headless-commerce.min.js +8 -0
- package/dist/tagada-headless-commerce.min.js.map +7 -0
- package/dist/tagada-headless-react.min.js +2 -2
- package/dist/tagada-headless-react.min.js.map +4 -4
- package/dist/tagada-headless.js +722 -288
- package/dist/tagada-headless.min.js +2 -2
- package/dist/tagada-headless.min.js.map +4 -4
- package/dist/tagada-pixels.min.js +7 -0
- package/dist/tagada-pixels.min.js.map +7 -0
- package/dist/tgd/contract/attribution.d.ts +28 -0
- package/dist/tgd/contract/attribution.d.ts.map +1 -0
- package/dist/tgd/contract/attribution.js +44 -0
- package/dist/tgd/contract/attribution.js.map +1 -0
- package/dist/tgd/contract/browser-api.d.ts +57 -0
- package/dist/tgd/contract/browser-api.d.ts.map +1 -0
- package/dist/tgd/contract/browser-api.js +2 -0
- package/dist/tgd/contract/browser-api.js.map +1 -0
- package/dist/tgd/contract/canvas.d.ts +120 -0
- package/dist/tgd/contract/canvas.d.ts.map +1 -0
- package/dist/tgd/contract/canvas.js +12 -0
- package/dist/tgd/contract/canvas.js.map +1 -0
- package/dist/tgd/contract/dedup-keys.d.ts +24 -0
- package/dist/tgd/contract/dedup-keys.d.ts.map +1 -0
- package/dist/tgd/contract/dedup-keys.js +23 -0
- package/dist/tgd/contract/dedup-keys.js.map +1 -0
- package/dist/tgd/contract/destination-names.d.ts +4 -0
- package/dist/tgd/contract/destination-names.d.ts.map +1 -0
- package/dist/tgd/contract/destination-names.js +66 -0
- package/dist/tgd/contract/destination-names.js.map +1 -0
- package/dist/tgd/contract/destinations.d.ts +124 -0
- package/dist/tgd/contract/destinations.d.ts.map +1 -0
- package/dist/tgd/contract/destinations.js +63 -0
- package/dist/tgd/contract/destinations.js.map +1 -0
- package/dist/tgd/contract/envelope.d.ts +203 -0
- package/dist/tgd/contract/envelope.d.ts.map +1 -0
- package/dist/tgd/contract/envelope.js +103 -0
- package/dist/tgd/contract/envelope.js.map +1 -0
- package/dist/tgd/contract/experiments.d.ts +87 -0
- package/dist/tgd/contract/experiments.d.ts.map +1 -0
- package/dist/tgd/contract/experiments.js +2 -0
- package/dist/tgd/contract/experiments.js.map +1 -0
- package/dist/tgd/contract/index.d.ts +20 -0
- package/dist/tgd/contract/index.d.ts.map +1 -0
- package/dist/tgd/contract/index.js +20 -0
- package/dist/tgd/contract/index.js.map +1 -0
- package/dist/tgd/contract/is-test.d.ts +2 -0
- package/dist/tgd/contract/is-test.d.ts.map +1 -0
- package/dist/tgd/contract/is-test.js +3 -0
- package/dist/tgd/contract/is-test.js.map +1 -0
- package/dist/tgd/contract/junk-ids.d.ts +4 -0
- package/dist/tgd/contract/junk-ids.d.ts.map +1 -0
- package/dist/tgd/contract/junk-ids.js +7 -0
- package/dist/tgd/contract/junk-ids.js.map +1 -0
- package/dist/tgd/contract/limits.d.ts +19 -0
- package/dist/tgd/contract/limits.d.ts.map +1 -0
- package/dist/tgd/contract/limits.js +20 -0
- package/dist/tgd/contract/limits.js.map +1 -0
- package/dist/tgd/contract/names.d.ts +13 -0
- package/dist/tgd/contract/names.d.ts.map +1 -0
- package/dist/tgd/contract/names.js +15 -0
- package/dist/tgd/contract/names.js.map +1 -0
- package/dist/tgd/contract/registry.d.ts +116 -0
- package/dist/tgd/contract/registry.d.ts.map +1 -0
- package/dist/tgd/contract/registry.js +59 -0
- package/dist/tgd/contract/registry.js.map +1 -0
- package/dist/tgd/contract/signed-ids.d.ts +33 -0
- package/dist/tgd/contract/signed-ids.d.ts.map +1 -0
- package/dist/tgd/contract/signed-ids.js +26 -0
- package/dist/tgd/contract/signed-ids.js.map +1 -0
- package/dist/tgd/contract/snapshots.d.ts +69 -0
- package/dist/tgd/contract/snapshots.d.ts.map +1 -0
- package/dist/tgd/contract/snapshots.js +2 -0
- package/dist/tgd/contract/snapshots.js.map +1 -0
- package/dist/tgd/contract/time.d.ts +9 -0
- package/dist/tgd/contract/time.d.ts.map +1 -0
- package/dist/tgd/contract/time.js +9 -0
- package/dist/tgd/contract/time.js.map +1 -0
- package/dist/tgd/contract/unions.d.ts +21 -0
- package/dist/tgd/contract/unions.d.ts.map +1 -0
- package/dist/tgd/contract/unions.js +17 -0
- package/dist/tgd/contract/unions.js.map +1 -0
- package/dist/tgd/contract/unit.d.ts +12 -0
- package/dist/tgd/contract/unit.d.ts.map +1 -0
- package/dist/tgd/contract/unit.js +18 -0
- package/dist/tgd/contract/unit.js.map +1 -0
- package/dist/tgd/contract/wire.d.ts +59 -0
- package/dist/tgd/contract/wire.d.ts.map +1 -0
- package/dist/tgd/contract/wire.js +2 -0
- package/dist/tgd/contract/wire.js.map +1 -0
- package/package.json +8 -1
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic pixel tracker.
|
|
3
|
+
*
|
|
4
|
+
* Init / fire logic for Meta, TikTok, Snapchat, Pinterest, GTM/GA4 and
|
|
5
|
+
* Microsoft Clarity pixels —
|
|
6
|
+
* extracted from `react/hooks/usePixelTracking.tsx` so non-React entry points
|
|
7
|
+
* (Studio islands hydrator, full-SPA runtime) can fire pixel events from
|
|
8
|
+
* `window.__TGD_STEP_CONFIG__.pixels`.
|
|
9
|
+
*
|
|
10
|
+
* The React provider is a thin adapter on top of this module.
|
|
11
|
+
*/
|
|
12
|
+
import { resolvePixelEvents, pixelIdKeyOf, GA4_PARAM_KEY, } from './pixelMapping.js';
|
|
13
|
+
import { buildHashedIdentity, canHashIdentity, } from './pixelIdentity.js';
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Duplicate guard (time-window based)
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function createDuplicateGuard(windowMs) {
|
|
18
|
+
const lastEvents = new Map();
|
|
19
|
+
return (eventName, parameters, eventId) => {
|
|
20
|
+
try {
|
|
21
|
+
// Include eventId in the dedup key so two distinct logical events with the
|
|
22
|
+
// same name+params (e.g. two Purchases) are not collapsed by the guard.
|
|
23
|
+
const key = JSON.stringify({ eventName, parameters, eventId });
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const last = lastEvents.get(key);
|
|
26
|
+
if (last && now - last < windowMs)
|
|
27
|
+
return false;
|
|
28
|
+
lastEvents.set(key, now);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Browser-specific: fire an event to the correct global pixel function
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
40
|
+
/**
|
|
41
|
+
* The entry's own identifier, read at whichever key its provider uses —
|
|
42
|
+
* `pixelId`, or `containerId` / `projectId` for GTM and Clarity.
|
|
43
|
+
*/
|
|
44
|
+
function pixelIdOf(pixel) {
|
|
45
|
+
if (!pixel)
|
|
46
|
+
return null;
|
|
47
|
+
const value = pixel[pixelIdKeyOf(pixel)];
|
|
48
|
+
return typeof value === 'string' && value ? value : null;
|
|
49
|
+
}
|
|
50
|
+
function fire(provider, name, params, pixel, options = {}) {
|
|
51
|
+
if (typeof window === 'undefined')
|
|
52
|
+
return;
|
|
53
|
+
const w = window;
|
|
54
|
+
const eventId = options.eventId;
|
|
55
|
+
switch (provider) {
|
|
56
|
+
case 'facebook': {
|
|
57
|
+
// Target this pixel by id rather than calling the global `fbq('track')`,
|
|
58
|
+
// which Meta broadcasts to EVERY pixel registered on the page — including
|
|
59
|
+
// ones a merchant initialised from a pasted script we don't manage. That
|
|
60
|
+
// broadcast made our Purchase land on foreign pixels, undeduplicated.
|
|
61
|
+
// Mirrors the per-pixel targeting already used for TikTok below.
|
|
62
|
+
//
|
|
63
|
+
// Meta dedup: pass eventID as the trailing `eventID` option so this
|
|
64
|
+
// browser-pixel event matches a server-side CAPI event with the same id.
|
|
65
|
+
// See https://developers.facebook.com/docs/marketing-api/conversions-api/deduplicate-pixel-and-server-events
|
|
66
|
+
// No id means no addressable pixel: stay silent rather than fall back to
|
|
67
|
+
// the broadcast this change exists to remove. `sanitizePixels` drops every
|
|
68
|
+
// entry without a usable id, so this is unreachable today.
|
|
69
|
+
const pixelId = pixelIdOf(pixel);
|
|
70
|
+
if (!pixelId)
|
|
71
|
+
break;
|
|
72
|
+
if (eventId)
|
|
73
|
+
w.fbq?.('trackSingle', pixelId, name, params, { eventID: eventId });
|
|
74
|
+
else
|
|
75
|
+
w.fbq?.('trackSingle', pixelId, name, params);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case 'tiktok': {
|
|
79
|
+
// Use ttq.instance(pixelId) to target each pixel individually,
|
|
80
|
+
// since the global ttq.track() only fires for the sdkid pixel.
|
|
81
|
+
const pixelId = pixelIdOf(pixel);
|
|
82
|
+
const target = pixelId && w.ttq?.instance ? w.ttq.instance(pixelId) : w.ttq;
|
|
83
|
+
if (name === 'Pageview') {
|
|
84
|
+
target?.page?.();
|
|
85
|
+
}
|
|
86
|
+
else if (eventId) {
|
|
87
|
+
target?.track?.(name, params, { event_id: eventId });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
target?.track?.(name, params);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'snapchat':
|
|
95
|
+
// Snap dedup: the browser event carries client_dedup_id = the event id that the
|
|
96
|
+
// server CAPI event sends as event_id (destinations.md §9.3).
|
|
97
|
+
w.snaptr?.('track', name, eventId ? { ...params, client_dedup_id: eventId } : params);
|
|
98
|
+
break;
|
|
99
|
+
case 'pinterest':
|
|
100
|
+
// Pinterest handles page views via pintrk('page'), not pintrk('track', 'pagevisit')
|
|
101
|
+
if (name === 'pagevisit') {
|
|
102
|
+
w.pintrk?.('page');
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
w.pintrk?.('track', name, eventId ? { ...params, event_id: eventId } : params);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
case 'gtm':
|
|
109
|
+
fireGTM(name, params);
|
|
110
|
+
break;
|
|
111
|
+
// Clarity's custom-event API takes a name and nothing else; params stay
|
|
112
|
+
// out rather than being stringified into the event name.
|
|
113
|
+
case 'clarity':
|
|
114
|
+
w.clarity?.('event', name);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Apply advanced matching to a single Snapchat pixel. Snap reads user info set
|
|
120
|
+
* at init and attaches it to every event fired afterwards; re-calling init with
|
|
121
|
+
* the (now-known) hashed identifiers updates it. We pass pre-hashed values via
|
|
122
|
+
* the `user_hashed_*` keys.
|
|
123
|
+
*/
|
|
124
|
+
function applySnapchatIdentity(w, pixelId, h) {
|
|
125
|
+
if (!w?.snaptr)
|
|
126
|
+
return;
|
|
127
|
+
const userInfo = {};
|
|
128
|
+
if (h.email)
|
|
129
|
+
userInfo.user_hashed_email = h.email;
|
|
130
|
+
if (h.phone)
|
|
131
|
+
userInfo.user_hashed_phone_number = h.phone;
|
|
132
|
+
if (Object.keys(userInfo).length === 0)
|
|
133
|
+
return;
|
|
134
|
+
w.snaptr('init', pixelId, userInfo);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Apply advanced matching to a single TikTok pixel via `ttq.identify`. TikTok
|
|
138
|
+
* applies identified data to subsequent events and accepts pre-hashed SHA-256
|
|
139
|
+
* values (it detects 64-char hex and won't double-hash). Target the specific
|
|
140
|
+
* pixel instance, mirroring the per-pixel track() targeting.
|
|
141
|
+
*/
|
|
142
|
+
function applyTikTokIdentity(w, pixelId, h) {
|
|
143
|
+
const target = w?.ttq?.instance ? w.ttq.instance(pixelId) : w?.ttq;
|
|
144
|
+
if (!target?.identify)
|
|
145
|
+
return;
|
|
146
|
+
const info = {};
|
|
147
|
+
if (h.email)
|
|
148
|
+
info.email = h.email;
|
|
149
|
+
if (h.phone)
|
|
150
|
+
info.phone_number = h.phone;
|
|
151
|
+
if (h.externalId)
|
|
152
|
+
info.external_id = h.externalId;
|
|
153
|
+
if (Object.keys(info).length === 0)
|
|
154
|
+
return;
|
|
155
|
+
target.identify(info);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Apply advanced matching to a single Meta pixel. Meta reads advanced-matching
|
|
159
|
+
* data passed to `fbq('init', pixelId, userData)` and attaches it to every
|
|
160
|
+
* event fired afterwards; re-calling init with the (now-known) identifiers
|
|
161
|
+
* updates it (init is idempotent and never auto-fires PageView). We pass RAW
|
|
162
|
+
* values — `fbevents.js` normalizes + SHA-256-hashes them in-browser before
|
|
163
|
+
* they leave the page — because Meta's phone normalization (digits-only) differs
|
|
164
|
+
* from Snap/TikTok's E.164, so letting Meta hash avoids a mismatch.
|
|
165
|
+
*/
|
|
166
|
+
function applyMetaIdentity(w, pixelId, raw) {
|
|
167
|
+
if (!w?.fbq)
|
|
168
|
+
return;
|
|
169
|
+
const ud = {};
|
|
170
|
+
if (raw.email)
|
|
171
|
+
ud.em = raw.email.trim().toLowerCase();
|
|
172
|
+
if (raw.phone) {
|
|
173
|
+
const d = raw.phone.replace(/[^0-9]/g, '');
|
|
174
|
+
if (d)
|
|
175
|
+
ud.ph = d;
|
|
176
|
+
}
|
|
177
|
+
if (raw.externalId)
|
|
178
|
+
ud.external_id = raw.externalId.trim();
|
|
179
|
+
if (Object.keys(ud).length === 0)
|
|
180
|
+
return;
|
|
181
|
+
w.fbq('init', pixelId, ud);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Google Ads enhanced conversions / GA4 user-provided data.
|
|
185
|
+
*
|
|
186
|
+
* Google's documented mechanism is `gtag('set', 'user_data', {...})` BEFORE the
|
|
187
|
+
* conversion event fires (support.google.com/google-ads/answer/13258081) —
|
|
188
|
+
* `user_data` as an event parameter is not read for enhanced conversions. We
|
|
189
|
+
* pass RAW values; the Google tag normalizes + SHA-256 hashes in-browser.
|
|
190
|
+
* Field names per spec: `email`, `phone_number` (strict E.164 — dropped when
|
|
191
|
+
* not in that shape, email is the preferred key anyway), and
|
|
192
|
+
* `address.{first_name,last_name,street,city,region,postal_code,country}`.
|
|
193
|
+
* Exported for unit tests.
|
|
194
|
+
*/
|
|
195
|
+
export function applyGoogleIdentity(w, ud) {
|
|
196
|
+
if (typeof w?.gtag !== 'function')
|
|
197
|
+
return;
|
|
198
|
+
const userData = {};
|
|
199
|
+
if (ud.email_address)
|
|
200
|
+
userData.email = ud.email_address.trim().toLowerCase();
|
|
201
|
+
if (ud.phone_number) {
|
|
202
|
+
const phone = ud.phone_number.replace(/[^0-9+]/g, '');
|
|
203
|
+
if (/^\+[0-9]{10,14}$/.test(phone))
|
|
204
|
+
userData.phone_number = phone;
|
|
205
|
+
}
|
|
206
|
+
if (ud.address) {
|
|
207
|
+
const a = ud.address;
|
|
208
|
+
const address = {};
|
|
209
|
+
if (a.first_name)
|
|
210
|
+
address.first_name = a.first_name;
|
|
211
|
+
if (a.last_name)
|
|
212
|
+
address.last_name = a.last_name;
|
|
213
|
+
if (a.street)
|
|
214
|
+
address.street = a.street;
|
|
215
|
+
if (a.city)
|
|
216
|
+
address.city = a.city;
|
|
217
|
+
if (a.region)
|
|
218
|
+
address.region = a.region;
|
|
219
|
+
if (a.postal_code)
|
|
220
|
+
address.postal_code = a.postal_code;
|
|
221
|
+
if (a.country)
|
|
222
|
+
address.country = a.country;
|
|
223
|
+
if (Object.keys(address).length > 0)
|
|
224
|
+
userData.address = address;
|
|
225
|
+
}
|
|
226
|
+
if (Object.keys(userData).length === 0)
|
|
227
|
+
return;
|
|
228
|
+
w.gtag('set', 'user_data', userData);
|
|
229
|
+
}
|
|
230
|
+
function fireGTM(event, params) {
|
|
231
|
+
try {
|
|
232
|
+
const w = window;
|
|
233
|
+
const hasEcommerce = params && typeof params === 'object' && 'ecommerce' in params;
|
|
234
|
+
if (w.gtag) {
|
|
235
|
+
// gtag.js (GA4 config tag) reads ecommerce fields at the top level of the
|
|
236
|
+
// event, not under a nested `ecommerce` key — flatten it back out.
|
|
237
|
+
if (hasEcommerce) {
|
|
238
|
+
const { ecommerce, ...rest } = params;
|
|
239
|
+
w.gtag('event', event, { ...rest, ...ecommerce });
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
w.gtag('event', event, params);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
else if (w.dataLayer) {
|
|
246
|
+
// GA4 best practice: clear the previous ecommerce object before pushing a
|
|
247
|
+
// new one so item arrays from earlier events don't bleed through.
|
|
248
|
+
if (hasEcommerce)
|
|
249
|
+
w.dataLayer.push({ ecommerce: null });
|
|
250
|
+
w.dataLayer.push({ event, ...params });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error('[SDK GTM] Error:', error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
258
|
+
// ===========================================================================
|
|
259
|
+
// Pixel initialization (browser-only)
|
|
260
|
+
// All pixel globals are accessed via `win` typed as `any` to avoid
|
|
261
|
+
// `declare global` augmentation issues across tsconfig scopes.
|
|
262
|
+
// ===========================================================================
|
|
263
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
264
|
+
const win = (typeof window !== 'undefined' ? window : undefined);
|
|
265
|
+
const SCRIPT_LOAD_TIMEOUT_MS = 5000;
|
|
266
|
+
function waitForScriptLoad(script) {
|
|
267
|
+
// If the script has already loaded, resolve immediately
|
|
268
|
+
if (script.dataset.loaded)
|
|
269
|
+
return Promise.resolve();
|
|
270
|
+
return new Promise((resolve) => {
|
|
271
|
+
const done = () => { script.dataset.loaded = '1'; clearTimeout(timer); resolve(); };
|
|
272
|
+
const timer = setTimeout(done, SCRIPT_LOAD_TIMEOUT_MS);
|
|
273
|
+
script.addEventListener('load', done, { once: true });
|
|
274
|
+
script.addEventListener('error', done, { once: true });
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
let _metaScriptEl = null;
|
|
278
|
+
function initMetaPixel(pixelId) {
|
|
279
|
+
if (!win)
|
|
280
|
+
return Promise.resolve();
|
|
281
|
+
// Initialize Meta base code once
|
|
282
|
+
if (!win.fbq) {
|
|
283
|
+
const n = function (...args) {
|
|
284
|
+
if (n.callMethod)
|
|
285
|
+
n.callMethod(...args);
|
|
286
|
+
else
|
|
287
|
+
n.queue.push(args);
|
|
288
|
+
};
|
|
289
|
+
n.queue = [];
|
|
290
|
+
n.loaded = true;
|
|
291
|
+
n.version = '2.0';
|
|
292
|
+
win.fbq = n;
|
|
293
|
+
if (!win._fbq)
|
|
294
|
+
win._fbq = n;
|
|
295
|
+
const t = document.createElement('script');
|
|
296
|
+
t.async = true;
|
|
297
|
+
t.src = 'https://connect.facebook.net/en_US/fbevents.js';
|
|
298
|
+
const s = document.getElementsByTagName('script')[0];
|
|
299
|
+
s?.parentNode?.insertBefore(t, s);
|
|
300
|
+
_metaScriptEl = t;
|
|
301
|
+
}
|
|
302
|
+
// Register each pixel ID (fbq supports multiple pixels via multiple init calls)
|
|
303
|
+
win.fbq('init', pixelId);
|
|
304
|
+
return _metaScriptEl ? waitForScriptLoad(_metaScriptEl) : Promise.resolve();
|
|
305
|
+
}
|
|
306
|
+
let _tiktokBaseInitialized = false;
|
|
307
|
+
function initTikTokPixel(pixelId) {
|
|
308
|
+
if (!win)
|
|
309
|
+
return Promise.resolve();
|
|
310
|
+
// Initialize TikTok base code once
|
|
311
|
+
if (!_tiktokBaseInitialized) {
|
|
312
|
+
_tiktokBaseInitialized = true;
|
|
313
|
+
win.TiktokAnalyticsObject = 'ttq';
|
|
314
|
+
const ttq = (win.ttq = win.ttq || []);
|
|
315
|
+
ttq.methods = [
|
|
316
|
+
'page', 'track', 'identify', 'instances', 'debug', 'on', 'off',
|
|
317
|
+
'once', 'ready', 'alias', 'group', 'enableCookie', 'disableCookie',
|
|
318
|
+
'holdConsent', 'revokeConsent', 'grantConsent',
|
|
319
|
+
];
|
|
320
|
+
ttq.setAndDefer = function (t, e) {
|
|
321
|
+
t[e] = function (...args) { t.push([e, ...args]); };
|
|
322
|
+
};
|
|
323
|
+
for (const method of ttq.methods) {
|
|
324
|
+
ttq.setAndDefer(ttq, method);
|
|
325
|
+
}
|
|
326
|
+
ttq.instance = function (t) {
|
|
327
|
+
const e = ttq._i[t] || [];
|
|
328
|
+
for (const method of ttq.methods) {
|
|
329
|
+
ttq.setAndDefer(e, method);
|
|
330
|
+
}
|
|
331
|
+
return e;
|
|
332
|
+
};
|
|
333
|
+
ttq.load = function (e, n) {
|
|
334
|
+
const r = 'https://analytics.tiktok.com/i18n/pixel/events.js';
|
|
335
|
+
ttq._i = ttq._i || {};
|
|
336
|
+
ttq._i[e] = [];
|
|
337
|
+
ttq._i[e]._u = r;
|
|
338
|
+
ttq._t = ttq._t || {};
|
|
339
|
+
ttq._t[e] = +new Date();
|
|
340
|
+
ttq._o = ttq._o || {};
|
|
341
|
+
ttq._o[e] = n || {};
|
|
342
|
+
const s = document.createElement('script');
|
|
343
|
+
s.type = 'text/javascript';
|
|
344
|
+
s.async = true;
|
|
345
|
+
s.src = r + '?sdkid=' + e + '&lib=ttq';
|
|
346
|
+
const p = document.getElementsByTagName('script')[0];
|
|
347
|
+
p?.parentNode?.insertBefore(s, p);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
// Skip if this specific pixel ID is already registered
|
|
351
|
+
if (win.ttq._i?.[pixelId])
|
|
352
|
+
return Promise.resolve();
|
|
353
|
+
// Register pixel and load its script
|
|
354
|
+
win.ttq.load(pixelId);
|
|
355
|
+
// Find the script we just created and wait for it
|
|
356
|
+
const scripts = document.querySelectorAll('script[src*="analytics.tiktok.com/i18n/pixel/events.js"]');
|
|
357
|
+
const lastScript = scripts[scripts.length - 1];
|
|
358
|
+
return lastScript ? waitForScriptLoad(lastScript) : Promise.resolve();
|
|
359
|
+
}
|
|
360
|
+
let _snapchatScriptEl = null;
|
|
361
|
+
function initSnapchatPixel(pixelId) {
|
|
362
|
+
if (!win)
|
|
363
|
+
return Promise.resolve();
|
|
364
|
+
// Initialize Snapchat base code once
|
|
365
|
+
if (!win.snaptr) {
|
|
366
|
+
const a = function (...args) {
|
|
367
|
+
if (a.handleRequest)
|
|
368
|
+
a.handleRequest(...args);
|
|
369
|
+
else
|
|
370
|
+
a.queue.push(args);
|
|
371
|
+
};
|
|
372
|
+
a.queue = [];
|
|
373
|
+
win.snaptr = a;
|
|
374
|
+
const r = document.createElement('script');
|
|
375
|
+
r.async = true;
|
|
376
|
+
r.src = 'https://sc-static.net/scevent.min.js';
|
|
377
|
+
const u = document.getElementsByTagName('script')[0];
|
|
378
|
+
u?.parentNode?.insertBefore(r, u);
|
|
379
|
+
_snapchatScriptEl = r;
|
|
380
|
+
}
|
|
381
|
+
// Register each pixel ID (snaptr supports multiple pixels via multiple init calls)
|
|
382
|
+
win.snaptr('init', pixelId);
|
|
383
|
+
return _snapchatScriptEl ? waitForScriptLoad(_snapchatScriptEl) : Promise.resolve();
|
|
384
|
+
}
|
|
385
|
+
let _pinterestScriptEl = null;
|
|
386
|
+
function initPinterestPixel(pixelId) {
|
|
387
|
+
if (!win)
|
|
388
|
+
return Promise.resolve();
|
|
389
|
+
// Initialize Pinterest base code once
|
|
390
|
+
if (!win.pintrk) {
|
|
391
|
+
const a = function (...args) { a.queue.push(args); };
|
|
392
|
+
a.queue = [];
|
|
393
|
+
win.pintrk = a;
|
|
394
|
+
const s = document.createElement('script');
|
|
395
|
+
s.async = true;
|
|
396
|
+
s.src = 'https://s.pinimg.com/ct/core.js';
|
|
397
|
+
const u = document.getElementsByTagName('script')[0];
|
|
398
|
+
u?.parentNode?.insertBefore(s, u);
|
|
399
|
+
_pinterestScriptEl = s;
|
|
400
|
+
}
|
|
401
|
+
// Register each pixel ID (pintrk supports multiple pixels via multiple load calls)
|
|
402
|
+
win.pintrk('load', pixelId);
|
|
403
|
+
// Note: pintrk('page') is NOT called here — the auto page-view fires via fire().
|
|
404
|
+
return _pinterestScriptEl ? waitForScriptLoad(_pinterestScriptEl) : Promise.resolve();
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Load the Microsoft Clarity tag for one project.
|
|
408
|
+
*
|
|
409
|
+
* Mirrors the vendor snippet: a queueing stub so `clarity(...)` calls made
|
|
410
|
+
* before the script lands are replayed, then the tag itself. Clarity ignores a
|
|
411
|
+
* second project on the same page, so a project already present is a no-op.
|
|
412
|
+
*/
|
|
413
|
+
function initClarity(projectId) {
|
|
414
|
+
if (!win || !projectId)
|
|
415
|
+
return Promise.resolve();
|
|
416
|
+
if (document.querySelector(`script[src*="clarity.ms/tag/${projectId}"]`))
|
|
417
|
+
return Promise.resolve();
|
|
418
|
+
if (!win.clarity) {
|
|
419
|
+
const q = function (...args) { q.q.push(args); };
|
|
420
|
+
q.q = [];
|
|
421
|
+
win.clarity = q;
|
|
422
|
+
}
|
|
423
|
+
const s = document.createElement('script');
|
|
424
|
+
s.async = true;
|
|
425
|
+
s.src = `https://www.clarity.ms/tag/${projectId}`;
|
|
426
|
+
const first = document.getElementsByTagName('script')[0];
|
|
427
|
+
if (first?.parentNode)
|
|
428
|
+
first.parentNode.insertBefore(s, first);
|
|
429
|
+
else
|
|
430
|
+
(document.head || document.getElementsByTagName('head')[0])?.appendChild(s);
|
|
431
|
+
return waitForScriptLoad(s);
|
|
432
|
+
}
|
|
433
|
+
function initGTM(containerId) {
|
|
434
|
+
if (!win || !containerId)
|
|
435
|
+
return Promise.resolve();
|
|
436
|
+
const isGtmContainer = containerId.startsWith('GTM-');
|
|
437
|
+
const scriptUrlPart = isGtmContainer
|
|
438
|
+
? `googletagmanager.com/gtm.js?id=${containerId}`
|
|
439
|
+
: `googletagmanager.com/gtag/js?id=${containerId}`;
|
|
440
|
+
if (document.querySelector(`script[src*="${scriptUrlPart}"]`))
|
|
441
|
+
return Promise.resolve();
|
|
442
|
+
win.dataLayer = win.dataLayer || [];
|
|
443
|
+
// Push referrer domain context into dataLayer before GTM loads, so tags
|
|
444
|
+
// inside the container can use it for cross-domain tracking configuration.
|
|
445
|
+
try {
|
|
446
|
+
const ref = document.referrer && new URL(document.referrer).hostname;
|
|
447
|
+
if (ref && ref !== window.location.hostname) {
|
|
448
|
+
win.dataLayer.push({ tagada_referrer_domain: ref });
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch { /* ignore invalid referrer */ }
|
|
452
|
+
if (isGtmContainer) {
|
|
453
|
+
win.dataLayer.push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
|
454
|
+
const f = document.getElementsByTagName('script')[0];
|
|
455
|
+
const j = document.createElement('script');
|
|
456
|
+
j.async = true;
|
|
457
|
+
j.src = 'https://www.googletagmanager.com/gtm.js?id=' + containerId;
|
|
458
|
+
if (f?.parentNode)
|
|
459
|
+
f.parentNode.insertBefore(j, f);
|
|
460
|
+
else
|
|
461
|
+
(document.head || document.getElementsByTagName('head')[0])?.appendChild(j);
|
|
462
|
+
const noscript = document.createElement('noscript');
|
|
463
|
+
const iframe = document.createElement('iframe');
|
|
464
|
+
iframe.src = `https://www.googletagmanager.com/ns.html?id=${containerId}`;
|
|
465
|
+
iframe.height = '0';
|
|
466
|
+
iframe.width = '0';
|
|
467
|
+
iframe.style.display = 'none';
|
|
468
|
+
iframe.style.visibility = 'hidden';
|
|
469
|
+
noscript.appendChild(iframe);
|
|
470
|
+
(document.body || document.getElementsByTagName('body')[0])?.insertBefore(noscript, document.body?.firstChild ?? null);
|
|
471
|
+
return waitForScriptLoad(j);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
if (!win.gtag) {
|
|
475
|
+
win.gtag = function (..._args) { win.dataLayer.push(arguments); };
|
|
476
|
+
}
|
|
477
|
+
win.gtag('js', new Date());
|
|
478
|
+
// Enable cross-domain tracking: accept incoming _gl parameter from referring
|
|
479
|
+
// domains (e.g. Shopify store → TagadaPay checkout) so Google preserves the
|
|
480
|
+
// client-id/session across the redirect.
|
|
481
|
+
const linkerConfig = { accept_incoming: true };
|
|
482
|
+
try {
|
|
483
|
+
const ref = document.referrer && new URL(document.referrer).hostname;
|
|
484
|
+
if (ref && ref !== window.location.hostname) {
|
|
485
|
+
linkerConfig.domains = [ref, window.location.hostname];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch { /* ignore invalid referrer */ }
|
|
489
|
+
win.gtag('config', containerId, { linker: linkerConfig });
|
|
490
|
+
const script = document.createElement('script');
|
|
491
|
+
script.async = true;
|
|
492
|
+
script.src = 'https://www.googletagmanager.com/gtag/js?id=' + containerId;
|
|
493
|
+
const firstScript = document.getElementsByTagName('script')[0];
|
|
494
|
+
if (firstScript?.parentNode)
|
|
495
|
+
firstScript.parentNode.insertBefore(script, firstScript);
|
|
496
|
+
else
|
|
497
|
+
(document.head || document.getElementsByTagName('head')[0])?.appendChild(script);
|
|
498
|
+
return waitForScriptLoad(script);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Tracker factory
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Config sanitisation (TAG-1871)
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
/** Longest ID we accept — real IDs top out around 20 chars. */
|
|
509
|
+
const MAX_PIXEL_ID_LENGTH = 64;
|
|
510
|
+
/**
|
|
511
|
+
* The only shape any supported pixel / container / project ID takes: Meta
|
|
512
|
+
* digits, TikTok alphanumerics, Snapchat alphanumerics + dashes, Pinterest
|
|
513
|
+
* digits, Clarity project ids and `GTM-`/`AW-`/`G-` containers are all drawn
|
|
514
|
+
* from `[A-Za-z0-9_-]`.
|
|
515
|
+
*/
|
|
516
|
+
const SAFE_PIXEL_ID_PATTERN = new RegExp(`^[A-Za-z0-9_-]{1,${MAX_PIXEL_ID_LENGTH}}$`);
|
|
517
|
+
/** True when `value` is a single, well-formed pixel / container ID. */
|
|
518
|
+
export function isSafePixelId(value) {
|
|
519
|
+
return typeof value === 'string' && SAFE_PIXEL_ID_PATTERN.test(value.trim());
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Trim every pixel/container ID and DROP the ones that aren't plausibly IDs.
|
|
523
|
+
*
|
|
524
|
+
* The trim keeps per-pixel event targeting (TikTok `ttq.instance`) working when
|
|
525
|
+
* the CRM stored stray whitespace. The drop is the load-bearing part: a
|
|
526
|
+
* merchant once pasted a whole TikTok `<script>` snippet into the Pixel ID
|
|
527
|
+
* field, the CRM split it on every comma into 56 "pixels", and handing those
|
|
528
|
+
* fragments to `ttq.load()` sent TikTok's SDK into a hot loop that pinned the
|
|
529
|
+
* renderer at 100% CPU *before first paint* — a blank checkout for days.
|
|
530
|
+
*
|
|
531
|
+
* The CRM now rejects such values at input, but this guard is deliberately
|
|
532
|
+
* independent: it neutralises configs that are ALREADY polluted, on pages that
|
|
533
|
+
* were baked before the fix, with no data migration and no re-publish.
|
|
534
|
+
*/
|
|
535
|
+
function sanitizePixels(pixels) {
|
|
536
|
+
const out = {};
|
|
537
|
+
const dropped = [];
|
|
538
|
+
for (const [provider, list] of Object.entries(pixels)) {
|
|
539
|
+
if (!Array.isArray(list))
|
|
540
|
+
continue;
|
|
541
|
+
const kept = [];
|
|
542
|
+
for (const px of list) {
|
|
543
|
+
// GTM keys off containerId, Clarity off projectId, everyone else pixelId.
|
|
544
|
+
const key = pixelIdKeyOf(px);
|
|
545
|
+
const raw = px?.[key];
|
|
546
|
+
if (!isSafePixelId(raw)) {
|
|
547
|
+
dropped.push(raw);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
kept.push({ ...px, [key]: raw.trim() });
|
|
551
|
+
}
|
|
552
|
+
out[provider] = kept;
|
|
553
|
+
}
|
|
554
|
+
if (dropped.length > 0) {
|
|
555
|
+
console.warn(`[SDK Pixels] Ignored ${dropped.length} malformed pixel ID(s) — tracking for those entries is disabled.`, dropped);
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Resolve once the browser has had a chance to paint.
|
|
561
|
+
*
|
|
562
|
+
* Third-party pixel SDKs run arbitrary vendor code on the main thread and can
|
|
563
|
+
* stall it (TikTok's did, indefinitely — TAG-1871). Injecting them before first
|
|
564
|
+
* paint turns any such stall into a blank page. Deferring past the first frame
|
|
565
|
+
* means the worst case degrades to "no tracking" instead of "no checkout".
|
|
566
|
+
*
|
|
567
|
+
* Double-rAF is the standard "after paint" signal. The timer is a fallback for
|
|
568
|
+
* backgrounded tabs (where rAF may never fire) and for non-browser environments
|
|
569
|
+
* that expose a `window` without `requestAnimationFrame`.
|
|
570
|
+
*/
|
|
571
|
+
const PAINT_FALLBACK_MS = 1500;
|
|
572
|
+
function afterFirstPaint() {
|
|
573
|
+
if (typeof window === 'undefined')
|
|
574
|
+
return Promise.resolve();
|
|
575
|
+
return new Promise((resolve) => {
|
|
576
|
+
let settled = false;
|
|
577
|
+
const finish = () => {
|
|
578
|
+
if (settled)
|
|
579
|
+
return;
|
|
580
|
+
settled = true;
|
|
581
|
+
clearTimeout(fallback);
|
|
582
|
+
resolve();
|
|
583
|
+
};
|
|
584
|
+
const raf = typeof win?.requestAnimationFrame === 'function'
|
|
585
|
+
? win.requestAnimationFrame.bind(win)
|
|
586
|
+
: undefined;
|
|
587
|
+
const fallback = setTimeout(finish, raf ? PAINT_FALLBACK_MS : 0);
|
|
588
|
+
if (raf)
|
|
589
|
+
raf(() => raf(finish));
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Create a pixel tracker for a given pixels config.
|
|
594
|
+
* Kicks off pixel script init once the first frame has painted, fires PageView
|
|
595
|
+
* when init resolves, and returns a `track()` function that no-ops until then.
|
|
596
|
+
*/
|
|
597
|
+
export function createPixelTracker(rawPixels, opts) {
|
|
598
|
+
const pixels = rawPixels ? sanitizePixels(rawPixels) : rawPixels;
|
|
599
|
+
const shouldTrackEvent = createDuplicateGuard(5000);
|
|
600
|
+
let pixelsInitialized = false;
|
|
601
|
+
const initPromise = (async () => {
|
|
602
|
+
if (!pixels || typeof window === 'undefined')
|
|
603
|
+
return;
|
|
604
|
+
// Never inject a third-party tag before the page has painted (TAG-1871).
|
|
605
|
+
await afterFirstPaint();
|
|
606
|
+
const loadPromises = [];
|
|
607
|
+
try {
|
|
608
|
+
pixels.facebook?.forEach((px) => { if (px.enabled && px.pixelId)
|
|
609
|
+
loadPromises.push(initMetaPixel(px.pixelId)); });
|
|
610
|
+
pixels.tiktok?.forEach((px) => { if (px.enabled && px.pixelId)
|
|
611
|
+
loadPromises.push(initTikTokPixel(px.pixelId)); });
|
|
612
|
+
pixels.snapchat?.forEach((px) => { if (px.enabled && px.pixelId)
|
|
613
|
+
loadPromises.push(initSnapchatPixel(px.pixelId)); });
|
|
614
|
+
pixels.pinterest?.forEach((px) => { if (px.enabled && px.pixelId)
|
|
615
|
+
loadPromises.push(initPinterestPixel(px.pixelId)); });
|
|
616
|
+
pixels.gtm?.forEach((px) => { if (px.enabled && px.containerId)
|
|
617
|
+
loadPromises.push(initGTM(px.containerId)); });
|
|
618
|
+
pixels.clarity?.forEach((px) => { if (px.enabled && px.projectId)
|
|
619
|
+
loadPromises.push(initClarity(px.projectId)); });
|
|
620
|
+
await Promise.all(loadPromises);
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
console.error('[SDK Pixels] Initialization error:', error);
|
|
624
|
+
}
|
|
625
|
+
pixelsInitialized = true;
|
|
626
|
+
})();
|
|
627
|
+
// Advanced matching: feed customer identity to Meta (fbq init, raw — Meta
|
|
628
|
+
// hashes), Snapchat (re-init, hashed) + TikTok (identify, hashed). Applied
|
|
629
|
+
// once per identity and cached, so repeated track()/identify() calls don't
|
|
630
|
+
// re-apply. Each provider persists the match keys onto every subsequent
|
|
631
|
+
// event it fires, so calling this as soon as the email is known lifts match
|
|
632
|
+
// quality on InitiateCheckout + Purchase alike. `_identityReady` is awaited
|
|
633
|
+
// before Snap/TikTok events fire so the hashed keys are attached in time.
|
|
634
|
+
let _identityKey = null;
|
|
635
|
+
let _identityReady = null;
|
|
636
|
+
const applyIdentity = (id) => {
|
|
637
|
+
if (!pixels)
|
|
638
|
+
return null;
|
|
639
|
+
if (!id.email && !id.phone && !id.externalId)
|
|
640
|
+
return null;
|
|
641
|
+
const key = `${id.email ?? ''}|${id.phone ?? ''}|${id.externalId ?? ''}`;
|
|
642
|
+
if (_identityKey === key && _identityReady)
|
|
643
|
+
return _identityReady;
|
|
644
|
+
_identityKey = key;
|
|
645
|
+
// Meta takes raw values and hashes in-browser — apply immediately, no need
|
|
646
|
+
// to await our own hashing (and it works even without SubtleCrypto).
|
|
647
|
+
pixels.facebook?.forEach((px) => {
|
|
648
|
+
if (px.enabled && px.pixelId)
|
|
649
|
+
applyMetaIdentity(win, px.pixelId, id);
|
|
650
|
+
});
|
|
651
|
+
// Google Ads enhanced conversions: gtag('set','user_data') persists onto
|
|
652
|
+
// every subsequent gtag event, including the send_to conversion. Raw values,
|
|
653
|
+
// Google hashes in-browser (same rationale as Meta).
|
|
654
|
+
if (pixels.gtm?.some((px) => px.enabled)) {
|
|
655
|
+
applyGoogleIdentity(win, { email_address: id.email, phone_number: id.phone });
|
|
656
|
+
}
|
|
657
|
+
// Snapchat + TikTok want pre-hashed SHA-256 values, which we compute
|
|
658
|
+
// ourselves so raw PII never leaves the page. Skips when no secure context.
|
|
659
|
+
if (!canHashIdentity())
|
|
660
|
+
return (_identityReady = Promise.resolve());
|
|
661
|
+
// Always resolves (errors are swallowed) so deferred event fires never hang.
|
|
662
|
+
_identityReady = (async () => {
|
|
663
|
+
try {
|
|
664
|
+
const hashed = await buildHashedIdentity(id);
|
|
665
|
+
pixels.snapchat?.forEach((px) => {
|
|
666
|
+
if (px.enabled && px.pixelId)
|
|
667
|
+
applySnapchatIdentity(win, px.pixelId, hashed);
|
|
668
|
+
});
|
|
669
|
+
pixels.tiktok?.forEach((px) => {
|
|
670
|
+
if (px.enabled && px.pixelId)
|
|
671
|
+
applyTikTokIdentity(win, px.pixelId, hashed);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
console.error('[SDK Pixels] identify error:', error);
|
|
676
|
+
}
|
|
677
|
+
})();
|
|
678
|
+
return _identityReady;
|
|
679
|
+
};
|
|
680
|
+
// Derive identity from the GA4 enrichment the thank-you page already attaches
|
|
681
|
+
// (`_ga4.user_data`), so Purchase events get advanced matching with no
|
|
682
|
+
// call-site changes. Returns the readiness promise, or null when absent.
|
|
683
|
+
const applyIdentityFromParams = (parameters) => {
|
|
684
|
+
const ga4 = parameters[GA4_PARAM_KEY];
|
|
685
|
+
const ud = ga4?.user_data;
|
|
686
|
+
if (!ud)
|
|
687
|
+
return null;
|
|
688
|
+
// Google enhanced conversions get the FULL user_data (incl. address) here,
|
|
689
|
+
// richer than the email/phone identity below — and this runs synchronously
|
|
690
|
+
// before the gtag('event') fires in track(), which is what Google requires.
|
|
691
|
+
// Bypasses the identity dedup on purpose: gtag('set') is idempotent and the
|
|
692
|
+
// address may not have been known at identify() time.
|
|
693
|
+
if (pixels?.gtm?.some((px) => px.enabled))
|
|
694
|
+
applyGoogleIdentity(win, ud);
|
|
695
|
+
return applyIdentity({
|
|
696
|
+
email: ud.email_address,
|
|
697
|
+
phone: ud.phone_number,
|
|
698
|
+
externalId: ga4?.user_id,
|
|
699
|
+
});
|
|
700
|
+
};
|
|
701
|
+
const track = (eventName, parameters = {}, options = {}) => {
|
|
702
|
+
if (!pixels || !pixelsInitialized)
|
|
703
|
+
return;
|
|
704
|
+
if (!shouldTrackEvent(eventName, parameters, options.eventId))
|
|
705
|
+
return;
|
|
706
|
+
try {
|
|
707
|
+
// Apply advanced matching before firing so Snap/TikTok carry the match
|
|
708
|
+
// keys on this event. Other providers fire immediately (unaffected).
|
|
709
|
+
const identityReady = applyIdentityFromParams(parameters);
|
|
710
|
+
const events = resolvePixelEvents(pixels, eventName, parameters);
|
|
711
|
+
// Deduplicate by provider: the Snapchat/Pinterest SDKs broadcast to all
|
|
712
|
+
// registered pixel IDs internally, so we only need to fire once per
|
|
713
|
+
// provider. GTM, TikTok and Meta need per-pixel firing (GTM for Google Ads
|
|
714
|
+
// send_to targeting, TikTok because ttq.instance() targets a specific
|
|
715
|
+
// pixel, Meta because fbq('trackSingle') does — relying on Meta's
|
|
716
|
+
// broadcast instead leaked our events onto pixels a pasted script had
|
|
717
|
+
// initialised).
|
|
718
|
+
const firedProviders = new Set();
|
|
719
|
+
// Per-pixel providers still need per-pixel dedup: a merchant field of
|
|
720
|
+
// "123;123" expands into two entries with the same id (splitPixelConfig),
|
|
721
|
+
// which would otherwise fire the same event twice at the same pixel.
|
|
722
|
+
const firedPixels = new Set();
|
|
723
|
+
for (const { provider, mapped, pixel } of events) {
|
|
724
|
+
// Gate Snap/TikTok on identity application (when present) so the match
|
|
725
|
+
// keys are set first; a few ms of deferral, only for those providers.
|
|
726
|
+
const emit = identityReady && (provider === 'snapchat' || provider === 'tiktok')
|
|
727
|
+
? (fn) => { identityReady.then(fn); }
|
|
728
|
+
: (fn) => { fn(); };
|
|
729
|
+
if (provider === 'gtm' || provider === 'tiktok' || provider === 'facebook') {
|
|
730
|
+
const pixelKey = `${provider}:${pixelIdOf(pixel) ?? ''}`;
|
|
731
|
+
if (firedPixels.has(pixelKey))
|
|
732
|
+
continue;
|
|
733
|
+
firedPixels.add(pixelKey);
|
|
734
|
+
emit(() => fire(provider, mapped.name, mapped.params, pixel, options));
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
if (!firedProviders.has(provider)) {
|
|
738
|
+
emit(() => fire(provider, mapped.name, mapped.params, undefined, options));
|
|
739
|
+
firedProviders.add(provider);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
console.error('[SDK Pixels] Tracking error:', error);
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
// Auto page-view: fire once after init resolves.
|
|
749
|
+
if (opts?.autoPageView !== false)
|
|
750
|
+
initPromise.then(() => {
|
|
751
|
+
if (typeof window === 'undefined')
|
|
752
|
+
return;
|
|
753
|
+
track('PageView', { path: window.location.pathname });
|
|
754
|
+
});
|
|
755
|
+
return {
|
|
756
|
+
initPromise,
|
|
757
|
+
get pixelsInitialized() { return pixelsInitialized; },
|
|
758
|
+
track,
|
|
759
|
+
identify: (identity) => { applyIdentity(identity); },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function readPixelsFromStepConfig() {
|
|
763
|
+
if (typeof window === 'undefined')
|
|
764
|
+
return undefined;
|
|
765
|
+
const raw = window.__TGD_STEP_CONFIG__;
|
|
766
|
+
if (!raw)
|
|
767
|
+
return undefined;
|
|
768
|
+
try {
|
|
769
|
+
const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
770
|
+
const pixels = parsed?.pixels;
|
|
771
|
+
return pixels && typeof pixels === 'object' ? pixels : undefined;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return undefined;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* One-shot bootstrap: reads `window.__TGD_STEP_CONFIG__.pixels`, creates a
|
|
779
|
+
* tracker, kicks off init, and stashes it on `window.__TGD_PIXEL_TRACKER__`.
|
|
780
|
+
*
|
|
781
|
+
* Idempotent — both the islands SDK and the full-SPA runtime may call this,
|
|
782
|
+
* but only the first call creates a tracker. Returns the active tracker
|
|
783
|
+
* (existing or newly created), or null if there's no window or no pixels.
|
|
784
|
+
*/
|
|
785
|
+
export function bootstrapPixelTrackerFromWindow() {
|
|
786
|
+
if (typeof window === 'undefined')
|
|
787
|
+
return null;
|
|
788
|
+
if (window.__TGD_PIXEL_TRACKER__)
|
|
789
|
+
return window.__TGD_PIXEL_TRACKER__;
|
|
790
|
+
const pixels = readPixelsFromStepConfig();
|
|
791
|
+
if (!pixels)
|
|
792
|
+
return null;
|
|
793
|
+
const tracker = createPixelTracker(pixels);
|
|
794
|
+
window.__TGD_PIXEL_TRACKER__ = tracker;
|
|
795
|
+
return tracker;
|
|
796
|
+
}
|
|
797
|
+
//# sourceMappingURL=pixelTracker.js.map
|