@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,2737 @@
|
|
|
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 {
|
|
30
|
+
writeAssignmentSyncCookie,
|
|
31
|
+
type AssignmentSyncPayload,
|
|
32
|
+
} from "./assignment-cookie-sync";
|
|
33
|
+
import { cleanupBucketingStorage } from "./bucketing-cleanup";
|
|
34
|
+
import { chooseVariation, inTrafficAllocation } from "./bucket";
|
|
35
|
+
import { buildAssignment, decideAssignments } from "./decide-assignments";
|
|
36
|
+
import { weekdayScheduleBlocksDelivery } from "./weekday-schedule";
|
|
37
|
+
import { CONSENT_REQUIRED, DNT_BLOCKED, evaluateSynchronousConsent, readConfiguredConsent, readPath, resolveSynchronousConsentGranted } from "./consent-evaluation";
|
|
38
|
+
import { clearEarlyInteractions, drainBootstrapEarlyInteractions, drainEarlyInteractions } from "./early-interaction-capture";
|
|
39
|
+
import { getCwvMetrics, markCwvReveal, startCwvMeasurement, waitForCwvMetrics } from "./cwv";
|
|
40
|
+
import { collectDeliveryTiming } from "./delivery-timing";
|
|
41
|
+
import {
|
|
42
|
+
startStorefrontVitals,
|
|
43
|
+
stopStorefrontVitals,
|
|
44
|
+
type StorefrontVitalsContext,
|
|
45
|
+
} from "./storefront-vitals";
|
|
46
|
+
import {
|
|
47
|
+
browserGroupStickyStore,
|
|
48
|
+
clearBrowserGroupStickyStore,
|
|
49
|
+
resolveExclusionGroups,
|
|
50
|
+
type ExclusionGroupDecision,
|
|
51
|
+
type PersistedGroupSelection,
|
|
52
|
+
} from "./exclusion-groups";
|
|
53
|
+
import type { CompactGroupSelections } from "./persistence-decoders";
|
|
54
|
+
import {
|
|
55
|
+
applyCommercePrice,
|
|
56
|
+
isCommercePriceWorkPending,
|
|
57
|
+
cleanupCommercePrice,
|
|
58
|
+
hasCommercePriceState,
|
|
59
|
+
COMMERCE_PRICE_SELECTORS,
|
|
60
|
+
isCommercePriceActive,
|
|
61
|
+
isCommercePriceExposureEligible,
|
|
62
|
+
notifyCommerceCartStampConfirmed,
|
|
63
|
+
setCommercePriceExposureListener,
|
|
64
|
+
} from "./commerce-price";
|
|
65
|
+
import {
|
|
66
|
+
applyCommerceShipping,
|
|
67
|
+
classifyShippingExposure,
|
|
68
|
+
cleanupCommerceShipping,
|
|
69
|
+
isShippingCartEligible,
|
|
70
|
+
isShippingCartEligibilityConfirmed,
|
|
71
|
+
} from "./commerce-shipping";
|
|
72
|
+
import {
|
|
73
|
+
addCssOwners,
|
|
74
|
+
injectCss,
|
|
75
|
+
injectExperimentCss,
|
|
76
|
+
injectExternalCss,
|
|
77
|
+
removeCssForOwner,
|
|
78
|
+
} from "./css";
|
|
79
|
+
import { configureDebug, debugLog, debugWarn, debugGroup, debugGroupEnd, isDebugEnabled } from "./debug";
|
|
80
|
+
import { loadDebugBuild } from "./debug-loader";
|
|
81
|
+
import { observeDomChanges } from "./dom-trigger";
|
|
82
|
+
import { edgeRuntimeActive, readEdgeRuntimeAssignments } from "./edge-runtime";
|
|
83
|
+
import { normalizeDataLayerEntry } from "./data-layer-normalize";
|
|
84
|
+
import { interceptEditorLaunch, isEditorDevicePreview } from "./editor-bootstrap";
|
|
85
|
+
import { cleanupGoals, goalWiringKeys, setupGoals, triggerGoal, triggerGoalsForEvent } from "./goals";
|
|
86
|
+
import { clearQaForceFromUrl, isBot, isDuplicate, isIframe, isOptedOut, isQaMode, isScreenshotMode, parseForceParam, persistQaForce, persistUrlQaForce, exitQaMode, getQaSessionId } from "./guards";
|
|
87
|
+
import { adoptUserId, applyHoldoutGate, configureIdentityStorage, getEphemeralUserId, getUserId, hashToFloat, isHoldoutVisitor, variationHashToFloat } from "./hash";
|
|
88
|
+
import { createBucketingHandoverPayload, handoverMintTimeoutMs, hasBucketingHandover, installBucketingHandover, readBucketingHandover } from "./handover";
|
|
89
|
+
import { revealAll } from "./hideflicker";
|
|
90
|
+
import {
|
|
91
|
+
addExternalJsOwners,
|
|
92
|
+
execJs,
|
|
93
|
+
loadExternalJs,
|
|
94
|
+
removeExternalJsForOwner,
|
|
95
|
+
setJsPolicy,
|
|
96
|
+
} from "./js";
|
|
97
|
+
import {
|
|
98
|
+
addAppliedMutationOwners,
|
|
99
|
+
applyMutations,
|
|
100
|
+
applyToElement,
|
|
101
|
+
invalidateArtifact,
|
|
102
|
+
reassertNonStructuralMutations,
|
|
103
|
+
replayArtifact,
|
|
104
|
+
resetAppliedMutationsForOwner,
|
|
105
|
+
} from "./mutate";
|
|
106
|
+
import { registerMutedObserver, runMuted, unregisterMutedObserver } from "./observer-mute";
|
|
107
|
+
import { assignmentsHaveVisualWork, awaitHydration, hasMutationHold, holdMutations, releaseMutations, signalHydrated } from "./hydration";
|
|
108
|
+
import {
|
|
109
|
+
assignmentsHaveCustomJs,
|
|
110
|
+
assignmentsHaveStructuralMutations,
|
|
111
|
+
installEarlyReassert,
|
|
112
|
+
shouldUseEarlyReassert,
|
|
113
|
+
} from "./early-reassert";
|
|
114
|
+
import { trackAppliedMutation } from "./reassert-sentinel";
|
|
115
|
+
import { observeRouteChanges, PAGE_URL_POLL_INTERVAL_MS } from "./router";
|
|
116
|
+
import { buildExposureSignalContext } from "./exposure-signals";
|
|
117
|
+
import {
|
|
118
|
+
buildRedirectDestination,
|
|
119
|
+
clearPendingRedirectAssignmentCarry,
|
|
120
|
+
finalizeRedirectAssignmentCarryHandover,
|
|
121
|
+
hasGeneratedRedirectAssignmentCarry,
|
|
122
|
+
filterUnsafeRedirectAssignments,
|
|
123
|
+
importRedirectAssignmentCarry,
|
|
124
|
+
isRedirectLoop,
|
|
125
|
+
markRedirectAssignmentCarryExposed,
|
|
126
|
+
parseRedirectAssignmentCarry,
|
|
127
|
+
persistPendingRedirectAssignmentCarry,
|
|
128
|
+
readPendingRedirectAssignmentCarry,
|
|
129
|
+
REDIRECT_ASSIGNMENT_PARAM,
|
|
130
|
+
redirectPlanForAssignment,
|
|
131
|
+
stripRedirectAssignmentCarry,
|
|
132
|
+
validateRedirectAssignmentCarry,
|
|
133
|
+
verifyRedirectAssignmentCarry,
|
|
134
|
+
} from "./redirect";
|
|
135
|
+
import type { VerifiedRedirectCarry } from "./redirect";
|
|
136
|
+
import { buildPrerequisiteContext, evaluateProjectPrerequisite } from "./prerequisites";
|
|
137
|
+
import {
|
|
138
|
+
REPLAY_PROGRESS_KEY_PREFIX,
|
|
139
|
+
REPLAY_SAMPLE_KEY_PREFIX,
|
|
140
|
+
resolveReplayRecorderUrl,
|
|
141
|
+
startReplayCapture,
|
|
142
|
+
type ReplayController,
|
|
143
|
+
} from "./replay";
|
|
144
|
+
import {
|
|
145
|
+
buildCartAttributeEntries,
|
|
146
|
+
buildCartMetafieldInputs,
|
|
147
|
+
buildSignedCartAttributeEntries,
|
|
148
|
+
APEX_CART_ATTRIBUTE_KEYS,
|
|
149
|
+
cannotRequestRequiredCommerceAuthorization,
|
|
150
|
+
getCartMetafieldStaleKeys as collectCartMetafieldStaleKeys,
|
|
151
|
+
invalidateShopifyCartAttributeSync,
|
|
152
|
+
mintShopwareAttributionEnvelope,
|
|
153
|
+
normalizeCartMetafieldNamespace,
|
|
154
|
+
syncShopifyCartAttributes,
|
|
155
|
+
type CartAssignment,
|
|
156
|
+
type CartAttributeEntry,
|
|
157
|
+
type CartMetafieldInput,
|
|
158
|
+
type CartAttributeSyncInput,
|
|
159
|
+
} from "./shopify-cart";
|
|
160
|
+
import { onShopifyCartMutation } from "./shopify-cart-events";
|
|
161
|
+
import {
|
|
162
|
+
getPendingGiftChoices,
|
|
163
|
+
noteCartSnapshotForGifts,
|
|
164
|
+
selectGiftOption,
|
|
165
|
+
syncGiftWithPurchase,
|
|
166
|
+
type GiftSyncOptions,
|
|
167
|
+
} from "./commerce-gift";
|
|
168
|
+
import { CART_DRAWER_OPEN_SIGNAL, installSurfaceSignals } from "./surface-signals";
|
|
169
|
+
import {
|
|
170
|
+
assignmentSignature,
|
|
171
|
+
canUseEmptyProjectProfile,
|
|
172
|
+
computeUrlDiffNeeds,
|
|
173
|
+
getBlankScreenMs,
|
|
174
|
+
readPerfFlags,
|
|
175
|
+
resolveActivationMode,
|
|
176
|
+
resolvePlatform,
|
|
177
|
+
resolveRouterMode,
|
|
178
|
+
shouldReevaluateForUrlChange,
|
|
179
|
+
} from "./runtime";
|
|
180
|
+
import type { PerfFlagState } from "./runtime";
|
|
181
|
+
import {
|
|
182
|
+
buildTargetingAttributes,
|
|
183
|
+
evaluateTargetingGatesDetailed,
|
|
184
|
+
matchPageTargetingRule,
|
|
185
|
+
readShopifySignal,
|
|
186
|
+
targetingHasJsTriggers,
|
|
187
|
+
} from "./targeting";
|
|
188
|
+
import { createApexRuntimeTools, type ApexRuntimeTools } from "./runtime-tools";
|
|
189
|
+
import { createEventId, Tracker } from "./track";
|
|
190
|
+
import {
|
|
191
|
+
clearStoredSignals,
|
|
192
|
+
configureSignalPersistenceAllowed,
|
|
193
|
+
clearRuntimeSignal,
|
|
194
|
+
getSignals as getStoredSignals,
|
|
195
|
+
getSignalDiagnostics,
|
|
196
|
+
rememberProduct,
|
|
197
|
+
setCartSignals,
|
|
198
|
+
setRuntimeSignal,
|
|
199
|
+
setSearchSignal,
|
|
200
|
+
setSignal as setStoredSignal,
|
|
201
|
+
} from "./signals";
|
|
202
|
+
import {
|
|
203
|
+
evaluateFlag as evaluateFlagFromExperiments,
|
|
204
|
+
evaluateFlagAssignment as evaluateFlagAssignmentFromExperiments,
|
|
205
|
+
evaluateFlagValue as evaluateFlagValueFromExperiments,
|
|
206
|
+
} from "./flags";
|
|
207
|
+
import {
|
|
208
|
+
ActivationMode,
|
|
209
|
+
AntiFlickerMode,
|
|
210
|
+
Assignment,
|
|
211
|
+
ActivationDiagnostics,
|
|
212
|
+
ActivationGateDiagnostic,
|
|
213
|
+
ConditionDiagnostic,
|
|
214
|
+
ConfigProfile,
|
|
215
|
+
ConsentModeSettings,
|
|
216
|
+
CustomJsPolicy,
|
|
217
|
+
DebugSettings,
|
|
218
|
+
ExecutionMode,
|
|
219
|
+
Experiment,
|
|
220
|
+
ExperimentActivationDiagnostic,
|
|
221
|
+
ExclusionGroupConfig,
|
|
222
|
+
ExclusionGroupMemberConfig,
|
|
223
|
+
Goal,
|
|
224
|
+
InitConfig,
|
|
225
|
+
PageTargetingRule,
|
|
226
|
+
PageActivationDiagnostic,
|
|
227
|
+
PageDeactivationMode,
|
|
228
|
+
PageTriggerMode,
|
|
229
|
+
PageTriggerSettings,
|
|
230
|
+
PerfFeatureFlag,
|
|
231
|
+
PerfFeatureFlags,
|
|
232
|
+
PlatformHint,
|
|
233
|
+
ProjectPrerequisites,
|
|
234
|
+
RouterMode,
|
|
235
|
+
SegmentCondition,
|
|
236
|
+
SpaSettings,
|
|
237
|
+
Targeting,
|
|
238
|
+
ExternalTrackingSettings,
|
|
239
|
+
ProjectCodeSettings,
|
|
240
|
+
RuntimeEnvironmentSettings,
|
|
241
|
+
RuntimeSignalDefinition,
|
|
242
|
+
SdkDiagnostics,
|
|
243
|
+
SnippetCriticalMutation,
|
|
244
|
+
SnippetCriticalMutationPlanItem,
|
|
245
|
+
SnippetDeliveryUrlRule,
|
|
246
|
+
BucketingCleanupSettings,
|
|
247
|
+
DataProtectionSettings,
|
|
248
|
+
DataLayerSignalMapping,
|
|
249
|
+
DataLayerTriggerSettings,
|
|
250
|
+
ExposureDestinationsSettings,
|
|
251
|
+
Mutation,
|
|
252
|
+
TrackEvent,
|
|
253
|
+
TrackingStart,
|
|
254
|
+
Variation,
|
|
255
|
+
} from "./types";
|
|
256
|
+
import {
|
|
257
|
+
addWaitEntryOwners,
|
|
258
|
+
applyMutationWithWait,
|
|
259
|
+
resetWaitEntriesForOwner,
|
|
260
|
+
} from "./wait";import { OrderIdentityPlatform, SDKState, QaAssistantRuntimeSnapshot, PageActivationRuntimeState, PageRulePollController, state, runtimeSharedState } from "./runtime-state";
|
|
261
|
+
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";
|
|
262
|
+
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";
|
|
263
|
+
import { attributionLedgerAssignments, 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";
|
|
264
|
+
import { loadQaToolbar, loadQaAssistant, loadDevtoolsIfEnabled, loadDevtools, init, trackEvent, track, funnelStepStorageKey, shouldTrackFunnelStep, trackStep, trackGoal, setConsent, hold, release, hydrated, recheckPrerequisites, reInit, RevenueEventData, normalizeRevenueCurrency, trackRevenue, flush, getAssignments, buildCurrentCartAttributes, getCartAttributes, getCartMetafields, getCartMetafieldStaleKeys, mergeCartAttributes, normalizedHeadlessCartToken, getSignedCartAttributes, confirmCartAttributesWritten, getExperiments, getGoals, getActiveExperimentIds, getRuntimeValue, getRuntimeTools, buildFlagContext, evaluateFlag, evaluateFlagAssignment, evaluateFlagValue, getDiagnostics, QueueCommand, processCommand, processQueue, installedGlobalApi, createGlobalApi, installGlobalApi } from "./public-api";
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
export function generateQaSessionId(): string {
|
|
268
|
+
return "qa_" + Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const SHOPIFY_ASSIGNMENT_SYNC_DELAYS_MS = [0, 250, 1000, 2500];
|
|
272
|
+
export let shopifyAssignmentSyncTimers: number[] = [];
|
|
273
|
+
|
|
274
|
+
export type CommerceOnlyAdoptionState = {
|
|
275
|
+
applying: boolean;
|
|
276
|
+
assignments: Assignment[];
|
|
277
|
+
context: string | null;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export let commercePriceAdoptionState: CommerceOnlyAdoptionState | null = null;
|
|
281
|
+
|
|
282
|
+
export function ensureCommercePriceAdoptionState(): CommerceOnlyAdoptionState {
|
|
283
|
+
return commercePriceAdoptionState ??= {
|
|
284
|
+
applying: false,
|
|
285
|
+
assignments: [],
|
|
286
|
+
context: null,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function currentCommercePriceContext(): string | null {
|
|
291
|
+
return state.config && state.userId
|
|
292
|
+
? `${state.config.shopId}\u0000${state.userId}`
|
|
293
|
+
: null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function withCommercePriceAssignments(assignments = state.assignments): Assignment[] {
|
|
297
|
+
const adoption = commercePriceAdoptionState;
|
|
298
|
+
if (
|
|
299
|
+
!adoption?.assignments.length ||
|
|
300
|
+
adoption.context !== currentCommercePriceContext()
|
|
301
|
+
) return assignments;
|
|
302
|
+
const activeExperimentIds = new Set(assignments.map((assignment) => assignment.experimentId));
|
|
303
|
+
const configuredPriceExperimentIds = new Set(
|
|
304
|
+
state.config?.commerce?.price?.authorization?.experimentIds ?? [],
|
|
305
|
+
);
|
|
306
|
+
const configuredOffersExperimentIds = new Set(
|
|
307
|
+
state.config?.commerce?.offers?.authorization?.experimentIds ?? [],
|
|
308
|
+
);
|
|
309
|
+
const experiments = new Map(
|
|
310
|
+
(state.config?.experiments ?? []).map((experiment) => [experiment.id, experiment]),
|
|
311
|
+
);
|
|
312
|
+
const validCommerceOnly = adoption.assignments.filter((assignment) => {
|
|
313
|
+
if (activeExperimentIds.has(assignment.experimentId)) return false;
|
|
314
|
+
if (
|
|
315
|
+
!configuredPriceExperimentIds.has(assignment.experimentId) &&
|
|
316
|
+
!configuredOffersExperimentIds.has(assignment.experimentId)
|
|
317
|
+
) return false;
|
|
318
|
+
const experiment = experiments.get(assignment.experimentId);
|
|
319
|
+
return !!experiment &&
|
|
320
|
+
__assignmentStoreInternals.assignmentEpoch(experiment) === assignment.assignmentEpoch &&
|
|
321
|
+
experiment.variations.some((variation) => variation.id === assignment.variationId);
|
|
322
|
+
});
|
|
323
|
+
return validCommerceOnly.length ? [...assignments, ...validCommerceOnly] : assignments;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function clearShopifyAssignmentSyncTimers(): void {
|
|
327
|
+
for (const timer of shopifyAssignmentSyncTimers) {
|
|
328
|
+
window.clearTimeout(timer);
|
|
329
|
+
}
|
|
330
|
+
shopifyAssignmentSyncTimers = [];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function adoptAuthoritativePriceAssignments(assignments: CartAssignment[]): boolean {
|
|
334
|
+
const config = state.config;
|
|
335
|
+
const price = config?.commerce?.price;
|
|
336
|
+
if (!config || !price || !state.userId) return false;
|
|
337
|
+
const configuredPriceExperimentIds = new Set(
|
|
338
|
+
price.authorization?.experimentIds ?? [],
|
|
339
|
+
);
|
|
340
|
+
if (configuredPriceExperimentIds.size === 0) return false;
|
|
341
|
+
const replacements = new Map<string, Assignment>();
|
|
342
|
+
const nextCommercePriceOnlyAssignments: Assignment[] = [];
|
|
343
|
+
const seenAuthoritativeExperimentIds = new Set<string>();
|
|
344
|
+
for (const authoritative of assignments) {
|
|
345
|
+
if (
|
|
346
|
+
seenAuthoritativeExperimentIds.has(authoritative.experimentId) ||
|
|
347
|
+
!configuredPriceExperimentIds.has(authoritative.experimentId) ||
|
|
348
|
+
!price.experiments.some(
|
|
349
|
+
(experiment) => experiment.experiment_id === authoritative.experimentId,
|
|
350
|
+
)
|
|
351
|
+
) return false;
|
|
352
|
+
seenAuthoritativeExperimentIds.add(authoritative.experimentId);
|
|
353
|
+
const experiment = config.experiments?.find(
|
|
354
|
+
(candidate) => candidate.id === authoritative.experimentId,
|
|
355
|
+
);
|
|
356
|
+
const variationIndex = experiment?.variations.findIndex(
|
|
357
|
+
(variation) => variation.id === authoritative.variationId,
|
|
358
|
+
) ?? -1;
|
|
359
|
+
if (!experiment || variationIndex < 0) return false;
|
|
360
|
+
if (
|
|
361
|
+
authoritative.assignmentEpoch !==
|
|
362
|
+
__assignmentStoreInternals.assignmentEpoch(experiment)
|
|
363
|
+
) return false;
|
|
364
|
+
const existingAssignment = state.assignments.find(
|
|
365
|
+
(assignment) => assignment.experimentId === authoritative.experimentId,
|
|
366
|
+
);
|
|
367
|
+
const replacement = buildAssignment(
|
|
368
|
+
experiment,
|
|
369
|
+
experiment.variations[variationIndex],
|
|
370
|
+
variationIndex,
|
|
371
|
+
existingAssignment?.hashValue ?? state.userId,
|
|
372
|
+
typeof location !== "undefined" ? location.href : undefined,
|
|
373
|
+
);
|
|
374
|
+
replacement.pageActive = experimentHasActivePage(experiment);
|
|
375
|
+
if (authoritative.commercePriceOnly) {
|
|
376
|
+
nextCommercePriceOnlyAssignments.push(replacement);
|
|
377
|
+
// A cart-only arm can still replace an existing off-target attribution
|
|
378
|
+
// assignment. Keep one shared copy in runtime state so revenue and
|
|
379
|
+
// custom attribution survive while the adoption list marks its cart role.
|
|
380
|
+
if (existingAssignment) {
|
|
381
|
+
replacements.set(authoritative.experimentId, replacement);
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
replacements.set(authoritative.experimentId, replacement);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const nextAssignments = state.assignments
|
|
389
|
+
.filter((assignment) => !configuredPriceExperimentIds.has(assignment.experimentId));
|
|
390
|
+
for (const authoritative of assignments) {
|
|
391
|
+
const replacement = replacements.get(authoritative.experimentId);
|
|
392
|
+
if (replacement) nextAssignments.push(replacement);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const previousAssignments = state.assignments;
|
|
396
|
+
const adoption = ensureCommercePriceAdoptionState();
|
|
397
|
+
const previousCommercePriceOnlyAssignments = adoption.assignments;
|
|
398
|
+
const previousCommercePriceOnlyContext = adoption.context;
|
|
399
|
+
try {
|
|
400
|
+
teardownTransaction(allAppliedPagesWithModes(), { skipCartSync: true });
|
|
401
|
+
state.assignments = nextAssignments;
|
|
402
|
+
adoption.assignments = [
|
|
403
|
+
...previousCommercePriceOnlyAssignments.filter(
|
|
404
|
+
(assignment) => !configuredPriceExperimentIds.has(assignment.experimentId),
|
|
405
|
+
),
|
|
406
|
+
...nextCommercePriceOnlyAssignments,
|
|
407
|
+
];
|
|
408
|
+
adoption.context = currentCommercePriceContext();
|
|
409
|
+
adoption.applying = true;
|
|
410
|
+
applyAssignments(nextAssignments, state.activationMode);
|
|
411
|
+
if (assignments.length === 0) cleanupCommercePrice();
|
|
412
|
+
state.lastAssignmentSignature = assignmentSignature(nextAssignments);
|
|
413
|
+
if (persistentStorageAllowed() && !state.qaMode) {
|
|
414
|
+
removePersistedAssignmentsForExperiments(configuredPriceExperimentIds);
|
|
415
|
+
for (const authoritative of assignments) {
|
|
416
|
+
if (!replacements.has(authoritative.experimentId)) continue;
|
|
417
|
+
const experiment = config.experiments?.find(
|
|
418
|
+
(candidate) => candidate.id === authoritative.experimentId,
|
|
419
|
+
);
|
|
420
|
+
if (experiment) {
|
|
421
|
+
persistAssignmentForExperiment(experiment, authoritative.variationId);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (state.analyticsStarted && !trackingBlocked() && state.trackingPrerequisiteMet) {
|
|
426
|
+
setupGoalTracking(config.experiments ?? []);
|
|
427
|
+
setupHeatmapTracking(nextAssignments, config.experiments ?? []);
|
|
428
|
+
setupReplayTracking(nextAssignments, config.experiments ?? []);
|
|
429
|
+
scheduleShopifyAssignmentSync();
|
|
430
|
+
}
|
|
431
|
+
notifyQaAssistantRuntimeListeners();
|
|
432
|
+
return true;
|
|
433
|
+
} catch {
|
|
434
|
+
state.assignments = previousAssignments;
|
|
435
|
+
adoption.assignments = previousCommercePriceOnlyAssignments;
|
|
436
|
+
adoption.context = previousCommercePriceOnlyContext;
|
|
437
|
+
return false;
|
|
438
|
+
} finally {
|
|
439
|
+
adoption.applying = false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export type OffersAdoptionPlan = {
|
|
444
|
+
configuredOffersExperimentIds: Set<string>;
|
|
445
|
+
replacements: Map<string, Assignment>;
|
|
446
|
+
nextCommercePriceOnlyAssignments: Assignment[];
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
export // Pure validation/planning — must not mutate any runtime state, because the
|
|
450
|
+
// cart-sync path runs it BEFORE the cart write to prove the arm is adoptable
|
|
451
|
+
// while the actual adoption is deferred until the write is confirmed.
|
|
452
|
+
function planAuthoritativeOffersAdoption(
|
|
453
|
+
assignments: CartAssignment[],
|
|
454
|
+
): OffersAdoptionPlan | null {
|
|
455
|
+
const config = state.config;
|
|
456
|
+
const offers = config?.commerce?.offers;
|
|
457
|
+
if (!config || !offers || !state.userId) return null;
|
|
458
|
+
const configuredOffersExperimentIds = new Set(
|
|
459
|
+
offers.authorization?.experimentIds ?? [],
|
|
460
|
+
);
|
|
461
|
+
if (configuredOffersExperimentIds.size === 0) return null;
|
|
462
|
+
const replacements = new Map<string, Assignment>();
|
|
463
|
+
const nextCommercePriceOnlyAssignments: Assignment[] = [];
|
|
464
|
+
const seenAuthoritativeExperimentIds = new Set<string>();
|
|
465
|
+
for (const authoritative of assignments) {
|
|
466
|
+
if (
|
|
467
|
+
seenAuthoritativeExperimentIds.has(authoritative.experimentId) ||
|
|
468
|
+
!configuredOffersExperimentIds.has(authoritative.experimentId)
|
|
469
|
+
) return null;
|
|
470
|
+
seenAuthoritativeExperimentIds.add(authoritative.experimentId);
|
|
471
|
+
const experiment = config.experiments?.find(
|
|
472
|
+
(candidate) => candidate.id === authoritative.experimentId,
|
|
473
|
+
);
|
|
474
|
+
const variationIndex = experiment?.variations.findIndex(
|
|
475
|
+
(variation) => variation.id === authoritative.variationId,
|
|
476
|
+
) ?? -1;
|
|
477
|
+
if (!experiment || variationIndex < 0) return null;
|
|
478
|
+
if (
|
|
479
|
+
authoritative.assignmentEpoch !==
|
|
480
|
+
__assignmentStoreInternals.assignmentEpoch(experiment)
|
|
481
|
+
) return null;
|
|
482
|
+
const existingAssignment = state.assignments.find(
|
|
483
|
+
(assignment) => assignment.experimentId === authoritative.experimentId,
|
|
484
|
+
);
|
|
485
|
+
const replacement = buildAssignment(
|
|
486
|
+
experiment,
|
|
487
|
+
experiment.variations[variationIndex],
|
|
488
|
+
variationIndex,
|
|
489
|
+
existingAssignment?.hashValue ?? state.userId,
|
|
490
|
+
typeof location !== "undefined" ? location.href : undefined,
|
|
491
|
+
);
|
|
492
|
+
replacement.pageActive = experimentHasActivePage(experiment);
|
|
493
|
+
if (authoritative.commercePriceOnly) {
|
|
494
|
+
nextCommercePriceOnlyAssignments.push(replacement);
|
|
495
|
+
if (existingAssignment) {
|
|
496
|
+
replacements.set(authoritative.experimentId, replacement);
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
replacements.set(authoritative.experimentId, replacement);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return { configuredOffersExperimentIds, replacements, nextCommercePriceOnlyAssignments };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function canAdoptAuthoritativeOffersAssignments(assignments: CartAssignment[]): boolean {
|
|
506
|
+
return planAuthoritativeOffersAdoption(assignments) !== null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function adoptAuthoritativeOffersAssignments(assignments: CartAssignment[]): boolean {
|
|
510
|
+
const config = state.config;
|
|
511
|
+
const plan = planAuthoritativeOffersAdoption(assignments);
|
|
512
|
+
if (!config || !plan) return false;
|
|
513
|
+
const {
|
|
514
|
+
configuredOffersExperimentIds,
|
|
515
|
+
replacements,
|
|
516
|
+
nextCommercePriceOnlyAssignments,
|
|
517
|
+
} = plan;
|
|
518
|
+
|
|
519
|
+
const nextAssignments = state.assignments.filter(
|
|
520
|
+
(assignment) => !configuredOffersExperimentIds.has(assignment.experimentId),
|
|
521
|
+
);
|
|
522
|
+
for (const authoritative of assignments) {
|
|
523
|
+
const replacement = replacements.get(authoritative.experimentId);
|
|
524
|
+
if (replacement) nextAssignments.push(replacement);
|
|
525
|
+
}
|
|
526
|
+
const previousAssignments = state.assignments;
|
|
527
|
+
const adoption = ensureCommercePriceAdoptionState();
|
|
528
|
+
const previousCommercePriceOnlyAssignments = adoption.assignments;
|
|
529
|
+
const previousCommercePriceOnlyContext = adoption.context;
|
|
530
|
+
try {
|
|
531
|
+
teardownTransaction(allAppliedPagesWithModes(), { skipCartSync: true });
|
|
532
|
+
state.assignments = nextAssignments;
|
|
533
|
+
adoption.assignments = [
|
|
534
|
+
...previousCommercePriceOnlyAssignments.filter(
|
|
535
|
+
(assignment) => !configuredOffersExperimentIds.has(assignment.experimentId),
|
|
536
|
+
),
|
|
537
|
+
...nextCommercePriceOnlyAssignments,
|
|
538
|
+
];
|
|
539
|
+
adoption.context = currentCommercePriceContext();
|
|
540
|
+
adoption.applying = true;
|
|
541
|
+
applyAssignments(nextAssignments, state.activationMode);
|
|
542
|
+
state.lastAssignmentSignature = assignmentSignature(nextAssignments);
|
|
543
|
+
if (persistentStorageAllowed() && !state.qaMode) {
|
|
544
|
+
removePersistedAssignmentsForExperiments(configuredOffersExperimentIds);
|
|
545
|
+
for (const authoritative of assignments) {
|
|
546
|
+
if (!replacements.has(authoritative.experimentId)) continue;
|
|
547
|
+
const experiment = config.experiments?.find(
|
|
548
|
+
(candidate) => candidate.id === authoritative.experimentId,
|
|
549
|
+
);
|
|
550
|
+
if (experiment) persistAssignmentForExperiment(experiment, authoritative.variationId);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (state.analyticsStarted && !trackingBlocked() && state.trackingPrerequisiteMet) {
|
|
554
|
+
setupGoalTracking(config.experiments ?? []);
|
|
555
|
+
setupHeatmapTracking(nextAssignments, config.experiments ?? []);
|
|
556
|
+
setupReplayTracking(nextAssignments, config.experiments ?? []);
|
|
557
|
+
scheduleShopifyAssignmentSync();
|
|
558
|
+
}
|
|
559
|
+
notifyQaAssistantRuntimeListeners();
|
|
560
|
+
return true;
|
|
561
|
+
} catch {
|
|
562
|
+
state.assignments = previousAssignments;
|
|
563
|
+
adoption.assignments = previousCommercePriceOnlyAssignments;
|
|
564
|
+
adoption.context = previousCommercePriceOnlyContext;
|
|
565
|
+
return false;
|
|
566
|
+
} finally {
|
|
567
|
+
adoption.applying = false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
export function sdkCartMintAllowed(): boolean {
|
|
572
|
+
// The union keeps the original "something to mint" contract for every
|
|
573
|
+
// consumer (click trigger, merge key-replacement) now that deactivated
|
|
574
|
+
// session assignments still mint through the attribution ledger.
|
|
575
|
+
const assignments = [
|
|
576
|
+
...state.assignments.filter(attributable),
|
|
577
|
+
...attributionLedgerAssignments(),
|
|
578
|
+
];
|
|
579
|
+
return (
|
|
580
|
+
state.config?.cartAttributeWrite !== "external" &&
|
|
581
|
+
state.config?.cartAttributeScope === "all" &&
|
|
582
|
+
!!assignments[0] &&
|
|
583
|
+
trackingBlocked() !== state.trackingDisabledByDnt
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function buildSdkCartMintInput(): CartAttributeSyncInput | null {
|
|
588
|
+
const assignments = [
|
|
589
|
+
...state.assignments.filter(attributable),
|
|
590
|
+
...attributionLedgerAssignments(),
|
|
591
|
+
];
|
|
592
|
+
if (!sdkCartMintAllowed() || !assignments[0]) return null;
|
|
593
|
+
return {
|
|
594
|
+
assignments,
|
|
595
|
+
// Empty identity values make the Shopify merge clear prior consented identity.
|
|
596
|
+
visitorId: "",
|
|
597
|
+
sessionId: "",
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function buildScopedCartAttributeSyncInput(): CartAttributeSyncInput | null {
|
|
602
|
+
const assignments = [
|
|
603
|
+
...state.assignments.filter(attributable),
|
|
604
|
+
...attributionLedgerAssignments(),
|
|
605
|
+
];
|
|
606
|
+
if (
|
|
607
|
+
state.config?.cartAttributeWrite !== "external" ||
|
|
608
|
+
state.config?.cartAttributeScope !== "all" ||
|
|
609
|
+
!state.consentRequired ||
|
|
610
|
+
state.consentGranted ||
|
|
611
|
+
state.trackingDisabledByDnt ||
|
|
612
|
+
!assignments[0]
|
|
613
|
+
) return null;
|
|
614
|
+
return {
|
|
615
|
+
assignments,
|
|
616
|
+
visitorId: "",
|
|
617
|
+
sessionId: "",
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function buildActiveCartAttributeSyncInput(functionalCartWrite = false): CartAttributeSyncInput | null {
|
|
622
|
+
if (!functionalCartWrite && (trackingBlocked() || !state.trackingPrerequisiteMet)) return null;
|
|
623
|
+
const controlStamp = state.holdout && state.assignments.length === 0;
|
|
624
|
+
const identityOnly = state.config?.holdout_v1_enabled === true
|
|
625
|
+
&& !state.holdout
|
|
626
|
+
&& state.assignments.length === 0
|
|
627
|
+
&& !hasActiveCommerceAuthorizationPlane();
|
|
628
|
+
if (
|
|
629
|
+
(
|
|
630
|
+
!state.assignments.length &&
|
|
631
|
+
!hasActiveCommerceAuthorizationPlane() &&
|
|
632
|
+
!controlStamp &&
|
|
633
|
+
!identityOnly
|
|
634
|
+
) ||
|
|
635
|
+
!state.userId
|
|
636
|
+
) return null;
|
|
637
|
+
|
|
638
|
+
const activeCartAssignments = withCommercePriceAssignments(state.assignments).filter(attributable);
|
|
639
|
+
const chargeAffectingExperimentIds = getChargeAffectingCommerceExperimentIds(
|
|
640
|
+
activeCartAssignments.filter(renderable),
|
|
641
|
+
);
|
|
642
|
+
const adoption = commercePriceAdoptionState;
|
|
643
|
+
const assignments = activeCartAssignments.map((assignment) => ({
|
|
644
|
+
experimentId: assignment.experimentId,
|
|
645
|
+
variationId: assignment.variationId,
|
|
646
|
+
pageActive: assignment.pageActive,
|
|
647
|
+
assignmentEpoch:
|
|
648
|
+
!assignment.kind || assignment.kind === "experiment"
|
|
649
|
+
? assignment.assignmentEpoch
|
|
650
|
+
: undefined,
|
|
651
|
+
...(adoption?.context === currentCommercePriceContext() &&
|
|
652
|
+
adoption.assignments.includes(assignment)
|
|
653
|
+
? { commercePriceOnly: true }
|
|
654
|
+
: {}),
|
|
655
|
+
}));
|
|
656
|
+
const assignedExperimentIds = new Set(
|
|
657
|
+
assignments.map((assignment) => assignment.experimentId),
|
|
658
|
+
);
|
|
659
|
+
const exclusionGroupSelections = state.config?.exclusionGroups
|
|
660
|
+
?.map((group) => {
|
|
661
|
+
const winner = group.members.find((member) =>
|
|
662
|
+
assignedExperimentIds.has(member.experimentId)
|
|
663
|
+
);
|
|
664
|
+
return winner
|
|
665
|
+
? {
|
|
666
|
+
groupId: group.id,
|
|
667
|
+
experimentId: winner.experimentId,
|
|
668
|
+
epoch: Number.isFinite(group.epoch)
|
|
669
|
+
? Math.max(1, Math.floor(group.epoch ?? 1))
|
|
670
|
+
: 1,
|
|
671
|
+
}
|
|
672
|
+
: null;
|
|
673
|
+
})
|
|
674
|
+
.filter((selection): selection is NonNullable<typeof selection> =>
|
|
675
|
+
selection !== null
|
|
676
|
+
);
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
assignments,
|
|
680
|
+
controlStamp: controlStamp || undefined,
|
|
681
|
+
identityOnly: identityOnly || undefined,
|
|
682
|
+
requiredAssignments: chargeAffectingExperimentIds.size > 0
|
|
683
|
+
? assignments.filter((assignment) =>
|
|
684
|
+
chargeAffectingExperimentIds.has(assignment.experimentId))
|
|
685
|
+
: undefined,
|
|
686
|
+
visitorId: state.userId,
|
|
687
|
+
sessionId: state.tracker?.getSessionId() ?? "",
|
|
688
|
+
shopId: state.config?.ingestShopId ?? state.config?.shopId,
|
|
689
|
+
endpoint: state.config?.endpoint,
|
|
690
|
+
ingestSignature: state.config?.ingestSignature,
|
|
691
|
+
url: typeof location !== "undefined" ? location.href : undefined,
|
|
692
|
+
environment: state.config?.environments?.activeKey,
|
|
693
|
+
platform: state.platform,
|
|
694
|
+
onAttributionEnvelope: (envelope) => {
|
|
695
|
+
state.shopwareAttributionEnvelope = envelope;
|
|
696
|
+
if (typeof document !== "undefined") {
|
|
697
|
+
document.dispatchEvent(new CustomEvent("apex:attribution-envelope"));
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
offersAuthorizationRevision:
|
|
701
|
+
state.config?.commerce?.offers?.authorization?.revision,
|
|
702
|
+
offersExperimentIds:
|
|
703
|
+
state.config?.commerce?.offers?.authorization?.experimentIds,
|
|
704
|
+
shippingAuthorizationRevision:
|
|
705
|
+
state.config?.commerce?.shipping?.authorization?.revision,
|
|
706
|
+
shippingExperimentIds:
|
|
707
|
+
state.config?.commerce?.shipping?.authorization?.experimentIds,
|
|
708
|
+
priceAuthorizationRevision:
|
|
709
|
+
state.config?.commerce?.price?.authorization?.revision,
|
|
710
|
+
priceExperimentIds:
|
|
711
|
+
state.config?.commerce?.price?.authorization?.experimentIds,
|
|
712
|
+
onCartToken: (cartToken) => {
|
|
713
|
+
if (state.shopifyCartToken === cartToken) return;
|
|
714
|
+
state.shopifyCartToken = cartToken;
|
|
715
|
+
if (state.analyticsStarted) publishAssignmentsToShopify();
|
|
716
|
+
},
|
|
717
|
+
...(state.config?.commerce?.offers?.authorization?.experimentIds?.length
|
|
718
|
+
? {
|
|
719
|
+
adoptOffersAssignments: adoptAuthoritativeOffersAssignments,
|
|
720
|
+
canAdoptOffersAssignments: canAdoptAuthoritativeOffersAssignments,
|
|
721
|
+
authorizeOffersDisplayAssignments: authorizeOffersDisplayAndRender,
|
|
722
|
+
}
|
|
723
|
+
: {}),
|
|
724
|
+
// Every stamped cart write returns the full cart; marked GWP lines seen
|
|
725
|
+
// there seed durable insertion records so a fresh device or cleared
|
|
726
|
+
// storage over a persistent/customer-synced cart still reconciles gifts.
|
|
727
|
+
onCartSnapshot: (cart: unknown) => {
|
|
728
|
+
if (noteCartSnapshotForGifts(cart)) {
|
|
729
|
+
void syncGiftWithPurchase(
|
|
730
|
+
state.config?.commerce?.offers,
|
|
731
|
+
withCommercePriceAssignments(state.assignments).filter(attributable),
|
|
732
|
+
giftSyncOptions(),
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
...(hasActivePriceAuthorizationPlane()
|
|
737
|
+
? {
|
|
738
|
+
adoptPriceAssignments: adoptAuthoritativePriceAssignments,
|
|
739
|
+
authorizePriceDisplayAssignments: (authorizedAssignments: CartAssignment[]) => {
|
|
740
|
+
state.authorizedPriceAssignmentKeys = new Set(
|
|
741
|
+
authorizedAssignments.map(priceAssignmentAuthorizationKey),
|
|
742
|
+
);
|
|
743
|
+
},
|
|
744
|
+
}
|
|
745
|
+
: {}),
|
|
746
|
+
...(hasActiveShippingAuthorizationPlane()
|
|
747
|
+
? {
|
|
748
|
+
authorizeShippingDisplayAssignments: (authorizedAssignments: CartAssignment[]) => {
|
|
749
|
+
state.authorizedShippingAssignmentKeys = new Set(
|
|
750
|
+
authorizedAssignments.map(priceAssignmentAuthorizationKey),
|
|
751
|
+
);
|
|
752
|
+
},
|
|
753
|
+
}
|
|
754
|
+
: {}),
|
|
755
|
+
exclusionGroupSelections:
|
|
756
|
+
exclusionGroupSelections?.length ? exclusionGroupSelections : undefined,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export let sdkCartMintSync: ReturnType<typeof syncShopifyCartAttributes> | null = null;
|
|
761
|
+
|
|
762
|
+
export function syncSdkCartMint(): void {
|
|
763
|
+
const input = !sdkCartMintSync && buildSdkCartMintInput();
|
|
764
|
+
if (!input) return;
|
|
765
|
+
const pending = sdkCartMintSync = syncShopifyCartAttributes(input);
|
|
766
|
+
const finish = () => {
|
|
767
|
+
if (sdkCartMintSync !== pending) return;
|
|
768
|
+
sdkCartMintSync = null;
|
|
769
|
+
if (!trackingBlocked()) syncActiveAssignmentToShopifyCart();
|
|
770
|
+
};
|
|
771
|
+
void pending.then(finish, finish);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export /**
|
|
775
|
+
* Mint on the first capture-phase click while Shopify identifies a product page.
|
|
776
|
+
* Capture starts the empty-cart write before an add-to-cart bubble handler runs.
|
|
777
|
+
*/
|
|
778
|
+
const removeSdkCartMintTrigger = () =>
|
|
779
|
+
document.removeEventListener("click", onSdkCartMintClick, true);
|
|
780
|
+
|
|
781
|
+
export function onSdkCartMintClick(): void {
|
|
782
|
+
if (!sdkCartMintAllowed() || readShopifySignal("shopify.page_type") !== "product") return;
|
|
783
|
+
removeSdkCartMintTrigger();
|
|
784
|
+
syncSdkCartMint();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function installSdkCartMintTrigger(): void {
|
|
788
|
+
removeSdkCartMintTrigger();
|
|
789
|
+
if (!sdkCartMintAllowed()) return;
|
|
790
|
+
document.addEventListener("click", onSdkCartMintClick, { capture: true, passive: true });
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export type HeadlessCartAuthorizationState = {
|
|
794
|
+
generation: number;
|
|
795
|
+
confirmationPending: boolean;
|
|
796
|
+
pending: {
|
|
797
|
+
generation: number;
|
|
798
|
+
cartToken: string;
|
|
799
|
+
priceAssignments: CartAssignment[];
|
|
800
|
+
shippingAssignments: CartAssignment[];
|
|
801
|
+
offersAssignments: CartAssignment[];
|
|
802
|
+
offersAdoptionAssignments: CartAssignment[] | null;
|
|
803
|
+
} | null;
|
|
804
|
+
};
|
|
805
|
+
|
|
806
|
+
export let headlessCartAuthorizationState: HeadlessCartAuthorizationState | null = null;
|
|
807
|
+
|
|
808
|
+
export function ensureHeadlessCartAuthorizationState(): HeadlessCartAuthorizationState {
|
|
809
|
+
return headlessCartAuthorizationState ??= {
|
|
810
|
+
generation: 0,
|
|
811
|
+
confirmationPending: false,
|
|
812
|
+
pending: null,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function hasChargeAffectingCommerceAssignment(assignments = state.assignments): boolean {
|
|
817
|
+
return getChargeAffectingCommerceExperimentIds(assignments).size > 0;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
export function hasActiveAuthorizationPlane(plane: "price" | "shipping" | "offers"): boolean {
|
|
821
|
+
const authorization = state.config?.commerce?.[plane]?.authorization;
|
|
822
|
+
return authorization?.version === 1
|
|
823
|
+
&& Number.isSafeInteger(authorization.revision)
|
|
824
|
+
&& authorization.revision > 0
|
|
825
|
+
&& Array.isArray(authorization.experimentIds)
|
|
826
|
+
&& authorization.experimentIds.length > 0;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export const hasActivePriceAuthorizationPlane = () => hasActiveAuthorizationPlane("price");
|
|
830
|
+
export const hasActiveShippingAuthorizationPlane = () => hasActiveAuthorizationPlane("shipping");
|
|
831
|
+
export const hasActiveOffersAuthorizationPlane = () => hasActiveAuthorizationPlane("offers");
|
|
832
|
+
|
|
833
|
+
export function hasActiveCommerceAuthorizationPlane(): boolean {
|
|
834
|
+
if (hasActivePriceAuthorizationPlane() || hasActiveOffersAuthorizationPlane()) return true;
|
|
835
|
+
const authorization = state.config?.commerce?.shipping?.authorization;
|
|
836
|
+
return authorization?.version === 1
|
|
837
|
+
&& Number.isSafeInteger(authorization.revision)
|
|
838
|
+
&& authorization.revision > 0
|
|
839
|
+
&& Array.isArray(authorization.experimentIds)
|
|
840
|
+
&& authorization.experimentIds.length > 0;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function requiresCommerceCohortPin(assignments = state.assignments): boolean {
|
|
844
|
+
return hasActivePriceAuthorizationPlane()
|
|
845
|
+
|| hasActiveOffersAuthorizationPlane()
|
|
846
|
+
|| hasChargeAffectingCommerceAssignment(withCommercePriceAssignments(assignments).filter(attributable));
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function getChargeAffectingCommerceExperimentIds(
|
|
850
|
+
assignments = state.assignments,
|
|
851
|
+
): Set<string> {
|
|
852
|
+
const active = new Map(
|
|
853
|
+
assignments
|
|
854
|
+
.filter((assignment) => !assignment.isControl)
|
|
855
|
+
.map((assignment) => [assignment.experimentId, assignment.variationId]),
|
|
856
|
+
);
|
|
857
|
+
const chargeAffecting = new Set<string>();
|
|
858
|
+
state.config?.commerce?.price?.experiments.forEach((experiment) => {
|
|
859
|
+
const variationId = active.get(experiment.experiment_id);
|
|
860
|
+
const assignment = assignments.find(
|
|
861
|
+
(candidate) => candidate.experimentId === experiment.experiment_id,
|
|
862
|
+
);
|
|
863
|
+
if (variationId != null
|
|
864
|
+
&& (experiment.assignment_epoch == null
|
|
865
|
+
|| experiment.assignment_epoch === assignment?.assignmentEpoch)
|
|
866
|
+
&& experiment.variants[variationId]?.some(
|
|
867
|
+
(adjustment) => typeof adjustment.fixedPricePerUnit === "string",
|
|
868
|
+
)) {
|
|
869
|
+
chargeAffecting.add(experiment.experiment_id);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
const offerExperimentIds = new Set(
|
|
874
|
+
state.config?.commerce?.offers?.authorization?.experimentIds ?? [],
|
|
875
|
+
);
|
|
876
|
+
for (const experimentId of offerExperimentIds) {
|
|
877
|
+
if (active.has(experimentId)) chargeAffecting.add(experimentId);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const shipping = state.config?.commerce?.shipping;
|
|
881
|
+
const enforcedExperimentIds = new Set(shipping?.authorization?.experimentIds ?? []);
|
|
882
|
+
shipping?.experiments.forEach((experiment) => {
|
|
883
|
+
const variationId = active.get(experiment.experiment_id);
|
|
884
|
+
const assignment = assignments.find(
|
|
885
|
+
(candidate) => candidate.experimentId === experiment.experiment_id,
|
|
886
|
+
);
|
|
887
|
+
const variant = variationId == null ? undefined : experiment.variants[variationId];
|
|
888
|
+
if (enforcedExperimentIds.has(experiment.experiment_id)
|
|
889
|
+
&& (experiment.assignment_epoch == null
|
|
890
|
+
|| experiment.assignment_epoch === assignment?.assignmentEpoch)
|
|
891
|
+
&& variant?.treatment !== "none"
|
|
892
|
+
&& isShippingCartEligible(experiment.experiment_id)) {
|
|
893
|
+
chargeAffecting.add(experiment.experiment_id);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
return chargeAffecting;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export function isCohortStampedOnCart(): boolean | "pending" {
|
|
900
|
+
if (headlessCartAuthorizationState?.confirmationPending) return false;
|
|
901
|
+
if (state.lastShopifyCartSync?.status === "pending") return "pending";
|
|
902
|
+
return state.lastShopifyCartSync?.status === "sent";
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function syncActiveAssignmentToShopifyCart(options: {
|
|
906
|
+
chargeCorrectness?: boolean;
|
|
907
|
+
force?: boolean;
|
|
908
|
+
holdoutCartClear?: boolean;
|
|
909
|
+
} = {}): void {
|
|
910
|
+
if (state.platform === "shopware") {
|
|
911
|
+
if (!trackingBlocked()) {
|
|
912
|
+
const shopwareInput = buildActiveCartAttributeSyncInput();
|
|
913
|
+
if (shopwareInput) void mintShopwareAttributionEnvelope(shopwareInput);
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (state.config?.cartAttributeWrite === "external") {
|
|
918
|
+
runtimeSharedState.shopifyCartSyncGeneration += 1;
|
|
919
|
+
invalidateShopifyCartAttributeSync();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (!trackingBlocked() && sdkCartMintSync) return;
|
|
923
|
+
const chargeCorrectness = options.chargeCorrectness === true
|
|
924
|
+
&& requiresCommerceCohortPin();
|
|
925
|
+
const holdoutCartClear = options.holdoutCartClear === true
|
|
926
|
+
&& state.holdout
|
|
927
|
+
&& state.config?.holdout_v1_enabled === true
|
|
928
|
+
&& state.assignments.length === 0;
|
|
929
|
+
const functionalCartWrite = chargeCorrectness || holdoutCartClear;
|
|
930
|
+
if (!functionalCartWrite && (trackingBlocked() || !state.trackingPrerequisiteMet)) return;
|
|
931
|
+
const input = buildActiveCartAttributeSyncInput(functionalCartWrite);
|
|
932
|
+
if (!input) return;
|
|
933
|
+
const generation = ++runtimeSharedState.shopifyCartSyncGeneration;
|
|
934
|
+
const authorizationUnavailable = cannotRequestRequiredCommerceAuthorization(input);
|
|
935
|
+
if (hasActivePriceAuthorizationPlane()) {
|
|
936
|
+
state.authorizedPriceAssignmentKeys?.clear();
|
|
937
|
+
}
|
|
938
|
+
if (hasActiveShippingAuthorizationPlane()) {
|
|
939
|
+
state.authorizedShippingAssignmentKeys?.clear();
|
|
940
|
+
}
|
|
941
|
+
// Offers display keys are deliberately NOT cleared at sync start: their
|
|
942
|
+
// renderer is applyAssignments itself, which triggers this sync — clearing
|
|
943
|
+
// here would revoke the authorization the re-apply is acting on. Applied
|
|
944
|
+
// DOM cannot be un-rendered by a key clear anyway; the keys are replaced
|
|
945
|
+
// wholesale by each completed authorization (exposure stays deduped).
|
|
946
|
+
state.lastShopifyCartSync = {
|
|
947
|
+
at: Date.now(),
|
|
948
|
+
status: authorizationUnavailable ? "failed" : "pending",
|
|
949
|
+
reason: authorizationUnavailable ? "authorization_failed" : undefined,
|
|
950
|
+
assignmentCount: state.assignments.length,
|
|
951
|
+
};
|
|
952
|
+
void syncShopifyCartAttributes(input, { force: options.force }).then((result) => {
|
|
953
|
+
if (generation !== runtimeSharedState.shopifyCartSyncGeneration) return;
|
|
954
|
+
state.lastShopifyCartSync = {
|
|
955
|
+
at: Date.now(),
|
|
956
|
+
status: result.status,
|
|
957
|
+
reason: result.reason,
|
|
958
|
+
assignmentCount: state.assignments.length,
|
|
959
|
+
};
|
|
960
|
+
if (result.status === "failed") {
|
|
961
|
+
// Fail-closed: the stamp refresh failed, so the Function may already
|
|
962
|
+
// refuse lines this cart still carries (e.g. the shopper dropped below
|
|
963
|
+
// the GWP threshold right before the failed sync). Run privilege-
|
|
964
|
+
// reducing gift cleanup — removals and normalization only, no inserts.
|
|
965
|
+
void syncGiftWithPurchase(
|
|
966
|
+
state.config?.commerce?.offers,
|
|
967
|
+
withCommercePriceAssignments(state.assignments).filter(attributable),
|
|
968
|
+
giftSyncOptions({ suppressInsertion: true }),
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
if (result.status === "sent") {
|
|
972
|
+
void syncGiftWithPurchase(
|
|
973
|
+
state.config?.commerce?.offers,
|
|
974
|
+
withCommercePriceAssignments(state.assignments).filter(attributable),
|
|
975
|
+
giftSyncOptions(),
|
|
976
|
+
);
|
|
977
|
+
notifyCommerceCartStampConfirmed();
|
|
978
|
+
// Render plane, not the raw assignment list: a deactivated page's
|
|
979
|
+
// shipping treatment must not come back on a cart-stamp callback.
|
|
980
|
+
reconcileCommerceShipping();
|
|
981
|
+
if (state.analyticsStarted && state.config && !trackingBlocked()) {
|
|
982
|
+
trackViewedAssignments(state.config);
|
|
983
|
+
}
|
|
984
|
+
} else if (hasActivePriceAuthorizationPlane()) {
|
|
985
|
+
cleanupCommercePrice();
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
export function configureChargeCorrectnessCartSync(assignments: Assignment[]): void {
|
|
991
|
+
state.stopShopifyCartMutationSync?.();
|
|
992
|
+
state.stopShopifyCartMutationSync = null;
|
|
993
|
+
if (!requiresCommerceCohortPin(assignments)) {
|
|
994
|
+
// Commerce planes exist but none is charge-affecting anymore, so the
|
|
995
|
+
// sync (and its post-"sent" gift pass) will not run; still give the gift
|
|
996
|
+
// manager its teardown-cleanup chance for an earlier insertion, and keep
|
|
997
|
+
// it subscribed to cart mutations — a login/customer-account cart merge
|
|
998
|
+
// can land an Apex-marked line long after the once-per-page check.
|
|
999
|
+
if (state.config?.commerce?.offers) {
|
|
1000
|
+
state.stopShopifyCartMutationSync = onShopifyCartMutation(() => {
|
|
1001
|
+
void syncGiftWithPurchase(
|
|
1002
|
+
state.config?.commerce?.offers,
|
|
1003
|
+
withCommercePriceAssignments(state.assignments).filter(attributable),
|
|
1004
|
+
giftSyncOptions({ recheckTeardown: true }),
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
void syncGiftWithPurchase(state.config?.commerce?.offers, withCommercePriceAssignments(state.assignments).filter(attributable));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
state.stopShopifyCartMutationSync = onShopifyCartMutation(() => {
|
|
1013
|
+
syncActiveAssignmentToShopifyCart({ chargeCorrectness: true, force: true });
|
|
1014
|
+
});
|
|
1015
|
+
if (commercePriceAdoptionState?.applying) return;
|
|
1016
|
+
// This narrow consent/DNT exemption is charge-correctness, not analytics:
|
|
1017
|
+
// the first-party cart attribute contains only assignment ids/epoch and must
|
|
1018
|
+
// exist whenever a commerce treatment can change what the shopper is charged.
|
|
1019
|
+
syncActiveAssignmentToShopifyCart({ chargeCorrectness: true });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
export function holdoutConfigEpoch(config: InitConfig | null | undefined): number | undefined {
|
|
1023
|
+
const raw = config?.holdout_config_updated_at;
|
|
1024
|
+
const epoch = typeof raw === "string" ? Date.parse(raw) : Number.NaN;
|
|
1025
|
+
return Number.isSafeInteger(epoch) && epoch > 0 ? epoch : undefined;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function buildShopifyAssignmentSyncPayload(
|
|
1029
|
+
assignments: AssignmentSyncPayload["assignments"],
|
|
1030
|
+
): AssignmentSyncPayload {
|
|
1031
|
+
return {
|
|
1032
|
+
visitorId: state.userId,
|
|
1033
|
+
sessionId: state.tracker?.getSessionId() ?? "",
|
|
1034
|
+
...(state.shopifyCartToken ? { cartToken: state.shopifyCartToken } : {}),
|
|
1035
|
+
qaMode: state.qaMode,
|
|
1036
|
+
qaSessionId: state.qaSessionId,
|
|
1037
|
+
cohort: state.config?.holdout_v1_enabled === true
|
|
1038
|
+
? (state.holdout ? "holdout" : "active")
|
|
1039
|
+
: undefined,
|
|
1040
|
+
holdoutConfigEpoch: holdoutConfigEpoch(state.config),
|
|
1041
|
+
assignments,
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export /**
|
|
1046
|
+
* Publish current experiment assignments to Shopify's analytics bus.
|
|
1047
|
+
* The Drip Web Pixel subscribes to this custom event to attribute
|
|
1048
|
+
* Shopify standard events (checkout, add_to_cart, etc.) to experiments.
|
|
1049
|
+
*
|
|
1050
|
+
* No-op on non-Shopify stores — guarded by the Shopify global check.
|
|
1051
|
+
*/
|
|
1052
|
+
function publishAssignmentsToShopify(): void {
|
|
1053
|
+
if (trackingBlocked() || !state.trackingPrerequisiteMet) {
|
|
1054
|
+
state.lastShopifyAssignmentSync = {
|
|
1055
|
+
at: Date.now(),
|
|
1056
|
+
status: "skipped",
|
|
1057
|
+
reason: "tracking_blocked",
|
|
1058
|
+
assignmentCount: state.assignments.length,
|
|
1059
|
+
};
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
try {
|
|
1063
|
+
const assignments = state.assignments.filter(attributable).map((assignment) => ({
|
|
1064
|
+
experimentId: assignment.experimentId,
|
|
1065
|
+
variationId: assignment.variationId,
|
|
1066
|
+
assignmentEpoch:
|
|
1067
|
+
!assignment.kind || assignment.kind === "experiment"
|
|
1068
|
+
? assignment.assignmentEpoch
|
|
1069
|
+
: undefined,
|
|
1070
|
+
}));
|
|
1071
|
+
const payload = buildShopifyAssignmentSyncPayload(assignments);
|
|
1072
|
+
const publish = (window as any).Shopify?.analytics?.publish;
|
|
1073
|
+
if (typeof publish !== "function") {
|
|
1074
|
+
if (state.config?.assignmentCookieSync === false) {
|
|
1075
|
+
state.lastShopifyAssignmentSync = {
|
|
1076
|
+
at: Date.now(),
|
|
1077
|
+
status: "skipped",
|
|
1078
|
+
reason: "assignment_cookie_sync_disabled",
|
|
1079
|
+
assignmentCount: state.assignments.length,
|
|
1080
|
+
};
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const cookieResult = writeAssignmentSyncCookie(payload);
|
|
1084
|
+
state.lastShopifyAssignmentSync = {
|
|
1085
|
+
at: Date.now(),
|
|
1086
|
+
status: cookieResult.written ? "cookie_fallback" : "skipped",
|
|
1087
|
+
reason: cookieResult.written ? undefined : cookieResult.reason ?? "shopify_publish_missing",
|
|
1088
|
+
assignmentCount: state.assignments.length,
|
|
1089
|
+
truncatedAssignmentCount: cookieResult.truncatedAssignmentCount,
|
|
1090
|
+
};
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
publish("drip_assignment_sync", payload);
|
|
1095
|
+
state.lastShopifyAssignmentSync = {
|
|
1096
|
+
at: Date.now(),
|
|
1097
|
+
status: "sent",
|
|
1098
|
+
assignmentCount: state.assignments.length,
|
|
1099
|
+
};
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
state.lastShopifyAssignmentSync = {
|
|
1102
|
+
at: Date.now(),
|
|
1103
|
+
status: "failed",
|
|
1104
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1105
|
+
assignmentCount: state.assignments.length,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
export function scheduleShopifyAssignmentSync(): void {
|
|
1111
|
+
clearShopifyAssignmentSyncTimers();
|
|
1112
|
+
if (state.config?.runtimeProfile === "empty_project" && state.perfFlags.perf_empty_project_v1) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
for (const delay of SHOPIFY_ASSIGNMENT_SYNC_DELAYS_MS) {
|
|
1116
|
+
if (delay <= 0) {
|
|
1117
|
+
publishAssignmentsToShopify();
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
shopifyAssignmentSyncTimers.push(
|
|
1121
|
+
window.setTimeout(publishAssignmentsToShopify, delay),
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export function variationsAllowed(config: InitConfig): boolean {
|
|
1127
|
+
return evaluateProjectPrerequisite(
|
|
1128
|
+
config.projectPrerequisites?.showVariationsRule,
|
|
1129
|
+
buildPrerequisiteContext(
|
|
1130
|
+
config,
|
|
1131
|
+
state.userId,
|
|
1132
|
+
state.consentRequired,
|
|
1133
|
+
state.consentGranted,
|
|
1134
|
+
"show_variations",
|
|
1135
|
+
),
|
|
1136
|
+
true,
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export function updateProjectActivationDiagnostics(config: InitConfig): void {
|
|
1141
|
+
const variationGate: ActivationGateDiagnostic = {
|
|
1142
|
+
id: "environment",
|
|
1143
|
+
label: "Project variation gate",
|
|
1144
|
+
status: state.variationPrerequisiteMet ? "passed" : "failed",
|
|
1145
|
+
reason: state.variationPrerequisiteMet ? "show_variations_allowed" : "show_variations_blocked",
|
|
1146
|
+
};
|
|
1147
|
+
const trackingGate: ActivationGateDiagnostic = {
|
|
1148
|
+
id: "tracking",
|
|
1149
|
+
label: "Tracking gate",
|
|
1150
|
+
status: trackingBlocked() || !state.trackingPrerequisiteMet ? "failed" : "passed",
|
|
1151
|
+
reason: trackingBlocked()
|
|
1152
|
+
? "consent_or_dnt_blocked"
|
|
1153
|
+
: state.trackingPrerequisiteMet
|
|
1154
|
+
? "start_tracking_allowed"
|
|
1155
|
+
: "start_tracking_blocked",
|
|
1156
|
+
};
|
|
1157
|
+
const goalsGate: ActivationGateDiagnostic = {
|
|
1158
|
+
id: "goals",
|
|
1159
|
+
label: "Goal listeners",
|
|
1160
|
+
status: state.analyticsStarted ? "passed" : "skipped",
|
|
1161
|
+
reason: state.analyticsStarted ? "listeners_active" : "tracking_not_started",
|
|
1162
|
+
detail: { goals: (config.experiments ?? []).reduce((count, experiment) => count + (experiment.goals?.length ?? 0), 0) },
|
|
1163
|
+
};
|
|
1164
|
+
state.activationDiagnostics.project = { gates: [variationGate, trackingGate, goalsGate] };
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export function trackingAllowed(config: InitConfig): boolean {
|
|
1168
|
+
return evaluateProjectPrerequisite(
|
|
1169
|
+
config.projectPrerequisites?.startTrackingRule,
|
|
1170
|
+
buildPrerequisiteContext(
|
|
1171
|
+
config,
|
|
1172
|
+
state.userId,
|
|
1173
|
+
state.consentRequired,
|
|
1174
|
+
state.consentGranted,
|
|
1175
|
+
"start_tracking",
|
|
1176
|
+
),
|
|
1177
|
+
true,
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export function experimentById(experiments: Experiment[]): Map<string, Experiment> {
|
|
1182
|
+
return new Map(experiments.map((experiment) => [experiment.id, experiment]));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
export function targetingLifecycleMode(
|
|
1186
|
+
experiment: Experiment | undefined,
|
|
1187
|
+
): "reset" | "deactivate" | "force_reset" | "persistent" {
|
|
1188
|
+
const mode = experiment?.targeting?.lifecycle?.mode;
|
|
1189
|
+
return mode === "reset" ||
|
|
1190
|
+
mode === "deactivate" ||
|
|
1191
|
+
mode === "force_reset" ||
|
|
1192
|
+
mode === "persistent"
|
|
1193
|
+
? mode
|
|
1194
|
+
: "reset";
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export function preservePersistentAssignments(
|
|
1198
|
+
previous: Assignment[],
|
|
1199
|
+
next: Assignment[],
|
|
1200
|
+
experiments: Experiment[],
|
|
1201
|
+
): Assignment[] {
|
|
1202
|
+
if (!previous.length) return next;
|
|
1203
|
+
const experimentsById = experimentById(experiments);
|
|
1204
|
+
const nextExperimentIds = new Set(next.map((assignment) => assignment.experimentId));
|
|
1205
|
+
const persistent = previous.filter(
|
|
1206
|
+
(assignment) => {
|
|
1207
|
+
if (nextExperimentIds.has(assignment.experimentId)) return false;
|
|
1208
|
+
const experiment = experimentsById.get(assignment.experimentId);
|
|
1209
|
+
if (!experiment) return false;
|
|
1210
|
+
return targetingLifecycleMode(experiment) === "persistent" ||
|
|
1211
|
+
includedPageRules(experiment).some(
|
|
1212
|
+
(rule) =>
|
|
1213
|
+
effectiveDeactivationMode(experiment, rule) === "persistent",
|
|
1214
|
+
);
|
|
1215
|
+
},
|
|
1216
|
+
);
|
|
1217
|
+
return persistent.length ? [...next, ...persistent] : next;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export function stopAnalyticsStage(): void {
|
|
1221
|
+
clearScheduledAnalyticsStart();
|
|
1222
|
+
clearShopifyAssignmentSyncTimers();
|
|
1223
|
+
state.pendingPerf = null;
|
|
1224
|
+
state.pendingDeliveryHealth = [];
|
|
1225
|
+
unwireGoals();
|
|
1226
|
+
cleanupHeatmapTracking();
|
|
1227
|
+
cleanupReplayTracking();
|
|
1228
|
+
cleanupSemanticTracking();
|
|
1229
|
+
if (state.tracker) {
|
|
1230
|
+
state.tracker.stop();
|
|
1231
|
+
state.tracker = null;
|
|
1232
|
+
}
|
|
1233
|
+
state.analyticsStarted = false;
|
|
1234
|
+
state.exposedAssignmentKeys.clear();
|
|
1235
|
+
if (state.config && state.config.runtimeProfile !== "empty_project") {
|
|
1236
|
+
updateProjectActivationDiagnostics(state.config);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
export function canMintVerifiedRedirectCarry(config: InitConfig): boolean {
|
|
1241
|
+
return Boolean(
|
|
1242
|
+
config.shopId &&
|
|
1243
|
+
config.endpoint &&
|
|
1244
|
+
config.ingestSignature &&
|
|
1245
|
+
config.externalTracking?.signed !== false,
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
export function planRedirectAssignment(
|
|
1250
|
+
config: InitConfig,
|
|
1251
|
+
experiments: Experiment[],
|
|
1252
|
+
assignments: Assignment[],
|
|
1253
|
+
): ReturnType<typeof redirectPlanForAssignment> {
|
|
1254
|
+
if (state.holdout) return null;
|
|
1255
|
+
return !state.qaMode && !state.screenshotMode
|
|
1256
|
+
? redirectPlanForAssignment(
|
|
1257
|
+
experiments,
|
|
1258
|
+
assignments,
|
|
1259
|
+
location.href,
|
|
1260
|
+
state.userId ?? "",
|
|
1261
|
+
config.externalTracking,
|
|
1262
|
+
// Match the external-link handover chokepoint: consent-pending (and DNT)
|
|
1263
|
+
// redirects still navigate, but cannot link attribution across origins.
|
|
1264
|
+
// The destination intentionally mints an independent identity.
|
|
1265
|
+
persistentStorageAllowed() && canMintVerifiedRedirectCarry(config),
|
|
1266
|
+
configuredStoreOrigins(config),
|
|
1267
|
+
)
|
|
1268
|
+
: null;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
export function navigateForRedirectAssignment(
|
|
1272
|
+
config: InitConfig,
|
|
1273
|
+
experiments: Experiment[],
|
|
1274
|
+
assignments: Assignment[],
|
|
1275
|
+
): boolean {
|
|
1276
|
+
const redirectPlan = planRedirectAssignment(config, experiments, assignments);
|
|
1277
|
+
if (!redirectPlan) return false;
|
|
1278
|
+
|
|
1279
|
+
state.redirectNavigationPending = true;
|
|
1280
|
+
// A fresh network config can replace a cached visual config while the
|
|
1281
|
+
// cached after-hydration apply is still pending. Redirect navigation has no
|
|
1282
|
+
// DOM apply to wait for, so detach that stale gate before emitting the
|
|
1283
|
+
// redirect exposure; otherwise both analytics startup and track() queue
|
|
1284
|
+
// behind a promise that cannot settle before location.replace unloads us.
|
|
1285
|
+
state.pendingInitialApply = null;
|
|
1286
|
+
if (state.analyticsStarted) {
|
|
1287
|
+
// A DOM/manual/prerequisite recheck can discover the redirect after this
|
|
1288
|
+
// pageload already exposed the assignment. Keep the pageload dedupe key:
|
|
1289
|
+
// track only when it has not yet been recorded, and never contaminate the
|
|
1290
|
+
// arm with a second exposure immediately before navigation.
|
|
1291
|
+
trackViewedAssignments(config, [redirectPlan.assignment]);
|
|
1292
|
+
} else {
|
|
1293
|
+
startAnalyticsStage(config, experiments, [redirectPlan.assignment]);
|
|
1294
|
+
}
|
|
1295
|
+
// Start the keepalive request before navigation. The redirect assignment is
|
|
1296
|
+
// now in the tracker queue and must not wait for the normal batch interval,
|
|
1297
|
+
// because location.replace may tear down this document immediately.
|
|
1298
|
+
void state.tracker?.flush();
|
|
1299
|
+
if (
|
|
1300
|
+
state.exposedAssignmentKeys.has(
|
|
1301
|
+
exposureAssignmentKey(redirectPlan.assignment),
|
|
1302
|
+
)
|
|
1303
|
+
) {
|
|
1304
|
+
markRedirectAssignmentCarryExposed(redirectPlan.destination);
|
|
1305
|
+
}
|
|
1306
|
+
const canMintVerifiedCarry = canMintVerifiedRedirectCarry(config);
|
|
1307
|
+
if (
|
|
1308
|
+
canMintVerifiedCarry &&
|
|
1309
|
+
hasGeneratedRedirectAssignmentCarry(redirectPlan.destination)
|
|
1310
|
+
) {
|
|
1311
|
+
void mintBucketingHandoverPayload(
|
|
1312
|
+
config,
|
|
1313
|
+
state.userId ?? "",
|
|
1314
|
+
[redirectPlan.assignment],
|
|
1315
|
+
).then((verifiedToken) => {
|
|
1316
|
+
finalizeRedirectAssignmentCarryHandover(
|
|
1317
|
+
redirectPlan.destination,
|
|
1318
|
+
verifiedToken,
|
|
1319
|
+
);
|
|
1320
|
+
location.replace(redirectPlan.destination.href);
|
|
1321
|
+
});
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
location.replace(redirectPlan.destination.href);
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
export /** Bounds the unwind drain below; a stuck reset-JS/pushState pair must not livelock. */
|
|
1329
|
+
const RECONCILE_MAX_DRAINS = 3;
|
|
1330
|
+
|
|
1331
|
+
export /**
|
|
1332
|
+
* Serialize reconciliation. A nested pass — merchant reset JS calling
|
|
1333
|
+
* `history.pushState` (the router notifies synchronously), or `drip.reevaluate()`
|
|
1334
|
+
* from variation JS — would read a page mid-teardown and let the outer pass
|
|
1335
|
+
* delete artifacts the nested pass just applied. A nested call only records
|
|
1336
|
+
* that another pass is due. The outer pass drains it on unwind and re-derives
|
|
1337
|
+
* URL, targeting, and page activation from live state, so a nested navigation
|
|
1338
|
+
* still lands; the drain runs the outer pass's own reason and force flag.
|
|
1339
|
+
*/
|
|
1340
|
+
function reevaluateAssignments(
|
|
1341
|
+
reason: "url" | "dom" | "manual" | "prerequisite",
|
|
1342
|
+
forceReapply = false,
|
|
1343
|
+
skipHoldGate = false,
|
|
1344
|
+
pageRuleScope?: PageRuleReconcileScope,
|
|
1345
|
+
): Assignment[] {
|
|
1346
|
+
if (state.reconcileInProgress) {
|
|
1347
|
+
state.reconcilePending = true;
|
|
1348
|
+
return state.assignments;
|
|
1349
|
+
}
|
|
1350
|
+
for (let drain = 0; drain < RECONCILE_MAX_DRAINS; drain += 1) {
|
|
1351
|
+
state.reconcilePending = false;
|
|
1352
|
+
state.reconcileInProgress = true;
|
|
1353
|
+
try {
|
|
1354
|
+
// Every pass path leaves its result in state.assignments.
|
|
1355
|
+
reevaluateAssignmentsPass(
|
|
1356
|
+
reason,
|
|
1357
|
+
forceReapply,
|
|
1358
|
+
skipHoldGate,
|
|
1359
|
+
pageRuleScope,
|
|
1360
|
+
);
|
|
1361
|
+
} finally {
|
|
1362
|
+
state.reconcileInProgress = false;
|
|
1363
|
+
}
|
|
1364
|
+
if (!state.reconcilePending) break;
|
|
1365
|
+
}
|
|
1366
|
+
if (state.reconcilePending) {
|
|
1367
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugWarn("eval", "Reconcile drain limit reached; dropped a queued re-evaluation");
|
|
1368
|
+
}
|
|
1369
|
+
state.reconcilePending = false;
|
|
1370
|
+
return state.assignments;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
export function reevaluateAssignmentsPass(
|
|
1374
|
+
reason: "url" | "dom" | "manual" | "prerequisite",
|
|
1375
|
+
forceReapply: boolean,
|
|
1376
|
+
skipHoldGate: boolean,
|
|
1377
|
+
pageRuleScope?: PageRuleReconcileScope,
|
|
1378
|
+
): Assignment[] {
|
|
1379
|
+
if (!state.config || !state.userId) return state.assignments;
|
|
1380
|
+
if (state.pendingInitialApply) {
|
|
1381
|
+
// The initial apply is gated behind hydration (after_hydration / a
|
|
1382
|
+
// pre-init hold). Running a recheck now — e.g. setConsent() before
|
|
1383
|
+
// hydration settles — would apply mutations outside the gate; queue ONE
|
|
1384
|
+
// coalesced recheck until the gate opens (mutation storms during a
|
|
1385
|
+
// multi-second hydration window must not each schedule a full scan).
|
|
1386
|
+
if (state.queuedRecheck) {
|
|
1387
|
+
state.queuedRecheck.reason = reason;
|
|
1388
|
+
state.queuedRecheck.forceReapply =
|
|
1389
|
+
state.queuedRecheck.forceReapply || forceReapply;
|
|
1390
|
+
if (state.queuedRecheck.pageRuleScope && pageRuleScope) {
|
|
1391
|
+
state.queuedRecheck.pageRuleScope = {
|
|
1392
|
+
triggeredPageIds: new Set([
|
|
1393
|
+
...state.queuedRecheck.pageRuleScope.triggeredPageIds,
|
|
1394
|
+
...pageRuleScope.triggeredPageIds,
|
|
1395
|
+
]),
|
|
1396
|
+
includeImmediate:
|
|
1397
|
+
state.queuedRecheck.pageRuleScope.includeImmediate ||
|
|
1398
|
+
pageRuleScope.includeImmediate,
|
|
1399
|
+
};
|
|
1400
|
+
} else {
|
|
1401
|
+
// A queued global pass must remain global.
|
|
1402
|
+
state.queuedRecheck.pageRuleScope = undefined;
|
|
1403
|
+
}
|
|
1404
|
+
} else {
|
|
1405
|
+
state.queuedRecheck = {
|
|
1406
|
+
reason,
|
|
1407
|
+
forceReapply,
|
|
1408
|
+
pageRuleScope: pageRuleScope
|
|
1409
|
+
? {
|
|
1410
|
+
triggeredPageIds: new Set(pageRuleScope.triggeredPageIds),
|
|
1411
|
+
includeImmediate: pageRuleScope.includeImmediate,
|
|
1412
|
+
}
|
|
1413
|
+
: undefined,
|
|
1414
|
+
};
|
|
1415
|
+
void state.pendingInitialApply.then(() => {
|
|
1416
|
+
const queued = state.queuedRecheck;
|
|
1417
|
+
state.queuedRecheck = null;
|
|
1418
|
+
if (queued) {
|
|
1419
|
+
reevaluateAssignments(
|
|
1420
|
+
queued.reason,
|
|
1421
|
+
queued.forceReapply,
|
|
1422
|
+
false,
|
|
1423
|
+
queued.pageRuleScope,
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
return state.assignments;
|
|
1429
|
+
}
|
|
1430
|
+
if (!skipHoldGate && hasMutationHold()) {
|
|
1431
|
+
// A framework hold (drip.hold()) during an SPA route hydration defers
|
|
1432
|
+
// recheck applies exactly like the initial one. The gate fails open at
|
|
1433
|
+
// the hydration timeout; skipHoldGate prevents re-gating after that.
|
|
1434
|
+
const gate: Promise<void> = awaitHydration(
|
|
1435
|
+
state.config.spa?.hydrationTimeoutMs ?? 3_000,
|
|
1436
|
+
{ waitForHydration: false },
|
|
1437
|
+
).then(() => {
|
|
1438
|
+
if (state.pendingInitialApply === gate) state.pendingInitialApply = null;
|
|
1439
|
+
reevaluateAssignments(
|
|
1440
|
+
reason,
|
|
1441
|
+
forceReapply,
|
|
1442
|
+
true,
|
|
1443
|
+
pageRuleScope,
|
|
1444
|
+
);
|
|
1445
|
+
});
|
|
1446
|
+
state.pendingInitialApply = gate;
|
|
1447
|
+
return state.assignments;
|
|
1448
|
+
}
|
|
1449
|
+
if (state.config.runtimeProfile === "empty_project" && state.perfFlags.perf_empty_project_v1) {
|
|
1450
|
+
state.assignments = [];
|
|
1451
|
+
state.variationPrerequisiteMet = true;
|
|
1452
|
+
state.activationDiagnostics = { project: { gates: [] }, experiments: [] };
|
|
1453
|
+
state.lastAssignmentSignature = "none";
|
|
1454
|
+
reconcileArtifacts(new Map());
|
|
1455
|
+
return state.assignments;
|
|
1456
|
+
}
|
|
1457
|
+
const exps = state.config.experiments ?? [];
|
|
1458
|
+
|
|
1459
|
+
captureUrlSignals(state.config);
|
|
1460
|
+
captureDomAndGlobalSignals(state.config);
|
|
1461
|
+
state.variationPrerequisiteMet = variationsAllowed(state.config);
|
|
1462
|
+
updateProjectActivationDiagnostics(state.config);
|
|
1463
|
+
if (!state.variationPrerequisiteMet) {
|
|
1464
|
+
state.assignments = [];
|
|
1465
|
+
state.activationDiagnostics.experiments = [];
|
|
1466
|
+
state.lastAssignmentSignature = "none";
|
|
1467
|
+
reconcileArtifacts(new Map());
|
|
1468
|
+
revealAll();
|
|
1469
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("eval", `Variations blocked by project prerequisite (${reason})`);
|
|
1470
|
+
notifyQaAssistantRuntimeListeners();
|
|
1471
|
+
return state.assignments;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const evaluationUrl = location.href;
|
|
1475
|
+
const targetingSourceAttributes = state.config.attributes ??
|
|
1476
|
+
EMPTY_TARGETING_ATTRIBUTES;
|
|
1477
|
+
const targetingPass = createTargetingEvaluationPass(
|
|
1478
|
+
targetingSourceAttributes,
|
|
1479
|
+
evaluationUrl,
|
|
1480
|
+
persistentStorageAllowed(),
|
|
1481
|
+
);
|
|
1482
|
+
const previousRuntimeKeys = assignmentRuntimeKeys(state.assignments);
|
|
1483
|
+
const pageActivation = reconcilePageActivation(exps, evaluationUrl, {
|
|
1484
|
+
targetingPass,
|
|
1485
|
+
pageRuleScope,
|
|
1486
|
+
});
|
|
1487
|
+
const teardownPages = pageActivation.transitions
|
|
1488
|
+
.filter((transition) => transition.teardown)
|
|
1489
|
+
.map((transition) => ({ pageId: transition.pageId, mode: transition.mode }));
|
|
1490
|
+
if (teardownPages.length > 0) {
|
|
1491
|
+
teardownTransaction(teardownPages, { goalUrl: evaluationUrl });
|
|
1492
|
+
}
|
|
1493
|
+
state.pageActivation = pageActivation.next;
|
|
1494
|
+
pageActivation.pollRules.forEach((rule) => startPageRulePoll(rule));
|
|
1495
|
+
syncAssignmentPageActive(state.assignments, exps);
|
|
1496
|
+
const edgeEligibility = state.config.userId == null && edgeRuntimeActive()
|
|
1497
|
+
? evaluateWithDiagnostics(exps, {
|
|
1498
|
+
url: evaluationUrl,
|
|
1499
|
+
attributes: targetingSourceAttributes,
|
|
1500
|
+
userId: state.userId,
|
|
1501
|
+
persistSignals: persistentStorageAllowed(),
|
|
1502
|
+
storeOrigin: configuredStoreOrigins(state.config),
|
|
1503
|
+
targetingPass,
|
|
1504
|
+
})
|
|
1505
|
+
: null;
|
|
1506
|
+
const edgeEvaluation = edgeEligibility
|
|
1507
|
+
? evaluateEdgeRuntimeAssignments(
|
|
1508
|
+
exps,
|
|
1509
|
+
{
|
|
1510
|
+
url: evaluationUrl,
|
|
1511
|
+
attributes: targetingAttributesForPass(
|
|
1512
|
+
targetingPass,
|
|
1513
|
+
targetingSourceAttributes,
|
|
1514
|
+
evaluationUrl,
|
|
1515
|
+
persistentStorageAllowed(),
|
|
1516
|
+
),
|
|
1517
|
+
userId: state.userId,
|
|
1518
|
+
},
|
|
1519
|
+
edgeEligibility.assignments,
|
|
1520
|
+
edgeEligibility.diagnostics,
|
|
1521
|
+
)
|
|
1522
|
+
: null;
|
|
1523
|
+
const fallbackExperiments = edgeEvaluation
|
|
1524
|
+
? edgeEvaluation.experimentIds.size > 0
|
|
1525
|
+
? exps.filter((experiment) => !edgeEvaluation.experimentIds.has(experiment.id))
|
|
1526
|
+
: []
|
|
1527
|
+
: exps;
|
|
1528
|
+
const fallbackEvaluation = evaluateWithDiagnostics(fallbackExperiments, {
|
|
1529
|
+
url: evaluationUrl,
|
|
1530
|
+
attributes: targetingSourceAttributes,
|
|
1531
|
+
userId: state.userId,
|
|
1532
|
+
persistSignals: persistentStorageAllowed(),
|
|
1533
|
+
storeOrigin: configuredStoreOrigins(state.config),
|
|
1534
|
+
exclusionGroups: state.config.exclusionGroups,
|
|
1535
|
+
targetingPass,
|
|
1536
|
+
});
|
|
1537
|
+
const evaluation = edgeEvaluation
|
|
1538
|
+
? resolveCombinedEdgeEvaluation(
|
|
1539
|
+
edgeEvaluation.assignments,
|
|
1540
|
+
fallbackEvaluation.assignments,
|
|
1541
|
+
[...edgeEvaluation.diagnostics, ...fallbackEvaluation.diagnostics],
|
|
1542
|
+
{
|
|
1543
|
+
userId: state.userId,
|
|
1544
|
+
exclusionGroups: state.config.exclusionGroups,
|
|
1545
|
+
},
|
|
1546
|
+
)
|
|
1547
|
+
: fallbackEvaluation;
|
|
1548
|
+
const preserved = preservePersistentAssignments(
|
|
1549
|
+
edgeEvaluation
|
|
1550
|
+
? state.assignments.filter(
|
|
1551
|
+
(assignment) => !edgeEvaluation.experimentIds.has(assignment.experimentId),
|
|
1552
|
+
)
|
|
1553
|
+
: state.assignments,
|
|
1554
|
+
evaluation.assignments,
|
|
1555
|
+
exps,
|
|
1556
|
+
);
|
|
1557
|
+
// A persistent targeting lifecycle may reintroduce a prior assignment after
|
|
1558
|
+
// pass two. Re-run the group plane so that cannot resurrect a group loser.
|
|
1559
|
+
const updated = syncAssignmentPageActive(filterUnsafeRedirectAssignments(
|
|
1560
|
+
exps,
|
|
1561
|
+
applyExclusionAndPersonalization(preserved, {
|
|
1562
|
+
userId: state.userId,
|
|
1563
|
+
exclusionGroups: state.config.exclusionGroups,
|
|
1564
|
+
}).assignments,
|
|
1565
|
+
evaluationUrl,
|
|
1566
|
+
state.userId,
|
|
1567
|
+
state.config.externalTracking,
|
|
1568
|
+
shouldPreserveOffTargetRedirectAssignment,
|
|
1569
|
+
configuredStoreOrigins(state.config),
|
|
1570
|
+
), exps);
|
|
1571
|
+
state.activationDiagnostics.experiments = evaluation.diagnostics;
|
|
1572
|
+
const nextSignature = assignmentSignature(updated);
|
|
1573
|
+
const signatureChanged = nextSignature !== state.lastAssignmentSignature;
|
|
1574
|
+
state.lastAssignmentSignature = nextSignature;
|
|
1575
|
+
state.assignments = updated;
|
|
1576
|
+
recordAttributableAssignments(updated);
|
|
1577
|
+
if (forceReapply) invalidateLiveArtifactsForReplay();
|
|
1578
|
+
const artifactResult = reconcileArtifacts(
|
|
1579
|
+
desiredArtifactSet(updated),
|
|
1580
|
+
evaluationUrl,
|
|
1581
|
+
);
|
|
1582
|
+
reassertAssignmentMutations(updated);
|
|
1583
|
+
const runtimeAssignmentsChanged = !sameStringSet(
|
|
1584
|
+
previousRuntimeKeys,
|
|
1585
|
+
assignmentRuntimeKeys(updated),
|
|
1586
|
+
);
|
|
1587
|
+
if (runtimeAssignmentsChanged) installSdkCartMintTrigger();
|
|
1588
|
+
if (
|
|
1589
|
+
runtimeAssignmentsChanged &&
|
|
1590
|
+
state.analyticsStarted &&
|
|
1591
|
+
!trackingBlocked() &&
|
|
1592
|
+
state.trackingPrerequisiteMet
|
|
1593
|
+
) {
|
|
1594
|
+
setupHeatmapTracking(state.assignments, exps);
|
|
1595
|
+
setupReplayTracking(state.assignments, exps);
|
|
1596
|
+
syncActiveAssignmentToShopifyCart();
|
|
1597
|
+
scheduleShopifyAssignmentSync();
|
|
1598
|
+
if (reason !== "url") {
|
|
1599
|
+
trackViewedAssignments(state.config, undefined, targetingPass);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (navigateForRedirectAssignment(state.config, exps, state.assignments)) {
|
|
1604
|
+
notifyQaAssistantRuntimeListeners();
|
|
1605
|
+
return state.assignments;
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("eval", `Re-evaluated assignments (${reason})`, {
|
|
1609
|
+
artifactResult,
|
|
1610
|
+
count: state.assignments.length,
|
|
1611
|
+
signatureChanged,
|
|
1612
|
+
});
|
|
1613
|
+
notifyQaAssistantRuntimeListeners();
|
|
1614
|
+
return state.assignments;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
export function reapplyQaAssistantForceMap(): void {
|
|
1618
|
+
if (!state.config || !state.userId) {
|
|
1619
|
+
try {
|
|
1620
|
+
location.reload();
|
|
1621
|
+
} catch {}
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
reconcileForcedUnavailable(state.config);
|
|
1625
|
+
reevaluateAssignments("manual", true);
|
|
1626
|
+
notifyQaAssistantRuntimeListeners();
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
export function reconcileForcedUnavailable(config: InitConfig): void {
|
|
1630
|
+
if (!state.qaMode) {
|
|
1631
|
+
state.forcedUnavailable = [];
|
|
1632
|
+
state.edgeDeviceAudienceUnsupported = [];
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const loadedExperimentIds = new Set((config.experiments ?? []).map((experiment) => experiment.id));
|
|
1636
|
+
const warningsByExperimentId = new Map(
|
|
1637
|
+
(config.qa_warnings ?? [])
|
|
1638
|
+
.filter((warning) => warning.code === "forced_experiment_unavailable")
|
|
1639
|
+
.flatMap((warning) => warning.reason ? [[warning.experiment_id, warning.reason] as const] : []),
|
|
1640
|
+
);
|
|
1641
|
+
state.forcedUnavailable = Object.keys(state.forceMap ?? {}).flatMap((experimentId) =>
|
|
1642
|
+
loadedExperimentIds.has(experimentId)
|
|
1643
|
+
? []
|
|
1644
|
+
: [{
|
|
1645
|
+
experimentId,
|
|
1646
|
+
reason: warningsByExperimentId.get(experimentId) ?? "not_in_config" as const,
|
|
1647
|
+
}],
|
|
1648
|
+
);
|
|
1649
|
+
state.edgeDeviceAudienceUnsupported = (config.qa_warnings ?? []).flatMap((warning) =>
|
|
1650
|
+
warning.code === "edge_device_audience_unsupported" && warning.audience_name
|
|
1651
|
+
? [{ experimentId: warning.experiment_id, audienceName: warning.audience_name }]
|
|
1652
|
+
: [],
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
export function __getQaAssistantRuntimeSnapshot(): QaAssistantRuntimeSnapshot {
|
|
1657
|
+
return {
|
|
1658
|
+
config: state.config,
|
|
1659
|
+
assignments: state.assignments.slice(),
|
|
1660
|
+
activationDiagnostics: {
|
|
1661
|
+
project: { gates: state.activationDiagnostics.project.gates.slice() },
|
|
1662
|
+
experiments: state.activationDiagnostics.experiments.slice(),
|
|
1663
|
+
},
|
|
1664
|
+
pageActivation: pageActivationDiagnosticRows({ includePageNames: true }),
|
|
1665
|
+
goalScopeUrl: state.wiredGoalUrl,
|
|
1666
|
+
forceMap: state.forceMap ? { ...state.forceMap } : null,
|
|
1667
|
+
forcedUnavailable: state.forcedUnavailable.map((entry) => ({ ...entry })),
|
|
1668
|
+
edgeDeviceAudienceUnsupported: state.edgeDeviceAudienceUnsupported.map((entry) => ({ ...entry })),
|
|
1669
|
+
platform: state.platform,
|
|
1670
|
+
qaMode: state.qaMode,
|
|
1671
|
+
orderIdentity: detectOrderIdentity(),
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
export function __subscribeQaAssistantRuntime(listener: () => void): () => void {
|
|
1676
|
+
runtimeSharedState.qaAssistantRuntimeListeners.add(listener);
|
|
1677
|
+
return () => {
|
|
1678
|
+
runtimeSharedState.qaAssistantRuntimeListeners.delete(listener);
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
export function __forceQaAssistantVariation(experimentId: string, variationId: string): void {
|
|
1683
|
+
if (!experimentId || !variationId) return;
|
|
1684
|
+
state.forceMap = { ...(state.forceMap ?? {}), [experimentId]: variationId };
|
|
1685
|
+
persistQaForce(state.forceMap);
|
|
1686
|
+
reapplyQaAssistantForceMap();
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
export function __resetQaAssistantForce(experimentId?: string): void {
|
|
1690
|
+
if (!experimentId) {
|
|
1691
|
+
state.forceMap = null;
|
|
1692
|
+
} else if (state.forceMap) {
|
|
1693
|
+
const next = { ...state.forceMap };
|
|
1694
|
+
delete next[experimentId];
|
|
1695
|
+
state.forceMap = Object.keys(next).length ? next : null;
|
|
1696
|
+
}
|
|
1697
|
+
persistQaForce(state.forceMap ?? {});
|
|
1698
|
+
reapplyQaAssistantForceMap();
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
export function clearQaForceFromCurrentUrl(): boolean {
|
|
1702
|
+
if (!clearQaForceFromUrl()) return false;
|
|
1703
|
+
state.forceMap = null;
|
|
1704
|
+
state.qaMode = false;
|
|
1705
|
+
state.qaSessionId = "";
|
|
1706
|
+
if (state.config && state.userId) reapplyQaAssistantForceMap();
|
|
1707
|
+
return true;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
export function initWithExperiments(
|
|
1711
|
+
config: InitConfig,
|
|
1712
|
+
initialExperiments: Experiment[],
|
|
1713
|
+
configSource: ConfigSource = "inline",
|
|
1714
|
+
perfContext: InitPerfContext = createInitPerfContext(),
|
|
1715
|
+
parsedRedirectCarry: VerifiedRedirectCarry | null = null,
|
|
1716
|
+
verifiedHandover: Awaited<ReturnType<typeof readBucketingHandover>> = null,
|
|
1717
|
+
): Assignment[] {
|
|
1718
|
+
removeSdkCartMintTrigger();
|
|
1719
|
+
if (runtimeSharedState.earlyReassertTeardownTimer) clearTimeout(runtimeSharedState.earlyReassertTeardownTimer);
|
|
1720
|
+
runtimeSharedState.stopEarlyReassert?.();
|
|
1721
|
+
runtimeSharedState.stopEarlyReassert = null;
|
|
1722
|
+
let experiments = initialExperiments;
|
|
1723
|
+
const previousConfigSource = state.configSource;
|
|
1724
|
+
const runtimeResolveStartedAt = monotonicNow();
|
|
1725
|
+
const runtimeConfig = resolveRuntimeConfig(config);
|
|
1726
|
+
const afMode = runtimeConfig.antiFlickerMode;
|
|
1727
|
+
const afTimeout = runtimeConfig.antiFlickerTimeoutMs;
|
|
1728
|
+
const trackStart = runtimeConfig.trackingStart;
|
|
1729
|
+
const idleTimeout = runtimeConfig.trackingIdleTimeoutMs;
|
|
1730
|
+
const jsPolicy = runtimeConfig.customJsPolicy;
|
|
1731
|
+
const platform = resolvePlatform(config);
|
|
1732
|
+
const perfFlags = readPerfFlags(config);
|
|
1733
|
+
const emptyProjectProfileCandidate = canUseEmptyProjectProfile(config, experiments, platform);
|
|
1734
|
+
const activationMode = resolveActivationMode(config);
|
|
1735
|
+
const pageTriggerMode = resolvePageTriggerMode(config);
|
|
1736
|
+
const detectorRules = collectPageDetectorRules(experiments);
|
|
1737
|
+
const hasHistoryPageRules = (detectorRules.get("history")?.length ?? 0) > 0;
|
|
1738
|
+
const hasPollingPageRules = (detectorRules.get("polling")?.length ?? 0) > 0;
|
|
1739
|
+
const hasDomPageRules = (detectorRules.get("dom")?.length ?? 0) > 0;
|
|
1740
|
+
const baseRouterMode = resolveRouterMode(config, platform);
|
|
1741
|
+
const historyForcedRouterMode =
|
|
1742
|
+
(pageTriggerMode === "url_change" || hasHistoryPageRules) &&
|
|
1743
|
+
baseRouterMode === "none"
|
|
1744
|
+
? "history"
|
|
1745
|
+
: baseRouterMode;
|
|
1746
|
+
const routerMode = hasPollingPageRules &&
|
|
1747
|
+
(historyForcedRouterMode === "none" || historyForcedRouterMode === "history")
|
|
1748
|
+
? "polling"
|
|
1749
|
+
: historyForcedRouterMode;
|
|
1750
|
+
const manualInitialTrigger = config.spa?.reinitOnly === true || pageTriggerMode === "manual";
|
|
1751
|
+
const shopDomPageTrigger = resolveDomPageTrigger(config, pageTriggerMode);
|
|
1752
|
+
const domPageTrigger = shopDomPageTrigger || hasDomPageRules;
|
|
1753
|
+
const urlDiffNeeds = computeUrlDiffNeeds(
|
|
1754
|
+
experimentsForGlobalRouteReevaluation(experiments),
|
|
1755
|
+
config.signals,
|
|
1756
|
+
);
|
|
1757
|
+
addInitPhase(perfContext, "runtime_setup_ms", runtimeResolveStartedAt);
|
|
1758
|
+
const assignmentStartedAt = monotonicNow();
|
|
1759
|
+
const initStartedAt = Date.now();
|
|
1760
|
+
updateLoadDiagnostics({
|
|
1761
|
+
stage: "init_started",
|
|
1762
|
+
startedAt: initStartedAt,
|
|
1763
|
+
configFetchStatus: configSource === "network" ? state.load.configFetchStatus : state.load.configFetchStatus ?? "idle",
|
|
1764
|
+
});
|
|
1765
|
+
const consentIdentityStartedAt = monotonicNow();
|
|
1766
|
+
const initialConsentRequired = resolveConsentRequired(config);
|
|
1767
|
+
const initialConsentGranted = resolveConsentGranted(config, initialConsentRequired);
|
|
1768
|
+
const initialTrackingDisabledByDnt = doNotTrackEnabled(config);
|
|
1769
|
+
const initialStorageAllowed = storageAllowed(
|
|
1770
|
+
initialConsentRequired,
|
|
1771
|
+
initialConsentGranted,
|
|
1772
|
+
initialTrackingDisabledByDnt,
|
|
1773
|
+
);
|
|
1774
|
+
// Only the exact baked inline payload may consume the handoff. Cache and
|
|
1775
|
+
// network refreshes are authoritative re-evaluations (kill-switch path).
|
|
1776
|
+
const predecided = configSource === "inline"
|
|
1777
|
+
? bootstrapPredecision(config, perfContext)
|
|
1778
|
+
: null;
|
|
1779
|
+
state.consentRequired = initialConsentRequired;
|
|
1780
|
+
state.consentGranted = initialConsentGranted;
|
|
1781
|
+
state.consentModeEnabled = config.consentMode?.enabled === true;
|
|
1782
|
+
state.trackingDisabledByDnt = initialTrackingDisabledByDnt;
|
|
1783
|
+
configureIdentityStorage(initialStorageAllowed ? config.dataProtection : { ...(config.dataProtection ?? {}), storageMode: "memory" });
|
|
1784
|
+
if (!initialStorageAllowed) {
|
|
1785
|
+
clearEarlyInteractions();
|
|
1786
|
+
clearConsentStorage();
|
|
1787
|
+
}
|
|
1788
|
+
const redirectCarry = emptyProjectProfileCandidate || !initialStorageAllowed || config.userId
|
|
1789
|
+
? null
|
|
1790
|
+
: validateRedirectAssignmentCarry(
|
|
1791
|
+
experiments,
|
|
1792
|
+
parsedRedirectCarry,
|
|
1793
|
+
typeof location !== "undefined" ? location.href : undefined,
|
|
1794
|
+
);
|
|
1795
|
+
if (redirectCarry) {
|
|
1796
|
+
adoptUserId(redirectCarry.visitorId);
|
|
1797
|
+
clearPendingRedirectAssignmentCarry();
|
|
1798
|
+
}
|
|
1799
|
+
const handover = emptyProjectProfileCandidate ? null : verifiedHandover;
|
|
1800
|
+
if (initialStorageAllowed && handover?.visitorId && !config.userId && !redirectCarry) {
|
|
1801
|
+
adoptUserId(handover.visitorId);
|
|
1802
|
+
}
|
|
1803
|
+
const predecidedIdentityHint = config.userId == null
|
|
1804
|
+
? predecided?.visitorId
|
|
1805
|
+
: undefined;
|
|
1806
|
+
const identityHint = config.userId ?? predecidedIdentityHint;
|
|
1807
|
+
const resolvedUserId = initialStorageAllowed
|
|
1808
|
+
? getUserId(identityHint)
|
|
1809
|
+
: getEphemeralUserId(identityHint);
|
|
1810
|
+
if (initialStorageAllowed && predecidedIdentityHint) {
|
|
1811
|
+
adoptUserId(resolvedUserId);
|
|
1812
|
+
}
|
|
1813
|
+
if (predecided && resolvedUserId !== predecided.visitorId) perfContext.predecisionMismatches += 1;
|
|
1814
|
+
|
|
1815
|
+
// This gate is intentionally synchronous and precedes both applyProjectCode
|
|
1816
|
+
// and assignment evaluation below. That ordering lets a cached config which
|
|
1817
|
+
// already carries holdout settings prevent cached project/variation code
|
|
1818
|
+
// without waiting for the network refresh. Operational sessions must always
|
|
1819
|
+
// retain the complete experiment runtime; QA includes preview sessions.
|
|
1820
|
+
state.qaMode = isQaMode();
|
|
1821
|
+
state.screenshotMode = isScreenshotMode();
|
|
1822
|
+
state.holdout = !state.qaMode && !state.screenshotMode && isHoldoutVisitor(config, resolvedUserId);
|
|
1823
|
+
if (state.holdout) experiments = applyHoldoutGate(config, resolvedUserId, experiments);
|
|
1824
|
+
if (
|
|
1825
|
+
configSource === "network" &&
|
|
1826
|
+
previousConfigSource === "cache" &&
|
|
1827
|
+
state.holdout
|
|
1828
|
+
) {
|
|
1829
|
+
// Cached direct/headless installs render before the network response. If
|
|
1830
|
+
// the authoritative config moves this visitor into holdout, retire every
|
|
1831
|
+
// cached treatment before replacing the assignment set.
|
|
1832
|
+
resetProjectCode();
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
configureDebug(config.debug, resolvedUserId, { storageAllowed: initialStorageAllowed });
|
|
1836
|
+
addInitPhase(perfContext, "consent_identity_ms", consentIdentityStartedAt);
|
|
1837
|
+
|
|
1838
|
+
const runtimeSetupStartedAt = monotonicNow();
|
|
1839
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugGroup("init");
|
|
1840
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("init", `Initializing with ${experiments.length} experiment(s)`, {
|
|
1841
|
+
executionMode: runtimeConfig.executionMode,
|
|
1842
|
+
antiFlicker: afMode,
|
|
1843
|
+
trackingStart: trackStart,
|
|
1844
|
+
platform,
|
|
1845
|
+
routerMode,
|
|
1846
|
+
activationMode,
|
|
1847
|
+
pageTrigger: pageTriggerMode,
|
|
1848
|
+
configSource,
|
|
1849
|
+
});
|
|
1850
|
+
__DRIP_FEATURE_DEVTOOLS__ && traceStep("Initiated snippet runtime", {
|
|
1851
|
+
revision: config.delivery?.revision,
|
|
1852
|
+
sdkVariant: config.runtimeFeatures?.sdkVariant,
|
|
1853
|
+
configSource,
|
|
1854
|
+
deliveryState: config.delivery?.deliveryState ?? config.deliveryState,
|
|
1855
|
+
});
|
|
1856
|
+
__DRIP_FEATURE_DEVTOOLS__ && traceStep("Initiated project", {
|
|
1857
|
+
shopId: config.shopId,
|
|
1858
|
+
executionMode: runtimeConfig.executionMode,
|
|
1859
|
+
platform,
|
|
1860
|
+
routerMode,
|
|
1861
|
+
activationMode,
|
|
1862
|
+
pageTrigger: pageTriggerMode,
|
|
1863
|
+
});
|
|
1864
|
+
__DRIP_FEATURE_DEVTOOLS__ && dtLog("init", `Init: ${experiments.length} experiment(s)`, {
|
|
1865
|
+
executionMode: runtimeConfig.executionMode,
|
|
1866
|
+
antiFlicker: afMode,
|
|
1867
|
+
platform,
|
|
1868
|
+
routerMode,
|
|
1869
|
+
activationMode,
|
|
1870
|
+
pageTrigger: pageTriggerMode,
|
|
1871
|
+
configSource,
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// Apply custom JS policy
|
|
1875
|
+
setJsPolicy(jsPolicy);
|
|
1876
|
+
if (!emptyProjectProfileCandidate && !state.holdout) applyProjectCode(config);
|
|
1877
|
+
|
|
1878
|
+
const urlForceMap = parseForceParam();
|
|
1879
|
+
const handoverForceMap = handover?.forceMap && Object.keys(handover.forceMap).length > 0
|
|
1880
|
+
? handover.forceMap
|
|
1881
|
+
: null;
|
|
1882
|
+
state.forceMap = handoverForceMap || urlForceMap
|
|
1883
|
+
? { ...(handoverForceMap ?? {}), ...(urlForceMap ?? {}) }
|
|
1884
|
+
: null;
|
|
1885
|
+
|
|
1886
|
+
// QA and screenshot sessions retain the full runtime even if the published
|
|
1887
|
+
// project is otherwise empty.
|
|
1888
|
+
const emptyProjectFastPath = emptyProjectProfileCandidate
|
|
1889
|
+
&& !state.qaMode
|
|
1890
|
+
&& !state.screenshotMode;
|
|
1891
|
+
const resolvedConfig = {
|
|
1892
|
+
...config,
|
|
1893
|
+
experiments,
|
|
1894
|
+
...(state.holdout ? { commerce: undefined } : {}),
|
|
1895
|
+
runtimeProfile: emptyProjectFastPath ? "empty_project" : "full",
|
|
1896
|
+
};
|
|
1897
|
+
if (
|
|
1898
|
+
resolvedConfig.cartAttributeWrite === "external" &&
|
|
1899
|
+
state.config?.cartAttributeWrite !== "external"
|
|
1900
|
+
) {
|
|
1901
|
+
runtimeSharedState.shopifyCartSyncGeneration += 1;
|
|
1902
|
+
invalidateShopifyCartAttributeSync();
|
|
1903
|
+
}
|
|
1904
|
+
if (state.config?.shopId && state.config.shopId !== resolvedConfig.shopId) {
|
|
1905
|
+
state.shopifyCartToken = null;
|
|
1906
|
+
state.shopwareAttributionEnvelope = null;
|
|
1907
|
+
}
|
|
1908
|
+
// A network refresh may remove the cached price plane while its display or
|
|
1909
|
+
// cart authorization is still live. The empty-project/lazy path deliberately
|
|
1910
|
+
// skips applyCommercePrice, so teardown must happen before replacing config.
|
|
1911
|
+
const priorPriceSyncPending = hasActivePriceAuthorizationPlane()
|
|
1912
|
+
&& state.lastShopifyCartSync?.status === "pending";
|
|
1913
|
+
if (
|
|
1914
|
+
!resolvedConfig.commerce?.price &&
|
|
1915
|
+
(hasCommercePriceState() || priorPriceSyncPending)
|
|
1916
|
+
) {
|
|
1917
|
+
runtimeSharedState.shopifyCartSyncGeneration += 1;
|
|
1918
|
+
invalidateShopifyCartAttributeSync();
|
|
1919
|
+
cleanupCommercePrice();
|
|
1920
|
+
}
|
|
1921
|
+
state.config = resolvedConfig;
|
|
1922
|
+
reconcileForcedUnavailable(state.config);
|
|
1923
|
+
if (state.userId && state.userId !== resolvedUserId) {
|
|
1924
|
+
// The envelope is signed for the previous visitor; a rotated identity must
|
|
1925
|
+
// never sync a stale envelope before the next assign response replaces it.
|
|
1926
|
+
state.shopwareAttributionEnvelope = null;
|
|
1927
|
+
}
|
|
1928
|
+
state.userId = resolvedUserId;
|
|
1929
|
+
state.trackingStart = trackStart;
|
|
1930
|
+
state.trackingIdleTimeoutMs = idleTimeout;
|
|
1931
|
+
state.consentRequired = initialConsentRequired;
|
|
1932
|
+
state.consentGranted = initialConsentGranted;
|
|
1933
|
+
state.consentModeEnabled = config.consentMode?.enabled === true;
|
|
1934
|
+
state.trackingDisabledByDnt = initialTrackingDisabledByDnt;
|
|
1935
|
+
state.variationPrerequisiteMet = emptyProjectFastPath || variationsAllowed(resolvedConfig);
|
|
1936
|
+
state.trackingPrerequisiteMet = emptyProjectFastPath || trackingAllowed(resolvedConfig);
|
|
1937
|
+
state.analyticsStarted = false;
|
|
1938
|
+
state.exposedAssignmentKeys.clear();
|
|
1939
|
+
// Cache-to-network init is a delivery refresh, not proof that an equivalent
|
|
1940
|
+
// assignment became healthy. Preserve its fail-closed state until the new
|
|
1941
|
+
// assignment set is known, then prune keys for assignments no longer held.
|
|
1942
|
+
state.authorizedPriceAssignmentKeys = null;
|
|
1943
|
+
state.authorizedShippingAssignmentKeys = null;
|
|
1944
|
+
state.authorizedOffersAssignmentKeys = null;
|
|
1945
|
+
state.redirectNavigationPending = false;
|
|
1946
|
+
if (headlessCartAuthorizationState) {
|
|
1947
|
+
headlessCartAuthorizationState.generation += 1;
|
|
1948
|
+
headlessCartAuthorizationState.confirmationPending = false;
|
|
1949
|
+
headlessCartAuthorizationState.pending = null;
|
|
1950
|
+
}
|
|
1951
|
+
state.platform = platform;
|
|
1952
|
+
state.routerMode = routerMode;
|
|
1953
|
+
state.activationMode = activationMode;
|
|
1954
|
+
state.perfFlags = perfFlags;
|
|
1955
|
+
state.urlDiffNeeds = urlDiffNeeds;
|
|
1956
|
+
state.configSource = configSource;
|
|
1957
|
+
state.runtimeConfig = runtimeConfig;
|
|
1958
|
+
state.pageTriggerMode = pageTriggerMode;
|
|
1959
|
+
pendingDataLayerEvents.length = 0;
|
|
1960
|
+
state.initializingAssignments = true;
|
|
1961
|
+
const url = config.url ?? location.href;
|
|
1962
|
+
__DRIP_FEATURE_DEVTOOLS__ && traceStep("Initiated page", {
|
|
1963
|
+
url,
|
|
1964
|
+
experiments: experiments.length,
|
|
1965
|
+
signals: resolvedConfig.signals?.length ?? 0,
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
// QA session persistence
|
|
1969
|
+
if (state.qaMode) {
|
|
1970
|
+
state.qaSessionId = getQaSessionId(generateQaSessionId, { storageAllowed: persistentStorageAllowed() });
|
|
1971
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("qa", "QA mode active", { forceMap: state.forceMap, sessionId: state.qaSessionId });
|
|
1972
|
+
__DRIP_FEATURE_DEVTOOLS__ && dtLog("qa", "QA mode active", { sessionId: state.qaSessionId });
|
|
1973
|
+
if (state.forceMap) {
|
|
1974
|
+
// This functional first-party merchant-QA cookie is written only after an explicit
|
|
1975
|
+
// QA URL open and deliberately bypasses the consent gate.
|
|
1976
|
+
persistUrlQaForce(state.forceMap);
|
|
1977
|
+
}
|
|
1978
|
+
} else {
|
|
1979
|
+
state.qaSessionId = "";
|
|
1980
|
+
}
|
|
1981
|
+
if (state.screenshotMode) {
|
|
1982
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("qa", "Screenshot mode active");
|
|
1983
|
+
__DRIP_FEATURE_DEVTOOLS__ && dtLog("qa", "Screenshot mode active");
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// Cleanup previous init
|
|
1987
|
+
if (state.stopObserver) {
|
|
1988
|
+
state.stopObserver();
|
|
1989
|
+
state.stopObserver = null;
|
|
1990
|
+
}
|
|
1991
|
+
if (state.deferredRouteReevaluationTimer !== null) {
|
|
1992
|
+
window.clearTimeout(state.deferredRouteReevaluationTimer);
|
|
1993
|
+
state.deferredRouteReevaluationTimer = null;
|
|
1994
|
+
}
|
|
1995
|
+
if (state.stopDomObserver) {
|
|
1996
|
+
state.stopDomObserver();
|
|
1997
|
+
state.stopDomObserver = null;
|
|
1998
|
+
}
|
|
1999
|
+
cancelAllPageRulePolls();
|
|
2000
|
+
runtimeSharedState.pageDetectorLastUrls.clear();
|
|
2001
|
+
runtimeSharedState.pageDetectorRules = detectorRules;
|
|
2002
|
+
if (state.stopHandover) {
|
|
2003
|
+
state.stopHandover();
|
|
2004
|
+
state.stopHandover = null;
|
|
2005
|
+
}
|
|
2006
|
+
if (state.stopSignalCaptures) {
|
|
2007
|
+
state.stopSignalCaptures();
|
|
2008
|
+
state.stopSignalCaptures = null;
|
|
2009
|
+
}
|
|
2010
|
+
if (state.stopSurfaceSignals) {
|
|
2011
|
+
state.stopSurfaceSignals();
|
|
2012
|
+
state.stopSurfaceSignals = null;
|
|
2013
|
+
}
|
|
2014
|
+
if (state.stopJsTriggerSignals) {
|
|
2015
|
+
state.stopJsTriggerSignals();
|
|
2016
|
+
state.stopJsTriggerSignals = null;
|
|
2017
|
+
}
|
|
2018
|
+
if (state.stopDataLayerTriggers) {
|
|
2019
|
+
state.stopDataLayerTriggers();
|
|
2020
|
+
state.stopDataLayerTriggers = null;
|
|
2021
|
+
}
|
|
2022
|
+
if (state.stopConsentListeners) {
|
|
2023
|
+
state.stopConsentListeners();
|
|
2024
|
+
state.stopConsentListeners = null;
|
|
2025
|
+
}
|
|
2026
|
+
// Keep an in-flight consent-pending write attached across cache-to-network re-init.
|
|
2027
|
+
// Its completion schedules the current consented stamp after the older write.
|
|
2028
|
+
state.stopShopifyCartMutationSync?.();
|
|
2029
|
+
state.stopShopifyCartMutationSync = null;
|
|
2030
|
+
clearScheduledAnalyticsStart();
|
|
2031
|
+
unwireGoals();
|
|
2032
|
+
cleanupHeatmapTracking();
|
|
2033
|
+
cleanupReplayTracking();
|
|
2034
|
+
cleanupSemanticTracking();
|
|
2035
|
+
if (state.tracker) {
|
|
2036
|
+
state.tracker.stop();
|
|
2037
|
+
state.tracker = null;
|
|
2038
|
+
}
|
|
2039
|
+
state.pendingDeliveryHealth = [];
|
|
2040
|
+
// The authorized empty-project profile has no visual treatment to measure;
|
|
2041
|
+
// reset any prior route measurement without constructing an observer.
|
|
2042
|
+
startCwvMeasurement(emptyProjectFastPath);
|
|
2043
|
+
|
|
2044
|
+
state.stopConsentListeners = installConsentChangeListeners(resolvedConfig);
|
|
2045
|
+
if (!emptyProjectFastPath) {
|
|
2046
|
+
captureUrlSignals(resolvedConfig);
|
|
2047
|
+
captureDomAndGlobalSignals(resolvedConfig);
|
|
2048
|
+
state.stopSignalCaptures = installClickSignalCaptures(resolvedConfig);
|
|
2049
|
+
state.stopSurfaceSignals = experimentsUseSurfaceSignals(experiments)
|
|
2050
|
+
? installSurfaceSignals(() => {
|
|
2051
|
+
captureDomAndGlobalSignals(resolvedConfig);
|
|
2052
|
+
reevaluateAssignments("dom");
|
|
2053
|
+
})
|
|
2054
|
+
: null;
|
|
2055
|
+
state.stopJsTriggerSignals = installJsTriggerObserver(
|
|
2056
|
+
experiments,
|
|
2057
|
+
() => {
|
|
2058
|
+
reevaluateAssignments("dom");
|
|
2059
|
+
},
|
|
2060
|
+
manualInitialTrigger,
|
|
2061
|
+
);
|
|
2062
|
+
state.stopDataLayerTriggers = installDataLayerTriggers(resolvedConfig);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// Evaluate and apply. For generated snippets, use the compact critical path
|
|
2066
|
+
// plan first, then fall back to the general evaluator for complex/noncritical
|
|
2067
|
+
// experiments that were not safely precompiled.
|
|
2068
|
+
if (emptyProjectFastPath) {
|
|
2069
|
+
state.activationDiagnostics = { project: { gates: [] }, experiments: [] };
|
|
2070
|
+
} else {
|
|
2071
|
+
updateProjectActivationDiagnostics(resolvedConfig);
|
|
2072
|
+
for (const gate of state.activationDiagnostics.project.gates) {
|
|
2073
|
+
__DRIP_FEATURE_DEVTOOLS__ && traceStep(`Project ${gate.label} is ${gate.status}`, {
|
|
2074
|
+
reason: gate.reason,
|
|
2075
|
+
...(gate.detail && typeof gate.detail === "object" ? { detail: gate.detail } : {}),
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
addInitPhase(perfContext, "runtime_setup_ms", runtimeSetupStartedAt);
|
|
2080
|
+
const targetingAssignmentStartedAt = monotonicNow();
|
|
2081
|
+
state.carryImportedAssignmentKeys.clear();
|
|
2082
|
+
if (redirectCarry && persistentStorageAllowed() && !state.qaMode && !state.screenshotMode) {
|
|
2083
|
+
importRedirectAssignmentCarry(redirectCarry, location.href, !edgeRuntimeActive());
|
|
2084
|
+
state.carryImportedAssignmentKeys.add(
|
|
2085
|
+
redirectCarryAssignmentKey(
|
|
2086
|
+
redirectCarry.experiment.id,
|
|
2087
|
+
redirectCarry.variationId,
|
|
2088
|
+
Math.max(
|
|
2089
|
+
1,
|
|
2090
|
+
Number.isSafeInteger(redirectCarry.experiment.assignmentEpoch)
|
|
2091
|
+
? redirectCarry.experiment.assignmentEpoch!
|
|
2092
|
+
: 1,
|
|
2093
|
+
),
|
|
2094
|
+
),
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
const persistedAssignments = emptyProjectFastPath
|
|
2098
|
+
? undefined
|
|
2099
|
+
: loadPersistedAssignments(experiments);
|
|
2100
|
+
let initialTargetingPass =
|
|
2101
|
+
!emptyProjectFastPath && !manualInitialTrigger && state.variationPrerequisiteMet
|
|
2102
|
+
? createTargetingEvaluationPass(
|
|
2103
|
+
resolvedConfig.attributes ?? EMPTY_TARGETING_ATTRIBUTES,
|
|
2104
|
+
url,
|
|
2105
|
+
persistentStorageAllowed(),
|
|
2106
|
+
)
|
|
2107
|
+
: undefined;
|
|
2108
|
+
const pageActivation = reconcilePageActivation(experiments, url, {
|
|
2109
|
+
previous: state.pageActivation,
|
|
2110
|
+
persistedAssignments,
|
|
2111
|
+
targetingPass: initialTargetingPass,
|
|
2112
|
+
});
|
|
2113
|
+
const teardownPages = pageActivation.transitions
|
|
2114
|
+
.filter((transition) => transition.teardown)
|
|
2115
|
+
.map((transition) => ({ pageId: transition.pageId, mode: transition.mode }));
|
|
2116
|
+
if (teardownPages.length > 0) {
|
|
2117
|
+
teardownTransaction(teardownPages, { goalUrl: url });
|
|
2118
|
+
}
|
|
2119
|
+
state.pageActivation = pageActivation.next;
|
|
2120
|
+
pageActivation.pollRules.forEach((rule) => startPageRulePoll(rule));
|
|
2121
|
+
if (emptyProjectFastPath) {
|
|
2122
|
+
state.assignments = [];
|
|
2123
|
+
} else if (!manualInitialTrigger && state.variationPrerequisiteMet) {
|
|
2124
|
+
const targetingSourceAttributes = resolvedConfig.attributes ??
|
|
2125
|
+
EMPTY_TARGETING_ATTRIBUTES;
|
|
2126
|
+
const targetingPass = initialTargetingPass ?? createTargetingEvaluationPass(
|
|
2127
|
+
targetingSourceAttributes,
|
|
2128
|
+
url,
|
|
2129
|
+
persistentStorageAllowed(),
|
|
2130
|
+
);
|
|
2131
|
+
initialTargetingPass = targetingPass;
|
|
2132
|
+
const attributes = targetingAttributesForPass(
|
|
2133
|
+
targetingPass,
|
|
2134
|
+
targetingSourceAttributes,
|
|
2135
|
+
url,
|
|
2136
|
+
persistentStorageAllowed(),
|
|
2137
|
+
);
|
|
2138
|
+
const usablePredecision = predecided && predecided.visitorId === state.userId
|
|
2139
|
+
? predecided
|
|
2140
|
+
: null;
|
|
2141
|
+
const edgeEligibility = config.userId == null && edgeRuntimeActive()
|
|
2142
|
+
? evaluateWithDiagnostics(experiments, {
|
|
2143
|
+
url,
|
|
2144
|
+
attributes: targetingSourceAttributes,
|
|
2145
|
+
userId: state.userId,
|
|
2146
|
+
persistSignals: false,
|
|
2147
|
+
storeOrigin: configuredStoreOrigins(resolvedConfig),
|
|
2148
|
+
persistedAssignments,
|
|
2149
|
+
targetingPass,
|
|
2150
|
+
})
|
|
2151
|
+
: null;
|
|
2152
|
+
const edgeEvaluation = edgeEligibility
|
|
2153
|
+
? evaluateEdgeRuntimeAssignments(
|
|
2154
|
+
experiments,
|
|
2155
|
+
{
|
|
2156
|
+
url,
|
|
2157
|
+
attributes,
|
|
2158
|
+
userId: state.userId,
|
|
2159
|
+
},
|
|
2160
|
+
edgeEligibility.assignments,
|
|
2161
|
+
edgeEligibility.diagnostics,
|
|
2162
|
+
)
|
|
2163
|
+
: null;
|
|
2164
|
+
// Targeting runs before the real SDK exclusion resolver. The bootstrap
|
|
2165
|
+
// deliberately hands over unfiltered per-experiment decisions.
|
|
2166
|
+
if (state.holdout) {
|
|
2167
|
+
runtimeSharedState.initialPredecidedByExperiment = null;
|
|
2168
|
+
state.assignments = [];
|
|
2169
|
+
state.activationDiagnostics.experiments = [];
|
|
2170
|
+
} else if (edgeEvaluation || usablePredecision) {
|
|
2171
|
+
const fallbackExperiments = edgeEvaluation
|
|
2172
|
+
? edgeEvaluation.experimentIds.size > 0
|
|
2173
|
+
? experiments.filter((experiment) => !edgeEvaluation.experimentIds.has(experiment.id))
|
|
2174
|
+
: []
|
|
2175
|
+
: experiments;
|
|
2176
|
+
if (usablePredecision) {
|
|
2177
|
+
runtimeSharedState.initialPredecidedByExperiment = new Map(usablePredecision.decision);
|
|
2178
|
+
}
|
|
2179
|
+
try {
|
|
2180
|
+
const fallback = evaluateWithDiagnostics(fallbackExperiments, {
|
|
2181
|
+
url,
|
|
2182
|
+
attributes: targetingSourceAttributes,
|
|
2183
|
+
userId: state.userId,
|
|
2184
|
+
persistSignals: false,
|
|
2185
|
+
targetingPass,
|
|
2186
|
+
storeOrigin: configuredStoreOrigins(resolvedConfig),
|
|
2187
|
+
persistedAssignments,
|
|
2188
|
+
exclusionGroups: config.exclusionGroups,
|
|
2189
|
+
storedGroupSelections: usablePredecision?.groups,
|
|
2190
|
+
holdout: usablePredecision
|
|
2191
|
+
? { enabled: config.holdout_v1_enabled, percentage: config.holdout_pct }
|
|
2192
|
+
: undefined,
|
|
2193
|
+
});
|
|
2194
|
+
if (edgeEvaluation) {
|
|
2195
|
+
const combined = resolveCombinedEdgeEvaluation(
|
|
2196
|
+
edgeEvaluation.assignments,
|
|
2197
|
+
fallback.assignments,
|
|
2198
|
+
[...edgeEvaluation.diagnostics, ...fallback.diagnostics],
|
|
2199
|
+
{
|
|
2200
|
+
userId: state.userId,
|
|
2201
|
+
exclusionGroups: config.exclusionGroups,
|
|
2202
|
+
},
|
|
2203
|
+
);
|
|
2204
|
+
state.assignments = combined.assignments;
|
|
2205
|
+
state.activationDiagnostics.experiments = combined.diagnostics;
|
|
2206
|
+
} else {
|
|
2207
|
+
state.assignments = fallback.assignments;
|
|
2208
|
+
state.activationDiagnostics.experiments = fallback.diagnostics;
|
|
2209
|
+
}
|
|
2210
|
+
} finally {
|
|
2211
|
+
runtimeSharedState.initialPredecidedByExperiment = null;
|
|
2212
|
+
}
|
|
2213
|
+
} else if (config.exclusionGroups?.length) {
|
|
2214
|
+
const evaluation = evaluateWithDiagnostics(experiments, {
|
|
2215
|
+
url,
|
|
2216
|
+
attributes: targetingSourceAttributes,
|
|
2217
|
+
userId: state.userId,
|
|
2218
|
+
persistSignals: false,
|
|
2219
|
+
storeOrigin: configuredStoreOrigins(resolvedConfig),
|
|
2220
|
+
exclusionGroups: config.exclusionGroups,
|
|
2221
|
+
persistedAssignments,
|
|
2222
|
+
targetingPass,
|
|
2223
|
+
});
|
|
2224
|
+
state.assignments = evaluation.assignments;
|
|
2225
|
+
state.activationDiagnostics.experiments = evaluation.diagnostics;
|
|
2226
|
+
} else {
|
|
2227
|
+
const critical = buildCriticalPathAssignments(
|
|
2228
|
+
experiments,
|
|
2229
|
+
config.delivery?.criticalMutationPlan,
|
|
2230
|
+
{ url, attributes, userId: state.userId },
|
|
2231
|
+
);
|
|
2232
|
+
const fallbackExperiments = critical.plannedExperimentIds.size
|
|
2233
|
+
? experiments.filter(
|
|
2234
|
+
(experiment) =>
|
|
2235
|
+
!critical.plannedExperimentIds.has(experiment.id) ||
|
|
2236
|
+
!critical.assignments.some(
|
|
2237
|
+
(assignment) => assignment.experimentId === experiment.id,
|
|
2238
|
+
),
|
|
2239
|
+
)
|
|
2240
|
+
: experiments;
|
|
2241
|
+
const fallback = evaluateWithDiagnostics(fallbackExperiments, {
|
|
2242
|
+
url,
|
|
2243
|
+
attributes: targetingSourceAttributes,
|
|
2244
|
+
userId: state.userId,
|
|
2245
|
+
persistSignals: false,
|
|
2246
|
+
storeOrigin: configuredStoreOrigins(resolvedConfig),
|
|
2247
|
+
persistedAssignments,
|
|
2248
|
+
targetingPass,
|
|
2249
|
+
});
|
|
2250
|
+
state.assignments = [
|
|
2251
|
+
...critical.assignments,
|
|
2252
|
+
...fallback.assignments,
|
|
2253
|
+
];
|
|
2254
|
+
const fallbackDiagnosticIds = new Set(
|
|
2255
|
+
fallback.diagnostics.map((diagnostic) => diagnostic.experimentId),
|
|
2256
|
+
);
|
|
2257
|
+
state.activationDiagnostics.experiments = [
|
|
2258
|
+
...critical.skippedDiagnostics.filter(
|
|
2259
|
+
(diagnostic) => !fallbackDiagnosticIds.has(diagnostic.experimentId),
|
|
2260
|
+
),
|
|
2261
|
+
...fallback.diagnostics,
|
|
2262
|
+
];
|
|
2263
|
+
for (const assignment of critical.assignments) {
|
|
2264
|
+
const experiment = experiments.find((candidate) => candidate.id === assignment.experimentId);
|
|
2265
|
+
if (!experiment) continue;
|
|
2266
|
+
state.activationDiagnostics.experiments.unshift(experimentDiagnostic(
|
|
2267
|
+
experiment,
|
|
2268
|
+
[
|
|
2269
|
+
{ id: "page", label: "Page check", status: "passed", reason: "precompiled_delivery_match" },
|
|
2270
|
+
// Critical-plan invariant: only campaigns without non-URL
|
|
2271
|
+
// targeting are precompiled — the truthful audience result is
|
|
2272
|
+
// "all visitors", same as the skip-path diagnostics.
|
|
2273
|
+
{ id: "audience", label: "Audience check", status: "skipped", reason: "no_audience_rules" },
|
|
2274
|
+
{ id: "traffic", label: "Traffic allocation", status: "passed", reason: "included" },
|
|
2275
|
+
{ id: "assignment", label: "Variation assignment", status: "passed", reason: "precompiled_assignment" },
|
|
2276
|
+
],
|
|
2277
|
+
assignment,
|
|
2278
|
+
"precompiled_assignment",
|
|
2279
|
+
));
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
} else {
|
|
2283
|
+
state.assignments = [];
|
|
2284
|
+
state.activationDiagnostics.experiments = manualInitialTrigger
|
|
2285
|
+
? experiments.map((experiment) => experimentDiagnostic(
|
|
2286
|
+
experiment,
|
|
2287
|
+
[{ id: "activation", label: "Page trigger", status: "skipped", reason: "manual_initial_trigger" }],
|
|
2288
|
+
null,
|
|
2289
|
+
"manual_initial_trigger",
|
|
2290
|
+
))
|
|
2291
|
+
: [];
|
|
2292
|
+
}
|
|
2293
|
+
if (state.assignments.length > 1) {
|
|
2294
|
+
const filteredInitial = enforcePersonalizationStopAfterMatch(
|
|
2295
|
+
state.assignments,
|
|
2296
|
+
state.activationDiagnostics.experiments,
|
|
2297
|
+
);
|
|
2298
|
+
state.assignments = filteredInitial.assignments;
|
|
2299
|
+
state.activationDiagnostics.experiments = filteredInitial.diagnostics ?? state.activationDiagnostics.experiments;
|
|
2300
|
+
}
|
|
2301
|
+
state.assignments = filterUnsafeRedirectAssignments(
|
|
2302
|
+
experiments,
|
|
2303
|
+
state.assignments,
|
|
2304
|
+
location.href,
|
|
2305
|
+
state.userId ?? "",
|
|
2306
|
+
resolvedConfig.externalTracking,
|
|
2307
|
+
shouldPreserveOffTargetRedirectAssignment,
|
|
2308
|
+
configuredStoreOrigins(resolvedConfig),
|
|
2309
|
+
);
|
|
2310
|
+
const currentExposureKeys = new Set(state.assignments.map(exposureAssignmentKey));
|
|
2311
|
+
for (const key of state.mutationPendingAssignmentKeys) {
|
|
2312
|
+
if (!currentExposureKeys.has(key)) state.mutationPendingAssignmentKeys.delete(key);
|
|
2313
|
+
}
|
|
2314
|
+
for (const key of state.contaminatedAssignmentKeys) {
|
|
2315
|
+
if (!currentExposureKeys.has(key)) state.contaminatedAssignmentKeys.delete(key);
|
|
2316
|
+
}
|
|
2317
|
+
syncAssignmentPageActive(state.assignments, experiments);
|
|
2318
|
+
recordAttributableAssignments(state.assignments);
|
|
2319
|
+
if (redirectCarry?.exposureSent) {
|
|
2320
|
+
const importedAssignment = state.assignments.find((assignment) =>
|
|
2321
|
+
assignment.experimentId === redirectCarry.experiment.id && assignment.variationId === redirectCarry.variationId);
|
|
2322
|
+
if (importedAssignment) state.exposedAssignmentKeys.add(exposureAssignmentKey(importedAssignment));
|
|
2323
|
+
}
|
|
2324
|
+
if (!emptyProjectFastPath && afMode !== "off") {
|
|
2325
|
+
const scopedSelectors = afMode === "scoped"
|
|
2326
|
+
? collectScopedSelectors(experiments, resolvedConfig, state.assignments)
|
|
2327
|
+
: undefined;
|
|
2328
|
+
applyRuntimeAntiFlicker(afMode, afTimeout, scopedSelectors, runtimeConfig.antiFlickerCustomCss);
|
|
2329
|
+
}
|
|
2330
|
+
// Assignment persistence belongs to bucketing, not tracking. In particular,
|
|
2331
|
+
// a redirect must carry a durable assignment even when the source page's
|
|
2332
|
+
// tracking prerequisite keeps analytics and exposure collection closed.
|
|
2333
|
+
if (!emptyProjectFastPath) {
|
|
2334
|
+
persistActiveExperimentAssignments(experiments);
|
|
2335
|
+
}
|
|
2336
|
+
if (
|
|
2337
|
+
configSource !== "cache" &&
|
|
2338
|
+
navigateForRedirectAssignment(
|
|
2339
|
+
resolvedConfig,
|
|
2340
|
+
experiments,
|
|
2341
|
+
state.assignments,
|
|
2342
|
+
)
|
|
2343
|
+
) {
|
|
2344
|
+
addInitPhase(perfContext, "targeting_assignment_ms", targetingAssignmentStartedAt);
|
|
2345
|
+
finishInitializingAssignments();
|
|
2346
|
+
return state.assignments;
|
|
2347
|
+
}
|
|
2348
|
+
addInitPhase(perfContext, "targeting_assignment_ms", targetingAssignmentStartedAt);
|
|
2349
|
+
const mutationFinalizeStartedAt = monotonicNow();
|
|
2350
|
+
const applyOrReconcileInitialArtifacts = (goalUrl: string): void => {
|
|
2351
|
+
if (appliedAssignmentsByKey.size === 0) {
|
|
2352
|
+
applyAssignments(state.assignments, state.activationMode, { goalUrl });
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
reconcileArtifacts(desiredArtifactSet(state.assignments), goalUrl);
|
|
2356
|
+
reassertAssignmentMutations(state.assignments);
|
|
2357
|
+
};
|
|
2358
|
+
let initialApplyPromise: Promise<void> | null = null;
|
|
2359
|
+
if (!emptyProjectFastPath) {
|
|
2360
|
+
const earlyReassertEligible = shouldUseEarlyReassert({
|
|
2361
|
+
applyTiming: resolvedConfig.spa?.applyTiming,
|
|
2362
|
+
configSource,
|
|
2363
|
+
hasTrustedPredecision: predecided !== null && resolvedUserId === predecided.visitorId,
|
|
2364
|
+
consentGranted: initialConsentGranted,
|
|
2365
|
+
dntBlocked: initialTrackingDisabledByDnt,
|
|
2366
|
+
});
|
|
2367
|
+
const earlyReassertAssignments = artifactRenderableAssignments(state.assignments);
|
|
2368
|
+
// Structural operations cannot be safely reasserted without duplicating
|
|
2369
|
+
// inserts or losing their original snapshots. Custom JS is never
|
|
2370
|
+
// reassert-safe because replaying it can repeat arbitrary side effects.
|
|
2371
|
+
// Route the whole assignment set through the hydration-delayed path.
|
|
2372
|
+
const deferStructuralEarlyReassert = earlyReassertEligible && (
|
|
2373
|
+
assignmentsHaveStructuralMutations(earlyReassertAssignments)
|
|
2374
|
+
|| assignmentsHaveCustomJs(earlyReassertAssignments)
|
|
2375
|
+
);
|
|
2376
|
+
const forcedPreviewSession = (state.qaMode || state.screenshotMode) && state.forceMap !== null;
|
|
2377
|
+
const waitForHydration = resolvedConfig.spa?.applyTiming === "after_hydration"
|
|
2378
|
+
|| deferStructuralEarlyReassert
|
|
2379
|
+
|| (forcedPreviewSession && resolvedConfig.spa?.applyTiming === "early_reassert");
|
|
2380
|
+
const earlyReassert = earlyReassertEligible && !deferStructuralEarlyReassert;
|
|
2381
|
+
const deferForVisualWork = assignmentsHaveVisualWork(
|
|
2382
|
+
earlyReassertAssignments,
|
|
2383
|
+
resolvedConfig.commerce?.price,
|
|
2384
|
+
);
|
|
2385
|
+
if ((waitForHydration || hasMutationHold()) && deferForVisualWork) {
|
|
2386
|
+
const scheduledConfig = resolvedConfig;
|
|
2387
|
+
const scheduledUrl = location.href;
|
|
2388
|
+
initialApplyPromise = awaitHydration(
|
|
2389
|
+
resolvedConfig.spa?.hydrationTimeoutMs ?? 3_000,
|
|
2390
|
+
{ waitForHydration },
|
|
2391
|
+
).then(() => {
|
|
2392
|
+
// A re-init (e.g. cached config superseded by the network config)
|
|
2393
|
+
// replaces the pending gate; a stale promise must not clear the
|
|
2394
|
+
// newer one and open the hydration gate early for rechecks.
|
|
2395
|
+
if (state.pendingInitialApply === initialApplyPromise) {
|
|
2396
|
+
state.pendingInitialApply = null;
|
|
2397
|
+
}
|
|
2398
|
+
if (state.config !== scheduledConfig) return;
|
|
2399
|
+
if (location.href !== scheduledUrl) {
|
|
2400
|
+
// The shopper navigated before the gate opened: applying the
|
|
2401
|
+
// original route's assignments would flash the previous page's
|
|
2402
|
+
// treatment. Re-evaluate for the current URL directly (forcing
|
|
2403
|
+
// apply + reveal even when nothing was queued or the signature is
|
|
2404
|
+
// unchanged), then run startup JS to preserve the ordering.
|
|
2405
|
+
reevaluateAssignments("url", true, true);
|
|
2406
|
+
runProjectStartupCode(scheduledConfig);
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
applyOrReconcileInitialArtifacts(scheduledUrl);
|
|
2410
|
+
// Preserve the immediate-mode ordering contract: project startup JS
|
|
2411
|
+
// runs after assignment mutations, so it can read the variant DOM.
|
|
2412
|
+
runProjectStartupCode(scheduledConfig);
|
|
2413
|
+
});
|
|
2414
|
+
state.pendingInitialApply = initialApplyPromise;
|
|
2415
|
+
} else {
|
|
2416
|
+
// Immediate apply supersedes any gate left by a previous init.
|
|
2417
|
+
state.pendingInitialApply = null;
|
|
2418
|
+
if (earlyReassert && deferForVisualWork) {
|
|
2419
|
+
const diagnostics = readDripPerf();
|
|
2420
|
+
diagnostics.early_apply_at = Date.now();
|
|
2421
|
+
diagnostics.reasserts_count = 0;
|
|
2422
|
+
diagnostics.revert_loops_detected = 0;
|
|
2423
|
+
// The watcher never captures an assignment array: it resolves the live
|
|
2424
|
+
// render plane on every fire, so a page teardown — which clears
|
|
2425
|
+
// `pageActive` and drops the artifacts — silences it by construction.
|
|
2426
|
+
const stop = installEarlyReassert({
|
|
2427
|
+
diagnostics,
|
|
2428
|
+
reapply: reassertAssignmentMutations,
|
|
2429
|
+
resolveAssignments: () =>
|
|
2430
|
+
activeRenderableAssignments().filter(isOffersDisplayAssignmentAuthorized),
|
|
2431
|
+
timeoutMs: Math.max(3_000, resolvedConfig.spa?.hydrationTimeoutMs ?? 3_000),
|
|
2432
|
+
});
|
|
2433
|
+
runtimeSharedState.stopEarlyReassert = stop;
|
|
2434
|
+
void awaitHydration(resolvedConfig.spa?.hydrationTimeoutMs ?? 3_000, {
|
|
2435
|
+
waitForHydration: true,
|
|
2436
|
+
// The variant was already applied synchronously. This second gate
|
|
2437
|
+
// only controls watcher teardown and is not an apply delay.
|
|
2438
|
+
recordMetrics: false,
|
|
2439
|
+
})
|
|
2440
|
+
.catch(() => debugWarn("hydration", "wait failed"))
|
|
2441
|
+
.finally(() => {
|
|
2442
|
+
if (runtimeSharedState.stopEarlyReassert !== stop) return;
|
|
2443
|
+
runtimeSharedState.earlyReassertTeardownTimer = setTimeout(stop, 250);
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
applyOrReconcileInitialArtifacts(url);
|
|
2447
|
+
runProjectStartupCode(resolvedConfig);
|
|
2448
|
+
}
|
|
2449
|
+
if (persistentStorageAllowed() && config.bucketingCleanup?.enabled !== false) {
|
|
2450
|
+
cleanupBucketingStorage(experiments, {
|
|
2451
|
+
shopId: config.shopId,
|
|
2452
|
+
retentionDays: config.bucketingCleanup?.retentionDays,
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
state.stopHandover = installBucketingHandover(config.externalTracking, () => (
|
|
2456
|
+
persistentStorageAllowed() && state.userId && state.assignments.length
|
|
2457
|
+
? createBucketingHandoverPayload(state.userId, state.assignments, config.externalTracking)
|
|
2458
|
+
: null
|
|
2459
|
+
), () => (
|
|
2460
|
+
persistentStorageAllowed() && state.userId && state.assignments.length
|
|
2461
|
+
? mintBucketingHandoverPayload(config, state.userId, state.assignments)
|
|
2462
|
+
: Promise.resolve(null)
|
|
2463
|
+
));
|
|
2464
|
+
} else {
|
|
2465
|
+
// A cached visual config can leave a hydration promise pending when the
|
|
2466
|
+
// authoritative network response resolves to the empty fast path. The
|
|
2467
|
+
// stale promise is config-guarded, so detach it immediately instead of
|
|
2468
|
+
// delaying install/pageview analytics until its timeout.
|
|
2469
|
+
state.pendingInitialApply = null;
|
|
2470
|
+
if (appliedAssignmentsByKey.size > 0) reconcileArtifacts(new Map(), url);
|
|
2471
|
+
}
|
|
2472
|
+
state.lastAssignmentSignature = assignmentSignature(state.assignments);
|
|
2473
|
+
installSdkCartMintTrigger();
|
|
2474
|
+
state.lastUrl = location.href;
|
|
2475
|
+
addInitPhase(perfContext, "mutation_finalize_ms", mutationFinalizeStartedAt);
|
|
2476
|
+
const assignmentReadyMonotonicAt = monotonicNow();
|
|
2477
|
+
const assignmentReadyMs = boundedPerfDuration(assignmentReadyMonotonicAt - assignmentStartedAt);
|
|
2478
|
+
const initTotalMs = boundedPerfDuration(assignmentReadyMonotonicAt - perfContext.startedAt);
|
|
2479
|
+
const assignmentReadyAt = Date.now();
|
|
2480
|
+
updateLoadDiagnostics({
|
|
2481
|
+
stage: "assignments_ready",
|
|
2482
|
+
assignmentReadyAt,
|
|
2483
|
+
assignmentReadyMs,
|
|
2484
|
+
});
|
|
2485
|
+
queuePerfEvent(buildPerfEvent(resolvedConfig, {
|
|
2486
|
+
eventKind: "init",
|
|
2487
|
+
assignmentReadyMs,
|
|
2488
|
+
initTotalMs,
|
|
2489
|
+
phases: { ...perfContext.phases },
|
|
2490
|
+
includesConfigFetch: perfContext.includesConfigFetch,
|
|
2491
|
+
predecisionMismatches: perfContext.predecisionMismatches,
|
|
2492
|
+
blankScreenMs: getBlankScreenMs(),
|
|
2493
|
+
assignmentReadyAt,
|
|
2494
|
+
}));
|
|
2495
|
+
const storefrontPerf = readDripPerf();
|
|
2496
|
+
const scriptExecStart = numberPerf(storefrontPerf, "script_exec_start");
|
|
2497
|
+
runtimeSharedState.storefrontVitalsContext = {
|
|
2498
|
+
endpoint: resolvedConfig.endpoint ? normalizeEndpoint(resolvedConfig.endpoint) : "",
|
|
2499
|
+
proxyEventDelivery: resolvedConfig.proxyEventDelivery,
|
|
2500
|
+
shopId: resolvedConfig.shopId ?? "",
|
|
2501
|
+
ingestShopId: resolvedConfig.ingestShopId,
|
|
2502
|
+
ingestSignature: resolvedConfig.ingestSignature,
|
|
2503
|
+
visitorId: state.userId ?? undefined,
|
|
2504
|
+
sampleRate: resolvedConfig.storefrontVitalsSampleRate,
|
|
2505
|
+
experimentActive: state.assignments.some(exposable),
|
|
2506
|
+
sdkBootToInitMs: scriptExecStart === undefined
|
|
2507
|
+
? undefined
|
|
2508
|
+
: Math.max(0, assignmentReadyAt - initTotalMs - scriptExecStart),
|
|
2509
|
+
getAntiFlickerInsertedToRevealMs: () => {
|
|
2510
|
+
const livePerf = readDripPerf();
|
|
2511
|
+
const insertedAt = numberPerf(livePerf, "anti_flicker_inserted_at");
|
|
2512
|
+
if (insertedAt === undefined) return undefined;
|
|
2513
|
+
const liveRevealAt = numberPerf(livePerf, "reveal_at") ?? Date.now();
|
|
2514
|
+
return Math.max(0, liveRevealAt - insertedAt);
|
|
2515
|
+
},
|
|
2516
|
+
sdkInitStartToAssignmentReadyMs: initTotalMs,
|
|
2517
|
+
};
|
|
2518
|
+
syncStorefrontVitals();
|
|
2519
|
+
writeIdentityContextCookie();
|
|
2520
|
+
|
|
2521
|
+
const beginTracking = () => {
|
|
2522
|
+
if (state.config !== resolvedConfig) return;
|
|
2523
|
+
finishInitializingAssignments();
|
|
2524
|
+
if (!state.trackingPrerequisiteMet) {
|
|
2525
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("track", "Tracking blocked by project prerequisite");
|
|
2526
|
+
} else if (trackStart === "idle") {
|
|
2527
|
+
scheduleAnalyticsStart(
|
|
2528
|
+
resolvedConfig,
|
|
2529
|
+
experiments,
|
|
2530
|
+
initialApplyPromise ? undefined : initialTargetingPass,
|
|
2531
|
+
);
|
|
2532
|
+
} else {
|
|
2533
|
+
startAnalyticsStage(
|
|
2534
|
+
resolvedConfig,
|
|
2535
|
+
experiments,
|
|
2536
|
+
undefined,
|
|
2537
|
+
initialApplyPromise ? undefined : initialTargetingPass,
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
// This callback is queued behind initialApplyPromise below, so Shopify
|
|
2541
|
+
// never receives assignment attribution before the treatment is visible.
|
|
2542
|
+
scheduleShopifyAssignmentSync();
|
|
2543
|
+
};
|
|
2544
|
+
if (initialApplyPromise) void initialApplyPromise.then(beginTracking);
|
|
2545
|
+
else beginTracking();
|
|
2546
|
+
|
|
2547
|
+
if (FEATURE_AUDIT) {
|
|
2548
|
+
import("./audit").then(({ runAuditIfRequested }) => {
|
|
2549
|
+
runAuditIfRequested(resolvedConfig, {
|
|
2550
|
+
configSource: state.configSource,
|
|
2551
|
+
trackerStarted: Boolean(state.tracker),
|
|
2552
|
+
platform: state.platform,
|
|
2553
|
+
});
|
|
2554
|
+
}).catch(() => {});
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
// Route observer (SPA fast path)
|
|
2558
|
+
if (
|
|
2559
|
+
!emptyProjectFastPath &&
|
|
2560
|
+
(pageTriggerMode !== "manual" || hasHistoryPageRules || hasPollingPageRules)
|
|
2561
|
+
) {
|
|
2562
|
+
state.stopObserver = observeRouteChanges(state.routerMode, ({ previousUrl, nextUrl, reason }) => {
|
|
2563
|
+
if (runtimeSharedState.clearingQaForceFromRoute) return;
|
|
2564
|
+
// A live runtime has already derived holdout, persistence, and tracker
|
|
2565
|
+
// state from QA mode. Strip the sentinel first, then reload so every
|
|
2566
|
+
// construction-time decision is rebuilt outside QA mode. Returning is
|
|
2567
|
+
// also essential: exitQaMode() uses replaceState, which this observer
|
|
2568
|
+
// wraps synchronously, and the outer callback must not continue with
|
|
2569
|
+
// its stale pre-strip URL.
|
|
2570
|
+
runtimeSharedState.clearingQaForceFromRoute = true;
|
|
2571
|
+
try {
|
|
2572
|
+
if (clearQaForceFromUrl()) {
|
|
2573
|
+
location.reload();
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
} finally {
|
|
2577
|
+
runtimeSharedState.clearingQaForceFromRoute = false;
|
|
2578
|
+
}
|
|
2579
|
+
if (!state.config || !state.userId) return;
|
|
2580
|
+
cancelAllPageRulePolls();
|
|
2581
|
+
const detector = reason === "polling" ? "polling" : "history";
|
|
2582
|
+
const triggeredPageIds = firePageDetector(detector, nextUrl);
|
|
2583
|
+
const routeAnalyticsGeneration = ++state.routeAnalyticsGeneration;
|
|
2584
|
+
// Finalize the prior route and start a fresh CLS window so SPA events
|
|
2585
|
+
// never reuse the first route's reveal boundary or shift totals.
|
|
2586
|
+
startCwvMeasurement();
|
|
2587
|
+
|
|
2588
|
+
// Fire page_exit for the page we're leaving
|
|
2589
|
+
if (!state.screenshotMode && state.analyticsStarted) {
|
|
2590
|
+
onPageExit(analyticsTrack);
|
|
2591
|
+
}
|
|
2592
|
+
const exps = state.config.experiments ?? [];
|
|
2593
|
+
const routeStartedAt = Date.now();
|
|
2594
|
+
const immediatePageRulesNeedReevaluation =
|
|
2595
|
+
state.pageTriggerMode === "url_change" ||
|
|
2596
|
+
shouldReevaluateForUrlChange(previousUrl, nextUrl, state.urlDiffNeeds);
|
|
2597
|
+
const shouldReevaluate = triggeredPageIds.size > 0 ||
|
|
2598
|
+
immediatePageRulesNeedReevaluation;
|
|
2599
|
+
state.lastRouteChange = {
|
|
2600
|
+
previousUrl,
|
|
2601
|
+
nextUrl,
|
|
2602
|
+
reason,
|
|
2603
|
+
at: Date.now(),
|
|
2604
|
+
reEvaluated: shouldReevaluate,
|
|
2605
|
+
};
|
|
2606
|
+
|
|
2607
|
+
if (shouldReevaluate) {
|
|
2608
|
+
reevaluateAssignments("url", false, false, {
|
|
2609
|
+
triggeredPageIds,
|
|
2610
|
+
includeImmediate: immediatePageRulesNeedReevaluation,
|
|
2611
|
+
});
|
|
2612
|
+
if (state.redirectNavigationPending) return;
|
|
2613
|
+
}
|
|
2614
|
+
if (reason !== "polling") scheduleDeferredRouteReevaluation();
|
|
2615
|
+
if (!state.pendingInitialApply) {
|
|
2616
|
+
// Routes with no visual re-apply still need a route-local reveal
|
|
2617
|
+
// boundary; applyAssignments() marks it for gated/visual routes.
|
|
2618
|
+
markCwvReveal();
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
state.lastUrl = nextUrl;
|
|
2622
|
+
state.trackingPrerequisiteMet = trackingAllowed(state.config);
|
|
2623
|
+
syncStorefrontVitals();
|
|
2624
|
+
updateProjectActivationDiagnostics(state.config);
|
|
2625
|
+
const assignmentReadyAt = Date.now();
|
|
2626
|
+
queuePerfEvent(buildPerfEvent(state.config, {
|
|
2627
|
+
eventKind: "route_change",
|
|
2628
|
+
assignmentReadyMs: assignmentReadyAt - routeStartedAt,
|
|
2629
|
+
blankScreenMs: 0,
|
|
2630
|
+
assignmentReadyAt,
|
|
2631
|
+
}));
|
|
2632
|
+
|
|
2633
|
+
const runRouteAnalytics = () => {
|
|
2634
|
+
if (routeAnalyticsGeneration !== state.routeAnalyticsGeneration) return;
|
|
2635
|
+
if (!state.config) return;
|
|
2636
|
+
// Re-publish only after a held route recheck has refreshed assignments.
|
|
2637
|
+
// The pixel can load after the SDK, so rebroadcast briefly instead of
|
|
2638
|
+
// relying on one event.
|
|
2639
|
+
scheduleShopifyAssignmentSync();
|
|
2640
|
+
if (state.analyticsStarted && !state.trackingPrerequisiteMet) {
|
|
2641
|
+
stopAnalyticsStage();
|
|
2642
|
+
} else if (state.analyticsStarted) {
|
|
2643
|
+
flushPendingPerf();
|
|
2644
|
+
state.exposedAssignmentKeys.clear();
|
|
2645
|
+
trackViewedAssignments(state.config);
|
|
2646
|
+
setupGoalTracking(exps, nextUrl);
|
|
2647
|
+
setupHeatmapTracking(state.assignments, exps);
|
|
2648
|
+
setupReplayTracking(state.assignments, exps);
|
|
2649
|
+
onPageEnter(analyticsTrack);
|
|
2650
|
+
} else if (!state.screenshotMode && !trackingBlocked() && state.trackingPrerequisiteMet) {
|
|
2651
|
+
if (state.trackingStart === "idle") {
|
|
2652
|
+
scheduleAnalyticsStart(state.config, exps);
|
|
2653
|
+
} else {
|
|
2654
|
+
startAnalyticsStage(state.config, exps);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
if (state.pendingInitialApply) {
|
|
2659
|
+
// The URL recheck queued behind a hold/hydration gate; running the
|
|
2660
|
+
// route analytics now would expose and wire goals for the previous
|
|
2661
|
+
// route's stale assignments. The queued recheck registered first,
|
|
2662
|
+
// so assignments are fresh by the time this runs.
|
|
2663
|
+
void state.pendingInitialApply.then(runRouteAnalytics);
|
|
2664
|
+
} else {
|
|
2665
|
+
runRouteAnalytics();
|
|
2666
|
+
}
|
|
2667
|
+
}, {
|
|
2668
|
+
...(hasPollingPageRules
|
|
2669
|
+
? {
|
|
2670
|
+
pollingIntervalMs: normalizeNumber(
|
|
2671
|
+
config.pageTrigger?.urlPollIntervalMs,
|
|
2672
|
+
PAGE_URL_POLL_INTERVAL_MS,
|
|
2673
|
+
),
|
|
2674
|
+
}
|
|
2675
|
+
: {}),
|
|
2676
|
+
onObserverError: (error) => recordObserverError("route", error),
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
if (
|
|
2681
|
+
!emptyProjectFastPath &&
|
|
2682
|
+
domPageTrigger &&
|
|
2683
|
+
(!manualInitialTrigger || hasDomPageRules)
|
|
2684
|
+
) {
|
|
2685
|
+
state.stopDomObserver = observeDomChanges(() => {
|
|
2686
|
+
try {
|
|
2687
|
+
const triggeredPageIds = firePageDetector("dom", location.href);
|
|
2688
|
+
if (shopDomPageTrigger || triggeredPageIds.size > 0) {
|
|
2689
|
+
reevaluateAssignments("dom", false, false, {
|
|
2690
|
+
triggeredPageIds,
|
|
2691
|
+
includeImmediate: shopDomPageTrigger,
|
|
2692
|
+
});
|
|
2693
|
+
}
|
|
2694
|
+
} catch (error) {
|
|
2695
|
+
recordObserverError("dom", error);
|
|
2696
|
+
}
|
|
2697
|
+
}, config.spa?.debounceMs ?? 250);
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("init", `${state.assignments.length} assignment(s) active`, state.assignments.map(a => ({
|
|
2701
|
+
experiment: a.experimentId,
|
|
2702
|
+
variation: a.variationId,
|
|
2703
|
+
isControl: a.isControl,
|
|
2704
|
+
})));
|
|
2705
|
+
__DRIP_FEATURE_DEVTOOLS__ && traceStep("Execution cycle complete", {
|
|
2706
|
+
assignments: state.assignments.map((assignment) => ({
|
|
2707
|
+
experimentId: assignment.experimentId,
|
|
2708
|
+
variationId: assignment.variationId,
|
|
2709
|
+
isControl: assignment.isControl,
|
|
2710
|
+
})),
|
|
2711
|
+
});
|
|
2712
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugGroupEnd();
|
|
2713
|
+
|
|
2714
|
+
// Lazy-load QA toolbar when in QA mode. Screenshot captures use QA forcing
|
|
2715
|
+
// without any visible Apex UI in the captured page.
|
|
2716
|
+
if (FEATURE_QA && state.qaMode && !state.screenshotMode) {
|
|
2717
|
+
// The QA Assistant replaces the legacy toolbar; if its module is stubbed
|
|
2718
|
+
// or fails to load, fall back to the toolbar/devtools path so QA mode
|
|
2719
|
+
// never goes dark.
|
|
2720
|
+
loadQaAssistant(() => {
|
|
2721
|
+
loadQaToolbar(experiments);
|
|
2722
|
+
loadDevtoolsIfEnabled(experiments);
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
notifyQaAssistantRuntimeListeners();
|
|
2726
|
+
|
|
2727
|
+
// Lazy-load devtools overlay when enabled
|
|
2728
|
+
if (!state.qaMode) {
|
|
2729
|
+
loadDevtoolsIfEnabled(experiments);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// Dispatch initialized event
|
|
2733
|
+
document.dispatchEvent(new CustomEvent("drip_initialized"));
|
|
2734
|
+
|
|
2735
|
+
return state.assignments;
|
|
2736
|
+
}
|
|
2737
|
+
|