@drip-apex/sdk 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/_headers +3 -0
- package/dist/bundle-report.json +564 -0
- package/dist/drip.debug.js +2 -0
- package/dist/drip.debug.js.map +1 -0
- package/dist/drip.heatmap.js +2 -0
- package/dist/drip.heatmap.js.map +1 -0
- package/dist/drip.js +2 -0
- package/dist/drip.js.map +1 -0
- package/dist/drip.live.js +2 -0
- package/dist/drip.live.js.map +1 -0
- package/dist/editor.js +480 -0
- package/dist/editor.js.map +7 -0
- package/dist/index.mjs +645 -0
- package/dist/index.mjs.map +7 -0
- package/dist/metafiles/drip.debug.meta.json +973 -0
- package/dist/metafiles/drip.heatmap.meta.json +918 -0
- package/dist/metafiles/drip.live.meta.json +918 -0
- package/dist/metafiles/drip.meta.json +973 -0
- package/dist/metafiles/editor.meta.json +321 -0
- package/dist/metafiles/index.meta.json +1039 -0
- package/dist/metafiles/index.mjs.meta.json +78 -0
- package/dist/react/index.js +16973 -0
- package/dist/react/index.js.map +7 -0
- package/dist/sri.json +27 -0
- package/dist/types/analytics.d.ts +15 -0
- package/dist/types/antiflicker.d.ts +14 -0
- package/dist/types/assignment-store.d.ts +30 -0
- package/dist/types/audit.d.ts +9 -0
- package/dist/types/bucket.d.ts +2 -0
- package/dist/types/bucketing-cleanup.d.ts +5 -0
- package/dist/types/commerce-gift.d.ts +41 -0
- package/dist/types/commerce-price.d.ts +22 -0
- package/dist/types/commerce-shipping.d.ts +28 -0
- package/dist/types/css.d.ts +5 -0
- package/dist/types/cwv.d.ts +10 -0
- package/dist/types/data-layer-normalize.d.ts +1 -0
- package/dist/types/debug.d.ts +23 -0
- package/dist/types/define.d.ts +87 -0
- package/dist/types/devtools.d.ts +32 -0
- package/dist/types/dom-trigger.d.ts +3 -0
- package/dist/types/editor/api.d.ts +37 -0
- package/dist/types/editor/artifacts.d.ts +15 -0
- package/dist/types/editor/command-runtime.d.ts +48 -0
- package/dist/types/editor/controller.d.ts +97 -0
- package/dist/types/editor/device-preview.d.ts +23 -0
- package/dist/types/editor/index.d.ts +9 -0
- package/dist/types/editor/operator.d.ts +39 -0
- package/dist/types/editor/picker.d.ts +23 -0
- package/dist/types/editor/selector.d.ts +2 -0
- package/dist/types/editor/types.d.ts +55 -0
- package/dist/types/editor/view-kit.d.ts +33 -0
- package/dist/types/editor/view.d.ts +5 -0
- package/dist/types/editor-bootstrap.d.ts +37 -0
- package/dist/types/exclusion-groups.d.ts +59 -0
- package/dist/types/exposure-signals.d.ts +7 -0
- package/dist/types/flags.d.ts +65 -0
- package/dist/types/goals.d.ts +16 -0
- package/dist/types/guards.d.ts +39 -0
- package/dist/types/handover.d.ts +10 -0
- package/dist/types/hash.d.ts +19 -0
- package/dist/types/heatmap.d.ts +8 -0
- package/dist/types/hideflicker.d.ts +15 -0
- package/dist/types/hydration.d.ts +11 -0
- package/dist/types/index.d.ts +144 -0
- package/dist/types/js.d.ts +6 -0
- package/dist/types/module.d.ts +4 -0
- package/dist/types/money.d.ts +14 -0
- package/dist/types/mutate.d.ts +34 -0
- package/dist/types/observer-mute.d.ts +3 -0
- package/dist/types/pii-keys.d.ts +1 -0
- package/dist/types/prerequisites.d.ts +12 -0
- package/dist/types/qa-assistant-state.d.ts +68 -0
- package/dist/types/qa-assistant-view.d.ts +6 -0
- package/dist/types/qa-assistant.d.ts +9 -0
- package/dist/types/qa-toolbar.d.ts +19 -0
- package/dist/types/react/index.d.ts +84 -0
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/router.d.ts +9 -0
- package/dist/types/runtime-tools.d.ts +50 -0
- package/dist/types/runtime.d.ts +22 -0
- package/dist/types/safe-regex.d.ts +6 -0
- package/dist/types/sanitize.d.ts +5 -0
- package/dist/types/semantic.d.ts +7 -0
- package/dist/types/shopify-cart-events.d.ts +7 -0
- package/dist/types/shopify-cart.d.ts +62 -0
- package/dist/types/signals.d.ts +31 -0
- package/dist/types/surface-signals.d.ts +6 -0
- package/dist/types/targeting-diagnostics.d.ts +5 -0
- package/dist/types/targeting.d.ts +50 -0
- package/dist/types/test-fixtures/commerce.d.ts +12 -0
- package/dist/types/track.d.ts +79 -0
- package/dist/types/types.d.ts +859 -0
- package/dist/types/wait.d.ts +7 -0
- package/package.json +74 -0
- package/src/analytics.ts +146 -0
- package/src/antiflicker.test.ts +127 -0
- package/src/antiflicker.ts +147 -0
- package/src/assignment-store.test.ts +171 -0
- package/src/assignment-store.ts +208 -0
- package/src/audit.test.ts +17 -0
- package/src/audit.ts +362 -0
- package/src/bucket.test.ts +65 -0
- package/src/bucket.ts +18 -0
- package/src/bucketing-cleanup.test.ts +30 -0
- package/src/bucketing-cleanup.ts +72 -0
- package/src/bucketing-parity.test.ts +41 -0
- package/src/commerce-analytics-lifecycle.test.ts +1416 -0
- package/src/commerce-config-lifecycle.test.ts +274 -0
- package/src/commerce-epoch-lifecycle.test.ts +62 -0
- package/src/commerce-gift.test.ts +1293 -0
- package/src/commerce-gift.ts +979 -0
- package/src/commerce-offers-display.test.ts +154 -0
- package/src/commerce-price.test.ts +1145 -0
- package/src/commerce-price.ts +941 -0
- package/src/commerce-revocation-lifecycle.test.ts +78 -0
- package/src/commerce-shipping.test.ts +630 -0
- package/src/commerce-shipping.ts +447 -0
- package/src/config-fetch.test.ts +325 -0
- package/src/css.ts +54 -0
- package/src/cwv.test.ts +95 -0
- package/src/cwv.ts +129 -0
- package/src/data-layer-normalize.ts +21 -0
- package/src/debug.test.ts +83 -0
- package/src/debug.ts +138 -0
- package/src/define.test.ts +327 -0
- package/src/define.ts +208 -0
- package/src/devtools.ts +582 -0
- package/src/dom-trigger.test.ts +70 -0
- package/src/dom-trigger.ts +55 -0
- package/src/editor/api.test.ts +154 -0
- package/src/editor/api.ts +185 -0
- package/src/editor/artifacts.test.ts +57 -0
- package/src/editor/artifacts.ts +115 -0
- package/src/editor/command-runtime.test.ts +225 -0
- package/src/editor/command-runtime.ts +572 -0
- package/src/editor/controller.test.ts +371 -0
- package/src/editor/controller.ts +596 -0
- package/src/editor/device-preview.test.ts +60 -0
- package/src/editor/device-preview.ts +120 -0
- package/src/editor/index.test.ts +161 -0
- package/src/editor/index.ts +169 -0
- package/src/editor/operator.test.ts +148 -0
- package/src/editor/operator.ts +256 -0
- package/src/editor/picker.test.ts +58 -0
- package/src/editor/picker.ts +152 -0
- package/src/editor/selector.test.ts +44 -0
- package/src/editor/selector.ts +110 -0
- package/src/editor/types.ts +92 -0
- package/src/editor/view-kit.ts +162 -0
- package/src/editor/view.test.ts +535 -0
- package/src/editor/view.ts +1395 -0
- package/src/editor-bootstrap.test.ts +116 -0
- package/src/editor-bootstrap.ts +213 -0
- package/src/event-schema-contract.test.ts +35 -0
- package/src/exclusion-groups.test.ts +383 -0
- package/src/exclusion-groups.ts +270 -0
- package/src/exposure-signals.test.ts +182 -0
- package/src/exposure-signals.ts +110 -0
- package/src/flags.test.ts +322 -0
- package/src/flags.ts +201 -0
- package/src/goals.test.ts +42 -0
- package/src/goals.ts +200 -0
- package/src/guards.test.ts +249 -0
- package/src/guards.ts +250 -0
- package/src/handover-cache-order.test.ts +32 -0
- package/src/handover.test.ts +349 -0
- package/src/handover.ts +309 -0
- package/src/hash.ts +214 -0
- package/src/heatmap.test.ts +305 -0
- package/src/heatmap.ts +373 -0
- package/src/hideflicker.ts +21 -0
- package/src/hydration.test.ts +252 -0
- package/src/hydration.ts +203 -0
- package/src/index.ts +7513 -0
- package/src/init-empty.test.ts +5572 -0
- package/src/js.test.ts +26 -0
- package/src/js.ts +61 -0
- package/src/module.ts +50 -0
- package/src/money.test.ts +35 -0
- package/src/money.ts +53 -0
- package/src/mutate.test.ts +304 -0
- package/src/mutate.ts +469 -0
- package/src/observer-mute.test.ts +88 -0
- package/src/observer-mute.ts +45 -0
- package/src/page-trigger.test.ts +84 -0
- package/src/personalization.test.ts +68 -0
- package/src/pii-keys.ts +17 -0
- package/src/prerequisites.test.ts +39 -0
- package/src/prerequisites.ts +94 -0
- package/src/qa-assistant-state.test.ts +471 -0
- package/src/qa-assistant-state.ts +505 -0
- package/src/qa-assistant-view.test.ts +185 -0
- package/src/qa-assistant-view.ts +768 -0
- package/src/qa-assistant.ts +40 -0
- package/src/qa-toolbar.ts +444 -0
- package/src/react/index.test.tsx +177 -0
- package/src/react/index.tsx +252 -0
- package/src/reassert-sentinel.test.ts +314 -0
- package/src/reassert-sentinel.ts +191 -0
- package/src/router.test.ts +78 -0
- package/src/router.ts +92 -0
- package/src/runtime-tools.test.ts +112 -0
- package/src/runtime-tools.ts +403 -0
- package/src/runtime.test.ts +217 -0
- package/src/runtime.ts +268 -0
- package/src/safe-regex.ts +35 -0
- package/src/sanitize.ts +34 -0
- package/src/semantic.ts +245 -0
- package/src/shopify-cart-events.test.ts +76 -0
- package/src/shopify-cart-events.ts +115 -0
- package/src/shopify-cart.test.ts +2823 -0
- package/src/shopify-cart.ts +1256 -0
- package/src/signals.ts +226 -0
- package/src/surface-signals.test.ts +83 -0
- package/src/surface-signals.ts +165 -0
- package/src/targeting-diagnostics.ts +560 -0
- package/src/targeting.detailed.test.ts +443 -0
- package/src/targeting.test.ts +1054 -0
- package/src/targeting.ts +1024 -0
- package/src/test-fixtures/commerce.ts +65 -0
- package/src/track.test.ts +817 -0
- package/src/track.ts +519 -0
- package/src/types.ts +991 -0
- package/src/wait.test.ts +96 -0
- package/src/wait.ts +238 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
__test,
|
|
4
|
+
consumeEditorLaunchFragment,
|
|
5
|
+
interceptEditorLaunch,
|
|
6
|
+
isEditorDevicePreview,
|
|
7
|
+
resolveEditorBundleUrl,
|
|
8
|
+
} from "./editor-bootstrap";
|
|
9
|
+
|
|
10
|
+
function launchToken(overrides: Record<string, unknown> = {}): string {
|
|
11
|
+
const payload = {
|
|
12
|
+
aud: "apex-editor-exchange",
|
|
13
|
+
sid: "editor-session-1",
|
|
14
|
+
apiOrigin: "https://app.drip-apex.com",
|
|
15
|
+
...overrides,
|
|
16
|
+
};
|
|
17
|
+
return `${btoa(JSON.stringify(payload)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}.signature`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function launchLocation(token: string) {
|
|
21
|
+
return {
|
|
22
|
+
hash: `#section=details&apex-editor=${encodeURIComponent(token)}`,
|
|
23
|
+
href: `https://shop.example/products/one#section=details&apex-editor=${encodeURIComponent(token)}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
__test.resetState();
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("editor launch bootstrap", () => {
|
|
33
|
+
it("does no work when the editor fragment is absent", () => {
|
|
34
|
+
const replaceState = vi.fn();
|
|
35
|
+
const loadBundle = vi.fn();
|
|
36
|
+
const intercepted = interceptEditorLaunch({
|
|
37
|
+
locationLike: { hash: "#details", href: "https://shop.example/#details" },
|
|
38
|
+
historyLike: { state: null, replaceState },
|
|
39
|
+
scriptSrc: "https://events.drip-apex.com/s/shop.js",
|
|
40
|
+
loadBundle,
|
|
41
|
+
onFailure: vi.fn(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(intercepted).toBe(false);
|
|
45
|
+
expect(replaceState).not.toHaveBeenCalled();
|
|
46
|
+
expect(loadBundle).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("strips the token immediately and preserves unrelated fragment state", () => {
|
|
50
|
+
const token = launchToken();
|
|
51
|
+
const replaceState = vi.fn();
|
|
52
|
+
const launch = consumeEditorLaunchFragment(
|
|
53
|
+
launchLocation(token),
|
|
54
|
+
{ state: { page: 1 }, replaceState },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(launch).toEqual({
|
|
58
|
+
token,
|
|
59
|
+
sid: "editor-session-1",
|
|
60
|
+
apiOrigin: "https://app.drip-apex.com",
|
|
61
|
+
});
|
|
62
|
+
expect(replaceState).toHaveBeenCalledWith(
|
|
63
|
+
{ page: 1 },
|
|
64
|
+
"",
|
|
65
|
+
"https://shop.example/products/one#section=details",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("suppresses repeated initialization while exchange is pending and restores after failure", async () => {
|
|
70
|
+
const token = launchToken();
|
|
71
|
+
let rejectLoad!: (error: Error) => void;
|
|
72
|
+
const loadBundle = vi.fn(() => new Promise<never>((_resolve, reject) => {
|
|
73
|
+
rejectLoad = reject;
|
|
74
|
+
}));
|
|
75
|
+
const onFailure = vi.fn();
|
|
76
|
+
const common = {
|
|
77
|
+
scriptSrc: "https://events.drip-apex.com/s/shop.js",
|
|
78
|
+
pageUrl: "https://shop.example/products/one",
|
|
79
|
+
loadBundle,
|
|
80
|
+
onFailure,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
expect(interceptEditorLaunch({
|
|
84
|
+
...common,
|
|
85
|
+
locationLike: launchLocation(token),
|
|
86
|
+
historyLike: { state: null, replaceState: vi.fn() },
|
|
87
|
+
})).toBe(true);
|
|
88
|
+
expect(__test.getState()).toBe("starting");
|
|
89
|
+
expect(interceptEditorLaunch({
|
|
90
|
+
...common,
|
|
91
|
+
locationLike: { hash: "", href: "https://shop.example/products/one" },
|
|
92
|
+
historyLike: { state: null, replaceState: vi.fn() },
|
|
93
|
+
})).toBe(true);
|
|
94
|
+
|
|
95
|
+
rejectLoad(new Error("blocked by CSP"));
|
|
96
|
+
await vi.waitFor(() => expect(onFailure).toHaveBeenCalledOnce());
|
|
97
|
+
expect(__test.getState()).toBe("idle");
|
|
98
|
+
expect(interceptEditorLaunch({
|
|
99
|
+
...common,
|
|
100
|
+
locationLike: { hash: "", href: "https://shop.example/products/one" },
|
|
101
|
+
historyLike: { state: null, replaceState: vi.fn() },
|
|
102
|
+
})).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses the versioned sibling editor asset for versioned SDK scripts", () => {
|
|
106
|
+
expect(resolveEditorBundleUrl({
|
|
107
|
+
scriptSrc: "https://events.drip-apex.com/sdk/0.1.0%2Babc/drip.live.js",
|
|
108
|
+
pageUrl: "https://shop.example/",
|
|
109
|
+
})).toBe("https://events.drip-apex.com/sdk/0.1.0%2Babc/editor.js");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("identifies isolated device-preview frames", () => {
|
|
113
|
+
expect(isEditorDevicePreview({ name: "apex-editor-device-preview" } as Window)).toBe(true);
|
|
114
|
+
expect(isEditorDevicePreview({ name: "storefront" } as Window)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
export const EDITOR_FRAGMENT_KEY = "apex-editor";
|
|
2
|
+
export const EDITOR_BUNDLE_FILENAME = "editor.js";
|
|
3
|
+
export const EDITOR_DEVICE_FRAME_NAME = "apex-editor-device-preview";
|
|
4
|
+
const EDITOR_SCRIPT_ID = "drip-editor-bundle";
|
|
5
|
+
const EDITOR_LOAD_TIMEOUT_MS = 10_000;
|
|
6
|
+
|
|
7
|
+
export type EditorLaunchHint = {
|
|
8
|
+
token: string;
|
|
9
|
+
sid: string;
|
|
10
|
+
apiOrigin: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type LocationLike = Pick<Location, "hash" | "href">;
|
|
14
|
+
type HistoryLike = Pick<History, "replaceState" | "state">;
|
|
15
|
+
|
|
16
|
+
export type EditorBundleApi = {
|
|
17
|
+
start: (launch: EditorLaunchHint) => boolean | Promise<boolean>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type EditorGlobal = typeof globalThis & {
|
|
21
|
+
ApexEditor?: EditorBundleApi;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let editorState: "idle" | "starting" | "active" = "idle";
|
|
25
|
+
|
|
26
|
+
export function isEditorDevicePreview(windowLike: Pick<Window, "name"> = window): boolean {
|
|
27
|
+
return windowLike.name === EDITOR_DEVICE_FRAME_NAME;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function decodeBase64Url(value: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
33
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
34
|
+
return atob(padded);
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeApiOrigin(value: unknown): string | null {
|
|
41
|
+
if (typeof value !== "string" || !value.trim()) return null;
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(value);
|
|
44
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
|
|
45
|
+
return url.origin;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function decodeEditorLaunchToken(token: string): Omit<EditorLaunchHint, "token"> | null {
|
|
52
|
+
const encodedPayload = token.split(".", 1)[0];
|
|
53
|
+
if (!encodedPayload) return null;
|
|
54
|
+
const decoded = decodeBase64Url(encodedPayload);
|
|
55
|
+
if (!decoded) return null;
|
|
56
|
+
try {
|
|
57
|
+
const payload = JSON.parse(decoded) as Record<string, unknown>;
|
|
58
|
+
const sid = typeof payload.sid === "string" ? payload.sid.trim() : "";
|
|
59
|
+
const apiOrigin = normalizeApiOrigin(payload.apiOrigin ?? payload.api_origin);
|
|
60
|
+
if (payload.aud !== "apex-editor-exchange" || !sid || !apiOrigin) return null;
|
|
61
|
+
return { sid, apiOrigin };
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function consumeEditorLaunchFragment(
|
|
68
|
+
locationLike: LocationLike = location,
|
|
69
|
+
historyLike: HistoryLike = history,
|
|
70
|
+
): EditorLaunchHint | null {
|
|
71
|
+
if (!locationLike.hash || locationLike.hash.length < 2) return null;
|
|
72
|
+
const params = new URLSearchParams(locationLike.hash.slice(1));
|
|
73
|
+
const encodedToken = params.get(EDITOR_FRAGMENT_KEY);
|
|
74
|
+
if (!encodedToken) return null;
|
|
75
|
+
|
|
76
|
+
const token = encodedToken.trim();
|
|
77
|
+
const hint = decodeEditorLaunchToken(token);
|
|
78
|
+
params.delete(EDITOR_FRAGMENT_KEY);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const cleanUrl = new URL(locationLike.href);
|
|
82
|
+
const remainingHash = params.toString();
|
|
83
|
+
cleanUrl.hash = remainingHash ? `#${remainingHash}` : "";
|
|
84
|
+
historyLike.replaceState(historyLike.state ?? null, "", cleanUrl.toString());
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return hint ? { token, ...hint } : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveEditorBundleUrl(input: {
|
|
93
|
+
configuredUrl?: string;
|
|
94
|
+
scriptSrc?: string;
|
|
95
|
+
pageUrl?: string;
|
|
96
|
+
}): string | null {
|
|
97
|
+
const baseUrl = input.scriptSrc || input.pageUrl;
|
|
98
|
+
if (input.configuredUrl) {
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(input.configuredUrl, baseUrl);
|
|
101
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!input.scriptSrc) return null;
|
|
107
|
+
try {
|
|
108
|
+
const scriptUrl = new URL(input.scriptSrc, input.pageUrl);
|
|
109
|
+
if (/^\/sdk\/[^/]+\/[^/]+\.js$/.test(scriptUrl.pathname)) {
|
|
110
|
+
scriptUrl.pathname = scriptUrl.pathname.replace(/[^/]+\.js$/, EDITOR_BUNDLE_FILENAME);
|
|
111
|
+
} else if (/\/drip(?:\.live|\.heatmap|\.debug)?\.js$/.test(scriptUrl.pathname)) {
|
|
112
|
+
scriptUrl.pathname = scriptUrl.pathname.replace(/[^/]+\.js$/, EDITOR_BUNDLE_FILENAME);
|
|
113
|
+
} else {
|
|
114
|
+
scriptUrl.pathname = `/${EDITOR_BUNDLE_FILENAME}`;
|
|
115
|
+
}
|
|
116
|
+
scriptUrl.search = "";
|
|
117
|
+
scriptUrl.hash = "";
|
|
118
|
+
return scriptUrl.toString();
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function loadEditorBundle(
|
|
125
|
+
bundleUrl: string,
|
|
126
|
+
documentLike: Document = document,
|
|
127
|
+
timeoutMs = EDITOR_LOAD_TIMEOUT_MS,
|
|
128
|
+
): Promise<EditorBundleApi> {
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const existing = documentLike.getElementById(EDITOR_SCRIPT_ID);
|
|
131
|
+
existing?.remove();
|
|
132
|
+
|
|
133
|
+
const script = documentLike.createElement("script");
|
|
134
|
+
script.id = EDITOR_SCRIPT_ID;
|
|
135
|
+
script.src = bundleUrl;
|
|
136
|
+
script.async = true;
|
|
137
|
+
script.crossOrigin = "anonymous";
|
|
138
|
+
|
|
139
|
+
const timeout = setTimeout(() => {
|
|
140
|
+
script.remove();
|
|
141
|
+
reject(new Error("Editor bundle load timed out"));
|
|
142
|
+
}, timeoutMs);
|
|
143
|
+
const finish = (error?: Error) => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
if (error) {
|
|
146
|
+
script.remove();
|
|
147
|
+
reject(error);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const editorGlobal = globalThis as EditorGlobal;
|
|
151
|
+
const api = editorGlobal.ApexEditor;
|
|
152
|
+
try {
|
|
153
|
+
editorGlobal.ApexEditor = undefined;
|
|
154
|
+
} catch {}
|
|
155
|
+
if (!api || typeof api.start !== "function") {
|
|
156
|
+
reject(new Error("Editor bundle did not expose a start function"));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
resolve(api);
|
|
160
|
+
};
|
|
161
|
+
script.onload = () => finish();
|
|
162
|
+
script.onerror = () => finish(new Error("Editor bundle failed to load"));
|
|
163
|
+
(documentLike.head || documentLike.documentElement).appendChild(script);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function interceptEditorLaunch(input: {
|
|
168
|
+
configuredBundleUrl?: string;
|
|
169
|
+
scriptSrc?: string;
|
|
170
|
+
pageUrl?: string;
|
|
171
|
+
onBeforeLoad?: () => void;
|
|
172
|
+
onFailure: () => void;
|
|
173
|
+
loadBundle?: (url: string) => Promise<EditorBundleApi>;
|
|
174
|
+
locationLike?: LocationLike;
|
|
175
|
+
historyLike?: HistoryLike;
|
|
176
|
+
}): boolean {
|
|
177
|
+
if (editorState !== "idle") return true;
|
|
178
|
+
const launch = consumeEditorLaunchFragment(input.locationLike, input.historyLike);
|
|
179
|
+
if (!launch) return false;
|
|
180
|
+
|
|
181
|
+
editorState = "starting";
|
|
182
|
+
input.onBeforeLoad?.();
|
|
183
|
+
const bundleUrl = resolveEditorBundleUrl({
|
|
184
|
+
configuredUrl: input.configuredBundleUrl,
|
|
185
|
+
scriptSrc: input.scriptSrc,
|
|
186
|
+
pageUrl: input.pageUrl,
|
|
187
|
+
});
|
|
188
|
+
const fail = () => {
|
|
189
|
+
editorState = "idle";
|
|
190
|
+
input.onFailure();
|
|
191
|
+
};
|
|
192
|
+
if (!bundleUrl) {
|
|
193
|
+
Promise.resolve().then(fail);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const loader = input.loadBundle ?? ((url: string) => loadEditorBundle(url));
|
|
198
|
+
loader(bundleUrl)
|
|
199
|
+
.then((api) => api.start(launch))
|
|
200
|
+
.then((active) => {
|
|
201
|
+
if (active) editorState = "active";
|
|
202
|
+
else fail();
|
|
203
|
+
})
|
|
204
|
+
.catch(fail);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export const __test = {
|
|
209
|
+
getState: () => editorState,
|
|
210
|
+
resetState: () => {
|
|
211
|
+
editorState = "idle";
|
|
212
|
+
},
|
|
213
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
incomingEventSchema,
|
|
4
|
+
type IncomingEvent,
|
|
5
|
+
} from "@drip/event-schema";
|
|
6
|
+
import type { TrackEvent } from "./types";
|
|
7
|
+
|
|
8
|
+
describe("event schema contract", () => {
|
|
9
|
+
it("keeps SDK TrackEvent assignable to the shared incoming event contract", () => {
|
|
10
|
+
expectTypeOf<TrackEvent>().toMatchTypeOf<IncomingEvent>();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("parses a fully populated TrackEvent literal as a shared incoming event", () => {
|
|
14
|
+
const event: TrackEvent = {
|
|
15
|
+
type: "drip:goal.click-1",
|
|
16
|
+
data: {
|
|
17
|
+
revenue: 19.99,
|
|
18
|
+
goalId: "goal_123",
|
|
19
|
+
utmSource: "newsletter",
|
|
20
|
+
},
|
|
21
|
+
experimentId: "exp_123",
|
|
22
|
+
variationId: "var_123",
|
|
23
|
+
shopId: "shop_123",
|
|
24
|
+
visitorId: "visitor_123",
|
|
25
|
+
sessionId: "session_123",
|
|
26
|
+
url: "https://example.com/products/coffee",
|
|
27
|
+
ts: 1783170000000,
|
|
28
|
+
event_id: "evt_123",
|
|
29
|
+
qa_mode: true,
|
|
30
|
+
qa_session_id: "qa_123",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
expect(incomingEventSchema.parse(event)).toEqual(event);
|
|
34
|
+
});
|
|
35
|
+
});
|