@cross-deck/web 1.6.4 → 1.8.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/dist/react.mjs CHANGED
@@ -11,6 +11,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
11
11
  this.requestId = payload.requestId;
12
12
  this.status = payload.status;
13
13
  this.retryAfterMs = payload.retryAfterMs;
14
+ this.minVersion = payload.minVersion;
15
+ this.surface = payload.surface;
14
16
  Object.setPrototypeOf(this, _CrossdeckError.prototype);
15
17
  }
16
18
  };
@@ -31,7 +33,10 @@ async function crossdeckErrorFromResponse(res) {
31
33
  message: envelope.message ?? `HTTP ${res.status}`,
32
34
  requestId: envelope.request_id ?? requestId,
33
35
  status: res.status,
34
- retryAfterMs
36
+ retryAfterMs,
37
+ // PARK metadata, present only on a 426 / sdk_version_unsupported body.
38
+ minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
39
+ surface: typeof envelope.surface === "string" ? envelope.surface : void 0
35
40
  });
36
41
  }
37
42
  return new CrossdeckError({
@@ -67,7 +72,7 @@ function typeMapForStatus(status) {
67
72
  }
68
73
 
69
74
  // src/_version.ts
70
- var SDK_VERSION = "1.6.4";
75
+ var SDK_VERSION = "1.8.0";
71
76
  var SDK_NAME = "@cross-deck/web";
72
77
 
73
78
  // src/http.ts
@@ -907,6 +912,18 @@ var EventQueue = class {
907
912
  this.cancelTimer = null;
908
913
  this.firstFlushFired = false;
909
914
  this.nextRetryAt = null;
915
+ /**
916
+ * PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
917
+ * queue stops flushing — the server has told us our wire dialect is too
918
+ * old, and retrying the same payload only burns the user's battery and
919
+ * bandwidth and drips pointless rejects into the server logs until the
920
+ * app ships an upgraded SDK. The held events stay durable (persisted)
921
+ * and are delivered by the next boot's rehydrate, post-upgrade. Parked
922
+ * is per-instance: a fresh boot (the upgraded build) starts unparked.
923
+ */
924
+ this.parked = false;
925
+ /** One developer-facing console warning per instance — never per-event spam. */
926
+ this.parkWarned = false;
910
927
  this.retry = new RetryPolicy(cfg.retry ?? {});
911
928
  this.persistent = cfg.persistentStore ?? null;
912
929
  if (this.persistent) {
@@ -962,6 +979,7 @@ var EventQueue = class {
962
979
  * flushes (pagehide / visibilitychange→hidden / beforeunload).
963
980
  */
964
981
  async flush(options = {}) {
982
+ if (this.parked) return null;
965
983
  let batch;
966
984
  let batchId;
967
985
  if (this.pendingBatch !== null && this.pendingBatchId !== null) {
@@ -983,9 +1001,13 @@ var EventQueue = class {
983
1001
  const env = this.cfg.envelope();
984
1002
  const result = await this.cfg.http.request("POST", "/events", {
985
1003
  body: {
986
- // NorthStar §13.1 batch envelope. The backend validates these
987
- // against the API-key-resolved app and rejects mismatches
988
- // loudly (env_mismatch).
1004
+ // Event Envelope v1 batch envelope (backend/docs/
1005
+ // event-envelope-spec-v1.md §1). `envelopeVersion` is the
1006
+ // schema/wire version the server parses against ("can I parse
1007
+ // this?") and is DISTINCT from `sdk.version` ("which build is in
1008
+ // the wild?") — two questions, two fields, never conflated. The
1009
+ // backend refuses payloads with a missing/unknown envelopeVersion.
1010
+ envelopeVersion: 1,
989
1011
  appId: env.appId,
990
1012
  environment: env.environment,
991
1013
  sdk: env.sdk,
@@ -1009,6 +1031,29 @@ var EventQueue = class {
1009
1031
  } catch (err) {
1010
1032
  const message = err instanceof Error ? err.message : String(err);
1011
1033
  this.lastError = message;
1034
+ if (isVersionRejected(err)) {
1035
+ this.parked = true;
1036
+ this.buffer = [...batch, ...this.buffer];
1037
+ if (this.buffer.length > HARD_BUFFER_CAP) {
1038
+ const overflow = this.buffer.length - HARD_BUFFER_CAP;
1039
+ this.buffer.splice(0, overflow);
1040
+ this.dropped += overflow;
1041
+ }
1042
+ this.pendingBatch = null;
1043
+ this.pendingBatchId = null;
1044
+ this.inFlight -= batch.length;
1045
+ this.persistAll();
1046
+ this.cfg.onBufferChange?.(this.buffer.length);
1047
+ const minVersion = versionRejectionFloor(err);
1048
+ if (!this.parkWarned) {
1049
+ this.parkWarned = true;
1050
+ console.warn(
1051
+ `[Crossdeck] SDK outdated \u2014 the server is no longer accepting this version's event format. Your events are PARKED on-device (held, not lost) and will deliver automatically once you update @cross-deck/web${minVersion ? ` to >= ${minVersion}` : ""} and redeploy.`
1052
+ );
1053
+ }
1054
+ this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
1055
+ return null;
1056
+ }
1012
1057
  if (isPermanent4xx(err)) {
1013
1058
  const droppedCount = batch.length;
1014
1059
  this.pendingBatch = null;
@@ -1127,8 +1172,22 @@ function isPermanent4xx(err) {
1127
1172
  if (typeof status !== "number" || !Number.isFinite(status)) return false;
1128
1173
  if (status < 400 || status >= 500) return false;
1129
1174
  if (status === 408 || status === 429) return false;
1175
+ if (status === 426) return false;
1130
1176
  return true;
1131
1177
  }
1178
+ function isVersionRejected(err) {
1179
+ if (!err || typeof err !== "object") return false;
1180
+ if (err.status === 426) return true;
1181
+ return err.code === "sdk_version_unsupported";
1182
+ }
1183
+ function versionRejectionFloor(err) {
1184
+ const v = err?.minVersion;
1185
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1186
+ }
1187
+ function versionRejectionSurface(err) {
1188
+ const v = err?.surface;
1189
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1190
+ }
1132
1191
  function defaultScheduler(fn, ms) {
1133
1192
  const id = setTimeout(fn, ms);
1134
1193
  if (typeof id.unref === "function") {
@@ -1440,6 +1499,15 @@ var AutoTracker = class {
1440
1499
  this.cleanups = [];
1441
1500
  /** Last time we flushed lastActivityAt to storage (throttle gate). */
1442
1501
  this.lastPersistAt = 0;
1502
+ /**
1503
+ * Event Envelope v1 §3 — per-session monotonic sequence counter.
1504
+ * The single owner of the seq counter lives here, alongside all other
1505
+ * session state. Incremented atomically at track() time (via nextSeq()),
1506
+ * reset to 0 whenever a new session boundary is crossed (startNewSession
1507
+ * path + resetSession). Persists across background/foreground within a
1508
+ * session — only a real session boundary (not a tab hide/show) resets it.
1509
+ */
1510
+ this._sessionSeq = 0;
1443
1511
  /**
1444
1512
  * Stable per-page-view identifier. Minted at every `page.viewed`
1445
1513
  * emission and attached to every subsequent event until the next
@@ -1524,6 +1592,20 @@ var AutoTracker = class {
1524
1592
  get currentAcquisition() {
1525
1593
  return this.session?.acquisition ?? EMPTY_ACQUISITION;
1526
1594
  }
1595
+ /**
1596
+ * Event Envelope v1 §3 — return the next seq value for the current session
1597
+ * and advance the counter. Called SYNCHRONOUSLY at track() time, before any
1598
+ * async dispatch, so the seq assignment order is deterministic (call order,
1599
+ * not scheduler luck). The counter is reset to 0 at every session boundary;
1600
+ * it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
1601
+ * reset seq). When no session is active (Node, before init, after uninstall),
1602
+ * returns 0 and leaves the counter unchanged — fallback, not an error.
1603
+ */
1604
+ nextSeq() {
1605
+ const seq = this._sessionSeq;
1606
+ this._sessionSeq += 1;
1607
+ return seq;
1608
+ }
1527
1609
  // ---------- sessions ----------
1528
1610
  installSessionTracking() {
1529
1611
  const now = Date.now();
@@ -1575,6 +1657,7 @@ var AutoTracker = class {
1575
1657
  }
1576
1658
  startNewSession() {
1577
1659
  const now = Date.now();
1660
+ this._sessionSeq = 0;
1578
1661
  return {
1579
1662
  sessionId: mintSessionId(),
1580
1663
  startedAt: now,
@@ -2303,6 +2386,40 @@ function writeJson(storage, key, value) {
2303
2386
  }
2304
2387
  }
2305
2388
 
2389
+ // src/internal-opt-out.ts
2390
+ var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
2391
+ var STORAGE_KEY = "crossdeck.internalOptOut";
2392
+ function localStore() {
2393
+ try {
2394
+ return typeof localStorage !== "undefined" ? localStorage : null;
2395
+ } catch {
2396
+ return null;
2397
+ }
2398
+ }
2399
+ function processInternalOptOutUrl() {
2400
+ try {
2401
+ const search = typeof location !== "undefined" ? location.search : "";
2402
+ const params = new URLSearchParams(search || "");
2403
+ if (!params.has("crossdeck_internal")) return;
2404
+ const store = localStore();
2405
+ if (!store) return;
2406
+ const v = params.get("crossdeck_internal");
2407
+ if (v === "1" || v === "true") {
2408
+ store.setItem(STORAGE_KEY, "1");
2409
+ } else if (v === "0" || v === "false") {
2410
+ store.removeItem(STORAGE_KEY);
2411
+ }
2412
+ } catch {
2413
+ }
2414
+ }
2415
+ function isInternalOptOut() {
2416
+ try {
2417
+ return localStore()?.getItem(STORAGE_KEY) === "1";
2418
+ } catch {
2419
+ return false;
2420
+ }
2421
+ }
2422
+
2306
2423
  // src/web-vitals.ts
2307
2424
  var WebVitalsTracker = class {
2308
2425
  constructor(cfg, report) {
@@ -2811,6 +2928,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
2811
2928
  description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
2812
2929
  resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
2813
2930
  retryable: false
2931
+ },
2932
+ {
2933
+ code: "sdk_version_unsupported",
2934
+ type: "version_error",
2935
+ description: "HTTP 426 \u2014 your installed SDK sends an event format the server no longer accepts. The data is good; only the wire dialect is too old. The SDK PARKS automatically: events are held on-device (paused, not lost) and deliver on the next launch after you upgrade.",
2936
+ resolution: "Update @cross-deck/web to at least the version in error.minVersion and redeploy \u2014 the parked queue backfills automatically. See https://cross-deck.com/docs/sdk-event-durability/.",
2937
+ retryable: false
2814
2938
  }
2815
2939
  ]);
2816
2940
  function getErrorCode(code) {
@@ -2833,15 +2957,6 @@ var VerifierReporter = class {
2833
2957
  constructor(ctx) {
2834
2958
  this.ctx = ctx;
2835
2959
  this.reentrancyDepth = 0;
2836
- /** The live verifier set — attached by the SDK right after init so the
2837
- * emit path can schema-lock its OWN outgoing telemetry against the real
2838
- * payload. Null until attached (boot-time reports still work without it). */
2839
- this.verifiers = null;
2840
- }
2841
- /** Hand the reporter the live verifier set so `reportFail` can run the
2842
- * `onReportContractFailure` hook on the exact payload it's about to emit. */
2843
- attachVerifiers(verifiers) {
2844
- this.verifiers = verifiers;
2845
2960
  }
2846
2961
  /**
2847
2962
  * Report a verifier result. Pass `operation` for hot-path results
@@ -2870,7 +2985,7 @@ var VerifierReporter = class {
2870
2985
  if (this.reentrancyDepth > 0) return;
2871
2986
  this.reentrancyDepth += 1;
2872
2987
  try {
2873
- const payload = {
2988
+ this.ctx.emitTelemetry({
2874
2989
  contract_id: result.contractId,
2875
2990
  sdk_version: this.ctx.sdkVersion,
2876
2991
  sdk_platform: "web",
@@ -2878,13 +2993,7 @@ var VerifierReporter = class {
2878
2993
  run_context: this.ctx.runContext,
2879
2994
  run_id: this.ctx.runId,
2880
2995
  verification_phase: phase
2881
- };
2882
- if (this.verifiers) {
2883
- runOnReportContractFailure(this.verifiers, this, this.ctx, {
2884
- outgoingPayload: payload
2885
- });
2886
- }
2887
- this.ctx.emitTelemetry(payload);
2996
+ });
2888
2997
  } finally {
2889
2998
  this.reentrancyDepth -= 1;
2890
2999
  }
@@ -3174,31 +3283,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
3174
3283
  "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
3175
3284
  nowMs() - t0
3176
3285
  );
3177
- },
3178
- // Hot-path hook — on every real track() we re-affirm the LIVE configured
3179
- // flush interval (carried on the observation), so the parity guarantee is
3180
- // verified against the running SDK in the customer flow, not just a
3181
- // canonical constant at boot. It can't change per-event, but observing it
3182
- // per-event is what makes the contract genuinely runtime-verified.
3183
- hooks: {
3184
- onTrack(obs) {
3185
- const t0 = nowMs();
3186
- const CANONICAL_DEFAULT_MS = 2e3;
3187
- const ms = obs.flushIntervalMs;
3188
- if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
3189
- return fail(
3190
- "flush-interval-parity",
3191
- `live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
3192
- nowMs() - t0
3193
- );
3194
- }
3195
- return pass(
3196
- "flush-interval-parity",
3197
- ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
3198
- nowMs() - t0
3199
- );
3200
- }
3201
3286
  }
3287
+ // No hot-path hook — the flush interval is set once at start() and
3288
+ // never changes per-operation.
3202
3289
  };
3203
3290
  function buildFlushIntervalVerifier(configuredIntervalMs) {
3204
3291
  return {
@@ -3325,26 +3412,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
3325
3412
  "referrer",
3326
3413
  "deviceId"
3327
3414
  ];
3328
- function contractFailedSchemaViolation(keys) {
3329
- const allowed = /* @__PURE__ */ new Set([
3330
- ...CONTRACT_FAILED_REQUIRED_FIELDS,
3331
- ...CONTRACT_FAILED_OPTIONAL_FIELDS
3332
- ]);
3333
- const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3334
- for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3335
- if (!keys.includes(required)) return `missing required field: ${required}`;
3336
- }
3337
- for (const k of keys) {
3338
- if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
3339
- }
3340
- for (const k of keys) {
3341
- if (!allowed.has(k)) {
3342
- return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
3343
- }
3344
- }
3345
- return null;
3346
- }
3347
- var CONTRACT_FAILED_OK_EVIDENCE = `fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`;
3348
3415
  var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3349
3416
  contractId: "contract-failed-payload-schema-lock",
3350
3417
  bootTest() {
@@ -3359,35 +3426,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3359
3426
  verification_phase: "boot"
3360
3427
  };
3361
3428
  const keys = Object.keys(syntheticPayload);
3362
- const violation = contractFailedSchemaViolation(keys);
3363
- if (violation) {
3364
- return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3429
+ const allowed = /* @__PURE__ */ new Set([
3430
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
3431
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
3432
+ ]);
3433
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3434
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3435
+ if (!keys.includes(required)) {
3436
+ return fail(
3437
+ "contract-failed-payload-schema-lock",
3438
+ `missing required field: ${required}`,
3439
+ nowMs() - t0
3440
+ );
3441
+ }
3442
+ }
3443
+ for (const k of keys) {
3444
+ if (forbidden.has(k)) {
3445
+ return fail(
3446
+ "contract-failed-payload-schema-lock",
3447
+ `forbidden field on wire: ${k}`,
3448
+ nowMs() - t0
3449
+ );
3450
+ }
3451
+ }
3452
+ for (const k of keys) {
3453
+ if (!allowed.has(k)) {
3454
+ return fail(
3455
+ "contract-failed-payload-schema-lock",
3456
+ `unrecognised field on wire: ${k} (not in required \u222A optional)`,
3457
+ nowMs() - t0
3458
+ );
3459
+ }
3365
3460
  }
3366
3461
  return pass(
3367
3462
  "contract-failed-payload-schema-lock",
3368
- `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3463
+ `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
3369
3464
  nowMs() - t0
3370
3465
  );
3371
- },
3372
- // Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
3373
- // inside the re-entrancy guard) against the exact payload about to go on
3374
- // the wire. This is the assertion the guard at reportFail was built in
3375
- // anticipation of: every reliability-channel write is schema-locked in the
3376
- // field, against real data, not just the synthetic boot mirror.
3377
- hooks: {
3378
- onReportContractFailure(obs) {
3379
- const t0 = nowMs();
3380
- const keys = Object.keys(obs.outgoingPayload);
3381
- const violation = contractFailedSchemaViolation(keys);
3382
- if (violation) {
3383
- return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3384
- }
3385
- return pass(
3386
- "contract-failed-payload-schema-lock",
3387
- `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3388
- nowMs() - t0
3389
- );
3390
- }
3391
3466
  }
3392
3467
  };
3393
3468
  var BACKEND_WIRE_CODES = Object.freeze([
@@ -3405,7 +3480,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
3405
3480
  "google_not_supported",
3406
3481
  "stripe_not_supported",
3407
3482
  "missing_required_param",
3408
- "invalid_param_value"
3483
+ "invalid_param_value",
3484
+ "sdk_version_unsupported"
3409
3485
  ]);
3410
3486
  var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3411
3487
  contractId: "sdk-error-codes-catalogue",
@@ -3438,39 +3514,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3438
3514
  nowMs() - t0
3439
3515
  );
3440
3516
  }
3441
- },
3442
- // Hot-path hook — when the SDK parses a real wire error, assert the code
3443
- // the customer actually hit carries usable remediation in the shipped
3444
- // catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
3445
- // codes are out of scope and pass. This turns the catalogue from a
3446
- // boot-only completeness claim into a per-error field assertion: a backend
3447
- // code that ships without remediation is caught the first time a customer
3448
- // hits it, not only if the off-by-default boot self-test ran.
3449
- hooks: {
3450
- onErrorParse(obs) {
3451
- const t0 = nowMs();
3452
- const code = obs.errorCode;
3453
- if (!code || !BACKEND_WIRE_CODES.includes(code)) {
3454
- return pass(
3455
- "sdk-error-codes-catalogue",
3456
- `wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
3457
- nowMs() - t0
3458
- );
3459
- }
3460
- const entry = getErrorCode(code);
3461
- if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
3462
- return fail(
3463
- "sdk-error-codes-catalogue",
3464
- `backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
3465
- nowMs() - t0
3466
- );
3467
- }
3468
- return pass(
3469
- "sdk-error-codes-catalogue",
3470
- `backend wire code "${code}" carries description + resolution`,
3471
- nowMs() - t0
3472
- );
3473
- }
3474
3517
  }
3475
3518
  };
3476
3519
  var STATIC_VERIFIERS = Object.freeze([
@@ -3594,24 +3637,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3594
3637
  reporter.report(result, "hot_path", "errorParse");
3595
3638
  }
3596
3639
  }
3597
- function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
3598
- if (ctx.disableContractAssertions) return;
3599
- for (const verifier of verifiers) {
3600
- const hook = verifier.hooks?.onReportContractFailure;
3601
- if (!hook) continue;
3602
- let result;
3603
- try {
3604
- result = hook(obs);
3605
- } catch (err) {
3606
- result = fail(
3607
- verifier.contractId,
3608
- `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3609
- 0
3610
- );
3611
- }
3612
- reporter.report(result, "hot_path", "reportContractFailure");
3613
- }
3614
- }
3615
3640
  function defaultDebugModeFlag() {
3616
3641
  try {
3617
3642
  if (typeof process !== "undefined" && process.env) {
@@ -3763,18 +3788,18 @@ function isInAppFrame(filename) {
3763
3788
  if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
3764
3789
  return true;
3765
3790
  }
3766
- function fingerprintError(message, frames, location) {
3791
+ function fingerprintError(message, frames, location2) {
3767
3792
  const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
3768
3793
  const parts = [
3769
3794
  (message || "").slice(0, 200),
3770
3795
  ...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
3771
3796
  ];
3772
- if (inAppFrames.length === 0 && location) {
3797
+ if (inAppFrames.length === 0 && location2) {
3773
3798
  const loc = [
3774
- location.errorType ?? "",
3775
- location.filename ?? "",
3776
- location.lineno ?? "",
3777
- location.colno ?? ""
3799
+ location2.errorType ?? "",
3800
+ location2.filename ?? "",
3801
+ location2.lineno ?? "",
3802
+ location2.colno ?? ""
3778
3803
  ].join(":");
3779
3804
  if (loc !== ":::") parts.push(loc);
3780
3805
  }
@@ -4444,9 +4469,6 @@ var CrossdeckClient = class {
4444
4469
  this.verifiers = null;
4445
4470
  this.verifierReporter = null;
4446
4471
  this.verifierCtx = null;
4447
- // The live configured event-flush interval, surfaced to the track-path
4448
- // verifier so flush-interval-parity validates the real cadence per event.
4449
- this.flushIntervalMs = 2e3;
4450
4472
  }
4451
4473
  /**
4452
4474
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -4616,6 +4638,13 @@ var CrossdeckClient = class {
4616
4638
  headline,
4617
4639
  { ...info }
4618
4640
  );
4641
+ },
4642
+ onParked: (info) => {
4643
+ debug.emit(
4644
+ "sdk.parked",
4645
+ `[crossdeck] SDK parked \u2014 server no longer accepts this version's event format. Events held on-device (paused, not lost); update @cross-deck/web${info.minVersion ? ` to >= ${info.minVersion}` : ""} to resume.`,
4646
+ { ...info }
4647
+ );
4619
4648
  }
4620
4649
  });
4621
4650
  const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
@@ -4623,6 +4652,7 @@ var CrossdeckClient = class {
4623
4652
  persistIdentity ? effectiveStorage : new MemoryStorage(),
4624
4653
  opts.storagePrefix
4625
4654
  );
4655
+ processInternalOptOutUrl();
4626
4656
  const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
4627
4657
  if (consent.isDntDenied) {
4628
4658
  debug.emit(
@@ -4726,8 +4756,6 @@ var CrossdeckClient = class {
4726
4756
  this.verifierReporter = new VerifierReporter(this.verifierCtx);
4727
4757
  const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4728
4758
  this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4729
- this.flushIntervalMs = opts.eventFlushIntervalMs;
4730
- this.verifierReporter.attachVerifiers(this.verifiers);
4731
4759
  if (localDevMode) {
4732
4760
  const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4733
4761
  if (verifyAtBootLocal && this.verifierReporter) {
@@ -5278,8 +5306,27 @@ var CrossdeckClient = class {
5278
5306
  );
5279
5307
  }
5280
5308
  }
5309
+ const seq = s.autoTracker?.nextSeq() ?? 0;
5310
+ const occurrenceTimestamp = Date.now();
5281
5311
  s.autoTracker?.markActivity();
5282
- const enriched = { ...s.deviceInfo };
5312
+ const wireContext = {};
5313
+ const di = s.deviceInfo;
5314
+ if (di.os) wireContext.os = di.os;
5315
+ if (di.osVersion) wireContext.osVersion = di.osVersion;
5316
+ const appVer = s.options.appVersion;
5317
+ if (appVer) wireContext.appVersion = appVer;
5318
+ wireContext.sdkName = SDK_NAME;
5319
+ wireContext.sdkVersion = s.options.sdkVersion;
5320
+ if (di.locale) wireContext.locale = di.locale;
5321
+ if (di.timezone) wireContext.timezone = di.timezone;
5322
+ if (di.browser) wireContext.browser = di.browser;
5323
+ if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
5324
+ const enriched = {};
5325
+ if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
5326
+ if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
5327
+ if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
5328
+ if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
5329
+ if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
5283
5330
  const sessionId = s.autoTracker?.currentSessionId;
5284
5331
  if (sessionId) enriched.sessionId = sessionId;
5285
5332
  const pageviewId = s.autoTracker?.currentPageviewId;
@@ -5302,17 +5349,24 @@ var CrossdeckClient = class {
5302
5349
  }
5303
5350
  }
5304
5351
  const supers = s.superProps.getSuperProperties();
5305
- Object.assign(enriched, supers);
5352
+ for (const k of Object.keys(supers)) {
5353
+ if (!(k in enriched)) enriched[k] = supers[k];
5354
+ }
5306
5355
  const groupIds = s.superProps.getGroupIds();
5307
5356
  if (Object.keys(groupIds).length > 0) {
5308
5357
  enriched.$groups = groupIds;
5309
5358
  }
5310
5359
  Object.assign(enriched, validation.properties);
5360
+ if (isInternalOptOut()) {
5361
+ enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
5362
+ }
5311
5363
  const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
5312
5364
  const event = {
5313
5365
  eventId: this.mintEventId(),
5314
5366
  name,
5315
- timestamp: Date.now(),
5367
+ timestamp: occurrenceTimestamp,
5368
+ seq,
5369
+ context: wireContext,
5316
5370
  properties: finalProperties
5317
5371
  };
5318
5372
  Object.assign(event, this.identityHintForEvent());
@@ -5322,8 +5376,7 @@ var CrossdeckClient = class {
5322
5376
  callerProperties: validation.properties,
5323
5377
  superProperties: supers,
5324
5378
  deviceProperties: s.deviceInfo,
5325
- mergedProperties: enriched,
5326
- flushIntervalMs: this.flushIntervalMs
5379
+ mergedProperties: enriched
5327
5380
  });
5328
5381
  }
5329
5382
  if (!isError && !isWebVital) {