@drip-apex/sdk 0.1.0 → 0.3.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 +19283 -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 +19267 -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 +16636 -13533
- 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 +28 -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 +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { decideAssignments } from "./decide-assignments";
|
|
4
|
+
|
|
5
|
+
const experiment = {
|
|
6
|
+
id: "exp_edge_identity",
|
|
7
|
+
name: "Edge identity",
|
|
8
|
+
status: "running" as const,
|
|
9
|
+
trafficAllocation: 1,
|
|
10
|
+
variations: [
|
|
11
|
+
{ id: "var_a", weight: 0.5, mutations: [] },
|
|
12
|
+
{ id: "var_b", weight: 0.5, mutations: [] },
|
|
13
|
+
],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const hashAttributeExperiment = {
|
|
17
|
+
id: "exp_edge_attribute",
|
|
18
|
+
kind: "feature_flag" as const,
|
|
19
|
+
featureFlag: { key: "edge_attribute" },
|
|
20
|
+
name: "Edge attribute",
|
|
21
|
+
status: "running" as const,
|
|
22
|
+
hashAttribute: "account_id",
|
|
23
|
+
trafficAllocation: 1,
|
|
24
|
+
variations: [
|
|
25
|
+
{ id: "var_attribute_a", weight: 0.5, mutations: [] },
|
|
26
|
+
{
|
|
27
|
+
id: "var_attribute_b",
|
|
28
|
+
weight: 0.5,
|
|
29
|
+
mutations: [{
|
|
30
|
+
selector: "#edge-target",
|
|
31
|
+
action: "insertAfter" as const,
|
|
32
|
+
html: '<span data-testid="edge-insert">Edge choice</span>',
|
|
33
|
+
}],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const idempotentEdgeExperiment = {
|
|
39
|
+
id: "exp_edge_text",
|
|
40
|
+
name: "Edge text",
|
|
41
|
+
status: "running" as const,
|
|
42
|
+
trafficAllocation: 1,
|
|
43
|
+
variations: [
|
|
44
|
+
{ id: "var_text_a", weight: 0.5, mutations: [] },
|
|
45
|
+
{
|
|
46
|
+
id: "var_text_b",
|
|
47
|
+
weight: 0.5,
|
|
48
|
+
mutations: [{ selector: "#edge-text", action: "text" as const, text: "Edge choice" }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function variationFor(visitorId: string): string {
|
|
54
|
+
return decideAssignments({
|
|
55
|
+
experiments: [experiment],
|
|
56
|
+
visitorId,
|
|
57
|
+
now: 1,
|
|
58
|
+
url: location.href,
|
|
59
|
+
})[0].variationId;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function visitorFor(prefix: string, variationId: string): string {
|
|
63
|
+
const visitorId = Array.from({ length: 100 }, (_, index) => `${prefix}-${index}`)
|
|
64
|
+
.find((candidate) => variationFor(candidate) === variationId);
|
|
65
|
+
if (!visitorId) throw new Error(`Could not find ${variationId} visitor`);
|
|
66
|
+
return visitorId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function attributeFor(variationId: string): string {
|
|
70
|
+
const attribute = Array.from({ length: 100 }, (_, index) => `account-${index}`)
|
|
71
|
+
.find((candidate) => decideAssignments({
|
|
72
|
+
experiments: [hashAttributeExperiment],
|
|
73
|
+
visitorId: candidate,
|
|
74
|
+
now: 1,
|
|
75
|
+
url: location.href,
|
|
76
|
+
})[0]?.variationId === variationId);
|
|
77
|
+
if (!attribute) throw new Error(`Could not find ${variationId} attribute`);
|
|
78
|
+
return attribute;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function installCookieJar(initial: Record<string, string>) {
|
|
82
|
+
const cookies = new Map(Object.entries(initial));
|
|
83
|
+
const writes: string[] = [];
|
|
84
|
+
Object.defineProperty(document, "cookie", {
|
|
85
|
+
configurable: true,
|
|
86
|
+
get: () => Array.from(cookies, ([name, value]) => `${name}=${value}`).join("; "),
|
|
87
|
+
set: (raw: string) => {
|
|
88
|
+
writes.push(raw);
|
|
89
|
+
const [pair] = raw.split(";");
|
|
90
|
+
const separator = pair.indexOf("=");
|
|
91
|
+
if (separator < 0) return;
|
|
92
|
+
cookies.set(pair.slice(0, separator).trim(), pair.slice(separator + 1));
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
return { cookies, writes };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("edge visitor identity adoption", () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
vi.resetModules();
|
|
101
|
+
localStorage.clear();
|
|
102
|
+
sessionStorage.clear();
|
|
103
|
+
delete (globalThis as Record<string, unknown>).__drip;
|
|
104
|
+
delete (globalThis as Record<string, unknown>).__dripEdgeInit;
|
|
105
|
+
delete (globalThis as Record<string, unknown>).__dripEdgeAssignments;
|
|
106
|
+
history.replaceState({}, "", "/");
|
|
107
|
+
document.body.innerHTML = "";
|
|
108
|
+
globalThis.fetch = vi.fn(async () => new Response("ok", { status: 202 })) as typeof fetch;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(async () => {
|
|
112
|
+
await vi.dynamicImportSettled();
|
|
113
|
+
vi.restoreAllMocks();
|
|
114
|
+
delete (globalThis as Record<string, unknown>).__drip;
|
|
115
|
+
delete (globalThis as Record<string, unknown>).__dripEdgeInit;
|
|
116
|
+
delete (globalThis as Record<string, unknown>).__dripEdgeAssignments;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("adopts the edge drip_uid for an anonymous visitor and buckets with it", async () => {
|
|
120
|
+
const edgeVisitorId = visitorFor("edge", "var_a");
|
|
121
|
+
const staleVisitorId = visitorFor("stale", "var_b");
|
|
122
|
+
const cookieJar = installCookieJar({ drip_uid: edgeVisitorId });
|
|
123
|
+
localStorage.setItem("drip_uid", staleVisitorId);
|
|
124
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
125
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
126
|
+
experimentId: experiment.id,
|
|
127
|
+
variationId: variationFor(edgeVisitorId),
|
|
128
|
+
index: experiment.variations.findIndex((variation) => variation.id === variationFor(edgeVisitorId)),
|
|
129
|
+
}];
|
|
130
|
+
const { getRuntimeValue, init } = await import("./index");
|
|
131
|
+
|
|
132
|
+
const assignments = init({ antiFlickerMode: "off", experiments: [experiment] });
|
|
133
|
+
|
|
134
|
+
expect(getRuntimeValue("orderAttribution")).toMatchObject({ visitorId: edgeVisitorId });
|
|
135
|
+
expect(assignments).toEqual([
|
|
136
|
+
expect.objectContaining({ experimentId: experiment.id, variationId: variationFor(edgeVisitorId) }),
|
|
137
|
+
]);
|
|
138
|
+
expect(localStorage.getItem("drip_uid")).toBe(edgeVisitorId);
|
|
139
|
+
expect(cookieJar.cookies.get("drip_uid")).toBe(edgeVisitorId);
|
|
140
|
+
expect(cookieJar.writes.filter((write) => write.startsWith("drip_uid="))).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("uses an explicit config userId and ignores the edge cookie", async () => {
|
|
144
|
+
const explicitVisitorId = visitorFor("explicit", "var_a");
|
|
145
|
+
const edgeVisitorId = visitorFor("edge", "var_b");
|
|
146
|
+
const cookieJar = installCookieJar({ drip_uid: edgeVisitorId });
|
|
147
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
148
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
149
|
+
experimentId: experiment.id,
|
|
150
|
+
variationId: variationFor(edgeVisitorId),
|
|
151
|
+
index: experiment.variations.findIndex((variation) => variation.id === variationFor(edgeVisitorId)),
|
|
152
|
+
}];
|
|
153
|
+
const { getRuntimeValue, init } = await import("./index");
|
|
154
|
+
|
|
155
|
+
const assignments = init({
|
|
156
|
+
antiFlickerMode: "off",
|
|
157
|
+
experiments: [experiment],
|
|
158
|
+
userId: explicitVisitorId,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(getRuntimeValue("orderAttribution")).toMatchObject({ visitorId: explicitVisitorId });
|
|
162
|
+
expect(assignments).toEqual([
|
|
163
|
+
expect.objectContaining({ experimentId: experiment.id, variationId: variationFor(explicitVisitorId) }),
|
|
164
|
+
]);
|
|
165
|
+
expect(cookieJar.cookies.get("drip_uid")).toBe(edgeVisitorId);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("assigns an experiment that is absent from the edge assignment payload normally", async () => {
|
|
169
|
+
const edgeVisitorId = visitorFor("edge", "var_b");
|
|
170
|
+
const sdkAttribute = attributeFor("var_attribute_a");
|
|
171
|
+
installCookieJar({ drip_uid: edgeVisitorId });
|
|
172
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
173
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
174
|
+
experimentId: experiment.id,
|
|
175
|
+
variationId: "var_b",
|
|
176
|
+
index: 1,
|
|
177
|
+
}];
|
|
178
|
+
const { init } = await import("./index");
|
|
179
|
+
|
|
180
|
+
const assignments = init({
|
|
181
|
+
antiFlickerMode: "off",
|
|
182
|
+
attributes: { account_id: sdkAttribute },
|
|
183
|
+
experiments: [experiment, hashAttributeExperiment],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(assignments).toEqual(expect.arrayContaining([
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
experimentId: experiment.id,
|
|
189
|
+
variationId: "var_b",
|
|
190
|
+
index: 1,
|
|
191
|
+
}),
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
experimentId: hashAttributeExperiment.id,
|
|
194
|
+
variationId: "var_attribute_a",
|
|
195
|
+
index: 0,
|
|
196
|
+
}),
|
|
197
|
+
]));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("fails closed when the active edge assignment payload is malformed", async () => {
|
|
201
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
202
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
203
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = { malformed: true };
|
|
204
|
+
const { init } = await import("./index");
|
|
205
|
+
|
|
206
|
+
const assignments = init({ antiFlickerMode: "off", experiments: [experiment] });
|
|
207
|
+
|
|
208
|
+
expect(assignments).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("fails closed when an edge assignment does not match a real variation index", async () => {
|
|
212
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
213
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
214
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
215
|
+
experimentId: experiment.id,
|
|
216
|
+
variationId: "var_missing",
|
|
217
|
+
index: 0,
|
|
218
|
+
}];
|
|
219
|
+
const { init } = await import("./index");
|
|
220
|
+
|
|
221
|
+
const assignments = init({ antiFlickerMode: "off", experiments: [experiment] });
|
|
222
|
+
|
|
223
|
+
expect(assignments).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("applies an edge insert exactly once in the SDK", async () => {
|
|
227
|
+
const sdkAttribute = attributeFor("var_attribute_a");
|
|
228
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
229
|
+
document.body.innerHTML = '<button id="edge-target">Target</button>';
|
|
230
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
231
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
232
|
+
experimentId: hashAttributeExperiment.id,
|
|
233
|
+
variationId: "var_attribute_b",
|
|
234
|
+
index: 1,
|
|
235
|
+
}];
|
|
236
|
+
const { init } = await import("./index");
|
|
237
|
+
|
|
238
|
+
init({
|
|
239
|
+
antiFlickerMode: "off",
|
|
240
|
+
attributes: { account_id: sdkAttribute },
|
|
241
|
+
experiments: [hashAttributeExperiment],
|
|
242
|
+
spa: { activationMode: "manual" },
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(document.querySelectorAll('[data-testid="edge-insert"]')).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("retains an edge-adopted hashAttribute assignment across SPA re-evaluation and applies its insert exactly once", async () => {
|
|
249
|
+
const sdkAttribute = attributeFor("var_attribute_a");
|
|
250
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
251
|
+
document.body.innerHTML = '<button id="edge-target">Target</button>';
|
|
252
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
253
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
254
|
+
experimentId: hashAttributeExperiment.id,
|
|
255
|
+
variationId: "var_attribute_b",
|
|
256
|
+
index: 1,
|
|
257
|
+
}];
|
|
258
|
+
const { init, reInit } = await import("./index");
|
|
259
|
+
|
|
260
|
+
const assignments = init({
|
|
261
|
+
antiFlickerMode: "off",
|
|
262
|
+
attributes: { account_id: sdkAttribute },
|
|
263
|
+
experiments: [hashAttributeExperiment],
|
|
264
|
+
spa: { activationMode: "manual" },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
expect(decideAssignments({
|
|
268
|
+
experiments: [hashAttributeExperiment],
|
|
269
|
+
visitorId: sdkAttribute,
|
|
270
|
+
now: 1,
|
|
271
|
+
url: location.href,
|
|
272
|
+
})[0]?.variationId).toBe("var_attribute_a");
|
|
273
|
+
expect(assignments).toEqual([
|
|
274
|
+
expect.objectContaining({
|
|
275
|
+
experimentId: hashAttributeExperiment.id,
|
|
276
|
+
variationId: "var_attribute_b",
|
|
277
|
+
index: 1,
|
|
278
|
+
}),
|
|
279
|
+
]);
|
|
280
|
+
expect(document.querySelectorAll('[data-testid="edge-insert"]')).toHaveLength(1);
|
|
281
|
+
|
|
282
|
+
const spaAssignments = reInit(true);
|
|
283
|
+
|
|
284
|
+
expect(spaAssignments).toEqual([
|
|
285
|
+
expect.objectContaining({
|
|
286
|
+
experimentId: hashAttributeExperiment.id,
|
|
287
|
+
variationId: "var_attribute_b",
|
|
288
|
+
index: 1,
|
|
289
|
+
}),
|
|
290
|
+
]);
|
|
291
|
+
expect(document.querySelectorAll('[data-testid="edge-insert"]')).toHaveLength(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("re-applies an idempotent edge text mutation without changing the rendered result", async () => {
|
|
295
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
296
|
+
document.body.innerHTML = '<p id="edge-text">Edge choice</p>';
|
|
297
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
298
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
299
|
+
experimentId: idempotentEdgeExperiment.id,
|
|
300
|
+
variationId: "var_text_b",
|
|
301
|
+
index: 1,
|
|
302
|
+
}];
|
|
303
|
+
const { init } = await import("./index");
|
|
304
|
+
|
|
305
|
+
const assignments = init({
|
|
306
|
+
antiFlickerMode: "off",
|
|
307
|
+
experiments: [idempotentEdgeExperiment],
|
|
308
|
+
spa: { activationMode: "manual" },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(assignments).toEqual([
|
|
312
|
+
expect.objectContaining({
|
|
313
|
+
experimentId: idempotentEdgeExperiment.id,
|
|
314
|
+
variationId: "var_text_b",
|
|
315
|
+
index: 1,
|
|
316
|
+
}),
|
|
317
|
+
]);
|
|
318
|
+
expect(document.querySelectorAll("#edge-text")).toHaveLength(1);
|
|
319
|
+
expect(document.querySelector("#edge-text")?.textContent).toBe("Edge choice");
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("keeps an edge assignment ahead of a client fallback in the same exclusion group", async () => {
|
|
323
|
+
const fallbackExperiment = {
|
|
324
|
+
id: "exp_edge_fallback",
|
|
325
|
+
name: "Edge fallback conflict",
|
|
326
|
+
status: "running" as const,
|
|
327
|
+
trafficAllocation: 1,
|
|
328
|
+
variations: [{ id: "var_fallback", weight: 1, mutations: [] }],
|
|
329
|
+
};
|
|
330
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
331
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
332
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
333
|
+
experimentId: experiment.id,
|
|
334
|
+
variationId: "var_a",
|
|
335
|
+
index: 0,
|
|
336
|
+
}];
|
|
337
|
+
const { getDiagnostics, init } = await import("./index");
|
|
338
|
+
|
|
339
|
+
const assignments = init({
|
|
340
|
+
antiFlickerMode: "off",
|
|
341
|
+
experiments: [experiment, fallbackExperiment],
|
|
342
|
+
exclusionGroups: [{
|
|
343
|
+
id: "edge-group",
|
|
344
|
+
members: [
|
|
345
|
+
{ experimentId: experiment.id, share: 0.5 },
|
|
346
|
+
{ experimentId: fallbackExperiment.id, share: 0.5 },
|
|
347
|
+
],
|
|
348
|
+
}],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(assignments).toEqual([
|
|
352
|
+
expect.objectContaining({ experimentId: experiment.id, variationId: "var_a" }),
|
|
353
|
+
]);
|
|
354
|
+
expect(getDiagnostics().activation.experiments.find(
|
|
355
|
+
(diagnostic) => diagnostic.experimentId === fallbackExperiment.id,
|
|
356
|
+
)).toMatchObject({ status: "failed", reason: "exclusion_group_overlap_lost" });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("deactivates an edge assignment after an SPA route becomes ineligible", async () => {
|
|
360
|
+
const targetedExperiment = {
|
|
361
|
+
...hashAttributeExperiment,
|
|
362
|
+
id: "exp_edge_targeted",
|
|
363
|
+
targeting: {
|
|
364
|
+
url: [{ type: "simple" as const, pattern: "/eligible", include: true }],
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
history.replaceState({}, "", "/eligible");
|
|
368
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
369
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
370
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
371
|
+
experimentId: targetedExperiment.id,
|
|
372
|
+
variationId: "var_attribute_b",
|
|
373
|
+
index: 1,
|
|
374
|
+
}];
|
|
375
|
+
const { init, reInit } = await import("./index");
|
|
376
|
+
|
|
377
|
+
expect(init({
|
|
378
|
+
antiFlickerMode: "off",
|
|
379
|
+
experiments: [targetedExperiment],
|
|
380
|
+
spa: { activationMode: "manual" },
|
|
381
|
+
})).toEqual([
|
|
382
|
+
expect.objectContaining({ experimentId: targetedExperiment.id, pageActive: true }),
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
history.replaceState({}, "", "/ineligible");
|
|
386
|
+
const spaAssignments = reInit(true);
|
|
387
|
+
|
|
388
|
+
expect(spaAssignments).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("does not activate an edge assignment on an ineligible initial route", async () => {
|
|
392
|
+
const targetedExperiment = {
|
|
393
|
+
...hashAttributeExperiment,
|
|
394
|
+
id: "exp_edge_initial_targeted",
|
|
395
|
+
targeting: {
|
|
396
|
+
url: [{ type: "simple" as const, pattern: "/eligible", include: true }],
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
history.replaceState({}, "", "/ineligible");
|
|
400
|
+
installCookieJar({ drip_uid: "edge-visitor" });
|
|
401
|
+
document.body.innerHTML = '<button id="edge-target">Target</button>';
|
|
402
|
+
(globalThis as Record<string, unknown>).__dripEdgeInit = true;
|
|
403
|
+
(globalThis as Record<string, unknown>).__dripEdgeAssignments = [{
|
|
404
|
+
experimentId: targetedExperiment.id,
|
|
405
|
+
variationId: "var_attribute_b",
|
|
406
|
+
index: 1,
|
|
407
|
+
}];
|
|
408
|
+
const { init } = await import("./index");
|
|
409
|
+
|
|
410
|
+
const assignments = init({
|
|
411
|
+
antiFlickerMode: "off",
|
|
412
|
+
experiments: [targetedExperiment],
|
|
413
|
+
spa: { activationMode: "manual" },
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(assignments).toEqual([]);
|
|
417
|
+
expect(document.querySelectorAll('[data-testid="edge-insert"]')).toHaveLength(0);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("keeps existing identity precedence when the edge runtime is inactive", async () => {
|
|
421
|
+
const storedVisitorId = visitorFor("stored", "var_a");
|
|
422
|
+
const cookieVisitorId = visitorFor("cookie", "var_b");
|
|
423
|
+
installCookieJar({ drip_uid: cookieVisitorId });
|
|
424
|
+
localStorage.setItem("drip_uid", storedVisitorId);
|
|
425
|
+
const { getRuntimeValue, init } = await import("./index");
|
|
426
|
+
|
|
427
|
+
const assignments = init({ antiFlickerMode: "off", experiments: [experiment] });
|
|
428
|
+
|
|
429
|
+
expect(getRuntimeValue("orderAttribution")).toMatchObject({ visitorId: storedVisitorId });
|
|
430
|
+
expect(assignments).toEqual([
|
|
431
|
+
expect.objectContaining({ experimentId: experiment.id, variationId: variationFor(storedVisitorId) }),
|
|
432
|
+
]);
|
|
433
|
+
expect(localStorage.getItem("drip_uid")).toBe(storedVisitorId);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type EdgeRuntimeAssignment = {
|
|
2
|
+
experimentId: string;
|
|
3
|
+
variationId: string;
|
|
4
|
+
index: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type EdgeRuntimeGlobals = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export function edgeRuntimeActive(): boolean {
|
|
10
|
+
return (globalThis as EdgeRuntimeGlobals).__dripEdgeInit === true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads the edge worker's authoritative assignment set.
|
|
15
|
+
* The adoption evaluator validates each selected variation and index.
|
|
16
|
+
*/
|
|
17
|
+
export function readEdgeRuntimeAssignments(): Map<string, EdgeRuntimeAssignment> | null {
|
|
18
|
+
if (!edgeRuntimeActive()) return null;
|
|
19
|
+
const raw = (globalThis as EdgeRuntimeGlobals).__dripEdgeAssignments;
|
|
20
|
+
if (!Array.isArray(raw)) return new Map();
|
|
21
|
+
const assignments = new Map<string, EdgeRuntimeAssignment>();
|
|
22
|
+
for (const value of raw) {
|
|
23
|
+
if (!value || typeof value !== "object") return new Map();
|
|
24
|
+
const assignment = value as EdgeRuntimeAssignment;
|
|
25
|
+
const { experimentId } = assignment;
|
|
26
|
+
if (typeof experimentId !== "string") return new Map();
|
|
27
|
+
assignments.set(experimentId, assignment);
|
|
28
|
+
}
|
|
29
|
+
return assignments;
|
|
30
|
+
}
|
package/src/editor/api.test.ts
CHANGED
|
@@ -97,6 +97,115 @@ describe("EditorApi", () => {
|
|
|
97
97
|
);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
it("parses a saved-but-pending publication response", async () => {
|
|
101
|
+
const fetchMock = vi.fn(async () => ({
|
|
102
|
+
ok: true,
|
|
103
|
+
status: 200,
|
|
104
|
+
json: async () => ({
|
|
105
|
+
ok: true,
|
|
106
|
+
saved: true,
|
|
107
|
+
artifact_hash: "hash-2",
|
|
108
|
+
publication: {
|
|
109
|
+
status: "pending",
|
|
110
|
+
code: "storefront_publication_pending",
|
|
111
|
+
message: "Saved, but publication is pending.",
|
|
112
|
+
},
|
|
113
|
+
reviewStatus: "approved",
|
|
114
|
+
warningIssues: [{ message: "Saved, but publication is pending." }],
|
|
115
|
+
artifacts: { mutations: [], customCss: "", customJs: "" },
|
|
116
|
+
}),
|
|
117
|
+
})) as unknown as typeof fetch;
|
|
118
|
+
const api = new EditorApi("https://app.example", session, fetchMock);
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
api.saveArtifacts(
|
|
122
|
+
"variation-1",
|
|
123
|
+
{ mutations: [], customCss: "", customJs: "" },
|
|
124
|
+
"hash-1",
|
|
125
|
+
),
|
|
126
|
+
).resolves.toMatchObject({
|
|
127
|
+
outcome: {
|
|
128
|
+
saved: true,
|
|
129
|
+
artifactHash: "hash-2",
|
|
130
|
+
publication: { status: "pending" },
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("maps a legacy successful save response to an OK publication outcome", async () => {
|
|
136
|
+
const fetchMock = vi.fn(async () => ({
|
|
137
|
+
ok: true,
|
|
138
|
+
status: 200,
|
|
139
|
+
json: async () => ({
|
|
140
|
+
ok: true,
|
|
141
|
+
artifactHash: "legacy-hash-2",
|
|
142
|
+
reviewStatus: "approved",
|
|
143
|
+
artifacts: { mutations: [], customCss: "", customJs: "" },
|
|
144
|
+
}),
|
|
145
|
+
})) as unknown as typeof fetch;
|
|
146
|
+
const api = new EditorApi("https://app.example", session, fetchMock);
|
|
147
|
+
|
|
148
|
+
await expect(
|
|
149
|
+
api.saveArtifacts(
|
|
150
|
+
"variation-1",
|
|
151
|
+
{ mutations: [], customCss: "", customJs: "" },
|
|
152
|
+
"hash-1",
|
|
153
|
+
),
|
|
154
|
+
).resolves.toMatchObject({
|
|
155
|
+
artifactHash: "legacy-hash-2",
|
|
156
|
+
outcome: {
|
|
157
|
+
saved: true,
|
|
158
|
+
artifactHash: "legacy-hash-2",
|
|
159
|
+
publication: { status: "ok" },
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("retains a committed publication failure payload on a 502", async () => {
|
|
165
|
+
const fetchMock = vi.fn(async () => ({
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 502,
|
|
168
|
+
json: async () => ({
|
|
169
|
+
ok: false,
|
|
170
|
+
saved: true,
|
|
171
|
+
artifact_hash: "hash-2",
|
|
172
|
+
publication: {
|
|
173
|
+
status: "failed",
|
|
174
|
+
code: "storefront_publication_failed",
|
|
175
|
+
message: "Saved, but storefront publication failed.",
|
|
176
|
+
},
|
|
177
|
+
error: "Saved, but storefront publication failed.",
|
|
178
|
+
}),
|
|
179
|
+
})) as unknown as typeof fetch;
|
|
180
|
+
const api = new EditorApi("https://app.example", session, fetchMock);
|
|
181
|
+
|
|
182
|
+
await expect(
|
|
183
|
+
api.saveArtifacts(
|
|
184
|
+
"variation-1",
|
|
185
|
+
{ mutations: [], customCss: "", customJs: "" },
|
|
186
|
+
"hash-1",
|
|
187
|
+
),
|
|
188
|
+
).rejects.toMatchObject<Partial<EditorApiError>>({
|
|
189
|
+
status: 502,
|
|
190
|
+
payload: {
|
|
191
|
+
ok: false,
|
|
192
|
+
saved: true,
|
|
193
|
+
artifact_hash: "hash-2",
|
|
194
|
+
publication: {
|
|
195
|
+
status: "failed",
|
|
196
|
+
code: "storefront_publication_failed",
|
|
197
|
+
message: "Saved, but storefront publication failed.",
|
|
198
|
+
},
|
|
199
|
+
error: "Saved, but storefront publication failed.",
|
|
200
|
+
},
|
|
201
|
+
outcome: {
|
|
202
|
+
saved: true,
|
|
203
|
+
artifactHash: "hash-2",
|
|
204
|
+
publication: { status: "failed" },
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
100
209
|
it("rejects load responses without the required optimistic artifact hash", async () => {
|
|
101
210
|
const fetchMock = vi.fn(async () => ({
|
|
102
211
|
ok: true,
|
package/src/editor/api.ts
CHANGED
|
@@ -5,14 +5,21 @@ import type {
|
|
|
5
5
|
EditorBrowserCommand,
|
|
6
6
|
EditorVariation,
|
|
7
7
|
} from "./types";
|
|
8
|
+
import {
|
|
9
|
+
parseVariationSaveOutcome,
|
|
10
|
+
type VariationSaveOutcome,
|
|
11
|
+
} from "./publication-result";
|
|
8
12
|
|
|
9
13
|
export class EditorApiError extends Error {
|
|
14
|
+
readonly outcome: VariationSaveOutcome | null;
|
|
15
|
+
|
|
10
16
|
constructor(
|
|
11
17
|
message: string,
|
|
12
18
|
readonly status: number,
|
|
13
19
|
readonly payload?: Record<string, unknown> | null,
|
|
14
20
|
) {
|
|
15
21
|
super(message);
|
|
22
|
+
this.outcome = parseVariationSaveOutcome(payload);
|
|
16
23
|
}
|
|
17
24
|
}
|
|
18
25
|
|
|
@@ -108,6 +115,7 @@ export class EditorApi {
|
|
|
108
115
|
): Promise<{
|
|
109
116
|
artifactHash: string;
|
|
110
117
|
reviewStatus: string | null;
|
|
118
|
+
outcome: VariationSaveOutcome;
|
|
111
119
|
warningIssues: Array<{ code?: string; message?: string }>;
|
|
112
120
|
artifacts: EditorArtifacts;
|
|
113
121
|
}> {
|
|
@@ -118,12 +126,35 @@ export class EditorApi {
|
|
|
118
126
|
body: JSON.stringify({ ...artifacts, expectedPriorHash }),
|
|
119
127
|
},
|
|
120
128
|
);
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
const parsedOutcome = parseVariationSaveOutcome(payload);
|
|
130
|
+
const hasPublicationOutcomeFields =
|
|
131
|
+
Object.prototype.hasOwnProperty.call(payload, "saved") ||
|
|
132
|
+
Object.prototype.hasOwnProperty.call(payload, "artifact_hash") ||
|
|
133
|
+
Object.prototype.hasOwnProperty.call(payload, "publication");
|
|
134
|
+
const legacyArtifactHash =
|
|
135
|
+
typeof payload.artifactHash === "string" && payload.artifactHash.trim()
|
|
136
|
+
? payload.artifactHash
|
|
137
|
+
: null;
|
|
138
|
+
const outcome =
|
|
139
|
+
parsedOutcome ??
|
|
140
|
+
(!hasPublicationOutcomeFields
|
|
141
|
+
? {
|
|
142
|
+
saved: true,
|
|
143
|
+
artifactHash: legacyArtifactHash,
|
|
144
|
+
publication: {
|
|
145
|
+
status: "ok" as const,
|
|
146
|
+
code: "variation_saved",
|
|
147
|
+
message: "Saved",
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
: null);
|
|
151
|
+
if (!outcome?.saved || !outcome.artifactHash) {
|
|
152
|
+
throw new EditorApiError("Editor save response did not include a complete publication outcome", 500, payload);
|
|
123
153
|
}
|
|
124
154
|
return {
|
|
125
|
-
artifactHash:
|
|
155
|
+
artifactHash: outcome.artifactHash,
|
|
126
156
|
reviewStatus: typeof payload.reviewStatus === "string" ? payload.reviewStatus : null,
|
|
157
|
+
outcome,
|
|
127
158
|
warningIssues: Array.isArray(payload.warningIssues)
|
|
128
159
|
? payload.warningIssues as Array<{ code?: string; message?: string }>
|
|
129
160
|
: [],
|