@drip-apex/sdk 0.1.0 → 0.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/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19262 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19246 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16621 -13539
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +27 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2070 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +138 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
package/src/track.test.ts
CHANGED
|
@@ -18,6 +18,7 @@ describe("Tracker flush resilience", () => {
|
|
|
18
18
|
const realSessionStorage = (globalThis as any).sessionStorage;
|
|
19
19
|
const realLocalStorage = (globalThis as any).localStorage;
|
|
20
20
|
const realNavigatorDescriptor = Object.getOwnPropertyDescriptor(globalThis, "navigator");
|
|
21
|
+
const realLocationDescriptor = Object.getOwnPropertyDescriptor(globalThis, "location");
|
|
21
22
|
|
|
22
23
|
function setNavigator(value: unknown): void {
|
|
23
24
|
Object.defineProperty(globalThis, "navigator", {
|
|
@@ -39,6 +40,10 @@ describe("Tracker flush resilience", () => {
|
|
|
39
40
|
addEventListener: () => {},
|
|
40
41
|
removeEventListener: () => {},
|
|
41
42
|
};
|
|
43
|
+
Object.defineProperty(globalThis, "location", {
|
|
44
|
+
configurable: true,
|
|
45
|
+
value: new URL("https://store.example/"),
|
|
46
|
+
});
|
|
42
47
|
});
|
|
43
48
|
|
|
44
49
|
afterEach(() => {
|
|
@@ -51,6 +56,8 @@ describe("Tracker flush resilience", () => {
|
|
|
51
56
|
} else {
|
|
52
57
|
delete (globalThis as any).navigator;
|
|
53
58
|
}
|
|
59
|
+
if (realLocationDescriptor) Object.defineProperty(globalThis, "location", realLocationDescriptor);
|
|
60
|
+
else delete (globalThis as any).location;
|
|
54
61
|
(globalThis as any).sessionStorage = realSessionStorage;
|
|
55
62
|
(globalThis as any).localStorage = realLocalStorage;
|
|
56
63
|
vi.restoreAllMocks();
|
|
@@ -71,6 +78,73 @@ describe("Tracker flush resilience", () => {
|
|
|
71
78
|
}
|
|
72
79
|
});
|
|
73
80
|
|
|
81
|
+
it("tags both holdout and active events when program holdout is enabled", async () => {
|
|
82
|
+
const bodies: string[] = [];
|
|
83
|
+
globalThis.fetch = vi.fn(async (_url, init) => {
|
|
84
|
+
bodies.push(String(init?.body));
|
|
85
|
+
return new Response(null, { status: 202 });
|
|
86
|
+
}) as typeof fetch;
|
|
87
|
+
const holdout = new Tracker({
|
|
88
|
+
endpoint: "https://e.test/events",
|
|
89
|
+
shopId: "s",
|
|
90
|
+
cohort: "holdout",
|
|
91
|
+
holdoutConfigEpoch: 1_785_104_515_000,
|
|
92
|
+
maxBatchSize: 1,
|
|
93
|
+
});
|
|
94
|
+
holdout.track({ type: "pageview", data: {} });
|
|
95
|
+
await holdout.flush();
|
|
96
|
+
const active = new Tracker({
|
|
97
|
+
endpoint: "https://e.test/events",
|
|
98
|
+
shopId: "s",
|
|
99
|
+
cohort: "active",
|
|
100
|
+
holdoutConfigEpoch: 1_785_104_515_000,
|
|
101
|
+
maxBatchSize: 1,
|
|
102
|
+
});
|
|
103
|
+
active.track({ type: "pageview", data: {} });
|
|
104
|
+
await active.flush();
|
|
105
|
+
expect(bodies[0]).toContain('"cohort":"holdout"');
|
|
106
|
+
expect(bodies[0]).toContain('"holdout_config_epoch":1785104515000');
|
|
107
|
+
expect(bodies[1]).toContain('"cohort":"active"');
|
|
108
|
+
expect(bodies[1]).toContain('"holdout_config_epoch":1785104515000');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("keeps SDK cohort metadata ahead of the Worker property cap", async () => {
|
|
112
|
+
let eventData: Record<string, unknown> | undefined;
|
|
113
|
+
globalThis.fetch = vi.fn(async (_url, init) => {
|
|
114
|
+
const payload = JSON.parse(String(init?.body)) as {
|
|
115
|
+
events: Array<{ data?: Record<string, unknown> }>;
|
|
116
|
+
};
|
|
117
|
+
eventData = payload.events[0]?.data;
|
|
118
|
+
return new Response(null, { status: 202 });
|
|
119
|
+
}) as typeof fetch;
|
|
120
|
+
const merchantData = Object.fromEntries(
|
|
121
|
+
Array.from({ length: 85 }, (_, index) => [`merchant_${index}`, index]),
|
|
122
|
+
);
|
|
123
|
+
const tracker = new Tracker({
|
|
124
|
+
endpoint: "https://e.test/events",
|
|
125
|
+
shopId: "s",
|
|
126
|
+
cohort: "holdout",
|
|
127
|
+
holdoutConfigEpoch: 1_785_104_515_000,
|
|
128
|
+
maxBatchSize: 1,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
tracker.track({
|
|
132
|
+
type: "custom",
|
|
133
|
+
data: { ...merchantData, cohort: "merchant-override" },
|
|
134
|
+
});
|
|
135
|
+
await tracker.flush();
|
|
136
|
+
|
|
137
|
+
expect(eventData?.cohort).toBe("holdout");
|
|
138
|
+
expect(eventData?.holdout_config_epoch).toBe(1_785_104_515_000);
|
|
139
|
+
expect(Object.keys(eventData ?? {})[0]).toBe("cohort");
|
|
140
|
+
const workerCappedData = Object.fromEntries(
|
|
141
|
+
Object.entries(eventData ?? {}).slice(0, 80),
|
|
142
|
+
);
|
|
143
|
+
expect(workerCappedData.cohort).toBe("holdout");
|
|
144
|
+
expect(workerCappedData.holdout_config_epoch).toBe(1_785_104_515_000);
|
|
145
|
+
expect(workerCappedData.merchant_84).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
74
148
|
it("counts lifetime visitor sessions without double-counting tracker re-initialization", () => {
|
|
75
149
|
sessionStorage.setItem("drip_sid", "session-one");
|
|
76
150
|
|
|
@@ -792,6 +866,79 @@ describe("Tracker flush resilience", () => {
|
|
|
792
866
|
expect(payload.events[0]).toMatchObject({ type: "checkout_started" });
|
|
793
867
|
});
|
|
794
868
|
|
|
869
|
+
it("uses the Shopify proxy and does not direct-send after a 2xx", async () => {
|
|
870
|
+
const fetchMock = vi.fn(async () => new Response(null, { status: 202 }));
|
|
871
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
872
|
+
const tracker = new Tracker({ endpoint: "https://e.test/events", shopId: "s", maxBatchSize: 1, proxyEventDelivery: { enabled: true } });
|
|
873
|
+
tracker.track({ type: "pageview" });
|
|
874
|
+
await tracker.flush();
|
|
875
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
876
|
+
expect(String(fetchMock.mock.calls[0]?.[0])).toContain("/apps/apex/events");
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("rejects an absolute proxy path and keeps delivery on the storefront origin", async () => {
|
|
880
|
+
const fetchMock = vi.fn(async () => new Response(null, { status: 202 }));
|
|
881
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
882
|
+
const tracker = new Tracker({
|
|
883
|
+
endpoint: "https://e.test/events",
|
|
884
|
+
shopId: "s",
|
|
885
|
+
maxBatchSize: 1,
|
|
886
|
+
proxyEventDelivery: { enabled: true, path: "https://evil.example/events" },
|
|
887
|
+
});
|
|
888
|
+
tracker.track({ type: "pageview" });
|
|
889
|
+
await tracker.flush();
|
|
890
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://store.example/apps/apex/events");
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("retries the same proxy batch directly once with a downgrade marker", async () => {
|
|
894
|
+
const fetchMock = vi.fn()
|
|
895
|
+
.mockResolvedValueOnce(new Response(null, { status: 502 }))
|
|
896
|
+
.mockResolvedValueOnce(new Response(null, { status: 202 }));
|
|
897
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
898
|
+
const tracker = new Tracker({ endpoint: "https://e.test/events", shopId: "s", maxBatchSize: 1, proxyEventDelivery: { enabled: true } });
|
|
899
|
+
tracker.track({ type: "pageview", data: { original: true } });
|
|
900
|
+
await tracker.flush();
|
|
901
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
902
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe("https://e.test/events");
|
|
903
|
+
expect(String(fetchMock.mock.calls[1]?.[1]?.body)).toContain('"delivery_downgrade":"proxy_failed"');
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it("marks every event after an asset fallback", async () => {
|
|
907
|
+
const fetchMock = vi.fn(async () => new Response(null, { status: 202 }));
|
|
908
|
+
globalThis.fetch = fetchMock as typeof fetch;
|
|
909
|
+
const tracker = new Tracker({ endpoint: "https://e.test/events", shopId: "s", maxBatchSize: 2, assetFallback: true });
|
|
910
|
+
tracker.track({ type: "sdk_perf" });
|
|
911
|
+
tracker.track({ type: "pageview", data: { asset_fallback: false } });
|
|
912
|
+
await tracker.flush();
|
|
913
|
+
const payload = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
|
914
|
+
expect(payload.events).toHaveLength(2);
|
|
915
|
+
expect(payload.events.every((event: { data: Record<string, unknown> }) => event.data.asset_fallback === true)).toBe(true);
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
it("falls back synchronously to the direct beacon when the proxy unload beacon is refused", () => {
|
|
919
|
+
let pagehide: (() => void) | undefined;
|
|
920
|
+
(globalThis as any).window.addEventListener = (name: string, handler: () => void) => {
|
|
921
|
+
if (name === "pagehide") pagehide = handler;
|
|
922
|
+
};
|
|
923
|
+
const sendBeacon = vi.fn()
|
|
924
|
+
.mockReturnValueOnce(false)
|
|
925
|
+
.mockReturnValueOnce(true);
|
|
926
|
+
setNavigator({ sendBeacon });
|
|
927
|
+
const tracker = new Tracker({
|
|
928
|
+
endpoint: "https://e.test/events",
|
|
929
|
+
shopId: "s",
|
|
930
|
+
proxyEventDelivery: { enabled: true },
|
|
931
|
+
});
|
|
932
|
+
tracker.start();
|
|
933
|
+
tracker.track({ type: "pageview" });
|
|
934
|
+
pagehide?.();
|
|
935
|
+
|
|
936
|
+
expect(sendBeacon).toHaveBeenCalledTimes(2);
|
|
937
|
+
expect(sendBeacon.mock.calls[0]?.[0]).toBe("https://store.example/apps/apex/events");
|
|
938
|
+
expect(sendBeacon.mock.calls[1]?.[0]).toBe("https://e.test/events");
|
|
939
|
+
expect(sendBeacon.mock.calls[1]?.[1]).toBeInstanceOf(Blob);
|
|
940
|
+
});
|
|
941
|
+
|
|
795
942
|
it("removes unload listeners when stopped", () => {
|
|
796
943
|
const addEventListener = vi.fn();
|
|
797
944
|
const removeEventListener = vi.fn();
|
package/src/track.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { TrackEvent } from "./types";
|
|
|
4
4
|
|
|
5
5
|
type TrackerOptions = {
|
|
6
6
|
endpoint: string;
|
|
7
|
+
proxyEventDelivery?: { enabled: boolean; path?: string };
|
|
8
|
+
assetFallback?: boolean;
|
|
7
9
|
shopId?: string;
|
|
8
10
|
ingestShopId?: string;
|
|
9
11
|
ingestSignature?: string;
|
|
@@ -20,8 +22,24 @@ type TrackerOptions = {
|
|
|
20
22
|
qaSessionId?: string;
|
|
21
23
|
/** Active Apex environment stamped onto every outgoing event. */
|
|
22
24
|
environment?: string;
|
|
25
|
+
/** Global cohort stamped on every event while program holdout is enabled. */
|
|
26
|
+
cohort?: "holdout" | "active";
|
|
27
|
+
/** Reporting epoch of the configuration that produced the global cohort. */
|
|
28
|
+
holdoutConfigEpoch?: number;
|
|
23
29
|
};
|
|
24
30
|
|
|
31
|
+
export function getOrCreateSessionId(): string {
|
|
32
|
+
try {
|
|
33
|
+
const existing = sessionStorage.getItem("drip_sid");
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
36
|
+
sessionStorage.setItem("drip_sid", id);
|
|
37
|
+
return id;
|
|
38
|
+
} catch {
|
|
39
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
25
43
|
const RECOVERY_KEY = "drip_events";
|
|
26
44
|
const VISITOR_SESSIONS_KEY = "drip_visitor_sessions_v1";
|
|
27
45
|
|
|
@@ -79,7 +97,7 @@ function visitorSessionCount(sessionId: string): number | undefined {
|
|
|
79
97
|
}
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
function createEventId(): string {
|
|
100
|
+
export function createEventId(): string {
|
|
83
101
|
const cryptoApi = globalThis.crypto as Crypto | undefined;
|
|
84
102
|
if (typeof cryptoApi?.randomUUID === "function") {
|
|
85
103
|
return cryptoApi.randomUUID();
|
|
@@ -93,6 +111,7 @@ function createEventId(): string {
|
|
|
93
111
|
|
|
94
112
|
export class Tracker {
|
|
95
113
|
private endpoint: string;
|
|
114
|
+
private proxyEndpoint: string | null;
|
|
96
115
|
private shopId: string;
|
|
97
116
|
private ingestShopId: string;
|
|
98
117
|
private ingestSignature: string;
|
|
@@ -114,6 +133,9 @@ export class Tracker {
|
|
|
114
133
|
private qaMode: boolean;
|
|
115
134
|
private qaSessionId: string;
|
|
116
135
|
private environment: string;
|
|
136
|
+
private cohort: "holdout" | "active" | undefined;
|
|
137
|
+
private holdoutConfigEpoch: number | undefined;
|
|
138
|
+
private assetFallback: boolean;
|
|
117
139
|
private pagehideHandler: ((event?: PageTransitionEvent) => void) | null = null;
|
|
118
140
|
private beforeUnloadHandler: (() => void) | null = null;
|
|
119
141
|
private pageshowHandler: (() => void) | null = null;
|
|
@@ -124,11 +146,12 @@ export class Tracker {
|
|
|
124
146
|
|
|
125
147
|
constructor(options: TrackerOptions) {
|
|
126
148
|
this.endpoint = options.endpoint;
|
|
149
|
+
this.proxyEndpoint = resolveProxyEventEndpoint(options.proxyEventDelivery);
|
|
127
150
|
this.shopId = options.shopId ?? "";
|
|
128
151
|
this.ingestShopId = options.ingestShopId ?? options.shopId ?? "";
|
|
129
152
|
this.ingestSignature = options.ingestSignature ?? "";
|
|
130
153
|
this.visitorId = options.visitorId ?? "";
|
|
131
|
-
this.sessionId =
|
|
154
|
+
this.sessionId = getOrCreateSessionId();
|
|
132
155
|
this.visitorSessions = visitorSessionCount(this.sessionId);
|
|
133
156
|
this.flushIntervalMs = options.flushIntervalMs ?? 5000;
|
|
134
157
|
this.maxBatchSize = options.maxBatchSize ?? 20;
|
|
@@ -137,20 +160,16 @@ export class Tracker {
|
|
|
137
160
|
this.qaMode = options.qaMode ?? false;
|
|
138
161
|
this.qaSessionId = options.qaSessionId ?? "";
|
|
139
162
|
this.environment = options.environment || "production";
|
|
163
|
+
this.cohort = options.cohort;
|
|
164
|
+
this.holdoutConfigEpoch =
|
|
165
|
+
Number.isSafeInteger(options.holdoutConfigEpoch) &&
|
|
166
|
+
(options.holdoutConfigEpoch ?? 0) > 0
|
|
167
|
+
? options.holdoutConfigEpoch
|
|
168
|
+
: undefined;
|
|
169
|
+
this.assetFallback = options.assetFallback === true;
|
|
140
170
|
}
|
|
141
171
|
|
|
142
|
-
|
|
143
|
-
const key = "drip_sid";
|
|
144
|
-
try {
|
|
145
|
-
const existing = sessionStorage.getItem(key);
|
|
146
|
-
if (existing) return existing;
|
|
147
|
-
const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
148
|
-
sessionStorage.setItem(key, id);
|
|
149
|
-
return id;
|
|
150
|
-
} catch {
|
|
151
|
-
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
172
|
+
|
|
154
173
|
|
|
155
174
|
/** Public accessor for the session ID (used by the Shopify pixel bridge). */
|
|
156
175
|
getSessionId(): string {
|
|
@@ -196,8 +215,32 @@ export class Tracker {
|
|
|
196
215
|
}
|
|
197
216
|
|
|
198
217
|
private enrichEvent(event: TrackEvent): TrackEvent {
|
|
218
|
+
const systemData: Record<string, unknown> = {};
|
|
219
|
+
if (this.assetFallback) systemData.asset_fallback = true;
|
|
220
|
+
if (this.cohort) {
|
|
221
|
+
systemData.cohort = this.cohort;
|
|
222
|
+
if (this.holdoutConfigEpoch) {
|
|
223
|
+
systemData.holdout_config_epoch = this.holdoutConfigEpoch;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (
|
|
227
|
+
this.visitorSessions &&
|
|
228
|
+
carriesVisitorSessions(event.type) &&
|
|
229
|
+
!("visitor_sessions" in (event.data ?? {}))
|
|
230
|
+
) {
|
|
231
|
+
systemData.visitor_sessions = this.visitorSessions;
|
|
232
|
+
}
|
|
199
233
|
const enriched: TrackEvent = {
|
|
200
234
|
...event,
|
|
235
|
+
...(Object.keys(systemData).length > 0
|
|
236
|
+
? {
|
|
237
|
+
// Worker sanitization retains the first 80 insertion-ordered
|
|
238
|
+
// properties. Insert trusted SDK fields before merchant data, then
|
|
239
|
+
// re-stamp them so user input cannot override their values without
|
|
240
|
+
// moving their insertion position behind the cap.
|
|
241
|
+
data: { ...systemData, ...event.data, ...systemData },
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
201
244
|
event_id:
|
|
202
245
|
typeof event.event_id === "string" && event.event_id.trim()
|
|
203
246
|
? event.event_id
|
|
@@ -209,13 +252,6 @@ export class Tracker {
|
|
|
209
252
|
ts: event.ts ?? Date.now(),
|
|
210
253
|
environment: this.environment,
|
|
211
254
|
};
|
|
212
|
-
if (
|
|
213
|
-
this.visitorSessions &&
|
|
214
|
-
carriesVisitorSessions(event.type) &&
|
|
215
|
-
!("visitor_sessions" in (event.data ?? {}))
|
|
216
|
-
) {
|
|
217
|
-
enriched.data = { ...event.data, visitor_sessions: this.visitorSessions };
|
|
218
|
-
}
|
|
219
255
|
if (this.qaMode) {
|
|
220
256
|
enriched.qa_mode = true;
|
|
221
257
|
if (this.qaSessionId) enriched.qa_session_id = this.qaSessionId;
|
|
@@ -267,10 +303,31 @@ export class Tracker {
|
|
|
267
303
|
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
268
304
|
this.activeBatch = batch;
|
|
269
305
|
if (!batch.length) return;
|
|
270
|
-
const
|
|
306
|
+
const payload = this.buildPayload(batch);
|
|
307
|
+
let res: Response;
|
|
308
|
+
if (this.proxyEndpoint) {
|
|
309
|
+
try {
|
|
310
|
+
res = await fetch(this.proxyEndpoint, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/json" },
|
|
313
|
+
body: payload,
|
|
314
|
+
keepalive: true,
|
|
315
|
+
});
|
|
316
|
+
} catch {
|
|
317
|
+
res = new Response(null, { status: 503 });
|
|
318
|
+
}
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
res = await fetch(this.endpoint, {
|
|
321
|
+
method: "POST",
|
|
322
|
+
headers: this.buildHeaders(),
|
|
323
|
+
body: this.buildPayload(batch, "proxy_failed"),
|
|
324
|
+
keepalive: true,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
} else res = await fetch(this.endpoint, {
|
|
271
328
|
method: "POST",
|
|
272
329
|
headers: this.buildHeaders(),
|
|
273
|
-
body:
|
|
330
|
+
body: payload,
|
|
274
331
|
keepalive: true,
|
|
275
332
|
});
|
|
276
333
|
if (res.ok) {
|
|
@@ -323,8 +380,11 @@ export class Tracker {
|
|
|
323
380
|
}
|
|
324
381
|
}
|
|
325
382
|
|
|
326
|
-
private buildPayload(batch: TrackEvent[]): string {
|
|
327
|
-
const
|
|
383
|
+
private buildPayload(batch: TrackEvent[], deliveryDowngrade?: string): string {
|
|
384
|
+
const events = deliveryDowngrade
|
|
385
|
+
? batch.map((event) => ({ ...event, data: { ...event.data, delivery_downgrade: deliveryDowngrade } }))
|
|
386
|
+
: batch;
|
|
387
|
+
const body: Record<string, unknown> = { events };
|
|
328
388
|
// Include auth in payload body so sendBeacon (which can't set headers) works.
|
|
329
389
|
if (this.ingestShopId) body._shopId = this.ingestShopId;
|
|
330
390
|
if (this.ingestSignature) body._signature = this.ingestSignature;
|
|
@@ -431,9 +491,27 @@ export class Tracker {
|
|
|
431
491
|
|
|
432
492
|
while (lifecycleEvents.length) {
|
|
433
493
|
const batch = lifecycleEvents.splice(0, this.maxBatchSize);
|
|
434
|
-
|
|
494
|
+
let payload = this.buildPayload(batch);
|
|
435
495
|
let queued = false;
|
|
436
496
|
|
|
497
|
+
if (this.proxyEndpoint) {
|
|
498
|
+
try {
|
|
499
|
+
const nav = typeof navigator !== "undefined" ? navigator : undefined;
|
|
500
|
+
if (typeof nav?.sendBeacon === "function") {
|
|
501
|
+
queued = nav.sendBeacon(
|
|
502
|
+
this.proxyEndpoint,
|
|
503
|
+
new Blob([payload], { type: "text/plain" }),
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
queued = false;
|
|
508
|
+
}
|
|
509
|
+
if (queued) continue;
|
|
510
|
+
// Fall through to the established direct unload lane with an explicit
|
|
511
|
+
// downgrade marker when the same-origin proxy beacon cannot be queued.
|
|
512
|
+
payload = this.buildPayload(batch, "proxy_failed");
|
|
513
|
+
}
|
|
514
|
+
|
|
437
515
|
try {
|
|
438
516
|
const nav = typeof navigator !== "undefined" ? navigator : undefined;
|
|
439
517
|
if (typeof nav?.sendBeacon === "function") {
|
|
@@ -517,3 +595,24 @@ export class Tracker {
|
|
|
517
595
|
}
|
|
518
596
|
}
|
|
519
597
|
}
|
|
598
|
+
|
|
599
|
+
function resolveProxyEventEndpoint(config: TrackerOptions["proxyEventDelivery"]): string | null {
|
|
600
|
+
if (!config?.enabled || typeof location === "undefined") return null;
|
|
601
|
+
const host = location.hostname.toLowerCase();
|
|
602
|
+
if (
|
|
603
|
+
host === "app.drip-apex.com"
|
|
604
|
+
|| host === "app-staging.drip-apex.com"
|
|
605
|
+
|| host === "commerce.drip-apex.com"
|
|
606
|
+
|| host === "preview.drip-apex.com"
|
|
607
|
+
|| host.endsWith(".workers.dev")
|
|
608
|
+
) return null;
|
|
609
|
+
const configuredPath = config.path?.trim();
|
|
610
|
+
const path = configuredPath && configuredPath.startsWith("/") && !configuredPath.startsWith("//")
|
|
611
|
+
? configuredPath
|
|
612
|
+
: "/apps/apex/events";
|
|
613
|
+
try {
|
|
614
|
+
return new URL(path, location.origin).toString();
|
|
615
|
+
} catch {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|