@drip-apex/sdk 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/bundle-report.json +584 -226
- package/dist/define.cjs +123 -0
- package/dist/define.cjs.map +7 -0
- package/dist/define.mjs +102 -0
- package/dist/define.mjs.map +7 -0
- package/dist/drip.cjs +19283 -0
- package/dist/drip.cjs.map +7 -0
- package/dist/drip.debug.js +1 -1
- package/dist/drip.debug.js.map +1 -1
- package/dist/drip.heatmap.js +1 -1
- package/dist/drip.heatmap.js.map +1 -1
- package/dist/drip.js +1 -1
- package/dist/drip.js.map +1 -1
- package/dist/drip.live.js +1 -1
- package/dist/drip.live.js.map +1 -1
- package/dist/drip.mjs +19267 -0
- package/dist/drip.mjs.map +7 -0
- package/dist/drip.replay-recorder.js +2 -0
- package/dist/drip.replay-recorder.js.map +1 -0
- package/dist/editor.js +147 -142
- package/dist/editor.js.map +4 -4
- package/dist/flags.cjs +1788 -0
- package/dist/flags.cjs.map +7 -0
- package/dist/flags.mjs +1765 -0
- package/dist/flags.mjs.map +7 -0
- package/dist/index.mjs +12 -0
- package/dist/index.mjs.map +2 -2
- package/dist/metafiles/define.cjs.meta.json +28 -0
- package/dist/metafiles/define.mjs.meta.json +32 -0
- package/dist/metafiles/drip.cjs.meta.json +3193 -0
- package/dist/metafiles/drip.debug.meta.json +2859 -574
- package/dist/metafiles/drip.heatmap.meta.json +2732 -460
- package/dist/metafiles/drip.live.meta.json +2732 -460
- package/dist/metafiles/drip.meta.json +2859 -574
- package/dist/metafiles/drip.mjs.meta.json +3235 -0
- package/dist/metafiles/drip.replay-recorder.meta.json +82 -0
- package/dist/metafiles/editor.meta.json +67 -27
- package/dist/metafiles/flags.cjs.meta.json +220 -0
- package/dist/metafiles/flags.mjs.meta.json +225 -0
- package/dist/metafiles/index.meta.json +2717 -491
- package/dist/metafiles/index.mjs.meta.json +14 -4
- package/dist/react/index.js +16636 -13533
- package/dist/react/index.js.map +4 -4
- package/dist/sri.json +20 -15
- package/dist/types/activation-runtime.d.ts +91 -0
- package/dist/types/analytics-runtime.d.ts +166 -0
- package/dist/types/antiflicker.d.ts +9 -3
- package/dist/types/assignment-cookie-sync.d.ts +31 -0
- package/dist/types/assignment-runtime.d.ts +257 -0
- package/dist/types/assignment-store.d.ts +4 -8
- package/dist/types/bucket.d.ts +1 -2
- package/dist/types/compact-store.d.ts +1 -0
- package/dist/types/config-runtime.d.ts +313 -0
- package/dist/types/consent-evaluation.d.ts +28 -0
- package/dist/types/css.d.ts +5 -3
- package/dist/types/debug-loader.d.ts +2 -0
- package/dist/types/decide-assignments.d.ts +31 -0
- package/dist/types/delivery-timing.d.ts +8 -0
- package/dist/types/early-interaction-capture.d.ts +79 -0
- package/dist/types/early-reassert.d.ts +32 -0
- package/dist/types/edge-runtime.d.ts +11 -0
- package/dist/types/editor/api.d.ts +3 -0
- package/dist/types/editor/controller.d.ts +3 -1
- package/dist/types/editor/publication-result.d.ts +11 -0
- package/dist/types/editor/types.d.ts +2 -1
- package/dist/types/editor/view-kit.d.ts +1 -1
- package/dist/types/endpoint.d.ts +8 -0
- package/dist/types/exclusion-groups.d.ts +4 -7
- package/dist/types/flags.d.ts +2 -0
- package/dist/types/goal-eligibility.d.ts +4 -0
- package/dist/types/goals.d.ts +10 -1
- package/dist/types/guards.d.ts +7 -7
- package/dist/types/handover.d.ts +12 -1
- package/dist/types/hash.d.ts +8 -4
- package/dist/types/hydration.d.ts +2 -1
- package/dist/types/index.d.ts +24 -128
- package/dist/types/init-test-helpers.d.ts +48 -0
- package/dist/types/inline-bootstrap.d.ts +26 -0
- package/dist/types/js.d.ts +3 -1
- package/dist/types/money.d.ts +12 -0
- package/dist/types/mutate.d.ts +45 -6
- package/dist/types/performance.d.ts +2 -0
- package/dist/types/persistence-decoders.d.ts +12 -0
- package/dist/types/public-api.d.ts +209 -0
- package/dist/types/qa-assistant-state.d.ts +34 -0
- package/dist/types/react/index.d.ts +28 -3
- package/dist/types/reassert-sentinel.d.ts +6 -0
- package/dist/types/redirect.d.ts +48 -0
- package/dist/types/replay-recorder-entry.d.ts +1 -0
- package/dist/types/replay-signal-state.d.ts +6 -0
- package/dist/types/replay-signals.d.ts +13 -0
- package/dist/types/replay.d.ts +110 -0
- package/dist/types/router.d.ts +12 -2
- package/dist/types/runtime-state.d.ts +166 -0
- package/dist/types/sanitize.d.ts +1 -0
- package/dist/types/shadow.d.ts +3 -0
- package/dist/types/shopify-cart.d.ts +25 -1
- package/dist/types/storefront-vitals.d.ts +24 -0
- package/dist/types/targeting-diagnostics.d.ts +1 -0
- package/dist/types/targeting.d.ts +16 -2
- package/dist/types/track.d.ts +15 -1
- package/dist/types/types.d.ts +115 -9
- package/dist/types/wait.d.ts +14 -1
- package/dist/types/weekday-schedule.d.ts +3 -0
- package/package.json +17 -8
- package/src/activation-runtime.ts +2737 -0
- package/src/analytics-runtime.ts +2175 -0
- package/src/antiflicker.test.ts +216 -44
- package/src/antiflicker.ts +103 -28
- package/src/artifact-concurrency.test.ts +1091 -0
- package/src/assignment-cookie-sync.test.ts +202 -0
- package/src/assignment-cookie-sync.ts +189 -0
- package/src/assignment-runtime.ts +3204 -0
- package/src/assignment-store.test.ts +56 -0
- package/src/assignment-store.ts +7 -38
- package/src/bucket.ts +3 -18
- package/src/commerce-analytics-lifecycle.test.ts +75 -15
- package/src/commerce-config-lifecycle.test.ts +81 -1
- package/src/commerce-price-preserve.test.ts +133 -0
- package/src/commerce-price.test.ts +97 -0
- package/src/commerce-price.ts +74 -20
- package/src/commerce-shipping.test.ts +53 -0
- package/src/commerce-shipping.ts +5 -11
- package/src/compact-store.ts +15 -0
- package/src/config-fetch.test.ts +480 -9
- package/src/config-runtime.ts +2071 -0
- package/src/consent-evaluation-borlabs.test.ts +41 -0
- package/src/consent-evaluation-cookiescript.test.ts +50 -0
- package/src/consent-evaluation.ts +151 -0
- package/src/css.test.ts +30 -0
- package/src/css.ts +120 -6
- package/src/cwv.test.ts +60 -77
- package/src/debug-loader.test.ts +214 -0
- package/src/debug-loader.ts +41 -0
- package/src/decide-assignments.test.ts +196 -0
- package/src/decide-assignments.ts +204 -0
- package/src/delivery-timing.test.ts +104 -0
- package/src/delivery-timing.ts +89 -0
- package/src/early-interaction-capture.test.ts +451 -0
- package/src/early-interaction-capture.ts +230 -0
- package/src/early-reassert.test.ts +207 -0
- package/src/early-reassert.ts +152 -0
- package/src/edge-adoption.test.ts +435 -0
- package/src/edge-runtime.ts +30 -0
- package/src/editor/api.test.ts +109 -0
- package/src/editor/api.ts +34 -3
- package/src/editor/controller.test.ts +297 -14
- package/src/editor/controller.ts +69 -5
- package/src/editor/index.test.ts +2 -0
- package/src/editor/index.ts +1 -0
- package/src/editor/publication-result.ts +58 -0
- package/src/editor/types.ts +10 -1
- package/src/editor/view-kit.ts +2 -1
- package/src/editor/view.test.ts +108 -0
- package/src/editor/view.ts +49 -7
- package/src/endpoint.test.ts +15 -0
- package/src/endpoint.ts +10 -0
- package/src/exclusion-groups.ts +8 -18
- package/src/flags.test.ts +88 -1
- package/src/flags.ts +29 -29
- package/src/global-runtime-reconciler.test.ts +271 -0
- package/src/goal-eligibility.test.ts +29 -0
- package/src/goal-eligibility.ts +17 -0
- package/src/goals.test.ts +144 -1
- package/src/goals.ts +97 -18
- package/src/guards.test.ts +99 -1
- package/src/guards.ts +34 -15
- package/src/handover-cache-order.test.ts +6 -3
- package/src/handover.test.ts +154 -8
- package/src/handover.ts +122 -12
- package/src/hash.ts +29 -54
- package/src/holdout.test.ts +26 -0
- package/src/hydration.test.ts +73 -2
- package/src/hydration.ts +18 -9
- package/src/index.ts +130 -7287
- package/src/init-analytics.test.ts +1694 -0
- package/src/init-anti-flicker.test.ts +1211 -0
- package/src/init-cart-attributes.test.ts +2003 -0
- package/src/init-config-resolution.test.ts +999 -0
- package/src/init-consent.test.ts +971 -0
- package/src/init-empty.test.ts +11 -5406
- package/src/init-integrations.test.ts +846 -0
- package/src/init-page-activation.test.ts +5064 -0
- package/src/init-qa-loading.test.ts +740 -0
- package/src/init-redirects.test.ts +2156 -0
- package/src/init-test-helpers.ts +287 -0
- package/src/inline-bootstrap.test.ts +184 -0
- package/src/inline-bootstrap.ts +73 -0
- package/src/js.test.ts +51 -1
- package/src/js.ts +86 -2
- package/src/money.test.ts +55 -1
- package/src/money.ts +104 -2
- package/src/money.web-parity.contract.test.ts +85 -0
- package/src/mutate.test.ts +474 -10
- package/src/mutate.ts +739 -230
- package/src/page-trigger.test.ts +218 -8
- package/src/performance.ts +10 -0
- package/src/persistence-decoders.ts +18 -0
- package/src/public-api.ts +1828 -0
- package/src/qa-assistant-state.test.ts +249 -3
- package/src/qa-assistant-state.ts +146 -6
- package/src/qa-assistant-view.test.ts +220 -2
- package/src/qa-assistant-view.ts +102 -5
- package/src/react/index.test.tsx +37 -0
- package/src/react/index.tsx +19 -3
- package/src/reassert-sentinel.test.ts +155 -1
- package/src/reassert-sentinel.ts +168 -8
- package/src/redirect.test.ts +541 -0
- package/src/redirect.ts +572 -0
- package/src/replay-recorder-entry.ts +115 -0
- package/src/replay-signal-state.ts +100 -0
- package/src/replay-signals.test.ts +249 -0
- package/src/replay-signals.ts +151 -0
- package/src/replay.test.ts +764 -0
- package/src/replay.ts +514 -0
- package/src/router.test.ts +61 -2
- package/src/router.ts +50 -12
- package/src/runtime-state.ts +507 -0
- package/src/runtime.test.ts +174 -7
- package/src/runtime.ts +88 -14
- package/src/sanitize.test.ts +34 -0
- package/src/sanitize.ts +23 -12
- package/src/shadow.test.ts +187 -0
- package/src/shadow.ts +99 -0
- package/src/shopify-cart.test.ts +733 -23
- package/src/shopify-cart.ts +402 -49
- package/src/storefront-vitals.test.ts +217 -0
- package/src/storefront-vitals.ts +206 -0
- package/src/surface-signals.ts +6 -0
- package/src/targeting-conformance.test.ts +9 -0
- package/src/targeting-diagnostics.ts +38 -8
- package/src/targeting-parity-corpus.json +559 -0
- package/src/targeting-parity-corpus.test.ts +18 -0
- package/src/targeting.detailed.test.ts +105 -0
- package/src/targeting.test.ts +211 -0
- package/src/targeting.ts +88 -207
- package/src/track.test.ts +147 -0
- package/src/track.ts +125 -26
- package/src/types.ts +120 -13
- package/src/wait.test.ts +23 -0
- package/src/wait.ts +129 -13
- package/src/weekday-delivery.test.ts +56 -0
- package/src/weekday-schedule-parity-corpus.json +92 -0
- package/src/weekday-schedule.test.ts +69 -0
- package/src/weekday-schedule.ts +20 -0
|
@@ -0,0 +1,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
|
+
|