@fire-spark/analytics 0.0.4 → 0.1.2
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/index.d.ts +61 -10
- package/dist/index.js +963 -71
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/types.d.ts
|
|
2
|
-
declare const FUNNEL_EVENTS: readonly ["menu_viewed", "product_viewed", "
|
|
2
|
+
declare const FUNNEL_EVENTS: readonly ["menu_viewed", "product_viewed", "add_to_cart", "purchase"];
|
|
3
3
|
declare const INTERACTION_EVENTS: readonly ["product_visited", "category_visited"];
|
|
4
4
|
type FunnelEventName = (typeof FUNNEL_EVENTS)[number];
|
|
5
5
|
type InteractionEventName = (typeof INTERACTION_EVENTS)[number];
|
|
@@ -26,6 +26,35 @@ type AnalyticsScope = {
|
|
|
26
26
|
brand_id?: string;
|
|
27
27
|
fulfillment_id?: string;
|
|
28
28
|
};
|
|
29
|
+
type DestinationProviderId = "ga4" | "meta" | "masivo" | "tiktok";
|
|
30
|
+
type AnalyticsRuntime = "web" | "react-native";
|
|
31
|
+
type AnalyticsConsent = {
|
|
32
|
+
analytics: boolean;
|
|
33
|
+
ads: boolean;
|
|
34
|
+
};
|
|
35
|
+
type AnalyticsSampleOptions = {
|
|
36
|
+
firstPerEvent?: boolean;
|
|
37
|
+
rate?: number;
|
|
38
|
+
};
|
|
39
|
+
type NormalizedEvent = {
|
|
40
|
+
name: string;
|
|
41
|
+
params: Record<string, unknown>;
|
|
42
|
+
items?: Array<Record<string, unknown>>;
|
|
43
|
+
userProperties?: Record<string, unknown>;
|
|
44
|
+
context: Record<string, unknown>;
|
|
45
|
+
runtime: AnalyticsRuntime;
|
|
46
|
+
consent: AnalyticsConsent;
|
|
47
|
+
};
|
|
48
|
+
type AnalyticsAdapter = {
|
|
49
|
+
provider: DestinationProviderId;
|
|
50
|
+
send: (event: NormalizedEvent) => Promise<void> | void;
|
|
51
|
+
};
|
|
52
|
+
type TrackOptions = {
|
|
53
|
+
only?: DestinationProviderId[];
|
|
54
|
+
exclude?: DestinationProviderId[];
|
|
55
|
+
extra?: AnalyticsAdapter[];
|
|
56
|
+
variant?: string;
|
|
57
|
+
};
|
|
29
58
|
type MenuViewedEvent = {
|
|
30
59
|
name: "menu_viewed";
|
|
31
60
|
properties?: Record<string, unknown>;
|
|
@@ -36,15 +65,15 @@ type ProductViewedEvent = {
|
|
|
36
65
|
product_id: string;
|
|
37
66
|
};
|
|
38
67
|
};
|
|
39
|
-
type
|
|
40
|
-
name: "
|
|
68
|
+
type AddToCartEvent = {
|
|
69
|
+
name: "add_to_cart";
|
|
41
70
|
properties: {
|
|
42
71
|
product_id: string;
|
|
43
72
|
quantity: number;
|
|
44
73
|
};
|
|
45
74
|
};
|
|
46
|
-
type
|
|
47
|
-
name: "
|
|
75
|
+
type PurchaseEvent = {
|
|
76
|
+
name: "purchase";
|
|
48
77
|
properties: {
|
|
49
78
|
order_id: string;
|
|
50
79
|
total?: number;
|
|
@@ -65,7 +94,7 @@ type CategoryVisitedEvent = {
|
|
|
65
94
|
category_id: string;
|
|
66
95
|
};
|
|
67
96
|
};
|
|
68
|
-
type FunnelTrackEvent = MenuViewedEvent | ProductViewedEvent |
|
|
97
|
+
type FunnelTrackEvent = MenuViewedEvent | ProductViewedEvent | AddToCartEvent | PurchaseEvent;
|
|
69
98
|
type InteractionTrackEvent = ProductVisitedEvent | CategoryVisitedEvent;
|
|
70
99
|
type FreeTrackEvent = {
|
|
71
100
|
name: string;
|
|
@@ -82,6 +111,7 @@ type AnalyticsLimits = {
|
|
|
82
111
|
maxAgeMs?: number;
|
|
83
112
|
maxBytes?: number;
|
|
84
113
|
};
|
|
114
|
+
type AnalyticsMerchantContext = () => Record<string, unknown>;
|
|
85
115
|
type AnalyticsConfigureOptions = {
|
|
86
116
|
baseUrl?: string;
|
|
87
117
|
getAccessToken?: () => Promise<string>;
|
|
@@ -89,11 +119,20 @@ type AnalyticsConfigureOptions = {
|
|
|
89
119
|
getCustomer: () => AnalyticsCustomer | null;
|
|
90
120
|
getScope?: () => AnalyticsScope;
|
|
91
121
|
platform: AnalyticsPlatform;
|
|
92
|
-
|
|
122
|
+
channel_id: string;
|
|
123
|
+
context?: AnalyticsContext | AnalyticsMerchantContext;
|
|
124
|
+
merchantContext?: AnalyticsMerchantContext;
|
|
93
125
|
isOnline?: () => boolean | Promise<boolean>;
|
|
94
126
|
limits?: AnalyticsLimits;
|
|
95
127
|
fetchImpl?: typeof fetch;
|
|
96
128
|
sessionMs?: number;
|
|
129
|
+
currency?: string;
|
|
130
|
+
adapters?: AnalyticsAdapter[];
|
|
131
|
+
env?: "production" | "staging";
|
|
132
|
+
planVersionId?: string;
|
|
133
|
+
consent?: () => AnalyticsConsent;
|
|
134
|
+
sample?: AnalyticsSampleOptions;
|
|
135
|
+
runtime?: AnalyticsRuntime;
|
|
97
136
|
};
|
|
98
137
|
type AnalyticsConfig = {
|
|
99
138
|
session: {
|
|
@@ -110,15 +149,27 @@ declare const flush: () => Promise<void>;
|
|
|
110
149
|
//#endregion
|
|
111
150
|
//#region src/index.d.ts
|
|
112
151
|
declare const configure: (options: AnalyticsConfigureOptions) => Promise<void>;
|
|
113
|
-
|
|
152
|
+
type TrackName = (name: string, params?: Record<string, unknown>, opts?: TrackOptions) => Promise<void>;
|
|
153
|
+
type TrackAlias = (event: TrackEvent) => Promise<void>;
|
|
154
|
+
type TrackEither = (nameOrEvent: string | TrackEvent, params?: Record<string, unknown>, opts?: TrackOptions) => Promise<void>;
|
|
155
|
+
type TrackFn = TrackName & TrackAlias & TrackEither;
|
|
156
|
+
declare const track: TrackFn;
|
|
114
157
|
declare const endSession: () => Promise<void>;
|
|
115
158
|
declare const reset: () => Promise<void>;
|
|
159
|
+
declare class Analytics {
|
|
160
|
+
constructor(options?: AnalyticsConfigureOptions);
|
|
161
|
+
configure: typeof configure;
|
|
162
|
+
track: TrackFn;
|
|
163
|
+
flush: typeof flush;
|
|
164
|
+
endSession: typeof endSession;
|
|
165
|
+
reset: typeof reset;
|
|
166
|
+
}
|
|
116
167
|
declare const analytics: {
|
|
117
168
|
configure: typeof configure;
|
|
118
|
-
track:
|
|
169
|
+
track: TrackFn;
|
|
119
170
|
flush: typeof flush;
|
|
120
171
|
endSession: typeof endSession;
|
|
121
172
|
reset: typeof reset;
|
|
122
173
|
};
|
|
123
174
|
//#endregion
|
|
124
|
-
export { type AnalyticsConfig, type AnalyticsConfigureOptions, type AnalyticsContext, type AnalyticsCustomer, type AnalyticsPlatform, type AnalyticsScope, type AnalyticsStorage, FUNNEL_EVENTS, type FunnelEventName, INTERACTION_EVENTS, type InteractionEventName, type TrackEvent, analytics, configure, endSession, isFunnelEventName, isInteractionEventName, reset, track };
|
|
175
|
+
export { Analytics, type AnalyticsAdapter, type AnalyticsConfig, type AnalyticsConfigureOptions, type AnalyticsConsent, type AnalyticsContext, type AnalyticsCustomer, type AnalyticsPlatform, type AnalyticsRuntime, type AnalyticsSampleOptions, type AnalyticsScope, type AnalyticsStorage, type DestinationProviderId, FUNNEL_EVENTS, type FunnelEventName, INTERACTION_EVENTS, type InteractionEventName, type NormalizedEvent, type TrackOptions as TrackCallOptions, type TrackOptions, type TrackEvent, analytics, configure, endSession, flush, isFunnelEventName, isInteractionEventName, reset, track };
|
package/dist/index.js
CHANGED
|
@@ -32,36 +32,54 @@ const buildEventContext = (context) => {
|
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
34
|
//#endregion
|
|
35
|
-
//#region src/
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
//#region src/pii.ts
|
|
36
|
+
const PII_KEYS = /* @__PURE__ */ new Set([
|
|
37
|
+
"phone",
|
|
38
|
+
"device_ip",
|
|
39
|
+
"email",
|
|
40
|
+
"ttclid"
|
|
41
|
+
]);
|
|
42
|
+
const toHex = (bytes) => {
|
|
43
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
40
44
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
45
|
+
const isPiiParam = (name) => {
|
|
46
|
+
return PII_KEYS.has(name);
|
|
47
|
+
};
|
|
48
|
+
const hashPiiValue = async (value) => {
|
|
49
|
+
const subtle = globalThis.crypto?.subtle;
|
|
50
|
+
if (!subtle) throw new Error("SHA-256 unavailable");
|
|
51
|
+
const normalized = value.trim().toLowerCase();
|
|
52
|
+
const data = new TextEncoder().encode(normalized);
|
|
53
|
+
const digest = await subtle.digest("SHA-256", data);
|
|
54
|
+
return toHex(new Uint8Array(digest));
|
|
55
|
+
};
|
|
56
|
+
const hashDeep = async (value) => {
|
|
57
|
+
if (typeof value === "string") return hashPiiValue(value);
|
|
58
|
+
if (typeof value === "number" || typeof value === "boolean") return hashPiiValue(String(value));
|
|
59
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => hashDeep(item)));
|
|
60
|
+
if (!value || typeof value !== "object") return value;
|
|
61
|
+
return hashPiiParams(value);
|
|
62
|
+
};
|
|
63
|
+
const hashPiiParams = async (params) => {
|
|
64
|
+
const next = {};
|
|
65
|
+
for (const [key, value] of Object.entries(params)) {
|
|
66
|
+
if (!isPiiParam(key)) {
|
|
67
|
+
next[key] = value;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
next[key] = await hashDeep(value);
|
|
72
|
+
} catch {}
|
|
55
73
|
}
|
|
56
|
-
|
|
74
|
+
return next;
|
|
57
75
|
};
|
|
58
76
|
//#endregion
|
|
59
77
|
//#region src/types.ts
|
|
60
78
|
const FUNNEL_EVENTS = [
|
|
61
79
|
"menu_viewed",
|
|
62
80
|
"product_viewed",
|
|
63
|
-
"
|
|
64
|
-
"
|
|
81
|
+
"add_to_cart",
|
|
82
|
+
"purchase"
|
|
65
83
|
];
|
|
66
84
|
const INTERACTION_EVENTS = ["product_visited", "category_visited"];
|
|
67
85
|
const isFunnelEventName = (name) => {
|
|
@@ -139,17 +157,31 @@ const enqueueEvent = (events, event, now, limits) => {
|
|
|
139
157
|
const CONFIG_KEY = "firespark:analytics:config";
|
|
140
158
|
const DEFAULT_SESSION_MS = 1800 * 1e3;
|
|
141
159
|
const RETRY_MS = 6e4;
|
|
142
|
-
const
|
|
160
|
+
const r1 = {
|
|
143
161
|
options: null,
|
|
144
|
-
context: null
|
|
162
|
+
context: null
|
|
163
|
+
};
|
|
164
|
+
const r2 = {
|
|
145
165
|
sessionMs: DEFAULT_SESSION_MS,
|
|
146
|
-
session: null
|
|
166
|
+
session: null
|
|
167
|
+
};
|
|
168
|
+
const r3 = {
|
|
147
169
|
interactions: null,
|
|
148
|
-
queue: []
|
|
170
|
+
queue: []
|
|
171
|
+
};
|
|
172
|
+
const r4 = {
|
|
149
173
|
hydrated: false,
|
|
150
174
|
sessionTimer: null,
|
|
151
175
|
retryTimer: null
|
|
152
176
|
};
|
|
177
|
+
const r5 = { sampleSeen: null };
|
|
178
|
+
const runtime = {
|
|
179
|
+
...r1,
|
|
180
|
+
...r2,
|
|
181
|
+
...r3,
|
|
182
|
+
...r4,
|
|
183
|
+
...r5
|
|
184
|
+
};
|
|
153
185
|
const getLimits = () => {
|
|
154
186
|
const limits = runtime.options?.limits;
|
|
155
187
|
return {
|
|
@@ -212,6 +244,698 @@ const resetRuntime = () => {
|
|
|
212
244
|
runtime.interactions = null;
|
|
213
245
|
runtime.queue = [];
|
|
214
246
|
runtime.hydrated = false;
|
|
247
|
+
runtime.sampleSeen = null;
|
|
248
|
+
};
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region ../../packages/constants/src/tracking-plan-catalog/currency.ts
|
|
251
|
+
const ISO_4217_CODE = /^[A-Z]{3}$/;
|
|
252
|
+
const USD_AMOUNT_PARAM_SUFFIX = "_in_usd";
|
|
253
|
+
const isUsdAmountParam = (name) => {
|
|
254
|
+
return name.endsWith(USD_AMOUNT_PARAM_SUFFIX);
|
|
255
|
+
};
|
|
256
|
+
const dropUsdDeep = (value) => {
|
|
257
|
+
if (Array.isArray(value)) return value.map(dropUsdDeep);
|
|
258
|
+
if (!value || typeof value !== "object") return value;
|
|
259
|
+
const next = {};
|
|
260
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
261
|
+
if (isUsdAmountParam(key)) continue;
|
|
262
|
+
next[key] = dropUsdDeep(nested);
|
|
263
|
+
}
|
|
264
|
+
return next;
|
|
265
|
+
};
|
|
266
|
+
const dropUsdAmountParams = (params) => {
|
|
267
|
+
return dropUsdDeep(params);
|
|
268
|
+
};
|
|
269
|
+
/** ISO 4217 from `merchants.currency_id` → `currencies.code`. */
|
|
270
|
+
const merchantCurrencyCode = (code) => {
|
|
271
|
+
const normalized = code.trim().toUpperCase();
|
|
272
|
+
if (!ISO_4217_CODE.test(normalized)) throw new Error("Merchant currency must be an ISO 4217 code");
|
|
273
|
+
return normalized;
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region ../../packages/constants/src/tracking-plan-catalog/params.ts
|
|
277
|
+
const valueParam = (required = true) => {
|
|
278
|
+
return {
|
|
279
|
+
name: "value",
|
|
280
|
+
type: "currency",
|
|
281
|
+
required,
|
|
282
|
+
scope: "ecommerce"
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
const priceParam = (required = false) => {
|
|
286
|
+
return {
|
|
287
|
+
name: "price",
|
|
288
|
+
type: "currency",
|
|
289
|
+
required,
|
|
290
|
+
scope: "item"
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
const quantityParam = (required = false) => {
|
|
294
|
+
return {
|
|
295
|
+
name: "quantity",
|
|
296
|
+
type: "integer",
|
|
297
|
+
required,
|
|
298
|
+
scope: "item"
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
const itemsParam = () => {
|
|
302
|
+
return {
|
|
303
|
+
name: "items",
|
|
304
|
+
type: "array<object>",
|
|
305
|
+
required: false,
|
|
306
|
+
scope: "ecommerce"
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
const transactionIdParam = () => {
|
|
310
|
+
return {
|
|
311
|
+
name: "transaction_id",
|
|
312
|
+
type: "string",
|
|
313
|
+
required: false,
|
|
314
|
+
scope: "event"
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region ../../packages/constants/src/tracking-plan-catalog/events.ts
|
|
319
|
+
const both = (name) => {
|
|
320
|
+
return {
|
|
321
|
+
web: name,
|
|
322
|
+
reactNative: name
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
const none = () => {
|
|
326
|
+
return {
|
|
327
|
+
web: null,
|
|
328
|
+
reactNative: null
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
const meta = (web, reactNative) => {
|
|
332
|
+
return {
|
|
333
|
+
web,
|
|
334
|
+
reactNative
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
const ecommerceParams = (valueRequired = true) => {
|
|
338
|
+
return [
|
|
339
|
+
valueParam(valueRequired),
|
|
340
|
+
itemsParam(),
|
|
341
|
+
transactionIdParam()
|
|
342
|
+
];
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Native events only. Custom events are merchant-authored and never shipped
|
|
346
|
+
* as catalog suggestions or plan defaults.
|
|
347
|
+
*/
|
|
348
|
+
const CATALOG_EVENTS = [
|
|
349
|
+
{
|
|
350
|
+
id: "add_to_cart",
|
|
351
|
+
name: "add_to_cart",
|
|
352
|
+
kind: "native",
|
|
353
|
+
category: "E-commerce",
|
|
354
|
+
runtimes: ["web", "react-native"],
|
|
355
|
+
recommendedParams: [
|
|
356
|
+
{
|
|
357
|
+
name: "product_id",
|
|
358
|
+
type: "string",
|
|
359
|
+
required: true,
|
|
360
|
+
scope: "event"
|
|
361
|
+
},
|
|
362
|
+
quantityParam(true),
|
|
363
|
+
...ecommerceParams(false)
|
|
364
|
+
],
|
|
365
|
+
destinations: {
|
|
366
|
+
"fire-spark": both("add_to_cart"),
|
|
367
|
+
ga4: both("add_to_cart"),
|
|
368
|
+
meta: meta("AddToCart", "fb_mobile_add_to_cart"),
|
|
369
|
+
tiktok: both("AddToCart"),
|
|
370
|
+
masivo: both("ADD_TO_CART")
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: "begin_checkout",
|
|
375
|
+
name: "begin_checkout",
|
|
376
|
+
kind: "native",
|
|
377
|
+
category: "E-commerce",
|
|
378
|
+
runtimes: ["web", "react-native"],
|
|
379
|
+
recommendedParams: ecommerceParams(),
|
|
380
|
+
destinations: {
|
|
381
|
+
"fire-spark": both("begin_checkout"),
|
|
382
|
+
ga4: both("begin_checkout"),
|
|
383
|
+
meta: meta("InitiateCheckout", "fb_mobile_initiated_checkout"),
|
|
384
|
+
tiktok: both("InitiateCheckout"),
|
|
385
|
+
masivo: both("begin_checkout")
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: "purchase",
|
|
390
|
+
name: "purchase",
|
|
391
|
+
kind: "native",
|
|
392
|
+
category: "E-commerce",
|
|
393
|
+
runtimes: ["web", "react-native"],
|
|
394
|
+
recommendedParams: [{
|
|
395
|
+
name: "order_id",
|
|
396
|
+
type: "string",
|
|
397
|
+
required: true,
|
|
398
|
+
scope: "event"
|
|
399
|
+
}, ...ecommerceParams(false)],
|
|
400
|
+
destinations: {
|
|
401
|
+
"fire-spark": both("purchase"),
|
|
402
|
+
ga4: both("purchase"),
|
|
403
|
+
meta: meta("Purchase", "fb_mobile_purchase"),
|
|
404
|
+
tiktok: both("Purchase"),
|
|
405
|
+
masivo: both("PURCHASE")
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: "view_item",
|
|
410
|
+
name: "view_item",
|
|
411
|
+
kind: "native",
|
|
412
|
+
category: "E-commerce",
|
|
413
|
+
runtimes: ["web", "react-native"],
|
|
414
|
+
recommendedParams: [
|
|
415
|
+
valueParam(false),
|
|
416
|
+
itemsParam(),
|
|
417
|
+
priceParam(),
|
|
418
|
+
quantityParam()
|
|
419
|
+
],
|
|
420
|
+
destinations: {
|
|
421
|
+
"fire-spark": both("product_viewed"),
|
|
422
|
+
ga4: both("view_item"),
|
|
423
|
+
meta: meta("ViewContent", "fb_mobile_content_view"),
|
|
424
|
+
tiktok: both("ViewContent"),
|
|
425
|
+
masivo: both("view_item")
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
id: "sign_up",
|
|
430
|
+
name: "sign_up",
|
|
431
|
+
kind: "native",
|
|
432
|
+
category: "Account",
|
|
433
|
+
runtimes: ["web", "react-native"],
|
|
434
|
+
recommendedParams: [{
|
|
435
|
+
name: "method",
|
|
436
|
+
type: "string",
|
|
437
|
+
required: false,
|
|
438
|
+
scope: "event"
|
|
439
|
+
}],
|
|
440
|
+
destinations: {
|
|
441
|
+
"fire-spark": both("sign_up"),
|
|
442
|
+
ga4: both("sign_up"),
|
|
443
|
+
meta: meta("CompleteRegistration", "fb_mobile_complete_registration"),
|
|
444
|
+
tiktok: both("Registration"),
|
|
445
|
+
masivo: both("sign_up")
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
id: "login",
|
|
450
|
+
name: "login",
|
|
451
|
+
kind: "native",
|
|
452
|
+
category: "Account",
|
|
453
|
+
runtimes: ["web", "react-native"],
|
|
454
|
+
recommendedParams: [{
|
|
455
|
+
name: "method",
|
|
456
|
+
type: "string",
|
|
457
|
+
required: false,
|
|
458
|
+
scope: "event"
|
|
459
|
+
}],
|
|
460
|
+
destinations: {
|
|
461
|
+
"fire-spark": both("login"),
|
|
462
|
+
ga4: both("login"),
|
|
463
|
+
meta: both("login"),
|
|
464
|
+
tiktok: both("Login"),
|
|
465
|
+
masivo: both("login")
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
id: "page_view",
|
|
470
|
+
name: "page_view",
|
|
471
|
+
kind: "native",
|
|
472
|
+
category: "Navegación",
|
|
473
|
+
runtimes: ["web"],
|
|
474
|
+
recommendedParams: [],
|
|
475
|
+
destinations: {
|
|
476
|
+
"fire-spark": both("page_view"),
|
|
477
|
+
ga4: both("page_view"),
|
|
478
|
+
meta: none(),
|
|
479
|
+
tiktok: none(),
|
|
480
|
+
masivo: none()
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
id: "screen_view",
|
|
485
|
+
name: "screen_view",
|
|
486
|
+
kind: "native",
|
|
487
|
+
category: "Navegación",
|
|
488
|
+
runtimes: ["react-native"],
|
|
489
|
+
recommendedParams: [],
|
|
490
|
+
destinations: {
|
|
491
|
+
"fire-spark": both("screen_view"),
|
|
492
|
+
ga4: both("screen_view"),
|
|
493
|
+
meta: none(),
|
|
494
|
+
tiktok: none(),
|
|
495
|
+
masivo: none()
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: "menu_viewed",
|
|
500
|
+
name: "menu_viewed",
|
|
501
|
+
kind: "native",
|
|
502
|
+
category: "Navegación",
|
|
503
|
+
runtimes: ["web", "react-native"],
|
|
504
|
+
recommendedParams: [],
|
|
505
|
+
destinations: {
|
|
506
|
+
"fire-spark": both("menu_viewed"),
|
|
507
|
+
ga4: both("menu_viewed"),
|
|
508
|
+
meta: both("menu_viewed"),
|
|
509
|
+
tiktok: both("menu_viewed"),
|
|
510
|
+
masivo: both("menu_viewed")
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "product_viewed",
|
|
515
|
+
name: "product_viewed",
|
|
516
|
+
kind: "native",
|
|
517
|
+
category: "E-commerce",
|
|
518
|
+
runtimes: ["web", "react-native"],
|
|
519
|
+
recommendedParams: [{
|
|
520
|
+
name: "product_id",
|
|
521
|
+
type: "string",
|
|
522
|
+
required: true,
|
|
523
|
+
scope: "event"
|
|
524
|
+
}],
|
|
525
|
+
destinations: {
|
|
526
|
+
"fire-spark": both("product_viewed"),
|
|
527
|
+
ga4: both("view_item"),
|
|
528
|
+
meta: meta("ViewContent", "fb_mobile_content_view"),
|
|
529
|
+
tiktok: both("ViewContent"),
|
|
530
|
+
masivo: both("view_item")
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: "product_visited",
|
|
535
|
+
name: "product_visited",
|
|
536
|
+
kind: "native",
|
|
537
|
+
category: "Navegación",
|
|
538
|
+
runtimes: ["web", "react-native"],
|
|
539
|
+
recommendedParams: [
|
|
540
|
+
{
|
|
541
|
+
name: "product_id",
|
|
542
|
+
type: "string",
|
|
543
|
+
required: true,
|
|
544
|
+
scope: "event"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "category_id",
|
|
548
|
+
type: "string",
|
|
549
|
+
required: false,
|
|
550
|
+
scope: "event"
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: "attribution_source",
|
|
554
|
+
type: "string",
|
|
555
|
+
required: false,
|
|
556
|
+
scope: "event"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
name: "attribution_id",
|
|
560
|
+
type: "string",
|
|
561
|
+
required: false,
|
|
562
|
+
scope: "event"
|
|
563
|
+
}
|
|
564
|
+
],
|
|
565
|
+
destinations: {
|
|
566
|
+
"fire-spark": both("product_visited"),
|
|
567
|
+
ga4: both("product_visited"),
|
|
568
|
+
meta: both("product_visited"),
|
|
569
|
+
tiktok: both("product_visited"),
|
|
570
|
+
masivo: both("product_visited")
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
id: "category_visited",
|
|
575
|
+
name: "category_visited",
|
|
576
|
+
kind: "native",
|
|
577
|
+
category: "Navegación",
|
|
578
|
+
runtimes: ["web", "react-native"],
|
|
579
|
+
recommendedParams: [{
|
|
580
|
+
name: "category_id",
|
|
581
|
+
type: "string",
|
|
582
|
+
required: true,
|
|
583
|
+
scope: "event"
|
|
584
|
+
}],
|
|
585
|
+
destinations: {
|
|
586
|
+
"fire-spark": both("category_visited"),
|
|
587
|
+
ga4: both("category_visited"),
|
|
588
|
+
meta: both("category_visited"),
|
|
589
|
+
tiktok: both("category_visited"),
|
|
590
|
+
masivo: both("category_visited")
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
];
|
|
594
|
+
const eventsByName = new Map(CATALOG_EVENTS.map((event) => [event.name, event]));
|
|
595
|
+
const catalogEventByName = (name) => {
|
|
596
|
+
return eventsByName.get(name);
|
|
597
|
+
};
|
|
598
|
+
const destinationEventName = (developerName, provider, runtime) => {
|
|
599
|
+
const event = catalogEventByName(developerName);
|
|
600
|
+
if (!event) return developerName;
|
|
601
|
+
const binding = event.destinations[provider];
|
|
602
|
+
if (!binding) return developerName;
|
|
603
|
+
if (runtime === "react-native") return binding.reactNative;
|
|
604
|
+
return binding.web;
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region ../../packages/constants/src/tracking-plan-catalog/transforms.ts
|
|
608
|
+
const stringVal = (value) => {
|
|
609
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
610
|
+
};
|
|
611
|
+
const numberVal = (value) => {
|
|
612
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
|
|
613
|
+
return value;
|
|
614
|
+
};
|
|
615
|
+
const itemId = (item) => {
|
|
616
|
+
return stringVal(item.item_id) ?? stringVal(item.content_id) ?? "";
|
|
617
|
+
};
|
|
618
|
+
const itemQty = (item) => {
|
|
619
|
+
return numberVal(item.quantity) ?? 1;
|
|
620
|
+
};
|
|
621
|
+
const itemPrice = (item) => {
|
|
622
|
+
return numberVal(item.price) ?? numberVal(item.item_price) ?? 0;
|
|
623
|
+
};
|
|
624
|
+
const itemsOf$1 = (input) => {
|
|
625
|
+
if (input.items && input.items.length > 0) return input.items;
|
|
626
|
+
const fromParams = input.params.items;
|
|
627
|
+
if (!Array.isArray(fromParams)) return [];
|
|
628
|
+
return fromParams.filter((row) => {
|
|
629
|
+
return !!row && typeof row === "object" && !Array.isArray(row);
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
const valueOf = (input, items) => {
|
|
633
|
+
const explicit = numberVal(input.params.value);
|
|
634
|
+
if (explicit !== void 0) return explicit;
|
|
635
|
+
return items.reduce((sum, item) => sum + itemPrice(item) * itemQty(item), 0);
|
|
636
|
+
};
|
|
637
|
+
/**
|
|
638
|
+
* Delivery-only TikTok names for storefronts already emitting these events.
|
|
639
|
+
* Never used to build or prefill tracking plans.
|
|
640
|
+
*/
|
|
641
|
+
const LEGACY_TIKTOK_ALIASES = {
|
|
642
|
+
medios_view_item_delivery: "UnlockAchievement",
|
|
643
|
+
medios_view_item_express: "StartTrial",
|
|
644
|
+
medios_purchase_delivery: "Subscribe",
|
|
645
|
+
medios_purchase_express: "GenerateLead"
|
|
646
|
+
};
|
|
647
|
+
const resolvedName = (input, provider) => {
|
|
648
|
+
const alias = LEGACY_TIKTOK_ALIASES[input.name];
|
|
649
|
+
if (provider === "tiktok" && alias) return alias;
|
|
650
|
+
return destinationEventName(input.name, provider, input.runtime) ?? input.name;
|
|
651
|
+
};
|
|
652
|
+
const ga4Payload = (input, items) => {
|
|
653
|
+
const currency = merchantCurrencyCode(input.currency);
|
|
654
|
+
const value = valueOf(input, items);
|
|
655
|
+
const b1 = {
|
|
656
|
+
items: items.map((item) => {
|
|
657
|
+
return {
|
|
658
|
+
item_id: itemId(item),
|
|
659
|
+
item_name: item.item_name,
|
|
660
|
+
price: itemPrice(item),
|
|
661
|
+
quantity: itemQty(item)
|
|
662
|
+
};
|
|
663
|
+
}),
|
|
664
|
+
value,
|
|
665
|
+
currency
|
|
666
|
+
};
|
|
667
|
+
const transaction_id = stringVal(input.params.transaction_id);
|
|
668
|
+
const order_id = stringVal(input.params.order_id);
|
|
669
|
+
return {
|
|
670
|
+
...b1,
|
|
671
|
+
transaction_id: transaction_id ?? order_id
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
const metaContents = (items) => {
|
|
675
|
+
return items.map((item) => {
|
|
676
|
+
return {
|
|
677
|
+
id: itemId(item),
|
|
678
|
+
quantity: itemQty(item),
|
|
679
|
+
item_price: itemPrice(item)
|
|
680
|
+
};
|
|
681
|
+
});
|
|
682
|
+
};
|
|
683
|
+
const metaPayload = (input, items) => {
|
|
684
|
+
const currency = merchantCurrencyCode(input.currency);
|
|
685
|
+
return {
|
|
686
|
+
value: valueOf(input, items),
|
|
687
|
+
currency,
|
|
688
|
+
contents: metaContents(items)
|
|
689
|
+
};
|
|
690
|
+
};
|
|
691
|
+
const tiktokContents = (items) => {
|
|
692
|
+
return items.map((item) => {
|
|
693
|
+
return {
|
|
694
|
+
content_id: itemId(item),
|
|
695
|
+
quantity: itemQty(item),
|
|
696
|
+
price: itemPrice(item)
|
|
697
|
+
};
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
const tiktokPayload = (input, items) => {
|
|
701
|
+
const currency = merchantCurrencyCode(input.currency);
|
|
702
|
+
const params = input.params;
|
|
703
|
+
const ctx = input.context ?? {};
|
|
704
|
+
const b1 = {
|
|
705
|
+
value: valueOf(input, items),
|
|
706
|
+
currency
|
|
707
|
+
};
|
|
708
|
+
const b2 = { content_type: params.content_type };
|
|
709
|
+
const b3 = { content_id: params.content_id };
|
|
710
|
+
const b4 = {
|
|
711
|
+
description: params.description,
|
|
712
|
+
order_id: params.order_id
|
|
713
|
+
};
|
|
714
|
+
const b5 = { contents: tiktokContents(items) };
|
|
715
|
+
const properties = {
|
|
716
|
+
...b1,
|
|
717
|
+
...b2,
|
|
718
|
+
...b3,
|
|
719
|
+
...b4,
|
|
720
|
+
...b5
|
|
721
|
+
};
|
|
722
|
+
const user = {
|
|
723
|
+
external_id: ctx.external_id,
|
|
724
|
+
ttclid: ctx.ttclid
|
|
725
|
+
};
|
|
726
|
+
const r1 = { event: resolvedName(input, "tiktok") };
|
|
727
|
+
const r2 = {
|
|
728
|
+
event_time: input.eventTime,
|
|
729
|
+
event_id: input.eventId
|
|
730
|
+
};
|
|
731
|
+
return {
|
|
732
|
+
...r1,
|
|
733
|
+
...r2,
|
|
734
|
+
properties,
|
|
735
|
+
context: user
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
const masivoPayload = (input, items) => {
|
|
739
|
+
const currency = merchantCurrencyCode(input.currency);
|
|
740
|
+
const ctx = input.context ?? {};
|
|
741
|
+
const products = items.map((item) => {
|
|
742
|
+
const metadata = { prices: {
|
|
743
|
+
net_price: itemPrice(item),
|
|
744
|
+
currency
|
|
745
|
+
} };
|
|
746
|
+
return {
|
|
747
|
+
id: itemId(item),
|
|
748
|
+
quantity: itemQty(item),
|
|
749
|
+
metadata
|
|
750
|
+
};
|
|
751
|
+
});
|
|
752
|
+
const order = {
|
|
753
|
+
id: input.params.order_id ?? input.params.transaction_id,
|
|
754
|
+
currency,
|
|
755
|
+
products
|
|
756
|
+
};
|
|
757
|
+
const metadata = {
|
|
758
|
+
store_id: ctx.store_id,
|
|
759
|
+
vendor_id: ctx.vendor_id
|
|
760
|
+
};
|
|
761
|
+
return {
|
|
762
|
+
type: resolvedName(input, "masivo"),
|
|
763
|
+
order,
|
|
764
|
+
metadata
|
|
765
|
+
};
|
|
766
|
+
};
|
|
767
|
+
const transformForProvider = (provider, input) => {
|
|
768
|
+
const params = dropUsdAmountParams(input.params);
|
|
769
|
+
const itemsIn = input.items?.map((item) => dropUsdAmountParams(item));
|
|
770
|
+
const next = {
|
|
771
|
+
...input,
|
|
772
|
+
params,
|
|
773
|
+
items: itemsIn
|
|
774
|
+
};
|
|
775
|
+
const items = itemsOf$1(next);
|
|
776
|
+
const name = resolvedName(next, provider);
|
|
777
|
+
if (provider === "ga4") return {
|
|
778
|
+
provider,
|
|
779
|
+
name,
|
|
780
|
+
payload: ga4Payload(next, items)
|
|
781
|
+
};
|
|
782
|
+
if (provider === "meta") return {
|
|
783
|
+
provider,
|
|
784
|
+
name,
|
|
785
|
+
payload: metaPayload(next, items)
|
|
786
|
+
};
|
|
787
|
+
if (provider === "tiktok") return {
|
|
788
|
+
provider,
|
|
789
|
+
name,
|
|
790
|
+
payload: tiktokPayload(next, items)
|
|
791
|
+
};
|
|
792
|
+
if (provider === "masivo") return {
|
|
793
|
+
provider,
|
|
794
|
+
name,
|
|
795
|
+
payload: masivoPayload(next, items)
|
|
796
|
+
};
|
|
797
|
+
return {
|
|
798
|
+
provider,
|
|
799
|
+
name,
|
|
800
|
+
payload: next.params
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/uuid.ts
|
|
805
|
+
/** UUID v4 — works when `crypto.randomUUID` is missing (React Native / Hermes). */
|
|
806
|
+
const randomUUID = () => {
|
|
807
|
+
const c = globalThis.crypto;
|
|
808
|
+
if (typeof c?.randomUUID === "function") return c.randomUUID();
|
|
809
|
+
if (typeof c?.getRandomValues !== "function") throw new Error("crypto.getRandomValues is required");
|
|
810
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
811
|
+
c.getRandomValues(bytes);
|
|
812
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
813
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
814
|
+
const toHex = (b) => b.toString(16).padStart(2, "0");
|
|
815
|
+
const hex = Array.from(bytes, toHex).join("");
|
|
816
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
817
|
+
};
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/fanout.ts
|
|
820
|
+
const ADS_PROVIDERS = /* @__PURE__ */ new Set(["meta", "tiktok"]);
|
|
821
|
+
const DEFAULT_CONSENT = {
|
|
822
|
+
analytics: true,
|
|
823
|
+
ads: true
|
|
824
|
+
};
|
|
825
|
+
const resolveRuntime = (options) => {
|
|
826
|
+
if (options.runtime) return options.runtime;
|
|
827
|
+
return options.platform === "web" ? "web" : "react-native";
|
|
828
|
+
};
|
|
829
|
+
const resolveMerchantContext = (options) => {
|
|
830
|
+
if (options.merchantContext) return options.merchantContext();
|
|
831
|
+
if (typeof options.context === "function") return options.context();
|
|
832
|
+
return { ...options.getScope?.() ?? {} };
|
|
833
|
+
};
|
|
834
|
+
const resolveAdapters = (opts) => {
|
|
835
|
+
const constructed = runtime.options?.adapters ?? [];
|
|
836
|
+
const extra = opts?.extra ?? [];
|
|
837
|
+
const available = [...constructed, ...extra];
|
|
838
|
+
if (opts?.only) {
|
|
839
|
+
for (const id of opts.only) if (!available.some((adapter) => adapter.provider === id)) throw new Error(`Adapter ${id} was never constructed`);
|
|
840
|
+
return available.filter((adapter) => opts.only?.includes(adapter.provider));
|
|
841
|
+
}
|
|
842
|
+
if (!opts?.exclude) return available;
|
|
843
|
+
return available.filter((adapter) => !opts.exclude?.includes(adapter.provider));
|
|
844
|
+
};
|
|
845
|
+
const itemsOf = (params) => {
|
|
846
|
+
if (!Array.isArray(params.items)) return void 0;
|
|
847
|
+
return params.items.filter((row) => {
|
|
848
|
+
return !!row && typeof row === "object" && !Array.isArray(row);
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
const sendOne = async (adapter, event, consent, runtimeId, currency, context) => {
|
|
852
|
+
const ads = ADS_PROVIDERS.has(adapter.provider);
|
|
853
|
+
if (ads && !consent.ads) return;
|
|
854
|
+
if (!ads && !consent.analytics) return;
|
|
855
|
+
const raw = event.properties ?? {};
|
|
856
|
+
const params = ads ? await hashPiiParams(raw) : raw;
|
|
857
|
+
const rows = itemsOf(params);
|
|
858
|
+
const items = ads && rows ? await Promise.all(rows.map(hashPiiParams)) : rows;
|
|
859
|
+
const b1 = {
|
|
860
|
+
name: event.name,
|
|
861
|
+
params,
|
|
862
|
+
items,
|
|
863
|
+
currency
|
|
864
|
+
};
|
|
865
|
+
const b2 = {
|
|
866
|
+
runtime: runtimeId,
|
|
867
|
+
eventId: randomUUID()
|
|
868
|
+
};
|
|
869
|
+
const b3 = {
|
|
870
|
+
eventTime: Math.floor(Date.now() / 1e3),
|
|
871
|
+
context
|
|
872
|
+
};
|
|
873
|
+
const input = {
|
|
874
|
+
...b1,
|
|
875
|
+
...b2,
|
|
876
|
+
...b3
|
|
877
|
+
};
|
|
878
|
+
const transformed = transformForProvider(adapter.provider, input);
|
|
879
|
+
const n1 = {
|
|
880
|
+
name: transformed.name,
|
|
881
|
+
params: transformed.payload
|
|
882
|
+
};
|
|
883
|
+
const n2 = {
|
|
884
|
+
items,
|
|
885
|
+
context,
|
|
886
|
+
runtime: runtimeId,
|
|
887
|
+
consent
|
|
888
|
+
};
|
|
889
|
+
const normalized = {
|
|
890
|
+
...n1,
|
|
891
|
+
...n2
|
|
892
|
+
};
|
|
893
|
+
await adapter.send(normalized);
|
|
894
|
+
};
|
|
895
|
+
const fanoutAdapters = async (event, opts) => {
|
|
896
|
+
const adapters = resolveAdapters(opts);
|
|
897
|
+
if (adapters.length === 0) return;
|
|
898
|
+
const options = runtime.options;
|
|
899
|
+
if (!options) return;
|
|
900
|
+
const consent = options.consent?.() ?? DEFAULT_CONSENT;
|
|
901
|
+
const runtimeId = resolveRuntime(options);
|
|
902
|
+
const currency = options.currency ?? "USD";
|
|
903
|
+
const context = resolveMerchantContext(options);
|
|
904
|
+
const send = async (adapter) => {
|
|
905
|
+
try {
|
|
906
|
+
await sendOne(...[
|
|
907
|
+
adapter,
|
|
908
|
+
event,
|
|
909
|
+
consent,
|
|
910
|
+
runtimeId
|
|
911
|
+
], currency, context);
|
|
912
|
+
} catch {}
|
|
913
|
+
};
|
|
914
|
+
await Promise.all(adapters.map(send));
|
|
915
|
+
};
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/retry.ts
|
|
918
|
+
const defaultSleep = (ms) => {
|
|
919
|
+
return new Promise((resolve) => {
|
|
920
|
+
setTimeout(resolve, ms);
|
|
921
|
+
});
|
|
922
|
+
};
|
|
923
|
+
/** 1 attempt + `extraAttempts` retries with exponential backoff. */
|
|
924
|
+
const withExponentialRetry = async (run, options = {}) => {
|
|
925
|
+
const extraAttempts = options.extraAttempts ?? 3;
|
|
926
|
+
const baseDelayMs = options.baseDelayMs ?? 100;
|
|
927
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
928
|
+
const shouldRetry = options.shouldRetry ?? (() => true);
|
|
929
|
+
let lastError;
|
|
930
|
+
for (let attempt = 0; attempt <= extraAttempts; attempt += 1) try {
|
|
931
|
+
return await run();
|
|
932
|
+
} catch (error) {
|
|
933
|
+
lastError = error;
|
|
934
|
+
if (attempt === extraAttempts) break;
|
|
935
|
+
if (!shouldRetry(error)) break;
|
|
936
|
+
await sleep(baseDelayMs * 2 ** attempt);
|
|
937
|
+
}
|
|
938
|
+
throw lastError;
|
|
215
939
|
};
|
|
216
940
|
//#endregion
|
|
217
941
|
//#region src/flush.ts
|
|
@@ -239,17 +963,20 @@ const isOnline = async () => {
|
|
|
239
963
|
if (!check) return true;
|
|
240
964
|
return check();
|
|
241
965
|
};
|
|
242
|
-
const
|
|
243
|
-
const http = await httpOptions();
|
|
966
|
+
const storefrontCall = async (path, init) => {
|
|
244
967
|
return storefrontJson(...[
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
body: JSON.stringify({ events })
|
|
249
|
-
},
|
|
250
|
-
http
|
|
968
|
+
path,
|
|
969
|
+
init,
|
|
970
|
+
await httpOptions()
|
|
251
971
|
]);
|
|
252
972
|
};
|
|
973
|
+
const postChunk = async (events) => {
|
|
974
|
+
const init = {
|
|
975
|
+
method: "POST",
|
|
976
|
+
body: JSON.stringify({ events })
|
|
977
|
+
};
|
|
978
|
+
return storefrontCall("/api/storefront/v1/analytics/events", init);
|
|
979
|
+
};
|
|
253
980
|
const scheduleRetry = () => {
|
|
254
981
|
if (runtime.retryTimer || runtime.queue.length === 0) return;
|
|
255
982
|
runtime.retryTimer = setTimeout(() => {
|
|
@@ -262,19 +989,21 @@ const preferencesPath = () => {
|
|
|
262
989
|
const params = new URLSearchParams();
|
|
263
990
|
if (scope.fulfillment_id) params.set("fulfillment_id", scope.fulfillment_id);
|
|
264
991
|
if (scope.store_id) params.set("store_id", scope.store_id);
|
|
265
|
-
|
|
992
|
+
const channel_id = scope.channel_id ?? runtime.options?.channel_id;
|
|
993
|
+
if (channel_id) params.set("channel_id", channel_id);
|
|
266
994
|
if (scope.brand_id) params.set("brand_id", scope.brand_id);
|
|
267
995
|
const query = params.toString();
|
|
268
996
|
const path = "/api/storefront/v1/preferences";
|
|
269
997
|
return query ? `${path}?${query}` : path;
|
|
270
998
|
};
|
|
271
999
|
const fetchRemoteConfig = async () => {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
1000
|
+
const path = preferencesPath();
|
|
1001
|
+
const prefs = await storefrontCall(path, { method: "GET" });
|
|
1002
|
+
const currency_code = prefs.currency?.code;
|
|
1003
|
+
return {
|
|
1004
|
+
...prefs.analytics,
|
|
1005
|
+
currency_code
|
|
1006
|
+
};
|
|
278
1007
|
};
|
|
279
1008
|
let flushWait = null;
|
|
280
1009
|
const flushOnce = async () => {
|
|
@@ -309,19 +1038,130 @@ const flush = async () => {
|
|
|
309
1038
|
return flushWait;
|
|
310
1039
|
};
|
|
311
1040
|
//#endregion
|
|
312
|
-
//#region src/
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return
|
|
1041
|
+
//#region src/sample.ts
|
|
1042
|
+
const SAMPLE_KEY = "firespark:analytics:samples";
|
|
1043
|
+
const PLAN_SAMPLES_PATH = "/api/storefront/v1/analytics/plan-samples";
|
|
1044
|
+
const DEFAULT_RATE = .01;
|
|
1045
|
+
const inferType = (value) => {
|
|
1046
|
+
if (value === null) return "null";
|
|
1047
|
+
if (Array.isArray(value)) {
|
|
1048
|
+
const first = value[0];
|
|
1049
|
+
if (first && typeof first === "object") return "array<object>";
|
|
1050
|
+
return "array";
|
|
1051
|
+
}
|
|
1052
|
+
if (typeof value === "number") return Number.isInteger(value) ? "integer" : "number";
|
|
1053
|
+
return typeof value;
|
|
1054
|
+
};
|
|
1055
|
+
const parseSeen = (raw) => {
|
|
1056
|
+
if (!raw) return /* @__PURE__ */ new Set();
|
|
1057
|
+
try {
|
|
1058
|
+
const parsed = JSON.parse(raw);
|
|
1059
|
+
return new Set(parsed.keys ?? []);
|
|
1060
|
+
} catch {
|
|
1061
|
+
return /* @__PURE__ */ new Set();
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
const loadSeen = async () => {
|
|
1065
|
+
if (runtime.sampleSeen) return runtime.sampleSeen;
|
|
1066
|
+
runtime.sampleSeen = parseSeen(await readStorage(SAMPLE_KEY));
|
|
1067
|
+
return runtime.sampleSeen;
|
|
1068
|
+
};
|
|
1069
|
+
const persistSeen = async (seen) => {
|
|
1070
|
+
runtime.sampleSeen = seen;
|
|
1071
|
+
const keys = [...seen];
|
|
1072
|
+
await writeStorage(SAMPLE_KEY, JSON.stringify({ keys }));
|
|
1073
|
+
};
|
|
1074
|
+
const leafName = (path) => {
|
|
1075
|
+
const parts = path.split(".");
|
|
1076
|
+
return parts[parts.length - 1] ?? path;
|
|
1077
|
+
};
|
|
1078
|
+
const shouldOmitParam = (path) => {
|
|
1079
|
+
return isPiiParam(path) || isPiiParam(leafName(path));
|
|
1080
|
+
};
|
|
1081
|
+
const pushParam = (path, value, param_names, inferred_types, presence) => {
|
|
1082
|
+
if (shouldOmitParam(path)) return;
|
|
1083
|
+
param_names.push(path);
|
|
1084
|
+
inferred_types.push(inferType(value));
|
|
1085
|
+
presence.push(value !== void 0 && value !== null);
|
|
1086
|
+
};
|
|
1087
|
+
const describeSample = (event, opts) => {
|
|
1088
|
+
const params = event.properties ?? {};
|
|
1089
|
+
const param_names = [];
|
|
1090
|
+
const inferred_types = [];
|
|
1091
|
+
const presence = [];
|
|
1092
|
+
const p1 = [
|
|
1093
|
+
param_names,
|
|
1094
|
+
inferred_types,
|
|
1095
|
+
presence
|
|
1096
|
+
];
|
|
1097
|
+
for (const [key, value] of Object.entries(params)) {
|
|
1098
|
+
if (shouldOmitParam(key)) continue;
|
|
1099
|
+
pushParam(key, value, ...p1);
|
|
1100
|
+
if (Array.isArray(value)) {
|
|
1101
|
+
const first = value.find((item) => item && typeof item === "object");
|
|
1102
|
+
if (!first) continue;
|
|
1103
|
+
for (const [ik, iv] of Object.entries(first)) pushParam(`${key}.*.${ik}`, iv, ...p1);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (!value || typeof value !== "object") continue;
|
|
1107
|
+
for (const [ck, cv] of Object.entries(value)) pushParam(`${key}.${ck}`, cv, ...p1);
|
|
1108
|
+
}
|
|
1109
|
+
const variant = opts?.variant;
|
|
1110
|
+
return {
|
|
1111
|
+
event_key: event.name,
|
|
1112
|
+
param_names,
|
|
1113
|
+
inferred_types,
|
|
1114
|
+
presence,
|
|
1115
|
+
variant
|
|
1116
|
+
};
|
|
1117
|
+
};
|
|
1118
|
+
const shouldSend = (seen, firstPerEvent, rate) => {
|
|
1119
|
+
if (!seen && firstPerEvent) return true;
|
|
1120
|
+
if (rate >= 1) return true;
|
|
1121
|
+
if (rate <= 0) return false;
|
|
1122
|
+
return Math.random() < rate;
|
|
1123
|
+
};
|
|
1124
|
+
const samplePlanEvent = async (event, opts) => {
|
|
1125
|
+
const options = runtime.options;
|
|
1126
|
+
if (!options?.planVersionId) return;
|
|
1127
|
+
const sample = options.sample ?? {};
|
|
1128
|
+
const firstPerEvent = sample.firstPerEvent !== false;
|
|
1129
|
+
const rate = sample.rate ?? DEFAULT_RATE;
|
|
1130
|
+
const variant = opts?.variant;
|
|
1131
|
+
const key = variant ? `${event.name}:${variant}` : event.name;
|
|
1132
|
+
const seen = await loadSeen();
|
|
1133
|
+
const already = seen.has(key);
|
|
1134
|
+
if (!shouldSend(already, firstPerEvent, rate)) return;
|
|
1135
|
+
const described = describeSample(event, opts);
|
|
1136
|
+
const env = options.env ?? "production";
|
|
1137
|
+
const body1 = { plan_version_id: options.planVersionId };
|
|
1138
|
+
const body2 = { event_key: described.event_key };
|
|
1139
|
+
const body3 = {
|
|
1140
|
+
variant_id: described.variant ?? null,
|
|
1141
|
+
env
|
|
1142
|
+
};
|
|
1143
|
+
const body4 = { channel_id: options.channel_id };
|
|
1144
|
+
const body5 = { param_names: described.param_names };
|
|
1145
|
+
const body6 = { inferred_types: described.inferred_types };
|
|
1146
|
+
const body7 = { presence: described.presence };
|
|
1147
|
+
const full = {
|
|
1148
|
+
...body1,
|
|
1149
|
+
...body2,
|
|
1150
|
+
...body3,
|
|
1151
|
+
...body4,
|
|
1152
|
+
...body5,
|
|
1153
|
+
...body6,
|
|
1154
|
+
...body7
|
|
1155
|
+
};
|
|
1156
|
+
const init = {
|
|
1157
|
+
method: "POST",
|
|
1158
|
+
body: JSON.stringify(full)
|
|
1159
|
+
};
|
|
1160
|
+
await storefrontCall(PLAN_SAMPLES_PATH, init);
|
|
1161
|
+
if (!already) {
|
|
1162
|
+
seen.add(key);
|
|
1163
|
+
await persistSeen(seen);
|
|
1164
|
+
}
|
|
325
1165
|
};
|
|
326
1166
|
//#endregion
|
|
327
1167
|
//#region src/session.ts
|
|
@@ -372,6 +1212,14 @@ const shouldCarryMenuViewed = (prev, next) => {
|
|
|
372
1212
|
};
|
|
373
1213
|
//#endregion
|
|
374
1214
|
//#region src/index.ts
|
|
1215
|
+
const toTrackEvent = (nameOrEvent, params) => {
|
|
1216
|
+
if (typeof nameOrEvent !== "string") return nameOrEvent;
|
|
1217
|
+
if (!params) return { name: nameOrEvent };
|
|
1218
|
+
return {
|
|
1219
|
+
name: nameOrEvent,
|
|
1220
|
+
properties: params
|
|
1221
|
+
};
|
|
1222
|
+
};
|
|
375
1223
|
const entityIdFromEvent = (event) => {
|
|
376
1224
|
const props = event.properties;
|
|
377
1225
|
if (!props) return void 0;
|
|
@@ -410,23 +1258,45 @@ const refreshConfig = async () => {
|
|
|
410
1258
|
return;
|
|
411
1259
|
}
|
|
412
1260
|
try {
|
|
413
|
-
|
|
1261
|
+
const config = await fetchRemoteConfig();
|
|
1262
|
+
runtime.sessionMs = config.session_ms || 18e5;
|
|
414
1263
|
await persistConfig(runtime.sessionMs);
|
|
1264
|
+
const code = config.currency_code;
|
|
1265
|
+
if (code && runtime.options && !runtime.options.currency) runtime.options = {
|
|
1266
|
+
...runtime.options,
|
|
1267
|
+
currency: code
|
|
1268
|
+
};
|
|
415
1269
|
} catch {
|
|
416
1270
|
if (!runtime.sessionMs) runtime.sessionMs = DEFAULT_SESSION_MS;
|
|
417
1271
|
}
|
|
418
1272
|
};
|
|
1273
|
+
let trackChain = Promise.resolve();
|
|
1274
|
+
const enqueue = (run) => {
|
|
1275
|
+
const next = trackChain.then(run, run);
|
|
1276
|
+
const ok = () => void 0;
|
|
1277
|
+
trackChain = next.then(ok, ok);
|
|
1278
|
+
return next;
|
|
1279
|
+
};
|
|
419
1280
|
const configure = async (options) => {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1281
|
+
const channel_id = options.channel_id?.trim() ?? "";
|
|
1282
|
+
if (!channel_id) throw new Error("channel_id is required");
|
|
1283
|
+
return enqueue(async () => {
|
|
1284
|
+
runtime.options = {
|
|
1285
|
+
...options,
|
|
1286
|
+
channel_id
|
|
1287
|
+
};
|
|
1288
|
+
const ctx = options.context;
|
|
1289
|
+
runtime.context = buildEventContext(typeof ctx === "function" ? void 0 : ctx);
|
|
1290
|
+
await loadPersisted();
|
|
1291
|
+
await refreshConfig();
|
|
1292
|
+
armSessionTimer();
|
|
1293
|
+
if (runtime.queue.length > 0) flush();
|
|
1294
|
+
});
|
|
426
1295
|
};
|
|
427
1296
|
const toQueued = (event, now, sessionId, customer) => {
|
|
428
1297
|
const scope = runtime.options?.getScope?.() ?? {};
|
|
429
1298
|
const platform = runtime.options?.platform ?? "web";
|
|
1299
|
+
const channel_id = runtime.options?.channel_id ?? scope.channel_id;
|
|
430
1300
|
const b1 = {
|
|
431
1301
|
id: randomUUID(),
|
|
432
1302
|
name: event.name,
|
|
@@ -436,7 +1306,7 @@ const toQueued = (event, now, sessionId, customer) => {
|
|
|
436
1306
|
const b3 = { timestamp: new Date(now).toISOString() };
|
|
437
1307
|
const b4 = {
|
|
438
1308
|
store_id: scope.store_id,
|
|
439
|
-
channel_id
|
|
1309
|
+
channel_id
|
|
440
1310
|
};
|
|
441
1311
|
const b5 = {
|
|
442
1312
|
brand_id: scope.brand_id,
|
|
@@ -517,19 +1387,28 @@ const trackEvent = async (event) => {
|
|
|
517
1387
|
runtime.queue = enqueueEvent(runtime.queue, queued, now, getLimits());
|
|
518
1388
|
await persistSession();
|
|
519
1389
|
await persistQueue();
|
|
520
|
-
if (event.name === "
|
|
1390
|
+
if (event.name === "purchase") {
|
|
521
1391
|
await endSession();
|
|
522
1392
|
return;
|
|
523
1393
|
}
|
|
524
1394
|
armSessionTimer();
|
|
525
1395
|
};
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
1396
|
+
const swallow = (task) => {
|
|
1397
|
+
return task.catch(() => void 0);
|
|
1398
|
+
};
|
|
1399
|
+
const dispatch = async (event, objectForm, opts) => {
|
|
1400
|
+
const sampleP = swallow(samplePlanEvent(event, opts));
|
|
1401
|
+
resolveAdapters(opts);
|
|
1402
|
+
const fanoutP = swallow(fanoutAdapters(event, opts));
|
|
1403
|
+
const funnel = isFunnelEventName(event.name);
|
|
1404
|
+
const interaction = isInteractionEventName(event.name);
|
|
1405
|
+
if (objectForm || funnel || interaction) await trackEvent(event);
|
|
1406
|
+
await Promise.all([sampleP, fanoutP]);
|
|
1407
|
+
};
|
|
1408
|
+
const track = (nameOrEvent, params, opts) => {
|
|
1409
|
+
const objectForm = typeof nameOrEvent !== "string";
|
|
1410
|
+
const event = toTrackEvent(nameOrEvent, params);
|
|
1411
|
+
return enqueue(() => dispatch(event, objectForm, opts));
|
|
533
1412
|
};
|
|
534
1413
|
const endSession = async () => {
|
|
535
1414
|
clearTimer("sessionTimer");
|
|
@@ -545,6 +1424,19 @@ const reset = async () => {
|
|
|
545
1424
|
await storage.removeItem(QUEUE_KEY);
|
|
546
1425
|
await storage.removeItem(CONFIG_KEY);
|
|
547
1426
|
await storage.removeItem(INTERACTIONS_KEY);
|
|
1427
|
+
await storage.removeItem(SAMPLE_KEY);
|
|
1428
|
+
};
|
|
1429
|
+
var Analytics = class {
|
|
1430
|
+
constructor(options) {
|
|
1431
|
+
if (!options) return;
|
|
1432
|
+
if (!(options.channel_id?.trim() ?? "")) throw new Error("channel_id is required");
|
|
1433
|
+
configure(options);
|
|
1434
|
+
}
|
|
1435
|
+
configure = configure;
|
|
1436
|
+
track = track;
|
|
1437
|
+
flush = flush;
|
|
1438
|
+
endSession = endSession;
|
|
1439
|
+
reset = reset;
|
|
548
1440
|
};
|
|
549
1441
|
const analytics = {
|
|
550
1442
|
configure,
|
|
@@ -554,4 +1446,4 @@ const analytics = {
|
|
|
554
1446
|
reset
|
|
555
1447
|
};
|
|
556
1448
|
//#endregion
|
|
557
|
-
export { FUNNEL_EVENTS, INTERACTION_EVENTS, analytics, configure, endSession, isFunnelEventName, isInteractionEventName, reset, track };
|
|
1449
|
+
export { Analytics, FUNNEL_EVENTS, INTERACTION_EVENTS, analytics, configure, endSession, flush, isFunnelEventName, isInteractionEventName, reset, track };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fire-spark/analytics",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Fire Spark storefront analytics SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fire-spark/core": "0.
|
|
20
|
+
"@fire-spark/core": "^0.1.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"tsdown": "^0.22.14",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"build": "tsdown",
|
|
49
|
+
"build": "tsdown && find ../../packages/constants/src -name '*.d.ts' -delete",
|
|
50
50
|
"dev": "tsdown --watch",
|
|
51
51
|
"check-types": "tsc --noEmit",
|
|
52
52
|
"test:utils": "vitest run",
|