@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/vue.cjs CHANGED
@@ -36,6 +36,8 @@ var CrossdeckError = class _CrossdeckError extends Error {
36
36
  this.requestId = payload.requestId;
37
37
  this.status = payload.status;
38
38
  this.retryAfterMs = payload.retryAfterMs;
39
+ this.minVersion = payload.minVersion;
40
+ this.surface = payload.surface;
39
41
  Object.setPrototypeOf(this, _CrossdeckError.prototype);
40
42
  }
41
43
  };
@@ -56,7 +58,10 @@ async function crossdeckErrorFromResponse(res) {
56
58
  message: envelope.message ?? `HTTP ${res.status}`,
57
59
  requestId: envelope.request_id ?? requestId,
58
60
  status: res.status,
59
- retryAfterMs
61
+ retryAfterMs,
62
+ // PARK metadata, present only on a 426 / sdk_version_unsupported body.
63
+ minVersion: typeof envelope.minVersion === "string" ? envelope.minVersion : void 0,
64
+ surface: typeof envelope.surface === "string" ? envelope.surface : void 0
60
65
  });
61
66
  }
62
67
  return new CrossdeckError({
@@ -92,7 +97,7 @@ function typeMapForStatus(status) {
92
97
  }
93
98
 
94
99
  // src/_version.ts
95
- var SDK_VERSION = "1.6.4";
100
+ var SDK_VERSION = "1.8.0";
96
101
  var SDK_NAME = "@cross-deck/web";
97
102
 
98
103
  // src/http.ts
@@ -932,6 +937,18 @@ var EventQueue = class {
932
937
  this.cancelTimer = null;
933
938
  this.firstFlushFired = false;
934
939
  this.nextRetryAt = null;
940
+ /**
941
+ * PARK state (HTTP 426 / `sdk_version_unsupported`). Once parked, the
942
+ * queue stops flushing — the server has told us our wire dialect is too
943
+ * old, and retrying the same payload only burns the user's battery and
944
+ * bandwidth and drips pointless rejects into the server logs until the
945
+ * app ships an upgraded SDK. The held events stay durable (persisted)
946
+ * and are delivered by the next boot's rehydrate, post-upgrade. Parked
947
+ * is per-instance: a fresh boot (the upgraded build) starts unparked.
948
+ */
949
+ this.parked = false;
950
+ /** One developer-facing console warning per instance — never per-event spam. */
951
+ this.parkWarned = false;
935
952
  this.retry = new RetryPolicy(cfg.retry ?? {});
936
953
  this.persistent = cfg.persistentStore ?? null;
937
954
  if (this.persistent) {
@@ -987,6 +1004,7 @@ var EventQueue = class {
987
1004
  * flushes (pagehide / visibilitychange→hidden / beforeunload).
988
1005
  */
989
1006
  async flush(options = {}) {
1007
+ if (this.parked) return null;
990
1008
  let batch;
991
1009
  let batchId;
992
1010
  if (this.pendingBatch !== null && this.pendingBatchId !== null) {
@@ -1008,9 +1026,13 @@ var EventQueue = class {
1008
1026
  const env = this.cfg.envelope();
1009
1027
  const result = await this.cfg.http.request("POST", "/events", {
1010
1028
  body: {
1011
- // NorthStar §13.1 batch envelope. The backend validates these
1012
- // against the API-key-resolved app and rejects mismatches
1013
- // loudly (env_mismatch).
1029
+ // Event Envelope v1 batch envelope (backend/docs/
1030
+ // event-envelope-spec-v1.md §1). `envelopeVersion` is the
1031
+ // schema/wire version the server parses against ("can I parse
1032
+ // this?") and is DISTINCT from `sdk.version` ("which build is in
1033
+ // the wild?") — two questions, two fields, never conflated. The
1034
+ // backend refuses payloads with a missing/unknown envelopeVersion.
1035
+ envelopeVersion: 1,
1014
1036
  appId: env.appId,
1015
1037
  environment: env.environment,
1016
1038
  sdk: env.sdk,
@@ -1034,6 +1056,29 @@ var EventQueue = class {
1034
1056
  } catch (err) {
1035
1057
  const message = err instanceof Error ? err.message : String(err);
1036
1058
  this.lastError = message;
1059
+ if (isVersionRejected(err)) {
1060
+ this.parked = true;
1061
+ this.buffer = [...batch, ...this.buffer];
1062
+ if (this.buffer.length > HARD_BUFFER_CAP) {
1063
+ const overflow = this.buffer.length - HARD_BUFFER_CAP;
1064
+ this.buffer.splice(0, overflow);
1065
+ this.dropped += overflow;
1066
+ }
1067
+ this.pendingBatch = null;
1068
+ this.pendingBatchId = null;
1069
+ this.inFlight -= batch.length;
1070
+ this.persistAll();
1071
+ this.cfg.onBufferChange?.(this.buffer.length);
1072
+ const minVersion = versionRejectionFloor(err);
1073
+ if (!this.parkWarned) {
1074
+ this.parkWarned = true;
1075
+ console.warn(
1076
+ `[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.`
1077
+ );
1078
+ }
1079
+ this.cfg.onParked?.({ minVersion, surface: versionRejectionSurface(err) });
1080
+ return null;
1081
+ }
1037
1082
  if (isPermanent4xx(err)) {
1038
1083
  const droppedCount = batch.length;
1039
1084
  this.pendingBatch = null;
@@ -1152,8 +1197,22 @@ function isPermanent4xx(err) {
1152
1197
  if (typeof status !== "number" || !Number.isFinite(status)) return false;
1153
1198
  if (status < 400 || status >= 500) return false;
1154
1199
  if (status === 408 || status === 429) return false;
1200
+ if (status === 426) return false;
1155
1201
  return true;
1156
1202
  }
1203
+ function isVersionRejected(err) {
1204
+ if (!err || typeof err !== "object") return false;
1205
+ if (err.status === 426) return true;
1206
+ return err.code === "sdk_version_unsupported";
1207
+ }
1208
+ function versionRejectionFloor(err) {
1209
+ const v = err?.minVersion;
1210
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1211
+ }
1212
+ function versionRejectionSurface(err) {
1213
+ const v = err?.surface;
1214
+ return typeof v === "string" && v.length > 0 ? v : void 0;
1215
+ }
1157
1216
  function defaultScheduler(fn, ms) {
1158
1217
  const id = setTimeout(fn, ms);
1159
1218
  if (typeof id.unref === "function") {
@@ -1465,6 +1524,15 @@ var AutoTracker = class {
1465
1524
  this.cleanups = [];
1466
1525
  /** Last time we flushed lastActivityAt to storage (throttle gate). */
1467
1526
  this.lastPersistAt = 0;
1527
+ /**
1528
+ * Event Envelope v1 §3 — per-session monotonic sequence counter.
1529
+ * The single owner of the seq counter lives here, alongside all other
1530
+ * session state. Incremented atomically at track() time (via nextSeq()),
1531
+ * reset to 0 whenever a new session boundary is crossed (startNewSession
1532
+ * path + resetSession). Persists across background/foreground within a
1533
+ * session — only a real session boundary (not a tab hide/show) resets it.
1534
+ */
1535
+ this._sessionSeq = 0;
1468
1536
  /**
1469
1537
  * Stable per-page-view identifier. Minted at every `page.viewed`
1470
1538
  * emission and attached to every subsequent event until the next
@@ -1549,6 +1617,20 @@ var AutoTracker = class {
1549
1617
  get currentAcquisition() {
1550
1618
  return this.session?.acquisition ?? EMPTY_ACQUISITION;
1551
1619
  }
1620
+ /**
1621
+ * Event Envelope v1 §3 — return the next seq value for the current session
1622
+ * and advance the counter. Called SYNCHRONOUSLY at track() time, before any
1623
+ * async dispatch, so the seq assignment order is deterministic (call order,
1624
+ * not scheduler luck). The counter is reset to 0 at every session boundary;
1625
+ * it is NOT reset by tab hide/show (spec §3 clause 1: backgrounding does not
1626
+ * reset seq). When no session is active (Node, before init, after uninstall),
1627
+ * returns 0 and leaves the counter unchanged — fallback, not an error.
1628
+ */
1629
+ nextSeq() {
1630
+ const seq = this._sessionSeq;
1631
+ this._sessionSeq += 1;
1632
+ return seq;
1633
+ }
1552
1634
  // ---------- sessions ----------
1553
1635
  installSessionTracking() {
1554
1636
  const now = Date.now();
@@ -1600,6 +1682,7 @@ var AutoTracker = class {
1600
1682
  }
1601
1683
  startNewSession() {
1602
1684
  const now = Date.now();
1685
+ this._sessionSeq = 0;
1603
1686
  return {
1604
1687
  sessionId: mintSessionId(),
1605
1688
  startedAt: now,
@@ -2328,6 +2411,40 @@ function writeJson(storage, key, value) {
2328
2411
  }
2329
2412
  }
2330
2413
 
2414
+ // src/internal-opt-out.ts
2415
+ var INTERNAL_OPT_OUT_PROPERTY = "$crossdeck_internal";
2416
+ var STORAGE_KEY = "crossdeck.internalOptOut";
2417
+ function localStore() {
2418
+ try {
2419
+ return typeof localStorage !== "undefined" ? localStorage : null;
2420
+ } catch {
2421
+ return null;
2422
+ }
2423
+ }
2424
+ function processInternalOptOutUrl() {
2425
+ try {
2426
+ const search = typeof location !== "undefined" ? location.search : "";
2427
+ const params = new URLSearchParams(search || "");
2428
+ if (!params.has("crossdeck_internal")) return;
2429
+ const store = localStore();
2430
+ if (!store) return;
2431
+ const v = params.get("crossdeck_internal");
2432
+ if (v === "1" || v === "true") {
2433
+ store.setItem(STORAGE_KEY, "1");
2434
+ } else if (v === "0" || v === "false") {
2435
+ store.removeItem(STORAGE_KEY);
2436
+ }
2437
+ } catch {
2438
+ }
2439
+ }
2440
+ function isInternalOptOut() {
2441
+ try {
2442
+ return localStore()?.getItem(STORAGE_KEY) === "1";
2443
+ } catch {
2444
+ return false;
2445
+ }
2446
+ }
2447
+
2331
2448
  // src/web-vitals.ts
2332
2449
  var WebVitalsTracker = class {
2333
2450
  constructor(cfg, report) {
@@ -2836,6 +2953,13 @@ var CROSSDECK_ERROR_CODES = Object.freeze([
2836
2953
  description: "A field is present but the value failed validation (wrong shape, wrong length, wrong enum value).",
2837
2954
  resolution: "Read error.message for the specific field + reason. SDK-managed call sites should never emit this \u2014 file a bug if you do.",
2838
2955
  retryable: false
2956
+ },
2957
+ {
2958
+ code: "sdk_version_unsupported",
2959
+ type: "version_error",
2960
+ 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.",
2961
+ 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/.",
2962
+ retryable: false
2839
2963
  }
2840
2964
  ]);
2841
2965
  function getErrorCode(code) {
@@ -2858,15 +2982,6 @@ var VerifierReporter = class {
2858
2982
  constructor(ctx) {
2859
2983
  this.ctx = ctx;
2860
2984
  this.reentrancyDepth = 0;
2861
- /** The live verifier set — attached by the SDK right after init so the
2862
- * emit path can schema-lock its OWN outgoing telemetry against the real
2863
- * payload. Null until attached (boot-time reports still work without it). */
2864
- this.verifiers = null;
2865
- }
2866
- /** Hand the reporter the live verifier set so `reportFail` can run the
2867
- * `onReportContractFailure` hook on the exact payload it's about to emit. */
2868
- attachVerifiers(verifiers) {
2869
- this.verifiers = verifiers;
2870
2985
  }
2871
2986
  /**
2872
2987
  * Report a verifier result. Pass `operation` for hot-path results
@@ -2895,7 +3010,7 @@ var VerifierReporter = class {
2895
3010
  if (this.reentrancyDepth > 0) return;
2896
3011
  this.reentrancyDepth += 1;
2897
3012
  try {
2898
- const payload = {
3013
+ this.ctx.emitTelemetry({
2899
3014
  contract_id: result.contractId,
2900
3015
  sdk_version: this.ctx.sdkVersion,
2901
3016
  sdk_platform: "web",
@@ -2903,13 +3018,7 @@ var VerifierReporter = class {
2903
3018
  run_context: this.ctx.runContext,
2904
3019
  run_id: this.ctx.runId,
2905
3020
  verification_phase: phase
2906
- };
2907
- if (this.verifiers) {
2908
- runOnReportContractFailure(this.verifiers, this, this.ctx, {
2909
- outgoingPayload: payload
2910
- });
2911
- }
2912
- this.ctx.emitTelemetry(payload);
3021
+ });
2913
3022
  } finally {
2914
3023
  this.reentrancyDepth -= 1;
2915
3024
  }
@@ -3199,31 +3308,9 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
3199
3308
  "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
3200
3309
  nowMs() - t0
3201
3310
  );
3202
- },
3203
- // Hot-path hook — on every real track() we re-affirm the LIVE configured
3204
- // flush interval (carried on the observation), so the parity guarantee is
3205
- // verified against the running SDK in the customer flow, not just a
3206
- // canonical constant at boot. It can't change per-event, but observing it
3207
- // per-event is what makes the contract genuinely runtime-verified.
3208
- hooks: {
3209
- onTrack(obs) {
3210
- const t0 = nowMs();
3211
- const CANONICAL_DEFAULT_MS = 2e3;
3212
- const ms = obs.flushIntervalMs;
3213
- if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
3214
- return fail(
3215
- "flush-interval-parity",
3216
- `live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
3217
- nowMs() - t0
3218
- );
3219
- }
3220
- return pass(
3221
- "flush-interval-parity",
3222
- ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
3223
- nowMs() - t0
3224
- );
3225
- }
3226
3311
  }
3312
+ // No hot-path hook — the flush interval is set once at start() and
3313
+ // never changes per-operation.
3227
3314
  };
3228
3315
  function buildFlushIntervalVerifier(configuredIntervalMs) {
3229
3316
  return {
@@ -3350,26 +3437,6 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
3350
3437
  "referrer",
3351
3438
  "deviceId"
3352
3439
  ];
3353
- function contractFailedSchemaViolation(keys) {
3354
- const allowed = /* @__PURE__ */ new Set([
3355
- ...CONTRACT_FAILED_REQUIRED_FIELDS,
3356
- ...CONTRACT_FAILED_OPTIONAL_FIELDS
3357
- ]);
3358
- const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3359
- for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3360
- if (!keys.includes(required)) return `missing required field: ${required}`;
3361
- }
3362
- for (const k of keys) {
3363
- if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
3364
- }
3365
- for (const k of keys) {
3366
- if (!allowed.has(k)) {
3367
- return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
3368
- }
3369
- }
3370
- return null;
3371
- }
3372
- 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`;
3373
3440
  var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3374
3441
  contractId: "contract-failed-payload-schema-lock",
3375
3442
  bootTest() {
@@ -3384,35 +3451,43 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3384
3451
  verification_phase: "boot"
3385
3452
  };
3386
3453
  const keys = Object.keys(syntheticPayload);
3387
- const violation = contractFailedSchemaViolation(keys);
3388
- if (violation) {
3389
- return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3454
+ const allowed = /* @__PURE__ */ new Set([
3455
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
3456
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
3457
+ ]);
3458
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3459
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3460
+ if (!keys.includes(required)) {
3461
+ return fail(
3462
+ "contract-failed-payload-schema-lock",
3463
+ `missing required field: ${required}`,
3464
+ nowMs() - t0
3465
+ );
3466
+ }
3467
+ }
3468
+ for (const k of keys) {
3469
+ if (forbidden.has(k)) {
3470
+ return fail(
3471
+ "contract-failed-payload-schema-lock",
3472
+ `forbidden field on wire: ${k}`,
3473
+ nowMs() - t0
3474
+ );
3475
+ }
3476
+ }
3477
+ for (const k of keys) {
3478
+ if (!allowed.has(k)) {
3479
+ return fail(
3480
+ "contract-failed-payload-schema-lock",
3481
+ `unrecognised field on wire: ${k} (not in required \u222A optional)`,
3482
+ nowMs() - t0
3483
+ );
3484
+ }
3390
3485
  }
3391
3486
  return pass(
3392
3487
  "contract-failed-payload-schema-lock",
3393
- `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3488
+ `${keys.length} fields \u2286 required(${CONTRACT_FAILED_REQUIRED_FIELDS.length}) \u222A optional(${CONTRACT_FAILED_OPTIONAL_FIELDS.length}); ${CONTRACT_FAILED_FORBIDDEN_FIELDS.length} forbidden absent`,
3394
3489
  nowMs() - t0
3395
3490
  );
3396
- },
3397
- // Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
3398
- // inside the re-entrancy guard) against the exact payload about to go on
3399
- // the wire. This is the assertion the guard at reportFail was built in
3400
- // anticipation of: every reliability-channel write is schema-locked in the
3401
- // field, against real data, not just the synthetic boot mirror.
3402
- hooks: {
3403
- onReportContractFailure(obs) {
3404
- const t0 = nowMs();
3405
- const keys = Object.keys(obs.outgoingPayload);
3406
- const violation = contractFailedSchemaViolation(keys);
3407
- if (violation) {
3408
- return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3409
- }
3410
- return pass(
3411
- "contract-failed-payload-schema-lock",
3412
- `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3413
- nowMs() - t0
3414
- );
3415
- }
3416
3491
  }
3417
3492
  };
3418
3493
  var BACKEND_WIRE_CODES = Object.freeze([
@@ -3430,7 +3505,8 @@ var BACKEND_WIRE_CODES = Object.freeze([
3430
3505
  "google_not_supported",
3431
3506
  "stripe_not_supported",
3432
3507
  "missing_required_param",
3433
- "invalid_param_value"
3508
+ "invalid_param_value",
3509
+ "sdk_version_unsupported"
3434
3510
  ]);
3435
3511
  var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3436
3512
  contractId: "sdk-error-codes-catalogue",
@@ -3463,39 +3539,6 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3463
3539
  nowMs() - t0
3464
3540
  );
3465
3541
  }
3466
- },
3467
- // Hot-path hook — when the SDK parses a real wire error, assert the code
3468
- // the customer actually hit carries usable remediation in the shipped
3469
- // catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
3470
- // codes are out of scope and pass. This turns the catalogue from a
3471
- // boot-only completeness claim into a per-error field assertion: a backend
3472
- // code that ships without remediation is caught the first time a customer
3473
- // hits it, not only if the off-by-default boot self-test ran.
3474
- hooks: {
3475
- onErrorParse(obs) {
3476
- const t0 = nowMs();
3477
- const code = obs.errorCode;
3478
- if (!code || !BACKEND_WIRE_CODES.includes(code)) {
3479
- return pass(
3480
- "sdk-error-codes-catalogue",
3481
- `wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
3482
- nowMs() - t0
3483
- );
3484
- }
3485
- const entry = getErrorCode(code);
3486
- if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
3487
- return fail(
3488
- "sdk-error-codes-catalogue",
3489
- `backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
3490
- nowMs() - t0
3491
- );
3492
- }
3493
- return pass(
3494
- "sdk-error-codes-catalogue",
3495
- `backend wire code "${code}" carries description + resolution`,
3496
- nowMs() - t0
3497
- );
3498
- }
3499
3542
  }
3500
3543
  };
3501
3544
  var STATIC_VERIFIERS = Object.freeze([
@@ -3619,24 +3662,6 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3619
3662
  reporter.report(result, "hot_path", "errorParse");
3620
3663
  }
3621
3664
  }
3622
- function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
3623
- if (ctx.disableContractAssertions) return;
3624
- for (const verifier of verifiers) {
3625
- const hook = verifier.hooks?.onReportContractFailure;
3626
- if (!hook) continue;
3627
- let result;
3628
- try {
3629
- result = hook(obs);
3630
- } catch (err) {
3631
- result = fail(
3632
- verifier.contractId,
3633
- `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3634
- 0
3635
- );
3636
- }
3637
- reporter.report(result, "hot_path", "reportContractFailure");
3638
- }
3639
- }
3640
3665
  function defaultDebugModeFlag() {
3641
3666
  try {
3642
3667
  if (typeof process !== "undefined" && process.env) {
@@ -3788,18 +3813,18 @@ function isInAppFrame(filename) {
3788
3813
  if (/\/crossdeck\.umd\.min\.js$/.test(filename)) return false;
3789
3814
  return true;
3790
3815
  }
3791
- function fingerprintError(message, frames, location) {
3816
+ function fingerprintError(message, frames, location2) {
3792
3817
  const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
3793
3818
  const parts = [
3794
3819
  (message || "").slice(0, 200),
3795
3820
  ...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
3796
3821
  ];
3797
- if (inAppFrames.length === 0 && location) {
3822
+ if (inAppFrames.length === 0 && location2) {
3798
3823
  const loc = [
3799
- location.errorType ?? "",
3800
- location.filename ?? "",
3801
- location.lineno ?? "",
3802
- location.colno ?? ""
3824
+ location2.errorType ?? "",
3825
+ location2.filename ?? "",
3826
+ location2.lineno ?? "",
3827
+ location2.colno ?? ""
3803
3828
  ].join(":");
3804
3829
  if (loc !== ":::") parts.push(loc);
3805
3830
  }
@@ -4469,9 +4494,6 @@ var CrossdeckClient = class {
4469
4494
  this.verifiers = null;
4470
4495
  this.verifierReporter = null;
4471
4496
  this.verifierCtx = null;
4472
- // The live configured event-flush interval, surfaced to the track-path
4473
- // verifier so flush-interval-parity validates the real cadence per event.
4474
- this.flushIntervalMs = 2e3;
4475
4497
  }
4476
4498
  /**
4477
4499
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -4641,6 +4663,13 @@ var CrossdeckClient = class {
4641
4663
  headline,
4642
4664
  { ...info }
4643
4665
  );
4666
+ },
4667
+ onParked: (info) => {
4668
+ debug.emit(
4669
+ "sdk.parked",
4670
+ `[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.`,
4671
+ { ...info }
4672
+ );
4644
4673
  }
4645
4674
  });
4646
4675
  const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
@@ -4648,6 +4677,7 @@ var CrossdeckClient = class {
4648
4677
  persistIdentity ? effectiveStorage : new MemoryStorage(),
4649
4678
  opts.storagePrefix
4650
4679
  );
4680
+ processInternalOptOutUrl();
4651
4681
  const consent = new ConsentManager({ respectDnt: options.respectDnt === true });
4652
4682
  if (consent.isDntDenied) {
4653
4683
  debug.emit(
@@ -4751,8 +4781,6 @@ var CrossdeckClient = class {
4751
4781
  this.verifierReporter = new VerifierReporter(this.verifierCtx);
4752
4782
  const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4753
4783
  this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4754
- this.flushIntervalMs = opts.eventFlushIntervalMs;
4755
- this.verifierReporter.attachVerifiers(this.verifiers);
4756
4784
  if (localDevMode) {
4757
4785
  const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4758
4786
  if (verifyAtBootLocal && this.verifierReporter) {
@@ -5303,8 +5331,27 @@ var CrossdeckClient = class {
5303
5331
  );
5304
5332
  }
5305
5333
  }
5334
+ const seq = s.autoTracker?.nextSeq() ?? 0;
5335
+ const occurrenceTimestamp = Date.now();
5306
5336
  s.autoTracker?.markActivity();
5307
- const enriched = { ...s.deviceInfo };
5337
+ const wireContext = {};
5338
+ const di = s.deviceInfo;
5339
+ if (di.os) wireContext.os = di.os;
5340
+ if (di.osVersion) wireContext.osVersion = di.osVersion;
5341
+ const appVer = s.options.appVersion;
5342
+ if (appVer) wireContext.appVersion = appVer;
5343
+ wireContext.sdkName = SDK_NAME;
5344
+ wireContext.sdkVersion = s.options.sdkVersion;
5345
+ if (di.locale) wireContext.locale = di.locale;
5346
+ if (di.timezone) wireContext.timezone = di.timezone;
5347
+ if (di.browser) wireContext.browser = di.browser;
5348
+ if (di.browserVersion) wireContext.browserVersion = di.browserVersion;
5349
+ const enriched = {};
5350
+ if (di.screenWidth !== void 0) enriched.screenWidth = di.screenWidth;
5351
+ if (di.screenHeight !== void 0) enriched.screenHeight = di.screenHeight;
5352
+ if (di.viewportWidth !== void 0) enriched.viewportWidth = di.viewportWidth;
5353
+ if (di.viewportHeight !== void 0) enriched.viewportHeight = di.viewportHeight;
5354
+ if (di.devicePixelRatio !== void 0) enriched.devicePixelRatio = di.devicePixelRatio;
5308
5355
  const sessionId = s.autoTracker?.currentSessionId;
5309
5356
  if (sessionId) enriched.sessionId = sessionId;
5310
5357
  const pageviewId = s.autoTracker?.currentPageviewId;
@@ -5327,17 +5374,24 @@ var CrossdeckClient = class {
5327
5374
  }
5328
5375
  }
5329
5376
  const supers = s.superProps.getSuperProperties();
5330
- Object.assign(enriched, supers);
5377
+ for (const k of Object.keys(supers)) {
5378
+ if (!(k in enriched)) enriched[k] = supers[k];
5379
+ }
5331
5380
  const groupIds = s.superProps.getGroupIds();
5332
5381
  if (Object.keys(groupIds).length > 0) {
5333
5382
  enriched.$groups = groupIds;
5334
5383
  }
5335
5384
  Object.assign(enriched, validation.properties);
5385
+ if (isInternalOptOut()) {
5386
+ enriched[INTERNAL_OPT_OUT_PROPERTY] = true;
5387
+ }
5336
5388
  const finalProperties = s.scrubPii ? scrubPiiFromProperties(enriched) : enriched;
5337
5389
  const event = {
5338
5390
  eventId: this.mintEventId(),
5339
5391
  name,
5340
- timestamp: Date.now(),
5392
+ timestamp: occurrenceTimestamp,
5393
+ seq,
5394
+ context: wireContext,
5341
5395
  properties: finalProperties
5342
5396
  };
5343
5397
  Object.assign(event, this.identityHintForEvent());
@@ -5347,8 +5401,7 @@ var CrossdeckClient = class {
5347
5401
  callerProperties: validation.properties,
5348
5402
  superProperties: supers,
5349
5403
  deviceProperties: s.deviceInfo,
5350
- mergedProperties: enriched,
5351
- flushIntervalMs: this.flushIntervalMs
5404
+ mergedProperties: enriched
5352
5405
  });
5353
5406
  }
5354
5407
  if (!isError && !isWebVital) {