@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
@@ -0,0 +1,2071 @@
1
+ declare const __DRIP_FEATURE_QA__: boolean;
2
+ declare const __DRIP_FEATURE_DEVTOOLS__: boolean;
3
+ declare const __DRIP_FEATURE_AUDIT__: boolean;
4
+ declare const __DRIP_FEATURE_HEATMAP__: boolean;
5
+ declare const __DRIP_FEATURE_REPLAY__: boolean;
6
+ declare const __DRIP_FEATURE_HARNESS__: boolean;
7
+ const FEATURE_QA = __DRIP_FEATURE_QA__;
8
+ const FEATURE_DEVTOOLS = __DRIP_FEATURE_DEVTOOLS__;
9
+ const FEATURE_AUDIT = __DRIP_FEATURE_AUDIT__;
10
+ const FEATURE_HEATMAP = __DRIP_FEATURE_HEATMAP__;
11
+ const FEATURE_REPLAY = __DRIP_FEATURE_REPLAY__;
12
+ const FEATURE_HARNESS = __DRIP_FEATURE_HARNESS__;
13
+ import { cleanUrl, onPageEnter, onPageExit } from "./analytics";
14
+ import { normalizeEndpoint } from "./endpoint";
15
+ import { EVENT_TYPE_PATTERN, normalizeEventName } from "@drip/event-schema/event-name";
16
+ import { isExperimentServable } from "@drip/event-schema/experiment-servability";
17
+ import {
18
+ applyAntiFlicker,
19
+ bootedTooLateForAntiFlicker,
20
+ removeAntiFlicker,
21
+ } from "./antiflicker";
22
+ import {
23
+ clearPersistedAssignments,
24
+ findPersistedVariation,
25
+ persistAssignmentForExperiment,
26
+ prunePersistedAssignments,
27
+ removePersistedAssignmentsForExperiments,
28
+ __assignmentStoreInternals,
29
+ } from "./assignment-store";
30
+ import {
31
+ clearAssignmentSyncCookie,
32
+ } from "./assignment-cookie-sync";
33
+ import { cleanupBucketingStorage } from "./bucketing-cleanup";
34
+ import { chooseVariation, inTrafficAllocation } from "./bucket";
35
+ import { buildAssignment, decideAssignments } from "./decide-assignments";
36
+ import { weekdayScheduleBlocksDelivery } from "./weekday-schedule";
37
+ import { CONSENT_REQUIRED, DNT_BLOCKED, evaluateSynchronousConsent, readConfiguredConsent, readPath, resolveSynchronousConsentGranted } from "./consent-evaluation";
38
+ import { clearEarlyInteractions, drainBootstrapEarlyInteractions, drainEarlyInteractions } from "./early-interaction-capture";
39
+ import { getCwvMetrics, markCwvReveal, startCwvMeasurement, waitForCwvMetrics } from "./cwv";
40
+ import { collectDeliveryTiming } from "./delivery-timing";
41
+ import {
42
+ startStorefrontVitals,
43
+ stopStorefrontVitals,
44
+ type StorefrontVitalsContext,
45
+ } from "./storefront-vitals";
46
+ import {
47
+ browserGroupStickyStore,
48
+ clearBrowserGroupStickyStore,
49
+ resolveExclusionGroups,
50
+ type ExclusionGroupDecision,
51
+ type PersistedGroupSelection,
52
+ } from "./exclusion-groups";
53
+ import type { CompactGroupSelections } from "./persistence-decoders";
54
+ import {
55
+ applyCommercePrice,
56
+ isCommercePriceWorkPending,
57
+ cleanupCommercePrice,
58
+ hasCommercePriceState,
59
+ COMMERCE_PRICE_SELECTORS,
60
+ isCommercePriceActive,
61
+ isCommercePriceExposureEligible,
62
+ notifyCommerceCartStampConfirmed,
63
+ setCommercePriceExposureListener,
64
+ } from "./commerce-price";
65
+ import {
66
+ applyCommerceShipping,
67
+ classifyShippingExposure,
68
+ cleanupCommerceShipping,
69
+ isShippingCartEligible,
70
+ isShippingCartEligibilityConfirmed,
71
+ } from "./commerce-shipping";
72
+ import {
73
+ addCssOwners,
74
+ injectCss,
75
+ injectExperimentCss,
76
+ injectExternalCss,
77
+ removeCssForOwner,
78
+ } from "./css";
79
+ import { configureDebug, debugLog, debugWarn, debugGroup, debugGroupEnd, isDebugEnabled } from "./debug";
80
+ import { loadDebugBuild } from "./debug-loader";
81
+ import { observeDomChanges } from "./dom-trigger";
82
+ import { edgeRuntimeActive, readEdgeRuntimeAssignments } from "./edge-runtime";
83
+ import { normalizeDataLayerEntry } from "./data-layer-normalize";
84
+ import { interceptEditorLaunch, isEditorDevicePreview } from "./editor-bootstrap";
85
+ import { cleanupGoals, goalWiringKeys, setupGoals, triggerGoal, triggerGoalsForEvent } from "./goals";
86
+ import { clearQaForceFromUrl, isBot, isDuplicate, isIframe, isOptedOut, isQaMode, isScreenshotMode, parseForceParam, persistQaForce, persistUrlQaForce, exitQaMode, getQaSessionId } from "./guards";
87
+ import { adoptUserId, applyHoldoutGate, configureIdentityStorage, getEphemeralUserId, getPersistedUserId, getUserId, hashToFloat, isHoldoutVisitor, variationHashToFloat } from "./hash";
88
+ import { createBucketingHandoverPayload, handoverMintTimeoutMs, hasBucketingHandover, installBucketingHandover, readBucketingHandover } from "./handover";
89
+ import { revealAll } from "./hideflicker";
90
+ import {
91
+ addExternalJsOwners,
92
+ execJs,
93
+ loadExternalJs,
94
+ removeExternalJsForOwner,
95
+ setJsPolicy,
96
+ } from "./js";
97
+ import {
98
+ addAppliedMutationOwners,
99
+ applyMutations,
100
+ applyToElement,
101
+ invalidateArtifact,
102
+ reassertNonStructuralMutations,
103
+ replayArtifact,
104
+ resetAppliedMutationsForOwner,
105
+ } from "./mutate";
106
+ import { registerMutedObserver, runMuted, unregisterMutedObserver } from "./observer-mute";
107
+ import { assignmentsHaveVisualWork, awaitHydration, hasMutationHold, holdMutations, releaseMutations, signalHydrated } from "./hydration";
108
+ import {
109
+ assignmentsHaveCustomJs,
110
+ assignmentsHaveStructuralMutations,
111
+ installEarlyReassert,
112
+ shouldUseEarlyReassert,
113
+ } from "./early-reassert";
114
+ import { trackAppliedMutation } from "./reassert-sentinel";
115
+ import { observeRouteChanges, PAGE_URL_POLL_INTERVAL_MS } from "./router";
116
+ import { buildExposureSignalContext } from "./exposure-signals";
117
+ import {
118
+ buildRedirectDestination,
119
+ clearPendingRedirectAssignmentCarry,
120
+ finalizeRedirectAssignmentCarryHandover,
121
+ hasGeneratedRedirectAssignmentCarry,
122
+ filterUnsafeRedirectAssignments,
123
+ importRedirectAssignmentCarry,
124
+ isRedirectLoop,
125
+ markRedirectAssignmentCarryExposed,
126
+ parseRedirectAssignmentCarry,
127
+ persistPendingRedirectAssignmentCarry,
128
+ readPendingRedirectAssignmentCarry,
129
+ REDIRECT_ASSIGNMENT_PARAM,
130
+ redirectPlanForAssignment,
131
+ stripRedirectAssignmentCarry,
132
+ validateRedirectAssignmentCarry,
133
+ verifyRedirectAssignmentCarry,
134
+ } from "./redirect";
135
+ import type { VerifiedRedirectCarry } from "./redirect";
136
+ import { buildPrerequisiteContext, evaluateProjectPrerequisite } from "./prerequisites";
137
+ import {
138
+ REPLAY_PROGRESS_KEY_PREFIX,
139
+ REPLAY_SAMPLE_KEY_PREFIX,
140
+ resolveReplayRecorderUrl,
141
+ startReplayCapture,
142
+ type ReplayController,
143
+ } from "./replay";
144
+ import {
145
+ buildCartAttributeEntries,
146
+ buildCartMetafieldInputs,
147
+ buildSignedCartAttributeEntries,
148
+ APEX_CART_ATTRIBUTE_KEYS,
149
+ cannotRequestRequiredCommerceAuthorization,
150
+ getCartMetafieldStaleKeys as collectCartMetafieldStaleKeys,
151
+ invalidateShopifyCartAttributeSync,
152
+ normalizeCartMetafieldNamespace,
153
+ syncShopifyCartAttributes,
154
+ type CartAssignment,
155
+ type CartAttributeEntry,
156
+ type CartMetafieldInput,
157
+ type CartAttributeSyncInput,
158
+ } from "./shopify-cart";
159
+ import { onShopifyCartMutation } from "./shopify-cart-events";
160
+ import {
161
+ getPendingGiftChoices,
162
+ noteCartSnapshotForGifts,
163
+ selectGiftOption,
164
+ syncGiftWithPurchase,
165
+ type GiftSyncOptions,
166
+ } from "./commerce-gift";
167
+ import { CART_DRAWER_OPEN_SIGNAL, installSurfaceSignals } from "./surface-signals";
168
+ import {
169
+ assignmentSignature,
170
+ canUseEmptyProjectProfile,
171
+ computeUrlDiffNeeds,
172
+ getBlankScreenMs,
173
+ readPerfFlags,
174
+ resolveActivationMode,
175
+ resolvePlatform,
176
+ resolveRouterMode,
177
+ shouldReevaluateForUrlChange,
178
+ } from "./runtime";
179
+ import type { PerfFlagState } from "./runtime";
180
+ import {
181
+ buildTargetingAttributes,
182
+ evaluateTargetingGatesDetailed,
183
+ matchPageTargetingRule,
184
+ readShopifySignal,
185
+ targetingHasJsTriggers,
186
+ } from "./targeting";
187
+ import { createApexRuntimeTools, type ApexRuntimeTools } from "./runtime-tools";
188
+ import { createEventId, Tracker } from "./track";
189
+ import {
190
+ clearStoredSignals,
191
+ configureSignalPersistenceAllowed,
192
+ clearRuntimeSignal,
193
+ getSignals as getStoredSignals,
194
+ getSignalDiagnostics,
195
+ rememberProduct,
196
+ setCartSignals,
197
+ setRuntimeSignal,
198
+ setSearchSignal,
199
+ setSignal as setStoredSignal,
200
+ } from "./signals";
201
+ import {
202
+ evaluateFlag as evaluateFlagFromExperiments,
203
+ evaluateFlagAssignment as evaluateFlagAssignmentFromExperiments,
204
+ evaluateFlagValue as evaluateFlagValueFromExperiments,
205
+ } from "./flags";
206
+ import {
207
+ ActivationMode,
208
+ AntiFlickerMode,
209
+ Assignment,
210
+ ActivationDiagnostics,
211
+ ActivationGateDiagnostic,
212
+ ConditionDiagnostic,
213
+ ConfigProfile,
214
+ ConsentModeSettings,
215
+ CustomJsPolicy,
216
+ DebugSettings,
217
+ ExecutionMode,
218
+ Experiment,
219
+ ExperimentActivationDiagnostic,
220
+ ExclusionGroupConfig,
221
+ ExclusionGroupMemberConfig,
222
+ Goal,
223
+ InitConfig,
224
+ PageTargetingRule,
225
+ PageActivationDiagnostic,
226
+ PageDeactivationMode,
227
+ PageTriggerMode,
228
+ PageTriggerSettings,
229
+ PerfFeatureFlag,
230
+ PerfFeatureFlags,
231
+ PlatformHint,
232
+ ProjectPrerequisites,
233
+ RouterMode,
234
+ SegmentCondition,
235
+ SpaSettings,
236
+ Targeting,
237
+ ExternalTrackingSettings,
238
+ ProjectCodeSettings,
239
+ RuntimeEnvironmentSettings,
240
+ RuntimeSignalDefinition,
241
+ SdkDiagnostics,
242
+ SnippetCriticalMutation,
243
+ SnippetCriticalMutationPlanItem,
244
+ SnippetDeliveryUrlRule,
245
+ BucketingCleanupSettings,
246
+ DataProtectionSettings,
247
+ DataLayerSignalMapping,
248
+ DataLayerTriggerSettings,
249
+ ExposureDestinationsSettings,
250
+ Mutation,
251
+ TrackEvent,
252
+ TrackingStart,
253
+ Variation,
254
+ } from "./types";
255
+ import {
256
+ addWaitEntryOwners,
257
+ applyMutationWithWait,
258
+ resetWaitEntriesForOwner,
259
+ } from "./wait";import { OrderIdentityPlatform, SDKState, QaAssistantRuntimeSnapshot, PageActivationRuntimeState, PageRulePollController, state, runtimeSharedState } from "./runtime-state";
260
+ import { scheduleDeferredRouteReevaluation, installDataLayerTriggers, experimentsUseSurfaceSignals, rememberExposureDestination, clarityUpgradeSent, clarityTagKey, pushExposureToQueue, emitExposureDestinations, ensureTrackerStarted, exposureAssignmentKey, priceAssignmentAuthorizationKey, isPriceDisplayAssignmentAuthorized, isShippingDisplayAssignmentAuthorized, isOffersDisplayAssignmentAuthorized, giftSyncOptions, assignmentHasRenderableArtifacts, authorizeOffersDisplayAndRender, revokeOffersDisplayAuthorization, exposureEligibleAssignments, isShippingExposureCartEligible, trackViewedAssignmentsAfterShippingEligibility, EMPTY_TARGETING_ATTRIBUTES, TargetingEvaluationPass, activeTargetingEvaluationPass, createTargetingEvaluationPass, targetingAttributesForPass, trackViewedAssignments, sameStringSet, setupGoalTracking, setupHeatmapTracking, cleanupHeatmapTracking, setupReplayTracking, cleanupReplayTracking, setupSemanticTracking, cleanupSemanticTracking, buildExperimentBehaviorEvents, buildPageExitEvents, analyticsTrack, syncLoadDiagnosticsToPerf, updateLoadDiagnostics, resetLoadDiagnostics, queuePerfEvent, RuntimeInstallClaim, RUNTIME_INSTALL_CLAIM_KEY, RUNTIME_INSTALL_CONFLICT_KEY, telemetryLabel, readRuntimeInstallClaim, rememberRuntimeInstallClaim, claimInstallConflictEmission, sdkAttributionData, sendRuntimeEventNow, sendRuntimePerfNow, readDripPerf, markDripPerf, numberPerf, coexistencePerfFields, buildPerfEvent, flushPendingPerf, orderIdentityEndpoint, ORDER_EXPOSURE_STORAGE_KEY, ORDER_EXPOSURE_MAX_ENTRIES, ORDER_EXPOSURE_TTL_MS, StoredOrderExposure, pruneStoredOrderExposures, readStoredOrderExposures, writeStoredOrderExposures, clearIdentityContextCookie, recentIdentityContextExposures, buildIdentityContextCookieValue, generateSessionId, getIdentityContextSessionId, writeIdentityContextCookie, isOffTargetPersistedAssignment, restoreRecordedExposureDedup, rememberOrderExposure, mergedOrderIdentityAssignments, cleanOrderIdentityText, resolveOrderIdentityPlatform, detectOrderIdentity, orderIdentityDedupeKey, scheduleOrderIdentityBeacon, deliveryStatus, queueDeliveryHealthEvent, flushPendingDeliveryHealth, startAnalyticsStage, scheduleAnalyticsStart, startAnalyticsIfReady, getConfigCacheKey, storageAvailable, getConfigCacheTtlMs, configuredStoreOrigins, readCachedExperiments, hashConfigPayload, writeCachedExperiments, withRuntimeSettings, serializeForceMap, getHashId } from "./analytics-runtime";
261
+ import { PersistedAssignmentMap, GLOBAL_PAGE_PREFIX, appliedAssignmentsByKey, ArtifactId, invalidatedArtifactIds, artifactEpochs, applyPassEpoch, artifactEpochCurrent, PageActivationTransition, normalizePageDeactivationMode, effectiveDeactivationMode, includedPageRules, currentPageMatchContext, pageActivationDiagnosticRows, reconcilePageActivation, cancelPageRulePoll, cancelAllPageRulePolls, pageRulePollSettings, deactivationModeForTriggeredRule, reconcileTriggeredPageMatch, startPageRulePoll, firePageDetector, activePageIds, experimentHasActivePage, syncAssignmentPageActive, assignmentPageIds, stableArtifactJson, artifactContentHash, baseArtifactApplicationKey, artifactApplicationKey, artifactIdFromApplicationKey, assignmentRuntimeKeys, artifactOwnerId, redirectCarryAssignmentKey, shouldPreserveOffTargetRedirectAssignment, isAssignmentArtifactInactive, renderable, exposable, attributable, recordAttributableAssignments, artifactRenderableAssignments, activeRenderableAssignments, chargeAffectingAssignments, loadPersistedAssignments, buildPersistedAssignmentResult, withAssignmentEpochData, usesStickyAssignmentPersistence, persistActiveExperimentAssignments, experimentDiagnostic, evaluateExperimentDetailed, evaluateExperiment, stoppedPersonalizationDiagnostic, enforcePersonalizationStopAfterMatch, excludedByGroupDiagnostic, keptByGroupDiagnostic, applyExclusionAndPersonalization, evaluate, evaluateWithDiagnostics, evaluateEdgeRuntimeAssignments, resolveCombinedEdgeEvaluation, escapeDeliveryRegex, matchSimpleDeliveryPattern, matchRegexDeliveryPattern, matchesDeliveryUrlRules, expandCriticalMutation, buildCriticalPathAssignments, runPostRevealReassert, schedulePostRevealReassert, revealExperimentContent, applyAssignmentCode, recordAppliedAssignment, appliedOwnerKeysForAssignment, applyAssignments, reassertAssignmentMutations, TeardownPage, TeardownRuntimeOptions, TeardownSelection, runAssignmentResetJs, pendingCommercePriceResolve, resolveCommercePricePending, unwireGoals, goalWiringEligible, reconcileGoals, reconcileHarnessPolling, reconcileCommercePrice, reconcileCommerceShipping, reconcileCartSync, syncGlobalRuntimes, configuredPageDeactivationModes, allAppliedPagesWithModes, rollbackArtifactOwner, unregisterArtifactOwner, executeTeardownTransaction, teardownTransaction, teardownArtifactsTransaction, DesiredArtifact, LiveArtifact, ArtifactReconcileResult, desiredArtifactSet, liveArtifactInventory, removeArtifactOwner, registerDesiredOwners, replayDesiredArtifacts, invalidateLiveArtifactsForReplay, reconcileArtifacts } from "./assignment-runtime";
262
+ import { generateQaSessionId, SHOPIFY_ASSIGNMENT_SYNC_DELAYS_MS, shopifyAssignmentSyncTimers, CommerceOnlyAdoptionState, commercePriceAdoptionState, ensureCommercePriceAdoptionState, currentCommercePriceContext, withCommercePriceAssignments, clearShopifyAssignmentSyncTimers, adoptAuthoritativePriceAssignments, OffersAdoptionPlan, planAuthoritativeOffersAdoption, canAdoptAuthoritativeOffersAssignments, adoptAuthoritativeOffersAssignments, sdkCartMintAllowed, buildSdkCartMintInput, buildScopedCartAttributeSyncInput, buildActiveCartAttributeSyncInput, sdkCartMintSync, syncSdkCartMint, removeSdkCartMintTrigger, onSdkCartMintClick, installSdkCartMintTrigger, HeadlessCartAuthorizationState, headlessCartAuthorizationState, ensureHeadlessCartAuthorizationState, hasChargeAffectingCommerceAssignment, hasActiveAuthorizationPlane, hasActivePriceAuthorizationPlane, hasActiveShippingAuthorizationPlane, hasActiveOffersAuthorizationPlane, hasActiveCommerceAuthorizationPlane, requiresCommerceCohortPin, getChargeAffectingCommerceExperimentIds, isCohortStampedOnCart, syncActiveAssignmentToShopifyCart, configureChargeCorrectnessCartSync, holdoutConfigEpoch, publishAssignmentsToShopify, scheduleShopifyAssignmentSync, variationsAllowed, updateProjectActivationDiagnostics, trackingAllowed, experimentById, targetingLifecycleMode, preservePersistentAssignments, stopAnalyticsStage, canMintVerifiedRedirectCarry, planRedirectAssignment, navigateForRedirectAssignment, RECONCILE_MAX_DRAINS, reevaluateAssignments, reevaluateAssignmentsPass, reapplyQaAssistantForceMap, reconcileForcedUnavailable, __getQaAssistantRuntimeSnapshot, __subscribeQaAssistantRuntime, __forceQaAssistantVariation, __resetQaAssistantForce, clearQaForceFromCurrentUrl, initWithExperiments } from "./activation-runtime";
263
+ import { loadQaToolbar, loadQaAssistant, loadDevtoolsIfEnabled, loadDevtools, init, trackEvent, track, funnelStepStorageKey, shouldTrackFunnelStep, trackStep, trackGoal, setConsent, hold, release, hydrated, recheckPrerequisites, reInit, RevenueEventData, normalizeRevenueCurrency, trackRevenue, flush, getAssignments, buildCurrentCartAttributes, getCartAttributes, getCartMetafields, getCartMetafieldStaleKeys, mergeCartAttributes, normalizedHeadlessCartToken, getSignedCartAttributes, confirmCartAttributesWritten, getExperiments, getGoals, getActiveExperimentIds, getRuntimeValue, getRuntimeTools, buildFlagContext, evaluateFlag, evaluateFlagAssignment, evaluateFlagValue, getDiagnostics, QueueCommand, processCommand, processQueue, installedGlobalApi, createGlobalApi, installGlobalApi } from "./public-api";
264
+
265
+
266
+ export function applyRuntimeAntiFlicker(
267
+ mode: AntiFlickerMode,
268
+ timeoutMs: number,
269
+ scopedSelectors?: string[],
270
+ customCss?: string,
271
+ ): boolean {
272
+ const bootedTooLate = runtimeSharedState.runtimeBootedTooLateForAntiFlicker ?? false;
273
+ const armed = applyAntiFlicker(mode, timeoutMs, scopedSelectors, customCss, bootedTooLate);
274
+ if (bootedTooLate && mode !== "off") {
275
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog(
276
+ "init",
277
+ "Anti-flicker skipped because the runtime loaded after the pre-paint window",
278
+ );
279
+ }
280
+ return armed;
281
+ }
282
+
283
+ export const SDK_SCRIPT_SRC =
284
+ typeof document !== "undefined"
285
+ ? (document.currentScript as HTMLScriptElement | null)?.src
286
+ : undefined;
287
+
288
+ export type HeatmapController = {
289
+ stop: () => void;
290
+ update: (assignments: Assignment[], experiments: Experiment[]) => void;
291
+ };
292
+
293
+ export type SemanticController = {
294
+ stop: () => void;
295
+ };
296
+
297
+ export type FetchAttemptInfo = {
298
+ attempt: number;
299
+ elapsedMs: number;
300
+ error?: string;
301
+ status?: number;
302
+ timedOut?: boolean;
303
+ };
304
+
305
+ export type FetchRetryOptions = {
306
+ timeoutMs?: number;
307
+ firstAttemptTimeoutMs?: number;
308
+ slowMs?: number;
309
+ onAttemptStart?: (attempt: number) => void;
310
+ onAttemptEnd?: (info: FetchAttemptInfo) => void;
311
+ onSlow?: (info: { attempt: number; elapsedMs: number }) => void;
312
+ };
313
+
314
+ export const DEFAULT_CONFIG_FETCH_TIMEOUT_MS = 4500;
315
+ export const DEFAULT_CONFIG_FETCH_SLOW_MS = 1500;
316
+ export const CONFIG_FETCH_RETRY_COUNT = 2;
317
+ export const CONFIG_FETCH_RETRY_DELAY_MS = 500;
318
+ export const CONFIG_FETCH_MAX_WAIT_MS =
319
+ DEFAULT_CONFIG_FETCH_TIMEOUT_MS * (CONFIG_FETCH_RETRY_COUNT + 1)
320
+ + CONFIG_FETCH_RETRY_DELAY_MS
321
+ * (CONFIG_FETCH_RETRY_COUNT * (CONFIG_FETCH_RETRY_COUNT + 1) / 2);
322
+ export // Keep the fail-open guard through the last abort/catch microtask. Without
323
+ // this small settle allowance, the hard reveal timer can run before the final
324
+ // fetch timeout confirms the cached redirect.
325
+ const CACHED_REDIRECT_HIDE_TIMEOUT_MS = CONFIG_FETCH_MAX_WAIT_MS + 100;
326
+
327
+ export function createFetchTimeoutError(): Error | DOMException {
328
+ if (typeof DOMException !== "undefined") {
329
+ return new DOMException("The operation was aborted.", "AbortError");
330
+ }
331
+ const error = new Error("The operation was aborted.");
332
+ error.name = "AbortError";
333
+ return error;
334
+ }
335
+
336
+ export function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
337
+ if (typeof AbortController === "undefined") {
338
+ let timer: ReturnType<typeof setTimeout> | null = null;
339
+ return Promise.race([
340
+ fetch(url),
341
+ new Promise<Response>((_, reject) => {
342
+ timer = setTimeout(() => reject(createFetchTimeoutError()), timeoutMs);
343
+ }),
344
+ ]).finally(() => {
345
+ if (timer !== null) clearTimeout(timer);
346
+ });
347
+ }
348
+ const controller = new AbortController();
349
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
350
+ return fetch(url, { signal: controller.signal }).finally(() => {
351
+ clearTimeout(timer);
352
+ });
353
+ }
354
+
355
+ export async function fetchWithRetry(
356
+ url: string,
357
+ retries = 2,
358
+ delayMs = 500,
359
+ options: FetchRetryOptions = {},
360
+ ): Promise<Response> {
361
+ const timeoutMs = normalizeNumber(options.timeoutMs, DEFAULT_CONFIG_FETCH_TIMEOUT_MS);
362
+ const firstAttemptTimeoutMs = normalizeNumber(options.firstAttemptTimeoutMs, timeoutMs);
363
+ for (let attempt = 0; attempt <= retries; attempt++) {
364
+ const startedAt = Date.now();
365
+ let slowTimer: ReturnType<typeof setTimeout> | null = null;
366
+ let slowMarked = false;
367
+ options.onAttemptStart?.(attempt + 1);
368
+ if (options.onSlow) {
369
+ const slowMs = normalizeNumber(options.slowMs, DEFAULT_CONFIG_FETCH_SLOW_MS);
370
+ slowTimer = setTimeout(() => {
371
+ slowMarked = true;
372
+ options.onSlow?.({ attempt: attempt + 1, elapsedMs: Date.now() - startedAt });
373
+ }, slowMs);
374
+ }
375
+ try {
376
+ const attemptTimeoutMs = attempt === 0 ? firstAttemptTimeoutMs : timeoutMs;
377
+ const res = await fetchWithTimeout(url, attemptTimeoutMs);
378
+ if (slowTimer !== null) clearTimeout(slowTimer);
379
+ options.onAttemptEnd?.({
380
+ attempt: attempt + 1,
381
+ elapsedMs: Date.now() - startedAt,
382
+ status: res.status,
383
+ });
384
+ if (res.ok || attempt === retries) return res;
385
+ } catch (err) {
386
+ if (slowTimer !== null) clearTimeout(slowTimer);
387
+ const error = err instanceof Error ? err.message : String(err);
388
+ const timedOut = typeof DOMException !== "undefined" && err instanceof DOMException
389
+ ? err.name === "AbortError"
390
+ : /abort|timeout/i.test(error);
391
+ options.onAttemptEnd?.({
392
+ attempt: attempt + 1,
393
+ elapsedMs: Date.now() - startedAt,
394
+ error,
395
+ timedOut,
396
+ });
397
+ if (attempt === retries) throw err;
398
+ }
399
+ if (slowTimer !== null && !slowMarked) clearTimeout(slowTimer);
400
+ await new Promise((r) => setTimeout(r, delayMs * (attempt + 1)));
401
+ }
402
+ throw new Error("Failed to fetch after retries");
403
+ }
404
+
405
+ export const CONFIG_CACHE_KEY_PREFIX = "drip_cfg:";
406
+ export const DEFAULT_CONFIG_CACHE_TTL_MS = 5 * 60 * 1000;
407
+ export const DEFAULT_ANTI_FLICKER_TIMEOUT_MS = 1200;
408
+ export const DEFAULT_TRACKING_IDLE_TIMEOUT_MS = 2000;
409
+ export const DEFAULT_EXECUTION_MODE: ExecutionMode = "auto";
410
+ export const DEFAULT_TRACKING_START: TrackingStart = "idle";
411
+ export const DEFAULT_JS_POLICY: CustomJsPolicy = "hybrid";
412
+ export const PAGE_RULE_POLL_WINDOW_MS = 2_000;
413
+ export const PAGE_RULE_POLL_INTERVAL_MS = 50;
414
+ export const CONSENT_COOKIE_NAME = "drip_consent";
415
+ export const IDENTITY_CONTEXT_COOKIE_NAME = "drip_ctx";
416
+ export const IDENTITY_CONTEXT_COOKIE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
417
+ export const IDENTITY_CONTEXT_COOKIE_MAX_ENCODED_BYTES = 1024;
418
+ export const SESSION_STORAGE_KEY = "drip_sid";
419
+
420
+ export /** Collect unique CSS selectors from experiments for scoped anti-flicker. */
421
+ function collectScopedSelectors(experiments: Experiment[], config?: InitConfig, assignments: Assignment[] = []): string[] {
422
+ const selectors = new Set<string>();
423
+ const pageInactiveExperimentIds = new Set(
424
+ assignments
425
+ .filter(isAssignmentArtifactInactive)
426
+ .map((assignment) => assignment.experimentId),
427
+ );
428
+ for (const exp of experiments) {
429
+ if (!isExperimentServable(exp, { qaMode: false, environment: config?.environments?.activeKey || "production" })) continue;
430
+ if (
431
+ !state.forceMap?.[exp.id] &&
432
+ weekdayScheduleBlocksDelivery(exp.weekdaySchedule, new Date())
433
+ ) continue;
434
+ // Redirect treatment artifacts are authored for the source page. The
435
+ // destination document is itself the treatment, so its preserved
436
+ // assignment must not hide source-only selectors before attribution
437
+ // startup.
438
+ if (pageInactiveExperimentIds.has(exp.id)) continue;
439
+ for (const v of exp.variations) {
440
+ v.antiFlickerSelectors?.forEach((selector) => selectors.add(selector));
441
+ for (const m of v.mutations) {
442
+ if (m.selector) selectors.add(m.selector);
443
+ }
444
+ for (const block of v.urlBlocks ?? []) {
445
+ for (const m of block.mutations) {
446
+ if (m.selector) selectors.add(m.selector);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ if (isCommercePriceActive(config?.commerce?.price, assignments)) {
452
+ COMMERCE_PRICE_SELECTORS.forEach((selector) => selectors.add(selector));
453
+ }
454
+ return Array.from(selectors);
455
+ }
456
+
457
+ export type ResolvedRuntimeConfig = {
458
+ executionMode: ExecutionMode;
459
+ antiFlickerMode: AntiFlickerMode;
460
+ antiFlickerTimeoutMs: number;
461
+ antiFlickerCustomCss?: string;
462
+ trackingStart: TrackingStart;
463
+ trackingIdleTimeoutMs: number;
464
+ customJsPolicy: CustomJsPolicy;
465
+ proxyEventDelivery?: { enabled: boolean; path?: string };
466
+ assetFallback?: boolean;
467
+ };
468
+
469
+ export type UrlDiffNeeds = {
470
+ path: boolean;
471
+ search: boolean;
472
+ hash: boolean;
473
+ };
474
+
475
+ export type ConfigSource = "inline" | "cache" | "network" | "none";
476
+
477
+ export type PendingPerfEvent = {
478
+ perf_event_kind: "init" | "route_change";
479
+ assignment_ready_ms: number;
480
+ init_total_ms?: number;
481
+ guard_resolve_ms?: number;
482
+ consent_identity_ms?: number;
483
+ runtime_setup_ms?: number;
484
+ targeting_assignment_ms?: number;
485
+ mutation_finalize_ms?: number;
486
+ predecision_mismatch_count?: number;
487
+ unattributed_init_ms?: number;
488
+ dom_content_loaded_ms: number;
489
+ /** @deprecated One-release alias for dom_content_loaded_ms. */
490
+ blank_screen_ms: number;
491
+ platform: PlatformHint;
492
+ config_source: ConfigSource;
493
+ activation_mode: ActivationMode;
494
+ sdk_version: string;
495
+ sdk_delivery_mode: string;
496
+ sdk_delivery_source: string;
497
+ sdk_policy_revision: string;
498
+ snippet_revision?: number;
499
+ runtime_profile: string;
500
+ sdk_variant: string;
501
+ snippet_fetch_ms: number;
502
+ snippet_transfer_size: number;
503
+ sdk_bundle_fetch_ms: number;
504
+ sdk_bundle_transfer_size: number;
505
+ time_to_sdk_ready_ms?: number;
506
+ experiment_count?: number;
507
+ assignment_count?: number;
508
+ mutation_count?: number;
509
+ script_exec_start?: number;
510
+ anti_flicker_inserted_at?: number;
511
+ assignment_ready_at?: number;
512
+ first_mutation_at?: number;
513
+ reveal_at?: number;
514
+ hidden_duration_ms?: number;
515
+ current_page_hide_selector_count?: number;
516
+ delivery_state?: string;
517
+ script_cache_source?: string;
518
+ install_surface?: string;
519
+ first_install_surface?: string;
520
+ config_fetch_ms?: number;
521
+ config_fetch_attempts?: number;
522
+ config_fetch_status?: RuntimeLoadDiagnostic["configFetchStatus"];
523
+ config_fetch_status_code?: number;
524
+ mutation_apply_pending?: number;
525
+ mutation_apply_applied?: number;
526
+ mutation_apply_missed?: number;
527
+ cls_total?: number;
528
+ cls_after_reveal?: number;
529
+ hydration_detected_at?: number;
530
+ apply_delayed_ms?: number;
531
+ hydration_timeout?: boolean;
532
+ hold_timeout?: boolean;
533
+ reapply_count?: number;
534
+ reassert_loop_detected?: boolean;
535
+ early_apply_at?: number;
536
+ reasserts_count?: number;
537
+ revert_loops_detected?: number;
538
+ };
539
+
540
+
541
+ export type InitPhaseKey =
542
+ | "guard_resolve_ms"
543
+ | "consent_identity_ms"
544
+ | "runtime_setup_ms"
545
+ | "targeting_assignment_ms"
546
+ | "mutation_finalize_ms";
547
+
548
+ export type InitPerfContext = {
549
+ startedAt: number;
550
+ phases: Partial<Record<InitPhaseKey, number>>;
551
+ includesConfigFetch: boolean;
552
+ predecisionMismatches: number;
553
+ };
554
+
555
+ export const MAX_PERF_DURATION_MS = 60_000;
556
+
557
+ export function monotonicNow(): number {
558
+ const value = globalThis.performance?.now?.();
559
+ return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
560
+ }
561
+
562
+ export function boundedPerfDuration(value: number): number {
563
+ if (!Number.isFinite(value)) return 0;
564
+ return Math.round(Math.min(MAX_PERF_DURATION_MS, Math.max(0, value)) * 10) / 10;
565
+ }
566
+
567
+ export function createInitPerfContext(): InitPerfContext {
568
+ return { startedAt: monotonicNow(), phases: {}, includesConfigFetch: false, predecisionMismatches: 0 };
569
+ }
570
+
571
+ export type BootstrapPredecision = {
572
+ decision: Array<[experimentId: string, variationId: string]>;
573
+ configRevision: number;
574
+ visitorId: string;
575
+ groups: CompactGroupSelections;
576
+ };
577
+
578
+ export function bootstrapPredecision(config: InitConfig, perf: InitPerfContext): BootstrapPredecision | null {
579
+ const candidate = (globalThis as typeof globalThis & { __dripPredecided?: unknown }).__dripPredecided;
580
+ if (!candidate || typeof candidate !== "object") return null;
581
+ const value = candidate as Partial<BootstrapPredecision>;
582
+ const revision = config.delivery?.revision;
583
+ if (!Number.isSafeInteger(revision) || value.configRevision !== revision ||
584
+ typeof value.visitorId !== "string" || !value.visitorId || !Array.isArray(value.decision) ||
585
+ !value.groups || typeof value.groups !== "object") {
586
+ perf.predecisionMismatches += 1;
587
+ return null;
588
+ }
589
+ return value as BootstrapPredecision;
590
+ }
591
+
592
+ export function addInitPhase(context: InitPerfContext, phase: InitPhaseKey, startedAt: number): void {
593
+ const elapsed = boundedPerfDuration(monotonicNow() - startedAt);
594
+ context.phases[phase] = boundedPerfDuration((context.phases[phase] ?? 0) + elapsed);
595
+ }
596
+
597
+ export type DeliveryHealthStatus = "applied" | "partial" | "miss" | "control" | "no_mutations";
598
+
599
+ export type DeliveryHealthTracker = {
600
+ assignment: Assignment;
601
+ expected: number;
602
+ applied: Set<number>;
603
+ missed: Set<number>;
604
+ emitted: boolean;
605
+ };
606
+
607
+ export type RouteDiagnostic = {
608
+ previousUrl: string;
609
+ nextUrl: string;
610
+ reason: string;
611
+ at: number;
612
+ reEvaluated: boolean;
613
+ };
614
+
615
+ export type DataLayerDiagnostic = {
616
+ eventName: string;
617
+ dataLayerName: string;
618
+ at: number;
619
+ mappedSignals: string[];
620
+ gtagNormalized?: boolean;
621
+ };
622
+
623
+ export type ShopifySyncDiagnostic = {
624
+ at: number;
625
+ status: "pending" | "sent" | "skipped" | "failed";
626
+ reason?: string;
627
+ assignmentCount: number;
628
+ };
629
+
630
+ export type ShopifyAssignmentSyncDiagnostic = Omit<ShopifySyncDiagnostic, "status"> & {
631
+ status: ShopifySyncDiagnostic["status"] | "cookie_fallback";
632
+ truncatedAssignmentCount?: number;
633
+ };
634
+
635
+ export type ExposureDestinationDiagnostic = {
636
+ type: string;
637
+ status: "sent" | "skipped" | "failed";
638
+ at: number;
639
+ reason?: string;
640
+ };
641
+
642
+ export type RuntimeLoadDiagnostic = SdkDiagnostics["load"];
643
+
644
+ export type DripPerfWindow = Window & {
645
+ __dripPerf?: Record<string, unknown>;
646
+ };
647
+
648
+ export function normalizeNumber(value: unknown, fallback: number): number {
649
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return value;
650
+ return fallback;
651
+ }
652
+
653
+ export function resolveRuntimeConfig(config: InitConfig): ResolvedRuntimeConfig {
654
+ const executionMode = config.executionMode ?? DEFAULT_EXECUTION_MODE;
655
+
656
+ const antiFlickerMode = config.antiFlickerMode
657
+ ?? (config.antiFlicker === false ? "off" : config.antiFlicker === true ? "full" : "scoped");
658
+
659
+ const antiFlickerTimeoutMs = normalizeNumber(
660
+ config.antiFlickerTimeoutMs ?? config.antiFlickerTimeout,
661
+ DEFAULT_ANTI_FLICKER_TIMEOUT_MS,
662
+ );
663
+
664
+ const trackingStart = config.trackingStart
665
+ ?? (typeof config.deferTracking === "boolean"
666
+ ? (config.deferTracking ? "idle" : "immediate")
667
+ : DEFAULT_TRACKING_START);
668
+
669
+ const trackingIdleTimeoutMs = normalizeNumber(
670
+ config.trackingIdleTimeoutMs,
671
+ DEFAULT_TRACKING_IDLE_TIMEOUT_MS,
672
+ );
673
+
674
+ const customJsPolicy = config.customJsPolicy ?? DEFAULT_JS_POLICY;
675
+
676
+ return {
677
+ executionMode,
678
+ antiFlickerMode,
679
+ antiFlickerTimeoutMs,
680
+ antiFlickerCustomCss: config.antiFlickerCustomCss,
681
+ trackingStart,
682
+ trackingIdleTimeoutMs,
683
+ customJsPolicy,
684
+ proxyEventDelivery: config.proxyEventDelivery?.enabled === true
685
+ ? { enabled: true, ...(typeof config.proxyEventDelivery.path === "string" ? { path: config.proxyEventDelivery.path } : {}) }
686
+ : undefined,
687
+ };
688
+ }
689
+
690
+ export function resolvePageTriggerMode(config: InitConfig): PageTriggerMode {
691
+ // Shop defaults map to page overrides as follows: direct -> immediate,
692
+ // url_change -> history/polling, dom_change -> dom, manual -> manual-*.
693
+ // The enums stay separate because the shop mode is a default while each
694
+ // published Page can select its own detector.
695
+ const mode = config.pageTrigger?.defaultMode;
696
+ if (mode === "url_change" || mode === "dom_change" || mode === "manual") return mode;
697
+ return "direct";
698
+ }
699
+
700
+ export function resolveDomPageTrigger(config: InitConfig, pageTriggerMode: PageTriggerMode): boolean {
701
+ if (config.spa?.domChangeTrigger === false) return false;
702
+ return (
703
+ config.spa?.domChangeTrigger === true ||
704
+ pageTriggerMode === "dom_change" ||
705
+ pageTriggerMode === "url_change" ||
706
+ config.platformHint === "spa"
707
+ );
708
+ }
709
+
710
+ export /** Bridge: log to both debug console and devtools overlay */
711
+ function dtLog(type: string, label: string, detail?: Record<string, unknown>): void {
712
+ if (!FEATURE_DEVTOOLS) return;
713
+ try {
714
+ import("./devtools").then(({ logDevtoolsEvent }) => {
715
+ logDevtoolsEvent(type, label, detail);
716
+ }).catch(() => {});
717
+ } catch {
718
+ // Dynamic import not supported.
719
+ }
720
+ }
721
+
722
+ export function traceName(id: string, name?: string): string {
723
+ return name && name !== id ? `"${name}" (${id})` : id;
724
+ }
725
+
726
+ export function traceStep(message: string, data?: Record<string, unknown>): void {
727
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog("trace", message, data);
728
+ }
729
+
730
+ export function traceGate(experiment: Experiment, gate: ActivationGateDiagnostic): void {
731
+ const status = gate.status === "passed"
732
+ ? "matching"
733
+ : gate.status === "failed"
734
+ ? "not matching"
735
+ : "skipped";
736
+ __DRIP_FEATURE_DEVTOOLS__ && traceStep(`${experiment.id}: ${gate.label} is ${status}`, {
737
+ reason: gate.reason,
738
+ ...(gate.detail && typeof gate.detail === "object" ? { detail: gate.detail } : {}),
739
+ });
740
+ }
741
+
742
+ export function browserDoNotTrackValue(): string {
743
+ const nav = globalThis.navigator as Navigator & { msDoNotTrack?: string } | undefined;
744
+ const win = globalThis as unknown as Window & { doNotTrack?: string };
745
+ return nav?.doNotTrack ?? nav?.msDoNotTrack ?? win.doNotTrack ?? "";
746
+ }
747
+
748
+ export function trackingBlocked(): boolean {
749
+ return state.trackingDisabledByDnt || (state.consentRequired && !state.consentGranted);
750
+ }
751
+
752
+ export let storefrontVitalsGateOpen: boolean | null = null;
753
+
754
+ export function syncStorefrontVitals(): void {
755
+ const config = state.config;
756
+ const gateOpen = Boolean(
757
+ runtimeSharedState.storefrontVitalsContext
758
+ && config
759
+ && !trackingBlocked()
760
+ && state.trackingPrerequisiteMet
761
+ && !state.screenshotMode
762
+ // Field vitals measure production customer traffic only; QA and preview
763
+ // sessions must not contaminate the baseline.
764
+ && !state.qaMode
765
+ && (config.environments?.activeKey ?? "production") === "production"
766
+ );
767
+ if (gateOpen === storefrontVitalsGateOpen) return;
768
+
769
+ const wasOpen = storefrontVitalsGateOpen;
770
+ storefrontVitalsGateOpen = gateOpen;
771
+ if (gateOpen && runtimeSharedState.storefrontVitalsContext) {
772
+ startStorefrontVitals(runtimeSharedState.storefrontVitalsContext);
773
+ } else if (wasOpen) {
774
+ stopStorefrontVitals();
775
+ }
776
+ }
777
+
778
+ export function doNotTrackEnabled(config: InitConfig): boolean {
779
+ return Boolean(evaluateSynchronousConsent({
780
+ consentCookieGranted: false,
781
+ respectDoNotTrack: config.dataProtection?.respectDoNotTrack,
782
+ doNotTrackValue: browserDoNotTrackValue(),
783
+ }) & DNT_BLOCKED);
784
+ }
785
+
786
+ export function storageAllowed(consentRequired: boolean, consentGranted: boolean, trackingDisabledByDnt: boolean): boolean {
787
+ return (!consentRequired || consentGranted) && !trackingDisabledByDnt;
788
+ }
789
+
790
+ export function readWindowPath(path: string | undefined): unknown {
791
+ return readPath(globalThis as Record<string, unknown>, path);
792
+ }
793
+
794
+ export function configuredConsentGranted(config: InitConfig): boolean | null {
795
+ const consent = config.consentMode;
796
+ return readConfiguredConsent(consent?.provider, consent?.requiredGroup, consent?.getter);
797
+ }
798
+
799
+ export function syncConfiguredConsent(config: InitConfig): void {
800
+ if (!state.consentRequired) return;
801
+ const configured = configuredConsentGranted(config);
802
+ if (configured == null || configured === state.consentGranted) return;
803
+ setConsent(configured);
804
+ }
805
+
806
+ export const CUSTOM_CONSENT_POLL_INTERVAL_MS = 1000;
807
+ export const CUSTOM_CONSENT_POLL_MAX_MS = 10 * 60 * 1000;
808
+ export const MANUAL_CALLBACK_TRIGGER_RERUN_INTERVAL_MS = 1000;
809
+ export const MANUAL_CALLBACK_TRIGGER_RERUN_MAX_MS = 10_000;
810
+ export const MANUAL_CALLBACK_TRIGGER_MAX_RERUNS = 10;
811
+ export const EVENT_DRIVEN_CONSENT_PROVIDERS = new Set(["onetrust", "usercentrics", "cookiebot", "ccm19"]);
812
+
813
+ export function shouldPollConfiguredConsent(config: InitConfig): boolean {
814
+ const consent = config.consentMode;
815
+ if (!consent?.enabled || state.consentGranted) return false;
816
+ if (consent.provider && EVENT_DRIVEN_CONSENT_PROVIDERS.has(consent.provider)) return false;
817
+ return consent.provider === "custom"
818
+ || consent.provider === "pandectes"
819
+ || consent.provider === "cookiescript"
820
+ || consent.provider === "borlabs"
821
+ || typeof consent.getter === "string";
822
+ }
823
+
824
+ export function installConsentChangeListeners(config: InitConfig): (() => void) | null {
825
+ if (!config.consentMode?.enabled || typeof window === "undefined") return null;
826
+ const events = [
827
+ "OneTrustGroupsUpdated",
828
+ "OptanonConsentChanged",
829
+ "CookiebotOnConsentReady",
830
+ "CookiebotOnAccept",
831
+ "CookiebotOnDecline",
832
+ "CookieConsentDeclaration",
833
+ "UC_UI_CMP_EVENT",
834
+ "ccm19EmbeddingAccepted",
835
+ "ccm19WidgetClosed",
836
+ "ccm19WidgetLoaded",
837
+ "PandectesInitialized",
838
+ "visitorConsentCollected",
839
+ "CookieScriptLoaded",
840
+ "CookieScriptAccept",
841
+ "CookieScriptAcceptAll",
842
+ "CookieScriptReject",
843
+ "CookieScriptClose",
844
+ ];
845
+ const handler = () => syncConfiguredConsent(config);
846
+ events.forEach((event) => window.addEventListener(event, handler));
847
+ const documentEvents = ["PandectesInitialized", "visitorConsentCollected"];
848
+ if (typeof document !== "undefined") {
849
+ documentEvents.forEach((event) => document.addEventListener(event, handler));
850
+ }
851
+
852
+ let pollTimer: ReturnType<typeof setInterval> | null = null;
853
+ let pollCapTimer: ReturnType<typeof setTimeout> | null = null;
854
+ const stopPolling = () => {
855
+ if (pollTimer !== null) {
856
+ clearInterval(pollTimer);
857
+ pollTimer = null;
858
+ }
859
+ if (pollCapTimer !== null) {
860
+ clearTimeout(pollCapTimer);
861
+ pollCapTimer = null;
862
+ }
863
+ };
864
+
865
+ if (shouldPollConfiguredConsent(config)) {
866
+ pollTimer = setInterval(() => {
867
+ syncConfiguredConsent(config);
868
+ if (state.consentGranted) stopPolling();
869
+ }, CUSTOM_CONSENT_POLL_INTERVAL_MS);
870
+ pollCapTimer = setTimeout(stopPolling, CUSTOM_CONSENT_POLL_MAX_MS);
871
+ }
872
+
873
+ return () => {
874
+ stopPolling();
875
+ events.forEach((event) => window.removeEventListener(event, handler));
876
+ if (typeof document !== "undefined") {
877
+ documentEvents.forEach((event) => document.removeEventListener(event, handler));
878
+ }
879
+ };
880
+ }
881
+
882
+ export function resolveConsentRequired(config: InitConfig): boolean {
883
+ return Boolean(evaluateSynchronousConsent({
884
+ requireConsent: config.requireConsent,
885
+ hasConsent: config.hasConsent,
886
+ consentModeEnabled: config.consentMode?.enabled,
887
+ consentCookieGranted: false,
888
+ }) & CONSENT_REQUIRED);
889
+ }
890
+
891
+ export function resolveConsentGranted(config: InitConfig, consentRequired: boolean): boolean {
892
+ if (!consentRequired) return true;
893
+ const configured = configuredConsentGranted(config);
894
+ return resolveSynchronousConsentGranted({
895
+ requireConsent: config.requireConsent,
896
+ hasConsent: config.hasConsent,
897
+ consentModeEnabled: config.consentMode?.enabled,
898
+ provider: config.consentMode?.provider,
899
+ configuredConsentGranted: configured,
900
+ consentCookieGranted: readConsentCookie(),
901
+ });
902
+ }
903
+
904
+ export function persistentStorageAllowed(): boolean {
905
+ return storageAllowed(state.consentRequired, state.consentGranted, state.trackingDisabledByDnt);
906
+ }
907
+
908
+ configureSignalPersistenceAllowed(() => persistentStorageAllowed());
909
+
910
+ export function coerceSignalValue(value: unknown, valueType?: RuntimeSignalDefinition["valueType"]): unknown {
911
+ if (value === undefined || value === null) return undefined;
912
+ if (valueType === "number") {
913
+ const numeric = Number(value);
914
+ return Number.isFinite(numeric) ? numeric : undefined;
915
+ }
916
+ if (valueType === "boolean") {
917
+ if (typeof value === "boolean") return value;
918
+ const normalized = String(value).trim().toLowerCase();
919
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
920
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
921
+ return undefined;
922
+ }
923
+ if (valueType === "string_array") {
924
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
925
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
926
+ }
927
+ if (valueType === "json") return value;
928
+ return String(value);
929
+ }
930
+
931
+ export function captureUrlSignals(config: InitConfig): void {
932
+ const definitions = config.signals ?? [];
933
+ if (!definitions.length) return;
934
+ let parsed: URL | null = null;
935
+ try {
936
+ parsed = new URL(config.url ?? location.href, typeof location !== "undefined" ? location.origin : "https://_");
937
+ } catch {
938
+ parsed = null;
939
+ }
940
+ if (!parsed) return;
941
+
942
+ for (const definition of definitions) {
943
+ const capture = definition.capture;
944
+ if (!definition.key || capture?.mode !== "url_parameter" || !capture.parameter) continue;
945
+ const raw = parsed.searchParams.get(capture.parameter);
946
+ if (raw == null || raw === "") {
947
+ if (capture.persist === false) clearRuntimeSignal(definition.key);
948
+ continue;
949
+ }
950
+ const value = coerceSignalValue(raw, definition.valueType);
951
+ if (value !== undefined) {
952
+ setCapturedSignal(definition.key, value, {
953
+ ttlDays: definition.ttlDays,
954
+ persist: capture.persist,
955
+ source: "url_parameter",
956
+ });
957
+ }
958
+ }
959
+ }
960
+
961
+ export function installClickSignalCaptures(config: InitConfig): (() => void) | null {
962
+ const definitions = (config.signals ?? []).filter((definition) =>
963
+ definition.capture?.mode === "click_selector" && definition.capture.selector,
964
+ );
965
+ if (!definitions.length || typeof document === "undefined" || !document.addEventListener) {
966
+ return null;
967
+ }
968
+
969
+ const handler = (event: Event) => {
970
+ if (!persistentStorageAllowed()) return;
971
+ const target = event.target;
972
+ if (!target || typeof (target as Element).closest !== "function") return;
973
+ const targetElement = target as Element;
974
+
975
+ for (const definition of definitions) {
976
+ const capture = definition.capture!;
977
+ const selector = capture.selector;
978
+ if (!selector) continue;
979
+ let element: Element | null = null;
980
+ try {
981
+ element = targetElement.closest(selector);
982
+ } catch {
983
+ element = null;
984
+ }
985
+ if (!element) continue;
986
+
987
+ let raw: unknown = true;
988
+ if (capture.source === "text") raw = element.textContent?.trim() ?? "";
989
+ if (capture.source === "attribute" && capture.attribute) raw = element.getAttribute(capture.attribute);
990
+ if (capture.source === "dataset" && capture.attribute) {
991
+ const key = capture.attribute.replace(/^data-/, "").replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());
992
+ raw = (element as HTMLElement).dataset?.[key];
993
+ }
994
+ if (capture.source === "constant") raw = capture.value ?? true;
995
+
996
+ const value = coerceSignalValue(raw, definition.valueType);
997
+ if (value !== undefined) setStoredSignal(definition.key, value, definition.ttlDays, "click_selector");
998
+ }
999
+ };
1000
+
1001
+ document.addEventListener("click", handler, true);
1002
+ return () => document.removeEventListener?.("click", handler, true);
1003
+ }
1004
+
1005
+ export function captureEventPropertySignals(type: string, data?: Record<string, unknown>): void {
1006
+ if (!state.config || !persistentStorageAllowed()) return;
1007
+ const definitions = state.config.signals ?? [];
1008
+ if (!definitions.length || !data) return;
1009
+
1010
+ for (const definition of definitions) {
1011
+ const capture = definition.capture;
1012
+ if (capture?.mode !== "event_property") continue;
1013
+ if (!capture.eventName || !capture.property || capture.eventName !== type) continue;
1014
+ const value = coerceSignalValue(data[capture.property], definition.valueType);
1015
+ if (value !== undefined) setStoredSignal(definition.key, value, definition.ttlDays, "event_property");
1016
+ }
1017
+ }
1018
+
1019
+ export function setCapturedSignal(
1020
+ key: string,
1021
+ value: unknown,
1022
+ opts: { ttlDays?: number; persist?: boolean; source: string },
1023
+ ): void {
1024
+ if (!key) return;
1025
+ if (opts.persist === false || !persistentStorageAllowed()) {
1026
+ setRuntimeSignal(key, value, opts.source);
1027
+ return;
1028
+ }
1029
+ setStoredSignal(key, value, opts.ttlDays, opts.source);
1030
+ setRuntimeSignal(key, value, opts.source);
1031
+ }
1032
+
1033
+ export function setPublicSignal(key: string, value: unknown, ttlDays?: number, source = "manual"): void {
1034
+ if (persistentStorageAllowed()) {
1035
+ setStoredSignal(key, value, ttlDays, source);
1036
+ } else {
1037
+ setRuntimeSignal(key, value, source);
1038
+ }
1039
+ }
1040
+
1041
+ export function getPublicSignals(options: { markVisitorSeen?: boolean } = {}): ReturnType<typeof getStoredSignals> {
1042
+ return getStoredSignals({
1043
+ ...options,
1044
+ markVisitorSeen: options.markVisitorSeen === false ? false : persistentStorageAllowed(),
1045
+ });
1046
+ }
1047
+
1048
+ export function normalizePageApiName(value: unknown): string {
1049
+ if (typeof value !== "string") return "";
1050
+ const key = value
1051
+ .trim()
1052
+ .toLowerCase()
1053
+ .replace(/['"`]/g, "")
1054
+ .replace(/[^a-z0-9]+/g, "_")
1055
+ .replace(/^_+|_+$/g, "")
1056
+ .replace(/_+/g, "_");
1057
+ if (!key) return "";
1058
+ return /^[a-z]/.test(key) ? key : `segment_${key}`;
1059
+ }
1060
+
1061
+ export function pageTriggerSignal(normalizedApiName: string): string {
1062
+ return `page.trigger.${normalizedApiName}`;
1063
+ }
1064
+
1065
+ export function triggerPage(apiName: unknown, active: unknown = true): void {
1066
+ const normalizedApiName = normalizePageApiName(apiName);
1067
+ if (!normalizedApiName) return;
1068
+ const nextActive =
1069
+ active === false
1070
+ ? false
1071
+ : active && typeof active === "object" && "isActive" in active
1072
+ ? Boolean((active as { isActive?: unknown }).isActive)
1073
+ : true;
1074
+ setRuntimeSignal(pageTriggerSignal(normalizedApiName), nextActive, "page_api");
1075
+ try {
1076
+ document.dispatchEvent(new CustomEvent("drip:signals-changed", {
1077
+ detail: { source: "page_api", apiName: normalizedApiName, active: nextActive },
1078
+ }));
1079
+ } catch {}
1080
+ }
1081
+
1082
+ export function readElementCaptureValue(element: Element, capture: NonNullable<RuntimeSignalDefinition["capture"]>): unknown {
1083
+ if (capture.source === "attribute" && capture.attribute) return element.getAttribute(capture.attribute);
1084
+ if (capture.source === "dataset" && capture.attribute) {
1085
+ const key = capture.attribute.replace(/^data-/, "").replace(/-([a-z])/g, (_, char: string) => char.toUpperCase());
1086
+ return (element as HTMLElement).dataset?.[key];
1087
+ }
1088
+ if (capture.source === "constant") return capture.value ?? true;
1089
+ return element.textContent?.trim() ?? "";
1090
+ }
1091
+
1092
+ export function captureDomAndGlobalSignals(config: InitConfig): void {
1093
+ const definitions = config.signals ?? [];
1094
+ for (const definition of definitions) {
1095
+ const capture = definition.capture;
1096
+ if (!capture) continue;
1097
+ if (capture.mode === "dom_selector" && capture.selector && typeof document !== "undefined") {
1098
+ let element: Element | null = null;
1099
+ try {
1100
+ element = document.querySelector(capture.selector);
1101
+ } catch {
1102
+ element = null;
1103
+ }
1104
+ if (!element) continue;
1105
+ const value = coerceSignalValue(readElementCaptureValue(element, capture), definition.valueType);
1106
+ if (value !== undefined) {
1107
+ setCapturedSignal(definition.key, value, {
1108
+ ttlDays: definition.ttlDays,
1109
+ persist: capture.persist,
1110
+ source: "dom_selector",
1111
+ });
1112
+ }
1113
+ }
1114
+ if (capture.mode === "global" && capture.path) {
1115
+ const value = coerceSignalValue(readWindowPath(capture.path), definition.valueType);
1116
+ if (value !== undefined) {
1117
+ setCapturedSignal(definition.key, value, {
1118
+ ttlDays: definition.ttlDays,
1119
+ persist: capture.persist,
1120
+ source: "global",
1121
+ });
1122
+ }
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ export function consentAwareDataProtection(config: InitConfig): DataProtectionSettings | undefined {
1128
+ if (persistentStorageAllowed()) return config.dataProtection;
1129
+ return { ...(config.dataProtection ?? {}), storageMode: "memory" };
1130
+ }
1131
+
1132
+ export function writeConsentCookie(granted: boolean): void {
1133
+ if (!globalThis.document) return;
1134
+ try {
1135
+ globalThis.document.cookie = granted
1136
+ ? `${CONSENT_COOKIE_NAME}=1; Max-Age=31536000; Path=/; SameSite=Lax`
1137
+ : `${CONSENT_COOKIE_NAME}=; Max-Age=0; Path=/; SameSite=Lax`;
1138
+ } catch {
1139
+ // ignore cookie failures
1140
+ }
1141
+ }
1142
+
1143
+ export function readConsentCookie(): boolean {
1144
+ if (!globalThis.document?.cookie) return false;
1145
+ try {
1146
+ return new RegExp(`(?:^|;\\s*)${CONSENT_COOKIE_NAME}=1(?:;|$)`).test(globalThis.document.cookie);
1147
+ } catch {
1148
+ return false;
1149
+ }
1150
+ }
1151
+
1152
+ export function clearConsentStorage(): void {
1153
+ clearPendingRedirectAssignmentCarry();
1154
+ try {
1155
+ const storage = globalThis.localStorage;
1156
+ storage?.removeItem("drip_uid");
1157
+ storage?.removeItem("drip_goals");
1158
+ storage?.removeItem("drip_visitor_sessions_v1");
1159
+ storage?.removeItem(ORDER_EXPOSURE_STORAGE_KEY);
1160
+ clearStoredSignals();
1161
+ if (storage) {
1162
+ for (let i = storage.length - 1; i >= 0; i--) {
1163
+ const key = storage.key(i);
1164
+ if (key?.startsWith(CONFIG_CACHE_KEY_PREFIX)) storage.removeItem(key);
1165
+ }
1166
+ }
1167
+ } catch {}
1168
+ try {
1169
+ const storage = globalThis.sessionStorage;
1170
+ storage?.removeItem("drip_uid");
1171
+ storage?.removeItem("drip_sid");
1172
+ storage?.removeItem("drip_events");
1173
+ storage?.removeItem("drip_utm");
1174
+ if (storage) {
1175
+ for (let i = storage.length - 1; i >= 0; i--) {
1176
+ const key = storage.key(i);
1177
+ if (
1178
+ key?.startsWith("drip_hm:")
1179
+ || key?.startsWith(REPLAY_SAMPLE_KEY_PREFIX)
1180
+ || key?.startsWith(REPLAY_PROGRESS_KEY_PREFIX)
1181
+ ) {
1182
+ storage.removeItem(key);
1183
+ }
1184
+ }
1185
+ }
1186
+ } catch {}
1187
+ clearIdentityContextCookie();
1188
+ clearPersistedAssignments();
1189
+ clearBrowserGroupStickyStore();
1190
+ clearAssignmentSyncCookie();
1191
+ if (globalThis.document) {
1192
+ try {
1193
+ globalThis.document.cookie = "drip_uid=; Max-Age=0; Path=/; SameSite=Lax";
1194
+ } catch {}
1195
+ }
1196
+ }
1197
+
1198
+ export function applyConsentStorageMode(): void {
1199
+ if (!state.config) return;
1200
+ configureIdentityStorage(consentAwareDataProtection(state.config));
1201
+ if (persistentStorageAllowed() && state.userId) {
1202
+ const persistedUserId = getPersistedUserId();
1203
+ state.userId = persistedUserId || state.userId;
1204
+ adoptUserId(state.userId);
1205
+ }
1206
+ }
1207
+
1208
+ export function runWhenDomReady(callback: () => void): void {
1209
+ if (!globalThis.document || document.readyState !== "loading") {
1210
+ callback();
1211
+ return;
1212
+ }
1213
+ document.addEventListener("DOMContentLoaded", callback, { once: true });
1214
+ }
1215
+
1216
+ export function applyProjectCode(config: InitConfig): void {
1217
+ const projectCode = config.projectCode;
1218
+ if (!projectCode?.enabled) return;
1219
+ if (projectCode.css && globalThis.document) {
1220
+ const id = "drip-project-css";
1221
+ let style = document.getElementById(id) as HTMLStyleElement | null;
1222
+ if (!style) {
1223
+ style = document.createElement("style");
1224
+ style.id = id;
1225
+ (document.head || document.documentElement).appendChild(style);
1226
+ }
1227
+ style.textContent = projectCode.css;
1228
+ }
1229
+ if (projectCode.helperJs) execJs(projectCode.helperJs, "project-helper");
1230
+ }
1231
+
1232
+ export function resetProjectCode(): void {
1233
+ // Project helper/startup JS has no general inverse. Run the same supported
1234
+ // cleanup path as a runtime re-init first: variation/experiment reset JS,
1235
+ // mutation restoration, commerce teardown, and injected artifact removal.
1236
+ teardownTransaction(allAppliedPagesWithModes());
1237
+ globalThis.document?.getElementById("drip-project-css")?.remove();
1238
+ }
1239
+
1240
+ export function runProjectStartupCode(config: InitConfig): void {
1241
+ const projectCode = config.projectCode;
1242
+ const js = projectCode?.js;
1243
+ if (!projectCode?.enabled || !js) return;
1244
+ runWhenDomReady(() => {
1245
+ // A cached config can be replaced before DOMContentLoaded. Never let its
1246
+ // deferred project startup code run after an authoritative re-init.
1247
+ if (state.config !== config || state.holdout) return;
1248
+ execJs(js, "project-startup");
1249
+ });
1250
+ }
1251
+
1252
+ export function collectJsTriggerConditions(experiments: Experiment[]): SegmentCondition[] {
1253
+ const seen = new Set<string>();
1254
+ const conditions: SegmentCondition[] = [];
1255
+ for (const experiment of experiments) {
1256
+ for (const rule of experiment.targeting?.segmentRules ?? []) {
1257
+ for (const condition of rule.rules?.conditions ?? []) {
1258
+ const code = condition.js?.code?.trim();
1259
+ if (!code) continue;
1260
+ const key = `${condition.signal}:${code}`;
1261
+ if (seen.has(key)) continue;
1262
+ seen.add(key);
1263
+ conditions.push(condition);
1264
+ }
1265
+ }
1266
+ }
1267
+ return conditions;
1268
+ }
1269
+
1270
+ export function collectPageTriggerRules(experiments: Experiment[]): PageTargetingRule[] {
1271
+ const seen = new Set<string>();
1272
+ const rules: PageTargetingRule[] = [];
1273
+ for (const experiment of experiments) {
1274
+ for (const pageRules of [
1275
+ experiment.targeting?.pageRules ?? [],
1276
+ experiment.goalPageRules ?? [],
1277
+ ]) {
1278
+ for (const rule of pageRules) {
1279
+ const mode = rule.trigger?.mode;
1280
+ if (mode !== "manual-callback" && mode !== "manual-api") continue;
1281
+ const code = rule.trigger?.triggerJs?.trim();
1282
+ const signal = rule.trigger?.signal;
1283
+ if (!code || !signal) continue;
1284
+ const key = `${signal}:${code}`;
1285
+ if (seen.has(key)) continue;
1286
+ seen.add(key);
1287
+ rules.push(rule);
1288
+ }
1289
+ }
1290
+ }
1291
+ return rules;
1292
+ }
1293
+
1294
+ export type PageDetectorMode = "history" | "polling" | "dom";
1295
+
1296
+ export type PageRuleReconcileScope = {
1297
+ triggeredPageIds: ReadonlySet<string>;
1298
+ includeImmediate: boolean;
1299
+ };
1300
+
1301
+ export function collectPageDetectorRules(
1302
+ experiments: Experiment[],
1303
+ ): Map<PageDetectorMode, PageTargetingRule[]> {
1304
+ const registry = new Map<PageDetectorMode, PageTargetingRule[]>([
1305
+ ["history", []],
1306
+ ["polling", []],
1307
+ ["dom", []],
1308
+ ]);
1309
+ const seen = new Set<string>();
1310
+ for (const experiment of experiments) {
1311
+ for (const rules of [
1312
+ experiment.targeting?.pageRules ?? [],
1313
+ experiment.goalPageRules ?? [],
1314
+ ]) {
1315
+ for (const rule of rules) {
1316
+ const mode = rule.trigger?.mode;
1317
+ if (mode !== "history" && mode !== "polling" && mode !== "dom") continue;
1318
+ const key = `${mode}:${rule.id}`;
1319
+ if (!rule.id || seen.has(key)) continue;
1320
+ seen.add(key);
1321
+ registry.get(mode)!.push(rule);
1322
+ }
1323
+ }
1324
+ }
1325
+ return registry;
1326
+ }
1327
+
1328
+ export function experimentsForGlobalRouteReevaluation(
1329
+ experiments: Experiment[],
1330
+ ): Experiment[] {
1331
+ return experiments.map((experiment) => ({
1332
+ ...experiment,
1333
+ goalPageRules: experiment.goalPageRules?.filter((rule) => {
1334
+ const mode = rule.trigger?.mode ?? "immediate";
1335
+ return mode === "immediate";
1336
+ }),
1337
+ targeting: experiment.targeting
1338
+ ? {
1339
+ ...experiment.targeting,
1340
+ pageRules: experiment.targeting.pageRules?.filter((rule) => {
1341
+ const mode = rule.trigger?.mode ?? "immediate";
1342
+ return mode === "immediate";
1343
+ }),
1344
+ }
1345
+ : experiment.targeting,
1346
+ }));
1347
+ }
1348
+
1349
+ export function conditionUsesActivateTrigger(condition: SegmentCondition): boolean {
1350
+ const code = condition.js?.code ?? "";
1351
+ return condition.js?.mode === "activate" ||
1352
+ /\bactivate\s*\(/.test(code) ||
1353
+ /\bcallbackFn\b/.test(code);
1354
+ }
1355
+
1356
+ export function buildJsTriggerContext(condition: SegmentCondition): Record<string, unknown> {
1357
+ const href = typeof window !== "undefined" ? window.location.href : "https://_";
1358
+ const apexTools = getRuntimeTools();
1359
+ return {
1360
+ apexTools,
1361
+ attributes: buildTargetingAttributes({}, href),
1362
+ document: typeof document !== "undefined" ? document : undefined,
1363
+ location: typeof location !== "undefined" ? location : undefined,
1364
+ signal: condition.signal,
1365
+ url: new URL(href, "https://_"),
1366
+ value: condition.value,
1367
+ window: typeof window !== "undefined" ? window : undefined,
1368
+ };
1369
+ }
1370
+
1371
+ export function runJsTriggerReset(condition: SegmentCondition): void {
1372
+ const resetCode = condition.js?.resetCode?.trim();
1373
+ if (!resetCode || resetCode.length > 4000) return;
1374
+ try {
1375
+ const context = buildJsTriggerContext(condition);
1376
+ const fn = new Function(
1377
+ "context",
1378
+ `"use strict";` +
1379
+ `const attributes=context.attributes;` +
1380
+ `const apexTools=context.apexTools;` +
1381
+ `const document=context.document;` +
1382
+ `const location=context.location;` +
1383
+ `const signal=context.signal;` +
1384
+ `const url=context.url;` +
1385
+ `const value=context.value;` +
1386
+ `const window=context.window;` +
1387
+ `(function(){${resetCode}}).call(window);`,
1388
+ );
1389
+ fn(context);
1390
+ } catch (error) {
1391
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("JS trigger reset failed", error instanceof Error ? error.message : String(error));
1392
+ }
1393
+ }
1394
+
1395
+ export function normalizeActivationValue(next: unknown, hasArgument: boolean): boolean {
1396
+ if (!hasArgument) return true;
1397
+ if (next && typeof next === "object" && "isActive" in next) {
1398
+ return Boolean((next as { isActive?: unknown }).isActive);
1399
+ }
1400
+ return Boolean(next);
1401
+ }
1402
+
1403
+ export function installActivateJsTrigger(condition: SegmentCondition, onChange: () => void): () => void {
1404
+ const source = condition.js?.code?.trim();
1405
+ if (!source || source.length > 4000) return () => {};
1406
+
1407
+ let teardown: unknown;
1408
+ let active = Boolean(getPublicSignals({ markVisitorSeen: false })[condition.signal]);
1409
+ const options = { isActive: active };
1410
+ function activate(next?: unknown) {
1411
+ const nextActive = normalizeActivationValue(next, arguments.length > 0);
1412
+ active = nextActive;
1413
+ options.isActive = nextActive;
1414
+ setRuntimeSignal(condition.signal, nextActive);
1415
+ onChange();
1416
+ }
1417
+
1418
+ try {
1419
+ const context = buildJsTriggerContext(condition);
1420
+ const fn = new Function(
1421
+ "activate",
1422
+ "options",
1423
+ "context",
1424
+ `"use strict";` +
1425
+ `const attributes=context.attributes;` +
1426
+ `const apexTools=context.apexTools;` +
1427
+ `const document=context.document;` +
1428
+ `const location=context.location;` +
1429
+ `const signal=context.signal;` +
1430
+ `const url=context.url;` +
1431
+ `const value=context.value;` +
1432
+ `const window=context.window;` +
1433
+ `${source}\n` +
1434
+ `if (typeof callbackFn === "function") return callbackFn(activate, options);`,
1435
+ );
1436
+ teardown = fn(activate, options, context);
1437
+ } catch (error) {
1438
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("JS trigger activation failed", error instanceof Error ? error.message : String(error));
1439
+ setRuntimeSignal(condition.signal, false);
1440
+ }
1441
+
1442
+ return () => {
1443
+ try {
1444
+ if (typeof teardown === "function") teardown();
1445
+ } catch (error) {
1446
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("JS trigger teardown failed", error instanceof Error ? error.message : String(error));
1447
+ }
1448
+ runJsTriggerReset(condition);
1449
+ clearRuntimeSignal(condition.signal);
1450
+ onChange();
1451
+ };
1452
+ }
1453
+
1454
+ export function installPageTriggerRule(
1455
+ rule: PageTargetingRule,
1456
+ onChange: () => void,
1457
+ manualInitialTrigger = false,
1458
+ ): () => void {
1459
+ const mode = rule.trigger?.mode;
1460
+ if (mode !== "manual-callback" && mode !== "manual-api") return () => {};
1461
+ const source = rule.trigger?.triggerJs?.trim();
1462
+ const signal = rule.trigger?.signal;
1463
+ if (!source || source.length > 8000 || !signal) return () => {};
1464
+ const triggerSignal = signal;
1465
+
1466
+ const teardowns: Array<() => void> = [];
1467
+ let active = Boolean(getPublicSignals({ markVisitorSeen: false })[triggerSignal]);
1468
+ const options = { isActive: active, pageId: rule.id, apiName: rule.trigger?.apiName };
1469
+ let rerunObserverCleanup: (() => void) | null = null;
1470
+ let rerunInterval: number | null = null;
1471
+ let rerunCapTimer: number | null = null;
1472
+ let rerunCount = 0;
1473
+ let totalAttemptCount = 0;
1474
+ let rerunsStarted = false;
1475
+ let rerunsStopped = false;
1476
+
1477
+ const triggerIsActive = () => Boolean(
1478
+ getPublicSignals({ markVisitorSeen: false })[triggerSignal],
1479
+ );
1480
+
1481
+ const stopReruns = (
1482
+ reason: "activated" | "attempt_limit" | "time_limit" | "teardown",
1483
+ ) => {
1484
+ if (rerunsStopped) return;
1485
+ rerunsStopped = true;
1486
+ if (rerunObserverCleanup) {
1487
+ rerunObserverCleanup();
1488
+ rerunObserverCleanup = null;
1489
+ }
1490
+ if (rerunInterval !== null) {
1491
+ window.clearInterval(rerunInterval);
1492
+ rerunInterval = null;
1493
+ }
1494
+ if (rerunCapTimer !== null) {
1495
+ window.clearTimeout(rerunCapTimer);
1496
+ rerunCapTimer = null;
1497
+ }
1498
+ if (rerunsStarted) {
1499
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog(
1500
+ "page-trigger",
1501
+ "Manual callback re-runs stopped",
1502
+ {
1503
+ attempts: totalAttemptCount,
1504
+ pageId: rule.id,
1505
+ reason,
1506
+ reruns: rerunCount,
1507
+ signal: triggerSignal,
1508
+ },
1509
+ );
1510
+ }
1511
+ };
1512
+
1513
+ const runTrigger = (isRerun: boolean) => {
1514
+ if (isRerun) {
1515
+ if (rerunsStopped) return;
1516
+ if (triggerIsActive()) {
1517
+ stopReruns("activated");
1518
+ return;
1519
+ }
1520
+ if (rerunCount >= MANUAL_CALLBACK_TRIGGER_MAX_RERUNS) {
1521
+ stopReruns("attempt_limit");
1522
+ return;
1523
+ }
1524
+ rerunCount += 1;
1525
+ }
1526
+ totalAttemptCount += 1;
1527
+ let activatedThisRun = false;
1528
+
1529
+ function activate(next?: unknown) {
1530
+ const nextActive = normalizeActivationValue(next, arguments.length > 0);
1531
+ if (nextActive) activatedThisRun = true;
1532
+ active = nextActive;
1533
+ options.isActive = nextActive;
1534
+ setRuntimeSignal(triggerSignal, nextActive, "page_trigger");
1535
+ onChange();
1536
+ if (nextActive) stopReruns("activated");
1537
+ }
1538
+
1539
+ try {
1540
+ const href = typeof window !== "undefined" ? window.location.href : "https://_";
1541
+ const context = {
1542
+ apexTools: getRuntimeTools(),
1543
+ attributes: buildTargetingAttributes({}, href),
1544
+ document: typeof document !== "undefined" ? document : undefined,
1545
+ location: typeof location !== "undefined" ? location : undefined,
1546
+ pageId: rule.id,
1547
+ apiName: rule.trigger?.apiName,
1548
+ signal: triggerSignal,
1549
+ url: new URL(href, "https://_"),
1550
+ window: typeof window !== "undefined" ? window : undefined,
1551
+ };
1552
+ const fn = new Function(
1553
+ "activate",
1554
+ "options",
1555
+ "context",
1556
+ `"use strict";` +
1557
+ `const apexTools=context.apexTools;` +
1558
+ `const attributes=context.attributes;` +
1559
+ `const document=context.document;` +
1560
+ `const location=context.location;` +
1561
+ `const pageId=context.pageId;` +
1562
+ `const apiName=context.apiName;` +
1563
+ `const signal=context.signal;` +
1564
+ `const url=context.url;` +
1565
+ `const window=context.window;` +
1566
+ `${source}\n`,
1567
+ );
1568
+ const teardown = fn(activate, options, context);
1569
+ if (typeof teardown === "function") teardowns.push(teardown);
1570
+ } catch (error) {
1571
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("Page trigger JS failed", error instanceof Error ? error.message : String(error));
1572
+ if (!activatedThisRun) setRuntimeSignal(triggerSignal, false, "page_trigger");
1573
+ }
1574
+
1575
+ if (triggerIsActive()) {
1576
+ stopReruns("activated");
1577
+ } else if (isRerun && rerunCount >= MANUAL_CALLBACK_TRIGGER_MAX_RERUNS) {
1578
+ stopReruns("attempt_limit");
1579
+ }
1580
+ };
1581
+
1582
+ runTrigger(false);
1583
+
1584
+ if (
1585
+ mode === "manual-callback" &&
1586
+ !manualInitialTrigger &&
1587
+ !rerunsStopped &&
1588
+ !triggerIsActive() &&
1589
+ typeof window !== "undefined" &&
1590
+ typeof document !== "undefined"
1591
+ ) {
1592
+ rerunsStarted = true;
1593
+ rerunObserverCleanup = observeDomChanges(() => runTrigger(true));
1594
+ rerunInterval = window.setInterval(
1595
+ () => runTrigger(true),
1596
+ MANUAL_CALLBACK_TRIGGER_RERUN_INTERVAL_MS,
1597
+ );
1598
+ rerunCapTimer = window.setTimeout(
1599
+ () => stopReruns("time_limit"),
1600
+ MANUAL_CALLBACK_TRIGGER_RERUN_MAX_MS,
1601
+ );
1602
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog(
1603
+ "page-trigger",
1604
+ "Manual callback re-runs started",
1605
+ {
1606
+ attempts: totalAttemptCount,
1607
+ intervalMs: MANUAL_CALLBACK_TRIGGER_RERUN_INTERVAL_MS,
1608
+ maxMs: MANUAL_CALLBACK_TRIGGER_RERUN_MAX_MS,
1609
+ maxReruns: MANUAL_CALLBACK_TRIGGER_MAX_RERUNS,
1610
+ pageId: rule.id,
1611
+ signal: triggerSignal,
1612
+ },
1613
+ );
1614
+ }
1615
+
1616
+ return () => {
1617
+ stopReruns("teardown");
1618
+ for (const teardown of teardowns) {
1619
+ try {
1620
+ teardown();
1621
+ } catch (error) {
1622
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("Page trigger teardown failed", error instanceof Error ? error.message : String(error));
1623
+ }
1624
+ }
1625
+ clearRuntimeSignal(triggerSignal);
1626
+ onChange();
1627
+ };
1628
+ }
1629
+
1630
+ export function installJsTriggerObserver(
1631
+ experiments: Experiment[],
1632
+ onChange: () => void,
1633
+ manualInitialTrigger: boolean,
1634
+ ): () => void {
1635
+ if (
1636
+ !targetingHasJsTriggers(experiments) ||
1637
+ typeof window === "undefined" ||
1638
+ typeof document === "undefined"
1639
+ ) {
1640
+ return () => {};
1641
+ }
1642
+
1643
+ const jsTriggerConditions = collectJsTriggerConditions(experiments);
1644
+ const activateCleanups = jsTriggerConditions
1645
+ .filter(conditionUsesActivateTrigger)
1646
+ .map((condition) => installActivateJsTrigger(condition, onChange));
1647
+ const pageTriggerCleanups = collectPageTriggerRules(experiments)
1648
+ .map((rule) => installPageTriggerRule(rule, onChange, manualInitialTrigger));
1649
+
1650
+ let timeoutId: number | null = null;
1651
+ const schedule = () => {
1652
+ if (timeoutId !== null) return;
1653
+ timeoutId = window.setTimeout(() => {
1654
+ timeoutId = null;
1655
+ onChange();
1656
+ }, 80);
1657
+ };
1658
+
1659
+ const listeners: Array<[EventTarget, string, EventListener, AddEventListenerOptions?]> = [
1660
+ [document, "click", schedule, { capture: true, passive: true }],
1661
+ [document, "input", schedule, { capture: true, passive: true }],
1662
+ [document, "change", schedule, { capture: true, passive: true }],
1663
+ [document, "keyup", schedule, { capture: true }],
1664
+ [document, "drip:trigger", schedule],
1665
+ [document, "drip:signals-changed", schedule],
1666
+ [window, "resize", schedule, { passive: true }],
1667
+ [window, "scroll", schedule, { passive: true }],
1668
+ ];
1669
+
1670
+ let observer: MutationObserver | null = null;
1671
+ if (typeof MutationObserver !== "undefined" && document.documentElement) {
1672
+ observer = new MutationObserver(schedule);
1673
+ registerMutedObserver(observer, schedule);
1674
+ observer.observe(document.documentElement, {
1675
+ attributes: true,
1676
+ childList: true,
1677
+ subtree: true,
1678
+ });
1679
+ }
1680
+
1681
+ listeners.forEach(([target, event, listener, options]) => {
1682
+ target.addEventListener(event, listener, options);
1683
+ });
1684
+
1685
+ return () => {
1686
+ if (timeoutId !== null) window.clearTimeout(timeoutId);
1687
+ if (observer) {
1688
+ observer.disconnect();
1689
+ unregisterMutedObserver(observer);
1690
+ }
1691
+ activateCleanups.forEach((cleanup) => cleanup());
1692
+ pageTriggerCleanups.forEach((cleanup) => cleanup());
1693
+ listeners.forEach(([target, event, listener, options]) => {
1694
+ target.removeEventListener(event, listener, options);
1695
+ });
1696
+ };
1697
+ }
1698
+
1699
+ export function notifyQaAssistantRuntimeListeners(): void {
1700
+ for (const listener of runtimeSharedState.qaAssistantRuntimeListeners) {
1701
+ try {
1702
+ listener();
1703
+ } catch {
1704
+ // QA panel subscriptions must not affect storefront behavior.
1705
+ }
1706
+ }
1707
+ }
1708
+
1709
+ export type HarnessManifest = {
1710
+ version?: number;
1711
+ expiresAt?: string;
1712
+ externalJsUrl?: string | null;
1713
+ externalCssUrl?: string | null;
1714
+ manifestUrl?: string;
1715
+ pollIntervalMs?: number;
1716
+ };
1717
+
1718
+ export let harnessPollTimer: number | null = null;
1719
+ export let harnessPollKey = "";
1720
+ export let harnessVisibilityInstalled = false;
1721
+
1722
+ export function activeHarnessAssignment(assignments: Assignment[]): Assignment | null {
1723
+ if (!FEATURE_HARNESS) return null;
1724
+ return artifactRenderableAssignments(assignments).find(
1725
+ (assignment) => assignment.harnessSession?.manifestUrl,
1726
+ ) ?? null;
1727
+ }
1728
+
1729
+ export function stopHarnessPolling(): void {
1730
+ if (harnessPollTimer !== null) {
1731
+ window.clearInterval(harnessPollTimer);
1732
+ harnessPollTimer = null;
1733
+ }
1734
+ harnessPollKey = "";
1735
+ }
1736
+
1737
+ export function applyHarnessManifest(manifest: HarnessManifest): void {
1738
+ if (!manifest.version || manifest.version <= 0) return;
1739
+ const current = activeHarnessAssignment(state.assignments);
1740
+ if (!current?.harnessSession?.manifestUrl) return;
1741
+ if (current.harnessSession.version === manifest.version) return;
1742
+
1743
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog("harness", `Reloading harness version ${manifest.version}`);
1744
+ __DRIP_FEATURE_DEVTOOLS__ && dtLog("harness", "reload", { version: manifest.version });
1745
+
1746
+ teardownTransaction(allAppliedPagesWithModes());
1747
+ state.assignments = state.assignments.map((assignment) => {
1748
+ const sessionInfo = assignment.harnessSession;
1749
+ if (!sessionInfo || sessionInfo.manifestUrl !== current.harnessSession?.manifestUrl) return assignment;
1750
+ return {
1751
+ ...assignment,
1752
+ externalJsUrl: manifest.externalJsUrl ?? undefined,
1753
+ externalCssUrl: manifest.externalCssUrl ?? undefined,
1754
+ harnessSession: {
1755
+ ...sessionInfo,
1756
+ version: manifest.version,
1757
+ expiresAt: manifest.expiresAt ?? sessionInfo.expiresAt,
1758
+ manifestUrl: manifest.manifestUrl ?? sessionInfo.manifestUrl,
1759
+ pollIntervalMs: manifest.pollIntervalMs ?? sessionInfo.pollIntervalMs,
1760
+ },
1761
+ };
1762
+ });
1763
+ applyAssignments(state.assignments, state.activationMode);
1764
+ state.lastAssignmentSignature = assignmentSignature(state.assignments);
1765
+ }
1766
+
1767
+ export function pollHarnessManifest(manifestUrl: string): void {
1768
+ if (!FEATURE_HARNESS) return;
1769
+ if (document.hidden) return;
1770
+ fetch(manifestUrl, { cache: "no-store" })
1771
+ .then((res) => {
1772
+ if (!res.ok) throw new Error("Harness manifest failed: " + res.status);
1773
+ return res.json() as Promise<HarnessManifest>;
1774
+ })
1775
+ .then(applyHarnessManifest)
1776
+ .catch((error) => {
1777
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("harness", error instanceof Error ? error.message : String(error));
1778
+ __DRIP_FEATURE_DEVTOOLS__ && dtLog("harness", "poll failed", {});
1779
+ });
1780
+ }
1781
+
1782
+ export function startHarnessPolling(assignments: Assignment[]): void {
1783
+ if (!FEATURE_HARNESS) return;
1784
+ const assignment = activeHarnessAssignment(assignments);
1785
+ const manifestUrl = assignment?.harnessSession?.manifestUrl;
1786
+ if (!manifestUrl) {
1787
+ stopHarnessPolling();
1788
+ return;
1789
+ }
1790
+
1791
+ const intervalMs = Math.max(250, assignment.harnessSession?.pollIntervalMs ?? 750);
1792
+ const nextKey = `${manifestUrl}:${intervalMs}`;
1793
+ if (harnessPollKey === nextKey && harnessPollTimer !== null) return;
1794
+
1795
+ stopHarnessPolling();
1796
+ harnessPollKey = nextKey;
1797
+ pollHarnessManifest(manifestUrl);
1798
+ harnessPollTimer = window.setInterval(() => pollHarnessManifest(manifestUrl), intervalMs);
1799
+
1800
+ if (!harnessVisibilityInstalled) {
1801
+ harnessVisibilityInstalled = true;
1802
+ document.addEventListener("visibilitychange", () => {
1803
+ const current = activeHarnessAssignment(state.assignments);
1804
+ if (!document.hidden && current?.harnessSession?.manifestUrl) {
1805
+ pollHarnessManifest(current.harnessSession.manifestUrl);
1806
+ }
1807
+ });
1808
+ }
1809
+ }
1810
+
1811
+
1812
+ export function getEventsEndpoint(config: InitConfig): string | undefined {
1813
+ return config.endpoint
1814
+ ? normalizeEndpoint(config.endpoint) + "/v1/events"
1815
+ : undefined;
1816
+ }
1817
+
1818
+ export async function mintBucketingHandoverPayload(
1819
+ config: InitConfig,
1820
+ visitorId: string,
1821
+ assignments: Assignment[],
1822
+ ): Promise<string | null> {
1823
+ if (!config.shopId || !config.ingestSignature || config.externalTracking?.signed === false) {
1824
+ return null;
1825
+ }
1826
+ if (!config.endpoint) return null;
1827
+
1828
+ const timeoutMs = handoverMintTimeoutMs(config.externalTracking);
1829
+ const controller = new AbortController();
1830
+ const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
1831
+ try {
1832
+ const response = await fetch(`${normalizeEndpoint(config.endpoint)}/v1/handover/mint`, {
1833
+ method: "POST",
1834
+ headers: {
1835
+ "Content-Type": "application/json",
1836
+ "X-Drip-Shop": config.ingestShopId || config.shopId,
1837
+ "X-Drip-Signature": config.ingestSignature,
1838
+ },
1839
+ body: JSON.stringify({
1840
+ visitorId,
1841
+ ttlSeconds: config.externalTracking?.handoverTtlSeconds,
1842
+ assignments: assignments.map((assignment) => ({
1843
+ experimentId: assignment.experimentId,
1844
+ variationId: assignment.variationId,
1845
+ })),
1846
+ }),
1847
+ signal: controller.signal,
1848
+ credentials: "omit",
1849
+ keepalive: false,
1850
+ });
1851
+ if (!response.ok) return null;
1852
+ const payload = await response.json() as { token?: unknown };
1853
+ return typeof payload.token === "string" && payload.token ? payload.token : null;
1854
+ } catch {
1855
+ return null;
1856
+ } finally {
1857
+ window.clearTimeout(timeoutId);
1858
+ }
1859
+ }
1860
+
1861
+ export async function verifyBucketingHandoverToken(config: InitConfig, token: string): Promise<unknown> {
1862
+ if (!config.shopId || !config.endpoint) return null;
1863
+ const response = await fetch(`${normalizeEndpoint(config.endpoint)}/v1/handover/verify`, {
1864
+ method: "POST",
1865
+ headers: {
1866
+ "Content-Type": "application/json",
1867
+ "X-Drip-Shop": config.shopId,
1868
+ },
1869
+ body: JSON.stringify({ verifyToken: token }),
1870
+ signal: AbortSignal.timeout(handoverMintTimeoutMs(config.externalTracking)),
1871
+ });
1872
+ if (!response.ok) return null;
1873
+ return (await response.json() as { payload?: unknown }).payload;
1874
+ }
1875
+
1876
+ export function clearScheduledAnalyticsStart(): void {
1877
+ if (state.analyticsTimeoutId !== null) {
1878
+ window.clearTimeout(state.analyticsTimeoutId);
1879
+ state.analyticsTimeoutId = null;
1880
+ }
1881
+ const w = globalThis as Record<string, unknown>;
1882
+ const cancelIdle = w.cancelIdleCallback;
1883
+ if (state.analyticsIdleId !== null && typeof cancelIdle === "function") {
1884
+ (cancelIdle as (id: number) => void)(state.analyticsIdleId);
1885
+ state.analyticsIdleId = null;
1886
+ }
1887
+ }
1888
+
1889
+ export function normalizeDataLayerTriggers(config: InitConfig): DataLayerTriggerSettings[] {
1890
+ return (config.dataLayerTriggers ?? []).filter((trigger) => trigger?.enabled !== false && Boolean(trigger.eventName));
1891
+ }
1892
+
1893
+ export function applyDataLayerMappings(
1894
+ eventPayload: Record<string, unknown>,
1895
+ mappings: DataLayerSignalMapping[] | undefined,
1896
+ ): string[] {
1897
+ const mapped: string[] = [];
1898
+ for (const mapping of mappings ?? []) {
1899
+ if (!mapping?.from || !mapping.to) continue;
1900
+ const value = coerceSignalValue(readPath(eventPayload, mapping.from), mapping.valueType);
1901
+ if (value === undefined) continue;
1902
+ setCapturedSignal(mapping.to, value, {
1903
+ ttlDays: mapping.ttlDays,
1904
+ persist: mapping.persist,
1905
+ source: "data_layer",
1906
+ });
1907
+ mapped.push(mapping.to);
1908
+ }
1909
+ return mapped;
1910
+ }
1911
+
1912
+ export function readNumericPath(payload: Record<string, unknown>, path: string | undefined): number | undefined {
1913
+ const value = coerceSignalValue(readPath(payload, path), "number");
1914
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1915
+ }
1916
+
1917
+ export function readStringPath(payload: Record<string, unknown>, path: string | undefined): string | undefined {
1918
+ const value = coerceSignalValue(readPath(payload, path), "string");
1919
+ return typeof value === "string" && value ? value : undefined;
1920
+ }
1921
+
1922
+ export const RESERVED_DATA_LAYER_REVENUE_KEYS = new Set([
1923
+ "revenue",
1924
+ "source",
1925
+ "dataLayerEvent",
1926
+ "orderId",
1927
+ "currency",
1928
+ "cost",
1929
+ "profit",
1930
+ "goalId",
1931
+ ]);
1932
+
1933
+ export type PendingDataLayerEvent = {
1934
+ trigger: DataLayerTriggerSettings;
1935
+ payload: Record<string, unknown>;
1936
+ dataLayerName: string;
1937
+ gtagNormalized: boolean;
1938
+ rawItem: unknown;
1939
+ };
1940
+
1941
+ export const MAX_PENDING_DATA_LAYER_EVENTS = 50;
1942
+ export const pendingDataLayerEvents: PendingDataLayerEvent[] = [];
1943
+ export const processedDataLayerEntries = new WeakSet<object>();
1944
+
1945
+ export function isDataLayerEntryObject(item: unknown): item is object {
1946
+ return item !== null && (typeof item === "object" || typeof item === "function");
1947
+ }
1948
+
1949
+ export function markDataLayerEntryProcessed(item: unknown): void {
1950
+ if (isDataLayerEntryObject(item)) processedDataLayerEntries.add(item);
1951
+ }
1952
+
1953
+ export function trackDataLayerRevenue(trigger: DataLayerTriggerSettings, payload: Record<string, unknown>): void {
1954
+ const settings = trigger.trackRevenue;
1955
+ if (!settings || settings.enabled === false) return;
1956
+
1957
+ const revenue = readNumericPath(payload, settings.revenueFrom || "value");
1958
+ if (revenue === undefined) return;
1959
+
1960
+ const eventData: Record<string, unknown> = {
1961
+ source: "data_layer",
1962
+ dataLayerEvent: trigger.eventName,
1963
+ };
1964
+ const orderId = readStringPath(payload, settings.orderIdFrom || "transaction_id");
1965
+ const currency = readStringPath(payload, settings.currencyFrom || "currency");
1966
+ const cost = readNumericPath(payload, settings.costFrom);
1967
+ const profit = readNumericPath(payload, settings.profitFrom);
1968
+ if (orderId) eventData.orderId = orderId;
1969
+ if (currency) eventData.currency = currency;
1970
+ if (cost !== undefined) eventData.cost = cost;
1971
+ if (profit !== undefined) eventData.profit = profit;
1972
+
1973
+ for (const mapping of settings.properties ?? []) {
1974
+ if (!mapping?.from || !mapping.to) continue;
1975
+ if (RESERVED_DATA_LAYER_REVENUE_KEYS.has(mapping.to)) continue;
1976
+ const value = coerceSignalValue(readPath(payload, mapping.from), mapping.valueType);
1977
+ if (value !== undefined) eventData[mapping.to] = value;
1978
+ }
1979
+
1980
+ trackRevenue(revenue, eventData);
1981
+ }
1982
+
1983
+ export function trackDataLayerEvent(
1984
+ trigger: DataLayerTriggerSettings,
1985
+ payload: Record<string, unknown>,
1986
+ ): void {
1987
+ const trackEventName = typeof trigger.trackEventName === "string" && trigger.trackEventName.length > 0
1988
+ ? trigger.trackEventName
1989
+ : null;
1990
+ if (trigger.trackPageview === true || trackEventName) {
1991
+ track(trackEventName ?? "pageview", {
1992
+ source: "data_layer",
1993
+ dataLayerEvent: trigger.eventName,
1994
+ ...payload,
1995
+ });
1996
+ }
1997
+
1998
+ trackDataLayerRevenue(trigger, payload);
1999
+ }
2000
+
2001
+ export function enqueuePendingDataLayerEvent(event: PendingDataLayerEvent): void {
2002
+ if (pendingDataLayerEvents.length >= MAX_PENDING_DATA_LAYER_EVENTS) {
2003
+ pendingDataLayerEvents.shift();
2004
+ }
2005
+ pendingDataLayerEvents.push(event);
2006
+ }
2007
+
2008
+ export function drainPendingDataLayerEvents(): void {
2009
+ const queuedEvents = pendingDataLayerEvents.splice(0);
2010
+ for (const event of queuedEvents) {
2011
+ markDataLayerEntryProcessed(event.rawItem);
2012
+ try {
2013
+ trackDataLayerEvent(event.trigger, event.payload);
2014
+ if (event.trigger.reEvaluate !== false) {
2015
+ reevaluateAssignments("dom");
2016
+ }
2017
+ } catch (error) {
2018
+ recordObserverError("data_layer", error);
2019
+ }
2020
+ }
2021
+ }
2022
+
2023
+ export function finishInitializingAssignments(): void {
2024
+ state.initializingAssignments = false;
2025
+ if (state.tracker) drainPendingDataLayerEvents();
2026
+ }
2027
+
2028
+ export function handleDataLayerEvent(
2029
+ trigger: DataLayerTriggerSettings,
2030
+ payload: Record<string, unknown>,
2031
+ dataLayerName: string,
2032
+ rawItem: unknown,
2033
+ gtagNormalized = false,
2034
+ ): void {
2035
+ const mappedSignals = applyDataLayerMappings(payload, trigger.mappings);
2036
+ state.lastDataLayerTrigger = {
2037
+ eventName: trigger.eventName,
2038
+ dataLayerName,
2039
+ at: Date.now(),
2040
+ mappedSignals,
2041
+ ...(gtagNormalized ? { gtagNormalized: true } : {}),
2042
+ };
2043
+
2044
+ if (state.initializingAssignments) {
2045
+ enqueuePendingDataLayerEvent({
2046
+ trigger,
2047
+ payload,
2048
+ dataLayerName,
2049
+ gtagNormalized,
2050
+ rawItem,
2051
+ });
2052
+ return;
2053
+ }
2054
+
2055
+ markDataLayerEntryProcessed(rawItem);
2056
+ trackDataLayerEvent(trigger, payload);
2057
+
2058
+ if (trigger.reEvaluate !== false) {
2059
+ reevaluateAssignments("dom");
2060
+ }
2061
+ }
2062
+
2063
+ export function recordObserverError(source: "route" | "data_layer" | "dom", error: unknown): void {
2064
+ const message = error instanceof Error ? error.message : String(error);
2065
+ state.observerErrors = {
2066
+ count: state.observerErrors.count + 1,
2067
+ last: { source, message, at: Date.now() },
2068
+ };
2069
+ debugLog("spa", `${source} observer error`, { message });
2070
+ }
2071
+