@tokenoftrust/storefront-runner 2.2.68 → 2.2.70

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.68",
3
+ "version": "2.2.70",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -507,6 +507,7 @@ interface ViewerConsentWindow {
507
507
  localStorage?: {
508
508
  getItem(key: string): string | null;
509
509
  setItem(key: string, value: string): void;
510
+ removeItem?(key: string): void;
510
511
  };
511
512
  document?: Document;
512
513
  addEventListener?: (type: string, listener: (event: { key?: string | null }) => void) => void;
@@ -649,6 +650,16 @@ export function applyEarlyViewerAnalyticsConsent(
649
650
  : rawEligible.includes(purpose)),
650
651
  );
651
652
  const approved = canonicalPurposes.filter((purpose) => rawApproved.includes(purpose));
653
+ // Analytics ineligible here (denial, GPC, tenant not approved) forgets this context's
654
+ // Amplitude visitor id and session before any adapter can run. The key is the
655
+ // native-amplitude.ts VISITOR_CONTINUITY_STORAGE_KEY prefix over the same contextKey.
656
+ if (!eligible.includes("analytics")) {
657
+ try {
658
+ win.localStorage?.removeItem?.(`tot.analytics-continuity.v1:${encodeURIComponent(contextKey)}`);
659
+ } catch {
660
+ // Storage denial leaves nothing readable to clear.
661
+ }
662
+ }
652
663
  win[consentGlobal] = Object.freeze({
653
664
  schemaVersion: 2,
654
665
  snapshotId: snapshotIdFor(eligible, approved),
@@ -42,6 +42,7 @@ import type { NavigationCompletedEvent, NavigationKind } from "./events.js";
42
42
  import { CommerceEventBroker } from "./commerce-broker.js";
43
43
  import { validateBrowserEnvironment, BROWSER_ENVIRONMENT_GLOBAL, type BrowserEnvironment } from "./environment.js";
44
44
  import { makePlatformAdapterRuntimeLoader } from "./platform-adapter-runtime.js";
45
+ import { createVisitorContinuity, type VisitorContinuityStorage } from "./native-amplitude.js";
45
46
  import { resolveNativeGa4SandboxUrl, type NativeGa4Transport } from "./native-ga4.js";
46
47
  import {
47
48
  createBrowserNativeGa4SandboxHost,
@@ -138,6 +139,11 @@ export interface BootLifecycleOptions {
138
139
  * queued in the transport. Injectable so a test can hold the mount.
139
140
  */
140
141
  scheduleGa4Sandbox?: NativeGa4SandboxScheduler;
142
+ /**
143
+ * The persistent store behind the Amplitude visitor id and session; defaults to the
144
+ * window's origin storage. Read lazily and only while analytics is eligible.
145
+ */
146
+ visitorStorage?: () => VisitorContinuityStorage | undefined;
141
147
  }
142
148
 
143
149
  /**
@@ -431,6 +437,10 @@ export function bootIntegrationLifecycle(
431
437
  fetchCapability: hostFetch ? deferFetchCapability(hostFetch, schedulePlatformAdapterStart) : hostFetch,
432
438
  importModule: options.importModule,
433
439
  fallback: ordinaryLoader,
440
+ visitorContinuity: createVisitorContinuity({
441
+ storage: options.visitorStorage ??
442
+ (() => win["localStorage"] as VisitorContinuityStorage | undefined),
443
+ }),
434
444
  nativeGa4Transport: () => {
435
445
  if (options.nativeGa4Transport) return options.nativeGa4Transport;
436
446
  if (typeof document === "undefined" || typeof MessageChannel === "undefined") {
@@ -2,10 +2,22 @@
2
2
  * Storefront-owned Amplitude HTTP API v2 adapter.
3
3
  *
4
4
  * This module intentionally uses no vendor SDK or browser discovery surface. It
5
- * consumes only validated commerce-broker events and sends a closed, anonymous
6
- * payload to one exact collection endpoint. The fixed device value groups events
7
- * as non-identifying storefront traffic; it is never derived from a browser,
8
- * person, account, persisted client value, network address, or device signal.
5
+ * consumes only validated commerce-broker events and sends a closed payload to one
6
+ * exact collection endpoint.
7
+ *
8
+ * VISITOR CONTINUITY. `device_id` is a pseudonymous browser id and `session_id` the
9
+ * epoch-ms start of a 30-minute-inactivity session, both from `VisitorContinuity`
10
+ * below. The id is a random UUID — never derived from a person, account, network
11
+ * address, or device signal, never joined to identity-verification records — with a
12
+ * fixed 90-day lifetime (no sliding renewal). It is scoped to one tenant, environment
13
+ * and site surface, so two tenants sharing an origin get independent, unlinkable ids.
14
+ * It is read or minted only while analytics is eligible; an update into ineligibility
15
+ * clears it, and the renderer's early consent boot clears it on every load where
16
+ * analytics is ineligible (denial, Global Privacy Control, tenant not approved).
17
+ * Storage is injected by the host and never touched by name here; when it is
18
+ * unavailable the id lives in memory for this document only and the event carries
19
+ * no session (`NO_SESSION_ID`), overcounting visitors rather than failing the event.
20
+ * There is no `user_id`.
9
21
  *
10
22
  * Page views also carry channel ATTRIBUTION (UTM tokens + the referrer's origin)
11
23
  * and the page LOCATION (`page_hostname` / `page_url`, from the server-stamped
@@ -14,10 +26,7 @@
14
26
  * mappings apply unchanged. These are labels about the page and the campaign, not
15
27
  * about the visitor: attribution is minimized and validated upstream
16
28
  * (`events.ts` `validatePageAttribution`) and re-screened for PII here, and the
17
- * host never comes from the browser. A persistent/session identity is a different thing entirely —
18
- * it stays out, on purpose: `device_id` remains the fixed, non-identifying value
19
- * below and there is still no `session_id`, `user_id`, or any persisted client
20
- * value anywhere in this file.
29
+ * host never comes from the browser.
21
30
  */
22
31
  import type { CommerceEventBroker, CommerceEventSubscription } from "./commerce-broker.js";
23
32
  import {
@@ -52,7 +61,18 @@ export const AMPLITUDE_COLLECTION_URL = `${AMPLITUDE_COLLECTION_ORIGIN}/2/httpap
52
61
  export const AMPLITUDE_HTTP_ENTRY_URL = "/platform/amplitude-http/v1/entry.mjs" as const;
53
62
  export const AMPLITUDE_HTTP_ADAPTER_VERSION = "1.0.0" as const;
54
63
  export const AMPLITUDE_HTTP_RUNTIME_ID = "native-amplitude-http-v1" as const;
64
+ /** Sent only when the browser offers no randomness to mint a visitor id from. */
55
65
  const ANONYMOUS_DEVICE_ID = "storefront-anonymous" as const;
66
+ /** Amplitude's documented "not in a session" value. */
67
+ export const NO_SESSION_ID = -1 as const;
68
+ /**
69
+ * Storage key prefix for the visitor record. The early consent boot
70
+ * (`consent-broker.ts` `applyEarlyViewerAnalyticsConsent`) removes the same key as a
71
+ * literal; a test pins the two together.
72
+ */
73
+ export const VISITOR_CONTINUITY_STORAGE_KEY = "tot.analytics-continuity.v1" as const;
74
+ export const VISITOR_ID_LIFETIME_MS = 90 * 24 * 60 * 60 * 1000;
75
+ export const SESSION_INACTIVITY_MS = 30 * 60 * 1000;
56
76
  export const AMPLITUDE_HTTP_EVENT_TYPES = Object.freeze([
57
77
  "storefront.page.viewed.v1",
58
78
  "storefront.page.viewed.v2",
@@ -79,7 +99,10 @@ export interface AmplitudePageLocationProperties {
79
99
 
80
100
  export interface AmplitudeHttpEvent {
81
101
  event_type: "page_view" | (typeof APPROVED_MARKETING_ACTION_NAMES)[number];
82
- device_id: typeof ANONYMOUS_DEVICE_ID;
102
+ /** Pseudonymous visitor UUID, or the anonymous value when no randomness exists. */
103
+ device_id: string;
104
+ /** Session start, epoch milliseconds, or `NO_SESSION_ID`. */
105
+ session_id: number;
83
106
  insert_id: string;
84
107
  /** Occurrence time, epoch milliseconds. */
85
108
  time: number;
@@ -134,6 +157,141 @@ class BoundedInsertIds {
134
157
  }
135
158
  }
136
159
 
160
+ /** The host-supplied persistent store; the browser host passes its own origin storage. */
161
+ export interface VisitorContinuityStorage {
162
+ getItem(key: string): string | null;
163
+ setItem(key: string, value: string): void;
164
+ removeItem(key: string): void;
165
+ }
166
+
167
+ /** The consent context a visitor id belongs to: the consent broker's `contextKey` triple. */
168
+ export interface VisitorContinuityScope {
169
+ tenantPublicId: string;
170
+ environment: string;
171
+ siteSurface: string;
172
+ }
173
+
174
+ export interface VisitorContinuityIds {
175
+ deviceId: string;
176
+ sessionId: number;
177
+ }
178
+
179
+ export interface VisitorContinuity {
180
+ /** Read the scope's visitor, minting or rolling it as needed, and record activity at `now`. */
181
+ readOrMint(scope: VisitorContinuityScope, now: number): VisitorContinuityIds;
182
+ /** Forget the scope's visitor, persisted and in memory. */
183
+ clear(scope: VisitorContinuityScope): void;
184
+ }
185
+
186
+ export interface CreateVisitorContinuityOptions {
187
+ /** Absent, or returning undefined, keeps the visitor in memory for this document only. */
188
+ storage?: () => VisitorContinuityStorage | undefined;
189
+ randomUUID?: () => string;
190
+ }
191
+
192
+ interface VisitorRecord {
193
+ schemaVersion: 1;
194
+ deviceId: string;
195
+ createdAt: number;
196
+ sessionStartedAt: number;
197
+ lastActivityAt: number;
198
+ }
199
+
200
+ const VISITOR_RECORD_KEYS = "createdAt,deviceId,lastActivityAt,schemaVersion,sessionStartedAt";
201
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
202
+
203
+ export function visitorContinuityStorageKey(scope: VisitorContinuityScope): string {
204
+ const contextKey = `${scope.tenantPublicId}:${scope.environment}:${scope.siteSurface}`;
205
+ return `${VISITOR_CONTINUITY_STORAGE_KEY}:${encodeURIComponent(contextKey)}`;
206
+ }
207
+
208
+ /** A stored record that is malformed, from the future, or past its fixed lifetime is not a visitor. */
209
+ function liveVisitorRecord(raw: string | null, now: number): VisitorRecord | null {
210
+ if (!raw) return null;
211
+ let parsed: unknown;
212
+ try { parsed = JSON.parse(raw); } catch { return null; }
213
+ if (!isPlainObject(parsed) || Object.keys(parsed).sort().join(",") !== VISITOR_RECORD_KEYS) return null;
214
+ const { schemaVersion, deviceId, createdAt, sessionStartedAt, lastActivityAt } = parsed;
215
+ if (
216
+ schemaVersion !== 1 || typeof deviceId !== "string" || !UUID_PATTERN.test(deviceId) ||
217
+ typeof createdAt !== "number" || typeof sessionStartedAt !== "number" ||
218
+ typeof lastActivityAt !== "number" ||
219
+ !Number.isFinite(createdAt) || !Number.isFinite(sessionStartedAt) || !Number.isFinite(lastActivityAt) ||
220
+ createdAt > now || now - createdAt >= VISITOR_ID_LIFETIME_MS ||
221
+ sessionStartedAt < createdAt || lastActivityAt < sessionStartedAt
222
+ ) return null;
223
+ return { schemaVersion, deviceId, createdAt, sessionStartedAt, lastActivityAt };
224
+ }
225
+
226
+ function defaultRandomUUID(): string {
227
+ return globalThis.crypto.randomUUID();
228
+ }
229
+
230
+ /**
231
+ * The visitor/session store behind `device_id` + `session_id`. Every storage access is
232
+ * guarded: a store that is missing, blocked, full, or throwing degrades to a
233
+ * memory-only id for this document with no session, and never fails the event.
234
+ */
235
+ export function createVisitorContinuity(options: CreateVisitorContinuityOptions = {}): VisitorContinuity {
236
+ const randomUUID = options.randomUUID ?? defaultRandomUUID;
237
+ const memory = new Map<string, string>();
238
+ const storage = (): VisitorContinuityStorage | undefined => {
239
+ try { return options.storage?.(); } catch { return undefined; }
240
+ };
241
+ const mint = (): string | null => {
242
+ try {
243
+ const id = randomUUID().toLowerCase();
244
+ return UUID_PATTERN.test(id) ? id : null;
245
+ } catch {
246
+ return null;
247
+ }
248
+ };
249
+
250
+ return {
251
+ readOrMint(scope, now) {
252
+ const key = visitorContinuityStorageKey(scope);
253
+ const store = storage();
254
+ if (store) {
255
+ try {
256
+ const existing = liveVisitorRecord(store.getItem(key), now);
257
+ let record: VisitorRecord | null = existing;
258
+ if (!record) {
259
+ const deviceId = mint();
260
+ if (deviceId) {
261
+ record = { schemaVersion: 1, deviceId, createdAt: now, sessionStartedAt: now, lastActivityAt: now };
262
+ }
263
+ } else {
264
+ const sessionStartedAt = now - record.lastActivityAt > SESSION_INACTIVITY_MS
265
+ ? now
266
+ : record.sessionStartedAt;
267
+ record = { ...record, sessionStartedAt, lastActivityAt: Math.max(now, record.lastActivityAt) };
268
+ }
269
+ if (record) {
270
+ store.setItem(key, JSON.stringify(record));
271
+ memory.delete(key);
272
+ return { deviceId: record.deviceId, sessionId: record.sessionStartedAt };
273
+ }
274
+ } catch {
275
+ // Blocked or full storage: fall through to this document's memory-only visitor.
276
+ }
277
+ }
278
+ let deviceId = memory.get(key);
279
+ if (!deviceId) {
280
+ const minted = mint();
281
+ if (!minted) return { deviceId: ANONYMOUS_DEVICE_ID, sessionId: NO_SESSION_ID };
282
+ deviceId = minted;
283
+ memory.set(key, deviceId);
284
+ }
285
+ return { deviceId, sessionId: NO_SESSION_ID };
286
+ },
287
+ clear(scope) {
288
+ const key = visitorContinuityStorageKey(scope);
289
+ memory.delete(key);
290
+ try { storage()?.removeItem(key); } catch { /* the early consent boot clears on the next load */ }
291
+ },
292
+ };
293
+ }
294
+
137
295
  function isPlainObject(value: unknown): value is Record<string, unknown> {
138
296
  return typeof value === "object" && value !== null && !Array.isArray(value);
139
297
  }
@@ -187,7 +345,9 @@ function opaqueEventTime(epochMs: number): string {
187
345
  /**
188
346
  * Reduce a broker event to the complete Amplitude allowlist. Returning null is
189
347
  * fail-closed for unknown events, invalid/extra data, consent loss, environment
190
- * disagreement, unsafe paths, or unapproved action names.
348
+ * disagreement, unsafe paths, or unapproved action names. The result carries the
349
+ * anonymous identity; the sending handler replaces it with the visitor's ids only
350
+ * after the event is admitted, so a refused event never reads or mints one.
191
351
  */
192
352
  export function mapCommerceEventToAmplitude(
193
353
  input: unknown,
@@ -220,6 +380,7 @@ export function mapCommerceEventToAmplitude(
220
380
  if (!Number.isFinite(time)) return null;
221
381
  const base = {
222
382
  device_id: ANONYMOUS_DEVICE_ID,
383
+ session_id: NO_SESSION_ID,
223
384
  insert_id: `${eventDedupeKey(event)}-${opaqueEventTime(time)}`,
224
385
  time,
225
386
  };
@@ -255,6 +416,8 @@ export interface RegisterAmplitudeHttpDestinationInput {
255
416
  environment: BrowserEnvironment;
256
417
  fetch: AmplitudeFetch;
257
418
  onDiagnostic?: AmplitudeDiagnosticObserver;
419
+ /** Defaults to a memory-only store: one visitor per document, no session. */
420
+ continuity?: VisitorContinuity;
258
421
  /** Test seam; production is clamped to the fixed default. */
259
422
  dedupeCapacity?: number;
260
423
  }
@@ -277,15 +440,27 @@ interface AmplitudeHandlerInput {
277
440
  fetcher: AmplitudeFetch;
278
441
  observer?: AmplitudeDiagnosticObserver;
279
442
  dedupe: BoundedInsertIds;
443
+ continuity: VisitorContinuity;
280
444
  isActive(): boolean;
281
445
  }
282
446
 
447
+ function continuityScope(tenantPublicId: string, environment: BrowserEnvironment): VisitorContinuityScope {
448
+ return { tenantPublicId, environment: environment.environment, siteSurface: environment.siteSurface };
449
+ }
450
+
283
451
  function createAmplitudeHandler(input: AmplitudeHandlerInput): (event: CommerceEvent) => void {
452
+ const scope = continuityScope(input.tenantPublicId, input.environment);
284
453
  return (event) => {
285
454
  if (!input.isActive()) return;
286
455
  const mapped = mapCommerceEventToAmplitude(event, input.environment, input.tenantPublicId);
287
456
  if (!mapped || input.dedupe.seenOrRemember(mapped.insert_id)) return;
288
- const body = JSON.stringify({ api_key: input.destinationId, events: [mapped] });
457
+ let identity: VisitorContinuityIds;
458
+ try { identity = input.continuity.readOrMint(scope, mapped.time); }
459
+ catch { identity = { deviceId: ANONYMOUS_DEVICE_ID, sessionId: NO_SESSION_ID }; }
460
+ const body = JSON.stringify({
461
+ api_key: input.destinationId,
462
+ events: [{ ...mapped, device_id: identity.deviceId, session_id: identity.sessionId }],
463
+ });
289
464
  try {
290
465
  void input.fetcher(AMPLITUDE_COLLECTION_URL, {
291
466
  method: "POST",
@@ -352,6 +527,7 @@ export function registerAmplitudeHttpDestination(
352
527
  fetcher,
353
528
  observer,
354
529
  dedupe,
530
+ continuity: input.continuity ?? createVisitorContinuity(),
355
531
  isActive: () => !disposed,
356
532
  }),
357
533
  },
@@ -372,6 +548,8 @@ export function registerAmplitudeHttpDestination(
372
548
  export interface CreateAmplitudeLifecycleModuleOptions {
373
549
  fetch?: AmplitudeFetch;
374
550
  onDiagnostic?: AmplitudeDiagnosticObserver;
551
+ /** The host's visitor store; defaults to memory-only (one visitor per document, no session). */
552
+ continuity?: VisitorContinuity;
375
553
  /** Narrow test seam; every value is clamped to a finite production ceiling. */
376
554
  dedupeCapacity?: number;
377
555
  }
@@ -390,6 +568,7 @@ export function createAmplitudeLifecycleModule(
390
568
  const fetcher: AmplitudeFetch = options.fetch ?? ((url, init) => globalThis.fetch(url, init));
391
569
  const observer = options.onDiagnostic;
392
570
  const dedupe = new BoundedInsertIds(options.dedupeCapacity ?? AMPLITUDE_DEDUPE_CAPACITY);
571
+ const continuity = options.continuity ?? createVisitorContinuity();
393
572
  let state: AmplitudeLifecycleState = "idle";
394
573
  let generation = 0;
395
574
  let unsubscribe: (() => void) | undefined;
@@ -407,7 +586,8 @@ export function createAmplitudeLifecycleModule(
407
586
  }
408
587
  };
409
588
 
410
- const bind = (ctx: IntegrationLifecycleContext): void => {
589
+ /** `clearIfIneligible`: an update that loses analytics eligibility forgets the visitor. */
590
+ const bind = (ctx: IntegrationLifecycleContext, clearIfIneligible: boolean): void => {
411
591
  let subscribe: IntegrationEventSubscribe | undefined;
412
592
  let environment: ReturnType<typeof validateBrowserEnvironment>;
413
593
  let destinationId: string | null;
@@ -415,6 +595,12 @@ export function createAmplitudeLifecycleModule(
415
595
  environment = validateBrowserEnvironment(ctx.environment);
416
596
  const eligible = ctx.consent.unknownState === "block" &&
417
597
  ctx.consent.eligiblePurposes.includes("analytics");
598
+ if (
599
+ !eligible && clearIfIneligible && environment.ok &&
600
+ typeof ctx.tenantPublicId === "string" && ctx.tenantPublicId.length > 0
601
+ ) {
602
+ continuity.clear(continuityScope(ctx.tenantPublicId, environment.value));
603
+ }
418
604
  subscribe = ctx.subscribe;
419
605
  destinationId = environment.ok
420
606
  ? soleDestinationId(ctx.config, environment.value.environment)
@@ -436,6 +622,7 @@ export function createAmplitudeLifecycleModule(
436
622
  fetcher,
437
623
  observer,
438
624
  dedupe,
625
+ continuity,
439
626
  isActive: () => state === "initialized" && generation === ownGeneration,
440
627
  });
441
628
  try {
@@ -456,12 +643,12 @@ export function createAmplitudeLifecycleModule(
456
643
  init(ctx) {
457
644
  if (state !== "idle") return;
458
645
  state = "initialized";
459
- bind(ctx);
646
+ bind(ctx, false);
460
647
  },
461
648
  update(_reason, ctx) {
462
649
  if (state !== "initialized") return;
463
650
  if (!teardown()) return;
464
- bind(ctx);
651
+ bind(ctx, true);
465
652
  },
466
653
  dispose() {
467
654
  if (state === "disposed") return;
@@ -15,6 +15,7 @@ import {
15
15
  AMPLITUDE_HTTP_ENTRY_URL,
16
16
  AMPLITUDE_HTTP_RUNTIME_ID,
17
17
  createAmplitudeLifecycleModule,
18
+ type VisitorContinuity,
18
19
  } from "./native-amplitude.js";
19
20
  import {
20
21
  createNativeGa4Module,
@@ -40,6 +41,8 @@ export interface PlatformAdapterEntryMarker {
40
41
  export interface PlatformAdapterRuntimeHost {
41
42
  fetch?: typeof fetch;
42
43
  nativeGa4Transport?: NativeGa4Transport;
44
+ /** The host's persistent visitor store for Amplitude; absent keeps visitors memory-only. */
45
+ visitorContinuity?: VisitorContinuity;
43
46
  }
44
47
 
45
48
  export interface PlatformAdapterRuntimeRegistration {
@@ -78,9 +81,9 @@ export const PLATFORM_ADAPTER_RUNTIME_REGISTRY: readonly PlatformAdapterRuntimeR
78
81
  }),
79
82
  Object.freeze({
80
83
  marker: AMPLITUDE_MARKER,
81
- create: ({ fetch: fetchCapability }: PlatformAdapterRuntimeHost) => {
84
+ create: ({ fetch: fetchCapability, visitorContinuity }: PlatformAdapterRuntimeHost) => {
82
85
  if (!fetchCapability) throw new Error("platform_adapter_fetch_unavailable");
83
- return createAmplitudeLifecycleModule({ fetch: fetchCapability });
86
+ return createAmplitudeLifecycleModule({ fetch: fetchCapability, continuity: visitorContinuity });
84
87
  },
85
88
  }),
86
89
  ]);
@@ -88,6 +91,7 @@ export const PLATFORM_ADAPTER_RUNTIME_REGISTRY: readonly PlatformAdapterRuntimeR
88
91
  export interface PlatformAdapterRuntimeLoaderOptions {
89
92
  fetchCapability?: typeof fetch;
90
93
  nativeGa4Transport?: () => NativeGa4Transport;
94
+ visitorContinuity?: VisitorContinuity;
91
95
  importModule?: (url: string) => Promise<unknown>;
92
96
  fallback: IntegrationModuleLoader;
93
97
  }
@@ -150,6 +154,9 @@ export function makePlatformAdapterRuntimeLoader(
150
154
  nativeGa4Transport: registration.marker.appId === GOOGLE_MEASUREMENT_ADAPTER.adapterId
151
155
  ? options.nativeGa4Transport?.()
152
156
  : undefined,
157
+ visitorContinuity: registration.marker.appId === AMPLITUDE_HTTP_ADAPTER.adapterId
158
+ ? options.visitorContinuity
159
+ : undefined,
153
160
  });
154
161
  };
155
162
  }