@replohq/sdk 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_vendor/schemas/generated/consent.d.ts +7 -1
- package/analytics/get-analytics-sinks.js +8 -4
- package/analytics/sinks/mixpanel-event-mapper.d.ts +48 -0
- package/analytics/sinks/mixpanel-event-mapper.js +96 -0
- package/analytics/sinks/mixpanel-sink.d.ts +27 -0
- package/analytics/sinks/mixpanel-sink.js +99 -0
- package/analytics/sinks/segment-event-mapper.d.ts +86 -0
- package/analytics/sinks/segment-event-mapper.js +71 -0
- package/analytics/sinks/segment-sink.d.ts +23 -0
- package/analytics/sinks/segment-sink.js +84 -0
- package/consent/script-snippets.d.ts +4 -3
- package/consent/script-snippets.js +19 -1
- package/consent/types.d.ts +7 -1
- package/lib/buildMetadata.js +3 -3
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "Mixpanel" | "HubSpot" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
2
2
|
export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
|
|
3
3
|
export type ConsentMode = "off" | "simple" | "per-category";
|
|
4
4
|
export type ScriptTagDescriptor = {
|
|
@@ -19,6 +19,7 @@ export type XPixelEvents = {
|
|
|
19
19
|
startCheckout?: string;
|
|
20
20
|
purchase?: string;
|
|
21
21
|
};
|
|
22
|
+
export type MixpanelRegion = "us" | "eu" | "in";
|
|
22
23
|
export type ReploScriptEntry = {
|
|
23
24
|
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot";
|
|
24
25
|
identifier: string;
|
|
@@ -28,6 +29,11 @@ export type ReploScriptEntry = {
|
|
|
28
29
|
identifier: string;
|
|
29
30
|
events?: Partial<XPixelEvents>;
|
|
30
31
|
requiredConsent?: ConsentCategory[];
|
|
32
|
+
} | {
|
|
33
|
+
type: "Mixpanel";
|
|
34
|
+
identifier: string;
|
|
35
|
+
region?: MixpanelRegion;
|
|
36
|
+
requiredConsent?: ConsentCategory[];
|
|
31
37
|
} | {
|
|
32
38
|
type: "Cookiebot";
|
|
33
39
|
identifier: string;
|
|
@@ -4,10 +4,12 @@ import { ConvergeSink } from "./sinks/converge-sink";
|
|
|
4
4
|
import { ElevarSink } from "./sinks/elevar-sink";
|
|
5
5
|
import { GA4Sink } from "./sinks/ga4-sink";
|
|
6
6
|
import { MetaSink } from "./sinks/meta-sink";
|
|
7
|
+
import { MixpanelSink } from "./sinks/mixpanel-sink";
|
|
7
8
|
import { NorthbeamSink } from "./sinks/northbeam-sink";
|
|
8
9
|
import { PinterestSink } from "./sinks/pinterest-sink";
|
|
9
10
|
import { RedditSink } from "./sinks/reddit-sink";
|
|
10
11
|
import { ReploSink } from "./sinks/replo-sink";
|
|
12
|
+
import { SegmentSink } from "./sinks/segment-sink";
|
|
11
13
|
import { SnapchatSink } from "./sinks/snapchat-sink";
|
|
12
14
|
import { TikTokSink } from "./sinks/tiktok-sink";
|
|
13
15
|
import { WindowSink } from "./sinks/window-sink";
|
|
@@ -21,11 +23,14 @@ const PROVIDER_SINK_FACTORIES = {
|
|
|
21
23
|
Pinterest: () => new PinterestSink(),
|
|
22
24
|
Reddit: () => new RedditSink(),
|
|
23
25
|
Northbeam: () => new NorthbeamSink(),
|
|
24
|
-
Converge: () => new ConvergeSink()
|
|
26
|
+
Converge: () => new ConvergeSink(),
|
|
27
|
+
Segment: () => new SegmentSink(),
|
|
28
|
+
Mixpanel: () => new MixpanelSink()
|
|
25
29
|
};
|
|
26
30
|
const SNIPPET_PROVIDER_SINK_FACTORIES = {
|
|
27
31
|
[snippetScriptId("Elevar")]: () => new ElevarSink()
|
|
28
32
|
};
|
|
33
|
+
const SHOPIFY_FUNNEL_GATE_EXEMPT_PROVIDERS = /* @__PURE__ */ new Set(["Segment", "Mixpanel"]);
|
|
29
34
|
function findSinkFactory(script) {
|
|
30
35
|
if (script.type === "X") {
|
|
31
36
|
return () => {
|
|
@@ -66,9 +71,8 @@ function getAnalyticsSinks({
|
|
|
66
71
|
requiredConsent: script.requiredConsent
|
|
67
72
|
})) {
|
|
68
73
|
const sink = factory();
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
74
|
+
const gated = shopifyOwnsCheckout && !SHOPIFY_FUNNEL_GATE_EXEMPT_PROVIDERS.has(script.type);
|
|
75
|
+
sinks.push(gated ? gateShopifyOwnedFunnelEvents(sink) : sink);
|
|
72
76
|
addedProviders.add(providerKey);
|
|
73
77
|
}
|
|
74
78
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AnalyticsProductLine, CartViewedData, CheckoutStartedData, CollectionViewedData, PurchasePayload } from "../analytics-sink";
|
|
2
|
+
/**
|
|
3
|
+
* Maps Replo's analytics payloads onto Mixpanel's conventional e-commerce
|
|
4
|
+
* property names (the Segment-style vocabulary Mixpanel's docs recommend:
|
|
5
|
+
* `product_id`, `name`, `price`, `quantity`, `products`, `revenue`, ...).
|
|
6
|
+
*
|
|
7
|
+
* Mixpanel accepts any property bag, so unlike the Converge mapper nothing
|
|
8
|
+
* here rejects an event: a funnel built on `Product Viewed` still counts an
|
|
9
|
+
* event whose payload had no price. Empty SKUs and variant titles are left
|
|
10
|
+
* out rather than sent blank.
|
|
11
|
+
*/
|
|
12
|
+
export type MixpanelProductProperties = {
|
|
13
|
+
product_id: string;
|
|
14
|
+
sku?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
variant?: string;
|
|
17
|
+
variant_id?: string;
|
|
18
|
+
price?: number;
|
|
19
|
+
quantity?: number;
|
|
20
|
+
currency?: string;
|
|
21
|
+
};
|
|
22
|
+
export type MixpanelCartProperties = {
|
|
23
|
+
total: number;
|
|
24
|
+
currency?: string;
|
|
25
|
+
products?: MixpanelProductProperties[];
|
|
26
|
+
};
|
|
27
|
+
export type MixpanelOrderProperties = {
|
|
28
|
+
total: number;
|
|
29
|
+
/** The property Mixpanel's revenue reports read. */
|
|
30
|
+
revenue: number;
|
|
31
|
+
currency: string;
|
|
32
|
+
order_id?: string;
|
|
33
|
+
products?: MixpanelProductProperties[];
|
|
34
|
+
tax?: number;
|
|
35
|
+
shipping?: number;
|
|
36
|
+
discount?: number;
|
|
37
|
+
coupon?: string;
|
|
38
|
+
};
|
|
39
|
+
export type MixpanelCollectionProperties = {
|
|
40
|
+
list_id: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
products?: MixpanelProductProperties[];
|
|
43
|
+
};
|
|
44
|
+
export declare function mapMixpanelProduct(line: AnalyticsProductLine): MixpanelProductProperties;
|
|
45
|
+
export declare function mapMixpanelViewedCart(data: CartViewedData): MixpanelCartProperties;
|
|
46
|
+
export declare function mapMixpanelCheckout(data: CheckoutStartedData): MixpanelCartProperties;
|
|
47
|
+
export declare function mapMixpanelCollection(data: CollectionViewedData): MixpanelCollectionProperties;
|
|
48
|
+
export declare function mapMixpanelOrder({ amount, currency, data, }: PurchasePayload): MixpanelOrderProperties;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { filterUndefined } from "../../_vendor/replo-utils/lib/object.mjs";
|
|
2
|
+
import { legacyResourceId } from "../utils/analytics-products";
|
|
3
|
+
function mapProduct({
|
|
4
|
+
line,
|
|
5
|
+
fallbackCurrency
|
|
6
|
+
}) {
|
|
7
|
+
return {
|
|
8
|
+
product_id: legacyResourceId(line.productId),
|
|
9
|
+
...filterUndefined({
|
|
10
|
+
sku: line.variantSku || void 0,
|
|
11
|
+
name: line.productTitle || void 0,
|
|
12
|
+
variant: line.variantTitle || void 0,
|
|
13
|
+
variant_id: line.variantId ? legacyResourceId(line.variantId) : void 0,
|
|
14
|
+
price: line.price,
|
|
15
|
+
quantity: line.quantity,
|
|
16
|
+
currency: line.currency ?? fallbackCurrency ?? void 0
|
|
17
|
+
})
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function mapProducts({
|
|
21
|
+
lines,
|
|
22
|
+
currency
|
|
23
|
+
}) {
|
|
24
|
+
if (lines.length === 0) {
|
|
25
|
+
return void 0;
|
|
26
|
+
}
|
|
27
|
+
return lines.map((line) => mapProduct({ line, fallbackCurrency: currency }));
|
|
28
|
+
}
|
|
29
|
+
function mapMixpanelProduct(line) {
|
|
30
|
+
return mapProduct({ line, fallbackCurrency: null });
|
|
31
|
+
}
|
|
32
|
+
function mapCart({
|
|
33
|
+
total,
|
|
34
|
+
currency,
|
|
35
|
+
lineItems
|
|
36
|
+
}) {
|
|
37
|
+
return {
|
|
38
|
+
total,
|
|
39
|
+
...filterUndefined({
|
|
40
|
+
currency: currency ?? void 0,
|
|
41
|
+
products: mapProducts({ lines: lineItems, currency })
|
|
42
|
+
})
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function mapMixpanelViewedCart(data) {
|
|
46
|
+
return mapCart({
|
|
47
|
+
total: data.subtotal,
|
|
48
|
+
currency: data.currency ?? null,
|
|
49
|
+
lineItems: data.lineItems
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function mapMixpanelCheckout(data) {
|
|
53
|
+
return mapCart({
|
|
54
|
+
total: data.total,
|
|
55
|
+
currency: data.currency ?? null,
|
|
56
|
+
lineItems: data.lineItems
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function mapMixpanelCollection(data) {
|
|
60
|
+
return {
|
|
61
|
+
list_id: legacyResourceId(data.collectionId),
|
|
62
|
+
...filterUndefined({
|
|
63
|
+
name: data.collectionTitle || void 0,
|
|
64
|
+
products: mapProducts({
|
|
65
|
+
lines: data.products,
|
|
66
|
+
currency: data.currency ?? null
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function mapMixpanelOrder({
|
|
72
|
+
amount,
|
|
73
|
+
currency,
|
|
74
|
+
data
|
|
75
|
+
}) {
|
|
76
|
+
return {
|
|
77
|
+
total: amount,
|
|
78
|
+
revenue: amount,
|
|
79
|
+
currency,
|
|
80
|
+
...filterUndefined({
|
|
81
|
+
order_id: data.orderId || void 0,
|
|
82
|
+
products: mapProducts({ lines: data.lineItems ?? [], currency }),
|
|
83
|
+
tax: data.totalTax,
|
|
84
|
+
shipping: data.totalShipping,
|
|
85
|
+
discount: data.totalDiscount,
|
|
86
|
+
coupon: data.discountCodes && data.discountCodes.length > 0 ? data.discountCodes.join(", ") : void 0
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
mapMixpanelCheckout,
|
|
92
|
+
mapMixpanelCollection,
|
|
93
|
+
mapMixpanelOrder,
|
|
94
|
+
mapMixpanelProduct,
|
|
95
|
+
mapMixpanelViewedCart
|
|
96
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AnalyticsSink, CartLineEventPayload, CartViewedPayload, CheckoutStartedPayload, ClickPayload, CollectionViewedPayload, CustomEventPayload, LegacyCheckoutStartedPayload, ProductViewedPayload, PurchasePayload } from "../analytics-sink";
|
|
2
|
+
import type { PageMetadata } from "../utils/navigation-tracker";
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
mixpanel?: {
|
|
6
|
+
track: (eventName: string, properties: Record<string, unknown>) => void;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Sends Replo's analytics events to Mixpanel with its conventional e-commerce
|
|
12
|
+
* event names. The consent-gated snippet sends the landing "Page Viewed"; this
|
|
13
|
+
* sink adds one for each in-site navigation, so each load counts once.
|
|
14
|
+
*/
|
|
15
|
+
export declare class MixpanelSink implements AnalyticsSink {
|
|
16
|
+
private send;
|
|
17
|
+
customEvent({ event, properties }: CustomEventPayload): Promise<void>;
|
|
18
|
+
pageView(metadata?: PageMetadata): Promise<void>;
|
|
19
|
+
purchase(payload: PurchasePayload): Promise<void>;
|
|
20
|
+
click({ data }: ClickPayload): Promise<void>;
|
|
21
|
+
productViewed({ data }: ProductViewedPayload): Promise<void>;
|
|
22
|
+
addToCart({ data }: CartLineEventPayload): Promise<void>;
|
|
23
|
+
removeFromCart({ data }: CartLineEventPayload): Promise<void>;
|
|
24
|
+
viewCart({ data }: CartViewedPayload): Promise<void>;
|
|
25
|
+
startCheckout(payload: CheckoutStartedPayload | LegacyCheckoutStartedPayload): Promise<void>;
|
|
26
|
+
viewCollection({ data }: CollectionViewedPayload): Promise<void>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { filterUndefined } from "../../_vendor/replo-utils/lib/object.mjs";
|
|
2
|
+
import { toCheckoutStartedData } from "../legacy-checkout";
|
|
3
|
+
import { createAnalyticsQueue } from "../utils/analytics-utils";
|
|
4
|
+
import {
|
|
5
|
+
mapMixpanelCheckout,
|
|
6
|
+
mapMixpanelCollection,
|
|
7
|
+
mapMixpanelOrder,
|
|
8
|
+
mapMixpanelProduct,
|
|
9
|
+
mapMixpanelViewedCart
|
|
10
|
+
} from "./mixpanel-event-mapper";
|
|
11
|
+
function findMixpanel() {
|
|
12
|
+
if (typeof window === "undefined") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return window.mixpanel ?? null;
|
|
16
|
+
}
|
|
17
|
+
function pathFromUrl(url) {
|
|
18
|
+
try {
|
|
19
|
+
return new URL(url).pathname;
|
|
20
|
+
} catch {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
class MixpanelSink {
|
|
25
|
+
send = createAnalyticsQueue({
|
|
26
|
+
isReady: () => Boolean(findMixpanel()),
|
|
27
|
+
send: ({ eventName, properties }) => {
|
|
28
|
+
findMixpanel()?.track(eventName, properties);
|
|
29
|
+
},
|
|
30
|
+
queueName: "Mixpanel"
|
|
31
|
+
});
|
|
32
|
+
async customEvent({ event, properties }) {
|
|
33
|
+
this.send({ eventName: event, properties: properties ?? {} });
|
|
34
|
+
}
|
|
35
|
+
async pageView(metadata) {
|
|
36
|
+
if (metadata?.navigationType === "initial") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const url = metadata?.url ?? (typeof window === "undefined" ? void 0 : window.location.href);
|
|
40
|
+
this.send({
|
|
41
|
+
eventName: "Page Viewed",
|
|
42
|
+
properties: filterUndefined({
|
|
43
|
+
url,
|
|
44
|
+
path: url ? pathFromUrl(url) : void 0,
|
|
45
|
+
title: metadata?.title || void 0,
|
|
46
|
+
referrer: metadata?.referrer || void 0,
|
|
47
|
+
navigation_type: metadata?.navigationType
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
async purchase(payload) {
|
|
52
|
+
this.send({
|
|
53
|
+
eventName: "Order Completed",
|
|
54
|
+
properties: mapMixpanelOrder(payload)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async click({ data }) {
|
|
58
|
+
this.send({ eventName: "Click", properties: data });
|
|
59
|
+
}
|
|
60
|
+
async productViewed({ data }) {
|
|
61
|
+
this.send({
|
|
62
|
+
eventName: "Product Viewed",
|
|
63
|
+
properties: mapMixpanelProduct(data)
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async addToCart({ data }) {
|
|
67
|
+
this.send({
|
|
68
|
+
eventName: "Product Added",
|
|
69
|
+
properties: mapMixpanelProduct(data)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async removeFromCart({ data }) {
|
|
73
|
+
this.send({
|
|
74
|
+
eventName: "Product Removed",
|
|
75
|
+
properties: mapMixpanelProduct(data)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async viewCart({ data }) {
|
|
79
|
+
this.send({
|
|
80
|
+
eventName: "Cart Viewed",
|
|
81
|
+
properties: mapMixpanelViewedCart(data)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async startCheckout(payload) {
|
|
85
|
+
this.send({
|
|
86
|
+
eventName: "Checkout Started",
|
|
87
|
+
properties: mapMixpanelCheckout(toCheckoutStartedData(payload.data))
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async viewCollection({ data }) {
|
|
91
|
+
this.send({
|
|
92
|
+
eventName: "Collection Viewed",
|
|
93
|
+
properties: mapMixpanelCollection(data)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export {
|
|
98
|
+
MixpanelSink
|
|
99
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { AnalyticsProductLine, CartLineData, CartViewedData, CheckoutStartedData, CollectionViewedData, PurchasePayload } from "../analytics-sink";
|
|
2
|
+
export declare function mapSegmentProduct(line: AnalyticsProductLine, currency: string | undefined): {
|
|
3
|
+
name?: string | undefined;
|
|
4
|
+
variant?: string | undefined;
|
|
5
|
+
variant_id?: string | undefined;
|
|
6
|
+
sku?: string | undefined;
|
|
7
|
+
price?: number | undefined;
|
|
8
|
+
quantity?: number | undefined;
|
|
9
|
+
currency?: string | undefined;
|
|
10
|
+
product_id: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function mapSegmentCartLine(line: CartLineData): {
|
|
13
|
+
name?: string | undefined;
|
|
14
|
+
variant?: string | undefined;
|
|
15
|
+
variant_id?: string | undefined;
|
|
16
|
+
sku?: string | undefined;
|
|
17
|
+
price?: number | undefined;
|
|
18
|
+
quantity?: number | undefined;
|
|
19
|
+
currency?: string | undefined;
|
|
20
|
+
product_id: string;
|
|
21
|
+
};
|
|
22
|
+
export declare function mapSegmentCartViewed(data: CartViewedData): {
|
|
23
|
+
currency?: string | undefined;
|
|
24
|
+
value: number;
|
|
25
|
+
products: {
|
|
26
|
+
name?: string | undefined;
|
|
27
|
+
variant?: string | undefined;
|
|
28
|
+
variant_id?: string | undefined;
|
|
29
|
+
sku?: string | undefined;
|
|
30
|
+
price?: number | undefined;
|
|
31
|
+
quantity?: number | undefined;
|
|
32
|
+
currency?: string | undefined;
|
|
33
|
+
product_id: string;
|
|
34
|
+
}[];
|
|
35
|
+
};
|
|
36
|
+
export declare function mapSegmentCheckoutStarted(data: CheckoutStartedData): {
|
|
37
|
+
currency?: string | undefined;
|
|
38
|
+
value: number;
|
|
39
|
+
total: number;
|
|
40
|
+
revenue: number;
|
|
41
|
+
products: {
|
|
42
|
+
name?: string | undefined;
|
|
43
|
+
variant?: string | undefined;
|
|
44
|
+
variant_id?: string | undefined;
|
|
45
|
+
sku?: string | undefined;
|
|
46
|
+
price?: number | undefined;
|
|
47
|
+
quantity?: number | undefined;
|
|
48
|
+
currency?: string | undefined;
|
|
49
|
+
product_id: string;
|
|
50
|
+
}[];
|
|
51
|
+
};
|
|
52
|
+
export declare function mapSegmentProductList(data: CollectionViewedData): {
|
|
53
|
+
category?: string | undefined;
|
|
54
|
+
list_id: string;
|
|
55
|
+
products: {
|
|
56
|
+
name?: string | undefined;
|
|
57
|
+
variant?: string | undefined;
|
|
58
|
+
variant_id?: string | undefined;
|
|
59
|
+
sku?: string | undefined;
|
|
60
|
+
price?: number | undefined;
|
|
61
|
+
quantity?: number | undefined;
|
|
62
|
+
currency?: string | undefined;
|
|
63
|
+
product_id: string;
|
|
64
|
+
}[];
|
|
65
|
+
};
|
|
66
|
+
export declare function mapSegmentOrder({ amount, currency, data }: PurchasePayload): {
|
|
67
|
+
tax?: number | undefined;
|
|
68
|
+
shipping?: number | undefined;
|
|
69
|
+
order_id?: string | undefined;
|
|
70
|
+
discount?: number | undefined;
|
|
71
|
+
coupon?: string | undefined;
|
|
72
|
+
value: number;
|
|
73
|
+
total: number;
|
|
74
|
+
revenue: number;
|
|
75
|
+
currency: string;
|
|
76
|
+
products: {
|
|
77
|
+
name?: string | undefined;
|
|
78
|
+
variant?: string | undefined;
|
|
79
|
+
variant_id?: string | undefined;
|
|
80
|
+
sku?: string | undefined;
|
|
81
|
+
price?: number | undefined;
|
|
82
|
+
quantity?: number | undefined;
|
|
83
|
+
currency?: string | undefined;
|
|
84
|
+
product_id: string;
|
|
85
|
+
}[];
|
|
86
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { filterUndefined } from "../../_vendor/replo-utils/lib/object.mjs";
|
|
2
|
+
import { legacyResourceId } from "../utils/analytics-products";
|
|
3
|
+
function mapSegmentProduct(line, currency) {
|
|
4
|
+
return {
|
|
5
|
+
product_id: legacyResourceId(line.productId),
|
|
6
|
+
...filterUndefined({
|
|
7
|
+
name: line.productTitle,
|
|
8
|
+
variant: line.variantTitle || void 0,
|
|
9
|
+
variant_id: line.variantId ? legacyResourceId(line.variantId) : void 0,
|
|
10
|
+
sku: line.variantSku || void 0,
|
|
11
|
+
price: line.price,
|
|
12
|
+
quantity: line.quantity,
|
|
13
|
+
currency: line.currency ?? currency
|
|
14
|
+
})
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function mapProducts(lines, currency) {
|
|
18
|
+
return lines.map((line) => mapSegmentProduct(line, currency));
|
|
19
|
+
}
|
|
20
|
+
function mapSegmentCartLine(line) {
|
|
21
|
+
return mapSegmentProduct(line, void 0);
|
|
22
|
+
}
|
|
23
|
+
function mapSegmentCartViewed(data) {
|
|
24
|
+
return {
|
|
25
|
+
value: data.subtotal,
|
|
26
|
+
products: mapProducts(data.lineItems, data.currency),
|
|
27
|
+
...filterUndefined({ currency: data.currency })
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function mapSegmentCheckoutStarted(data) {
|
|
31
|
+
return {
|
|
32
|
+
value: data.total,
|
|
33
|
+
total: data.total,
|
|
34
|
+
revenue: data.subtotal,
|
|
35
|
+
products: mapProducts(data.lineItems, data.currency),
|
|
36
|
+
...filterUndefined({ currency: data.currency })
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function mapSegmentProductList(data) {
|
|
40
|
+
return {
|
|
41
|
+
list_id: legacyResourceId(data.collectionId),
|
|
42
|
+
products: mapProducts(data.products, data.currency),
|
|
43
|
+
...filterUndefined({ category: data.collectionTitle })
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mapSegmentOrder({ amount, currency, data }) {
|
|
47
|
+
const tax = data.totalTax ?? 0;
|
|
48
|
+
const shipping = data.totalShipping ?? 0;
|
|
49
|
+
return {
|
|
50
|
+
value: amount,
|
|
51
|
+
total: amount,
|
|
52
|
+
revenue: Math.round((amount - tax - shipping) * 100) / 100,
|
|
53
|
+
currency,
|
|
54
|
+
products: mapProducts(data.lineItems ?? [], currency),
|
|
55
|
+
...filterUndefined({
|
|
56
|
+
tax: data.totalTax,
|
|
57
|
+
shipping: data.totalShipping,
|
|
58
|
+
order_id: data.orderId,
|
|
59
|
+
discount: data.totalDiscount,
|
|
60
|
+
coupon: data.discountCodes?.join(",")
|
|
61
|
+
})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
mapSegmentCartLine,
|
|
66
|
+
mapSegmentCartViewed,
|
|
67
|
+
mapSegmentCheckoutStarted,
|
|
68
|
+
mapSegmentOrder,
|
|
69
|
+
mapSegmentProduct,
|
|
70
|
+
mapSegmentProductList
|
|
71
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnalyticsSink, CartLineEventPayload, CartViewedPayload, CheckoutStartedPayload, ClickPayload, CollectionViewedPayload, CustomEventPayload, LegacyCheckoutStartedPayload, ProductViewedPayload, PurchasePayload } from "../analytics-sink";
|
|
2
|
+
import type { PageMetadata } from "../utils/navigation-tracker";
|
|
3
|
+
/**
|
|
4
|
+
* Sends Replo's analytics events to Segment as V2 E-Commerce spec `track`
|
|
5
|
+
* events, so a tracking plan written against the spec ("Product Added",
|
|
6
|
+
* "Order Completed") is met without page-level code. The consent-gated
|
|
7
|
+
* snippet sends the landing pageview; this sink adds a `page` call for each
|
|
8
|
+
* in-site navigation.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SegmentSink implements AnalyticsSink {
|
|
11
|
+
private send;
|
|
12
|
+
private track;
|
|
13
|
+
customEvent({ event, properties }: CustomEventPayload): Promise<void>;
|
|
14
|
+
pageView(metadata?: PageMetadata): Promise<void>;
|
|
15
|
+
purchase(payload: PurchasePayload): Promise<void>;
|
|
16
|
+
click({ data }: ClickPayload): Promise<void>;
|
|
17
|
+
productViewed({ data }: ProductViewedPayload): Promise<void>;
|
|
18
|
+
addToCart({ data }: CartLineEventPayload): Promise<void>;
|
|
19
|
+
removeFromCart({ data }: CartLineEventPayload): Promise<void>;
|
|
20
|
+
viewCart({ data }: CartViewedPayload): Promise<void>;
|
|
21
|
+
startCheckout(payload: CheckoutStartedPayload | LegacyCheckoutStartedPayload): Promise<void>;
|
|
22
|
+
viewCollection({ data }: CollectionViewedPayload): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { toCheckoutStartedData } from "../legacy-checkout";
|
|
2
|
+
import { createAnalyticsQueue } from "../utils/analytics-utils";
|
|
3
|
+
import {
|
|
4
|
+
mapSegmentCartLine,
|
|
5
|
+
mapSegmentCartViewed,
|
|
6
|
+
mapSegmentCheckoutStarted,
|
|
7
|
+
mapSegmentOrder,
|
|
8
|
+
mapSegmentProduct,
|
|
9
|
+
mapSegmentProductList
|
|
10
|
+
} from "./segment-event-mapper";
|
|
11
|
+
function findSegment() {
|
|
12
|
+
if (typeof window === "undefined") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return window.analytics ?? null;
|
|
16
|
+
}
|
|
17
|
+
class SegmentSink {
|
|
18
|
+
send = createAnalyticsQueue({
|
|
19
|
+
isReady: () => Boolean(findSegment()),
|
|
20
|
+
send: (call) => {
|
|
21
|
+
const analytics = findSegment();
|
|
22
|
+
if (call.method === "page") {
|
|
23
|
+
analytics?.page(call.properties);
|
|
24
|
+
} else {
|
|
25
|
+
analytics?.track(call.event, call.properties);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
queueName: "Segment"
|
|
29
|
+
});
|
|
30
|
+
track(event, properties) {
|
|
31
|
+
this.send({ method: "track", event, properties });
|
|
32
|
+
}
|
|
33
|
+
async customEvent({ event, properties }) {
|
|
34
|
+
this.track(event, properties ?? {});
|
|
35
|
+
}
|
|
36
|
+
async pageView(metadata) {
|
|
37
|
+
if (metadata?.navigationType === "initial") {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!metadata) {
|
|
41
|
+
this.send({ method: "page", properties: {} });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
this.send({
|
|
45
|
+
method: "page",
|
|
46
|
+
properties: {
|
|
47
|
+
url: metadata.url,
|
|
48
|
+
path: new URL(metadata.url, window.location.origin).pathname,
|
|
49
|
+
title: metadata.title,
|
|
50
|
+
referrer: metadata.referrer
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async purchase(payload) {
|
|
55
|
+
this.track("Order Completed", mapSegmentOrder(payload));
|
|
56
|
+
}
|
|
57
|
+
async click({ data }) {
|
|
58
|
+
this.track("Element Clicked", data);
|
|
59
|
+
}
|
|
60
|
+
async productViewed({ data }) {
|
|
61
|
+
this.track("Product Viewed", mapSegmentProduct(data, data.currency));
|
|
62
|
+
}
|
|
63
|
+
async addToCart({ data }) {
|
|
64
|
+
this.track("Product Added", mapSegmentCartLine(data));
|
|
65
|
+
}
|
|
66
|
+
async removeFromCart({ data }) {
|
|
67
|
+
this.track("Product Removed", mapSegmentCartLine(data));
|
|
68
|
+
}
|
|
69
|
+
async viewCart({ data }) {
|
|
70
|
+
this.track("Cart Viewed", mapSegmentCartViewed(data));
|
|
71
|
+
}
|
|
72
|
+
async startCheckout(payload) {
|
|
73
|
+
this.track(
|
|
74
|
+
"Checkout Started",
|
|
75
|
+
mapSegmentCheckoutStarted(toCheckoutStartedData(payload.data))
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
async viewCollection({ data }) {
|
|
79
|
+
this.track("Product List Viewed", mapSegmentProductList(data));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export {
|
|
83
|
+
SegmentSink
|
|
84
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { ConsentCategory, ReploScriptEntry, ReploScriptType, ScriptTagDescriptor } from "../_vendor/schemas/generated/consent";
|
|
2
2
|
type ProviderType = Exclude<ReploScriptType, "custom" | "snippet" | "consentPlatform" | "Cookiebot">;
|
|
3
|
+
type ProviderScriptEntry = Extract<ReploScriptEntry, {
|
|
4
|
+
type: ProviderType;
|
|
5
|
+
}>;
|
|
3
6
|
/**
|
|
4
7
|
* Snippet providers don't synthesize tags (the user pastes the provider-issued
|
|
5
8
|
* snippet as the entry's `body`), but they still get a first-class default
|
|
@@ -11,9 +14,7 @@ export declare function isXEventIdForPixel({ eventId, pixelId, }: {
|
|
|
11
14
|
pixelId: string;
|
|
12
15
|
}): boolean;
|
|
13
16
|
/** The runtime injects these tags in order after consent allows tracking. */
|
|
14
|
-
export declare function buildScriptTags(entry:
|
|
15
|
-
type: ProviderType;
|
|
16
|
-
}>): ScriptTagDescriptor[];
|
|
17
|
+
export declare function buildScriptTags(entry: ProviderScriptEntry): ScriptTagDescriptor[];
|
|
17
18
|
/**
|
|
18
19
|
* The default consent categories that gate an identifier-provider script when an
|
|
19
20
|
* entry does not specify `requiredConsent`. `snippet` and `custom` entries carry
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
const MIXPANEL_API_HOSTS = {
|
|
2
|
+
us: null,
|
|
3
|
+
eu: "https://api-eu.mixpanel.com",
|
|
4
|
+
in: "https://api-in.mixpanel.com"
|
|
5
|
+
};
|
|
6
|
+
function mixpanelInitOptions(region) {
|
|
7
|
+
const apiHost = MIXPANEL_API_HOSTS[region ?? "us"];
|
|
8
|
+
return apiHost ? `{track_pageview:false,api_host:${JSON.stringify(apiHost)}}` : "{track_pageview:false}";
|
|
9
|
+
}
|
|
1
10
|
const PROVIDER_SNIPPETS = {
|
|
2
11
|
GA4: {
|
|
3
12
|
defaultConsent: ["analytics"],
|
|
@@ -138,6 +147,15 @@ gtag('config', '${id}');`
|
|
|
138
147
|
}
|
|
139
148
|
]
|
|
140
149
|
},
|
|
150
|
+
Mixpanel: {
|
|
151
|
+
defaultConsent: ["analytics"],
|
|
152
|
+
buildTags: (id, entry) => [
|
|
153
|
+
{
|
|
154
|
+
kind: "inline",
|
|
155
|
+
body: `(function(f,b){if(!b.__SV){var e,g,i,h;window.mixpanel=b;b._i=[];b.init=function(e,f,c){function g(a,d){var b=d.split(".");2==b.length&&(a=a[b[0]],d=b[1]);a[d]=function(){a.push([d].concat(Array.prototype.slice.call(arguments,0)))}}var a=b;"undefined"!==typeof c?a=b[c]=[]:c="mixpanel";a.people=a.people||[];a.toString=function(a){var d="mixpanel";"mixpanel"!==c&&(d+="."+c);a||(d+=" (stub)");return d};a.people.toString=function(){return a.toString(1)+".people (stub)"};i="disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking start_batch_senders people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove".split(" ");for(h=0;h<i.length;h++)g(a,i[h]);var j="set set_once union unset remove delete".split(" ");a.get_group=function(){function b(c){d[c]=function(){var e=arguments;a.push([f,[c].concat(Array.prototype.slice.call(e,0))])}}for(var d={},f=["get_group"].concat(Array.prototype.slice.call(arguments,0)),c=0;c<j.length;c++)b(j[c]);return d};b._i.push([e,f,c])};b.__SV=1.2;e=f.createElement("script");e.type="text/javascript";e.async=!0;e.src="https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js";f.head.appendChild(e)}})(document,window.mixpanel||[]);mixpanel.init("${id}",${mixpanelInitOptions(entry.type === "Mixpanel" ? entry.region : void 0)});mixpanel.track("Page Viewed",{url:location.href,path:location.pathname,title:document.title||undefined,referrer:document.referrer||undefined,navigation_type:"initial"});`
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
141
159
|
Gorgias: {
|
|
142
160
|
defaultConsent: ["preferences"],
|
|
143
161
|
buildTags: (id) => [
|
|
@@ -167,7 +185,7 @@ function isXEventIdForPixel({
|
|
|
167
185
|
return match?.[1]?.toLowerCase() === pixelId.toLowerCase();
|
|
168
186
|
}
|
|
169
187
|
function buildScriptTags(entry) {
|
|
170
|
-
const tags = PROVIDER_SNIPPETS[entry.type].buildTags(entry.identifier);
|
|
188
|
+
const tags = PROVIDER_SNIPPETS[entry.type].buildTags(entry.identifier, entry);
|
|
171
189
|
if (entry.type === "X") {
|
|
172
190
|
const pageViewId = entry.events?.pageView?.trim();
|
|
173
191
|
if (pageViewId && isXEventIdForPixel({ eventId: pageViewId, pixelId: entry.identifier })) {
|
package/consent/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
1
|
+
export type ReploScriptType = "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "X" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Mixpanel" | "Cookiebot" | "snippet" | "custom" | "consentPlatform";
|
|
2
2
|
export type ConsentCategory = "necessary" | "analytics" | "marketing" | "preferences" | "sale_of_data";
|
|
3
3
|
export type ConsentMode = "off" | "simple" | "per-category";
|
|
4
4
|
declare module "react" {
|
|
@@ -23,6 +23,7 @@ export type XPixelEvents = {
|
|
|
23
23
|
startCheckout?: string;
|
|
24
24
|
purchase?: string;
|
|
25
25
|
};
|
|
26
|
+
export type MixpanelRegion = "us" | "eu" | "in";
|
|
26
27
|
export type ReploScriptEntry = {
|
|
27
28
|
type: "GA4" | "GoogleTagManager" | "Meta" | "TikTok" | "Pinterest" | "Reddit" | "Snapchat" | "Hotjar" | "MicrosoftClarity" | "Contentsquare" | "Segment" | "Northbeam" | "Gorgias" | "Converge" | "HubSpot" | "Cookiebot";
|
|
28
29
|
identifier: string;
|
|
@@ -32,6 +33,11 @@ export type ReploScriptEntry = {
|
|
|
32
33
|
identifier: string;
|
|
33
34
|
events?: XPixelEvents;
|
|
34
35
|
requiredConsent?: ConsentCategory[];
|
|
36
|
+
} | {
|
|
37
|
+
type: "Mixpanel";
|
|
38
|
+
identifier: string;
|
|
39
|
+
region?: MixpanelRegion;
|
|
40
|
+
requiredConsent?: ConsentCategory[];
|
|
35
41
|
} | {
|
|
36
42
|
type: "snippet";
|
|
37
43
|
id: string;
|
package/lib/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-10-
|
|
5
|
+
"commit": "425a057ac5f569a875e63976fa5a936dc651e292",
|
|
6
|
+
"builtAt": "2026-10-20T23:04:33.045Z"
|
|
7
7
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sdk",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.2.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-10-
|
|
8
|
+
"commit": "425a057ac5f569a875e63976fa5a936dc651e292",
|
|
9
|
+
"builtAt": "2026-10-20T23:04:33.045Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo SDK — cart, analytics, and data loaders for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|