@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.
Files changed (246) hide show
  1. package/README.md +5 -0
  2. package/dist/bundle-report.json +584 -226
  3. package/dist/define.cjs +123 -0
  4. package/dist/define.cjs.map +7 -0
  5. package/dist/define.mjs +102 -0
  6. package/dist/define.mjs.map +7 -0
  7. package/dist/drip.cjs +19283 -0
  8. package/dist/drip.cjs.map +7 -0
  9. package/dist/drip.debug.js +1 -1
  10. package/dist/drip.debug.js.map +1 -1
  11. package/dist/drip.heatmap.js +1 -1
  12. package/dist/drip.heatmap.js.map +1 -1
  13. package/dist/drip.js +1 -1
  14. package/dist/drip.js.map +1 -1
  15. package/dist/drip.live.js +1 -1
  16. package/dist/drip.live.js.map +1 -1
  17. package/dist/drip.mjs +19267 -0
  18. package/dist/drip.mjs.map +7 -0
  19. package/dist/drip.replay-recorder.js +2 -0
  20. package/dist/drip.replay-recorder.js.map +1 -0
  21. package/dist/editor.js +147 -142
  22. package/dist/editor.js.map +4 -4
  23. package/dist/flags.cjs +1788 -0
  24. package/dist/flags.cjs.map +7 -0
  25. package/dist/flags.mjs +1765 -0
  26. package/dist/flags.mjs.map +7 -0
  27. package/dist/index.mjs +12 -0
  28. package/dist/index.mjs.map +2 -2
  29. package/dist/metafiles/define.cjs.meta.json +28 -0
  30. package/dist/metafiles/define.mjs.meta.json +32 -0
  31. package/dist/metafiles/drip.cjs.meta.json +3193 -0
  32. package/dist/metafiles/drip.debug.meta.json +2859 -574
  33. package/dist/metafiles/drip.heatmap.meta.json +2732 -460
  34. package/dist/metafiles/drip.live.meta.json +2732 -460
  35. package/dist/metafiles/drip.meta.json +2859 -574
  36. package/dist/metafiles/drip.mjs.meta.json +3235 -0
  37. package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
  38. package/dist/metafiles/editor.meta.json +67 -27
  39. package/dist/metafiles/flags.cjs.meta.json +220 -0
  40. package/dist/metafiles/flags.mjs.meta.json +225 -0
  41. package/dist/metafiles/index.meta.json +2717 -491
  42. package/dist/metafiles/index.mjs.meta.json +14 -4
  43. package/dist/react/index.js +16636 -13533
  44. package/dist/react/index.js.map +4 -4
  45. package/dist/sri.json +20 -15
  46. package/dist/types/activation-runtime.d.ts +91 -0
  47. package/dist/types/analytics-runtime.d.ts +166 -0
  48. package/dist/types/antiflicker.d.ts +9 -3
  49. package/dist/types/assignment-cookie-sync.d.ts +31 -0
  50. package/dist/types/assignment-runtime.d.ts +257 -0
  51. package/dist/types/assignment-store.d.ts +4 -8
  52. package/dist/types/bucket.d.ts +1 -2
  53. package/dist/types/compact-store.d.ts +1 -0
  54. package/dist/types/config-runtime.d.ts +313 -0
  55. package/dist/types/consent-evaluation.d.ts +28 -0
  56. package/dist/types/css.d.ts +5 -3
  57. package/dist/types/debug-loader.d.ts +2 -0
  58. package/dist/types/decide-assignments.d.ts +31 -0
  59. package/dist/types/delivery-timing.d.ts +8 -0
  60. package/dist/types/early-interaction-capture.d.ts +79 -0
  61. package/dist/types/early-reassert.d.ts +32 -0
  62. package/dist/types/edge-runtime.d.ts +11 -0
  63. package/dist/types/editor/api.d.ts +3 -0
  64. package/dist/types/editor/controller.d.ts +3 -1
  65. package/dist/types/editor/publication-result.d.ts +11 -0
  66. package/dist/types/editor/types.d.ts +2 -1
  67. package/dist/types/editor/view-kit.d.ts +1 -1
  68. package/dist/types/endpoint.d.ts +8 -0
  69. package/dist/types/exclusion-groups.d.ts +4 -7
  70. package/dist/types/flags.d.ts +2 -0
  71. package/dist/types/goal-eligibility.d.ts +4 -0
  72. package/dist/types/goals.d.ts +10 -1
  73. package/dist/types/guards.d.ts +7 -7
  74. package/dist/types/handover.d.ts +12 -1
  75. package/dist/types/hash.d.ts +8 -4
  76. package/dist/types/hydration.d.ts +2 -1
  77. package/dist/types/index.d.ts +24 -128
  78. package/dist/types/init-test-helpers.d.ts +48 -0
  79. package/dist/types/inline-bootstrap.d.ts +26 -0
  80. package/dist/types/js.d.ts +3 -1
  81. package/dist/types/money.d.ts +12 -0
  82. package/dist/types/mutate.d.ts +45 -6
  83. package/dist/types/performance.d.ts +2 -0
  84. package/dist/types/persistence-decoders.d.ts +12 -0
  85. package/dist/types/public-api.d.ts +209 -0
  86. package/dist/types/qa-assistant-state.d.ts +34 -0
  87. package/dist/types/react/index.d.ts +28 -3
  88. package/dist/types/reassert-sentinel.d.ts +6 -0
  89. package/dist/types/redirect.d.ts +48 -0
  90. package/dist/types/replay-recorder-entry.d.ts +1 -0
  91. package/dist/types/replay-signal-state.d.ts +6 -0
  92. package/dist/types/replay-signals.d.ts +13 -0
  93. package/dist/types/replay.d.ts +110 -0
  94. package/dist/types/router.d.ts +12 -2
  95. package/dist/types/runtime-state.d.ts +166 -0
  96. package/dist/types/sanitize.d.ts +1 -0
  97. package/dist/types/shadow.d.ts +3 -0
  98. package/dist/types/shopify-cart.d.ts +25 -1
  99. package/dist/types/storefront-vitals.d.ts +24 -0
  100. package/dist/types/targeting-diagnostics.d.ts +1 -0
  101. package/dist/types/targeting.d.ts +16 -2
  102. package/dist/types/track.d.ts +15 -1
  103. package/dist/types/types.d.ts +115 -9
  104. package/dist/types/wait.d.ts +14 -1
  105. package/dist/types/weekday-schedule.d.ts +3 -0
  106. package/package.json +17 -8
  107. package/src/activation-runtime.ts +2737 -0
  108. package/src/analytics-runtime.ts +2175 -0
  109. package/src/antiflicker.test.ts +216 -44
  110. package/src/antiflicker.ts +103 -28
  111. package/src/artifact-concurrency.test.ts +1091 -0
  112. package/src/assignment-cookie-sync.test.ts +202 -0
  113. package/src/assignment-cookie-sync.ts +189 -0
  114. package/src/assignment-runtime.ts +3204 -0
  115. package/src/assignment-store.test.ts +56 -0
  116. package/src/assignment-store.ts +7 -38
  117. package/src/bucket.ts +3 -18
  118. package/src/commerce-analytics-lifecycle.test.ts +75 -15
  119. package/src/commerce-config-lifecycle.test.ts +81 -1
  120. package/src/commerce-price-preserve.test.ts +133 -0
  121. package/src/commerce-price.test.ts +97 -0
  122. package/src/commerce-price.ts +74 -20
  123. package/src/commerce-shipping.test.ts +53 -0
  124. package/src/commerce-shipping.ts +5 -11
  125. package/src/compact-store.ts +15 -0
  126. package/src/config-fetch.test.ts +480 -9
  127. package/src/config-runtime.ts +2071 -0
  128. package/src/consent-evaluation-borlabs.test.ts +41 -0
  129. package/src/consent-evaluation-cookiescript.test.ts +50 -0
  130. package/src/consent-evaluation.ts +151 -0
  131. package/src/css.test.ts +30 -0
  132. package/src/css.ts +120 -6
  133. package/src/cwv.test.ts +60 -77
  134. package/src/debug-loader.test.ts +214 -0
  135. package/src/debug-loader.ts +41 -0
  136. package/src/decide-assignments.test.ts +196 -0
  137. package/src/decide-assignments.ts +204 -0
  138. package/src/delivery-timing.test.ts +104 -0
  139. package/src/delivery-timing.ts +89 -0
  140. package/src/early-interaction-capture.test.ts +451 -0
  141. package/src/early-interaction-capture.ts +230 -0
  142. package/src/early-reassert.test.ts +207 -0
  143. package/src/early-reassert.ts +152 -0
  144. package/src/edge-adoption.test.ts +435 -0
  145. package/src/edge-runtime.ts +30 -0
  146. package/src/editor/api.test.ts +109 -0
  147. package/src/editor/api.ts +34 -3
  148. package/src/editor/controller.test.ts +297 -14
  149. package/src/editor/controller.ts +69 -5
  150. package/src/editor/index.test.ts +2 -0
  151. package/src/editor/index.ts +1 -0
  152. package/src/editor/publication-result.ts +58 -0
  153. package/src/editor/types.ts +10 -1
  154. package/src/editor/view-kit.ts +2 -1
  155. package/src/editor/view.test.ts +108 -0
  156. package/src/editor/view.ts +49 -7
  157. package/src/endpoint.test.ts +15 -0
  158. package/src/endpoint.ts +10 -0
  159. package/src/exclusion-groups.ts +8 -18
  160. package/src/flags.test.ts +88 -1
  161. package/src/flags.ts +29 -29
  162. package/src/global-runtime-reconciler.test.ts +271 -0
  163. package/src/goal-eligibility.test.ts +29 -0
  164. package/src/goal-eligibility.ts +17 -0
  165. package/src/goals.test.ts +144 -1
  166. package/src/goals.ts +97 -18
  167. package/src/guards.test.ts +99 -1
  168. package/src/guards.ts +34 -15
  169. package/src/handover-cache-order.test.ts +6 -3
  170. package/src/handover.test.ts +154 -8
  171. package/src/handover.ts +122 -12
  172. package/src/hash.ts +29 -54
  173. package/src/holdout.test.ts +26 -0
  174. package/src/hydration.test.ts +73 -2
  175. package/src/hydration.ts +18 -9
  176. package/src/index.ts +130 -7287
  177. package/src/init-analytics.test.ts +1694 -0
  178. package/src/init-anti-flicker.test.ts +1211 -0
  179. package/src/init-cart-attributes.test.ts +2003 -0
  180. package/src/init-config-resolution.test.ts +999 -0
  181. package/src/init-consent.test.ts +971 -0
  182. package/src/init-empty.test.ts +11 -5406
  183. package/src/init-integrations.test.ts +846 -0
  184. package/src/init-page-activation.test.ts +5064 -0
  185. package/src/init-qa-loading.test.ts +740 -0
  186. package/src/init-redirects.test.ts +2156 -0
  187. package/src/init-test-helpers.ts +287 -0
  188. package/src/inline-bootstrap.test.ts +184 -0
  189. package/src/inline-bootstrap.ts +73 -0
  190. package/src/js.test.ts +51 -1
  191. package/src/js.ts +86 -2
  192. package/src/money.test.ts +55 -1
  193. package/src/money.ts +104 -2
  194. package/src/money.web-parity.contract.test.ts +85 -0
  195. package/src/mutate.test.ts +474 -10
  196. package/src/mutate.ts +739 -230
  197. package/src/page-trigger.test.ts +218 -8
  198. package/src/performance.ts +10 -0
  199. package/src/persistence-decoders.ts +18 -0
  200. package/src/public-api.ts +1828 -0
  201. package/src/qa-assistant-state.test.ts +249 -3
  202. package/src/qa-assistant-state.ts +146 -6
  203. package/src/qa-assistant-view.test.ts +220 -2
  204. package/src/qa-assistant-view.ts +102 -5
  205. package/src/react/index.test.tsx +37 -0
  206. package/src/react/index.tsx +19 -3
  207. package/src/reassert-sentinel.test.ts +155 -1
  208. package/src/reassert-sentinel.ts +168 -8
  209. package/src/redirect.test.ts +541 -0
  210. package/src/redirect.ts +572 -0
  211. package/src/replay-recorder-entry.ts +115 -0
  212. package/src/replay-signal-state.ts +100 -0
  213. package/src/replay-signals.test.ts +249 -0
  214. package/src/replay-signals.ts +151 -0
  215. package/src/replay.test.ts +764 -0
  216. package/src/replay.ts +514 -0
  217. package/src/router.test.ts +61 -2
  218. package/src/router.ts +50 -12
  219. package/src/runtime-state.ts +507 -0
  220. package/src/runtime.test.ts +174 -7
  221. package/src/runtime.ts +88 -14
  222. package/src/sanitize.test.ts +34 -0
  223. package/src/sanitize.ts +23 -12
  224. package/src/shadow.test.ts +187 -0
  225. package/src/shadow.ts +99 -0
  226. package/src/shopify-cart.test.ts +733 -23
  227. package/src/shopify-cart.ts +402 -49
  228. package/src/storefront-vitals.test.ts +217 -0
  229. package/src/storefront-vitals.ts +206 -0
  230. package/src/surface-signals.ts +6 -0
  231. package/src/targeting-conformance.test.ts +9 -0
  232. package/src/targeting-diagnostics.ts +38 -8
  233. package/src/targeting-parity-corpus.json +559 -0
  234. package/src/targeting-parity-corpus.test.ts +18 -0
  235. package/src/targeting.detailed.test.ts +105 -0
  236. package/src/targeting.test.ts +211 -0
  237. package/src/targeting.ts +88 -207
  238. package/src/track.test.ts +147 -0
  239. package/src/track.ts +125 -26
  240. package/src/types.ts +120 -13
  241. package/src/wait.test.ts +23 -0
  242. package/src/wait.ts +129 -13
  243. package/src/weekday-delivery.test.ts +56 -0
  244. package/src/weekday-schedule-parity-corpus.json +92 -0
  245. package/src/weekday-schedule.test.ts +69 -0
  246. package/src/weekday-schedule.ts +20 -0
package/src/types.ts CHANGED
@@ -29,7 +29,7 @@ export interface UrlRule {
29
29
 
30
30
  export interface AttributeRule {
31
31
  attribute: string;
32
- operator: "equals" | "contains" | "exists" | "in";
32
+ operator: "equals" | "contains" | "exists" | "in" | "not_in";
33
33
  value?: string | string[];
34
34
  }
35
35
 
@@ -94,7 +94,6 @@ export interface PageTargetingRule {
94
94
  advancedCondition?: {
95
95
  code?: string;
96
96
  description?: string;
97
- pollOnRules?: boolean;
98
97
  } | null;
99
98
  trigger?: {
100
99
  mode?:
@@ -130,6 +129,8 @@ export interface Targeting {
130
129
  segmentRules?: SegmentTargetingRule[];
131
130
  lifecycle?: {
132
131
  mode?: "reset" | "deactivate" | "force_reset" | "persistent";
132
+ explicit?: boolean;
133
+ source?: "author" | "page";
133
134
  };
134
135
  }
135
136
 
@@ -144,11 +145,14 @@ export interface CaptureRule {
144
145
 
145
146
  export interface Goal {
146
147
  id: string;
148
+ name?: string;
147
149
  type: "click" | "pageview" | "custom" | "revenue";
148
150
  selector?: string;
149
151
  /** Case-insensitive text content match. When set, the clicked element's textContent must include this string. */
150
152
  textMatch?: string;
151
153
  urlPattern?: string;
154
+ /** Optional Page scope. The goal is active only while this Page matches. */
155
+ pageId?: string;
152
156
  /** Custom analytics event name that should count as this goal. */
153
157
  eventName?: string;
154
158
  countOnce?: boolean;
@@ -189,11 +193,21 @@ export interface SnippetDeliveryManifest {
189
193
  criticalMutationPlan?: SnippetCriticalMutationPlanItem[];
190
194
  }
191
195
 
196
+ export interface UrlBlock {
197
+ id: string;
198
+ name: string;
199
+ urlRules: UrlRule[];
200
+ mutations: Mutation[];
201
+ css?: string;
202
+ js?: string;
203
+ }
204
+
192
205
  export interface Variation {
193
206
  id: string;
194
207
  name?: string;
195
208
  weight: number;
196
209
  mutations: Mutation[];
210
+ urlBlocks?: UrlBlock[];
197
211
  value?: unknown;
198
212
  css?: string;
199
213
  js?: string;
@@ -203,6 +217,10 @@ export interface Variation {
203
217
  externalJsUrl?: string;
204
218
  harnessSession?: HarnessSessionInfo;
205
219
  isControl?: boolean;
220
+ /** Destination for redirect experiments; absent on the control variation. */
221
+ redirectUrl?: string;
222
+ /** Persistence provenance used by runtime safety policy. */
223
+ artifactSource?: string;
206
224
  }
207
225
 
208
226
  export interface HarnessSessionInfo {
@@ -218,7 +236,14 @@ export interface Experiment {
218
236
  kind?: "experiment" | "feature_flag" | "personalization";
219
237
  name?: string;
220
238
  status?: string;
239
+ runtime_disabled?: boolean | null;
240
+ environment_key?: string | null;
241
+ weekdaySchedule?: import("./weekday-schedule").WeekdaySchedule;
242
+ /** Delivery classification authored by Apex (`mutation` or `redirect`). */
243
+ experimentType?: "mutation" | "redirect";
221
244
  targeting?: Targeting;
245
+ /** Page definitions used only to decide whether page-scoped goals listen. */
246
+ goalPageRules?: PageTargetingRule[];
222
247
  trafficAllocation?: number;
223
248
  assignmentEpoch?: number;
224
249
  assignmentEpochStartedAt?: string | null;
@@ -232,6 +257,10 @@ export interface Experiment {
232
257
  moves?: boolean;
233
258
  sampleRate?: number;
234
259
  };
260
+ replayCapture?: {
261
+ enabled?: boolean;
262
+ sampleRate?: number;
263
+ };
235
264
  variations: Variation[];
236
265
  hashAttribute?: string;
237
266
  goals?: Goal[];
@@ -273,6 +302,10 @@ export interface Assignment {
273
302
  experimentId: string;
274
303
  experimentName?: string;
275
304
  variationId: string;
305
+ /** Whether this assignment may render and activate page-scoped behavior here. */
306
+ pageActive: boolean;
307
+ /** Identifies the selected per-URL artifact without retaining its content. */
308
+ activeUrlBlockId?: string;
276
309
  index: number;
277
310
  mutations: Mutation[];
278
311
  value?: unknown;
@@ -289,6 +322,8 @@ export interface Assignment {
289
322
  externalJsUrl?: string;
290
323
  harnessSession?: HarnessSessionInfo;
291
324
  isControl: boolean;
325
+ redirectUrl?: string;
326
+ artifactSource?: string;
292
327
  hashValue: string;
293
328
  assignmentEpoch?: number;
294
329
  }
@@ -296,16 +331,12 @@ export interface Assignment {
296
331
  export type ExecutionMode = "auto" | "edge" | "client";
297
332
  export type AntiFlickerMode = "scoped" | "full" | "off" | "custom";
298
333
  export type TrackingStart = "idle" | "immediate";
299
- export type CustomJsPolicy = "strict" | "hybrid" | "flex";
334
+ export type CustomJsPolicy = "strict" | "hybrid";
300
335
  export type PlatformHint = "auto" | "shopify" | "spa" | "shopware";
301
- export type RouterMode = "auto" | "history" | "navigation" | "none";
336
+ export type RouterMode = "auto" | "history" | "navigation" | "polling" | "none";
302
337
  export type ActivationMode = "auto" | "observer" | "manual";
303
338
  export type PageTriggerMode = "direct" | "url_change" | "dom_change" | "manual";
304
- export type PerfFeatureFlag =
305
- | "perf_platform_router_v1"
306
- | "perf_shopify_pixel_buffer_v1"
307
- | "perf_spa_incremental_reapply_v1"
308
- | "perf_empty_project_v1";
339
+ export type PerfFeatureFlag = "perf_empty_project_v1";
309
340
  export type PerfFeatureFlags = Partial<Record<PerfFeatureFlag, boolean>>;
310
341
 
311
342
  export interface ProjectPrerequisites {
@@ -315,7 +346,7 @@ export interface ProjectPrerequisites {
315
346
 
316
347
  export interface ConsentModeSettings {
317
348
  enabled?: boolean;
318
- provider?: "custom" | "onetrust" | "usercentrics" | "cookiebot" | "ccm19" | "pandectes";
349
+ provider?: "custom" | "onetrust" | "usercentrics" | "cookiebot" | "ccm19" | "pandectes" | "cookiescript" | "borlabs";
319
350
  getter?: string;
320
351
  requiredGroup?: string;
321
352
  }
@@ -325,6 +356,7 @@ export interface DebugSettings {
325
356
  restrictMode?: "off" | "query_param" | "user_ids" | "always";
326
357
  queryParam?: string;
327
358
  token?: string;
359
+ tokenRequired?: boolean;
328
360
  allowedUserIds?: string[];
329
361
  persist?: boolean;
330
362
  disableCache?: boolean;
@@ -340,12 +372,15 @@ export interface SpaSettings {
340
372
  domChangeTrigger?: boolean;
341
373
  debounceMs?: number;
342
374
  allowIframes?: boolean;
343
- applyTiming?: "immediate" | "after_hydration";
375
+ applyTiming?: "immediate" | "after_hydration" | "early_reassert";
344
376
  hydrationTimeoutMs?: number;
345
377
  }
346
378
 
347
379
  export interface PageTriggerSettings {
348
380
  defaultMode?: PageTriggerMode;
381
+ urlPollIntervalMs?: number;
382
+ rulePollWindowMs?: number;
383
+ rulePollIntervalMs?: number;
349
384
  }
350
385
 
351
386
  export interface ExternalTrackingSettings {
@@ -527,6 +562,27 @@ export interface ActivationDiagnostics {
527
562
  experiments: ExperimentActivationDiagnostic[];
528
563
  }
529
564
 
565
+ export type PageDeactivationMode =
566
+ | "reset"
567
+ | "deactivate"
568
+ | "force_reset"
569
+ | "persistent";
570
+
571
+ export interface PageActivationDiagnostic {
572
+ pageId: string;
573
+ pageName?: string;
574
+ active: boolean;
575
+ appliedArtifactCount: number;
576
+ alwaysEmbedInSnippet?: boolean;
577
+ lastTransitionMode?: PageDeactivationMode;
578
+ isolationFallback?: "whole_experiment";
579
+ isolationFallbackExperimentId?: string;
580
+ triggerDetector?: "history" | "polling" | "dom";
581
+ triggerPollAttempts?: number;
582
+ triggerOutcome?: "activated" | "condition_failed" | "window_expired";
583
+ triggerElapsedMs?: number;
584
+ }
585
+
530
586
  export interface SdkDiagnostics {
531
587
  /** Public diagnostics redact consent and tracking-gate internals unless SDK debug mode is enabled. */
532
588
  mode: "public" | "debug";
@@ -549,6 +605,7 @@ export interface SdkDiagnostics {
549
605
  personalizationStopAfterMatch?: boolean;
550
606
  value?: unknown;
551
607
  }>;
608
+ pageActivation: PageActivationDiagnostic[];
552
609
  experimentsLoaded: number;
553
610
  analyticsStarted: boolean;
554
611
  trackerStarted: boolean;
@@ -625,11 +682,20 @@ export interface SdkDiagnostics {
625
682
  mappedSignals: string[];
626
683
  gtagNormalized?: boolean;
627
684
  };
685
+ observerErrors: {
686
+ count: number;
687
+ last?: {
688
+ source: "route" | "data_layer" | "dom";
689
+ message: string;
690
+ at: number;
691
+ };
692
+ };
628
693
  lastShopifyAssignmentSync?: {
629
694
  at: number;
630
- status: "pending" | "sent" | "skipped" | "failed";
695
+ status: "pending" | "sent" | "cookie_fallback" | "skipped" | "failed";
631
696
  reason?: string;
632
697
  assignmentCount: number;
698
+ truncatedAssignmentCount?: number;
633
699
  };
634
700
  lastShopifyCartSync?: {
635
701
  at: number;
@@ -789,7 +855,27 @@ export interface CommerceOfferExperiment {
789
855
  >;
790
856
  }
791
857
 
858
+ export interface AggregateModeSettings {
859
+ enabled?: boolean;
860
+ constructions?: {
861
+ loggedInSegment?: boolean;
862
+ };
863
+ }
864
+
792
865
  export interface InitConfig {
866
+ /** QA-only diagnostics for forced experiments omitted from the served config. */
867
+ qa_warnings?: Array<{
868
+ code: "forced_experiment_unavailable" | "edge_device_audience_unsupported";
869
+ experiment_id: string;
870
+ reason?: "archived" | "runtime_disabled" | "not_found" | "environment_mismatch";
871
+ /** Named audience that the edge runtime cannot reproduce. */
872
+ audience_name?: string;
873
+ }>;
874
+ /** Global program holdout. Missing or malformed values fail open. */
875
+ holdout_v1_enabled?: boolean;
876
+ holdout_pct?: number;
877
+ /** ISO timestamp for the reporting epoch that produced the holdout cohort. */
878
+ holdout_config_updated_at?: string | null;
793
879
  /** Provide experiments inline OR use shopId to auto-fetch from API. */
794
880
  experiments?: Experiment[];
795
881
  /** Request-scoped edge geography used for targeting. */
@@ -806,18 +892,29 @@ export interface InitConfig {
806
892
  };
807
893
  /** Shop ID – when provided (without experiments), the SDK fetches running experiments from the API. */
808
894
  shopId?: string;
895
+ /** Canonical storefront origin used to scope host-agnostic redirect source rules. */
896
+ storeOrigin?: string;
897
+ /** Registered public storefront origins accepted for host-agnostic redirect source rules. */
898
+ storeOrigins?: string[];
809
899
  /** Base URL for event tracking & config API (e.g. "https://events.drip-apex.com"). */
810
900
  endpoint?: string;
901
+ /** Prefer same-origin Shopify App Proxy delivery, with direct-worker fallback. */
902
+ proxyEventDelivery?: { enabled: boolean; path?: string };
903
+ assetFallback?: boolean;
811
904
  /** Optional endpoint for signed storefront audit snapshots. Defaults to Apex production. */
812
905
  auditEndpoint?: string;
813
906
  /** Versioned editor bundle URL emitted by the Apex snippet. */
814
907
  editorBundleUrl?: string;
908
+ /** Versioned replay recorder URL emitted by the Apex snippet. */
909
+ replayRecorderUrl?: string;
815
910
  ingestShopId?: string;
816
911
  ingestSignature?: string;
817
912
  attributes?: Record<string, unknown>;
818
913
  userId?: string;
819
914
  url?: string;
820
915
  autoTrack?: boolean;
916
+ /** Per-page storefront Core Web Vitals sample rate. Defaults to 0.1. */
917
+ storefrontVitalsSampleRate?: number;
821
918
  /** Persist fetched config in localStorage for faster repeat loads. Default: true. */
822
919
  configCache?: boolean;
823
920
  /** Fresh cache window in ms for local config cache. Default: 300000 (5m). */
@@ -890,6 +987,14 @@ export interface InitConfig {
890
987
  semanticTracking?: SemanticTrackingSettings;
891
988
  /** Server-authoritative runtime kill switch state. */
892
989
  runtimeControl?: RuntimeControlSettings;
990
+ /** Write assignment sync to a shared-domain cookie when Shopify's analytics bus is unavailable. Default: true. */
991
+ assignmentCookieSync?: boolean;
992
+ /** Legal-gated server-side aggregate-mode constructions. Missing values are off. */
993
+ aggregateMode?: AggregateModeSettings;
994
+ /** Selects whether the SDK or an external integration owns Shopify cart attribute writes. */
995
+ cartAttributeWrite?: "sdk" | "external";
996
+ /** Selects which assignment labels an external cart integration can read. */
997
+ cartAttributeScope?: "consented" | "all";
893
998
  /** Snippet compiler delivery state for SDK performance telemetry. */
894
999
  deliveryState?:
895
1000
  | "no_visual_tests"
@@ -911,6 +1016,8 @@ export interface InitConfig {
911
1016
  | "shopify_app_embed"
912
1017
  | "legacy_shopify_redirect"
913
1018
  | string;
1019
+ /** Exact snippet URL used for a per-visitor debug-build handoff. */
1020
+ snippetUrl?: string;
914
1021
  /** Immutable SDK artifact version selected by the Worker. */
915
1022
  sdkVersion?: string;
916
1023
  /** Resolved SDK policy mode selected by the Worker. */
@@ -931,6 +1038,7 @@ export interface InitConfig {
931
1038
  qa?: boolean;
932
1039
  harness?: boolean;
933
1040
  audit?: boolean;
1041
+ debugLoader?: boolean;
934
1042
  };
935
1043
  /** Runtime kill-switches for staged rollout features. */
936
1044
  perfFlags?: PerfFeatureFlags;
@@ -963,7 +1071,6 @@ export interface InitConfig {
963
1071
  * Custom JS execution policy. Default: "hybrid".
964
1072
  * - "strict": no custom JS execution in production.
965
1073
  * - "hybrid": custom JS allowed with safety guards (sourceURL, error isolation).
966
- * - "flex": unrestricted custom JS (legacy behavior).
967
1074
  */
968
1075
  customJsPolicy?: CustomJsPolicy;
969
1076
 
package/src/wait.test.ts CHANGED
@@ -93,4 +93,27 @@ describe("waitForElement", () => {
93
93
  vi.advanceTimersByTime(1001);
94
94
  expect(onMiss).not.toHaveBeenCalled();
95
95
  });
96
+
97
+ it("does not apply a pending-root match after its final owner tears down", async () => {
98
+ const { resetWaitEntriesForOwner, waitForElement } = await import("./wait");
99
+ const callback = vi.fn();
100
+ const node = {
101
+ nodeType: 1,
102
+ matches: vi.fn(() => {
103
+ resetWaitEntriesForOwner("page-a");
104
+ return true;
105
+ }),
106
+ querySelectorAll: vi.fn(() => []),
107
+ } as unknown as Element;
108
+
109
+ waitForElement(".late-node", callback, { ownerKeys: ["page-a"] });
110
+ observerCallback?.(
111
+ [{ type: "childList", addedNodes: [node] } as unknown as MutationRecord],
112
+ {} as MutationObserver,
113
+ );
114
+
115
+ await Promise.resolve();
116
+ expect(callback).not.toHaveBeenCalled();
117
+ expect(disconnect).toHaveBeenCalled();
118
+ });
96
119
  });
package/src/wait.ts CHANGED
@@ -1,13 +1,22 @@
1
1
  import { Mutation } from "./types";
2
- import { applyTrackedMutation, isInsertAction, isInsertDuplicate, getMutationKey } from "./mutate";
2
+ import { applyTrackedMutation, getMutationKey, rejectTrackedMutation } from "./mutate";
3
+ import type { MutationJournalEntry } from "./mutate";
4
+ import { extractCurrencyTokens, type CurrencyToken } from "./money";
5
+ import { onShadowMutations, queryAllDeep } from "./shadow";
3
6
 
4
7
  type WaitEntry = {
8
+ seq: number;
9
+ artifactId: string;
10
+ epoch: number;
5
11
  selector: string;
6
12
  callback: (el: Element) => void;
7
13
  handled: WeakSet<Element>;
8
14
  matched: boolean;
9
15
  expiresAt: number;
10
16
  onMiss?: () => void;
17
+ ownerKeys: Set<string>;
18
+ requiresOwner: boolean;
19
+ state: "applied" | "invalidated";
11
20
  };
12
21
 
13
22
  const entries: WaitEntry[] = [];
@@ -16,6 +25,8 @@ let pending = false;
16
25
  const pendingRoots = new Set<ParentNode>();
17
26
  const DEFAULT_WAIT_TTL_MS = 30_000;
18
27
  let expiryTimer: ReturnType<typeof setTimeout> | null = null;
28
+ let waitSequence = 0;
29
+ let stopShadowListener: (() => void) | null = null;
19
30
 
20
31
  function isElement(node: Node): node is Element {
21
32
  return node.nodeType === Node.ELEMENT_NODE;
@@ -27,6 +38,7 @@ function hasQuerySelectorAll(node: ParentNode): node is ParentNode & { querySele
27
38
 
28
39
  function applyToEntry(entry: WaitEntry, element: Element): void {
29
40
  if (entry.expiresAt <= Date.now()) return;
41
+ if (entry.requiresOwner && entry.ownerKeys.size === 0) return;
30
42
  if (entry.handled.has(element)) return;
31
43
  entry.handled.add(element);
32
44
  entry.matched = true;
@@ -69,6 +81,8 @@ function pruneExpiredEntries(): void {
69
81
  pending = false;
70
82
  pendingRoots.clear();
71
83
  clearExpiryTimer();
84
+ stopShadowListener?.();
85
+ stopShadowListener = null;
72
86
  } else {
73
87
  scheduleExpiryPrune();
74
88
  }
@@ -116,7 +130,7 @@ function checkAllEntries(): void {
116
130
  const selectorMap = buildSelectorMap();
117
131
  for (const [selector, selectorEntries] of selectorMap) {
118
132
  try {
119
- const elements = document.querySelectorAll(selector);
133
+ const elements = queryAllDeep(selector);
120
134
  elements.forEach((el) => {
121
135
  selectorEntries.forEach((entry) => applyToEntry(entry, el));
122
136
  });
@@ -177,17 +191,34 @@ function ensureObserver(): void {
177
191
  childList: true,
178
192
  subtree: true,
179
193
  });
194
+ stopShadowListener ??= onShadowMutations((records) => {
195
+ for (const record of records) {
196
+ if (record.type === "childList") {
197
+ record.addedNodes.forEach((node) => pendingRoots.add(node as ParentNode));
198
+ } else {
199
+ pendingRoots.add(record.target as ParentNode);
200
+ }
201
+ }
202
+ if (pendingRoots.size > 0) debouncedCheck();
203
+ });
180
204
  }
181
205
 
182
206
  export function waitForElement(
183
207
  selector: string,
184
208
  callback: (el: Element) => void,
185
- options: { ttlMs?: number; onMiss?: () => void } = {},
209
+ options: {
210
+ ttlMs?: number;
211
+ onMiss?: () => void;
212
+ ownerKeys?: readonly string[];
213
+ ownerKeysSet?: Set<string>;
214
+ artifactId?: string;
215
+ epoch?: number;
216
+ } = {},
186
217
  ): void {
187
218
  const handled = new WeakSet<Element>();
188
219
  let matched = false;
189
220
  try {
190
- const existing = document.querySelectorAll(selector);
221
+ const existing = queryAllDeep(selector);
191
222
  existing.forEach((el) => {
192
223
  handled.add(el);
193
224
  matched = true;
@@ -198,7 +229,21 @@ export function waitForElement(
198
229
  return;
199
230
  }
200
231
  const ttlMs = Math.max(1_000, Math.min(options.ttlMs ?? DEFAULT_WAIT_TTL_MS, 120_000));
201
- entries.push({ selector, callback, handled, matched, expiresAt: Date.now() + ttlMs, onMiss: options.onMiss });
232
+ entries.push({
233
+ seq: ++waitSequence,
234
+ artifactId: options.artifactId ?? options.ownerKeys?.[0] ?? "wait:" + selector,
235
+ epoch: options.epoch ?? 0,
236
+ selector,
237
+ callback,
238
+ handled,
239
+ matched,
240
+ expiresAt: Date.now() + ttlMs,
241
+ onMiss: options.onMiss,
242
+ ownerKeys: options.ownerKeysSet ?? new Set(options.ownerKeys ?? []),
243
+ requiresOwner:
244
+ options.ownerKeysSet !== undefined || (options.ownerKeys?.length ?? 0) > 0,
245
+ state: "applied",
246
+ });
202
247
  scheduleExpiryPrune();
203
248
  ensureObserver();
204
249
  }
@@ -206,26 +251,97 @@ export function waitForElement(
206
251
  export function applyMutationWithWait(
207
252
  mutation: Mutation,
208
253
  index: number,
209
- onApplied?: ((mutation: Mutation, element: Element, index: number) => void) | null,
254
+ onApplied?: ((mutation: Mutation, element: Element, index: number, journalSeq?: number) => void) | null,
210
255
  onMiss?: ((mutation: Mutation, index: number) => void) | null,
256
+ ownerKeys: readonly string[] = [],
257
+ artifactId?: string,
258
+ epoch = 0,
259
+ pricePreservation?: {
260
+ enabled: boolean;
261
+ onViolation: (input: { selector: string; originalPriceTokens: CurrencyToken[] }) => void;
262
+ },
211
263
  ): void {
264
+ const mutableOwnerKeys = new Set(ownerKeys);
212
265
  const key = getMutationKey(mutation, index);
266
+ let resolved = false;
213
267
  waitForElement(mutation.selector, (element) => {
214
- if (isInsertAction(mutation.action)) {
215
- if (isInsertDuplicate(element, key)) {
216
- onApplied?.(mutation, element, index);
268
+ if (resolved) return;
269
+ const originalPrices = pricePreservation?.enabled &&
270
+ (mutation.action === "html" || mutation.action === "text" || mutation.action === "setText")
271
+ ? extractCurrencyTokens(element.textContent ?? "")
272
+ : [];
273
+ const originalHtml = originalPrices.length > 0 ? element.innerHTML : null;
274
+ let journalSeq: number | undefined;
275
+ let journalEntry: MutationJournalEntry | undefined;
276
+ applyTrackedMutation(
277
+ mutation,
278
+ element,
279
+ key,
280
+ Array.from(mutableOwnerKeys),
281
+ artifactId,
282
+ (entry) => { journalSeq = entry.seq; journalEntry = entry; },
283
+ epoch,
284
+ );
285
+ if (originalHtml !== null) {
286
+ const remaining = extractCurrencyTokens(element.textContent ?? "");
287
+ const preserved = originalPrices.every((token) => remaining.some((candidate) =>
288
+ candidate.symbol === token.symbol && candidate.cents === token.cents));
289
+ if (!preserved) {
290
+ rejectTrackedMutation(journalEntry, element, originalHtml);
291
+ pricePreservation?.onViolation({ selector: mutation.selector, originalPriceTokens: originalPrices });
292
+ resolved = true;
293
+ onMiss?.(mutation, index);
217
294
  return;
218
295
  }
219
- applyTrackedMutation(mutation, element, key);
220
- } else {
221
- applyTrackedMutation(mutation, element, key);
222
296
  }
223
- onApplied?.(mutation, element, index);
297
+ onApplied?.(mutation, element, index, journalSeq);
224
298
  }, {
225
299
  onMiss: () => onMiss?.(mutation, index),
300
+ ownerKeysSet: mutableOwnerKeys,
301
+ artifactId,
302
+ epoch,
226
303
  });
227
304
  }
228
305
 
306
+ export function addWaitEntryOwners(
307
+ existingOwnerKeys: ReadonlySet<string>,
308
+ ownerKeys: readonly string[],
309
+ artifactId?: string,
310
+ ): void {
311
+ if (existingOwnerKeys.size === 0 || ownerKeys.length === 0) return;
312
+ for (const entry of entries) {
313
+ if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
314
+ if (
315
+ !Array.from(existingOwnerKeys).some((ownerKey) =>
316
+ entry.ownerKeys.has(ownerKey)
317
+ )
318
+ ) continue;
319
+ ownerKeys.forEach((ownerKey) => entry.ownerKeys.add(ownerKey));
320
+ }
321
+ }
322
+
323
+ export function resetWaitEntriesForOwner(
324
+ ownerKey: string,
325
+ artifactId?: string,
326
+ ): void {
327
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
328
+ const entry = entries[i];
329
+ if (artifactId !== undefined && entry.artifactId !== artifactId) continue;
330
+ if (!entry.ownerKeys.has(ownerKey)) continue;
331
+ entry.ownerKeys.delete(ownerKey);
332
+ if (entry.ownerKeys.size === 0) entries.splice(i, 1);
333
+ }
334
+ if (entries.length > 0) {
335
+ scheduleExpiryPrune();
336
+ return;
337
+ }
338
+ pending = false;
339
+ pendingRoots.clear();
340
+ observer?.disconnect();
341
+ observer = null;
342
+ clearExpiryTimer();
343
+ }
344
+
229
345
  export function resetWaitEntries(): void {
230
346
  entries.length = 0;
231
347
  pending = false;
@@ -0,0 +1,56 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { __test } from "./index";
3
+ import type { Experiment } from "./types";
4
+
5
+ // Hermeticity: weekday tests pin the most recent UTC Monday at 12:00 instead
6
+ // of a literal calendar date.
7
+ const RECENT_MONDAY_NOON = (() => {
8
+ const d = new Date();
9
+ d.setUTCDate(d.getUTCDate() - ((d.getUTCDay() + 6) % 7));
10
+ d.setUTCHours(12, 0, 0, 0);
11
+ return d;
12
+ })();
13
+
14
+ const excludedExperiment: Experiment = {
15
+ id: "weekday",
16
+ name: "Weekday test",
17
+ status: "running",
18
+ weekdaySchedule: {
19
+ excludedDays: ["monday"],
20
+ timezone: "UTC",
21
+ },
22
+ trafficAllocation: 1,
23
+ variations: [{
24
+ id: "variation",
25
+ weight: 1,
26
+ mutations: [{ action: "text", selector: "h1", text: "Variant" }],
27
+ }],
28
+ };
29
+
30
+ describe("SDK weekday delivery sites", () => {
31
+ afterEach(() => vi.useRealTimers());
32
+
33
+ it("does not collect anti-flicker selectors on an excluded day", () => {
34
+ vi.useFakeTimers();
35
+ vi.setSystemTime(new Date(RECENT_MONDAY_NOON));
36
+ expect(__test.collectScopedSelectors([excludedExperiment])).toEqual([]);
37
+ });
38
+
39
+ it("does not build critical-path work on an excluded day", () => {
40
+ vi.useFakeTimers();
41
+ vi.setSystemTime(new Date(RECENT_MONDAY_NOON));
42
+ const result = __test.buildCriticalPathAssignments(
43
+ [excludedExperiment],
44
+ [{
45
+ e: "weekday",
46
+ v: [{ id: "variation", w: 1, m: [["h1", "text", "Variant"]] }],
47
+ }],
48
+ { url: "https://shop.test/", attributes: {}, userId: "visitor" },
49
+ );
50
+ expect(result.assignments).toEqual([]);
51
+ expect(result.skippedDiagnostics[0]).toMatchObject({
52
+ reason: "excluded_weekday",
53
+ gates: [{ id: "weekday", label: "Weekday schedule", status: "failed" }],
54
+ });
55
+ });
56
+ });