@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,2175 @@
|
|
|
1
|
+
declare const __DRIP_FEATURE_QA__: boolean;
|
|
2
|
+
declare const __DRIP_FEATURE_DEVTOOLS__: boolean;
|
|
3
|
+
declare const __DRIP_FEATURE_AUDIT__: boolean;
|
|
4
|
+
declare const __DRIP_FEATURE_HEATMAP__: boolean;
|
|
5
|
+
declare const __DRIP_FEATURE_REPLAY__: boolean;
|
|
6
|
+
declare const __DRIP_FEATURE_HARNESS__: boolean;
|
|
7
|
+
const FEATURE_QA = __DRIP_FEATURE_QA__;
|
|
8
|
+
const FEATURE_DEVTOOLS = __DRIP_FEATURE_DEVTOOLS__;
|
|
9
|
+
const FEATURE_AUDIT = __DRIP_FEATURE_AUDIT__;
|
|
10
|
+
const FEATURE_HEATMAP = __DRIP_FEATURE_HEATMAP__;
|
|
11
|
+
const FEATURE_REPLAY = __DRIP_FEATURE_REPLAY__;
|
|
12
|
+
const FEATURE_HARNESS = __DRIP_FEATURE_HARNESS__;
|
|
13
|
+
import { cleanUrl, onPageEnter, onPageExit } from "./analytics";
|
|
14
|
+
import { normalizeEndpoint } from "./endpoint";
|
|
15
|
+
import { EVENT_TYPE_PATTERN, normalizeEventName } from "@drip/event-schema/event-name";
|
|
16
|
+
import {
|
|
17
|
+
applyAntiFlicker,
|
|
18
|
+
bootedTooLateForAntiFlicker,
|
|
19
|
+
removeAntiFlicker,
|
|
20
|
+
} from "./antiflicker";
|
|
21
|
+
import {
|
|
22
|
+
clearPersistedAssignments,
|
|
23
|
+
findPersistedVariation,
|
|
24
|
+
persistAssignmentForExperiment,
|
|
25
|
+
prunePersistedAssignments,
|
|
26
|
+
removePersistedAssignmentsForExperiments,
|
|
27
|
+
__assignmentStoreInternals,
|
|
28
|
+
} from "./assignment-store";
|
|
29
|
+
import { cleanupBucketingStorage } from "./bucketing-cleanup";
|
|
30
|
+
import { chooseVariation, inTrafficAllocation } from "./bucket";
|
|
31
|
+
import { buildAssignment, decideAssignments } from "./decide-assignments";
|
|
32
|
+
import { weekdayScheduleBlocksDelivery } from "./weekday-schedule";
|
|
33
|
+
import { CONSENT_REQUIRED, DNT_BLOCKED, evaluateSynchronousConsent, readConfiguredConsent, readPath, resolveSynchronousConsentGranted } from "./consent-evaluation";
|
|
34
|
+
import { clearEarlyInteractions, drainBootstrapEarlyInteractions, drainEarlyInteractions } from "./early-interaction-capture";
|
|
35
|
+
import { getCwvMetrics, markCwvReveal, startCwvMeasurement, waitForCwvMetrics } from "./cwv";
|
|
36
|
+
import { collectDeliveryTiming } from "./delivery-timing";
|
|
37
|
+
import {
|
|
38
|
+
startStorefrontVitals,
|
|
39
|
+
stopStorefrontVitals,
|
|
40
|
+
type StorefrontVitalsContext,
|
|
41
|
+
} from "./storefront-vitals";
|
|
42
|
+
import {
|
|
43
|
+
browserGroupStickyStore,
|
|
44
|
+
clearBrowserGroupStickyStore,
|
|
45
|
+
resolveExclusionGroups,
|
|
46
|
+
type ExclusionGroupDecision,
|
|
47
|
+
type PersistedGroupSelection,
|
|
48
|
+
} from "./exclusion-groups";
|
|
49
|
+
import type { CompactGroupSelections } from "./persistence-decoders";
|
|
50
|
+
import {
|
|
51
|
+
applyCommercePrice,
|
|
52
|
+
isCommercePriceWorkPending,
|
|
53
|
+
cleanupCommercePrice,
|
|
54
|
+
hasCommercePriceState,
|
|
55
|
+
COMMERCE_PRICE_SELECTORS,
|
|
56
|
+
isCommercePriceActive,
|
|
57
|
+
isCommercePriceExposureEligible,
|
|
58
|
+
notifyCommerceCartStampConfirmed,
|
|
59
|
+
setCommercePriceExposureListener,
|
|
60
|
+
} from "./commerce-price";
|
|
61
|
+
import {
|
|
62
|
+
applyCommerceShipping,
|
|
63
|
+
classifyShippingExposure,
|
|
64
|
+
cleanupCommerceShipping,
|
|
65
|
+
isShippingCartEligible,
|
|
66
|
+
isShippingCartEligibilityConfirmed,
|
|
67
|
+
} from "./commerce-shipping";
|
|
68
|
+
import {
|
|
69
|
+
addCssOwners,
|
|
70
|
+
injectCss,
|
|
71
|
+
injectExperimentCss,
|
|
72
|
+
injectExternalCss,
|
|
73
|
+
removeCssForOwner,
|
|
74
|
+
} from "./css";
|
|
75
|
+
import { configureDebug, debugLog, debugWarn, debugGroup, debugGroupEnd, isDebugEnabled } from "./debug";
|
|
76
|
+
import { loadDebugBuild } from "./debug-loader";
|
|
77
|
+
import { observeDomChanges } from "./dom-trigger";
|
|
78
|
+
import { edgeRuntimeActive, readEdgeRuntimeAssignments } from "./edge-runtime";
|
|
79
|
+
import { normalizeDataLayerEntry } from "./data-layer-normalize";
|
|
80
|
+
import { interceptEditorLaunch, isEditorDevicePreview } from "./editor-bootstrap";
|
|
81
|
+
import { cleanupGoals, goalWiringKeys, setupGoals, triggerGoal, triggerGoalsForEvent } from "./goals";
|
|
82
|
+
import { clearQaForceFromUrl, isBot, isDuplicate, isIframe, isOptedOut, isQaMode, isScreenshotMode, parseForceParam, persistQaForce, persistUrlQaForce, exitQaMode, getQaSessionId } from "./guards";
|
|
83
|
+
import { adoptUserId, applyHoldoutGate, configureIdentityStorage, getEphemeralUserId, getUserId, hashToFloat, isHoldoutVisitor, variationHashToFloat } from "./hash";
|
|
84
|
+
import { createBucketingHandoverPayload, handoverMintTimeoutMs, hasBucketingHandover, installBucketingHandover, readBucketingHandover } from "./handover";
|
|
85
|
+
import { revealAll } from "./hideflicker";
|
|
86
|
+
import {
|
|
87
|
+
addExternalJsOwners,
|
|
88
|
+
execJs,
|
|
89
|
+
loadExternalJs,
|
|
90
|
+
removeExternalJsForOwner,
|
|
91
|
+
setJsPolicy,
|
|
92
|
+
} from "./js";
|
|
93
|
+
import {
|
|
94
|
+
addAppliedMutationOwners,
|
|
95
|
+
applyMutations,
|
|
96
|
+
applyToElement,
|
|
97
|
+
invalidateArtifact,
|
|
98
|
+
reassertNonStructuralMutations,
|
|
99
|
+
replayArtifact,
|
|
100
|
+
resetAppliedMutationsForOwner,
|
|
101
|
+
} from "./mutate";
|
|
102
|
+
import { registerMutedObserver, runMuted, unregisterMutedObserver } from "./observer-mute";
|
|
103
|
+
import { assignmentsHaveVisualWork, awaitHydration, hasMutationHold, holdMutations, releaseMutations, signalHydrated } from "./hydration";
|
|
104
|
+
import {
|
|
105
|
+
assignmentsHaveCustomJs,
|
|
106
|
+
assignmentsHaveStructuralMutations,
|
|
107
|
+
installEarlyReassert,
|
|
108
|
+
shouldUseEarlyReassert,
|
|
109
|
+
} from "./early-reassert";
|
|
110
|
+
import { trackAppliedMutation } from "./reassert-sentinel";
|
|
111
|
+
import { observeRouteChanges, PAGE_URL_POLL_INTERVAL_MS } from "./router";
|
|
112
|
+
import { buildExposureSignalContext } from "./exposure-signals";
|
|
113
|
+
import {
|
|
114
|
+
buildRedirectDestination,
|
|
115
|
+
clearPendingRedirectAssignmentCarry,
|
|
116
|
+
finalizeRedirectAssignmentCarryHandover,
|
|
117
|
+
hasGeneratedRedirectAssignmentCarry,
|
|
118
|
+
filterUnsafeRedirectAssignments,
|
|
119
|
+
importRedirectAssignmentCarry,
|
|
120
|
+
isRedirectLoop,
|
|
121
|
+
markRedirectAssignmentCarryExposed,
|
|
122
|
+
parseRedirectAssignmentCarry,
|
|
123
|
+
persistPendingRedirectAssignmentCarry,
|
|
124
|
+
readPendingRedirectAssignmentCarry,
|
|
125
|
+
REDIRECT_ASSIGNMENT_PARAM,
|
|
126
|
+
redirectPlanForAssignment,
|
|
127
|
+
stripRedirectAssignmentCarry,
|
|
128
|
+
validateRedirectAssignmentCarry,
|
|
129
|
+
verifyRedirectAssignmentCarry,
|
|
130
|
+
} from "./redirect";
|
|
131
|
+
import type { VerifiedRedirectCarry } from "./redirect";
|
|
132
|
+
import { buildPrerequisiteContext, evaluateProjectPrerequisite } from "./prerequisites";
|
|
133
|
+
import {
|
|
134
|
+
REPLAY_PROGRESS_KEY_PREFIX,
|
|
135
|
+
REPLAY_SAMPLE_KEY_PREFIX,
|
|
136
|
+
resolveReplayRecorderUrl,
|
|
137
|
+
startReplayCapture,
|
|
138
|
+
type ReplayController,
|
|
139
|
+
} from "./replay";
|
|
140
|
+
import {
|
|
141
|
+
buildCartAttributeEntries,
|
|
142
|
+
buildCartMetafieldInputs,
|
|
143
|
+
buildSignedCartAttributeEntries,
|
|
144
|
+
APEX_CART_ATTRIBUTE_KEYS,
|
|
145
|
+
cannotRequestRequiredCommerceAuthorization,
|
|
146
|
+
getCartMetafieldStaleKeys as collectCartMetafieldStaleKeys,
|
|
147
|
+
invalidateShopifyCartAttributeSync,
|
|
148
|
+
normalizeCartMetafieldNamespace,
|
|
149
|
+
syncShopifyCartAttributes,
|
|
150
|
+
type CartAssignment,
|
|
151
|
+
type CartAttributeEntry,
|
|
152
|
+
type CartMetafieldInput,
|
|
153
|
+
type CartAttributeSyncInput,
|
|
154
|
+
} from "./shopify-cart";
|
|
155
|
+
import { onShopifyCartMutation } from "./shopify-cart-events";
|
|
156
|
+
import {
|
|
157
|
+
getPendingGiftChoices,
|
|
158
|
+
noteCartSnapshotForGifts,
|
|
159
|
+
selectGiftOption,
|
|
160
|
+
syncGiftWithPurchase,
|
|
161
|
+
type GiftSyncOptions,
|
|
162
|
+
} from "./commerce-gift";
|
|
163
|
+
import { CART_DRAWER_OPEN_SIGNAL, installSurfaceSignals } from "./surface-signals";
|
|
164
|
+
import {
|
|
165
|
+
assignmentSignature,
|
|
166
|
+
canUseEmptyProjectProfile,
|
|
167
|
+
computeUrlDiffNeeds,
|
|
168
|
+
getBlankScreenMs,
|
|
169
|
+
readPerfFlags,
|
|
170
|
+
resolveActivationMode,
|
|
171
|
+
resolvePlatform,
|
|
172
|
+
resolveRouterMode,
|
|
173
|
+
shouldReevaluateForUrlChange,
|
|
174
|
+
} from "./runtime";
|
|
175
|
+
import type { PerfFlagState } from "./runtime";
|
|
176
|
+
import {
|
|
177
|
+
buildTargetingAttributes,
|
|
178
|
+
evaluateTargetingGatesDetailed,
|
|
179
|
+
matchPageTargetingRule,
|
|
180
|
+
readShopifySignal,
|
|
181
|
+
targetingHasJsTriggers,
|
|
182
|
+
} from "./targeting";
|
|
183
|
+
import { createApexRuntimeTools, type ApexRuntimeTools } from "./runtime-tools";
|
|
184
|
+
import { createEventId, Tracker } from "./track";
|
|
185
|
+
import {
|
|
186
|
+
clearStoredSignals,
|
|
187
|
+
configureSignalPersistenceAllowed,
|
|
188
|
+
clearRuntimeSignal,
|
|
189
|
+
getSignals as getStoredSignals,
|
|
190
|
+
getSignalDiagnostics,
|
|
191
|
+
rememberProduct,
|
|
192
|
+
setCartSignals,
|
|
193
|
+
setRuntimeSignal,
|
|
194
|
+
setSearchSignal,
|
|
195
|
+
setSignal as setStoredSignal,
|
|
196
|
+
} from "./signals";
|
|
197
|
+
import {
|
|
198
|
+
evaluateFlag as evaluateFlagFromExperiments,
|
|
199
|
+
evaluateFlagAssignment as evaluateFlagAssignmentFromExperiments,
|
|
200
|
+
evaluateFlagValue as evaluateFlagValueFromExperiments,
|
|
201
|
+
} from "./flags";
|
|
202
|
+
import {
|
|
203
|
+
ActivationMode,
|
|
204
|
+
AntiFlickerMode,
|
|
205
|
+
Assignment,
|
|
206
|
+
ActivationDiagnostics,
|
|
207
|
+
ActivationGateDiagnostic,
|
|
208
|
+
ConditionDiagnostic,
|
|
209
|
+
ConfigProfile,
|
|
210
|
+
ConsentModeSettings,
|
|
211
|
+
CustomJsPolicy,
|
|
212
|
+
DebugSettings,
|
|
213
|
+
ExecutionMode,
|
|
214
|
+
Experiment,
|
|
215
|
+
ExperimentActivationDiagnostic,
|
|
216
|
+
ExclusionGroupConfig,
|
|
217
|
+
ExclusionGroupMemberConfig,
|
|
218
|
+
Goal,
|
|
219
|
+
InitConfig,
|
|
220
|
+
PageTargetingRule,
|
|
221
|
+
PageActivationDiagnostic,
|
|
222
|
+
PageDeactivationMode,
|
|
223
|
+
PageTriggerMode,
|
|
224
|
+
PageTriggerSettings,
|
|
225
|
+
PerfFeatureFlag,
|
|
226
|
+
PerfFeatureFlags,
|
|
227
|
+
PlatformHint,
|
|
228
|
+
ProjectPrerequisites,
|
|
229
|
+
RouterMode,
|
|
230
|
+
SegmentCondition,
|
|
231
|
+
SpaSettings,
|
|
232
|
+
Targeting,
|
|
233
|
+
ExternalTrackingSettings,
|
|
234
|
+
ProjectCodeSettings,
|
|
235
|
+
RuntimeEnvironmentSettings,
|
|
236
|
+
RuntimeSignalDefinition,
|
|
237
|
+
SdkDiagnostics,
|
|
238
|
+
SnippetCriticalMutation,
|
|
239
|
+
SnippetCriticalMutationPlanItem,
|
|
240
|
+
SnippetDeliveryUrlRule,
|
|
241
|
+
BucketingCleanupSettings,
|
|
242
|
+
DataProtectionSettings,
|
|
243
|
+
DataLayerSignalMapping,
|
|
244
|
+
DataLayerTriggerSettings,
|
|
245
|
+
ExposureDestinationsSettings,
|
|
246
|
+
Mutation,
|
|
247
|
+
TrackEvent,
|
|
248
|
+
TrackingStart,
|
|
249
|
+
Variation,
|
|
250
|
+
} from "./types";
|
|
251
|
+
import {
|
|
252
|
+
addWaitEntryOwners,
|
|
253
|
+
applyMutationWithWait,
|
|
254
|
+
resetWaitEntriesForOwner,
|
|
255
|
+
} from "./wait";import { OrderIdentityPlatform, SDKState, QaAssistantRuntimeSnapshot, PageActivationRuntimeState, PageRulePollController, state, runtimeSharedState } from "./runtime-state";
|
|
256
|
+
import { applyRuntimeAntiFlicker, SDK_SCRIPT_SRC, HeatmapController, SemanticController, FetchAttemptInfo, FetchRetryOptions, DEFAULT_CONFIG_FETCH_TIMEOUT_MS, DEFAULT_CONFIG_FETCH_SLOW_MS, CONFIG_FETCH_RETRY_COUNT, CONFIG_FETCH_RETRY_DELAY_MS, CONFIG_FETCH_MAX_WAIT_MS, CACHED_REDIRECT_HIDE_TIMEOUT_MS, createFetchTimeoutError, fetchWithTimeout, fetchWithRetry, CONFIG_CACHE_KEY_PREFIX, DEFAULT_CONFIG_CACHE_TTL_MS, DEFAULT_ANTI_FLICKER_TIMEOUT_MS, DEFAULT_TRACKING_IDLE_TIMEOUT_MS, DEFAULT_EXECUTION_MODE, DEFAULT_TRACKING_START, DEFAULT_JS_POLICY, PAGE_RULE_POLL_WINDOW_MS, PAGE_RULE_POLL_INTERVAL_MS, CONSENT_COOKIE_NAME, IDENTITY_CONTEXT_COOKIE_NAME, IDENTITY_CONTEXT_COOKIE_MAX_AGE_SECONDS, IDENTITY_CONTEXT_COOKIE_MAX_ENCODED_BYTES, SESSION_STORAGE_KEY, collectScopedSelectors, ResolvedRuntimeConfig, UrlDiffNeeds, ConfigSource, PendingPerfEvent, InitPhaseKey, InitPerfContext, MAX_PERF_DURATION_MS, monotonicNow, boundedPerfDuration, createInitPerfContext, BootstrapPredecision, bootstrapPredecision, addInitPhase, DeliveryHealthStatus, DeliveryHealthTracker, RouteDiagnostic, DataLayerDiagnostic, ShopifySyncDiagnostic, ExposureDestinationDiagnostic, RuntimeLoadDiagnostic, DripPerfWindow, normalizeNumber, resolveRuntimeConfig, resolvePageTriggerMode, resolveDomPageTrigger, dtLog, traceName, traceStep, traceGate, browserDoNotTrackValue, trackingBlocked, storefrontVitalsGateOpen, syncStorefrontVitals, doNotTrackEnabled, storageAllowed, readWindowPath, configuredConsentGranted, syncConfiguredConsent, CUSTOM_CONSENT_POLL_INTERVAL_MS, CUSTOM_CONSENT_POLL_MAX_MS, MANUAL_CALLBACK_TRIGGER_RERUN_INTERVAL_MS, MANUAL_CALLBACK_TRIGGER_RERUN_MAX_MS, MANUAL_CALLBACK_TRIGGER_MAX_RERUNS, EVENT_DRIVEN_CONSENT_PROVIDERS, shouldPollConfiguredConsent, installConsentChangeListeners, resolveConsentRequired, resolveConsentGranted, persistentStorageAllowed, coerceSignalValue, captureUrlSignals, installClickSignalCaptures, captureEventPropertySignals, setCapturedSignal, setPublicSignal, getPublicSignals, normalizePageApiName, pageTriggerSignal, triggerPage, readElementCaptureValue, captureDomAndGlobalSignals, consentAwareDataProtection, writeConsentCookie, readConsentCookie, clearConsentStorage, applyConsentStorageMode, runWhenDomReady, applyProjectCode, resetProjectCode, runProjectStartupCode, collectJsTriggerConditions, collectPageTriggerRules, PageDetectorMode, PageRuleReconcileScope, collectPageDetectorRules, experimentsForGlobalRouteReevaluation, conditionUsesActivateTrigger, buildJsTriggerContext, runJsTriggerReset, normalizeActivationValue, installActivateJsTrigger, installPageTriggerRule, installJsTriggerObserver, notifyQaAssistantRuntimeListeners, HarnessManifest, harnessPollTimer, harnessPollKey, harnessVisibilityInstalled, activeHarnessAssignment, stopHarnessPolling, applyHarnessManifest, pollHarnessManifest, startHarnessPolling, getEventsEndpoint, mintBucketingHandoverPayload, verifyBucketingHandoverToken, clearScheduledAnalyticsStart, normalizeDataLayerTriggers, applyDataLayerMappings, readNumericPath, readStringPath, RESERVED_DATA_LAYER_REVENUE_KEYS, PendingDataLayerEvent, MAX_PENDING_DATA_LAYER_EVENTS, pendingDataLayerEvents, processedDataLayerEntries, isDataLayerEntryObject, markDataLayerEntryProcessed, trackDataLayerRevenue, trackDataLayerEvent, enqueuePendingDataLayerEvent, drainPendingDataLayerEvents, finishInitializingAssignments, handleDataLayerEvent, recordObserverError } from "./config-runtime";
|
|
257
|
+
import { 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";
|
|
258
|
+
import { generateQaSessionId, SHOPIFY_ASSIGNMENT_SYNC_DELAYS_MS, shopifyAssignmentSyncTimers, CommerceOnlyAdoptionState, commercePriceAdoptionState, ensureCommercePriceAdoptionState, currentCommercePriceContext, withCommercePriceAssignments, clearShopifyAssignmentSyncTimers, adoptAuthoritativePriceAssignments, OffersAdoptionPlan, planAuthoritativeOffersAdoption, canAdoptAuthoritativeOffersAssignments, adoptAuthoritativeOffersAssignments, sdkCartMintAllowed, buildSdkCartMintInput, buildScopedCartAttributeSyncInput, buildActiveCartAttributeSyncInput, sdkCartMintSync, syncSdkCartMint, removeSdkCartMintTrigger, onSdkCartMintClick, installSdkCartMintTrigger, HeadlessCartAuthorizationState, headlessCartAuthorizationState, ensureHeadlessCartAuthorizationState, hasChargeAffectingCommerceAssignment, hasActiveAuthorizationPlane, hasActivePriceAuthorizationPlane, hasActiveShippingAuthorizationPlane, hasActiveOffersAuthorizationPlane, hasActiveCommerceAuthorizationPlane, requiresCommerceCohortPin, getChargeAffectingCommerceExperimentIds, isCohortStampedOnCart, syncActiveAssignmentToShopifyCart, configureChargeCorrectnessCartSync, holdoutConfigEpoch, publishAssignmentsToShopify, scheduleShopifyAssignmentSync, variationsAllowed, updateProjectActivationDiagnostics, trackingAllowed, experimentById, targetingLifecycleMode, preservePersistentAssignments, stopAnalyticsStage, canMintVerifiedRedirectCarry, planRedirectAssignment, navigateForRedirectAssignment, RECONCILE_MAX_DRAINS, reevaluateAssignments, reevaluateAssignmentsPass, reapplyQaAssistantForceMap, reconcileForcedUnavailable, __getQaAssistantRuntimeSnapshot, __subscribeQaAssistantRuntime, __forceQaAssistantVariation, __resetQaAssistantForce, clearQaForceFromCurrentUrl, initWithExperiments } from "./activation-runtime";
|
|
259
|
+
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";
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
export function scheduleDeferredRouteReevaluation(): void {
|
|
263
|
+
if (state.deferredRouteReevaluationTimer !== null) return;
|
|
264
|
+
state.deferredRouteReevaluationTimer = window.setTimeout(() => {
|
|
265
|
+
state.deferredRouteReevaluationTimer = null;
|
|
266
|
+
try {
|
|
267
|
+
reevaluateAssignments("dom", false, false, {
|
|
268
|
+
triggeredPageIds: new Set(),
|
|
269
|
+
includeImmediate: true,
|
|
270
|
+
});
|
|
271
|
+
} catch (error) {
|
|
272
|
+
recordObserverError("route", error);
|
|
273
|
+
}
|
|
274
|
+
}, 0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function installDataLayerTriggers(config: InitConfig): (() => void) | null {
|
|
278
|
+
const triggers = normalizeDataLayerTriggers(config);
|
|
279
|
+
if (!triggers.length || typeof window === "undefined") return null;
|
|
280
|
+
|
|
281
|
+
const byLayer = new Map<string, DataLayerTriggerSettings[]>();
|
|
282
|
+
for (const trigger of triggers) {
|
|
283
|
+
const layerName = trigger.dataLayerName || "dataLayer";
|
|
284
|
+
byLayer.set(layerName, [...(byLayer.get(layerName) ?? []), trigger]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const cleanups: Array<() => void> = [];
|
|
288
|
+
for (const [layerName, layerTriggers] of byLayer.entries()) {
|
|
289
|
+
const w = window as unknown as Record<string, any>;
|
|
290
|
+
const layer = Array.isArray(w[layerName]) ? w[layerName] : [];
|
|
291
|
+
w[layerName] = layer;
|
|
292
|
+
const originalPush = layer.push;
|
|
293
|
+
let active = true;
|
|
294
|
+
const process = (item: unknown) => {
|
|
295
|
+
if (!active) return;
|
|
296
|
+
if (isDataLayerEntryObject(item) && processedDataLayerEntries.has(item)) return;
|
|
297
|
+
const payload = normalizeDataLayerEntry(item);
|
|
298
|
+
if (!payload) {
|
|
299
|
+
markDataLayerEntryProcessed(item);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const eventName = typeof payload.event === "string" ? payload.event : "";
|
|
303
|
+
let matched = false;
|
|
304
|
+
for (const trigger of layerTriggers) {
|
|
305
|
+
if (eventName === trigger.eventName) {
|
|
306
|
+
matched = true;
|
|
307
|
+
handleDataLayerEvent(trigger, payload, layerName, item, payload !== item);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (!matched) markDataLayerEntryProcessed(item);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const safelyProcess = (item: unknown) => {
|
|
314
|
+
try {
|
|
315
|
+
process(item);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
recordObserverError("data_layer", error);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
layer.slice(Math.max(0, layer.length - 50)).forEach(safelyProcess);
|
|
321
|
+
const wrappedPush = function (...items: unknown[]) {
|
|
322
|
+
const result = originalPush.apply(layer, items);
|
|
323
|
+
items.forEach(safelyProcess);
|
|
324
|
+
return result;
|
|
325
|
+
};
|
|
326
|
+
layer.push = wrappedPush as typeof layer.push;
|
|
327
|
+
cleanups.push(() => {
|
|
328
|
+
active = false;
|
|
329
|
+
if (layer.push === wrappedPush) layer.push = originalPush;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function experimentsUseSurfaceSignals(experiments: Experiment[]): boolean {
|
|
337
|
+
return experiments.some((experiment) => {
|
|
338
|
+
try {
|
|
339
|
+
return JSON.stringify(experiment.targeting ?? {}).includes(CART_DRAWER_OPEN_SIGNAL);
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function rememberExposureDestination(result: ExposureDestinationDiagnostic): void {
|
|
347
|
+
state.exposureDestinationDiagnostics = [result, ...state.exposureDestinationDiagnostics].slice(0, 20);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export let clarityUpgradeSent = false;
|
|
351
|
+
|
|
352
|
+
export function clarityTagKey(prefix: string, name: string): string {
|
|
353
|
+
const key = `${prefix}${name}`
|
|
354
|
+
.replace(/[^A-Za-z0-9 _-]/g, "")
|
|
355
|
+
.replace(/\s+/g, " ")
|
|
356
|
+
.trim()
|
|
357
|
+
.slice(0, 80);
|
|
358
|
+
return key || "apex experiment";
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function pushExposureToQueue(destination: NonNullable<ExposureDestinationsSettings["queues"]>[number], payload: Record<string, unknown>): void {
|
|
362
|
+
const queue = readWindowPath(destination.globalName) as any;
|
|
363
|
+
if (!queue) throw new Error("queue_missing");
|
|
364
|
+
const eventName = destination.eventName || "Viewed an experiment";
|
|
365
|
+
if (Array.isArray(queue)) {
|
|
366
|
+
queue.push(destination.method
|
|
367
|
+
? { method: destination.method, event_name: eventName, event_attributes: payload }
|
|
368
|
+
: { event: eventName, ...payload });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (typeof queue.push === "function") {
|
|
372
|
+
queue.push(destination.method
|
|
373
|
+
? { method: destination.method, event_name: eventName, event_attributes: payload }
|
|
374
|
+
: { event: eventName, ...payload });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
throw new Error("queue_push_missing");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function emitExposureDestinations(assignment: Assignment, config: InitConfig): void {
|
|
381
|
+
const destinations = config.exposureDestinations;
|
|
382
|
+
if (!destinations) return;
|
|
383
|
+
const payload = {
|
|
384
|
+
project_id: config.shopId ?? "",
|
|
385
|
+
experiment_id: assignment.experimentId,
|
|
386
|
+
variation_id: assignment.variationId,
|
|
387
|
+
experiment_name: assignment.experimentName ?? assignment.experimentId,
|
|
388
|
+
variation_index: assignment.index,
|
|
389
|
+
variation_name: assignment.variationId,
|
|
390
|
+
is_control: assignment.isControl,
|
|
391
|
+
};
|
|
392
|
+
const at = Date.now();
|
|
393
|
+
|
|
394
|
+
if (destinations.dataLayer?.enabled) {
|
|
395
|
+
try {
|
|
396
|
+
const dataLayerName = destinations.dataLayer.dataLayerName || "dataLayer";
|
|
397
|
+
const w = window as unknown as Record<string, any>;
|
|
398
|
+
const layer = Array.isArray(w[dataLayerName]) ? w[dataLayerName] : [];
|
|
399
|
+
w[dataLayerName] = layer;
|
|
400
|
+
layer.push({
|
|
401
|
+
event: destinations.dataLayer.eventName || "apex_experiment_viewed",
|
|
402
|
+
...(destinations.dataLayer.includeMetadata === false ? {
|
|
403
|
+
experiment_id: assignment.experimentId,
|
|
404
|
+
variation_id: assignment.variationId,
|
|
405
|
+
} : payload),
|
|
406
|
+
});
|
|
407
|
+
rememberExposureDestination({ type: "dataLayer", status: "sent", at });
|
|
408
|
+
} catch (error) {
|
|
409
|
+
rememberExposureDestination({ type: "dataLayer", status: "failed", at, reason: error instanceof Error ? error.message : String(error) });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (destinations.hotjar?.enabled) {
|
|
414
|
+
try {
|
|
415
|
+
const hj = (window as any).hj;
|
|
416
|
+
if (typeof hj !== "function") throw new Error("hotjar_missing");
|
|
417
|
+
if (destinations.hotjar.mode === "event") {
|
|
418
|
+
hj("event", destinations.hotjar.eventName || "apex_experiment_viewed");
|
|
419
|
+
} else {
|
|
420
|
+
hj("identify", null, { apex_ab_test: `${assignment.experimentId}-${assignment.variationId}` });
|
|
421
|
+
}
|
|
422
|
+
rememberExposureDestination({ type: "hotjar", status: "sent", at });
|
|
423
|
+
} catch (error) {
|
|
424
|
+
rememberExposureDestination({ type: "hotjar", status: "failed", at, reason: error instanceof Error ? error.message : String(error) });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (destinations.clarity?.enabled) {
|
|
429
|
+
try {
|
|
430
|
+
const clarityFn = (window as any).clarity;
|
|
431
|
+
if (typeof clarityFn !== "function") throw new Error("clarity_missing");
|
|
432
|
+
const mode = destinations.clarity.mode === "event" || destinations.clarity.mode === "both"
|
|
433
|
+
? destinations.clarity.mode
|
|
434
|
+
: "set";
|
|
435
|
+
if (mode === "set" || mode === "both") {
|
|
436
|
+
const key = clarityTagKey(
|
|
437
|
+
destinations.clarity.tagPrefix ?? "apex ",
|
|
438
|
+
assignment.experimentName ?? assignment.experimentId,
|
|
439
|
+
);
|
|
440
|
+
const value = assignment.isControl ? "control" : `variant-${assignment.index}`;
|
|
441
|
+
clarityFn("set", key, value);
|
|
442
|
+
}
|
|
443
|
+
if (mode === "event" || mode === "both") {
|
|
444
|
+
clarityFn("event", destinations.clarity.eventName || "apex_experiment_viewed");
|
|
445
|
+
}
|
|
446
|
+
if (destinations.clarity.upgrade && !clarityUpgradeSent) {
|
|
447
|
+
try {
|
|
448
|
+
clarityFn("upgrade", "apex_experiment");
|
|
449
|
+
clarityUpgradeSent = true;
|
|
450
|
+
} catch {
|
|
451
|
+
// Leave clarityUpgradeSent false so the upgrade retries on the next exposure.
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
rememberExposureDestination({ type: "clarity", status: "sent", at });
|
|
455
|
+
} catch (error) {
|
|
456
|
+
rememberExposureDestination({ type: "clarity", status: "failed", at, reason: error instanceof Error ? error.message : String(error) });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (destinations.contentsquare?.enabled) {
|
|
461
|
+
try {
|
|
462
|
+
const w = window as any;
|
|
463
|
+
const uxa = (w._uxa = Array.isArray(w._uxa) || (w._uxa && typeof w._uxa.push === "function") ? w._uxa : []);
|
|
464
|
+
const key = clarityTagKey(
|
|
465
|
+
destinations.contentsquare.keyPrefix ?? "apex ",
|
|
466
|
+
assignment.experimentName ?? assignment.experimentId,
|
|
467
|
+
).slice(0, 50);
|
|
468
|
+
uxa.push([
|
|
469
|
+
"trackDynamicVariable",
|
|
470
|
+
{
|
|
471
|
+
key,
|
|
472
|
+
value: assignment.isControl ? "control" : `variant-${assignment.index}`,
|
|
473
|
+
},
|
|
474
|
+
]);
|
|
475
|
+
rememberExposureDestination({ type: "contentsquare", status: "sent", at });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
rememberExposureDestination({ type: "contentsquare", status: "failed", at, reason: error instanceof Error ? error.message : String(error) });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (destinations.posthog?.enabled) {
|
|
482
|
+
try {
|
|
483
|
+
const posthog = (window as any).posthog;
|
|
484
|
+
if (!posthog || (typeof posthog !== "object" && typeof posthog !== "function") || typeof posthog.capture !== "function") {
|
|
485
|
+
rememberExposureDestination({ type: "posthog", status: "failed", at, reason: "posthog_missing" });
|
|
486
|
+
} else {
|
|
487
|
+
const mode = destinations.posthog.mode === "capture" || destinations.posthog.mode === "register"
|
|
488
|
+
? destinations.posthog.mode
|
|
489
|
+
: "both";
|
|
490
|
+
if (mode === "capture" || mode === "both") {
|
|
491
|
+
posthog.capture(destinations.posthog.eventName || "apex_experiment_viewed", payload);
|
|
492
|
+
}
|
|
493
|
+
if (mode === "register" || mode === "both") {
|
|
494
|
+
if (typeof posthog.register !== "function") {
|
|
495
|
+
rememberExposureDestination({ type: "posthog", status: "failed", at, reason: "posthog_register_missing" });
|
|
496
|
+
} else {
|
|
497
|
+
posthog.register({ [`apex_exp_${assignment.experimentId}`]: assignment.variationId });
|
|
498
|
+
rememberExposureDestination({ type: "posthog", status: "sent", at });
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
rememberExposureDestination({ type: "posthog", status: "sent", at });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
} catch (error) {
|
|
505
|
+
rememberExposureDestination({ type: "posthog", status: "failed", at, reason: error instanceof Error ? error.message : String(error) });
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
for (const queue of destinations.queues ?? []) {
|
|
510
|
+
if (queue.enabled === false) continue;
|
|
511
|
+
try {
|
|
512
|
+
pushExposureToQueue(queue, payload);
|
|
513
|
+
rememberExposureDestination({ type: `queue:${queue.globalName}`, status: "sent", at });
|
|
514
|
+
} catch (error) {
|
|
515
|
+
rememberExposureDestination({
|
|
516
|
+
type: `queue:${queue.globalName}`,
|
|
517
|
+
status: "failed",
|
|
518
|
+
at,
|
|
519
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export function ensureTrackerStarted(config: InitConfig): void {
|
|
526
|
+
if (state.tracker) return;
|
|
527
|
+
if (state.screenshotMode) return;
|
|
528
|
+
if (state.trackingDisabledByDnt) return;
|
|
529
|
+
const eventsEndpoint = getEventsEndpoint(config);
|
|
530
|
+
if (!eventsEndpoint) return;
|
|
531
|
+
|
|
532
|
+
state.tracker = new Tracker({
|
|
533
|
+
endpoint: eventsEndpoint,
|
|
534
|
+
proxyEventDelivery: config.proxyEventDelivery,
|
|
535
|
+
assetFallback: config.assetFallback,
|
|
536
|
+
shopId: config.shopId,
|
|
537
|
+
ingestShopId: config.ingestShopId ?? config.shopId,
|
|
538
|
+
ingestSignature: config.ingestSignature,
|
|
539
|
+
visitorId: state.userId ?? undefined,
|
|
540
|
+
getActiveAssignment: () => {
|
|
541
|
+
const a = state.assignments.length > 0 ? state.assignments[0] : null;
|
|
542
|
+
return a ? { experimentId: a.experimentId, variationId: a.variationId } : null;
|
|
543
|
+
},
|
|
544
|
+
buildPageExitEvents: buildPageExitEvents,
|
|
545
|
+
qaMode: state.qaMode,
|
|
546
|
+
qaSessionId: state.qaSessionId,
|
|
547
|
+
environment: config.environments?.activeKey || "production",
|
|
548
|
+
cohort: config.holdout_v1_enabled === true ? (state.holdout ? "holdout" : "active") : undefined,
|
|
549
|
+
holdoutConfigEpoch: holdoutConfigEpoch(config),
|
|
550
|
+
});
|
|
551
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("tracker", "Tracker started", { endpoint: eventsEndpoint, qaMode: state.qaMode });
|
|
552
|
+
state.tracker.start();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function exposureAssignmentKey(assignment: Assignment): string {
|
|
556
|
+
const entityId = assignment.kind === "personalization" && assignment.personalization
|
|
557
|
+
? assignment.personalization.id
|
|
558
|
+
: assignment.experimentId;
|
|
559
|
+
return `${entityId}:${assignment.variationId}:${assignment.assignmentEpoch ?? ""}`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function priceAssignmentAuthorizationKey(assignment: {
|
|
563
|
+
experimentId: string;
|
|
564
|
+
variationId: string;
|
|
565
|
+
assignmentEpoch?: number;
|
|
566
|
+
}): string {
|
|
567
|
+
return `${assignment.experimentId}:${assignment.variationId}:${assignment.assignmentEpoch ?? ""}`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function isPriceDisplayAssignmentAuthorized(assignment: Assignment): boolean {
|
|
571
|
+
const authorization = state.config?.commerce?.price?.authorization;
|
|
572
|
+
if (
|
|
573
|
+
authorization?.version !== 1 ||
|
|
574
|
+
!Number.isSafeInteger(authorization.revision) ||
|
|
575
|
+
authorization.revision <= 0 ||
|
|
576
|
+
!authorization.experimentIds?.includes(assignment.experimentId)
|
|
577
|
+
) return true;
|
|
578
|
+
// Inside a mixed experiment, a variation without any charged adjustment is
|
|
579
|
+
// display-only: it needs no cart proof even though its experiment is on
|
|
580
|
+
// the authorization plane.
|
|
581
|
+
const adjustments = state.config?.commerce?.price?.experiments
|
|
582
|
+
.find((experiment) => experiment.experiment_id === assignment.experimentId)
|
|
583
|
+
?.variants[assignment.variationId];
|
|
584
|
+
if (
|
|
585
|
+
adjustments &&
|
|
586
|
+
!adjustments.some((adjustment) => typeof adjustment.fixedPricePerUnit === "string")
|
|
587
|
+
) return true;
|
|
588
|
+
return state.authorizedPriceAssignmentKeys?.has(
|
|
589
|
+
priceAssignmentAuthorizationKey(assignment),
|
|
590
|
+
) === true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function isShippingDisplayAssignmentAuthorized(assignment: Assignment): boolean {
|
|
594
|
+
const authorization = state.config?.commerce?.shipping?.authorization;
|
|
595
|
+
if (
|
|
596
|
+
authorization?.version !== 1 ||
|
|
597
|
+
!Number.isSafeInteger(authorization.revision) ||
|
|
598
|
+
authorization.revision <= 0 ||
|
|
599
|
+
!authorization.experimentIds?.includes(assignment.experimentId)
|
|
600
|
+
) return true;
|
|
601
|
+
return state.authorizedShippingAssignmentKeys?.has(
|
|
602
|
+
priceAssignmentAuthorizationKey(assignment),
|
|
603
|
+
) === true;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function isOffersDisplayAssignmentAuthorized(assignment: Assignment): boolean {
|
|
607
|
+
const authorization = state.config?.commerce?.offers?.authorization;
|
|
608
|
+
if (
|
|
609
|
+
authorization?.version !== 1 ||
|
|
610
|
+
!Number.isSafeInteger(authorization.revision) ||
|
|
611
|
+
authorization.revision <= 0 ||
|
|
612
|
+
!authorization.experimentIds?.includes(assignment.experimentId)
|
|
613
|
+
) return true;
|
|
614
|
+
return state.authorizedOffersAssignmentKeys?.has(
|
|
615
|
+
priceAssignmentAuthorizationKey(assignment),
|
|
616
|
+
) === true;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export function giftSyncOptions(extra?: Partial<GiftSyncOptions>): GiftSyncOptions {
|
|
620
|
+
return {
|
|
621
|
+
// Proof comes from the CURRENT stamped offers plane, not config: a
|
|
622
|
+
// shared price+offers assignment whose offers proof was refused must
|
|
623
|
+
// not insert (or keep) a gift the Function will not discount.
|
|
624
|
+
isArmProven: (assignment) =>
|
|
625
|
+
state.authorizedOffersAssignmentKeys?.has(
|
|
626
|
+
priceAssignmentAuthorizationKey(assignment),
|
|
627
|
+
) === true,
|
|
628
|
+
...extra,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export /** Every artifact applyAssignments/applyAssignmentCode can render. */
|
|
633
|
+
function assignmentHasRenderableArtifacts(assignment: Assignment): boolean {
|
|
634
|
+
return !isAssignmentArtifactInactive(assignment) && !!(
|
|
635
|
+
assignment.mutations.length ||
|
|
636
|
+
assignment.css ||
|
|
637
|
+
assignment.js ||
|
|
638
|
+
assignment.experimentCss ||
|
|
639
|
+
assignment.experimentJs ||
|
|
640
|
+
assignment.externalCssUrl ||
|
|
641
|
+
assignment.externalJsUrl
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export /** Install offers display keys and render any DOM they just unlocked. */
|
|
646
|
+
function authorizeOffersDisplayAndRender(
|
|
647
|
+
authorizedAssignments: CartAssignment[],
|
|
648
|
+
): void {
|
|
649
|
+
const previous = state.authorizedOffersAssignmentKeys;
|
|
650
|
+
const next = new Set(
|
|
651
|
+
authorizedAssignments.map(priceAssignmentAuthorizationKey),
|
|
652
|
+
);
|
|
653
|
+
state.authorizedOffersAssignmentKeys = next;
|
|
654
|
+
// Re-apply only when authorization just unlocked withheld DOM for an
|
|
655
|
+
// offers-enforced assignment; the identity guard breaks the
|
|
656
|
+
// apply -> sync -> already-stamped -> authorize loop.
|
|
657
|
+
const offersEnforcedIds = new Set(
|
|
658
|
+
state.config?.commerce?.offers?.authorization?.experimentIds ?? [],
|
|
659
|
+
);
|
|
660
|
+
const newlyRenderable = state.assignments.some((assignment) =>
|
|
661
|
+
offersEnforcedIds.has(assignment.experimentId) &&
|
|
662
|
+
next.has(priceAssignmentAuthorizationKey(assignment)) &&
|
|
663
|
+
!previous?.has(priceAssignmentAuthorizationKey(assignment)) &&
|
|
664
|
+
assignmentHasRenderableArtifacts(assignment));
|
|
665
|
+
if (newlyRenderable) {
|
|
666
|
+
// Full reapply behind the established cleanup (the adoption pattern):
|
|
667
|
+
// without it, parallel already-rendered assignments would duplicate
|
|
668
|
+
// structural insert mutations and re-run custom JS un-reset.
|
|
669
|
+
teardownTransaction(allAppliedPagesWithModes(), { skipCartSync: true });
|
|
670
|
+
applyAssignments(state.assignments, state.activationMode);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
export /** Revoke the previous cart's offer grant and restore un-authorized DOM. */
|
|
675
|
+
function revokeOffersDisplayAuthorization(): void {
|
|
676
|
+
const previous = state.authorizedOffersAssignmentKeys;
|
|
677
|
+
if (!previous || previous.size === 0) return;
|
|
678
|
+
const offersEnforcedIds = new Set(
|
|
679
|
+
state.config?.commerce?.offers?.authorization?.experimentIds ?? [],
|
|
680
|
+
);
|
|
681
|
+
const hadRenderable = state.assignments.some((assignment) =>
|
|
682
|
+
offersEnforcedIds.has(assignment.experimentId) &&
|
|
683
|
+
previous.has(priceAssignmentAuthorizationKey(assignment)) &&
|
|
684
|
+
assignmentHasRenderableArtifacts(assignment));
|
|
685
|
+
state.authorizedOffersAssignmentKeys = new Set();
|
|
686
|
+
if (hadRenderable) {
|
|
687
|
+
// The new cart holds no stamp for these arms; retire their treatment
|
|
688
|
+
// (cleanup + withheld reapply) until its authorization is confirmed.
|
|
689
|
+
teardownTransaction(allAppliedPagesWithModes(), { skipCartSync: true });
|
|
690
|
+
applyAssignments(state.assignments, state.activationMode);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function exposureEligibleAssignments(config: InitConfig): Assignment[] {
|
|
695
|
+
return state.assignments
|
|
696
|
+
.filter(exposable)
|
|
697
|
+
.filter(
|
|
698
|
+
(assignment) =>
|
|
699
|
+
assignment.isControl ||
|
|
700
|
+
(isPriceDisplayAssignmentAuthorized(assignment) &&
|
|
701
|
+
isCommercePriceExposureEligible(config.commerce?.price, assignment)),
|
|
702
|
+
)
|
|
703
|
+
.filter(
|
|
704
|
+
(assignment) =>
|
|
705
|
+
isShippingDisplayAssignmentAuthorized(assignment) &&
|
|
706
|
+
isShippingExposureCartEligible(config, assignment),
|
|
707
|
+
)
|
|
708
|
+
.filter(isOffersDisplayAssignmentAuthorized);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function isShippingExposureCartEligible(
|
|
712
|
+
config: InitConfig,
|
|
713
|
+
assignment: Assignment,
|
|
714
|
+
): boolean {
|
|
715
|
+
const experiment = config.commerce?.shipping?.experiments.find(
|
|
716
|
+
(candidate) => candidate.experiment_id === assignment.experimentId,
|
|
717
|
+
);
|
|
718
|
+
if (!experiment) return true;
|
|
719
|
+
const exposure = classifyShippingExposure(
|
|
720
|
+
experiment.variants,
|
|
721
|
+
Boolean(assignment.isControl),
|
|
722
|
+
experiment.variants[assignment.variationId],
|
|
723
|
+
);
|
|
724
|
+
return (!exposure.needsTargetProof && !exposure.isCurrencyBound)
|
|
725
|
+
|| isShippingCartEligibilityConfirmed(assignment.experimentId);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function trackViewedAssignmentsAfterShippingEligibility(): void {
|
|
729
|
+
if (state.analyticsStarted && state.config && !trackingBlocked()) {
|
|
730
|
+
trackViewedAssignments(state.config);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
setCommercePriceExposureListener(
|
|
735
|
+
trackViewedAssignmentsAfterShippingEligibility,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
export const EMPTY_TARGETING_ATTRIBUTES: Record<string, unknown> = {};
|
|
739
|
+
|
|
740
|
+
export type TargetingEvaluationPass = {
|
|
741
|
+
attributes?: Record<string, unknown>;
|
|
742
|
+
persistSignals: boolean;
|
|
743
|
+
sourceAttributes: Record<string, unknown>;
|
|
744
|
+
url: string;
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
export let activeTargetingEvaluationPass: TargetingEvaluationPass | undefined;
|
|
748
|
+
|
|
749
|
+
export function createTargetingEvaluationPass(
|
|
750
|
+
sourceAttributes: Record<string, unknown>,
|
|
751
|
+
url: string,
|
|
752
|
+
persistSignals: boolean,
|
|
753
|
+
): TargetingEvaluationPass {
|
|
754
|
+
return { persistSignals, sourceAttributes, url };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function targetingAttributesForPass(
|
|
758
|
+
pass: TargetingEvaluationPass | undefined,
|
|
759
|
+
sourceAttributes: Record<string, unknown>,
|
|
760
|
+
url: string,
|
|
761
|
+
persistSignals: boolean,
|
|
762
|
+
): Record<string, unknown> {
|
|
763
|
+
if (
|
|
764
|
+
pass &&
|
|
765
|
+
pass.url === url &&
|
|
766
|
+
pass.sourceAttributes === sourceAttributes
|
|
767
|
+
) {
|
|
768
|
+
return pass.attributes ??= buildTargetingAttributes(sourceAttributes, url, {
|
|
769
|
+
persistSignals,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return buildTargetingAttributes(sourceAttributes, url, { persistSignals });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export function trackViewedAssignments(
|
|
776
|
+
config: InitConfig,
|
|
777
|
+
onlyAssignments?: Assignment[],
|
|
778
|
+
targetingPass?: TargetingEvaluationPass,
|
|
779
|
+
): void {
|
|
780
|
+
if (config.autoTrack === false) return;
|
|
781
|
+
const eligibleAssignments = exposureEligibleAssignments(config);
|
|
782
|
+
const assignments = onlyAssignments
|
|
783
|
+
? eligibleAssignments.filter((candidate) => onlyAssignments.some((assignment) =>
|
|
784
|
+
assignment.experimentId === candidate.experimentId && assignment.variationId === candidate.variationId))
|
|
785
|
+
: eligibleAssignments;
|
|
786
|
+
if (assignments.length === 0) return;
|
|
787
|
+
const url = config.url ?? (typeof location !== "undefined" ? location.href : "");
|
|
788
|
+
const sourceAttributes = config.attributes ?? EMPTY_TARGETING_ATTRIBUTES;
|
|
789
|
+
const persistSignals = persistentStorageAllowed();
|
|
790
|
+
const attributes = targetingAttributesForPass(
|
|
791
|
+
targetingPass,
|
|
792
|
+
sourceAttributes,
|
|
793
|
+
url,
|
|
794
|
+
persistSignals,
|
|
795
|
+
);
|
|
796
|
+
const exposureSignals = buildExposureSignalContext({
|
|
797
|
+
attributes,
|
|
798
|
+
customSignals: config.signals,
|
|
799
|
+
});
|
|
800
|
+
assignments.forEach((a) => {
|
|
801
|
+
const exposureKey = exposureAssignmentKey(a);
|
|
802
|
+
if (state.exposedAssignmentKeys.has(exposureKey)) return;
|
|
803
|
+
state.exposedAssignmentKeys.add(exposureKey);
|
|
804
|
+
if (a.kind === "personalization" && a.personalization) {
|
|
805
|
+
track(
|
|
806
|
+
"personalization_rendered",
|
|
807
|
+
{
|
|
808
|
+
index: a.index,
|
|
809
|
+
...(exposureSignals ? { signals: exposureSignals } : {}),
|
|
810
|
+
entity_kind: "personalization",
|
|
811
|
+
personalization_id: a.personalization.id,
|
|
812
|
+
personalization_key: a.personalization.key,
|
|
813
|
+
personalization_rule_id: a.personalization.ruleId,
|
|
814
|
+
personalization_experience_id: a.personalization.experienceId,
|
|
815
|
+
personalization_surface: a.personalization.surface,
|
|
816
|
+
personalization_template_key: a.personalization.templateKey,
|
|
817
|
+
},
|
|
818
|
+
a.experimentId,
|
|
819
|
+
a.variationId,
|
|
820
|
+
);
|
|
821
|
+
} else {
|
|
822
|
+
track(
|
|
823
|
+
"experiment_viewed",
|
|
824
|
+
{
|
|
825
|
+
index: a.index,
|
|
826
|
+
...(exposureSignals ? { signals: exposureSignals } : {}),
|
|
827
|
+
},
|
|
828
|
+
a.experimentId,
|
|
829
|
+
a.variationId,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
emitExposureDestinations(a, config);
|
|
833
|
+
rememberOrderExposure(a);
|
|
834
|
+
config.onExperimentViewed?.(a);
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export function sameStringSet(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
|
|
839
|
+
if (left.size !== right.size) return false;
|
|
840
|
+
for (const value of left) {
|
|
841
|
+
if (!right.has(value)) return false;
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function setupGoalTracking(
|
|
847
|
+
experiments: Experiment[],
|
|
848
|
+
url = state.wiredGoalUrl ?? state.config?.url ??
|
|
849
|
+
(typeof location !== "undefined" ? location.href : "https://_"),
|
|
850
|
+
activePageIdsForGoals = activePageIds(),
|
|
851
|
+
): void {
|
|
852
|
+
// Goals listen on the assigned plane. Their own Page scope decides where
|
|
853
|
+
// they convert; exposure and commerce remain gated by the render plane.
|
|
854
|
+
const assignments = state.assignments.filter(attributable);
|
|
855
|
+
state.wiredGoalKeys = goalWiringKeys(
|
|
856
|
+
assignments,
|
|
857
|
+
experiments,
|
|
858
|
+
activePageIdsForGoals,
|
|
859
|
+
);
|
|
860
|
+
state.wiredGoalUrl = url;
|
|
861
|
+
cleanupGoals();
|
|
862
|
+
setupGoals(assignments, experiments, (goalId, assignment, data) => {
|
|
863
|
+
const originalTimestamp = typeof data?.early_interaction_timestamp === "number"
|
|
864
|
+
? data.early_interaction_timestamp
|
|
865
|
+
: undefined;
|
|
866
|
+
const eventData: Record<string, unknown> = { goalId, ...data };
|
|
867
|
+
delete eventData.early_interaction_timestamp;
|
|
868
|
+
trackEvent("goal", eventData, assignment.experimentId, assignment.variationId, undefined, false, originalTimestamp);
|
|
869
|
+
}, activePageIdsForGoals);
|
|
870
|
+
drainBootstrapEarlyInteractions({
|
|
871
|
+
// Bootstrap interactions were captured before mutation delivery could
|
|
872
|
+
// resolve. Keep them available to the assignment that existed at click
|
|
873
|
+
// time; triggerGoal applies the normal count-once and goal validation.
|
|
874
|
+
assignments: state.assignments.filter((assignment) => {
|
|
875
|
+
const key = exposureAssignmentKey(assignment);
|
|
876
|
+
return assignment.experimentId.length > 0 && assignment.variationId.length > 0 &&
|
|
877
|
+
!state.contaminatedAssignmentKeys.has(key);
|
|
878
|
+
}),
|
|
879
|
+
experiments,
|
|
880
|
+
activePageIds: activePageIdsForGoals,
|
|
881
|
+
requireConsent: state.config?.requireConsent,
|
|
882
|
+
consentModeEnabled: state.config?.consentMode?.enabled,
|
|
883
|
+
consentProvider: state.config?.consentMode?.provider,
|
|
884
|
+
consentRequiredGroup: state.config?.consentMode?.requiredGroup,
|
|
885
|
+
consentGetter: state.config?.consentMode?.getter,
|
|
886
|
+
respectDoNotTrack: state.config?.dataProtection?.respectDoNotTrack,
|
|
887
|
+
consume: (goalId, assignment, timestamp, kind, pageUrl) => triggerGoal(
|
|
888
|
+
goalId,
|
|
889
|
+
[assignment],
|
|
890
|
+
experiments,
|
|
891
|
+
{ early_interaction: true, early_page_url: pageUrl, early_interaction_kind: kind, early_interaction_timestamp: timestamp },
|
|
892
|
+
),
|
|
893
|
+
});
|
|
894
|
+
if (state.tracker && state.config) {
|
|
895
|
+
drainEarlyInteractions({
|
|
896
|
+
configRevision: state.config.delivery?.revision,
|
|
897
|
+
sessionId: state.tracker.getSessionId(),
|
|
898
|
+
storageMode: state.config.dataProtection?.storageMode,
|
|
899
|
+
experiments,
|
|
900
|
+
requireConsent: state.config.requireConsent,
|
|
901
|
+
consentModeEnabled: state.config.consentMode?.enabled,
|
|
902
|
+
consentProvider: state.config.consentMode?.provider,
|
|
903
|
+
consentRequiredGroup: state.config.consentMode?.requiredGroup,
|
|
904
|
+
consentGetter: state.config.consentMode?.getter,
|
|
905
|
+
respectDoNotTrack: state.config.dataProtection?.respectDoNotTrack,
|
|
906
|
+
forcedExperimentIds: Object.keys(state.forceMap ?? {}),
|
|
907
|
+
consume: (entry) => {
|
|
908
|
+
// The origin stamp is authoritative across navigation, but still route
|
|
909
|
+
// through the normal goal implementation for count-once and callbacks.
|
|
910
|
+
triggerGoal(
|
|
911
|
+
entry.goalId,
|
|
912
|
+
[{ experimentId: entry.experimentId, variationId: entry.variationId } as Assignment],
|
|
913
|
+
experiments,
|
|
914
|
+
{ ...entry.data, early_page_url: entry.pageUrl, early_interaction_kind: entry.kind },
|
|
915
|
+
);
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function setupHeatmapTracking(assignments: Assignment[], experiments: Experiment[]): void {
|
|
922
|
+
if (!FEATURE_HEATMAP) return;
|
|
923
|
+
if (!state.tracker || trackingBlocked()) return;
|
|
924
|
+
if (state.heatmapController) {
|
|
925
|
+
state.heatmapController.update(assignments, experiments);
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
import("./heatmap").then(({ startHeatmapCapture }) => {
|
|
929
|
+
if (!state.tracker || trackingBlocked() || state.heatmapController) return;
|
|
930
|
+
state.heatmapController = startHeatmapCapture(assignments, experiments, track);
|
|
931
|
+
}).catch(() => {});
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export function cleanupHeatmapTracking(): void {
|
|
935
|
+
if (!state.heatmapController) return;
|
|
936
|
+
state.heatmapController.stop();
|
|
937
|
+
state.heatmapController = null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export function setupReplayTracking(assignments: Assignment[], experiments: Experiment[]): void {
|
|
941
|
+
if (!FEATURE_REPLAY) return;
|
|
942
|
+
if (!state.tracker || !state.config || trackingBlocked()) {
|
|
943
|
+
cleanupReplayTracking();
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (state.replayController?.isStopped()) {
|
|
947
|
+
state.replayController = null;
|
|
948
|
+
}
|
|
949
|
+
if (state.replayController) {
|
|
950
|
+
state.replayController.update(assignments, experiments);
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (state.replayStartPromise) return;
|
|
954
|
+
|
|
955
|
+
const generation = state.replayGeneration;
|
|
956
|
+
const config = state.config;
|
|
957
|
+
const tracker = state.tracker;
|
|
958
|
+
const replayBlocked = () =>
|
|
959
|
+
trackingBlocked()
|
|
960
|
+
|| generation !== state.replayGeneration
|
|
961
|
+
|| state.tracker !== tracker;
|
|
962
|
+
const recorderUrl = resolveReplayRecorderUrl({
|
|
963
|
+
configuredUrl: config.replayRecorderUrl,
|
|
964
|
+
scriptSrc: SDK_SCRIPT_SRC,
|
|
965
|
+
pageUrl: typeof location !== "undefined" ? location.href : undefined,
|
|
966
|
+
});
|
|
967
|
+
if (!config.endpoint || !recorderUrl) return;
|
|
968
|
+
let startPromise: Promise<void>;
|
|
969
|
+
startPromise = startReplayCapture({
|
|
970
|
+
assignments: state.assignments,
|
|
971
|
+
experiments: config.experiments ?? [],
|
|
972
|
+
recorderUrl,
|
|
973
|
+
endpoint: config.endpoint,
|
|
974
|
+
shopId: config.ingestShopId ?? config.shopId ?? "",
|
|
975
|
+
visitorId: state.userId ?? "",
|
|
976
|
+
sessionId: tracker.getSessionId(),
|
|
977
|
+
environment: config.environments?.activeKey || "production",
|
|
978
|
+
qaMode: state.qaMode,
|
|
979
|
+
qaSessionId: state.qaSessionId,
|
|
980
|
+
sdkVersion: config.sdkVersion || "unknown",
|
|
981
|
+
ingestSignature: config.ingestSignature,
|
|
982
|
+
isTrackingBlocked: replayBlocked,
|
|
983
|
+
})
|
|
984
|
+
.then((controller) => {
|
|
985
|
+
if (!controller) return;
|
|
986
|
+
if (replayBlocked()) {
|
|
987
|
+
controller.stop(true);
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
controller.update(state.assignments, state.config?.experiments ?? []);
|
|
991
|
+
state.replayController = controller;
|
|
992
|
+
})
|
|
993
|
+
.catch(() => {})
|
|
994
|
+
.finally(() => {
|
|
995
|
+
if (state.replayStartPromise === startPromise) state.replayStartPromise = null;
|
|
996
|
+
if (
|
|
997
|
+
generation !== state.replayGeneration
|
|
998
|
+
&& state.tracker
|
|
999
|
+
&& state.config
|
|
1000
|
+
&& !trackingBlocked()
|
|
1001
|
+
) {
|
|
1002
|
+
setupReplayTracking(state.assignments, state.config.experiments ?? []);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
state.replayStartPromise = startPromise;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
export function cleanupReplayTracking(): void {
|
|
1009
|
+
state.replayGeneration += 1;
|
|
1010
|
+
state.replayController?.stop(true);
|
|
1011
|
+
state.replayController = null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function setupSemanticTracking(config: InitConfig): void {
|
|
1015
|
+
if (!state.tracker || trackingBlocked()) return;
|
|
1016
|
+
if (state.semanticController) return;
|
|
1017
|
+
import("./semantic").then(({ startSemanticCapture }) => {
|
|
1018
|
+
if (!state.tracker || trackingBlocked() || state.semanticController) return;
|
|
1019
|
+
state.semanticController = startSemanticCapture(config.semanticTracking, track);
|
|
1020
|
+
}).catch(() => {});
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export function cleanupSemanticTracking(): void {
|
|
1024
|
+
if (!state.semanticController) return;
|
|
1025
|
+
state.semanticController.stop();
|
|
1026
|
+
state.semanticController = null;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function buildExperimentBehaviorEvents(
|
|
1030
|
+
type: "experiment_pageview" | "experiment_page_exit",
|
|
1031
|
+
data?: Record<string, unknown>,
|
|
1032
|
+
): TrackEvent[] {
|
|
1033
|
+
return state.assignments.filter(exposable).map((assignment) => ({
|
|
1034
|
+
type,
|
|
1035
|
+
data: withAssignmentEpochData(data, assignment.experimentId, assignment.variationId),
|
|
1036
|
+
url: cleanUrl(),
|
|
1037
|
+
experimentId: assignment.experimentId,
|
|
1038
|
+
variationId: assignment.variationId,
|
|
1039
|
+
}));
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export function buildPageExitEvents(type: string, data?: Record<string, unknown>): TrackEvent[] {
|
|
1043
|
+
const a = state.assignments.find(exposable) ?? null;
|
|
1044
|
+
return [
|
|
1045
|
+
{
|
|
1046
|
+
type,
|
|
1047
|
+
data: withAssignmentEpochData(data, a?.experimentId, a?.variationId),
|
|
1048
|
+
url: cleanUrl(),
|
|
1049
|
+
experimentId: a?.experimentId,
|
|
1050
|
+
variationId: a?.variationId,
|
|
1051
|
+
},
|
|
1052
|
+
...buildExperimentBehaviorEvents("experiment_page_exit", data),
|
|
1053
|
+
];
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export /** Track analytics events with legacy global events plus per-assignment behavior events. */
|
|
1057
|
+
function analyticsTrack(type: string, data?: Record<string, unknown>): void {
|
|
1058
|
+
if (trackingBlocked()) return;
|
|
1059
|
+
if (!state.tracker) return;
|
|
1060
|
+
// Attach identity only when an assignment is exposable. The shop-level
|
|
1061
|
+
// event stays unassigned while all held assignments remain ineligible.
|
|
1062
|
+
const a = state.assignments.find(exposable) ?? null;
|
|
1063
|
+
const event: TrackEvent = {
|
|
1064
|
+
type,
|
|
1065
|
+
data: withAssignmentEpochData(data, a?.experimentId, a?.variationId),
|
|
1066
|
+
url: cleanUrl(),
|
|
1067
|
+
experimentId: a?.experimentId,
|
|
1068
|
+
variationId: a?.variationId,
|
|
1069
|
+
};
|
|
1070
|
+
__DRIP_FEATURE_DEVTOOLS__ && dtLog("track", type, {
|
|
1071
|
+
experimentId: a?.experimentId,
|
|
1072
|
+
variationId: a?.variationId,
|
|
1073
|
+
...data,
|
|
1074
|
+
});
|
|
1075
|
+
state.tracker.track(event);
|
|
1076
|
+
if (type === "pageview") {
|
|
1077
|
+
if (state.config) scheduleOrderIdentityBeacon(state.config);
|
|
1078
|
+
for (const behaviorEvent of buildExperimentBehaviorEvents("experiment_pageview", data)) {
|
|
1079
|
+
state.tracker.track(behaviorEvent);
|
|
1080
|
+
}
|
|
1081
|
+
} else if (type === "page_exit") {
|
|
1082
|
+
for (const behaviorEvent of buildExperimentBehaviorEvents("experiment_page_exit", data)) {
|
|
1083
|
+
state.tracker.track(behaviorEvent);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
export function syncLoadDiagnosticsToPerf(): void {
|
|
1089
|
+
if (typeof window === "undefined") return;
|
|
1090
|
+
const perfWindow = window as DripPerfWindow;
|
|
1091
|
+
const perf = perfWindow.__dripPerf = perfWindow.__dripPerf ?? {};
|
|
1092
|
+
perf.load_stage = state.load.stage;
|
|
1093
|
+
perf.config_fetch_status = state.load.configFetchStatus;
|
|
1094
|
+
if (state.load.configFetchAttempts !== undefined) perf.config_fetch_attempts = state.load.configFetchAttempts;
|
|
1095
|
+
if (state.load.configFetchMs !== undefined) perf.config_fetch_ms = state.load.configFetchMs;
|
|
1096
|
+
if (state.load.assignmentReadyMs !== undefined) perf.assignment_ready_ms = state.load.assignmentReadyMs;
|
|
1097
|
+
if (state.load.pendingMutations !== undefined) perf.pending_mutations = state.load.pendingMutations;
|
|
1098
|
+
if (state.load.appliedMutations !== undefined) perf.applied_mutations = state.load.appliedMutations;
|
|
1099
|
+
if (state.load.missedMutations !== undefined) perf.missed_mutations = state.load.missedMutations;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
export function updateLoadDiagnostics(patch: Partial<RuntimeLoadDiagnostic> & { stage: string }): void {
|
|
1103
|
+
state.load = {
|
|
1104
|
+
...state.load,
|
|
1105
|
+
...patch,
|
|
1106
|
+
updatedAt: Date.now(),
|
|
1107
|
+
};
|
|
1108
|
+
syncLoadDiagnosticsToPerf();
|
|
1109
|
+
try {
|
|
1110
|
+
document.dispatchEvent(new CustomEvent("drip_runtime_status", { detail: { ...state.load } }));
|
|
1111
|
+
} catch {
|
|
1112
|
+
// ignore event dispatch failures
|
|
1113
|
+
}
|
|
1114
|
+
__DRIP_FEATURE_DEVTOOLS__ && debugLog("trace", `Runtime status: ${patch.stage}`, patch as Record<string, unknown>);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export function resetLoadDiagnostics(stage = "boot"): void {
|
|
1118
|
+
const now = Date.now();
|
|
1119
|
+
state.load = {
|
|
1120
|
+
stage,
|
|
1121
|
+
startedAt: now,
|
|
1122
|
+
updatedAt: now,
|
|
1123
|
+
configFetchStatus: "idle",
|
|
1124
|
+
configFetchAttempts: 0,
|
|
1125
|
+
appliedMutations: 0,
|
|
1126
|
+
missedMutations: 0,
|
|
1127
|
+
};
|
|
1128
|
+
syncLoadDiagnosticsToPerf();
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
export function queuePerfEvent(data: PendingPerfEvent): void {
|
|
1132
|
+
if (trackingBlocked()) return;
|
|
1133
|
+
state.pendingPerf = {
|
|
1134
|
+
...data,
|
|
1135
|
+
config_fetch_ms: state.load.configFetchMs,
|
|
1136
|
+
config_fetch_attempts: state.load.configFetchAttempts,
|
|
1137
|
+
config_fetch_status: state.load.configFetchStatus,
|
|
1138
|
+
config_fetch_status_code: state.load.configFetchStatusCode,
|
|
1139
|
+
mutation_apply_pending: state.load.pendingMutations,
|
|
1140
|
+
mutation_apply_applied: state.load.appliedMutations,
|
|
1141
|
+
mutation_apply_missed: state.load.missedMutations,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
export type RuntimeInstallClaim = {
|
|
1146
|
+
installSurface: string;
|
|
1147
|
+
sdkVersion: string;
|
|
1148
|
+
runtimeProfile: string;
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
export const RUNTIME_INSTALL_CLAIM_KEY = "__dripRuntimeInstallClaim";
|
|
1152
|
+
export const RUNTIME_INSTALL_CONFLICT_KEY = "__dripRuntimeInstallConflictEmitted";
|
|
1153
|
+
|
|
1154
|
+
export function telemetryLabel(value: unknown, fallback = "unknown"): string {
|
|
1155
|
+
if (typeof value !== "string") return fallback;
|
|
1156
|
+
const cleaned = value.trim().slice(0, 120);
|
|
1157
|
+
return cleaned || fallback;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export function readRuntimeInstallClaim(): RuntimeInstallClaim | null {
|
|
1161
|
+
const value = (globalThis as Record<string, unknown>)[RUNTIME_INSTALL_CLAIM_KEY];
|
|
1162
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1163
|
+
const record = value as Record<string, unknown>;
|
|
1164
|
+
return {
|
|
1165
|
+
installSurface: telemetryLabel(record.installSurface, "direct_script"),
|
|
1166
|
+
sdkVersion: telemetryLabel(record.sdkVersion),
|
|
1167
|
+
runtimeProfile: telemetryLabel(record.runtimeProfile, "full"),
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
export function rememberRuntimeInstallClaim(config: InitConfig): RuntimeInstallClaim {
|
|
1172
|
+
const claim = {
|
|
1173
|
+
installSurface: telemetryLabel(config.installSurface, "direct_script"),
|
|
1174
|
+
sdkVersion: telemetryLabel(config.sdkVersion),
|
|
1175
|
+
runtimeProfile: telemetryLabel(config.runtimeProfile, "full"),
|
|
1176
|
+
};
|
|
1177
|
+
const runtimeGlobal = globalThis as Record<string, unknown>;
|
|
1178
|
+
runtimeGlobal[RUNTIME_INSTALL_CLAIM_KEY] = claim;
|
|
1179
|
+
delete runtimeGlobal[RUNTIME_INSTALL_CONFLICT_KEY];
|
|
1180
|
+
return claim;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export function claimInstallConflictEmission(): boolean {
|
|
1184
|
+
const runtimeGlobal = globalThis as Record<string, unknown>;
|
|
1185
|
+
if (runtimeGlobal[RUNTIME_INSTALL_CONFLICT_KEY] === true) return false;
|
|
1186
|
+
runtimeGlobal[RUNTIME_INSTALL_CONFLICT_KEY] = true;
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export function sdkAttributionData(config: InitConfig): Record<string, unknown> {
|
|
1191
|
+
const firstInstall = readRuntimeInstallClaim();
|
|
1192
|
+
const snippetRevision = config.delivery?.revision;
|
|
1193
|
+
return {
|
|
1194
|
+
sdk_version: telemetryLabel(config.sdkVersion),
|
|
1195
|
+
sdk_delivery_mode: telemetryLabel(config.sdkDeliveryMode),
|
|
1196
|
+
sdk_delivery_source: telemetryLabel(config.sdkDeliverySource),
|
|
1197
|
+
sdk_policy_revision: telemetryLabel(config.sdkPolicyRevision),
|
|
1198
|
+
snippet_revision: typeof snippetRevision === "number" && Number.isFinite(snippetRevision)
|
|
1199
|
+
? Math.max(0, Math.floor(snippetRevision))
|
|
1200
|
+
: undefined,
|
|
1201
|
+
runtime_profile: telemetryLabel(config.runtimeProfile, "full"),
|
|
1202
|
+
sdk_variant: telemetryLabel(config.runtimeFeatures?.sdkVariant, "live"),
|
|
1203
|
+
install_surface: telemetryLabel(config.installSurface, "direct_script"),
|
|
1204
|
+
first_install_surface: firstInstall?.installSurface ?? telemetryLabel(config.installSurface, "direct_script"),
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
export function sendRuntimeEventNow(
|
|
1209
|
+
config: InitConfig,
|
|
1210
|
+
type: string,
|
|
1211
|
+
data: Record<string, unknown>,
|
|
1212
|
+
userId?: string | null,
|
|
1213
|
+
): void {
|
|
1214
|
+
if (trackingBlocked()) return;
|
|
1215
|
+
if (state.screenshotMode) return;
|
|
1216
|
+
const hydratedConfig = state.config && state.config.shopId === config.shopId ? state.config : config;
|
|
1217
|
+
const endpoint = getEventsEndpoint(config);
|
|
1218
|
+
if (!endpoint || !config.shopId) return;
|
|
1219
|
+
try {
|
|
1220
|
+
const tracker = new Tracker({
|
|
1221
|
+
endpoint,
|
|
1222
|
+
proxyEventDelivery: hydratedConfig.proxyEventDelivery,
|
|
1223
|
+
assetFallback: hydratedConfig.assetFallback,
|
|
1224
|
+
shopId: config.shopId,
|
|
1225
|
+
ingestShopId: config.ingestShopId ?? config.shopId,
|
|
1226
|
+
ingestSignature: config.ingestSignature,
|
|
1227
|
+
visitorId: userId ?? undefined,
|
|
1228
|
+
flushIntervalMs: 60_000,
|
|
1229
|
+
maxBatchSize: 1,
|
|
1230
|
+
qaMode: state.qaMode,
|
|
1231
|
+
qaSessionId: state.qaSessionId,
|
|
1232
|
+
cohort: state.config?.holdout_v1_enabled === true ? (state.holdout ? "holdout" : "active") : undefined,
|
|
1233
|
+
holdoutConfigEpoch: holdoutConfigEpoch(state.config),
|
|
1234
|
+
environment: config.environments?.activeKey || "production",
|
|
1235
|
+
});
|
|
1236
|
+
tracker.track({
|
|
1237
|
+
type,
|
|
1238
|
+
data: { ...sdkAttributionData(config), ...data },
|
|
1239
|
+
url: cleanUrl(),
|
|
1240
|
+
});
|
|
1241
|
+
void tracker.flush().finally(() => tracker.stop());
|
|
1242
|
+
} catch {
|
|
1243
|
+
// best-effort runtime telemetry
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
export function sendRuntimePerfNow(
|
|
1248
|
+
config: InitConfig,
|
|
1249
|
+
data: Record<string, unknown>,
|
|
1250
|
+
userId?: string | null,
|
|
1251
|
+
): void {
|
|
1252
|
+
sendRuntimeEventNow(config, "sdk_perf", data, userId);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
export function readDripPerf(): Record<string, unknown> {
|
|
1256
|
+
return ((window as DripPerfWindow).__dripPerf ?? {}) as Record<string, unknown>;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
export function markDripPerf(name: string): number {
|
|
1260
|
+
const perfWindow = window as DripPerfWindow;
|
|
1261
|
+
const perf = perfWindow.__dripPerf = perfWindow.__dripPerf ?? {};
|
|
1262
|
+
const current = perf[name];
|
|
1263
|
+
if (typeof current === "number" && Number.isFinite(current)) return current;
|
|
1264
|
+
const now = Date.now();
|
|
1265
|
+
perf[name] = now;
|
|
1266
|
+
return now;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export function numberPerf(perf: Record<string, unknown>, name: string): number | undefined {
|
|
1270
|
+
const value = perf[name];
|
|
1271
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
export function coexistencePerfFields(perf: Record<string, unknown>): Partial<PendingPerfEvent> {
|
|
1275
|
+
return {
|
|
1276
|
+
...getCwvMetrics(),
|
|
1277
|
+
hydration_detected_at: numberPerf(perf, "hydration_detected_at"),
|
|
1278
|
+
apply_delayed_ms: numberPerf(perf, "apply_delayed_ms"),
|
|
1279
|
+
hydration_timeout: perf.hydration_timeout === true,
|
|
1280
|
+
hold_timeout: perf.hold_timeout === true,
|
|
1281
|
+
reapply_count: numberPerf(perf, "reapply_count") ?? 0,
|
|
1282
|
+
reassert_loop_detected: perf.reassert_loop_detected === true,
|
|
1283
|
+
early_apply_at: numberPerf(perf, "early_apply_at"),
|
|
1284
|
+
reasserts_count: numberPerf(perf, "reasserts_count") ?? 0,
|
|
1285
|
+
revert_loops_detected: numberPerf(perf, "revert_loops_detected") ?? 0,
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
export function buildPerfEvent(config: InitConfig, timing: {
|
|
1290
|
+
eventKind: "init" | "route_change";
|
|
1291
|
+
assignmentReadyMs: number;
|
|
1292
|
+
initTotalMs?: number;
|
|
1293
|
+
phases?: Partial<Record<InitPhaseKey, number>>;
|
|
1294
|
+
includesConfigFetch?: boolean;
|
|
1295
|
+
predecisionMismatches?: number;
|
|
1296
|
+
blankScreenMs: number;
|
|
1297
|
+
assignmentReadyAt?: number;
|
|
1298
|
+
}): PendingPerfEvent {
|
|
1299
|
+
const perf = readDripPerf();
|
|
1300
|
+
const assignmentReadyAt = timing.assignmentReadyAt ?? Date.now();
|
|
1301
|
+
const antiFlickerInsertedAt = numberPerf(perf, "anti_flicker_inserted_at");
|
|
1302
|
+
const revealAt = numberPerf(perf, "reveal_at");
|
|
1303
|
+
const phases = timing.phases ?? {};
|
|
1304
|
+
const measuredPhaseMs = Object.values(phases).reduce((total, value) => total + (value ?? 0), 0);
|
|
1305
|
+
const configFetchMs = timing.includesConfigFetch ? state.load.configFetchMs ?? 0 : 0;
|
|
1306
|
+
const unattributedInitMs = timing.initTotalMs === undefined
|
|
1307
|
+
? undefined
|
|
1308
|
+
: boundedPerfDuration(timing.initTotalMs - measuredPhaseMs - configFetchMs);
|
|
1309
|
+
const mutationCount = state.assignments.reduce(
|
|
1310
|
+
(total, assignment) => total + assignment.mutations.length,
|
|
1311
|
+
0,
|
|
1312
|
+
);
|
|
1313
|
+
const attribution = sdkAttributionData(config);
|
|
1314
|
+
const deliveryTiming = collectDeliveryTiming(numberPerf(readDripPerf(), "sdk_init_start"), SDK_SCRIPT_SRC);
|
|
1315
|
+
return {
|
|
1316
|
+
perf_event_kind: timing.eventKind,
|
|
1317
|
+
assignment_ready_ms: timing.assignmentReadyMs,
|
|
1318
|
+
init_total_ms: timing.initTotalMs,
|
|
1319
|
+
guard_resolve_ms: phases.guard_resolve_ms,
|
|
1320
|
+
consent_identity_ms: phases.consent_identity_ms,
|
|
1321
|
+
runtime_setup_ms: phases.runtime_setup_ms,
|
|
1322
|
+
targeting_assignment_ms: phases.targeting_assignment_ms,
|
|
1323
|
+
mutation_finalize_ms: phases.mutation_finalize_ms,
|
|
1324
|
+
predecision_mismatch_count: timing.predecisionMismatches ?? 0,
|
|
1325
|
+
unattributed_init_ms: unattributedInitMs,
|
|
1326
|
+
// domContentLoadedEventStart - navigationStart. This is page readiness,
|
|
1327
|
+
// not the Apex anti-flicker hold (hidden_duration_ms is that metric).
|
|
1328
|
+
dom_content_loaded_ms: timing.blankScreenMs,
|
|
1329
|
+
blank_screen_ms: timing.blankScreenMs,
|
|
1330
|
+
platform: state.platform,
|
|
1331
|
+
config_source: state.configSource,
|
|
1332
|
+
activation_mode: state.activationMode,
|
|
1333
|
+
sdk_version: String(attribution.sdk_version),
|
|
1334
|
+
sdk_delivery_mode: String(attribution.sdk_delivery_mode),
|
|
1335
|
+
sdk_delivery_source: String(attribution.sdk_delivery_source),
|
|
1336
|
+
sdk_policy_revision: String(attribution.sdk_policy_revision),
|
|
1337
|
+
snippet_revision: typeof attribution.snippet_revision === "number" ? attribution.snippet_revision : undefined,
|
|
1338
|
+
runtime_profile: String(attribution.runtime_profile),
|
|
1339
|
+
sdk_variant: String(attribution.sdk_variant),
|
|
1340
|
+
...deliveryTiming,
|
|
1341
|
+
experiment_count: config.experiments?.length ?? 0,
|
|
1342
|
+
assignment_count: state.assignments.length,
|
|
1343
|
+
mutation_count: mutationCount,
|
|
1344
|
+
script_exec_start: numberPerf(perf, "script_exec_start"),
|
|
1345
|
+
anti_flicker_inserted_at: antiFlickerInsertedAt,
|
|
1346
|
+
assignment_ready_at: assignmentReadyAt,
|
|
1347
|
+
first_mutation_at: numberPerf(perf, "first_mutation_at"),
|
|
1348
|
+
reveal_at: revealAt,
|
|
1349
|
+
hidden_duration_ms: antiFlickerInsertedAt && revealAt ? Math.max(0, revealAt - antiFlickerInsertedAt) : undefined,
|
|
1350
|
+
current_page_hide_selector_count: numberPerf(perf, "current_page_hide_selector_count"),
|
|
1351
|
+
delivery_state: typeof config.deliveryState === "string" ? config.deliveryState : undefined,
|
|
1352
|
+
script_cache_source: typeof config.scriptCacheSource === "string" ? config.scriptCacheSource : undefined,
|
|
1353
|
+
install_surface: String(attribution.install_surface),
|
|
1354
|
+
first_install_surface: String(attribution.first_install_surface),
|
|
1355
|
+
...coexistencePerfFields(perf),
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
export function flushPendingPerf(): void {
|
|
1360
|
+
if (!state.pendingPerf || !state.tracker) return;
|
|
1361
|
+
const pendingPerf = state.pendingPerf;
|
|
1362
|
+
state.pendingPerf = null;
|
|
1363
|
+
const a = state.assignments.length > 0 ? state.assignments[0] : null;
|
|
1364
|
+
const perf = readDripPerf();
|
|
1365
|
+
const event: TrackEvent = {
|
|
1366
|
+
type: "sdk_perf",
|
|
1367
|
+
data: withAssignmentEpochData({
|
|
1368
|
+
...pendingPerf,
|
|
1369
|
+
...coexistencePerfFields(perf),
|
|
1370
|
+
}, a?.experimentId, a?.variationId),
|
|
1371
|
+
url: cleanUrl(),
|
|
1372
|
+
experimentId: a?.experimentId,
|
|
1373
|
+
variationId: a?.variationId,
|
|
1374
|
+
};
|
|
1375
|
+
state.tracker.track(event);
|
|
1376
|
+
const completion = waitForCwvMetrics();
|
|
1377
|
+
const experimentId = a?.experimentId;
|
|
1378
|
+
const variationId = a?.variationId;
|
|
1379
|
+
const url = cleanUrl();
|
|
1380
|
+
void completion.then((metrics) => {
|
|
1381
|
+
if (!metrics || state.screenshotMode || trackingBlocked() || !state.tracker) return;
|
|
1382
|
+
state.tracker.track({
|
|
1383
|
+
type: "sdk_cwv",
|
|
1384
|
+
data: withAssignmentEpochData({
|
|
1385
|
+
...metrics,
|
|
1386
|
+
cwv_finalized: true,
|
|
1387
|
+
}, experimentId, variationId),
|
|
1388
|
+
url,
|
|
1389
|
+
experimentId,
|
|
1390
|
+
variationId,
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
export function orderIdentityEndpoint(config: InitConfig): string | null {
|
|
1396
|
+
if (!config.endpoint) return null;
|
|
1397
|
+
return normalizeEndpoint(config.endpoint) + "/v1/order-identity";
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
export const ORDER_EXPOSURE_STORAGE_KEY = "drip_exposures";
|
|
1401
|
+
export const ORDER_EXPOSURE_MAX_ENTRIES = 50;
|
|
1402
|
+
export const ORDER_EXPOSURE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
1403
|
+
|
|
1404
|
+
export type StoredOrderExposure = {
|
|
1405
|
+
e: string;
|
|
1406
|
+
v: string;
|
|
1407
|
+
t: number;
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
export function pruneStoredOrderExposures(exposures: StoredOrderExposure[], now = Date.now()): StoredOrderExposure[] {
|
|
1411
|
+
return exposures
|
|
1412
|
+
.filter((entry) =>
|
|
1413
|
+
typeof entry.e === "string" &&
|
|
1414
|
+
entry.e &&
|
|
1415
|
+
typeof entry.v === "string" &&
|
|
1416
|
+
entry.v &&
|
|
1417
|
+
typeof entry.t === "number" &&
|
|
1418
|
+
Number.isFinite(entry.t) &&
|
|
1419
|
+
now - entry.t <= ORDER_EXPOSURE_TTL_MS,
|
|
1420
|
+
)
|
|
1421
|
+
.sort((a, b) => a.t - b.t)
|
|
1422
|
+
.slice(-ORDER_EXPOSURE_MAX_ENTRIES);
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
export function readStoredOrderExposures(): StoredOrderExposure[] {
|
|
1426
|
+
if (!persistentStorageAllowed()) return [];
|
|
1427
|
+
try {
|
|
1428
|
+
const raw = globalThis.localStorage?.getItem(ORDER_EXPOSURE_STORAGE_KEY);
|
|
1429
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
1430
|
+
if (!Array.isArray(parsed)) return [];
|
|
1431
|
+
return pruneStoredOrderExposures(parsed as StoredOrderExposure[]);
|
|
1432
|
+
} catch {
|
|
1433
|
+
return [];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
export function writeStoredOrderExposures(exposures: StoredOrderExposure[]): void {
|
|
1438
|
+
if (!persistentStorageAllowed()) return;
|
|
1439
|
+
try {
|
|
1440
|
+
globalThis.localStorage?.setItem(
|
|
1441
|
+
ORDER_EXPOSURE_STORAGE_KEY,
|
|
1442
|
+
JSON.stringify(pruneStoredOrderExposures(exposures)),
|
|
1443
|
+
);
|
|
1444
|
+
} catch {}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
export function clearIdentityContextCookie(): void {
|
|
1448
|
+
if (!globalThis.document) return;
|
|
1449
|
+
try {
|
|
1450
|
+
globalThis.document.cookie = `${IDENTITY_CONTEXT_COOKIE_NAME}=; Max-Age=0; Path=/; SameSite=Lax`;
|
|
1451
|
+
} catch {}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
export function recentIdentityContextExposures(): StoredOrderExposure[] {
|
|
1455
|
+
const deduped = new Map<string, StoredOrderExposure>();
|
|
1456
|
+
const exposures = readStoredOrderExposures();
|
|
1457
|
+
for (let index = exposures.length - 1; index >= 0; index--) {
|
|
1458
|
+
const exposure = exposures[index];
|
|
1459
|
+
if (deduped.has(exposure.e)) continue;
|
|
1460
|
+
deduped.set(exposure.e, exposure);
|
|
1461
|
+
if (deduped.size >= 10) break;
|
|
1462
|
+
}
|
|
1463
|
+
return Array.from(deduped.values()).reverse();
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export function buildIdentityContextCookieValue(
|
|
1467
|
+
visitorId: string,
|
|
1468
|
+
sessionId: string,
|
|
1469
|
+
exposures: StoredOrderExposure[],
|
|
1470
|
+
): string | null {
|
|
1471
|
+
let remaining = exposures.slice();
|
|
1472
|
+
while (true) {
|
|
1473
|
+
// Public contract for in-process commerce platforms (WooCommerce now,
|
|
1474
|
+
// Magento later): URL-encoded `1|visitorId|sessionId|expId:varId,...`.
|
|
1475
|
+
const raw = `1|${visitorId}|${sessionId}|${remaining.map((entry) => `${entry.e}:${entry.v}`).join(",")}`;
|
|
1476
|
+
const encoded = encodeURIComponent(raw);
|
|
1477
|
+
if (encoded.length <= IDENTITY_CONTEXT_COOKIE_MAX_ENCODED_BYTES) return encoded;
|
|
1478
|
+
if (remaining.length === 0) return null;
|
|
1479
|
+
remaining = remaining.slice(1);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export function generateSessionId(): string {
|
|
1484
|
+
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
export function getIdentityContextSessionId(): string {
|
|
1488
|
+
const trackerSessionId = state.tracker?.getSessionId();
|
|
1489
|
+
if (trackerSessionId) return trackerSessionId;
|
|
1490
|
+
try {
|
|
1491
|
+
const existing = globalThis.sessionStorage?.getItem(SESSION_STORAGE_KEY);
|
|
1492
|
+
if (existing) return existing;
|
|
1493
|
+
const created = generateSessionId();
|
|
1494
|
+
globalThis.sessionStorage?.setItem(SESSION_STORAGE_KEY, created);
|
|
1495
|
+
return created;
|
|
1496
|
+
} catch {
|
|
1497
|
+
return generateSessionId();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
export function writeIdentityContextCookie(): StoredOrderExposure[] {
|
|
1502
|
+
if (trackingBlocked()) {
|
|
1503
|
+
clearIdentityContextCookie();
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
if (!globalThis.document || !state.userId) return [];
|
|
1507
|
+
const exposures = recentIdentityContextExposures();
|
|
1508
|
+
try {
|
|
1509
|
+
const encoded = buildIdentityContextCookieValue(
|
|
1510
|
+
state.userId,
|
|
1511
|
+
getIdentityContextSessionId(),
|
|
1512
|
+
exposures,
|
|
1513
|
+
);
|
|
1514
|
+
if (!encoded) {
|
|
1515
|
+
clearIdentityContextCookie();
|
|
1516
|
+
return exposures;
|
|
1517
|
+
}
|
|
1518
|
+
globalThis.document.cookie =
|
|
1519
|
+
`${IDENTITY_CONTEXT_COOKIE_NAME}=${encoded}; Path=/; SameSite=Lax; Max-Age=${IDENTITY_CONTEXT_COOKIE_MAX_AGE_SECONDS}`;
|
|
1520
|
+
} catch {
|
|
1521
|
+
clearIdentityContextCookie();
|
|
1522
|
+
}
|
|
1523
|
+
return exposures;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
export function isOffTargetPersistedAssignment(assignment: Assignment): boolean {
|
|
1527
|
+
return state.activationDiagnostics.experiments.find(
|
|
1528
|
+
(diagnostic) => diagnostic.experimentId === assignment.experimentId,
|
|
1529
|
+
)?.reason === "persisted_assignment_off_target";
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
export function restoreRecordedExposureDedup(exposures: StoredOrderExposure[]): void {
|
|
1533
|
+
if (!exposures.length) return;
|
|
1534
|
+
const recorded = new Set(exposures.map((entry) => `${entry.e}:${entry.v}`));
|
|
1535
|
+
for (const assignment of state.assignments) {
|
|
1536
|
+
if (
|
|
1537
|
+
isOffTargetPersistedAssignment(assignment) &&
|
|
1538
|
+
recorded.has(`${assignment.experimentId}:${assignment.variationId}`)
|
|
1539
|
+
) {
|
|
1540
|
+
state.exposedAssignmentKeys.add(exposureAssignmentKey(assignment));
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
export function rememberOrderExposure(assignment: Assignment): void {
|
|
1546
|
+
if (!persistentStorageAllowed()) return;
|
|
1547
|
+
const now = Date.now();
|
|
1548
|
+
const exposures = readStoredOrderExposures()
|
|
1549
|
+
.filter((entry) => entry.e !== assignment.experimentId);
|
|
1550
|
+
exposures.push({ e: assignment.experimentId, v: assignment.variationId, t: now });
|
|
1551
|
+
writeStoredOrderExposures(exposures);
|
|
1552
|
+
writeIdentityContextCookie();
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
export function mergedOrderIdentityAssignments(): Array<{ experimentId: string; variationId: string }> {
|
|
1556
|
+
const merged = new Map<string, { experimentId: string; variationId: string }>();
|
|
1557
|
+
for (const exposure of readStoredOrderExposures()) {
|
|
1558
|
+
merged.set(exposure.e, { experimentId: exposure.e, variationId: exposure.v });
|
|
1559
|
+
}
|
|
1560
|
+
for (const assignment of state.assignments) {
|
|
1561
|
+
merged.set(assignment.experimentId, {
|
|
1562
|
+
experimentId: assignment.experimentId,
|
|
1563
|
+
variationId: assignment.variationId,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
return Array.from(merged.values()).slice(0, ORDER_EXPOSURE_MAX_ENTRIES);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
export function cleanOrderIdentityText(value: unknown): string | null {
|
|
1570
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1571
|
+
if (typeof value !== "string") return null;
|
|
1572
|
+
const trimmed = value.trim();
|
|
1573
|
+
return trimmed ? trimmed.slice(0, 255) : null;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
export function resolveOrderIdentityPlatform(): OrderIdentityPlatform {
|
|
1577
|
+
if (state.platform === "shopify" || state.platform === "shopware") return state.platform;
|
|
1578
|
+
const w = globalThis as Record<string, unknown>;
|
|
1579
|
+
if (w.__apexShopware) return "shopware";
|
|
1580
|
+
if (w.Shopify) return "shopify";
|
|
1581
|
+
if (w.wc || w.woocommerce_params) return "woocommerce";
|
|
1582
|
+
return "custom";
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
export function detectOrderIdentity(): {
|
|
1586
|
+
platform: OrderIdentityPlatform;
|
|
1587
|
+
platformOrderId: string | null;
|
|
1588
|
+
orderNumber: string | null;
|
|
1589
|
+
} | null {
|
|
1590
|
+
if (typeof window === "undefined" || typeof document === "undefined") return null;
|
|
1591
|
+
const currentUrl = typeof location !== "undefined" ? location.href : "";
|
|
1592
|
+
if (state.orderIdentityDetectionUrl === currentUrl && state.orderIdentityDetection !== undefined) {
|
|
1593
|
+
return state.orderIdentityDetection;
|
|
1594
|
+
}
|
|
1595
|
+
const w = window as unknown as Record<string, unknown>;
|
|
1596
|
+
const explicit: unknown[] = [
|
|
1597
|
+
w.__apexOrderIdentity,
|
|
1598
|
+
readPath(w, "__apexShopware.orderIdentity"),
|
|
1599
|
+
readPath(w, "__apexShopware.order"),
|
|
1600
|
+
];
|
|
1601
|
+
const dataLayer = Array.isArray(w.dataLayer) ? w.dataLayer : [];
|
|
1602
|
+
for (const item of dataLayer) {
|
|
1603
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
|
1604
|
+
const eventName = cleanOrderIdentityText((item as Record<string, unknown>).event);
|
|
1605
|
+
if (eventName !== "purchase" && eventName !== "transaction" && eventName !== "checkout_completed") continue;
|
|
1606
|
+
explicit.push(readPath(item, "ecommerce.purchase"));
|
|
1607
|
+
explicit.push(readPath(item, "ecommerce"));
|
|
1608
|
+
explicit.push(item);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
let platformOrderId: string | null = null;
|
|
1612
|
+
let orderNumber: string | null = null;
|
|
1613
|
+
let platform = resolveOrderIdentityPlatform();
|
|
1614
|
+
let strongSignal = false;
|
|
1615
|
+
for (const source of explicit) {
|
|
1616
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) continue;
|
|
1617
|
+
strongSignal = true;
|
|
1618
|
+
const record = source as Record<string, unknown>;
|
|
1619
|
+
platformOrderId ||= cleanOrderIdentityText(
|
|
1620
|
+
record.platformOrderId ?? record.orderId ?? record.order_id ?? record.transaction_id,
|
|
1621
|
+
);
|
|
1622
|
+
orderNumber ||= cleanOrderIdentityText(
|
|
1623
|
+
record.orderNumber ?? record.order_number ?? record.number ?? record.transaction_id,
|
|
1624
|
+
);
|
|
1625
|
+
const hintedPlatform = cleanOrderIdentityText(record.platform);
|
|
1626
|
+
if (
|
|
1627
|
+
hintedPlatform === "shopify" ||
|
|
1628
|
+
hintedPlatform === "shopware" ||
|
|
1629
|
+
hintedPlatform === "woocommerce" ||
|
|
1630
|
+
hintedPlatform === "centra" ||
|
|
1631
|
+
hintedPlatform === "custom"
|
|
1632
|
+
) {
|
|
1633
|
+
platform = hintedPlatform;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
if (!strongSignal) {
|
|
1638
|
+
const orderIdEl = document.querySelector<HTMLElement>("[data-apex-order-id]");
|
|
1639
|
+
const orderNumberEl = document.querySelector<HTMLElement>("[data-apex-order-number]");
|
|
1640
|
+
const domOrderId = cleanOrderIdentityText(orderIdEl?.dataset.apexOrderId);
|
|
1641
|
+
const domOrderNumber = cleanOrderIdentityText(orderNumberEl?.dataset.apexOrderNumber);
|
|
1642
|
+
if (domOrderId || domOrderNumber) {
|
|
1643
|
+
strongSignal = true;
|
|
1644
|
+
platformOrderId ||= domOrderId;
|
|
1645
|
+
orderNumber ||= domOrderNumber;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
if (strongSignal) {
|
|
1650
|
+
try {
|
|
1651
|
+
const params = new URLSearchParams(location.search);
|
|
1652
|
+
platformOrderId ||= cleanOrderIdentityText(
|
|
1653
|
+
params.get("platformOrderId") ?? params.get("orderId") ?? params.get("order_id"),
|
|
1654
|
+
);
|
|
1655
|
+
orderNumber ||= cleanOrderIdentityText(
|
|
1656
|
+
params.get("orderNumber") ?? params.get("order_number") ?? params.get("transaction_id"),
|
|
1657
|
+
);
|
|
1658
|
+
} catch {}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const result = strongSignal && (platformOrderId || orderNumber)
|
|
1662
|
+
? { platform, platformOrderId, orderNumber }
|
|
1663
|
+
: null;
|
|
1664
|
+
state.orderIdentityDetectionUrl = currentUrl;
|
|
1665
|
+
state.orderIdentityDetection = result;
|
|
1666
|
+
return result;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
export function orderIdentityDedupeKey(identity: {
|
|
1670
|
+
platform: string;
|
|
1671
|
+
platformOrderId: string | null;
|
|
1672
|
+
orderNumber: string | null;
|
|
1673
|
+
}, assignments: Array<{ experimentId: string; variationId: string }>): string {
|
|
1674
|
+
return [
|
|
1675
|
+
"drip:order_identity",
|
|
1676
|
+
identity.platform,
|
|
1677
|
+
identity.platformOrderId || "",
|
|
1678
|
+
identity.orderNumber || "",
|
|
1679
|
+
assignments.map((assignment) => `${assignment.experimentId}:${assignment.variationId}`).join(",") || "none",
|
|
1680
|
+
].join(":").slice(0, 240);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
export function scheduleOrderIdentityBeacon(config: InitConfig): void {
|
|
1684
|
+
if (state.screenshotMode || trackingBlocked()) return;
|
|
1685
|
+
if (!state.tracker) return;
|
|
1686
|
+
const endpoint = orderIdentityEndpoint(config);
|
|
1687
|
+
if (!endpoint || !config.shopId || !config.ingestSignature) return;
|
|
1688
|
+
|
|
1689
|
+
window.setTimeout(() => {
|
|
1690
|
+
const identity = detectOrderIdentity();
|
|
1691
|
+
if (!identity) return;
|
|
1692
|
+
const assignments = mergedOrderIdentityAssignments();
|
|
1693
|
+
if (assignments.length === 0) return;
|
|
1694
|
+
const key = orderIdentityDedupeKey(identity, assignments);
|
|
1695
|
+
try {
|
|
1696
|
+
if (sessionStorage.getItem(key) === "1") return;
|
|
1697
|
+
} catch {}
|
|
1698
|
+
if (state.orderIdentityInFlight.has(key)) return;
|
|
1699
|
+
state.orderIdentityInFlight.add(key);
|
|
1700
|
+
|
|
1701
|
+
const markDelivered = () => {
|
|
1702
|
+
try {
|
|
1703
|
+
sessionStorage.setItem(key, "1");
|
|
1704
|
+
} catch {}
|
|
1705
|
+
};
|
|
1706
|
+
const markComplete = () => {
|
|
1707
|
+
state.orderIdentityInFlight.delete(key);
|
|
1708
|
+
};
|
|
1709
|
+
|
|
1710
|
+
const payload = {
|
|
1711
|
+
_shopId: config.ingestShopId ?? config.shopId,
|
|
1712
|
+
_signature: config.ingestSignature,
|
|
1713
|
+
platform: identity.platform,
|
|
1714
|
+
environmentKey: config.environments?.activeKey || "production",
|
|
1715
|
+
platformOrderId: identity.platformOrderId,
|
|
1716
|
+
orderNumber: identity.orderNumber,
|
|
1717
|
+
visitorId: state.userId ?? "",
|
|
1718
|
+
sessionId: state.tracker?.getSessionId() ?? "",
|
|
1719
|
+
assignments,
|
|
1720
|
+
url: cleanUrl(),
|
|
1721
|
+
};
|
|
1722
|
+
const body = JSON.stringify(payload);
|
|
1723
|
+
const sendOnPagehide = () => {
|
|
1724
|
+
// A queued beacon has no application-level acknowledgement. Do not set
|
|
1725
|
+
// the session marker: the server dedupes the stable order/event identity.
|
|
1726
|
+
try {
|
|
1727
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
1728
|
+
navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
|
|
1729
|
+
}
|
|
1730
|
+
} catch {}
|
|
1731
|
+
};
|
|
1732
|
+
window.addEventListener("pagehide", sendOnPagehide, { once: true });
|
|
1733
|
+
void fetch(endpoint, {
|
|
1734
|
+
method: "POST",
|
|
1735
|
+
headers: { "Content-Type": "application/json" },
|
|
1736
|
+
body,
|
|
1737
|
+
keepalive: true,
|
|
1738
|
+
})
|
|
1739
|
+
.then((response) => {
|
|
1740
|
+
if (response.status >= 200 && response.status < 300) {
|
|
1741
|
+
markDelivered();
|
|
1742
|
+
// Only a verified delivery retires the pagehide fallback; after a
|
|
1743
|
+
// failed or rejected fetch it stays armed as the last-chance send
|
|
1744
|
+
// when the shopper leaves the page (server idempotency absorbs
|
|
1745
|
+
// any duplicate).
|
|
1746
|
+
window.removeEventListener("pagehide", sendOnPagehide);
|
|
1747
|
+
}
|
|
1748
|
+
})
|
|
1749
|
+
.catch(() => {})
|
|
1750
|
+
.finally(markComplete);
|
|
1751
|
+
}, 0);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
export function deliveryStatus(expected: number, applied: number, missed: number, isControl: boolean): DeliveryHealthStatus {
|
|
1755
|
+
if (expected === 0) return isControl ? "control" : "no_mutations";
|
|
1756
|
+
if (applied >= expected && missed === 0) return "applied";
|
|
1757
|
+
if (applied > 0) return "partial";
|
|
1758
|
+
return "miss";
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
export function queueDeliveryHealthEvent(
|
|
1762
|
+
assignment: Assignment,
|
|
1763
|
+
data: {
|
|
1764
|
+
expectedMutations: number;
|
|
1765
|
+
appliedMutations: number;
|
|
1766
|
+
selectorMisses: number;
|
|
1767
|
+
applyGeneration: number;
|
|
1768
|
+
activationMode: ActivationMode;
|
|
1769
|
+
},
|
|
1770
|
+
): void {
|
|
1771
|
+
if (state.screenshotMode) return;
|
|
1772
|
+
const event: TrackEvent = {
|
|
1773
|
+
type: "delivery_health",
|
|
1774
|
+
data: {
|
|
1775
|
+
expected_mutations: data.expectedMutations,
|
|
1776
|
+
applied_mutations: data.appliedMutations,
|
|
1777
|
+
selector_misses: data.selectorMisses,
|
|
1778
|
+
apply_status: deliveryStatus(
|
|
1779
|
+
data.expectedMutations,
|
|
1780
|
+
data.appliedMutations,
|
|
1781
|
+
data.selectorMisses,
|
|
1782
|
+
assignment.isControl,
|
|
1783
|
+
),
|
|
1784
|
+
assignment_index: assignment.index,
|
|
1785
|
+
is_control: assignment.isControl,
|
|
1786
|
+
entity_kind: assignment.kind ?? "experiment",
|
|
1787
|
+
activation_mode: data.activationMode,
|
|
1788
|
+
config_source: state.configSource,
|
|
1789
|
+
platform: state.platform,
|
|
1790
|
+
apply_generation: data.applyGeneration,
|
|
1791
|
+
delivery_state: typeof state.config?.deliveryState === "string" ? state.config.deliveryState : undefined,
|
|
1792
|
+
assignment_epoch:
|
|
1793
|
+
!assignment.kind || assignment.kind === "experiment"
|
|
1794
|
+
? assignment.assignmentEpoch
|
|
1795
|
+
: undefined,
|
|
1796
|
+
},
|
|
1797
|
+
url: cleanUrl(),
|
|
1798
|
+
experimentId: assignment.experimentId,
|
|
1799
|
+
variationId: assignment.variationId,
|
|
1800
|
+
};
|
|
1801
|
+
if (state.tracker && !trackingBlocked()) {
|
|
1802
|
+
state.tracker.track(event);
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
state.pendingDeliveryHealth.push(event);
|
|
1806
|
+
if (state.pendingDeliveryHealth.length > 50) {
|
|
1807
|
+
state.pendingDeliveryHealth.splice(0, state.pendingDeliveryHealth.length - 50);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
export function flushPendingDeliveryHealth(): void {
|
|
1812
|
+
if (!state.tracker || trackingBlocked() || state.pendingDeliveryHealth.length === 0) return;
|
|
1813
|
+
const events = state.pendingDeliveryHealth.splice(0);
|
|
1814
|
+
for (const event of events) {
|
|
1815
|
+
state.tracker.track(event);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
export function startAnalyticsStage(
|
|
1820
|
+
config: InitConfig,
|
|
1821
|
+
experiments: Experiment[],
|
|
1822
|
+
initialExposureAssignments?: Assignment[],
|
|
1823
|
+
targetingPass = activeTargetingEvaluationPass,
|
|
1824
|
+
): void {
|
|
1825
|
+
if (state.analyticsStarted) return;
|
|
1826
|
+
if (state.pendingInitialApply) {
|
|
1827
|
+
// Exposure (experiment_viewed) and goal wiring must not precede the
|
|
1828
|
+
// gated initial apply (after_hydration / pre-init hold) — consent and
|
|
1829
|
+
// route paths can reach this before the gate opens. Re-enter after;
|
|
1830
|
+
// the analyticsStarted flag keeps queued re-entries idempotent, and a
|
|
1831
|
+
// config replaced while queued (cached init superseded by the network
|
|
1832
|
+
// fetch) must not start analytics with the stale experiment list.
|
|
1833
|
+
void state.pendingInitialApply.then(() => {
|
|
1834
|
+
if (state.config !== config) return;
|
|
1835
|
+
startAnalyticsStage(
|
|
1836
|
+
config,
|
|
1837
|
+
experiments,
|
|
1838
|
+
initialExposureAssignments,
|
|
1839
|
+
targetingPass,
|
|
1840
|
+
);
|
|
1841
|
+
});
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (state.screenshotMode) return;
|
|
1845
|
+
if (trackingBlocked()) return;
|
|
1846
|
+
if (!state.trackingPrerequisiteMet) return;
|
|
1847
|
+
|
|
1848
|
+
const emptyProjectFastPath = config.runtimeProfile === "empty_project"
|
|
1849
|
+
&& state.perfFlags.perf_empty_project_v1;
|
|
1850
|
+
ensureTrackerStarted(config);
|
|
1851
|
+
drainPendingDataLayerEvents();
|
|
1852
|
+
if (!emptyProjectFastPath) syncActiveAssignmentToShopifyCart();
|
|
1853
|
+
flushPendingPerf();
|
|
1854
|
+
flushPendingDeliveryHealth();
|
|
1855
|
+
const storedOrderExposures = writeIdentityContextCookie();
|
|
1856
|
+
if (!emptyProjectFastPath) {
|
|
1857
|
+
const previousTargetingPass = activeTargetingEvaluationPass;
|
|
1858
|
+
activeTargetingEvaluationPass = targetingPass;
|
|
1859
|
+
try {
|
|
1860
|
+
restoreRecordedExposureDedup(storedOrderExposures);
|
|
1861
|
+
trackViewedAssignments(config, initialExposureAssignments, targetingPass);
|
|
1862
|
+
setupGoalTracking(experiments);
|
|
1863
|
+
setupHeatmapTracking(state.assignments, experiments);
|
|
1864
|
+
setupReplayTracking(state.assignments, experiments);
|
|
1865
|
+
setupSemanticTracking(config);
|
|
1866
|
+
} finally {
|
|
1867
|
+
activeTargetingEvaluationPass = previousTargetingPass;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
scheduleOrderIdentityBeacon(config);
|
|
1871
|
+
state.analyticsStarted = true;
|
|
1872
|
+
if (!emptyProjectFastPath) updateProjectActivationDiagnostics(config);
|
|
1873
|
+
__DRIP_FEATURE_DEVTOOLS__ && dtLog("track", "Analytics stage started", { assignments: state.assignments.length });
|
|
1874
|
+
onPageEnter(analyticsTrack);
|
|
1875
|
+
if (state.consentRequired && !emptyProjectFastPath) {
|
|
1876
|
+
scheduleShopifyAssignmentSync();
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
export function scheduleAnalyticsStart(
|
|
1881
|
+
config: InitConfig,
|
|
1882
|
+
experiments: Experiment[],
|
|
1883
|
+
targetingPass?: TargetingEvaluationPass,
|
|
1884
|
+
): void {
|
|
1885
|
+
clearScheduledAnalyticsStart();
|
|
1886
|
+
const run = () => {
|
|
1887
|
+
clearScheduledAnalyticsStart();
|
|
1888
|
+
startAnalyticsStage(config, experiments, undefined, targetingPass);
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1891
|
+
const idleTimeout = state.trackingIdleTimeoutMs;
|
|
1892
|
+
|
|
1893
|
+
state.analyticsTimeoutId = window.setTimeout(run, idleTimeout);
|
|
1894
|
+
|
|
1895
|
+
const w = globalThis as Record<string, unknown>;
|
|
1896
|
+
const requestIdle = w.requestIdleCallback;
|
|
1897
|
+
if (typeof requestIdle === "function") {
|
|
1898
|
+
state.analyticsIdleId = (requestIdle as (
|
|
1899
|
+
cb: () => void,
|
|
1900
|
+
opts?: { timeout?: number },
|
|
1901
|
+
) => number)(run, { timeout: idleTimeout });
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
export function startAnalyticsIfReady(): void {
|
|
1906
|
+
if (state.tracker || !state.config) return;
|
|
1907
|
+
if (trackingBlocked()) return;
|
|
1908
|
+
if (!state.trackingPrerequisiteMet) return;
|
|
1909
|
+
|
|
1910
|
+
const experiments = state.config.experiments ?? [];
|
|
1911
|
+
startAnalyticsStage(state.config, experiments);
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
export function getConfigCacheKey(shopId: string, environmentKey?: string): string {
|
|
1915
|
+
const env = environmentKey || "production";
|
|
1916
|
+
return CONFIG_CACHE_KEY_PREFIX + shopId + ":" + env;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
export function storageAvailable(kind: "localStorage" | "sessionStorage"): boolean {
|
|
1920
|
+
try {
|
|
1921
|
+
const storage = globalThis[kind];
|
|
1922
|
+
if (!storage) return false;
|
|
1923
|
+
const key = "__drip_storage_probe__";
|
|
1924
|
+
storage.setItem(key, "1");
|
|
1925
|
+
storage.removeItem(key);
|
|
1926
|
+
return true;
|
|
1927
|
+
} catch {
|
|
1928
|
+
return false;
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
export function getConfigCacheTtlMs(config: InitConfig): number {
|
|
1933
|
+
const ttl = config.configCacheTtlMs;
|
|
1934
|
+
if (typeof ttl === "number" && Number.isFinite(ttl) && ttl > 0) return ttl;
|
|
1935
|
+
return DEFAULT_CONFIG_CACHE_TTL_MS;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
export function configuredStoreOrigins(
|
|
1939
|
+
config: Pick<InitConfig, "storeOrigin" | "storeOrigins">,
|
|
1940
|
+
): string | string[] | undefined {
|
|
1941
|
+
return config.storeOrigins?.length ? config.storeOrigins : config.storeOrigin;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
export function readCachedExperiments(
|
|
1945
|
+
shopId: string,
|
|
1946
|
+
ttlMs: number,
|
|
1947
|
+
environmentKey?: string,
|
|
1948
|
+
): { experiments: Experiment[]; storeOrigin?: string; storeOrigins?: string[]; exclusionGroups?: ExclusionGroupConfig[]; profile?: InitConfig["profile"]; runtime?: unknown; signals?: RuntimeSignalDefinition[]; commerce?: InitConfig["commerce"]; geo?: { country?: string }; holdout?: [enabled?: boolean, pct?: number, configUpdatedAt?: string | null]; hash: string } | null {
|
|
1949
|
+
try {
|
|
1950
|
+
const raw = localStorage.getItem(getConfigCacheKey(shopId, environmentKey));
|
|
1951
|
+
if (!raw) return null;
|
|
1952
|
+
const parsed = JSON.parse(raw) as {
|
|
1953
|
+
fetchedAt?: number;
|
|
1954
|
+
hash?: string;
|
|
1955
|
+
experiments?: Experiment[];
|
|
1956
|
+
storeOrigin?: string;
|
|
1957
|
+
storeOrigins?: string[];
|
|
1958
|
+
exclusionGroups?: ExclusionGroupConfig[];
|
|
1959
|
+
profile?: InitConfig["profile"];
|
|
1960
|
+
runtime?: unknown;
|
|
1961
|
+
signals?: RuntimeSignalDefinition[];
|
|
1962
|
+
commerce?: InitConfig["commerce"];
|
|
1963
|
+
geo?: { country?: unknown };
|
|
1964
|
+
holdout?: [enabled?: boolean, pct?: number, configUpdatedAt?: string | null];
|
|
1965
|
+
};
|
|
1966
|
+
if (!parsed || typeof parsed.fetchedAt !== "number" || !Array.isArray(parsed.experiments)) {
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
if (Date.now() - parsed.fetchedAt > ttlMs) return null;
|
|
1970
|
+
return {
|
|
1971
|
+
experiments: parsed.experiments,
|
|
1972
|
+
storeOrigin:
|
|
1973
|
+
typeof parsed.storeOrigin === "string"
|
|
1974
|
+
? parsed.storeOrigin
|
|
1975
|
+
: undefined,
|
|
1976
|
+
storeOrigins: Array.isArray(parsed.storeOrigins)
|
|
1977
|
+
? parsed.storeOrigins.filter(
|
|
1978
|
+
(origin): origin is string => typeof origin === "string",
|
|
1979
|
+
)
|
|
1980
|
+
: undefined,
|
|
1981
|
+
exclusionGroups: Array.isArray(parsed.exclusionGroups)
|
|
1982
|
+
? parsed.exclusionGroups
|
|
1983
|
+
: undefined,
|
|
1984
|
+
profile: parsed.profile,
|
|
1985
|
+
runtime: parsed.runtime,
|
|
1986
|
+
signals: Array.isArray(parsed.signals) ? parsed.signals : undefined,
|
|
1987
|
+
commerce: parsed.commerce,
|
|
1988
|
+
geo: typeof parsed.geo?.country === "string"
|
|
1989
|
+
? { country: parsed.geo.country }
|
|
1990
|
+
: undefined,
|
|
1991
|
+
holdout: parsed.holdout,
|
|
1992
|
+
hash: typeof parsed.hash === "string" ? parsed.hash : "",
|
|
1993
|
+
};
|
|
1994
|
+
} catch {
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
export function hashConfigPayload(
|
|
2000
|
+
experiments: Experiment[],
|
|
2001
|
+
storeOrigin?: string,
|
|
2002
|
+
storeOrigins?: string[],
|
|
2003
|
+
exclusionGroups?: ExclusionGroupConfig[],
|
|
2004
|
+
profile?: InitConfig["profile"],
|
|
2005
|
+
runtime?: unknown,
|
|
2006
|
+
signals?: RuntimeSignalDefinition[],
|
|
2007
|
+
commerce?: InitConfig["commerce"],
|
|
2008
|
+
holdout_v1_enabled?: boolean,
|
|
2009
|
+
holdout_pct?: number,
|
|
2010
|
+
holdout_config_updated_at?: string | null,
|
|
2011
|
+
): string {
|
|
2012
|
+
try {
|
|
2013
|
+
return JSON.stringify({
|
|
2014
|
+
experiments,
|
|
2015
|
+
...(storeOrigin ? { storeOrigin } : {}),
|
|
2016
|
+
...(storeOrigins?.length ? { storeOrigins } : {}),
|
|
2017
|
+
exclusionGroups: exclusionGroups ?? [],
|
|
2018
|
+
profile: profile ?? null,
|
|
2019
|
+
runtime: runtime ?? null,
|
|
2020
|
+
signals: signals ?? [],
|
|
2021
|
+
commerce: commerce ?? null,
|
|
2022
|
+
holdout: [
|
|
2023
|
+
holdout_v1_enabled ?? false,
|
|
2024
|
+
holdout_pct ?? 0,
|
|
2025
|
+
holdout_config_updated_at ?? null,
|
|
2026
|
+
],
|
|
2027
|
+
});
|
|
2028
|
+
} catch {
|
|
2029
|
+
return "";
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
export function writeCachedExperiments(
|
|
2034
|
+
shopId: string,
|
|
2035
|
+
experiments: Experiment[],
|
|
2036
|
+
storeOrigin?: string,
|
|
2037
|
+
storeOrigins?: string[],
|
|
2038
|
+
exclusionGroups?: ExclusionGroupConfig[],
|
|
2039
|
+
profile?: InitConfig["profile"],
|
|
2040
|
+
runtime?: unknown,
|
|
2041
|
+
signals?: RuntimeSignalDefinition[],
|
|
2042
|
+
commerce?: InitConfig["commerce"],
|
|
2043
|
+
holdout_v1_enabled?: boolean,
|
|
2044
|
+
holdout_pct?: number,
|
|
2045
|
+
holdout_config_updated_at?: string | null,
|
|
2046
|
+
geo?: { country?: string },
|
|
2047
|
+
environmentKey?: string,
|
|
2048
|
+
): void {
|
|
2049
|
+
try {
|
|
2050
|
+
const payload = {
|
|
2051
|
+
fetchedAt: Date.now(),
|
|
2052
|
+
hash: hashConfigPayload(
|
|
2053
|
+
experiments,
|
|
2054
|
+
storeOrigin,
|
|
2055
|
+
storeOrigins,
|
|
2056
|
+
exclusionGroups,
|
|
2057
|
+
profile,
|
|
2058
|
+
runtime,
|
|
2059
|
+
signals,
|
|
2060
|
+
commerce,
|
|
2061
|
+
holdout_v1_enabled,
|
|
2062
|
+
holdout_pct,
|
|
2063
|
+
holdout_config_updated_at,
|
|
2064
|
+
),
|
|
2065
|
+
experiments,
|
|
2066
|
+
storeOrigin,
|
|
2067
|
+
storeOrigins,
|
|
2068
|
+
exclusionGroups,
|
|
2069
|
+
profile,
|
|
2070
|
+
runtime,
|
|
2071
|
+
signals,
|
|
2072
|
+
commerce,
|
|
2073
|
+
geo,
|
|
2074
|
+
holdout: [
|
|
2075
|
+
holdout_v1_enabled,
|
|
2076
|
+
holdout_pct,
|
|
2077
|
+
holdout_config_updated_at,
|
|
2078
|
+
],
|
|
2079
|
+
};
|
|
2080
|
+
localStorage.setItem(getConfigCacheKey(shopId, environmentKey), JSON.stringify(payload));
|
|
2081
|
+
} catch {
|
|
2082
|
+
// no-op on storage failures
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
export function withRuntimeSettings(
|
|
2087
|
+
config: InitConfig,
|
|
2088
|
+
runtime: unknown,
|
|
2089
|
+
profile?: InitConfig["profile"],
|
|
2090
|
+
): InitConfig {
|
|
2091
|
+
if (!runtime || typeof runtime !== "object") {
|
|
2092
|
+
return profile ? { ...config, profile } : config;
|
|
2093
|
+
}
|
|
2094
|
+
const r = runtime as Record<string, any>;
|
|
2095
|
+
const antiFlicker = r.antiFlicker && typeof r.antiFlicker === "object" ? r.antiFlicker : {};
|
|
2096
|
+
const snippet = r.snippet && typeof r.snippet === "object" ? r.snippet : {};
|
|
2097
|
+
const prerequisites = r.prerequisites && typeof r.prerequisites === "object" ? r.prerequisites : {};
|
|
2098
|
+
const debug = r.debug && typeof r.debug === "object" ? r.debug : undefined;
|
|
2099
|
+
const pageTrigger = r.pageTrigger && typeof r.pageTrigger === "object" ? r.pageTrigger : undefined;
|
|
2100
|
+
const consentMode = r.consentMode && typeof r.consentMode === "object" ? r.consentMode : undefined;
|
|
2101
|
+
const spa = r.spa && typeof r.spa === "object" ? r.spa : undefined;
|
|
2102
|
+
const externalTracking = r.externalTracking && typeof r.externalTracking === "object" ? r.externalTracking : undefined;
|
|
2103
|
+
const dataLayerTriggers = Array.isArray(r.dataLayerTriggers) ? r.dataLayerTriggers : undefined;
|
|
2104
|
+
const exposureDestinations = r.exposureDestinations && typeof r.exposureDestinations === "object" ? r.exposureDestinations : undefined;
|
|
2105
|
+
const projectCode = r.projectCode && typeof r.projectCode === "object" ? r.projectCode : undefined;
|
|
2106
|
+
const environments = r.environments && typeof r.environments === "object" ? r.environments : undefined;
|
|
2107
|
+
const bucketingCleanup = r.bucketingCleanup && typeof r.bucketingCleanup === "object" ? r.bucketingCleanup : undefined;
|
|
2108
|
+
const dataProtection = r.dataProtection && typeof r.dataProtection === "object" ? r.dataProtection : undefined;
|
|
2109
|
+
const semanticTracking = r.semanticTracking && typeof r.semanticTracking === "object" ? r.semanticTracking : undefined;
|
|
2110
|
+
const runtimeControl = r.runtimeControl && typeof r.runtimeControl === "object" ? r.runtimeControl : undefined;
|
|
2111
|
+
const commerce = r.commerce && typeof r.commerce === "object" ? r.commerce : undefined;
|
|
2112
|
+
|
|
2113
|
+
return {
|
|
2114
|
+
...config,
|
|
2115
|
+
profile: profile ?? config.profile,
|
|
2116
|
+
antiFlickerMode: antiFlicker.mode ?? config.antiFlickerMode,
|
|
2117
|
+
antiFlickerTimeoutMs: antiFlicker.timeoutMs ?? config.antiFlickerTimeoutMs,
|
|
2118
|
+
antiFlickerCustomCss: antiFlicker.customCss ?? config.antiFlickerCustomCss,
|
|
2119
|
+
configCacheTtlMs: snippet.configCacheTtlMs ?? config.configCacheTtlMs,
|
|
2120
|
+
projectPrerequisites: {
|
|
2121
|
+
...config.projectPrerequisites,
|
|
2122
|
+
showVariationsRule: prerequisites.showVariationsRule ?? config.projectPrerequisites?.showVariationsRule,
|
|
2123
|
+
startTrackingRule: prerequisites.startTrackingRule ?? config.projectPrerequisites?.startTrackingRule,
|
|
2124
|
+
},
|
|
2125
|
+
consentMode: consentMode ? { ...config.consentMode, ...consentMode } : config.consentMode,
|
|
2126
|
+
debug: debug ? { ...config.debug, ...debug } : config.debug,
|
|
2127
|
+
pageTrigger: pageTrigger ? { ...config.pageTrigger, ...pageTrigger } : config.pageTrigger,
|
|
2128
|
+
spa: spa ? { ...config.spa, ...spa } : config.spa,
|
|
2129
|
+
externalTracking: externalTracking ? { ...config.externalTracking, ...externalTracking } : config.externalTracking,
|
|
2130
|
+
dataLayerTriggers: dataLayerTriggers ?? config.dataLayerTriggers,
|
|
2131
|
+
exposureDestinations: exposureDestinations ? { ...config.exposureDestinations, ...exposureDestinations } : config.exposureDestinations,
|
|
2132
|
+
projectCode: projectCode ? { ...config.projectCode, ...projectCode } : config.projectCode,
|
|
2133
|
+
environments: environments ? { ...config.environments, ...environments } : config.environments,
|
|
2134
|
+
bucketingCleanup: bucketingCleanup ? { ...config.bucketingCleanup, ...bucketingCleanup } : config.bucketingCleanup,
|
|
2135
|
+
dataProtection: dataProtection ? { ...config.dataProtection, ...dataProtection } : config.dataProtection,
|
|
2136
|
+
semanticTracking: semanticTracking ? { ...config.semanticTracking, ...semanticTracking } : config.semanticTracking,
|
|
2137
|
+
runtimeControl: runtimeControl ? { ...config.runtimeControl, ...runtimeControl } : config.runtimeControl,
|
|
2138
|
+
assignmentCookieSync: typeof r.assignmentCookieSync === "boolean"
|
|
2139
|
+
? r.assignmentCookieSync
|
|
2140
|
+
: config.assignmentCookieSync,
|
|
2141
|
+
aggregateMode: (r.aggregateMode as InitConfig["aggregateMode"]) ?? config.aggregateMode,
|
|
2142
|
+
proxyEventDelivery: r.proxyEventDelivery?.enabled === true
|
|
2143
|
+
? { enabled: true }
|
|
2144
|
+
: config.proxyEventDelivery,
|
|
2145
|
+
cartAttributeWrite: commerce?.cartAttributeWrite === "external"
|
|
2146
|
+
? "external"
|
|
2147
|
+
: commerce?.cartAttributeWrite === "sdk"
|
|
2148
|
+
? "sdk"
|
|
2149
|
+
: commerce?.cartAttributeWrite == null && config.cartAttributeWrite === "external"
|
|
2150
|
+
? "external"
|
|
2151
|
+
: "sdk",
|
|
2152
|
+
cartAttributeScope: commerce?.cartAttributeScope === "all"
|
|
2153
|
+
? "all"
|
|
2154
|
+
: commerce?.cartAttributeScope === "consented"
|
|
2155
|
+
? "consented"
|
|
2156
|
+
: commerce?.cartAttributeScope == null && config.cartAttributeScope === "all"
|
|
2157
|
+
? "all"
|
|
2158
|
+
: "consented",
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
export function serializeForceMap(forceMap: Record<string, string> | null): string {
|
|
2163
|
+
if (!forceMap) return "";
|
|
2164
|
+
return Object.entries(forceMap)
|
|
2165
|
+
.map(([experimentId, variationId]) => `${experimentId}:${variationId}`)
|
|
2166
|
+
.join(",");
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
export function getHashId(exp: Experiment, userId: string, attrs: Record<string, unknown>): string {
|
|
2170
|
+
if (exp.hashAttribute && attrs[exp.hashAttribute] != null) {
|
|
2171
|
+
return String(attrs[exp.hashAttribute]);
|
|
2172
|
+
}
|
|
2173
|
+
return userId;
|
|
2174
|
+
}
|
|
2175
|
+
|