@replohq/sdk 1.2.0 → 1.2.1
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/analytics/hooks/use-product-viewed-analytics.d.ts +6 -1
- package/analytics/hooks/use-product-viewed-analytics.js +4 -1
- package/analytics/hooks/use-product-viewed-analytics.js.map +2 -2
- package/analytics/replo-pixel-script.d.ts +1 -1
- package/analytics/replo-pixel-script.js.map +1 -1
- package/analytics/sinks/converge-event-mapper.d.ts +41 -0
- package/analytics/sinks/converge-event-mapper.js +182 -0
- package/analytics/sinks/converge-event-mapper.js.map +7 -0
- package/analytics/sinks/converge-sink.d.ts +24 -9
- package/analytics/sinks/converge-sink.js +45 -23
- package/analytics/sinks/converge-sink.js.map +2 -2
- package/analytics/sinks/northbeam-sink.d.ts +2 -4
- package/analytics/sinks/northbeam-sink.js +1 -4
- package/analytics/sinks/northbeam-sink.js.map +2 -2
- package/analytics/utils/analytics-utils.d.ts +6 -1
- package/analytics/utils/analytics-utils.js +24 -11
- package/analytics/utils/analytics-utils.js.map +2 -2
- package/analytics/utils/navigation-tracker.d.ts +2 -0
- package/analytics/utils/navigation-tracker.js +7 -3
- package/analytics/utils/navigation-tracker.js.map +2 -2
- package/cart/cart-provider.js +4 -1
- package/cart/cart-provider.js.map +2 -2
- package/consent/inject-script-descriptors.d.ts +3 -3
- package/consent/inject-script-descriptors.js.map +1 -1
- package/consent/replo-scripts.d.ts +8 -2
- package/consent/replo-scripts.js +49 -33
- package/consent/replo-scripts.js.map +2 -2
- package/consent/script-snippets.js +2 -6
- package/consent/script-snippets.js.map +2 -2
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
|
@@ -8,7 +8,12 @@ interface AnalyticsProductVariant {
|
|
|
8
8
|
id: string;
|
|
9
9
|
title?: string;
|
|
10
10
|
sku?: string;
|
|
11
|
-
price?: number
|
|
11
|
+
price?: number | {
|
|
12
|
+
amount: number | string;
|
|
13
|
+
currencyCode: string;
|
|
14
|
+
};
|
|
15
|
+
currency?: string;
|
|
16
|
+
priceCurrencyCode?: string | null;
|
|
12
17
|
}
|
|
13
18
|
interface UseProductViewedAnalyticsProps {
|
|
14
19
|
product: AnalyticsProduct | null;
|
|
@@ -12,6 +12,8 @@ function useProductViewedAnalytics({
|
|
|
12
12
|
const trackingKey = `${product.id}-${selectedVariant.id}`;
|
|
13
13
|
if (lastTrackedRef.current !== trackingKey) {
|
|
14
14
|
lastTrackedRef.current = trackingKey;
|
|
15
|
+
const price = typeof selectedVariant.price === "object" ? Number(selectedVariant.price.amount) : selectedVariant.price ?? 0;
|
|
16
|
+
const currency = typeof selectedVariant.price === "object" ? selectedVariant.price.currencyCode : selectedVariant.currency ?? selectedVariant.priceCurrencyCode ?? void 0;
|
|
15
17
|
void analytics.productViewed({
|
|
16
18
|
data: {
|
|
17
19
|
productId: product.id,
|
|
@@ -21,7 +23,8 @@ function useProductViewedAnalytics({
|
|
|
21
23
|
variantId: selectedVariant.id ?? "",
|
|
22
24
|
variantTitle: selectedVariant.title ?? "",
|
|
23
25
|
variantSku: selectedVariant.sku ?? "",
|
|
24
|
-
price
|
|
26
|
+
price,
|
|
27
|
+
currency
|
|
25
28
|
}
|
|
26
29
|
});
|
|
27
30
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../analytics/hooks/use-product-viewed-analytics.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { useAnalytics } from \"../analytics-provider\";\n\ninterface AnalyticsProduct {\n id: string;\n title: string;\n slug?: string;\n description?: string;\n}\n\ninterface AnalyticsProductVariant {\n id: string;\n title?: string;\n sku?: string;\n price?: number;\n}\n\ninterface UseProductViewedAnalyticsProps {\n product: AnalyticsProduct | null;\n selectedVariant?: AnalyticsProductVariant | null;\n}\n\n/**\n * Fires a `productViewed` analytics event when the product or selected variant\n * changes. Deduplicates by product+variant ID so the same combination is only\n * tracked once per component lifecycle.\n */\nexport function useProductViewedAnalytics({\n product,\n selectedVariant,\n}: UseProductViewedAnalyticsProps) {\n const analytics = useAnalytics();\n const lastTrackedRef = useRef<string | null>(null);\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (analytics && product && selectedVariant) {\n const trackingKey = `${product.id}-${selectedVariant.id}`;\n\n if (lastTrackedRef.current !== trackingKey) {\n lastTrackedRef.current = trackingKey;\n\n void analytics.productViewed({\n data: {\n productId: product.id,\n productTitle: product.title,\n productSlug: product.slug,\n productDescription: product.description,\n variantId: selectedVariant.id ?? \"\",\n variantTitle: selectedVariant.title ?? \"\",\n variantSku: selectedVariant.sku ?? \"\",\n price
|
|
5
|
-
"mappings": ";AAEA,SAAS,WAAW,cAAc;AAElC,SAAS,oBAAoB;
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n\nimport { useAnalytics } from \"../analytics-provider\";\n\ninterface AnalyticsProduct {\n id: string;\n title: string;\n slug?: string;\n description?: string;\n}\n\ninterface AnalyticsProductVariant {\n id: string;\n title?: string;\n sku?: string;\n price?: number | { amount: number | string; currencyCode: string };\n currency?: string;\n priceCurrencyCode?: string | null;\n}\n\ninterface UseProductViewedAnalyticsProps {\n product: AnalyticsProduct | null;\n selectedVariant?: AnalyticsProductVariant | null;\n}\n\n/**\n * Fires a `productViewed` analytics event when the product or selected variant\n * changes. Deduplicates by product+variant ID so the same combination is only\n * tracked once per component lifecycle.\n */\nexport function useProductViewedAnalytics({\n product,\n selectedVariant,\n}: UseProductViewedAnalyticsProps) {\n const analytics = useAnalytics();\n const lastTrackedRef = useRef<string | null>(null);\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (analytics && product && selectedVariant) {\n const trackingKey = `${product.id}-${selectedVariant.id}`;\n\n if (lastTrackedRef.current !== trackingKey) {\n lastTrackedRef.current = trackingKey;\n\n const price =\n typeof selectedVariant.price === \"object\"\n ? Number(selectedVariant.price.amount)\n : selectedVariant.price ?? 0;\n const currency =\n typeof selectedVariant.price === \"object\"\n ? selectedVariant.price.currencyCode\n : selectedVariant.currency ??\n selectedVariant.priceCurrencyCode ??\n undefined;\n\n void analytics.productViewed({\n data: {\n productId: product.id,\n productTitle: product.title,\n productSlug: product.slug,\n productDescription: product.description,\n variantId: selectedVariant.id ?? \"\",\n variantTitle: selectedVariant.title ?? \"\",\n variantSku: selectedVariant.sku ?? \"\",\n price,\n currency,\n },\n });\n }\n }\n }, [analytics, product, selectedVariant]);\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,WAAW,cAAc;AAElC,SAAS,oBAAoB;AA4BtB,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAAmC;AACjC,QAAM,YAAY,aAAa;AAC/B,QAAM,iBAAiB,OAAsB,IAAI;AAGjD,YAAU,MAAM;AACd,QAAI,aAAa,WAAW,iBAAiB;AAC3C,YAAM,cAAc,GAAG,QAAQ,EAAE,IAAI,gBAAgB,EAAE;AAEvD,UAAI,eAAe,YAAY,aAAa;AAC1C,uBAAe,UAAU;AAEzB,cAAM,QACJ,OAAO,gBAAgB,UAAU,WAC7B,OAAO,gBAAgB,MAAM,MAAM,IACnC,gBAAgB,SAAS;AAC/B,cAAM,WACJ,OAAO,gBAAgB,UAAU,WAC7B,gBAAgB,MAAM,eACtB,gBAAgB,YAChB,gBAAgB,qBAChB;AAEN,aAAK,UAAU,cAAc;AAAA,UAC3B,MAAM;AAAA,YACJ,WAAW,QAAQ;AAAA,YACnB,cAAc,QAAQ;AAAA,YACtB,aAAa,QAAQ;AAAA,YACrB,oBAAoB,QAAQ;AAAA,YAC5B,WAAW,gBAAgB,MAAM;AAAA,YACjC,cAAc,gBAAgB,SAAS;AAAA,YACvC,YAAY,gBAAgB,OAAO;AAAA,YACnC;AAAA,YACA;AAAA,UACF;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,SAAS,eAAe,CAAC;AAC1C;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.
|
|
8
8
|
*
|
|
9
9
|
* Under a consent platform the tag is rendered regardless and `InjectedScript`
|
|
10
|
-
* stamps the blocking attributes at
|
|
10
|
+
* stamps the blocking attributes at mount time, since delegation is not yet
|
|
11
11
|
* set here.
|
|
12
12
|
*/
|
|
13
13
|
export declare function ReploPixelScript({ pixelUrl, apiUrl, projectId, }: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../analytics/replo-pixel-script.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\nimport { findDelegatedPlatform } from \"../consent/consent-platform\";\nimport { isConsentAllowed, useConsent } from \"../consent/consent-store\";\nimport { InjectedScript } from \"../consent/replo-scripts\";\n\n// Stable injection id so re-renders and React Strict Mode double-invokes never\n// inject the pixel twice (the loader itself also guards via `__reploPixelLoaded`).\nconst PIXEL_SCRIPT_ID = \"replo:first-party-pixel\";\n\n/**\n * Consent-gated injector for Replo's first-party analytics pixel. The loaded\n * script sets `window.ReploPixel`, which the always-on `ReploSink` forwards\n * events into, so gating it on `analytics` consent keeps the first-party sink\n * consent-correct without the pixel loading unconditionally in `<head>` the way\n * the legacy scaffold does. `analytics` consent here matches the\n * `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.\n *\n * Under a consent platform the tag is rendered regardless and `InjectedScript`\n * stamps the blocking attributes at
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport type { ScriptTagDescriptor } from \"schemas/generated/consent\";\n\nimport { findDelegatedPlatform } from \"../consent/consent-platform\";\nimport { isConsentAllowed, useConsent } from \"../consent/consent-store\";\nimport { InjectedScript } from \"../consent/replo-scripts\";\n\n// Stable injection id so re-renders and React Strict Mode double-invokes never\n// inject the pixel twice (the loader itself also guards via `__reploPixelLoaded`).\nconst PIXEL_SCRIPT_ID = \"replo:first-party-pixel\";\n\n/**\n * Consent-gated injector for Replo's first-party analytics pixel. The loaded\n * script sets `window.ReploPixel`, which the always-on `ReploSink` forwards\n * events into, so gating it on `analytics` consent keeps the first-party sink\n * consent-correct without the pixel loading unconditionally in `<head>` the way\n * the legacy scaffold does. `analytics` consent here matches the\n * `firstPartyAllowed` gate that controls whether `ReploSink` is created at all.\n *\n * Under a consent platform the tag is rendered regardless and `InjectedScript`\n * stamps the blocking attributes at mount time, since delegation is not yet\n * set here.\n */\nexport function ReploPixelScript({\n pixelUrl,\n apiUrl,\n projectId,\n}: {\n pixelUrl: string;\n apiUrl: string;\n projectId: string;\n}) {\n const consent = useConsent();\n if (\n !findDelegatedPlatform() &&\n !isConsentAllowed({ state: consent, requiredConsent: [\"analytics\"] })\n ) {\n return null;\n }\n\n const descriptors: ScriptTagDescriptor[] = [\n {\n kind: \"external\",\n src: `${pixelUrl}/latest.js`,\n attributes: {\n \"data-api-url\": apiUrl,\n \"data-project-id\": projectId,\n },\n },\n ];\n\n return (\n <InjectedScript\n baseId={PIXEL_SCRIPT_ID}\n descriptors={descriptors}\n requiredConsent={[\"analytics\"]}\n />\n );\n}\n"],
|
|
5
5
|
"mappings": ";AAqDI;AAjDJ,SAAS,6BAA6B;AACtC,SAAS,kBAAkB,kBAAkB;AAC7C,SAAS,sBAAsB;AAI/B,MAAM,kBAAkB;AAcjB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,UAAU,WAAW;AAC3B,MACE,CAAC,sBAAsB,KACvB,CAAC,iBAAiB,EAAE,OAAO,SAAS,iBAAiB,CAAC,WAAW,EAAE,CAAC,GACpE;AACA,WAAO;AAAA,EACT;AAEA,QAAM,cAAqC;AAAA,IACzC;AAAA,MACE,MAAM;AAAA,MACN,KAAK,GAAG,QAAQ;AAAA,MAChB,YAAY;AAAA,QACV,gBAAgB;AAAA,QAChB,mBAAmB;AAAA,MACrB;AAAA,IACF;AAAA,EACF;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ;AAAA,MACR;AAAA,MACA,iBAAiB,CAAC,WAAW;AAAA;AAAA,EAC/B;AAEJ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PurchasePayload } from "../analytics-sink";
|
|
2
|
+
type AnalyticsRecord = Record<string, unknown>;
|
|
3
|
+
export type ConvergeProductProperties = {
|
|
4
|
+
product_id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
price: number;
|
|
7
|
+
currency: string;
|
|
8
|
+
variant_id?: string;
|
|
9
|
+
sku?: string;
|
|
10
|
+
variant_name?: string;
|
|
11
|
+
vendor?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
image_url?: string;
|
|
14
|
+
};
|
|
15
|
+
type ConvergeCartItem = ConvergeProductProperties & {
|
|
16
|
+
quantity: number;
|
|
17
|
+
};
|
|
18
|
+
export type ConvergeCartProperties = {
|
|
19
|
+
total_price: number;
|
|
20
|
+
total_tax: number;
|
|
21
|
+
total_shipping: number;
|
|
22
|
+
currency: string;
|
|
23
|
+
items?: ConvergeCartItem[];
|
|
24
|
+
total_discount?: number;
|
|
25
|
+
coupon?: string | string[];
|
|
26
|
+
};
|
|
27
|
+
export type ConvergeOrderProperties = ConvergeCartProperties & {
|
|
28
|
+
id: string;
|
|
29
|
+
};
|
|
30
|
+
export type ConvergeCollectionProperties = {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
items?: ConvergeProductProperties[];
|
|
35
|
+
};
|
|
36
|
+
export declare function mapConvergeProduct(data: AnalyticsRecord): ConvergeProductProperties | null;
|
|
37
|
+
export declare function mapConvergeCartItem(data: AnalyticsRecord): ConvergeCartItem | null;
|
|
38
|
+
export declare function mapConvergeCollection(data: AnalyticsRecord): ConvergeCollectionProperties | null;
|
|
39
|
+
export declare function mapConvergeCart(data: AnalyticsRecord): ConvergeCartProperties | null;
|
|
40
|
+
export declare function mapConvergeOrder({ amount, currency, data, }: PurchasePayload): ConvergeOrderProperties | null;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const analyticsRecordSchema = z.record(z.string(), z.unknown());
|
|
3
|
+
const nonEmptyStringSchema = z.union([
|
|
4
|
+
z.string().min(1),
|
|
5
|
+
z.number().finite().transform(String)
|
|
6
|
+
]);
|
|
7
|
+
const finiteNumberSchema = z.union([
|
|
8
|
+
z.number().finite(),
|
|
9
|
+
z.string().trim().min(1).transform(Number).pipe(z.number().finite())
|
|
10
|
+
]);
|
|
11
|
+
const couponSchema = z.union([z.string(), z.array(z.string())]);
|
|
12
|
+
function parseRecord(value) {
|
|
13
|
+
const result = analyticsRecordSchema.safeParse(value);
|
|
14
|
+
return result.success ? result.data : null;
|
|
15
|
+
}
|
|
16
|
+
function firstValue(record, keys) {
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = record[key];
|
|
19
|
+
if (value !== null && value !== void 0) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
function parseString(value) {
|
|
26
|
+
const result = nonEmptyStringSchema.safeParse(value);
|
|
27
|
+
return result.success ? result.data : void 0;
|
|
28
|
+
}
|
|
29
|
+
function numberValue(value) {
|
|
30
|
+
const result = finiteNumberSchema.safeParse(value);
|
|
31
|
+
return result.success ? result.data : void 0;
|
|
32
|
+
}
|
|
33
|
+
function stringFrom(record, keys) {
|
|
34
|
+
return parseString(firstValue(record, keys));
|
|
35
|
+
}
|
|
36
|
+
function numberFrom(record, keys) {
|
|
37
|
+
return numberValue(firstValue(record, keys));
|
|
38
|
+
}
|
|
39
|
+
function moneyFrom(value) {
|
|
40
|
+
const record = parseRecord(value);
|
|
41
|
+
if (!record) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
amount: numberFrom(record, ["amount"]),
|
|
46
|
+
currency: stringFrom(record, ["currencyCode", "currency"])
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function optionalProductProperties({
|
|
50
|
+
source,
|
|
51
|
+
variant,
|
|
52
|
+
product
|
|
53
|
+
}) {
|
|
54
|
+
return {
|
|
55
|
+
variant_id: stringFrom(source, ["variant_id", "variantId"]) ?? stringFrom(variant, ["id"]),
|
|
56
|
+
sku: stringFrom(source, ["sku", "variantSku"]) ?? stringFrom(variant, ["sku"]),
|
|
57
|
+
variant_name: stringFrom(source, ["variant_name", "variantTitle"]) ?? stringFrom(variant, ["title"]),
|
|
58
|
+
vendor: stringFrom(source, ["vendor", "productVendor"]) ?? stringFrom(product, ["vendor"]),
|
|
59
|
+
url: stringFrom(source, ["url", "productUrl"]) ?? stringFrom(product, ["url"]),
|
|
60
|
+
image_url: stringFrom(source, ["image_url", "imageUrl", "productImageUrl"]) ?? stringFrom(product, ["imageUrl", "image_url"])
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function withoutUndefined(record) {
|
|
64
|
+
return Object.fromEntries(
|
|
65
|
+
Object.entries(record).filter(([, value]) => value !== void 0)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function mapProduct(value, defaults = {}) {
|
|
69
|
+
const source = parseRecord(value);
|
|
70
|
+
if (!source) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const variant = parseRecord(source.variant) ?? {};
|
|
74
|
+
const product = parseRecord(variant.product) ?? parseRecord(source.product) ?? {};
|
|
75
|
+
const variantPrice = moneyFrom(variant.price);
|
|
76
|
+
const sourcePrice = moneyFrom(source.price);
|
|
77
|
+
const productId = stringFrom(source, ["product_id", "productId"]) ?? stringFrom(product, ["id"]);
|
|
78
|
+
const name = stringFrom(source, ["name", "productTitle", "title"]) ?? stringFrom(product, ["title", "name"]);
|
|
79
|
+
const price = numberFrom(source, ["price"]) ?? sourcePrice?.amount ?? variantPrice?.amount;
|
|
80
|
+
const currency = stringFrom(source, ["currency", "currencyCode"]) ?? sourcePrice?.currency ?? variantPrice?.currency ?? defaults.currency;
|
|
81
|
+
if (!productId || !name || price === void 0 || !currency) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const quantity = numberFrom(source, ["quantity"]) ?? defaults.quantity;
|
|
85
|
+
const mapped = {
|
|
86
|
+
product_id: productId,
|
|
87
|
+
name,
|
|
88
|
+
price,
|
|
89
|
+
currency,
|
|
90
|
+
...withoutUndefined(
|
|
91
|
+
optionalProductProperties({ source, variant, product })
|
|
92
|
+
),
|
|
93
|
+
...quantity === void 0 ? {} : { quantity }
|
|
94
|
+
};
|
|
95
|
+
return mapped;
|
|
96
|
+
}
|
|
97
|
+
function listFrom(record, keys) {
|
|
98
|
+
const value = firstValue(record, keys);
|
|
99
|
+
return Array.isArray(value) ? value : [];
|
|
100
|
+
}
|
|
101
|
+
function couponFrom(record) {
|
|
102
|
+
const result = couponSchema.safeParse(
|
|
103
|
+
firstValue(record, ["coupon", "discountCode"])
|
|
104
|
+
);
|
|
105
|
+
return result.success ? result.data : void 0;
|
|
106
|
+
}
|
|
107
|
+
function mapConvergeProduct(data) {
|
|
108
|
+
const product = mapProduct(data);
|
|
109
|
+
if (!product) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const { quantity: _quantity, ...properties } = product;
|
|
113
|
+
return properties;
|
|
114
|
+
}
|
|
115
|
+
function mapConvergeCartItem(data) {
|
|
116
|
+
const product = mapProduct(data, { quantity: 1 });
|
|
117
|
+
if (!product || product.quantity === void 0) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { ...product, quantity: product.quantity };
|
|
121
|
+
}
|
|
122
|
+
function mapConvergeCollection(data) {
|
|
123
|
+
const id = stringFrom(data, ["id", "collectionId"]);
|
|
124
|
+
const name = stringFrom(data, ["name", "collectionTitle"]);
|
|
125
|
+
if (!id || !name) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const items = listFrom(data, ["items", "products"]).map((item) => mapProduct(item)).filter((item) => item !== null).map(({ quantity: _quantity, ...item }) => item);
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
name,
|
|
132
|
+
...withoutUndefined({
|
|
133
|
+
description: stringFrom(data, ["description", "collectionDescription"]),
|
|
134
|
+
items: items.length > 0 ? items : void 0
|
|
135
|
+
})
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function mapConvergeCart(data) {
|
|
139
|
+
const totalPriceMoney = moneyFrom(firstValue(data, ["totalPrice"]));
|
|
140
|
+
const subtotalMoney = moneyFrom(firstValue(data, ["subtotalPrice"]));
|
|
141
|
+
const currency = stringFrom(data, ["currency", "currencyCode"]) ?? totalPriceMoney?.currency ?? subtotalMoney?.currency;
|
|
142
|
+
const totalPrice = numberFrom(data, ["total_price", "totalAmount", "subtotal"]) ?? totalPriceMoney?.amount ?? subtotalMoney?.amount;
|
|
143
|
+
if (totalPrice === void 0 || !currency) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const items = listFrom(data, ["items", "lineItems"]).map((item) => mapProduct(item, { currency, quantity: 1 })).filter((item) => item?.quantity !== void 0);
|
|
147
|
+
return {
|
|
148
|
+
total_price: totalPrice,
|
|
149
|
+
total_tax: numberFrom(data, ["total_tax", "totalTax"]) ?? 0,
|
|
150
|
+
total_shipping: numberFrom(data, ["total_shipping", "totalShipping"]) ?? 0,
|
|
151
|
+
currency,
|
|
152
|
+
...withoutUndefined({
|
|
153
|
+
total_discount: numberFrom(data, ["total_discount", "totalDiscount"]),
|
|
154
|
+
coupon: couponFrom(data),
|
|
155
|
+
items: items.length > 0 ? items : void 0
|
|
156
|
+
})
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function mapConvergeOrder({
|
|
160
|
+
amount,
|
|
161
|
+
currency,
|
|
162
|
+
data
|
|
163
|
+
}) {
|
|
164
|
+
const id = stringFrom(data, ["id", "order_id", "orderId"]);
|
|
165
|
+
if (!id) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const cart = mapConvergeCart({
|
|
169
|
+
...data,
|
|
170
|
+
total_price: amount,
|
|
171
|
+
currency
|
|
172
|
+
});
|
|
173
|
+
return cart ? { id, ...cart } : null;
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
mapConvergeCart,
|
|
177
|
+
mapConvergeCartItem,
|
|
178
|
+
mapConvergeCollection,
|
|
179
|
+
mapConvergeOrder,
|
|
180
|
+
mapConvergeProduct
|
|
181
|
+
};
|
|
182
|
+
//# sourceMappingURL=converge-event-mapper.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../analytics/sinks/converge-event-mapper.ts"],
|
|
4
|
+
"sourcesContent": ["import type { PurchasePayload } from \"../analytics-sink\";\n\nimport { z } from \"zod\";\n\ntype AnalyticsRecord = Record<string, unknown>;\n\nconst analyticsRecordSchema = z.record(z.string(), z.unknown());\nconst nonEmptyStringSchema = z.union([\n z.string().min(1),\n z.number().finite().transform(String),\n]);\nconst finiteNumberSchema = z.union([\n z.number().finite(),\n z.string().trim().min(1).transform(Number).pipe(z.number().finite()),\n]);\nconst couponSchema = z.union([z.string(), z.array(z.string())]);\n\nexport type ConvergeProductProperties = {\n product_id: string;\n name: string;\n price: number;\n currency: string;\n variant_id?: string;\n sku?: string;\n variant_name?: string;\n vendor?: string;\n url?: string;\n image_url?: string;\n};\n\ntype ConvergeCartItem = ConvergeProductProperties & { quantity: number };\ntype MappedConvergeProduct = ConvergeProductProperties & {\n quantity?: number;\n};\n\nexport type ConvergeCartProperties = {\n total_price: number;\n total_tax: number;\n total_shipping: number;\n currency: string;\n items?: ConvergeCartItem[];\n total_discount?: number;\n coupon?: string | string[];\n};\n\nexport type ConvergeOrderProperties = ConvergeCartProperties & { id: string };\n\nexport type ConvergeCollectionProperties = {\n id: string;\n name: string;\n description?: string;\n items?: ConvergeProductProperties[];\n};\n\nfunction parseRecord(value: unknown): AnalyticsRecord | null {\n const result = analyticsRecordSchema.safeParse(value);\n return result.success ? result.data : null;\n}\n\nfunction firstValue(record: AnalyticsRecord, keys: string[]): unknown {\n for (const key of keys) {\n const value = record[key];\n if (value !== null && value !== undefined) {\n return value;\n }\n }\n return undefined;\n}\n\nfunction parseString(value: unknown): string | undefined {\n const result = nonEmptyStringSchema.safeParse(value);\n return result.success ? result.data : undefined;\n}\n\nfunction numberValue(value: unknown): number | undefined {\n const result = finiteNumberSchema.safeParse(value);\n return result.success ? result.data : undefined;\n}\n\nfunction stringFrom(record: AnalyticsRecord, keys: string[]) {\n return parseString(firstValue(record, keys));\n}\n\nfunction numberFrom(record: AnalyticsRecord, keys: string[]) {\n return numberValue(firstValue(record, keys));\n}\n\nfunction moneyFrom(value: unknown) {\n const record = parseRecord(value);\n if (!record) {\n return null;\n }\n return {\n amount: numberFrom(record, [\"amount\"]),\n currency: stringFrom(record, [\"currencyCode\", \"currency\"]),\n };\n}\n\nfunction optionalProductProperties({\n source,\n variant,\n product,\n}: {\n source: AnalyticsRecord;\n variant: AnalyticsRecord;\n product: AnalyticsRecord;\n}) {\n return {\n variant_id:\n stringFrom(source, [\"variant_id\", \"variantId\"]) ??\n stringFrom(variant, [\"id\"]),\n sku:\n stringFrom(source, [\"sku\", \"variantSku\"]) ?? stringFrom(variant, [\"sku\"]),\n variant_name:\n stringFrom(source, [\"variant_name\", \"variantTitle\"]) ??\n stringFrom(variant, [\"title\"]),\n vendor:\n stringFrom(source, [\"vendor\", \"productVendor\"]) ??\n stringFrom(product, [\"vendor\"]),\n url:\n stringFrom(source, [\"url\", \"productUrl\"]) ?? stringFrom(product, [\"url\"]),\n image_url:\n stringFrom(source, [\"image_url\", \"imageUrl\", \"productImageUrl\"]) ??\n stringFrom(product, [\"imageUrl\", \"image_url\"]),\n };\n}\n\nfunction withoutUndefined<RecordType extends AnalyticsRecord>(\n record: RecordType,\n): Partial<RecordType> {\n // oxlint-disable-next-line replo/no-as-cast -- Object.fromEntries cannot preserve the input key mapping after filtering\n return Object.fromEntries(\n Object.entries(record).filter(([, value]) => value !== undefined),\n ) as Partial<RecordType>;\n}\n\nfunction mapProduct(\n value: unknown,\n defaults: { currency?: string; quantity?: number } = {},\n): MappedConvergeProduct | null {\n const source = parseRecord(value);\n if (!source) {\n return null;\n }\n\n const variant = parseRecord(source.variant) ?? {};\n const product =\n parseRecord(variant.product) ?? parseRecord(source.product) ?? {};\n const variantPrice = moneyFrom(variant.price);\n const sourcePrice = moneyFrom(source.price);\n\n const productId =\n stringFrom(source, [\"product_id\", \"productId\"]) ??\n stringFrom(product, [\"id\"]);\n const name =\n stringFrom(source, [\"name\", \"productTitle\", \"title\"]) ??\n stringFrom(product, [\"title\", \"name\"]);\n const price =\n numberFrom(source, [\"price\"]) ??\n sourcePrice?.amount ??\n variantPrice?.amount;\n const currency =\n stringFrom(source, [\"currency\", \"currencyCode\"]) ??\n sourcePrice?.currency ??\n variantPrice?.currency ??\n defaults.currency;\n\n if (!productId || !name || price === undefined || !currency) {\n return null;\n }\n\n const quantity = numberFrom(source, [\"quantity\"]) ?? defaults.quantity;\n const mapped: MappedConvergeProduct = {\n product_id: productId,\n name,\n price,\n currency,\n ...withoutUndefined(\n optionalProductProperties({ source, variant, product }),\n ),\n ...(quantity === undefined ? {} : { quantity }),\n };\n return mapped;\n}\n\nfunction listFrom(record: AnalyticsRecord, keys: string[]): unknown[] {\n const value = firstValue(record, keys);\n return Array.isArray(value) ? value : [];\n}\n\nfunction couponFrom(record: AnalyticsRecord): string | string[] | undefined {\n const result = couponSchema.safeParse(\n firstValue(record, [\"coupon\", \"discountCode\"]),\n );\n return result.success ? result.data : undefined;\n}\n\nexport function mapConvergeProduct(\n data: AnalyticsRecord,\n): ConvergeProductProperties | null {\n const product = mapProduct(data);\n if (!product) {\n return null;\n }\n const { quantity: _quantity, ...properties } = product;\n return properties;\n}\n\nexport function mapConvergeCartItem(\n data: AnalyticsRecord,\n): ConvergeCartItem | null {\n const product = mapProduct(data, { quantity: 1 });\n if (!product || product.quantity === undefined) {\n return null;\n }\n return { ...product, quantity: product.quantity };\n}\n\nexport function mapConvergeCollection(\n data: AnalyticsRecord,\n): ConvergeCollectionProperties | null {\n const id = stringFrom(data, [\"id\", \"collectionId\"]);\n const name = stringFrom(data, [\"name\", \"collectionTitle\"]);\n if (!id || !name) {\n return null;\n }\n const items = listFrom(data, [\"items\", \"products\"])\n .map((item) => mapProduct(item))\n .filter((item): item is MappedConvergeProduct => item !== null)\n .map(({ quantity: _quantity, ...item }) => item);\n\n return {\n id,\n name,\n ...withoutUndefined({\n description: stringFrom(data, [\"description\", \"collectionDescription\"]),\n items: items.length > 0 ? items : undefined,\n }),\n };\n}\n\nexport function mapConvergeCart(\n data: AnalyticsRecord,\n): ConvergeCartProperties | null {\n const totalPriceMoney = moneyFrom(firstValue(data, [\"totalPrice\"]));\n const subtotalMoney = moneyFrom(firstValue(data, [\"subtotalPrice\"]));\n const currency =\n stringFrom(data, [\"currency\", \"currencyCode\"]) ??\n totalPriceMoney?.currency ??\n subtotalMoney?.currency;\n const totalPrice =\n numberFrom(data, [\"total_price\", \"totalAmount\", \"subtotal\"]) ??\n totalPriceMoney?.amount ??\n subtotalMoney?.amount;\n if (totalPrice === undefined || !currency) {\n return null;\n }\n\n const items = listFrom(data, [\"items\", \"lineItems\"])\n .map((item) => mapProduct(item, { currency, quantity: 1 }))\n .filter((item): item is ConvergeCartItem => item?.quantity !== undefined);\n\n return {\n total_price: totalPrice,\n total_tax: numberFrom(data, [\"total_tax\", \"totalTax\"]) ?? 0,\n total_shipping: numberFrom(data, [\"total_shipping\", \"totalShipping\"]) ?? 0,\n currency,\n ...withoutUndefined({\n total_discount: numberFrom(data, [\"total_discount\", \"totalDiscount\"]),\n coupon: couponFrom(data),\n items: items.length > 0 ? items : undefined,\n }),\n };\n}\n\nexport function mapConvergeOrder({\n amount,\n currency,\n data,\n}: PurchasePayload): ConvergeOrderProperties | null {\n const id = stringFrom(data, [\"id\", \"order_id\", \"orderId\"]);\n if (!id) {\n return null;\n }\n const cart = mapConvergeCart({\n ...data,\n total_price: amount,\n currency,\n });\n return cart ? { id, ...cart } : null;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import type { AnalyticsSink, DataPayload, PurchasePayload } from "../analytics-sink";
|
|
2
|
+
import type { PageMetadata } from "../utils/navigation-tracker";
|
|
3
|
+
type ConvergeCall = {
|
|
4
|
+
method: "track";
|
|
5
|
+
eventName: string;
|
|
6
|
+
properties?: Record<string, unknown>;
|
|
7
|
+
} | {
|
|
8
|
+
method: "forward";
|
|
9
|
+
eventName: "Placed Order";
|
|
10
|
+
eventID: string;
|
|
11
|
+
properties: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
cvg?: (call: ConvergeCall) => void;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
2
18
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The Converge snippet does not self-fire `$page_load`, so this sink owns every
|
|
7
|
-
* pageview: the NavigationTracker broadcast covers both the initial load and
|
|
8
|
-
* soft navigations, which is Converge's documented SPA integration.
|
|
19
|
+
* The consent-gated Converge snippet owns the initial pageview. This sink maps
|
|
20
|
+
* Replo's analytics payloads to Converge's strict event spec and adds SPA
|
|
21
|
+
* pageviews after client-side navigations.
|
|
9
22
|
*/
|
|
10
23
|
export declare class ConvergeSink implements AnalyticsSink {
|
|
11
|
-
private
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
private send;
|
|
25
|
+
private trackIfValid;
|
|
26
|
+
pageView(metadata?: PageMetadata): Promise<void>;
|
|
27
|
+
purchase(payload: PurchasePayload): Promise<void>;
|
|
14
28
|
click(): Promise<void>;
|
|
15
29
|
productViewed({ data }: DataPayload): Promise<void>;
|
|
16
30
|
addToCart({ data }: DataPayload): Promise<void>;
|
|
@@ -19,3 +33,4 @@ export declare class ConvergeSink implements AnalyticsSink {
|
|
|
19
33
|
startCheckout({ data }: DataPayload): Promise<void>;
|
|
20
34
|
viewCollection({ data }: DataPayload): Promise<void>;
|
|
21
35
|
}
|
|
36
|
+
export {};
|
|
@@ -1,48 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { createAnalyticsQueue } from "../utils/analytics-utils";
|
|
2
|
+
import {
|
|
3
|
+
mapConvergeCart,
|
|
4
|
+
mapConvergeCartItem,
|
|
5
|
+
mapConvergeCollection,
|
|
6
|
+
mapConvergeOrder,
|
|
7
|
+
mapConvergeProduct
|
|
8
|
+
} from "./converge-event-mapper";
|
|
9
|
+
function findConvergeQueue() {
|
|
3
10
|
if (typeof window === "undefined") {
|
|
4
11
|
return null;
|
|
5
12
|
}
|
|
6
13
|
return window.cvg ?? null;
|
|
7
14
|
}
|
|
8
15
|
class ConvergeSink {
|
|
9
|
-
|
|
10
|
-
() =>
|
|
11
|
-
(
|
|
12
|
-
|
|
16
|
+
send = createAnalyticsQueue({
|
|
17
|
+
isReady: () => Boolean(findConvergeQueue()),
|
|
18
|
+
send: (call) => {
|
|
19
|
+
findConvergeQueue()?.(call);
|
|
13
20
|
},
|
|
14
|
-
"Converge"
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
queueName: "Converge"
|
|
22
|
+
});
|
|
23
|
+
trackIfValid(eventName, properties) {
|
|
24
|
+
if (!properties) {
|
|
25
|
+
console.warn(`[analytics] skipped invalid Converge ${eventName} event`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.send({ method: "track", eventName, properties });
|
|
29
|
+
}
|
|
30
|
+
async pageView(metadata) {
|
|
31
|
+
if (metadata?.navigationType === "initial") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.send({ method: "track", eventName: "$page_load" });
|
|
35
|
+
}
|
|
36
|
+
async purchase(payload) {
|
|
37
|
+
const properties = mapConvergeOrder(payload);
|
|
38
|
+
if (!properties) {
|
|
39
|
+
console.warn("[analytics] skipped invalid Converge Placed Order event");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.send({
|
|
43
|
+
method: "forward",
|
|
44
|
+
eventName: "Placed Order",
|
|
45
|
+
eventID: properties.id,
|
|
46
|
+
properties
|
|
25
47
|
});
|
|
26
48
|
}
|
|
27
49
|
async click() {
|
|
28
50
|
}
|
|
29
51
|
async productViewed({ data }) {
|
|
30
|
-
this.
|
|
52
|
+
this.trackIfValid("Viewed Product", mapConvergeProduct(data));
|
|
31
53
|
}
|
|
32
54
|
async addToCart({ data }) {
|
|
33
|
-
this.
|
|
55
|
+
this.trackIfValid("Added To Cart", mapConvergeCartItem(data));
|
|
34
56
|
}
|
|
35
57
|
async removeFromCart({ data }) {
|
|
36
|
-
this.
|
|
58
|
+
this.trackIfValid("Removed From Cart", mapConvergeCartItem(data));
|
|
37
59
|
}
|
|
38
60
|
async viewCart({ data }) {
|
|
39
|
-
this.
|
|
61
|
+
this.trackIfValid("Viewed Cart", mapConvergeCart(data));
|
|
40
62
|
}
|
|
41
63
|
async startCheckout({ data }) {
|
|
42
|
-
this.
|
|
64
|
+
this.trackIfValid("Started Checkout", mapConvergeCart(data));
|
|
43
65
|
}
|
|
44
66
|
async viewCollection({ data }) {
|
|
45
|
-
this.
|
|
67
|
+
this.trackIfValid("Viewed Collection", mapConvergeCollection(data));
|
|
46
68
|
}
|
|
47
69
|
}
|
|
48
70
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../analytics/sinks/converge-sink.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n AnalyticsSink,\n DataPayload,\n PurchasePayload,\n} from \"../analytics-sink\";\n\nimport {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type {\n AnalyticsSink,\n DataPayload,\n PurchasePayload,\n} from \"../analytics-sink\";\nimport type { PageMetadata } from \"../utils/navigation-tracker\";\n\nimport { createAnalyticsQueue } from \"../utils/analytics-utils\";\nimport {\n mapConvergeCart,\n mapConvergeCartItem,\n mapConvergeCollection,\n mapConvergeOrder,\n mapConvergeProduct,\n} from \"./converge-event-mapper\";\n\ntype ConvergeCall =\n | {\n method: \"track\";\n eventName: string;\n properties?: Record<string, unknown>;\n }\n | {\n method: \"forward\";\n eventName: \"Placed Order\";\n eventID: string;\n properties: Record<string, unknown>;\n };\n\ndeclare global {\n interface Window {\n cvg?: (call: ConvergeCall) => void;\n }\n}\n\nfunction findConvergeQueue(): ((call: ConvergeCall) => void) | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n return window.cvg ?? null;\n}\n\n/**\n * The consent-gated Converge snippet owns the initial pageview. This sink maps\n * Replo's analytics payloads to Converge's strict event spec and adds SPA\n * pageviews after client-side navigations.\n */\nexport class ConvergeSink implements AnalyticsSink {\n private send = createAnalyticsQueue<ConvergeCall>({\n isReady: () => Boolean(findConvergeQueue()),\n send: (call) => {\n findConvergeQueue()?.(call);\n },\n queueName: \"Converge\",\n });\n\n private trackIfValid(\n eventName: string,\n properties: Record<string, unknown> | null,\n ) {\n if (!properties) {\n console.warn(`[analytics] skipped invalid Converge ${eventName} event`);\n return;\n }\n this.send({ method: \"track\", eventName, properties });\n }\n\n async pageView(metadata?: PageMetadata) {\n if (metadata?.navigationType === \"initial\") {\n return;\n }\n this.send({ method: \"track\", eventName: \"$page_load\" });\n }\n\n async purchase(payload: PurchasePayload) {\n const properties = mapConvergeOrder(payload);\n if (!properties) {\n console.warn(\"[analytics] skipped invalid Converge Placed Order event\");\n return;\n }\n this.send({\n method: \"forward\",\n eventName: \"Placed Order\",\n eventID: properties.id,\n properties,\n });\n }\n\n async click() {\n // No Converge equivalent\n }\n\n async productViewed({ data }: DataPayload) {\n this.trackIfValid(\"Viewed Product\", mapConvergeProduct(data));\n }\n\n async addToCart({ data }: DataPayload) {\n this.trackIfValid(\"Added To Cart\", mapConvergeCartItem(data));\n }\n\n async removeFromCart({ data }: DataPayload) {\n this.trackIfValid(\"Removed From Cart\", mapConvergeCartItem(data));\n }\n\n async viewCart({ data }: DataPayload) {\n this.trackIfValid(\"Viewed Cart\", mapConvergeCart(data));\n }\n\n async startCheckout({ data }: DataPayload) {\n this.trackIfValid(\"Started Checkout\", mapConvergeCart(data));\n }\n\n async viewCollection({ data }: DataPayload) {\n this.trackIfValid(\"Viewed Collection\", mapConvergeCollection(data));\n }\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAqBP,SAAS,oBAA2D;AAClE,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AACA,SAAO,OAAO,OAAO;AACvB;AAOO,MAAM,aAAsC;AAAA,EACzC,OAAO,qBAAmC;AAAA,IAChD,SAAS,MAAM,QAAQ,kBAAkB,CAAC;AAAA,IAC1C,MAAM,CAAC,SAAS;AACd,wBAAkB,IAAI,IAAI;AAAA,IAC5B;AAAA,IACA,WAAW;AAAA,EACb,CAAC;AAAA,EAEO,aACN,WACA,YACA;AACA,QAAI,CAAC,YAAY;AACf,cAAQ,KAAK,wCAAwC,SAAS,QAAQ;AACtE;AAAA,IACF;AACA,SAAK,KAAK,EAAE,QAAQ,SAAS,WAAW,WAAW,CAAC;AAAA,EACtD;AAAA,EAEA,MAAM,SAAS,UAAyB;AACtC,QAAI,UAAU,mBAAmB,WAAW;AAC1C;AAAA,IACF;AACA,SAAK,KAAK,EAAE,QAAQ,SAAS,WAAW,aAAa,CAAC;AAAA,EACxD;AAAA,EAEA,MAAM,SAAS,SAA0B;AACvC,UAAM,aAAa,iBAAiB,OAAO;AAC3C,QAAI,CAAC,YAAY;AACf,cAAQ,KAAK,yDAAyD;AACtE;AAAA,IACF;AACA,SAAK,KAAK;AAAA,MACR,QAAQ;AAAA,MACR,WAAW;AAAA,MACX,SAAS,WAAW;AAAA,MACpB;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAQ;AAAA,EAEd;AAAA,EAEA,MAAM,cAAc,EAAE,KAAK,GAAgB;AACzC,SAAK,aAAa,kBAAkB,mBAAmB,IAAI,CAAC;AAAA,EAC9D;AAAA,EAEA,MAAM,UAAU,EAAE,KAAK,GAAgB;AACrC,SAAK,aAAa,iBAAiB,oBAAoB,IAAI,CAAC;AAAA,EAC9D;AAAA,EAEA,MAAM,eAAe,EAAE,KAAK,GAAgB;AAC1C,SAAK,aAAa,qBAAqB,oBAAoB,IAAI,CAAC;AAAA,EAClE;AAAA,EAEA,MAAM,SAAS,EAAE,KAAK,GAAgB;AACpC,SAAK,aAAa,eAAe,gBAAgB,IAAI,CAAC;AAAA,EACxD;AAAA,EAEA,MAAM,cAAc,EAAE,KAAK,GAAgB;AACzC,SAAK,aAAa,oBAAoB,gBAAgB,IAAI,CAAC;AAAA,EAC7D;AAAA,EAEA,MAAM,eAAe,EAAE,KAAK,GAAgB;AAC1C,SAAK,aAAa,qBAAqB,sBAAsB,IAAI,CAAC;AAAA,EACpE;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type { AnalyticsSink } from "../analytics-sink";
|
|
2
2
|
import type { PageMetadata } from "../utils/navigation-tracker";
|
|
3
3
|
/**
|
|
4
|
-
* The Northbeam snippet
|
|
5
|
-
*
|
|
6
|
-
* initial load and soft navigations. Purchases are not fired client-side —
|
|
7
|
-
* Northbeam ingests orders server-side via its own commerce integrations.
|
|
4
|
+
* The consent-gated Northbeam snippet owns the initial pageview. This sink only
|
|
5
|
+
* adds the virtual pageviews that Northbeam requires for SPA navigations.
|
|
8
6
|
*/
|
|
9
7
|
export declare class NorthbeamSink implements AnalyticsSink {
|
|
10
8
|
private call;
|
|
@@ -5,15 +5,12 @@ function northbeamPixel() {
|
|
|
5
5
|
}
|
|
6
6
|
return window.Northbeam ?? null;
|
|
7
7
|
}
|
|
8
|
-
let hasFiredInitialPageView = false;
|
|
9
8
|
class NorthbeamSink {
|
|
10
9
|
call = createMethodQueue(northbeamPixel, {
|
|
11
10
|
queueName: "Northbeam"
|
|
12
11
|
});
|
|
13
12
|
async pageView(metadata) {
|
|
14
|
-
if (
|
|
15
|
-
hasFiredInitialPageView = true;
|
|
16
|
-
this.call("trackPageViewInitial", metadata?.url ?? window.location.href);
|
|
13
|
+
if (metadata?.navigationType === "initial") {
|
|
17
14
|
return;
|
|
18
15
|
}
|
|
19
16
|
this.call("trackPageView");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../analytics/sinks/northbeam-sink.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AnalyticsSink } from \"../analytics-sink\";\nimport type { PageMetadata } from \"../utils/navigation-tracker\";\n\nimport { createMethodQueue } from \"../utils/analytics-utils\";\n\ntype NorthbeamPixel = Record<string, (...args: unknown[]) => void>;\n\ntype WindowWithNorthbeam = Window & { Northbeam?: NorthbeamPixel };\n\nfunction northbeamPixel(): NorthbeamPixel | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n // oxlint-disable-next-line replo/no-as-cast -- the pixel's method map is an untyped window global; this is the one place it's narrowed to its known shape\n return (window as WindowWithNorthbeam).Northbeam ?? null;\n}\n\n
|
|
5
|
-
"mappings": "AAGA,SAAS,yBAAyB;AAMlC,SAAS,iBAAwC;AAC/C,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAEA,SAAQ,OAA+B,aAAa;AACtD;
|
|
4
|
+
"sourcesContent": ["import type { AnalyticsSink } from \"../analytics-sink\";\nimport type { PageMetadata } from \"../utils/navigation-tracker\";\n\nimport { createMethodQueue } from \"../utils/analytics-utils\";\n\ntype NorthbeamPixel = Record<string, (...args: unknown[]) => void>;\n\ntype WindowWithNorthbeam = Window & { Northbeam?: NorthbeamPixel };\n\nfunction northbeamPixel(): NorthbeamPixel | null {\n if (typeof window === \"undefined\") {\n return null;\n }\n // oxlint-disable-next-line replo/no-as-cast -- the pixel's method map is an untyped window global; this is the one place it's narrowed to its known shape\n return (window as WindowWithNorthbeam).Northbeam ?? null;\n}\n\n/**\n * The consent-gated Northbeam snippet owns the initial pageview. This sink only\n * adds the virtual pageviews that Northbeam requires for SPA navigations.\n */\nexport class NorthbeamSink implements AnalyticsSink {\n private call = createMethodQueue<NorthbeamPixel>(northbeamPixel, {\n queueName: \"Northbeam\",\n });\n\n async pageView(metadata?: PageMetadata) {\n if (metadata?.navigationType === \"initial\") {\n return;\n }\n this.call(\"trackPageView\");\n }\n\n async purchase() {\n // Northbeam ingests orders server-side via its commerce integrations\n }\n\n async click() {\n // No Northbeam equivalent\n }\n\n async productViewed() {\n // No Northbeam equivalent\n }\n\n async addToCart() {\n // No Northbeam equivalent\n }\n\n async removeFromCart() {\n // No Northbeam equivalent\n }\n\n async viewCart() {\n // No Northbeam equivalent\n }\n\n async startCheckout() {\n // No Northbeam equivalent\n }\n\n async viewCollection() {\n // No Northbeam equivalent\n }\n}\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,yBAAyB;AAMlC,SAAS,iBAAwC;AAC/C,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO;AAAA,EACT;AAEA,SAAQ,OAA+B,aAAa;AACtD;AAMO,MAAM,cAAuC;AAAA,EAC1C,OAAO,kBAAkC,gBAAgB;AAAA,IAC/D,WAAW;AAAA,EACb,CAAC;AAAA,EAED,MAAM,SAAS,UAAyB;AACtC,QAAI,UAAU,mBAAmB,WAAW;AAC1C;AAAA,IACF;AACA,SAAK,KAAK,eAAe;AAAA,EAC3B;AAAA,EAEA,MAAM,WAAW;AAAA,EAEjB;AAAA,EAEA,MAAM,QAAQ;AAAA,EAEd;AAAA,EAEA,MAAM,gBAAgB;AAAA,EAEtB;AAAA,EAEA,MAAM,YAAY;AAAA,EAElB;AAAA,EAEA,MAAM,iBAAiB;AAAA,EAEvB;AAAA,EAEA,MAAM,WAAW;AAAA,EAEjB;AAAA,EAEA,MAAM,gBAAgB;AAAA,EAEtB;AAAA,EAEA,MAAM,iBAAiB;AAAA,EAEvB;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function createAnalyticsQueue<Payload>({ isReady, send, queueName, }: {
|
|
2
|
+
isReady: () => boolean;
|
|
3
|
+
send: (payload: Payload) => void;
|
|
4
|
+
queueName: string;
|
|
5
|
+
}): (payload: Payload) => void;
|
|
6
|
+
export declare function createEventQueue(checkPixelReady: () => boolean, sendEvent: (event: string, params: Record<string, unknown>) => void, pixelName: string): (event: string, params?: Record<string, unknown>) => void;
|
|
2
7
|
type MethodQueueOptions = {
|
|
3
8
|
queueName?: string;
|
|
4
9
|
onTimeout?: () => void;
|