@cross-deck/web 1.6.2 → 1.6.4

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.cjs CHANGED
@@ -93,7 +93,7 @@ function typeMapForStatus(status) {
93
93
  }
94
94
 
95
95
  // src/_version.ts
96
- var SDK_VERSION = "1.6.2";
96
+ var SDK_VERSION = "1.6.4";
97
97
  var SDK_NAME = "@cross-deck/web";
98
98
 
99
99
  // src/http.ts
@@ -2859,6 +2859,15 @@ var VerifierReporter = class {
2859
2859
  constructor(ctx) {
2860
2860
  this.ctx = ctx;
2861
2861
  this.reentrancyDepth = 0;
2862
+ /** The live verifier set — attached by the SDK right after init so the
2863
+ * emit path can schema-lock its OWN outgoing telemetry against the real
2864
+ * payload. Null until attached (boot-time reports still work without it). */
2865
+ this.verifiers = null;
2866
+ }
2867
+ /** Hand the reporter the live verifier set so `reportFail` can run the
2868
+ * `onReportContractFailure` hook on the exact payload it's about to emit. */
2869
+ attachVerifiers(verifiers) {
2870
+ this.verifiers = verifiers;
2862
2871
  }
2863
2872
  /**
2864
2873
  * Report a verifier result. Pass `operation` for hot-path results
@@ -2887,7 +2896,7 @@ var VerifierReporter = class {
2887
2896
  if (this.reentrancyDepth > 0) return;
2888
2897
  this.reentrancyDepth += 1;
2889
2898
  try {
2890
- this.ctx.emitTelemetry({
2899
+ const payload = {
2891
2900
  contract_id: result.contractId,
2892
2901
  sdk_version: this.ctx.sdkVersion,
2893
2902
  sdk_platform: "web",
@@ -2895,7 +2904,13 @@ var VerifierReporter = class {
2895
2904
  run_context: this.ctx.runContext,
2896
2905
  run_id: this.ctx.runId,
2897
2906
  verification_phase: phase
2898
- });
2907
+ };
2908
+ if (this.verifiers) {
2909
+ runOnReportContractFailure(this.verifiers, this, this.ctx, {
2910
+ outgoingPayload: payload
2911
+ });
2912
+ }
2913
+ this.ctx.emitTelemetry(payload);
2899
2914
  } finally {
2900
2915
  this.reentrancyDepth -= 1;
2901
2916
  }
@@ -3185,9 +3200,31 @@ var VERIFIER_FLUSH_INTERVAL_PARITY = {
3185
3200
  "eventFlushIntervalMs default = 2000ms (Web/Node/RN/Swift/Android parity)",
3186
3201
  nowMs() - t0
3187
3202
  );
3203
+ },
3204
+ // Hot-path hook — on every real track() we re-affirm the LIVE configured
3205
+ // flush interval (carried on the observation), so the parity guarantee is
3206
+ // verified against the running SDK in the customer flow, not just a
3207
+ // canonical constant at boot. It can't change per-event, but observing it
3208
+ // per-event is what makes the contract genuinely runtime-verified.
3209
+ hooks: {
3210
+ onTrack(obs) {
3211
+ const t0 = nowMs();
3212
+ const CANONICAL_DEFAULT_MS = 2e3;
3213
+ const ms = obs.flushIntervalMs;
3214
+ if (typeof ms !== "number" || ms < 100 || ms > 6e4) {
3215
+ return fail(
3216
+ "flush-interval-parity",
3217
+ `live eventFlushIntervalMs=${ms} outside reasonable bounds [100, 60000]`,
3218
+ nowMs() - t0
3219
+ );
3220
+ }
3221
+ return pass(
3222
+ "flush-interval-parity",
3223
+ ms === CANONICAL_DEFAULT_MS ? "live eventFlushIntervalMs = 2000ms (canonical parity value)" : `live eventFlushIntervalMs = ${ms}ms (permitted override; canonical default 2000ms)`,
3224
+ nowMs() - t0
3225
+ );
3226
+ }
3188
3227
  }
3189
- // No hot-path hook — the flush interval is set once at start() and
3190
- // never changes per-operation.
3191
3228
  };
3192
3229
  function buildFlushIntervalVerifier(configuredIntervalMs) {
3193
3230
  return {
@@ -3314,6 +3351,26 @@ var CONTRACT_FAILED_FORBIDDEN_FIELDS = [
3314
3351
  "referrer",
3315
3352
  "deviceId"
3316
3353
  ];
3354
+ function contractFailedSchemaViolation(keys) {
3355
+ const allowed = /* @__PURE__ */ new Set([
3356
+ ...CONTRACT_FAILED_REQUIRED_FIELDS,
3357
+ ...CONTRACT_FAILED_OPTIONAL_FIELDS
3358
+ ]);
3359
+ const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3360
+ for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3361
+ if (!keys.includes(required)) return `missing required field: ${required}`;
3362
+ }
3363
+ for (const k of keys) {
3364
+ if (forbidden.has(k)) return `forbidden field on wire: ${k}`;
3365
+ }
3366
+ for (const k of keys) {
3367
+ if (!allowed.has(k)) {
3368
+ return `unrecognised field on wire: ${k} (not in required \u222A optional)`;
3369
+ }
3370
+ }
3371
+ return null;
3372
+ }
3373
+ 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`;
3317
3374
  var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3318
3375
  contractId: "contract-failed-payload-schema-lock",
3319
3376
  bootTest() {
@@ -3328,43 +3385,35 @@ var VERIFIER_CONTRACT_FAILED_PAYLOAD_SCHEMA_LOCK = {
3328
3385
  verification_phase: "boot"
3329
3386
  };
3330
3387
  const keys = Object.keys(syntheticPayload);
3331
- const allowed = /* @__PURE__ */ new Set([
3332
- ...CONTRACT_FAILED_REQUIRED_FIELDS,
3333
- ...CONTRACT_FAILED_OPTIONAL_FIELDS
3334
- ]);
3335
- const forbidden = new Set(CONTRACT_FAILED_FORBIDDEN_FIELDS);
3336
- for (const required of CONTRACT_FAILED_REQUIRED_FIELDS) {
3337
- if (!keys.includes(required)) {
3338
- return fail(
3339
- "contract-failed-payload-schema-lock",
3340
- `missing required field: ${required}`,
3341
- nowMs() - t0
3342
- );
3343
- }
3344
- }
3345
- for (const k of keys) {
3346
- if (forbidden.has(k)) {
3347
- return fail(
3348
- "contract-failed-payload-schema-lock",
3349
- `forbidden field on wire: ${k}`,
3350
- nowMs() - t0
3351
- );
3352
- }
3353
- }
3354
- for (const k of keys) {
3355
- if (!allowed.has(k)) {
3356
- return fail(
3357
- "contract-failed-payload-schema-lock",
3358
- `unrecognised field on wire: ${k} (not in required \u222A optional)`,
3359
- nowMs() - t0
3360
- );
3361
- }
3388
+ const violation = contractFailedSchemaViolation(keys);
3389
+ if (violation) {
3390
+ return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3362
3391
  }
3363
3392
  return pass(
3364
3393
  "contract-failed-payload-schema-lock",
3365
- `${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
+ `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3366
3395
  nowMs() - t0
3367
3396
  );
3397
+ },
3398
+ // Hot-path hook — fires on the REAL reportFail emit (VerifierReporter,
3399
+ // inside the re-entrancy guard) against the exact payload about to go on
3400
+ // the wire. This is the assertion the guard at reportFail was built in
3401
+ // anticipation of: every reliability-channel write is schema-locked in the
3402
+ // field, against real data, not just the synthetic boot mirror.
3403
+ hooks: {
3404
+ onReportContractFailure(obs) {
3405
+ const t0 = nowMs();
3406
+ const keys = Object.keys(obs.outgoingPayload);
3407
+ const violation = contractFailedSchemaViolation(keys);
3408
+ if (violation) {
3409
+ return fail("contract-failed-payload-schema-lock", violation, nowMs() - t0);
3410
+ }
3411
+ return pass(
3412
+ "contract-failed-payload-schema-lock",
3413
+ `${keys.length} ${CONTRACT_FAILED_OK_EVIDENCE}`,
3414
+ nowMs() - t0
3415
+ );
3416
+ }
3368
3417
  }
3369
3418
  };
3370
3419
  var BACKEND_WIRE_CODES = Object.freeze([
@@ -3415,6 +3464,39 @@ var VERIFIER_SDK_ERROR_CODES_CATALOGUE = {
3415
3464
  nowMs() - t0
3416
3465
  );
3417
3466
  }
3467
+ },
3468
+ // Hot-path hook — when the SDK parses a real wire error, assert the code
3469
+ // the customer actually hit carries usable remediation in the shipped
3470
+ // catalogue. Scoped to BACKEND_WIRE_CODES (the contract's set); other
3471
+ // codes are out of scope and pass. This turns the catalogue from a
3472
+ // boot-only completeness claim into a per-error field assertion: a backend
3473
+ // code that ships without remediation is caught the first time a customer
3474
+ // hits it, not only if the off-by-default boot self-test ran.
3475
+ hooks: {
3476
+ onErrorParse(obs) {
3477
+ const t0 = nowMs();
3478
+ const code = obs.errorCode;
3479
+ if (!code || !BACKEND_WIRE_CODES.includes(code)) {
3480
+ return pass(
3481
+ "sdk-error-codes-catalogue",
3482
+ `wire code "${code || "(none)"}" is out of the backend-catalogue scope`,
3483
+ nowMs() - t0
3484
+ );
3485
+ }
3486
+ const entry = getErrorCode(code);
3487
+ if (!entry || !entry.description || entry.description.trim().length === 0 || !entry.resolution || entry.resolution.trim().length === 0) {
3488
+ return fail(
3489
+ "sdk-error-codes-catalogue",
3490
+ `backend wire code "${code}" hit in the field has no description+resolution in the shipped catalogue`,
3491
+ nowMs() - t0
3492
+ );
3493
+ }
3494
+ return pass(
3495
+ "sdk-error-codes-catalogue",
3496
+ `backend wire code "${code}" carries description + resolution`,
3497
+ nowMs() - t0
3498
+ );
3499
+ }
3418
3500
  }
3419
3501
  };
3420
3502
  var STATIC_VERIFIERS = Object.freeze([
@@ -3538,6 +3620,24 @@ function runOnErrorParse(verifiers, reporter, ctx, obs) {
3538
3620
  reporter.report(result, "hot_path", "errorParse");
3539
3621
  }
3540
3622
  }
3623
+ function runOnReportContractFailure(verifiers, reporter, ctx, obs) {
3624
+ if (ctx.disableContractAssertions) return;
3625
+ for (const verifier of verifiers) {
3626
+ const hook = verifier.hooks?.onReportContractFailure;
3627
+ if (!hook) continue;
3628
+ let result;
3629
+ try {
3630
+ result = hook(obs);
3631
+ } catch (err) {
3632
+ result = fail(
3633
+ verifier.contractId,
3634
+ `hook threw: ${err.message?.slice(0, 80) ?? "unknown"}`,
3635
+ 0
3636
+ );
3637
+ }
3638
+ reporter.report(result, "hot_path", "reportContractFailure");
3639
+ }
3640
+ }
3541
3641
  function defaultDebugModeFlag() {
3542
3642
  try {
3543
3643
  if (typeof process !== "undefined" && process.env) {
@@ -3937,7 +4037,7 @@ var ErrorTracker = class {
3937
4037
  }
3938
4038
  return response;
3939
4039
  } catch (err) {
3940
- if (this.opts.isConsented() && !url.includes("api.cross-deck.com")) {
4040
+ if (this.opts.isConsented() && !url.includes("api.cross-deck.com") && !isClientNetworkNoise(url, err, w)) {
3941
4041
  this.captureHttp({
3942
4042
  url,
3943
4043
  method,
@@ -4336,6 +4436,19 @@ function isSelfRequest(requestUrl, selfHostname) {
4336
4436
  return false;
4337
4437
  }
4338
4438
  }
4439
+ function isClientNetworkNoise(requestUrl, err, w) {
4440
+ if (err instanceof Error && err.name === "AbortError") return true;
4441
+ const nav = w.navigator;
4442
+ if (nav && nav.onLine === false) return true;
4443
+ const pageHref = w.location?.href;
4444
+ const pageOrigin = w.location?.origin;
4445
+ if (!pageOrigin) return false;
4446
+ try {
4447
+ return new URL(requestUrl, pageHref ?? pageOrigin).origin === pageOrigin;
4448
+ } catch {
4449
+ return false;
4450
+ }
4451
+ }
4339
4452
 
4340
4453
  // src/crossdeck.ts
4341
4454
  var CrossdeckClient = class {
@@ -4357,6 +4470,9 @@ var CrossdeckClient = class {
4357
4470
  this.verifiers = null;
4358
4471
  this.verifierReporter = null;
4359
4472
  this.verifierCtx = null;
4473
+ // The live configured event-flush interval, surfaced to the track-path
4474
+ // verifier so flush-interval-parity validates the real cadence per event.
4475
+ this.flushIntervalMs = 2e3;
4360
4476
  }
4361
4477
  /**
4362
4478
  * Boot the SDK. Idempotent — calling init twice with the same options
@@ -4636,6 +4752,8 @@ var CrossdeckClient = class {
4636
4752
  this.verifierReporter = new VerifierReporter(this.verifierCtx);
4637
4753
  const flushVerifier = buildFlushIntervalVerifier(opts.eventFlushIntervalMs);
4638
4754
  this.verifiers = Object.freeze([...STATIC_VERIFIERS, flushVerifier]);
4755
+ this.flushIntervalMs = opts.eventFlushIntervalMs;
4756
+ this.verifierReporter.attachVerifiers(this.verifiers);
4639
4757
  if (localDevMode) {
4640
4758
  const verifyAtBootLocal = options.verifyContractsAtBoot ?? debugDefault;
4641
4759
  if (verifyAtBootLocal && this.verifierReporter) {
@@ -5210,9 +5328,7 @@ var CrossdeckClient = class {
5210
5328
  }
5211
5329
  }
5212
5330
  const supers = s.superProps.getSuperProperties();
5213
- for (const k of Object.keys(supers)) {
5214
- if (!(k in enriched)) enriched[k] = supers[k];
5215
- }
5331
+ Object.assign(enriched, supers);
5216
5332
  const groupIds = s.superProps.getGroupIds();
5217
5333
  if (Object.keys(groupIds).length > 0) {
5218
5334
  enriched.$groups = groupIds;
@@ -5232,7 +5348,8 @@ var CrossdeckClient = class {
5232
5348
  callerProperties: validation.properties,
5233
5349
  superProperties: supers,
5234
5350
  deviceProperties: s.deviceInfo,
5235
- mergedProperties: enriched
5351
+ mergedProperties: enriched,
5352
+ flushIntervalMs: this.flushIntervalMs
5236
5353
  });
5237
5354
  }
5238
5355
  if (!isError && !isWebVital) {