@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,1828 @@
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 {
17
+ applyAntiFlicker,
18
+ bootedTooLateForAntiFlicker,
19
+ removeAntiFlicker,
20
+ } from "./antiflicker";
21
+ import {
22
+ clearPersistedAssignments,
23
+ findPersistedVariation,
24
+ persistAssignmentForExperiment,
25
+ prunePersistedAssignments,
26
+ removePersistedAssignmentsForExperiments,
27
+ __assignmentStoreInternals,
28
+ } from "./assignment-store";
29
+ import { cleanupBucketingStorage } from "./bucketing-cleanup";
30
+ import { chooseVariation, inTrafficAllocation } from "./bucket";
31
+ import { buildAssignment, decideAssignments } from "./decide-assignments";
32
+ import { weekdayScheduleBlocksDelivery } from "./weekday-schedule";
33
+ import { CONSENT_REQUIRED, DNT_BLOCKED, evaluateSynchronousConsent, readConfiguredConsent, readPath, resolveSynchronousConsentGranted } from "./consent-evaluation";
34
+ import { clearEarlyInteractions, drainBootstrapEarlyInteractions, drainEarlyInteractions } from "./early-interaction-capture";
35
+ import { getCwvMetrics, markCwvReveal, startCwvMeasurement, waitForCwvMetrics } from "./cwv";
36
+ import { collectDeliveryTiming } from "./delivery-timing";
37
+ import {
38
+ startStorefrontVitals,
39
+ stopStorefrontVitals,
40
+ type StorefrontVitalsContext,
41
+ } from "./storefront-vitals";
42
+ import {
43
+ browserGroupStickyStore,
44
+ clearBrowserGroupStickyStore,
45
+ resolveExclusionGroups,
46
+ type ExclusionGroupDecision,
47
+ type PersistedGroupSelection,
48
+ } from "./exclusion-groups";
49
+ import type { CompactGroupSelections } from "./persistence-decoders";
50
+ import {
51
+ applyCommercePrice,
52
+ isCommercePriceWorkPending,
53
+ cleanupCommercePrice,
54
+ hasCommercePriceState,
55
+ COMMERCE_PRICE_SELECTORS,
56
+ isCommercePriceActive,
57
+ isCommercePriceExposureEligible,
58
+ notifyCommerceCartStampConfirmed,
59
+ setCommercePriceExposureListener,
60
+ } from "./commerce-price";
61
+ import {
62
+ applyCommerceShipping,
63
+ classifyShippingExposure,
64
+ cleanupCommerceShipping,
65
+ isShippingCartEligible,
66
+ isShippingCartEligibilityConfirmed,
67
+ } from "./commerce-shipping";
68
+ import {
69
+ addCssOwners,
70
+ injectCss,
71
+ injectExperimentCss,
72
+ injectExternalCss,
73
+ removeCssForOwner,
74
+ } from "./css";
75
+ import { configureDebug, debugLog, debugWarn, debugGroup, debugGroupEnd, isDebugEnabled } from "./debug";
76
+ import { loadDebugBuild } from "./debug-loader";
77
+ import { observeDomChanges } from "./dom-trigger";
78
+ import { edgeRuntimeActive, readEdgeRuntimeAssignments } from "./edge-runtime";
79
+ import { normalizeDataLayerEntry } from "./data-layer-normalize";
80
+ import { interceptEditorLaunch, isEditorDevicePreview } from "./editor-bootstrap";
81
+ import { cleanupGoals, goalWiringKeys, setupGoals, triggerGoal, triggerGoalsForEvent } from "./goals";
82
+ import { clearQaForceFromUrl, isBot, isDuplicate, isIframe, isOptedOut, isQaMode, isScreenshotMode, parseForceParam, persistQaForce, persistUrlQaForce, exitQaMode, getQaSessionId } from "./guards";
83
+ import { adoptUserId, applyHoldoutGate, configureIdentityStorage, getEphemeralUserId, getUserId, hashToFloat, isHoldoutVisitor, variationHashToFloat } from "./hash";
84
+ import { createBucketingHandoverPayload, handoverMintTimeoutMs, hasBucketingHandover, installBucketingHandover, readBucketingHandover } from "./handover";
85
+ import { revealAll } from "./hideflicker";
86
+ import {
87
+ addExternalJsOwners,
88
+ execJs,
89
+ loadExternalJs,
90
+ removeExternalJsForOwner,
91
+ setJsPolicy,
92
+ } from "./js";
93
+ import {
94
+ addAppliedMutationOwners,
95
+ applyMutations,
96
+ applyToElement,
97
+ invalidateArtifact,
98
+ reassertNonStructuralMutations,
99
+ replayArtifact,
100
+ resetAppliedMutationsForOwner,
101
+ } from "./mutate";
102
+ import { registerMutedObserver, runMuted, unregisterMutedObserver } from "./observer-mute";
103
+ import { assignmentsHaveVisualWork, awaitHydration, hasMutationHold, holdMutations, releaseMutations, signalHydrated } from "./hydration";
104
+ import {
105
+ assignmentsHaveCustomJs,
106
+ assignmentsHaveStructuralMutations,
107
+ installEarlyReassert,
108
+ shouldUseEarlyReassert,
109
+ } from "./early-reassert";
110
+ import { trackAppliedMutation } from "./reassert-sentinel";
111
+ import { observeRouteChanges, PAGE_URL_POLL_INTERVAL_MS } from "./router";
112
+ import { buildExposureSignalContext } from "./exposure-signals";
113
+ import {
114
+ buildRedirectDestination,
115
+ clearPendingRedirectAssignmentCarry,
116
+ finalizeRedirectAssignmentCarryHandover,
117
+ hasGeneratedRedirectAssignmentCarry,
118
+ filterUnsafeRedirectAssignments,
119
+ importRedirectAssignmentCarry,
120
+ isRedirectLoop,
121
+ markRedirectAssignmentCarryExposed,
122
+ parseRedirectAssignmentCarry,
123
+ persistPendingRedirectAssignmentCarry,
124
+ readPendingRedirectAssignmentCarry,
125
+ REDIRECT_ASSIGNMENT_PARAM,
126
+ redirectPlanForAssignment,
127
+ stripRedirectAssignmentCarry,
128
+ validateRedirectAssignmentCarry,
129
+ verifyRedirectAssignmentCarry,
130
+ } from "./redirect";
131
+ import type { VerifiedRedirectCarry } from "./redirect";
132
+ import { buildPrerequisiteContext, evaluateProjectPrerequisite } from "./prerequisites";
133
+ import {
134
+ REPLAY_PROGRESS_KEY_PREFIX,
135
+ REPLAY_SAMPLE_KEY_PREFIX,
136
+ resolveReplayRecorderUrl,
137
+ startReplayCapture,
138
+ type ReplayController,
139
+ } from "./replay";
140
+ import {
141
+ buildCartAttributeEntries,
142
+ buildCartMetafieldInputs,
143
+ buildSignedCartAttributeEntries,
144
+ APEX_CART_ATTRIBUTE_KEYS,
145
+ cannotRequestRequiredCommerceAuthorization,
146
+ getCartMetafieldStaleKeys as collectCartMetafieldStaleKeys,
147
+ invalidateShopifyCartAttributeSync,
148
+ normalizeCartMetafieldNamespace,
149
+ syncShopifyCartAttributes,
150
+ type CartAssignment,
151
+ type CartAttributeEntry,
152
+ type CartMetafieldInput,
153
+ type CartAttributeSyncInput,
154
+ } from "./shopify-cart";
155
+ import { onShopifyCartMutation } from "./shopify-cart-events";
156
+ import {
157
+ getPendingGiftChoices,
158
+ noteCartSnapshotForGifts,
159
+ selectGiftOption,
160
+ syncGiftWithPurchase,
161
+ type GiftSyncOptions,
162
+ } from "./commerce-gift";
163
+ import { CART_DRAWER_OPEN_SIGNAL, installSurfaceSignals } from "./surface-signals";
164
+ import {
165
+ assignmentSignature,
166
+ canUseEmptyProjectProfile,
167
+ computeUrlDiffNeeds,
168
+ getBlankScreenMs,
169
+ readPerfFlags,
170
+ resolveActivationMode,
171
+ resolvePlatform,
172
+ resolveRouterMode,
173
+ shouldReevaluateForUrlChange,
174
+ } from "./runtime";
175
+ import type { PerfFlagState } from "./runtime";
176
+ import {
177
+ buildTargetingAttributes,
178
+ evaluateTargetingGatesDetailed,
179
+ matchPageTargetingRule,
180
+ readShopifySignal,
181
+ targetingHasJsTriggers,
182
+ } from "./targeting";
183
+ import { createApexRuntimeTools, type ApexRuntimeTools } from "./runtime-tools";
184
+ import { createEventId, Tracker } from "./track";
185
+ import {
186
+ clearStoredSignals,
187
+ configureSignalPersistenceAllowed,
188
+ clearRuntimeSignal,
189
+ getSignals as getStoredSignals,
190
+ getSignalDiagnostics,
191
+ rememberProduct,
192
+ setCartSignals,
193
+ setRuntimeSignal,
194
+ setSearchSignal,
195
+ setSignal as setStoredSignal,
196
+ } from "./signals";
197
+ import {
198
+ evaluateFlag as evaluateFlagFromExperiments,
199
+ evaluateFlagAssignment as evaluateFlagAssignmentFromExperiments,
200
+ evaluateFlagValue as evaluateFlagValueFromExperiments,
201
+ } from "./flags";
202
+ import {
203
+ ActivationMode,
204
+ AntiFlickerMode,
205
+ Assignment,
206
+ ActivationDiagnostics,
207
+ ActivationGateDiagnostic,
208
+ ConditionDiagnostic,
209
+ ConfigProfile,
210
+ ConsentModeSettings,
211
+ CustomJsPolicy,
212
+ DebugSettings,
213
+ ExecutionMode,
214
+ Experiment,
215
+ ExperimentActivationDiagnostic,
216
+ ExclusionGroupConfig,
217
+ ExclusionGroupMemberConfig,
218
+ Goal,
219
+ InitConfig,
220
+ PageTargetingRule,
221
+ PageActivationDiagnostic,
222
+ PageDeactivationMode,
223
+ PageTriggerMode,
224
+ PageTriggerSettings,
225
+ PerfFeatureFlag,
226
+ PerfFeatureFlags,
227
+ PlatformHint,
228
+ ProjectPrerequisites,
229
+ RouterMode,
230
+ SegmentCondition,
231
+ SpaSettings,
232
+ Targeting,
233
+ ExternalTrackingSettings,
234
+ ProjectCodeSettings,
235
+ RuntimeEnvironmentSettings,
236
+ RuntimeSignalDefinition,
237
+ SdkDiagnostics,
238
+ SnippetCriticalMutation,
239
+ SnippetCriticalMutationPlanItem,
240
+ SnippetDeliveryUrlRule,
241
+ BucketingCleanupSettings,
242
+ DataProtectionSettings,
243
+ DataLayerSignalMapping,
244
+ DataLayerTriggerSettings,
245
+ ExposureDestinationsSettings,
246
+ Mutation,
247
+ TrackEvent,
248
+ TrackingStart,
249
+ Variation,
250
+ } from "./types";
251
+ import {
252
+ addWaitEntryOwners,
253
+ applyMutationWithWait,
254
+ resetWaitEntriesForOwner,
255
+ } from "./wait";import { OrderIdentityPlatform, SDKState, QaAssistantRuntimeSnapshot, PageActivationRuntimeState, PageRulePollController, state, runtimeSharedState } from "./runtime-state";
256
+ import { applyRuntimeAntiFlicker, SDK_SCRIPT_SRC, HeatmapController, SemanticController, FetchAttemptInfo, FetchRetryOptions, DEFAULT_CONFIG_FETCH_TIMEOUT_MS, DEFAULT_CONFIG_FETCH_SLOW_MS, CONFIG_FETCH_RETRY_COUNT, CONFIG_FETCH_RETRY_DELAY_MS, CONFIG_FETCH_MAX_WAIT_MS, CACHED_REDIRECT_HIDE_TIMEOUT_MS, createFetchTimeoutError, fetchWithTimeout, fetchWithRetry, CONFIG_CACHE_KEY_PREFIX, DEFAULT_CONFIG_CACHE_TTL_MS, DEFAULT_ANTI_FLICKER_TIMEOUT_MS, DEFAULT_TRACKING_IDLE_TIMEOUT_MS, DEFAULT_EXECUTION_MODE, DEFAULT_TRACKING_START, DEFAULT_JS_POLICY, PAGE_RULE_POLL_WINDOW_MS, PAGE_RULE_POLL_INTERVAL_MS, CONSENT_COOKIE_NAME, IDENTITY_CONTEXT_COOKIE_NAME, IDENTITY_CONTEXT_COOKIE_MAX_AGE_SECONDS, IDENTITY_CONTEXT_COOKIE_MAX_ENCODED_BYTES, SESSION_STORAGE_KEY, collectScopedSelectors, ResolvedRuntimeConfig, UrlDiffNeeds, ConfigSource, PendingPerfEvent, InitPhaseKey, InitPerfContext, MAX_PERF_DURATION_MS, monotonicNow, boundedPerfDuration, createInitPerfContext, BootstrapPredecision, bootstrapPredecision, addInitPhase, DeliveryHealthStatus, DeliveryHealthTracker, RouteDiagnostic, DataLayerDiagnostic, ShopifySyncDiagnostic, ExposureDestinationDiagnostic, RuntimeLoadDiagnostic, DripPerfWindow, normalizeNumber, resolveRuntimeConfig, resolvePageTriggerMode, resolveDomPageTrigger, dtLog, traceName, traceStep, traceGate, browserDoNotTrackValue, trackingBlocked, storefrontVitalsGateOpen, syncStorefrontVitals, doNotTrackEnabled, storageAllowed, readWindowPath, configuredConsentGranted, syncConfiguredConsent, CUSTOM_CONSENT_POLL_INTERVAL_MS, CUSTOM_CONSENT_POLL_MAX_MS, MANUAL_CALLBACK_TRIGGER_RERUN_INTERVAL_MS, MANUAL_CALLBACK_TRIGGER_RERUN_MAX_MS, MANUAL_CALLBACK_TRIGGER_MAX_RERUNS, EVENT_DRIVEN_CONSENT_PROVIDERS, shouldPollConfiguredConsent, installConsentChangeListeners, resolveConsentRequired, resolveConsentGranted, persistentStorageAllowed, coerceSignalValue, captureUrlSignals, installClickSignalCaptures, captureEventPropertySignals, setCapturedSignal, setPublicSignal, getPublicSignals, normalizePageApiName, pageTriggerSignal, triggerPage, readElementCaptureValue, captureDomAndGlobalSignals, consentAwareDataProtection, writeConsentCookie, readConsentCookie, clearConsentStorage, applyConsentStorageMode, runWhenDomReady, applyProjectCode, resetProjectCode, runProjectStartupCode, collectJsTriggerConditions, collectPageTriggerRules, PageDetectorMode, PageRuleReconcileScope, collectPageDetectorRules, experimentsForGlobalRouteReevaluation, conditionUsesActivateTrigger, buildJsTriggerContext, runJsTriggerReset, normalizeActivationValue, installActivateJsTrigger, installPageTriggerRule, installJsTriggerObserver, notifyQaAssistantRuntimeListeners, HarnessManifest, harnessPollTimer, harnessPollKey, harnessVisibilityInstalled, activeHarnessAssignment, stopHarnessPolling, applyHarnessManifest, pollHarnessManifest, startHarnessPolling, getEventsEndpoint, mintBucketingHandoverPayload, verifyBucketingHandoverToken, clearScheduledAnalyticsStart, normalizeDataLayerTriggers, applyDataLayerMappings, readNumericPath, readStringPath, RESERVED_DATA_LAYER_REVENUE_KEYS, PendingDataLayerEvent, MAX_PENDING_DATA_LAYER_EVENTS, pendingDataLayerEvents, processedDataLayerEntries, isDataLayerEntryObject, markDataLayerEntryProcessed, trackDataLayerRevenue, trackDataLayerEvent, enqueuePendingDataLayerEvent, drainPendingDataLayerEvents, finishInitializingAssignments, handleDataLayerEvent, recordObserverError } from "./config-runtime";
257
+ 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";
258
+ 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";
259
+ 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";
260
+
261
+
262
+ export function loadQaToolbar(experiments: Experiment[]): void {
263
+ if (!FEATURE_QA) return;
264
+ try {
265
+ import("./qa-toolbar").then(({ mountQaToolbar }) => {
266
+ mountQaToolbar({
267
+ experiments,
268
+ assignments: state.assignments,
269
+ forceMap: state.forceMap,
270
+ qaSessionId: state.qaSessionId,
271
+ onSwitchVariation: (experimentId: string, variationId: string) => {
272
+ if (!state.forceMap) state.forceMap = {};
273
+ state.forceMap[experimentId] = variationId;
274
+ persistQaForce(state.forceMap);
275
+ // Re-evaluate with new force map
276
+ if (state.config) {
277
+ const exps = state.config.experiments ?? [];
278
+ teardownTransaction(allAppliedPagesWithModes());
279
+ const evaluation = evaluateWithDiagnostics(exps, {
280
+ url: location.href,
281
+ attributes: state.config.attributes ?? {},
282
+ userId: state.userId!,
283
+ persistSignals: persistentStorageAllowed(),
284
+ storeOrigin: configuredStoreOrigins(state.config),
285
+ exclusionGroups: state.config.exclusionGroups,
286
+ });
287
+ state.assignments = evaluation.assignments;
288
+ state.activationDiagnostics.experiments = evaluation.diagnostics;
289
+ applyAssignments(state.assignments, state.activationMode);
290
+ state.lastAssignmentSignature = assignmentSignature(state.assignments);
291
+ }
292
+ },
293
+ onExitQa: () => {
294
+ exitQaMode();
295
+ location.reload();
296
+ },
297
+ onReloadLatest: () => {
298
+ const assignment = activeHarnessAssignment(state.assignments);
299
+ if (assignment?.harnessSession?.manifestUrl) {
300
+ pollHarnessManifest(assignment.harnessSession.manifestUrl);
301
+ }
302
+ },
303
+ onExitHarness: () => {
304
+ const url = new URL(location.href);
305
+ url.searchParams.delete("drip_harness");
306
+ history.replaceState(null, "", url.toString());
307
+ location.reload();
308
+ },
309
+ });
310
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog("qa", "QA toolbar mounted");
311
+ }).catch(() => {
312
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("qa", "QA toolbar module not available");
313
+ });
314
+ } catch {
315
+ // Dynamic import not supported — skip toolbar
316
+ }
317
+ }
318
+
319
+ export function loadQaAssistant(onUnavailable?: () => void): void {
320
+ if (!FEATURE_QA) {
321
+ onUnavailable?.();
322
+ return;
323
+ }
324
+ try {
325
+ import("./qa-assistant")
326
+ .then(({ mountQaAssistant }) => {
327
+ const view = mountQaAssistant();
328
+ if (!view) onUnavailable?.();
329
+ })
330
+ .catch(() => onUnavailable?.());
331
+ } catch {
332
+ // Dynamic import not supported — fall back to the legacy toolbar.
333
+ onUnavailable?.();
334
+ }
335
+ }
336
+
337
+ export function loadDevtoolsIfEnabled(experiments: Experiment[]): void {
338
+ if (!FEATURE_DEVTOOLS || state.screenshotMode || !persistentStorageAllowed()) return;
339
+ try {
340
+ import("./devtools").then(({ isDevtoolsEnabled }) => {
341
+ if (isDevtoolsEnabled()) loadDevtools(experiments);
342
+ }).catch(() => {});
343
+ } catch {
344
+ // Dynamic import not supported.
345
+ }
346
+ }
347
+
348
+ export function loadDevtools(experiments: Experiment[]): void {
349
+ if (!FEATURE_DEVTOOLS) return;
350
+ try {
351
+ import("./devtools").then(({ mountDevtools }) => {
352
+ mountDevtools({
353
+ experiments,
354
+ assignments: state.assignments,
355
+ diagnostics: getDiagnostics(),
356
+ forceMap: state.forceMap,
357
+ onForceVariation: (experimentId: string, variationId: string) => {
358
+ if (!state.forceMap) state.forceMap = {};
359
+ state.forceMap[experimentId] = variationId;
360
+ // Re-evaluate with new force map
361
+ if (state.config) {
362
+ const exps = state.config.experiments ?? [];
363
+ teardownTransaction(allAppliedPagesWithModes());
364
+ const evaluation = evaluateWithDiagnostics(exps, {
365
+ url: location.href,
366
+ attributes: state.config.attributes ?? {},
367
+ userId: state.userId!,
368
+ persistSignals: persistentStorageAllowed(),
369
+ storeOrigin: configuredStoreOrigins(state.config),
370
+ exclusionGroups: state.config.exclusionGroups,
371
+ });
372
+ state.assignments = evaluation.assignments;
373
+ state.activationDiagnostics.experiments = evaluation.diagnostics;
374
+ applyAssignments(state.assignments, state.activationMode);
375
+ state.lastAssignmentSignature = assignmentSignature(state.assignments);
376
+ // Re-mount devtools with updated state
377
+ import("./devtools").then(({ mountDevtools: remount }) => {
378
+ remount({
379
+ experiments: exps,
380
+ assignments: state.assignments,
381
+ diagnostics: getDiagnostics(),
382
+ forceMap: state.forceMap,
383
+ onForceVariation: () => {}, // Will be replaced on next full load
384
+ });
385
+ }).catch(() => {});
386
+ }
387
+ },
388
+ });
389
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog("init", "Devtools overlay mounted");
390
+ __DRIP_FEATURE_DEVTOOLS__ && dtLog("init", "Devtools overlay mounted");
391
+ }).catch(() => {
392
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("init", "Devtools module not available");
393
+ });
394
+ } catch {
395
+ // Dynamic import not supported — skip devtools
396
+ }
397
+ }
398
+
399
+ export function init(config: InitConfig): Assignment[] {
400
+ installGlobalApi();
401
+ markDripPerf("sdk_init_start");
402
+ runtimeSharedState.runtimeBootedTooLateForAntiFlicker ??= bootedTooLateForAntiFlicker();
403
+ if (
404
+ (config.runtimeFeatures?.sdkVariant === "live" || config.runtimeFeatures?.sdkVariant === "heatmap")
405
+ && loadDebugBuild(config.debug, config.snippetUrl ?? SDK_SCRIPT_SRC, () => init(config))
406
+ ) return [];
407
+ // `drip_force=clear` is an escape hatch and must run before cookie-backed QA
408
+ // detection so a poisoned reference tab cannot silently re-enter QA mode.
409
+ clearQaForceFromCurrentUrl();
410
+ // Remove redirect carry from the visible URL before any guard can return.
411
+ // Keep the original URL in memory so the normal init path can still validate
412
+ // and import the carry after consent and runtime guards have passed.
413
+ const initialPageUrl =
414
+ typeof location !== "undefined" ? location.href : undefined;
415
+ if (initialPageUrl) stripRedirectAssignmentCarry(initialPageUrl);
416
+
417
+ if (typeof window !== "undefined" && isEditorDevicePreview(window)) {
418
+ revealAll();
419
+ removeAntiFlicker();
420
+ return [];
421
+ }
422
+ if (interceptEditorLaunch({
423
+ configuredBundleUrl: config.editorBundleUrl,
424
+ scriptSrc: SDK_SCRIPT_SRC,
425
+ pageUrl: typeof location !== "undefined" ? location.href : undefined,
426
+ onBeforeLoad: () => {
427
+ revealAll();
428
+ removeAntiFlicker();
429
+ },
430
+ onFailure: () => {
431
+ init(config);
432
+ },
433
+ })) {
434
+ return [];
435
+ }
436
+
437
+ const perfContext = createInitPerfContext();
438
+ const earlyConsentRequired = resolveConsentRequired(config);
439
+ const earlyConsentGranted = resolveConsentGranted(config, earlyConsentRequired);
440
+ const earlyTrackingDisabledByDnt = doNotTrackEnabled(config);
441
+ const earlyStorageAllowed = storageAllowed(earlyConsentRequired, earlyConsentGranted, earlyTrackingDisabledByDnt);
442
+ configureIdentityStorage(earlyStorageAllowed ? config.dataProtection : { ...(config.dataProtection ?? {}), storageMode: "memory" });
443
+ const earlyUserId = config.userId ?? null;
444
+ configureDebug(config.debug, earlyUserId, { storageAllowed: earlyStorageAllowed });
445
+ // Guards
446
+ if (isDuplicate()) {
447
+ state.consentRequired = earlyConsentRequired;
448
+ state.consentGranted = earlyConsentGranted;
449
+ state.trackingDisabledByDnt = earlyTrackingDisabledByDnt;
450
+ state.screenshotMode = isScreenshotMode();
451
+ const firstInstall = readRuntimeInstallClaim();
452
+ const attemptedInstallSurface = telemetryLabel(config.installSurface, "direct_script");
453
+ if (
454
+ firstInstall
455
+ && firstInstall.installSurface !== attemptedInstallSurface
456
+ && claimInstallConflictEmission()
457
+ ) {
458
+ sendRuntimeEventNow(config, "sdk_install_conflict", {
459
+ first_install_surface: firstInstall.installSurface,
460
+ attempted_install_surface: attemptedInstallSurface,
461
+ }, earlyUserId);
462
+ }
463
+ return [];
464
+ }
465
+ rememberRuntimeInstallClaim(config);
466
+ if (isBot()) { __DRIP_FEATURE_DEVTOOLS__ && debugLog("guard", "Blocked: bot detected"); __DRIP_FEATURE_DEVTOOLS__ && dtLog("guard", "Blocked: bot detected"); return []; }
467
+ if (!config.spa?.allowIframes && isIframe()) { __DRIP_FEATURE_DEVTOOLS__ && debugLog("guard", "Blocked: iframe detected"); __DRIP_FEATURE_DEVTOOLS__ && dtLog("guard", "Blocked: iframe detected"); return []; }
468
+ if (isOptedOut({ storageAllowed: earlyStorageAllowed })) { __DRIP_FEATURE_DEVTOOLS__ && debugLog("guard", "Blocked: opted out"); __DRIP_FEATURE_DEVTOOLS__ && dtLog("guard", "Blocked: opted out"); return []; }
469
+
470
+ resetLoadDiagnostics("boot");
471
+ const runtimeConfig = resolveRuntimeConfig(config);
472
+ addInitPhase(perfContext, "guard_resolve_ms", perfContext.startedAt);
473
+
474
+ if (config.geo?.country) {
475
+ setRuntimeSignal("geo.country", config.geo.country, "edge");
476
+ }
477
+
478
+ // In edge-only mode, the client SDK should not evaluate or mutate.
479
+ if (runtimeConfig.executionMode === "edge") {
480
+ if (!edgeRuntimeActive() && typeof console !== "undefined") {
481
+ console.warn("[drip] executionMode='edge' but no edge runtime detected; skipping client init");
482
+ }
483
+ return [];
484
+ }
485
+
486
+ // Only a committed re-init resets the session attribution ledger. A guarded
487
+ // no-op init (bot, iframe, duplicate, edge) must not wipe labels the session
488
+ // already earned.
489
+ runtimeSharedState.attributionLedger.clear();
490
+
491
+ // Parse once before the visible URL is cleaned. A cached config may predate
492
+ // the redirect experiment or its assignment epoch, so the parsed carry must
493
+ // remain available to the authoritative network refresh.
494
+ const urlRedirectCarry = initialPageUrl
495
+ ? parseRedirectAssignmentCarry(initialPageUrl)
496
+ : null;
497
+ if (earlyStorageAllowed) {
498
+ persistPendingRedirectAssignmentCarry(urlRedirectCarry);
499
+ } else {
500
+ clearPendingRedirectAssignmentCarry();
501
+ }
502
+ const parsedRedirectCarry = urlRedirectCarry
503
+ ?? (earlyStorageAllowed ? readPendingRedirectAssignmentCarry() : null);
504
+ const verifiedRedirectCarryPromise =
505
+ parsedRedirectCarry && config.shopId && config.endpoint
506
+ ? verifyRedirectAssignmentCarry(
507
+ parsedRedirectCarry,
508
+ config.shopId,
509
+ (token) => verifyBucketingHandoverToken(config, token),
510
+ )
511
+ : Promise.resolve(null);
512
+
513
+ // If experiments are provided inline, use them directly. An empty array is a
514
+ // valid published project state and still needs analytics for install checks.
515
+ if (Array.isArray(config.experiments)) {
516
+ if (
517
+ (parsedRedirectCarry && config.shopId && config.endpoint) ||
518
+ (config.shopId && hasBucketingHandover(config.externalTracking))
519
+ ) {
520
+ const handoverPromise = config.shopId
521
+ ? readBucketingHandover(
522
+ config.externalTracking,
523
+ config.shopId,
524
+ (token) => verifyBucketingHandoverToken(config, token),
525
+ )
526
+ : Promise.resolve(null);
527
+ void Promise.all([
528
+ verifiedRedirectCarryPromise,
529
+ handoverPromise,
530
+ ]).then(([verifiedRedirectCarry, handover]) => {
531
+ initWithExperiments(
532
+ config,
533
+ config.experiments!,
534
+ "inline",
535
+ perfContext,
536
+ verifiedRedirectCarry,
537
+ handover,
538
+ );
539
+ });
540
+ return [];
541
+ }
542
+ const assignments = initWithExperiments(
543
+ config,
544
+ config.experiments,
545
+ "inline",
546
+ perfContext,
547
+ );
548
+ return assignments;
549
+ }
550
+
551
+ // If shopId is provided, fetch experiments from the API
552
+ if (config.shopId && config.endpoint) {
553
+ state.consentRequired = earlyConsentRequired;
554
+ state.consentGranted = earlyConsentGranted;
555
+ state.trackingDisabledByDnt = earlyTrackingDisabledByDnt;
556
+ const forceMap = parseForceParam();
557
+ const qaMode = isQaMode();
558
+ const screenshotMode = isScreenshotMode();
559
+ const serializedForceMap = serializeForceMap(forceMap);
560
+ const environmentKey = config.environments?.activeKey || "production";
561
+ const configUrl = new URL(normalizeEndpoint(config.endpoint) + "/config");
562
+ configUrl.searchParams.set("shopId", config.shopId);
563
+ if (environmentKey !== "production") {
564
+ configUrl.searchParams.set("env", environmentKey);
565
+ }
566
+ const qaRequested = qaMode || screenshotMode;
567
+ if (qaRequested) {
568
+ configUrl.searchParams.set("drip_qa", "1");
569
+ if (serializedForceMap) {
570
+ configUrl.searchParams.set("drip_force", serializedForceMap);
571
+ const harnessToken = new URLSearchParams(location.search).get("drip_harness");
572
+ if (harnessToken) {
573
+ configUrl.searchParams.set("drip_harness", harnessToken);
574
+ }
575
+ }
576
+ }
577
+ // QA responses are request-scoped and can contain diagnostics omitted from
578
+ // the ordinary cached config, so interactive QA always fetches fresh data.
579
+ const useConfigCache = earlyStorageAllowed && config.configCache !== false && !qaRequested;
580
+ const cacheTtlMs = getConfigCacheTtlMs(config);
581
+ const cached = useConfigCache
582
+ ? readCachedExperiments(config.shopId, cacheTtlMs, environmentKey)
583
+ : null;
584
+ let navigateDeferredCachedRedirect: (() => boolean) | null = null;
585
+ const handoverPromise = readBucketingHandover(
586
+ config.externalTracking,
587
+ config.shopId,
588
+ (token) => verifyBucketingHandoverToken(config, token),
589
+ );
590
+
591
+ if (cached) {
592
+ updateLoadDiagnostics({
593
+ stage: "config_cache_hit",
594
+ configFetchStatus: "idle",
595
+ });
596
+ const cachedConfig = cached.runtime
597
+ ? withRuntimeSettings(config, cached.runtime, cached.profile)
598
+ : { ...config, profile: cached.profile ?? config.profile };
599
+ if (cached.geo?.country && !config.geo?.country) {
600
+ setRuntimeSignal("geo.country", cached.geo.country, "edge");
601
+ }
602
+ // Cached applies used to be effectively instant, making flicker cover
603
+ // pointless — but under after_hydration (or a pre-init hold) the apply
604
+ // is deferred, so the configured anti-flicker must stay on or repeat
605
+ // visitors see the original DOM until the gate resolves.
606
+ const cachedApplyDeferred =
607
+ cachedConfig.spa?.applyTiming === "after_hydration" || hasMutationHold();
608
+ const initCachedConfig = (
609
+ verifiedHandover: Awaited<ReturnType<typeof readBucketingHandover>>,
610
+ ) => {
611
+ initWithExperiments(
612
+ {
613
+ ...cachedConfig,
614
+ storeOrigin: cached.storeOrigin ?? cachedConfig.storeOrigin,
615
+ storeOrigins: cached.storeOrigins ?? cachedConfig.storeOrigins,
616
+ antiFlickerMode: cachedApplyDeferred ? cachedConfig.antiFlickerMode : "off",
617
+ exclusionGroups: cached.exclusionGroups,
618
+ signals: cached.signals ?? config.signals,
619
+ commerce: cached.commerce,
620
+ geo: cached.geo ?? cachedConfig.geo,
621
+ holdout_v1_enabled: cached.holdout?.[0],
622
+ holdout_pct: cached.holdout?.[1],
623
+ holdout_config_updated_at: cached.holdout?.[2],
624
+ },
625
+ cached.experiments,
626
+ "cache",
627
+ perfContext,
628
+ cachedVerifiedRedirectCarry,
629
+ verifiedHandover,
630
+ );
631
+ const cachedRuntimeConfig = state.config;
632
+ const cachedRuntimeExperiments =
633
+ cachedRuntimeConfig?.experiments ?? cached.experiments;
634
+ if (!cachedRuntimeConfig) return;
635
+ const cachedRedirectPlan = planRedirectAssignment(
636
+ cachedRuntimeConfig,
637
+ cachedRuntimeExperiments,
638
+ state.assignments,
639
+ );
640
+ if (!cachedRedirectPlan) return;
641
+ // Cached init deliberately defers redirects until the authoritative
642
+ // refresh confirms them. Hold a full-page SDK guard for that bounded
643
+ // wait so the snippet's independent safety timeout cannot paint the
644
+ // source page before confirmation. Explicitly replace any shorter
645
+ // cached-init guard: applyAntiFlicker otherwise preserves its live
646
+ // timer rather than extending it.
647
+ removeAntiFlicker();
648
+ applyRuntimeAntiFlicker(
649
+ "full",
650
+ CACHED_REDIRECT_HIDE_TIMEOUT_MS,
651
+ );
652
+ navigateDeferredCachedRedirect = () => {
653
+ if (
654
+ state.config !== cachedRuntimeConfig ||
655
+ state.configSource !== "cache"
656
+ ) {
657
+ return false;
658
+ }
659
+ return navigateForRedirectAssignment(
660
+ cachedRuntimeConfig,
661
+ cachedRuntimeExperiments,
662
+ state.assignments,
663
+ );
664
+ };
665
+ };
666
+ let cachedVerifiedRedirectCarry: VerifiedRedirectCarry | null = null;
667
+ if (parsedRedirectCarry && config.shopId && config.endpoint) {
668
+ void Promise.all([
669
+ verifiedRedirectCarryPromise,
670
+ handoverPromise,
671
+ ]).then(([verifiedRedirectCarry, verifiedHandover]) => {
672
+ cachedVerifiedRedirectCarry = verifiedRedirectCarry;
673
+ initCachedConfig(verifiedHandover);
674
+ });
675
+ } else if (hasBucketingHandover(config.externalTracking)) {
676
+ void handoverPromise.then(initCachedConfig);
677
+ } else {
678
+ initCachedConfig(null);
679
+ }
680
+ }
681
+
682
+ // Always apply anti-flicker during async fetch to prevent FOUC. Forced QA
683
+ // fetches may not have scoped selectors until the fresh config arrives, so
684
+ // use a QA-only full-page guard instead of letting the page paint stale UI.
685
+ const asyncFetchAntiFlickerIntended = !cached && runtimeConfig.antiFlickerMode !== "off";
686
+ if (asyncFetchAntiFlickerIntended) {
687
+ const asyncFetchAntiFlickerMode =
688
+ runtimeConfig.antiFlickerMode === "scoped" && (qaMode || screenshotMode) && serializedForceMap
689
+ ? "full"
690
+ : runtimeConfig.antiFlickerMode;
691
+ applyRuntimeAntiFlicker(
692
+ asyncFetchAntiFlickerMode,
693
+ runtimeConfig.antiFlickerTimeoutMs,
694
+ undefined,
695
+ runtimeConfig.antiFlickerCustomCss,
696
+ );
697
+ }
698
+
699
+ const configFetchStartedAt = Date.now();
700
+ if (!cached) perfContext.includesConfigFetch = true;
701
+ const antiFlickerFetchBudgetMs = asyncFetchAntiFlickerIntended
702
+ ? Math.max(runtimeConfig.antiFlickerTimeoutMs, 300)
703
+ : null;
704
+ const hardRevealDeadline = antiFlickerFetchBudgetMs !== null
705
+ ? configFetchStartedAt + antiFlickerFetchBudgetMs
706
+ : null;
707
+ updateLoadDiagnostics({
708
+ stage: "config_fetch_started",
709
+ configFetchStartedAt,
710
+ configFetchAttempts: 0,
711
+ configFetchStatus: "loading",
712
+ });
713
+
714
+ fetchWithRetry(configUrl.toString(), CONFIG_FETCH_RETRY_COUNT, CONFIG_FETCH_RETRY_DELAY_MS, {
715
+ timeoutMs: DEFAULT_CONFIG_FETCH_TIMEOUT_MS,
716
+ firstAttemptTimeoutMs: antiFlickerFetchBudgetMs !== null
717
+ ? Math.min(DEFAULT_CONFIG_FETCH_TIMEOUT_MS, antiFlickerFetchBudgetMs)
718
+ : DEFAULT_CONFIG_FETCH_TIMEOUT_MS,
719
+ slowMs: DEFAULT_CONFIG_FETCH_SLOW_MS,
720
+ onAttemptStart: (attempt) => {
721
+ updateLoadDiagnostics({
722
+ stage: "config_fetch_attempt",
723
+ configFetchStartedAt,
724
+ configFetchAttempts: attempt,
725
+ configFetchStatus: attempt === 1 ? "loading" : state.load.configFetchStatus,
726
+ });
727
+ },
728
+ onSlow: ({ attempt, elapsedMs }) => {
729
+ updateLoadDiagnostics({
730
+ stage: "config_fetch_slow",
731
+ configFetchStartedAt,
732
+ configFetchAttempts: attempt,
733
+ configFetchMs: Date.now() - configFetchStartedAt,
734
+ configFetchStatus: "slow",
735
+ });
736
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("init", `Config fetch attempt ${attempt} is slow after ${elapsedMs}ms`);
737
+ },
738
+ onAttemptEnd: (info) => {
739
+ updateLoadDiagnostics({
740
+ stage: "config_fetch_attempt_complete",
741
+ configFetchStartedAt,
742
+ configFetchAttempts: info.attempt,
743
+ configFetchMs: Date.now() - configFetchStartedAt,
744
+ configFetchStatus: info.timedOut ? "timeout" : state.load.configFetchStatus,
745
+ configFetchStatusCode: info.status,
746
+ configFetchError: info.error,
747
+ });
748
+ },
749
+ })
750
+ .then((res) => {
751
+ if (!res.ok) throw new Error("Failed to fetch config: " + res.status);
752
+ return res.json();
753
+ })
754
+ .then(async (data: {
755
+ experiments?: Experiment[];
756
+ storeOrigin?: string;
757
+ storeOrigins?: string[];
758
+ exclusionGroups?: ExclusionGroupConfig[];
759
+ profile?: InitConfig["profile"];
760
+ runtime?: unknown;
761
+ signals?: RuntimeSignalDefinition[];
762
+ commerce?: InitConfig["commerce"];
763
+ qa_warnings?: InitConfig["qa_warnings"];
764
+ holdout_v1_enabled?: boolean;
765
+ holdout_pct?: number;
766
+ holdout_config_updated_at?: string | null;
767
+ geo?: { country?: string };
768
+ }) => {
769
+ const [verifiedRedirectCarry, verifiedHandover] = await Promise.all([
770
+ verifiedRedirectCarryPromise,
771
+ handoverPromise,
772
+ ]);
773
+ const configFetchCompletedAt = Date.now();
774
+ updateLoadDiagnostics({
775
+ stage: "config_fetch_complete",
776
+ configFetchStartedAt,
777
+ configFetchCompletedAt,
778
+ configFetchMs: configFetchCompletedAt - configFetchStartedAt,
779
+ configFetchStatus: "ok",
780
+ });
781
+ const experiments = data.experiments ?? [];
782
+ const storeOrigin =
783
+ typeof data.storeOrigin === "string"
784
+ ? data.storeOrigin
785
+ : config.storeOrigin;
786
+ const storeOrigins = Array.isArray(data.storeOrigins)
787
+ ? data.storeOrigins.filter(
788
+ (origin): origin is string => typeof origin === "string",
789
+ )
790
+ : config.storeOrigins;
791
+ const exclusionGroups = Array.isArray(data.exclusionGroups)
792
+ ? data.exclusionGroups
793
+ : undefined;
794
+ const profile = data.profile;
795
+ const runtime = data.runtime;
796
+ const signals = Array.isArray(data.signals) ? data.signals : config.signals;
797
+ // Omission is authoritative: it represents an empty commerce plane and
798
+ // must clear any cached/active treatment rather than inherit init input.
799
+ const commerce = data.commerce;
800
+ const geo = data.geo;
801
+ const countryChanged = Boolean(
802
+ geo?.country && getStoredSignals({ markVisitorSeen: false })["geo.country"] !== geo.country,
803
+ );
804
+ if (geo?.country) setRuntimeSignal("geo.country", geo.country, "edge");
805
+ const configWithRuntime = {
806
+ ...withRuntimeSettings(config, runtime, profile),
807
+ storeOrigin,
808
+ storeOrigins,
809
+ exclusionGroups,
810
+ signals,
811
+ commerce,
812
+ qa_warnings: data.qa_warnings,
813
+ holdout_v1_enabled: data.holdout_v1_enabled,
814
+ holdout_pct: data.holdout_pct,
815
+ holdout_config_updated_at: data.holdout_config_updated_at,
816
+ };
817
+ const runtimeConsentRequired = resolveConsentRequired(configWithRuntime);
818
+ const runtimeStorageAllowed = storageAllowed(
819
+ runtimeConsentRequired,
820
+ resolveConsentGranted(configWithRuntime, runtimeConsentRequired),
821
+ doNotTrackEnabled(configWithRuntime),
822
+ );
823
+ if (useConfigCache && runtimeStorageAllowed) {
824
+ writeCachedExperiments(
825
+ config.shopId!,
826
+ experiments,
827
+ storeOrigin,
828
+ storeOrigins,
829
+ exclusionGroups,
830
+ profile,
831
+ runtime,
832
+ signals,
833
+ commerce,
834
+ data.holdout_v1_enabled,
835
+ data.holdout_pct,
836
+ data.holdout_config_updated_at,
837
+ geo,
838
+ environmentKey,
839
+ );
840
+ }
841
+ const incomingHash = hashConfigPayload(
842
+ experiments,
843
+ storeOrigin,
844
+ storeOrigins,
845
+ exclusionGroups,
846
+ profile,
847
+ runtime,
848
+ signals,
849
+ commerce,
850
+ data.holdout_v1_enabled,
851
+ data.holdout_pct,
852
+ data.holdout_config_updated_at,
853
+ );
854
+ const cacheUnchanged = cached && cached.hash && cached.hash === incomingHash && !countryChanged;
855
+ if (cacheUnchanged && !verifiedHandover) {
856
+ // The cached apply may still be gated behind hydration; revealing
857
+ // now would undo the anti-flicker kept for exactly that window.
858
+ // The gated applyAssignments() reveals when it runs.
859
+ const navigated = navigateDeferredCachedRedirect?.() ?? false;
860
+ if (!state.pendingInitialApply && !navigated) removeAntiFlicker();
861
+ return;
862
+ }
863
+ const lateConfigPolicy = ((qaMode || screenshotMode) && serializedForceMap)
864
+ ? "apply"
865
+ : config.lateConfigPolicy ?? "skip";
866
+ const arrivedAfterReveal = hardRevealDeadline !== null && configFetchCompletedAt > hardRevealDeadline;
867
+ if (arrivedAfterReveal && lateConfigPolicy === "skip") {
868
+ const elapsedMs = configFetchCompletedAt - configFetchStartedAt;
869
+ updateLoadDiagnostics({
870
+ stage: "late_config_skip",
871
+ configFetchStartedAt,
872
+ configFetchCompletedAt,
873
+ configFetchMs: elapsedMs,
874
+ configFetchStatus: "ok",
875
+ });
876
+ __DRIP_FEATURE_DEVTOOLS__ && dtLog("init", "late_config_skip", { elapsedMs });
877
+ removeAntiFlicker();
878
+ return;
879
+ }
880
+ // Remove the initial barrier before re-init. Preserve the configured
881
+ // mode so fetched non-commerce mutations get the same scoped hiding as
882
+ // inline mutations; commerce modules can add selectors to that scope.
883
+ removeAntiFlicker();
884
+ initWithExperiments(
885
+ configWithRuntime,
886
+ experiments,
887
+ "network",
888
+ cached ? createInitPerfContext() : perfContext,
889
+ verifiedRedirectCarry,
890
+ verifiedHandover,
891
+ );
892
+ })
893
+ .catch((err) => {
894
+ const configFetchCompletedAt = Date.now();
895
+ const error = err instanceof Error ? err.message : String(err);
896
+ updateLoadDiagnostics({
897
+ stage: "config_fetch_failed",
898
+ configFetchStartedAt,
899
+ configFetchCompletedAt,
900
+ configFetchMs: configFetchCompletedAt - configFetchStartedAt,
901
+ configFetchStatus: state.load.configFetchStatus === "timeout" ? "timeout" : "failed",
902
+ configFetchError: error,
903
+ });
904
+ if (typeof console !== "undefined") {
905
+ console.error("[drip] Failed to fetch experiments:", err);
906
+ }
907
+ sendRuntimePerfNow(config, {
908
+ perf_event_kind: "init",
909
+ assignment_ready_ms: 0,
910
+ // domContentLoadedEventStart - navigationStart. Keep the old alias
911
+ // for one release while Tinybird consumers move to the clear name.
912
+ dom_content_loaded_ms: getBlankScreenMs(),
913
+ blank_screen_ms: getBlankScreenMs(),
914
+ ...collectDeliveryTiming(numberPerf(readDripPerf(), "sdk_init_start"), SDK_SCRIPT_SRC),
915
+ sdk_variant: telemetryLabel(config.runtimeFeatures?.sdkVariant, "live"),
916
+ platform: resolvePlatform(config),
917
+ config_source: cached ? "cache" : "none",
918
+ config_fetch_status: state.load.configFetchStatus,
919
+ config_fetch_ms: state.load.configFetchMs,
920
+ config_fetch_attempts: state.load.configFetchAttempts,
921
+ config_fetch_error: error,
922
+ activation_mode: resolveActivationMode(config),
923
+ delivery_state: typeof config.deliveryState === "string" ? config.deliveryState : undefined,
924
+ script_cache_source: typeof config.scriptCacheSource === "string" ? config.scriptCacheSource : undefined,
925
+ }, earlyUserId);
926
+ // A cached treatment can still be waiting behind hydration. Keep its
927
+ // barrier on fetch failure; the gated apply reveals when it completes.
928
+ const navigated = navigateDeferredCachedRedirect?.() ?? false;
929
+ if (!state.pendingInitialApply && !navigated) removeAntiFlicker();
930
+ });
931
+
932
+ return []; // Assignments will be applied async
933
+ }
934
+
935
+ // No experiments and no shopId – just initialize tracker
936
+ const assignments = initWithExperiments(
937
+ config,
938
+ [],
939
+ "none",
940
+ perfContext,
941
+ );
942
+ return assignments;
943
+ }
944
+
945
+ export function trackEvent(
946
+ type: string,
947
+ data?: Record<string, unknown>,
948
+ experimentId?: string,
949
+ variationId?: string,
950
+ eventId?: string,
951
+ forceUnassigned = false,
952
+ timestamp?: number,
953
+ ): void {
954
+ if (state.screenshotMode) return;
955
+ if (trackingBlocked()) return;
956
+ if (state.pendingInitialApply) {
957
+ // A held SPA route still has the previous route's assignments in state.
958
+ // Replay every event after the gate, even when the tracker is already
959
+ // running, so attribution and goal evaluation use the refreshed route.
960
+ void state.pendingInitialApply.then(() => {
961
+ trackEvent(type, data, experimentId, variationId, eventId, forceUnassigned, timestamp);
962
+ });
963
+ return;
964
+ }
965
+ captureEventPropertySignals(type, data);
966
+ startAnalyticsIfReady();
967
+ if (!state.tracker) {
968
+ return;
969
+ }
970
+ const activeAssignment = forceUnassigned
971
+ ? undefined
972
+ : state.assignments.find(exposable) ?? state.assignments.find(attributable);
973
+ const resolvedExperimentId = forceUnassigned ? undefined : experimentId ?? activeAssignment?.experimentId;
974
+ const resolvedVariationId = forceUnassigned ? undefined : variationId ?? activeAssignment?.variationId;
975
+ // Empty types keep their existing quarantine path; normalizing them would
976
+ // silently invent a "custom_event" the customer never sent.
977
+ const wireType =
978
+ EVENT_TYPE_PATTERN.test(type) || !type.trim() ? type : normalizeEventName(type);
979
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog("track", `${type}`, { experimentId: resolvedExperimentId, variationId: resolvedVariationId, data });
980
+ __DRIP_FEATURE_DEVTOOLS__ && dtLog("track", `${type}`, { experimentId: resolvedExperimentId, variationId: resolvedVariationId, ...data });
981
+ const event: TrackEvent = {
982
+ type: wireType,
983
+ data: withAssignmentEpochData(data, resolvedExperimentId, resolvedVariationId),
984
+ url: cleanUrl(),
985
+ experimentId: resolvedExperimentId,
986
+ variationId: resolvedVariationId,
987
+ event_id: eventId,
988
+ ts: timestamp,
989
+ };
990
+ state.tracker.track(event);
991
+ if (wireType !== "goal" && state.config) {
992
+ triggerGoalsForEvent(
993
+ wireType,
994
+ state.assignments.filter(attributable),
995
+ state.config.experiments ?? [],
996
+ data,
997
+ );
998
+ }
999
+ }
1000
+
1001
+ export function track(
1002
+ type: string,
1003
+ data?: Record<string, unknown>,
1004
+ experimentId?: string,
1005
+ variationId?: string,
1006
+ ): void {
1007
+ trackEvent(type, data, experimentId, variationId);
1008
+ }
1009
+
1010
+ export function funnelStepStorageKey(stepName: string): string {
1011
+ const path =
1012
+ typeof location !== "undefined"
1013
+ ? `${location.origin ?? ""}${location.pathname ?? ""}`
1014
+ : "unknown";
1015
+ return `drip:funnel_step:${path}:${stepName}`.slice(0, 240);
1016
+ }
1017
+
1018
+ export function shouldTrackFunnelStep(stepName: string, oncePerSession: boolean): boolean {
1019
+ if (!oncePerSession || typeof sessionStorage === "undefined") return true;
1020
+ const key = funnelStepStorageKey(stepName);
1021
+ try {
1022
+ if (sessionStorage.getItem(key) === "1") return false;
1023
+ sessionStorage.setItem(key, "1");
1024
+ } catch {
1025
+ return true;
1026
+ }
1027
+ return true;
1028
+ }
1029
+
1030
+ /**
1031
+ * Track a virtual funnel step that has no reliable browser event by default,
1032
+ * such as a postcode form submit, embedded quiz step, or third-party handoff.
1033
+ */
1034
+ export function trackStep(name: string, data?: Record<string, unknown>): void {
1035
+ const stepName = name.trim().slice(0, 160);
1036
+ if (!stepName) return;
1037
+ if (state.screenshotMode || trackingBlocked()) return;
1038
+ if (state.pendingInitialApply) {
1039
+ // Virtual funnel steps can fire from data layers before the hydration
1040
+ // gate creates the tracker. Replay after apply so the step is preserved
1041
+ // and attributed to the refreshed assignment set.
1042
+ void state.pendingInitialApply.then(() => {
1043
+ trackStep(stepName, data);
1044
+ });
1045
+ return;
1046
+ }
1047
+ if (!state.tracker) return;
1048
+ const oncePerSession = data?.oncePerSession !== false;
1049
+ if (!shouldTrackFunnelStep(stepName, oncePerSession)) return;
1050
+ const payload = { ...(data ?? {}) };
1051
+ delete payload.oncePerSession;
1052
+ track("funnel_step", {
1053
+ ...payload,
1054
+ source: "virtual_step",
1055
+ step_name: stepName,
1056
+ path: typeof location !== "undefined" ? location.pathname : undefined,
1057
+ });
1058
+ }
1059
+
1060
+ export function trackGoal(goalId: string, data?: Record<string, unknown>): void {
1061
+ if (state.screenshotMode) return;
1062
+ if (trackingBlocked()) return;
1063
+ if (!state.config) return;
1064
+ if (state.pendingInitialApply) {
1065
+ // Goal wiring is installed after the gated initial apply; firing now
1066
+ // would be a silent no-op. Replay once the gate opens, like track().
1067
+ void state.pendingInitialApply.then(() => {
1068
+ trackGoal(goalId, data);
1069
+ });
1070
+ return;
1071
+ }
1072
+ __DRIP_FEATURE_DEVTOOLS__ && debugLog("goal", `Triggering goal: ${goalId}`, data);
1073
+ __DRIP_FEATURE_DEVTOOLS__ && dtLog("goal", `Goal: ${goalId}`, data as Record<string, unknown> | undefined);
1074
+ triggerGoal(
1075
+ goalId,
1076
+ state.assignments.filter(attributable),
1077
+ state.config.experiments ?? [],
1078
+ data,
1079
+ );
1080
+ }
1081
+
1082
+ export function setConsent(granted: boolean): void {
1083
+ state.consentGranted = Boolean(granted);
1084
+ writeConsentCookie(state.consentGranted);
1085
+ if (!state.consentGranted) {
1086
+ clearEarlyInteractions();
1087
+ }
1088
+
1089
+ if (!state.config) {
1090
+ if (!state.consentGranted) clearConsentStorage();
1091
+ return;
1092
+ }
1093
+ applyConsentStorageMode();
1094
+ recheckPrerequisites();
1095
+ // Stop live capture before the purge so discard teardown cannot restore replay progress.
1096
+ if (!state.consentGranted) clearConsentStorage();
1097
+ if (state.consentGranted) {
1098
+ removeSdkCartMintTrigger();
1099
+ syncActiveAssignmentToShopifyCart();
1100
+ scheduleShopifyAssignmentSync();
1101
+ }
1102
+ }
1103
+
1104
+ export function hold(): number {
1105
+ return holdMutations();
1106
+ }
1107
+
1108
+ export function release(): number {
1109
+ return releaseMutations();
1110
+ }
1111
+
1112
+ export function hydrated(): void {
1113
+ signalHydrated();
1114
+ }
1115
+
1116
+ export function recheckPrerequisites(): void {
1117
+ if (!state.config) return;
1118
+ const experiments = state.config.experiments ?? [];
1119
+ const emptyProjectFastPath = state.config.runtimeProfile === "empty_project"
1120
+ && state.perfFlags.perf_empty_project_v1;
1121
+ state.trackingPrerequisiteMet = emptyProjectFastPath || trackingAllowed(state.config);
1122
+ syncStorefrontVitals();
1123
+ if (!emptyProjectFastPath) updateProjectActivationDiagnostics(state.config);
1124
+ reevaluateAssignments("prerequisite");
1125
+
1126
+ if (trackingBlocked() || !state.trackingPrerequisiteMet) {
1127
+ stopAnalyticsStage();
1128
+ return;
1129
+ }
1130
+
1131
+ if (state.trackingStart === "idle") {
1132
+ scheduleAnalyticsStart(state.config, experiments);
1133
+ } else {
1134
+ startAnalyticsStage(state.config, experiments);
1135
+ }
1136
+ }
1137
+
1138
+ export function reInit(forceReapply = false): Assignment[] {
1139
+ if (!runtimeSharedState.clearingQaForceFromRoute) {
1140
+ runtimeSharedState.clearingQaForceFromRoute = true;
1141
+ try {
1142
+ if (clearQaForceFromUrl()) {
1143
+ location.reload();
1144
+ return state.assignments;
1145
+ }
1146
+ } finally {
1147
+ runtimeSharedState.clearingQaForceFromRoute = false;
1148
+ }
1149
+ }
1150
+ const assignments = reevaluateAssignments("manual", forceReapply);
1151
+ state.orderIdentityDetectionUrl = "";
1152
+ state.orderIdentityDetection = undefined;
1153
+ if (state.config) scheduleOrderIdentityBeacon(state.config);
1154
+ return assignments;
1155
+ }
1156
+
1157
+ /**
1158
+ * Track a revenue/purchase event. Convenience method that fires a "purchase" event
1159
+ * with revenue data for all active experiments.
1160
+ *
1161
+ * Usage: drip.trackRevenue(49.99) or drip.trackRevenue(49.99, { orderId: "123", currency: "EUR" })
1162
+ */
1163
+ export type RevenueEventData = Record<string, unknown> & { currency?: string };
1164
+
1165
+ export function normalizeRevenueCurrency(value: unknown): string | undefined {
1166
+ if (typeof value !== "string") return undefined;
1167
+ const currency = value.trim().toUpperCase();
1168
+ return /^[A-Z]{3}$/.test(currency) ? currency : undefined;
1169
+ }
1170
+
1171
+ export function trackRevenue(revenue: number, data?: RevenueEventData): void {
1172
+ if (typeof revenue !== "number" || !Number.isFinite(revenue) || revenue < 0) {
1173
+ if (typeof console !== "undefined") {
1174
+ console.warn("[drip] trackRevenue ignored invalid revenue; expected a finite, non-negative number");
1175
+ }
1176
+ return;
1177
+ }
1178
+ if (state.screenshotMode) return;
1179
+ if (trackingBlocked()) return;
1180
+ if (state.pendingInitialApply) {
1181
+ // Initial and route hydration gates defer tracker creation/assignment
1182
+ // refresh. Replay purchases after the gate so early checkout data-layer
1183
+ // events are not dropped or attributed to stale assignments.
1184
+ void state.pendingInitialApply.then(() => {
1185
+ trackRevenue(revenue, data);
1186
+ });
1187
+ return;
1188
+ }
1189
+ if (!state.tracker) return;
1190
+ const eventData: Record<string, unknown> = { ...data, revenue };
1191
+ if (data && "currency" in data) {
1192
+ const currency = normalizeRevenueCurrency(data.currency);
1193
+ if (currency) {
1194
+ eventData.currency = currency;
1195
+ } else {
1196
+ delete eventData.currency;
1197
+ eventData.currency_raw = data.currency;
1198
+ eventData.money_error = "invalid_currency";
1199
+ }
1200
+ }
1201
+ // A purchase is one semantic event even though attribution fans it out to
1202
+ // every active assignment. Sharing its ID lets program-level analytics
1203
+ // collapse the fanout while assignment-scoped analytics retains each copy.
1204
+ const eventId = createEventId();
1205
+ // Fire for each clean, delivery-resolved experiment assignment. Preserve
1206
+ // the purchase as an unassigned shop-level event while mutation delivery is
1207
+ // pending rather than attaching it to a variant that may still roll back.
1208
+ const attributableAssignments = state.assignments.filter(attributable);
1209
+ if (attributableAssignments.length === 0) {
1210
+ // Keep purchases that occurred while a real assignment was suppressed,
1211
+ // plus both holdout-v1 cohorts, at shop scope. A visitor with no assignment
1212
+ // under ordinary non-holdout bucketing remains silent as before.
1213
+ if (state.assignments.length > 0 || state.config?.holdout_v1_enabled === true) {
1214
+ trackEvent("goal", { ...eventData, goalId: "__revenue__" }, undefined, undefined, eventId, true);
1215
+ }
1216
+ return;
1217
+ }
1218
+ for (const a of attributableAssignments) {
1219
+ trackEvent("goal", { ...eventData, goalId: "__revenue__" }, a.experimentId, a.variationId, eventId);
1220
+ }
1221
+ }
1222
+
1223
+ export function flush(): Promise<void> {
1224
+ if (state.screenshotMode) return Promise.resolve();
1225
+ const pendingApply = state.pendingInitialApply;
1226
+ if (pendingApply) {
1227
+ // Deferred public tracking callbacks are registered before the caller's
1228
+ // flush(). Wait for the gate, then recurse so every replayed event has
1229
+ // entered the tracker queue before the returned promise resolves.
1230
+ return pendingApply.then(() => flush());
1231
+ }
1232
+ startAnalyticsIfReady();
1233
+ return state.tracker ? state.tracker.flush() : Promise.resolve();
1234
+ }
1235
+
1236
+ export function getAssignments(): Assignment[] {
1237
+ return state.assignments;
1238
+ }
1239
+
1240
+ export function buildCurrentCartAttributes(scopedInput: CartAttributeSyncInput | null): CartAttributeEntry[] {
1241
+ if (scopedInput) {
1242
+ return buildCartAttributeEntries(scopedInput)
1243
+ .filter(({ key }) => key === "apex_assignments");
1244
+ }
1245
+ const input = buildSdkCartMintInput()
1246
+ ?? buildActiveCartAttributeSyncInput();
1247
+ return input ? buildCartAttributeEntries(input) : [];
1248
+ }
1249
+
1250
+ export function getCartAttributes(): CartAttributeEntry[] {
1251
+ return buildCurrentCartAttributes(buildScopedCartAttributeSyncInput());
1252
+ }
1253
+
1254
+ export function getCartMetafields(options?: { namespace?: string }): CartMetafieldInput[] {
1255
+ const input = buildActiveCartAttributeSyncInput();
1256
+ if (!input) return [];
1257
+ const namespace = normalizeCartMetafieldNamespace(options?.namespace);
1258
+ return buildCartMetafieldInputs(input, namespace);
1259
+ }
1260
+
1261
+ /** Return cart-metafield keys that must be deleted to clear the previous mirror. */
1262
+ export function getCartMetafieldStaleKeys(options?: { namespace?: string }): string[] {
1263
+ const namespace = normalizeCartMetafieldNamespace(options?.namespace);
1264
+ return collectCartMetafieldStaleKeys(getCartMetafields({ namespace }), namespace);
1265
+ }
1266
+
1267
+ /** Replace entries supplied by the fresh Apex stamp while preserving valid cart attributes. */
1268
+ export function mergeCartAttributes(existing: unknown): CartAttributeEntry[] {
1269
+ const scopedInput = buildScopedCartAttributeSyncInput();
1270
+ const scopedActive = scopedInput !== null;
1271
+ const fresh = buildCurrentCartAttributes(scopedInput);
1272
+ const freshKeys = new Set(fresh.map((entry) => entry.key));
1273
+ const replacedKeys = sdkCartMintAllowed() || scopedActive
1274
+ ? APEX_CART_ATTRIBUTE_KEYS
1275
+ : freshKeys;
1276
+ const preserved: CartAttributeEntry[] = [];
1277
+
1278
+ for (const entry of Array.isArray(existing) ? existing : []) {
1279
+ if (!entry || typeof entry !== "object") continue;
1280
+ const { key, value } = entry as { key?: unknown; value?: unknown };
1281
+ if (typeof key !== "string" || typeof value !== "string" || replacedKeys.has(key)) continue;
1282
+ preserved.push({ key, value });
1283
+ }
1284
+
1285
+ return [...preserved, ...fresh];
1286
+ }
1287
+
1288
+ export function normalizedHeadlessCartToken(value: unknown): string | null {
1289
+ if (typeof value !== "string") return null;
1290
+ const withoutQuery = value.trim().split("?", 1)[0] ?? "";
1291
+ const token = withoutQuery.startsWith("gid://shopify/Cart/")
1292
+ ? withoutQuery.slice("gid://shopify/Cart/".length)
1293
+ : withoutQuery;
1294
+ return token.length > 0 && token.length <= 4096 ? token : null;
1295
+ }
1296
+
1297
+ /**
1298
+ * Build cart attributes for a headless Shopify Storefront API cart.
1299
+ *
1300
+ * Integrator contract: pass the cart's full Storefront Cart GID (including its
1301
+ * `?key=` secret), write every returned entry to that same cart, and verify the
1302
+ * Storefront API mutation succeeded with no `userErrors`. Only then call
1303
+ * {@link confirmCartAttributesWritten} with the same Cart GID. Returning signed
1304
+ * attributes does not authorize treatment display or exposure by itself. An
1305
+ * empty result is fail-closed and must not be replaced with unsigned attributes.
1306
+ * Theme storefronts use the SDK-managed `/cart/update.js` path and do not call
1307
+ * either headless API.
1308
+ */
1309
+ export async function getSignedCartAttributes(
1310
+ cartToken?: string,
1311
+ ): Promise<CartAttributeEntry[]> {
1312
+ const requiresCohortPin = requiresCommerceCohortPin();
1313
+ const input = buildActiveCartAttributeSyncInput(requiresCohortPin);
1314
+ if (!input) return [];
1315
+ const headless = ensureHeadlessCartAuthorizationState();
1316
+ const generation = ++headless.generation;
1317
+ // A public headless write takes ownership from any SDK-managed theme sync
1318
+ // that was still resolving when the integrator selected this cart.
1319
+ runtimeSharedState.shopifyCartSyncGeneration += 1;
1320
+ headless.confirmationPending = requiresCohortPin;
1321
+ headless.pending = null;
1322
+ if (requiresCohortPin) {
1323
+ state.authorizedPriceAssignmentKeys?.clear();
1324
+ state.authorizedShippingAssignmentKeys?.clear();
1325
+ // A headless generation for a (possibly different) cart revokes the
1326
+ // previous cart's offer grant and retires its rendered treatment — cart
1327
+ // B was never stamped, so keeping cart A's offer DOM would show an offer
1328
+ // Shopify will not charge if B's authorization or write fails.
1329
+ revokeOffersDisplayAuthorization();
1330
+ state.lastShopifyCartSync = {
1331
+ at: Date.now(),
1332
+ status: "pending",
1333
+ assignmentCount: state.assignments.length,
1334
+ };
1335
+ cleanupCommercePrice();
1336
+ cleanupCommerceShipping();
1337
+ }
1338
+ let priceAssignments: CartAssignment[] = [];
1339
+ let shippingAssignments: CartAssignment[] = [];
1340
+ let offersAssignments: CartAssignment[] = [];
1341
+ let offersAdoptionAssignments: CartAssignment[] | null = null;
1342
+ const entries = await buildSignedCartAttributeEntries({
1343
+ ...input,
1344
+ cartToken,
1345
+ authorizePriceDisplayAssignments: (assignments) => {
1346
+ priceAssignments = assignments;
1347
+ },
1348
+ authorizeShippingDisplayAssignments: (assignments) => {
1349
+ shippingAssignments = assignments;
1350
+ },
1351
+ authorizeOffersDisplayAssignments: (assignments) => {
1352
+ offersAssignments = assignments;
1353
+ },
1354
+ stageOffersAdoptionAssignments: (assignments) => {
1355
+ offersAdoptionAssignments = assignments;
1356
+ },
1357
+ });
1358
+ if (generation !== headless.generation) return [];
1359
+ const normalizedCartToken = normalizedHeadlessCartToken(cartToken);
1360
+ if (entries.length === 0 && requiresCohortPin) {
1361
+ headless.confirmationPending = false;
1362
+ state.lastShopifyCartSync = {
1363
+ at: Date.now(),
1364
+ status: "failed",
1365
+ reason: "authorization_failed",
1366
+ assignmentCount: state.assignments.length,
1367
+ };
1368
+ // Reconcile from the render plane instead of a hand-picked list; the cart
1369
+ // step is skipped because this runs inside the cart-authorization path.
1370
+ syncGlobalRuntimes({ skipCartSync: true });
1371
+ return [];
1372
+ }
1373
+ if (entries.length > 0 && requiresCohortPin && normalizedCartToken) {
1374
+ headless.pending = {
1375
+ generation,
1376
+ cartToken: normalizedCartToken,
1377
+ priceAssignments,
1378
+ shippingAssignments,
1379
+ offersAssignments,
1380
+ offersAdoptionAssignments,
1381
+ };
1382
+ // Authorization/adoption may rebuild commerce render state while the proof
1383
+ // request resolves; keep the generated-but-unacknowledged state published.
1384
+ // Reconcile from the render plane instead of a hand-picked list; the cart
1385
+ // step is skipped because this runs inside the cart-authorization path.
1386
+ syncGlobalRuntimes({ skipCartSync: true });
1387
+ }
1388
+ return entries;
1389
+ }
1390
+
1391
+ /**
1392
+ * Acknowledge that the latest signed headless cart attributes were persisted.
1393
+ *
1394
+ * Call this only after the Storefront API cart-attributes mutation succeeded
1395
+ * and returned no `userErrors`. The Cart GID must match the one passed to the
1396
+ * latest {@link getSignedCartAttributes} call. Stale-generation, mismatched,
1397
+ * or repeated acknowledgements are no-ops and cannot grant or change display
1398
+ * and exposure authorization. When a staged offer adoption can no longer
1399
+ * apply, the acknowledgement is rejected (`false`) with retryable pending
1400
+ * state and no authorization is granted.
1401
+ *
1402
+ * @returns `true` only when the latest matching acknowledgement was accepted.
1403
+ */
1404
+ export function confirmCartAttributesWritten(cartToken?: string): boolean {
1405
+ const normalizedCartToken = normalizedHeadlessCartToken(cartToken);
1406
+ const headless = headlessCartAuthorizationState;
1407
+ const pending = headless?.pending;
1408
+ if (
1409
+ !normalizedCartToken ||
1410
+ !headless ||
1411
+ !pending ||
1412
+ pending.generation !== headless.generation ||
1413
+ pending.cartToken !== normalizedCartToken
1414
+ ) return false;
1415
+
1416
+ // The integrator has confirmed the stamped cart write — only now may a
1417
+ // staged offers adoption reach display, persistence, and tracking. When the
1418
+ // adoption can no longer apply (config changed between generation and
1419
+ // acknowledgement), keep the pending state retryable, withhold every
1420
+ // authorization grant, and tell the integrator the acknowledgement was NOT
1421
+ // accepted so stale local assignments cannot be exposed as confirmed.
1422
+ if (
1423
+ pending.offersAdoptionAssignments &&
1424
+ !adoptAuthoritativeOffersAssignments(pending.offersAdoptionAssignments)
1425
+ ) {
1426
+ state.lastShopifyCartSync = {
1427
+ at: Date.now(),
1428
+ status: "failed",
1429
+ reason: "offers_adoption_failed",
1430
+ assignmentCount: state.assignments.length,
1431
+ };
1432
+ return false;
1433
+ }
1434
+ headless.pending = null;
1435
+ headless.confirmationPending = false;
1436
+ state.authorizedPriceAssignmentKeys = new Set(
1437
+ pending.priceAssignments.map(priceAssignmentAuthorizationKey),
1438
+ );
1439
+ state.authorizedShippingAssignmentKeys = new Set(
1440
+ pending.shippingAssignments.map(priceAssignmentAuthorizationKey),
1441
+ );
1442
+ // Same render path as the theme flow: withheld offer companion DOM must
1443
+ // appear once the integrator's write is acknowledged, or exposure could
1444
+ // record a treatment whose DOM never rendered while Shopify charges it.
1445
+ authorizeOffersDisplayAndRender(pending.offersAssignments);
1446
+ state.lastShopifyCartSync = {
1447
+ at: Date.now(),
1448
+ status: "sent",
1449
+ assignmentCount: state.assignments.length,
1450
+ };
1451
+ // Same rule as the authorization path: the acknowledged write republishes
1452
+ // the render plane, never a page-independent assignment list.
1453
+ syncGlobalRuntimes({ skipCartSync: true });
1454
+ if (state.analyticsStarted && state.config && !trackingBlocked()) {
1455
+ trackViewedAssignments(state.config);
1456
+ }
1457
+ return true;
1458
+ }
1459
+
1460
+ export function getExperiments(): Experiment[] {
1461
+ return state.config?.experiments ?? [];
1462
+ }
1463
+
1464
+ export function getGoals(): Goal[] {
1465
+ const seen = new Set<string>();
1466
+ const goals: Goal[] = [];
1467
+ for (const experiment of getExperiments()) {
1468
+ for (const goal of experiment.goals ?? []) {
1469
+ if (seen.has(goal.id)) continue;
1470
+ seen.add(goal.id);
1471
+ goals.push(goal);
1472
+ }
1473
+ }
1474
+ return goals;
1475
+ }
1476
+
1477
+ export function getActiveExperimentIds(): Record<string, string> {
1478
+ const ids: Record<string, string> = {};
1479
+ for (const assignment of state.assignments) {
1480
+ ids[assignment.experimentId] = assignment.variationId;
1481
+ }
1482
+ return ids;
1483
+ }
1484
+
1485
+ export function getRuntimeValue(key: string): unknown {
1486
+ switch (key) {
1487
+ case "tools":
1488
+ return getRuntimeTools();
1489
+ case "experiments":
1490
+ return getExperiments();
1491
+ case "goals":
1492
+ return getGoals();
1493
+ case "assignments":
1494
+ return getAssignments();
1495
+ case "activeAssignments":
1496
+ return getAssignments();
1497
+ case "activeExperimentIds":
1498
+ return getActiveExperimentIds();
1499
+ case "orderAttribution":
1500
+ // Same consent chokepoint as the order-identity beacon: while tracking
1501
+ // is blocked, no identity or assignments leave the SDK on any surface.
1502
+ if (state.screenshotMode || trackingBlocked()) {
1503
+ return { visitorId: "", assignments: [] };
1504
+ }
1505
+ return {
1506
+ visitorId: state.userId ?? "",
1507
+ assignments: mergedOrderIdentityAssignments(),
1508
+ ...(state.shopwareAttributionEnvelope ? { envelope: state.shopwareAttributionEnvelope } : {}),
1509
+ };
1510
+ case "signals":
1511
+ return getPublicSignals({ markVisitorSeen: false });
1512
+ case "diagnostics":
1513
+ return getDiagnostics();
1514
+ case "data":
1515
+ return {
1516
+ initialized: Boolean(state.config),
1517
+ shopId: state.config?.shopId ?? null,
1518
+ experimentsLoaded: state.config?.experiments?.length ?? 0,
1519
+ };
1520
+ default:
1521
+ return undefined;
1522
+ }
1523
+ }
1524
+
1525
+ export function getRuntimeTools(): ApexRuntimeTools {
1526
+ return createApexRuntimeTools({
1527
+ document: typeof document !== "undefined" ? document : undefined,
1528
+ window: typeof window !== "undefined" ? window : undefined,
1529
+ location: typeof location !== "undefined" ? location : undefined,
1530
+ track,
1531
+ trackGoal,
1532
+ signal: setPublicSignal,
1533
+ reInit,
1534
+ get: getRuntimeValue,
1535
+ });
1536
+ }
1537
+
1538
+ export function buildFlagContext(
1539
+ context?: { url?: string; attributes?: Record<string, unknown>; userId?: string },
1540
+ ) {
1541
+ const url = context?.url ?? (typeof location !== "undefined" ? location.href : "");
1542
+ return {
1543
+ url,
1544
+ attributes: buildTargetingAttributes(
1545
+ { ...(state.config?.attributes ?? {}), ...(context?.attributes ?? {}) },
1546
+ url,
1547
+ { persistSignals: persistentStorageAllowed() },
1548
+ ),
1549
+ userId: context?.userId ?? state.userId ?? undefined,
1550
+ forceMap: state.forceMap ?? undefined,
1551
+ };
1552
+ }
1553
+
1554
+ export function evaluateFlag(
1555
+ flagId: string,
1556
+ context?: { url?: string; attributes?: Record<string, unknown>; userId?: string },
1557
+ ): boolean {
1558
+ return evaluateFlagFromExperiments(flagId, getExperiments(), buildFlagContext(context));
1559
+ }
1560
+
1561
+ export function evaluateFlagAssignment(
1562
+ flagId: string,
1563
+ context?: { url?: string; attributes?: Record<string, unknown>; userId?: string },
1564
+ ): Assignment | null {
1565
+ return evaluateFlagAssignmentFromExperiments(
1566
+ flagId,
1567
+ getExperiments(),
1568
+ buildFlagContext(context),
1569
+ );
1570
+ }
1571
+
1572
+ export function evaluateFlagValue<T = unknown>(
1573
+ flagId: string,
1574
+ fallback?: T,
1575
+ context?: { url?: string; attributes?: Record<string, unknown>; userId?: string },
1576
+ ): T | unknown {
1577
+ return evaluateFlagValueFromExperiments(
1578
+ flagId,
1579
+ getExperiments(),
1580
+ buildFlagContext(context),
1581
+ fallback,
1582
+ );
1583
+ }
1584
+
1585
+ export function getDiagnostics(): SdkDiagnostics {
1586
+ const config = state.config;
1587
+ const runtimeConfig = state.runtimeConfig;
1588
+ const storageMode = config?.dataProtection?.storageMode ?? "localStorage";
1589
+ const signalDiagnostics = getSignalDiagnostics({ markVisitorSeen: persistentStorageAllowed() });
1590
+ const diagnostics: SdkDiagnostics = {
1591
+ mode: "public",
1592
+ initialized: Boolean(config),
1593
+ shopId: config?.shopId ?? null,
1594
+ userIdPresent: Boolean(state.userId),
1595
+ assignments: state.assignments.map((assignment) => {
1596
+ const kind =
1597
+ assignment.kind ??
1598
+ (assignment.personalization
1599
+ ? "personalization"
1600
+ : assignment.featureFlag
1601
+ ? "feature_flag"
1602
+ : "experiment");
1603
+ return {
1604
+ experimentId: assignment.experimentId,
1605
+ variationId: assignment.variationId,
1606
+ kind,
1607
+ isControl: assignment.isControl,
1608
+ mutationCount: assignment.mutations.length,
1609
+ featureFlagKey: assignment.featureFlag?.key,
1610
+ personalizationKey: assignment.personalization?.key,
1611
+ personalizationRuleId: assignment.personalization?.ruleId,
1612
+ personalizationExperienceId: assignment.personalization?.experienceId,
1613
+ personalizationSurface: assignment.personalization?.surface,
1614
+ personalizationTemplateKey: assignment.personalization?.templateKey,
1615
+ personalizationPriority: assignment.personalization?.priority,
1616
+ personalizationStopAfterMatch: assignment.personalization?.stopAfterMatch,
1617
+ value: assignment.value,
1618
+ };
1619
+ }),
1620
+ pageActivation: pageActivationDiagnosticRows({
1621
+ includePageNames: isDebugEnabled(),
1622
+ }),
1623
+ experimentsLoaded: config?.experiments?.length ?? 0,
1624
+ analyticsStarted: state.analyticsStarted,
1625
+ trackerStarted: Boolean(state.tracker),
1626
+ activation: state.activationDiagnostics,
1627
+ qaMode: state.qaMode,
1628
+ qaSessionId: state.qaSessionId,
1629
+ screenshotMode: state.screenshotMode,
1630
+ configSource: state.configSource,
1631
+ load: { ...state.load },
1632
+ platform: state.platform,
1633
+ routerMode: state.routerMode,
1634
+ activationMode: state.activationMode,
1635
+ storage: {
1636
+ mode: storageMode,
1637
+ localStorageAvailable: storageAvailable("localStorage"),
1638
+ sessionStorageAvailable: storageAvailable("sessionStorage"),
1639
+ cookiesEnabled: typeof navigator !== "undefined" ? navigator.cookieEnabled !== false : false,
1640
+ },
1641
+ runtime: {
1642
+ antiFlickerMode: runtimeConfig?.antiFlickerMode,
1643
+ antiFlickerTimeoutMs: runtimeConfig?.antiFlickerTimeoutMs,
1644
+ trackingStart: runtimeConfig?.trackingStart,
1645
+ pageTriggerMode: state.pageTriggerMode,
1646
+ customJsPolicy: runtimeConfig?.customJsPolicy,
1647
+ externalHandoverDomains: config?.externalTracking?.domains ?? [],
1648
+ handoverQueryParam: config?.externalTracking?.handoverQueryParam ?? "drip_bucket",
1649
+ handoverMintTimeoutMs: config?.externalTracking?.handoverMintTimeoutMs,
1650
+ runtimeDisabled: config?.runtimeControl?.disabled === true,
1651
+ runtimeDisabledAt: config?.runtimeControl?.disabledAt,
1652
+ },
1653
+ spa: {
1654
+ lastUrl: state.lastUrl,
1655
+ lastRouteChange: state.lastRouteChange ?? undefined,
1656
+ lastDataLayerTrigger: state.lastDataLayerTrigger ?? undefined,
1657
+ observerErrors: state.observerErrors,
1658
+ lastShopifyAssignmentSync: state.lastShopifyAssignmentSync ?? undefined,
1659
+ lastShopifyCartSync: state.lastShopifyCartSync ?? undefined,
1660
+ },
1661
+ signals: {
1662
+ values: signalDiagnostics.values,
1663
+ sources: signalDiagnostics.sources,
1664
+ },
1665
+ integrations: {
1666
+ exposureDestinations: state.exposureDestinationDiagnostics,
1667
+ },
1668
+ };
1669
+
1670
+ if (isDebugEnabled()) {
1671
+ diagnostics.mode = "debug";
1672
+ diagnostics.trackingBlocked = trackingBlocked();
1673
+ diagnostics.trackingDisabledByDnt = state.trackingDisabledByDnt;
1674
+ diagnostics.trackingPrerequisiteMet = state.trackingPrerequisiteMet;
1675
+ diagnostics.consentRequired = state.consentRequired;
1676
+ diagnostics.consentGranted = state.consentGranted;
1677
+ diagnostics.consentModeEnabled = state.consentModeEnabled;
1678
+ }
1679
+
1680
+ return diagnostics;
1681
+ }
1682
+
1683
+ export // --- Command queue ---
1684
+ type QueueCommand =
1685
+ | { type: "trackGoal"; goalId: string; data?: Record<string, unknown> }
1686
+ | { type: "trackRevenue"; revenue: number; data?: RevenueEventData }
1687
+ | { type: "track"; event: string; data?: Record<string, unknown> }
1688
+ | { type: "step"; name: string; data?: Record<string, unknown> }
1689
+ | { type: "signal"; key: string; value: unknown; ttlDays?: number }
1690
+ | { type: "triggerPage"; apiName: string; active?: unknown }
1691
+ | { type: "rememberProduct"; product: Parameters<typeof rememberProduct>[0]; ttlDays?: number }
1692
+ | { type: "setCart"; itemCount?: number; productIds?: string[]; ttlDays?: number }
1693
+ | { type: "setSearch"; query: string; ttlDays?: number }
1694
+ | { type: "setConsent"; granted: boolean }
1695
+ | { type: "hold" }
1696
+ | { type: "release" }
1697
+ | { type: "hydrated" }
1698
+ | { type: "reInit"; force?: boolean }
1699
+ | { type: "recheckPrerequisites" }
1700
+ | { type: "init"; config: InitConfig };
1701
+
1702
+ export function processCommand(cmd: QueueCommand): void {
1703
+ switch (cmd.type) {
1704
+ case "init":
1705
+ init(cmd.config);
1706
+ break;
1707
+ case "track":
1708
+ track(cmd.event, cmd.data);
1709
+ break;
1710
+ case "step":
1711
+ trackStep(cmd.name, cmd.data);
1712
+ break;
1713
+ case "trackGoal":
1714
+ trackGoal(cmd.goalId, cmd.data);
1715
+ break;
1716
+ case "trackRevenue":
1717
+ trackRevenue(cmd.revenue, cmd.data);
1718
+ break;
1719
+ case "signal":
1720
+ setPublicSignal(cmd.key, cmd.value, cmd.ttlDays);
1721
+ break;
1722
+ case "triggerPage":
1723
+ triggerPage(cmd.apiName, cmd.active);
1724
+ break;
1725
+ case "rememberProduct":
1726
+ rememberProduct(cmd.product, cmd.ttlDays);
1727
+ break;
1728
+ case "setCart":
1729
+ setCartSignals({ itemCount: cmd.itemCount, productIds: cmd.productIds }, cmd.ttlDays);
1730
+ break;
1731
+ case "setSearch":
1732
+ setSearchSignal(cmd.query, cmd.ttlDays);
1733
+ break;
1734
+ case "setConsent":
1735
+ setConsent(cmd.granted);
1736
+ break;
1737
+ case "hold":
1738
+ hold();
1739
+ break;
1740
+ case "release":
1741
+ release();
1742
+ break;
1743
+ case "hydrated":
1744
+ hydrated();
1745
+ break;
1746
+ case "reInit":
1747
+ reInit(cmd.force === true);
1748
+ break;
1749
+ case "recheckPrerequisites":
1750
+ recheckPrerequisites();
1751
+ break;
1752
+ }
1753
+ }
1754
+
1755
+ export function processQueue(): void {
1756
+ const w = globalThis as Record<string, unknown>;
1757
+ const pending = w.drip;
1758
+ if (Array.isArray(pending)) {
1759
+ for (const cmd of pending) {
1760
+ processCommand(cmd as QueueCommand);
1761
+ }
1762
+ }
1763
+ installGlobalApi();
1764
+ }
1765
+
1766
+ export let installedGlobalApi: ReturnType<typeof createGlobalApi> | undefined;
1767
+
1768
+ export function createGlobalApi() {
1769
+ return Object.assign((command: string) => {
1770
+ if (command === "hold") return hold();
1771
+ if (command === "release") return release();
1772
+ if (command === "hydrated") return hydrated();
1773
+ __DRIP_FEATURE_DEVTOOLS__ && debugWarn("api", `Unknown command: ${command}`);
1774
+ }, {
1775
+ init,
1776
+ track,
1777
+ step: trackStep,
1778
+ trackGoal,
1779
+ trackRevenue,
1780
+ setConsent,
1781
+ hold,
1782
+ release,
1783
+ hydrated,
1784
+ reInit,
1785
+ recheckPrerequisites,
1786
+ flush,
1787
+ evaluate,
1788
+ evaluateFlag,
1789
+ evaluateFlagAssignment,
1790
+ evaluateFlagValue,
1791
+ signal: setPublicSignal,
1792
+ triggerPage,
1793
+ rememberProduct,
1794
+ setCart: setCartSignals,
1795
+ setSearch: setSearchSignal,
1796
+ getSignals: getPublicSignals,
1797
+ getAssignments,
1798
+ getCartAttributes,
1799
+ getCartMetafields,
1800
+ getCartMetafieldStaleKeys,
1801
+ mergeCartAttributes,
1802
+ getSignedCartAttributes,
1803
+ confirmCartAttributesWritten,
1804
+ getExperiments,
1805
+ getTools: getRuntimeTools,
1806
+ get: getRuntimeValue,
1807
+ getDiagnostics,
1808
+ getPendingGiftChoices,
1809
+ selectGiftOption,
1810
+ push(cmd: QueueCommand) {
1811
+ processCommand(cmd);
1812
+ },
1813
+ });
1814
+ }
1815
+
1816
+ export function installGlobalApi(): void {
1817
+ const w = globalThis as Record<string, unknown>;
1818
+ if (installedGlobalApi !== undefined && w.drip === installedGlobalApi) {
1819
+ w.Apex = installedGlobalApi;
1820
+ w.apexTools = getRuntimeTools();
1821
+ return;
1822
+ }
1823
+ const api = installedGlobalApi ??= createGlobalApi();
1824
+ w.drip = api;
1825
+ w.Apex = api;
1826
+ w.apexTools = getRuntimeTools();
1827
+ }
1828
+