@shipeasy/sdk 4.4.0 → 5.0.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 CHANGED
@@ -69,12 +69,12 @@ try {
69
69
  }
70
70
 
71
71
  // Non-exception problems — the name is a stable identifier (it participates
72
- // in the issue fingerprint); variable data goes in .message() / .extras():
72
+ // in the issue fingerprint); all variable data goes in .extras():
73
73
  if (rows.length > LIMIT) {
74
74
  see.Violation("large query")
75
- .message(`got ${rows.length} rows`)
76
75
  .causes_the("search results")
77
- .to("be trimmed");
76
+ .to("be trimmed")
77
+ .extras({ rows: rows.length });
78
78
  }
79
79
 
80
80
  // Expected control-flow exceptions: document them, report nothing —
@@ -82,7 +82,7 @@ if (rows.length > LIMIT) {
82
82
  try {
83
83
  return decodeFoo(blob);
84
84
  } catch (e) {
85
- see.ControlFlowException(e, "because it wasn't an encoded Foo");
85
+ see.ControlFlowException(e).because("because it wasn't an encoded Foo");
86
86
  return decodeBar(blob);
87
87
  }
88
88
  ```
@@ -94,16 +94,19 @@ occurrence timeseries. The chain dispatches on the next microtask — no
94
94
  `fetch` on the server), spam-guarded by a 30s dedup window and a per-session
95
95
  cap.
96
96
 
97
- The client SDK also auto-captures uncaught exceptions, unhandled rejections,
98
- and network failures (fetch network errors + 5xx) into the same primitive
99
- (`autoCollect: { errors }`, on by default). Auto-capture is the outer safety
100
- net it does not replace `see()` in catch blocks, where you know the
101
- consequence and it cannot.
102
-
103
- **Rules**: if you don't know the consequence, don't catch the exception. Never
104
- `see()` then `throw` (double counting — either handle or rethrow). Never use
105
- `see.Violation()` for a caught exception (you'd drop the stack). No PII or
106
- high-cardinality data in extras.
97
+ The client SDK also auto-captures **network failures** (fetch network errors +
98
+ 5xx) into the same primitive (`autoCollect: { errors }`, on by default) each
99
+ names a specific endpoint and a specific outcome. It deliberately does **not**
100
+ blanket-report uncaught exceptions or unhandled promise rejections: those carry
101
+ no actionable consequence ("the page hit an error" names the plumbing, not the
102
+ feature). Code that knows the consequence reports it explicitly with `see()` at
103
+ the catch site.
104
+
105
+ **Rules**: if you don't know the consequence, don't catch the exception. You
106
+ **may** `see()` then re-throw the re-thrown error links to its inner report
107
+ as a `caused_by` chain instead of double-counting. Never use `see.Violation()`
108
+ for a caught exception (you'd drop the stack). No PII or high-cardinality data
109
+ in extras.
107
110
 
108
111
  ## Drop-in `<script>` loader (no bundler)
109
112
 
@@ -8,15 +8,13 @@ interface Consequence {
8
8
  }
9
9
  /**
10
10
  * Non-exception problem, built by `violation(name)`. A plain branded object
11
- * (not an Error subclass) so `.message()` can be a builder method without
12
- * colliding with `Error.prototype.message`.
11
+ * (not an Error subclass). The name is the whole identity there is no
12
+ * separate message; any variable/context data belongs in `.extras()` on the
13
+ * see chain, never on the violation itself.
13
14
  */
14
15
  interface Violation {
15
16
  readonly __seViolation: true;
16
17
  readonly violationName: string;
17
- readonly violationMessage?: string;
18
- /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
- message(msg: string): Violation;
20
18
  }
21
19
  /**
22
20
  * Identity of a problem that see() already reported, carried on the wire as
@@ -84,9 +82,22 @@ interface SeeChain {
84
82
  /** camelCase alias of {@link SeeChain.causes_the}. */
85
83
  causesThe(subject: string): SeeOutcomeStep;
86
84
  }
87
- interface SeeViolationChain extends SeeChain {
88
- /** Free-form detail. Variable data goes here (or extras), never in the name. */
89
- message(msg: string): SeeViolationChain;
85
+ /**
86
+ * Violations share the exception consequence grammar exactly there is no
87
+ * separate `.message()`. Put any variable/context data in `.extras()`.
88
+ */
89
+ type SeeViolationChain = SeeChain;
90
+ interface SeeControlFlowTail {
91
+ /**
92
+ * Optional debugging context for the expected exception. Kept on the mark for
93
+ * local debugging only (expected exceptions are never reported). Callable
94
+ * repeatedly — keys merge, later wins.
95
+ */
96
+ extras(extras: SeeExtras): SeeControlFlowTail;
97
+ }
98
+ interface SeeControlFlowChain {
99
+ /** Document why the exception is expected. The reason should start with "because". */
100
+ because(reason: string): SeeControlFlowTail;
90
101
  }
91
102
 
92
103
  declare global {
@@ -430,28 +441,29 @@ interface SeeApi {
430
441
  /**
431
442
  * Report a non-exception problem. Prefer passing a caught Error to `see()`
432
443
  * when one exists. The name is a stable identifier (it participates in the
433
- * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
444
+ * issue fingerprint) — variable data goes in `.extras()`, never the name.
434
445
  *
435
446
  * ```ts
436
447
  * if (rows.length > LIMIT) {
437
- * see.Violation("large query").message(`got ${rows.length} rows`)
438
- * .causes_the("search results").to("be trimmed");
448
+ * see.Violation("large query")
449
+ * .causes_the("search results").to("be trimmed").extras({ rows: rows.length });
439
450
  * }
440
451
  * ```
441
452
  */
442
453
  Violation(name: string): SeeViolationChain;
443
454
  /**
444
455
  * Mark an exception as expected control flow — auto-capture skips it and
445
- * nothing is reported. The reason must start with "because".
456
+ * nothing is reported. Say why with `.because()` (reason should start with
457
+ * "because"); attach optional debug context with `.extras()`.
446
458
  *
447
459
  * ```ts
448
460
  * } catch (e) {
449
- * see.ControlFlowException(e, "because the blob wasn't an encoded Foo");
461
+ * see.ControlFlowException(e).because("because the blob wasn't an encoded Foo");
450
462
  * return decodeAsBar(blob);
451
463
  * }
452
464
  * ```
453
465
  */
454
- ControlFlowException(err: unknown, because: string): void;
466
+ ControlFlowException(err: unknown): SeeControlFlowChain;
455
467
  }
456
468
  /**
457
469
  * Structured error reporter — the whole grammar hangs off this one import.
@@ -497,4 +509,4 @@ interface I18nFacade {
497
509
  }
498
510
  declare const i18n: I18nFacade;
499
511
 
500
- export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
512
+ export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -8,15 +8,13 @@ interface Consequence {
8
8
  }
9
9
  /**
10
10
  * Non-exception problem, built by `violation(name)`. A plain branded object
11
- * (not an Error subclass) so `.message()` can be a builder method without
12
- * colliding with `Error.prototype.message`.
11
+ * (not an Error subclass). The name is the whole identity there is no
12
+ * separate message; any variable/context data belongs in `.extras()` on the
13
+ * see chain, never on the violation itself.
13
14
  */
14
15
  interface Violation {
15
16
  readonly __seViolation: true;
16
17
  readonly violationName: string;
17
- readonly violationMessage?: string;
18
- /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
- message(msg: string): Violation;
20
18
  }
21
19
  /**
22
20
  * Identity of a problem that see() already reported, carried on the wire as
@@ -84,9 +82,22 @@ interface SeeChain {
84
82
  /** camelCase alias of {@link SeeChain.causes_the}. */
85
83
  causesThe(subject: string): SeeOutcomeStep;
86
84
  }
87
- interface SeeViolationChain extends SeeChain {
88
- /** Free-form detail. Variable data goes here (or extras), never in the name. */
89
- message(msg: string): SeeViolationChain;
85
+ /**
86
+ * Violations share the exception consequence grammar exactly there is no
87
+ * separate `.message()`. Put any variable/context data in `.extras()`.
88
+ */
89
+ type SeeViolationChain = SeeChain;
90
+ interface SeeControlFlowTail {
91
+ /**
92
+ * Optional debugging context for the expected exception. Kept on the mark for
93
+ * local debugging only (expected exceptions are never reported). Callable
94
+ * repeatedly — keys merge, later wins.
95
+ */
96
+ extras(extras: SeeExtras): SeeControlFlowTail;
97
+ }
98
+ interface SeeControlFlowChain {
99
+ /** Document why the exception is expected. The reason should start with "because". */
100
+ because(reason: string): SeeControlFlowTail;
90
101
  }
91
102
 
92
103
  declare global {
@@ -430,28 +441,29 @@ interface SeeApi {
430
441
  /**
431
442
  * Report a non-exception problem. Prefer passing a caught Error to `see()`
432
443
  * when one exists. The name is a stable identifier (it participates in the
433
- * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
444
+ * issue fingerprint) — variable data goes in `.extras()`, never the name.
434
445
  *
435
446
  * ```ts
436
447
  * if (rows.length > LIMIT) {
437
- * see.Violation("large query").message(`got ${rows.length} rows`)
438
- * .causes_the("search results").to("be trimmed");
448
+ * see.Violation("large query")
449
+ * .causes_the("search results").to("be trimmed").extras({ rows: rows.length });
439
450
  * }
440
451
  * ```
441
452
  */
442
453
  Violation(name: string): SeeViolationChain;
443
454
  /**
444
455
  * Mark an exception as expected control flow — auto-capture skips it and
445
- * nothing is reported. The reason must start with "because".
456
+ * nothing is reported. Say why with `.because()` (reason should start with
457
+ * "because"); attach optional debug context with `.extras()`.
446
458
  *
447
459
  * ```ts
448
460
  * } catch (e) {
449
- * see.ControlFlowException(e, "because the blob wasn't an encoded Foo");
461
+ * see.ControlFlowException(e).because("because the blob wasn't an encoded Foo");
450
462
  * return decodeAsBar(blob);
451
463
  * }
452
464
  * ```
453
465
  */
454
- ControlFlowException(err: unknown, because: string): void;
466
+ ControlFlowException(err: unknown): SeeControlFlowChain;
455
467
  }
456
468
  /**
457
469
  * Structured error reporter — the whole grammar hangs off this one import.
@@ -497,4 +509,4 @@ interface I18nFacade {
497
509
  }
498
510
  declare const i18n: I18nFacade;
499
511
 
500
- export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
512
+ export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -126,25 +126,29 @@ function causesThe(subject) {
126
126
  };
127
127
  }
128
128
  function violation(name) {
129
- const make = (msg) => ({
130
- __seViolation: true,
131
- violationName: String(name),
132
- ...msg !== void 0 ? { violationMessage: msg } : {},
133
- message(m) {
134
- return make(String(m));
135
- }
136
- });
137
- return make();
129
+ return { __seViolation: true, violationName: String(name) };
138
130
  }
139
131
  function isViolation(p) {
140
132
  return typeof p === "object" && p !== null && p.__seViolation === true;
141
133
  }
142
134
  var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
143
- function markExpected(err, because) {
135
+ function readExpectedMark(err) {
136
+ if (typeof err !== "object" || err === null) return void 0;
137
+ const v = err[EXPECTED_SYM];
138
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
139
+ }
140
+ function markExpected(err, because, extras) {
144
141
  if (typeof err !== "object" || err === null) return;
142
+ const prev = readExpectedMark(err);
143
+ const clean = sanitizeExtras(extras);
144
+ const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
145
+ const mark = {
146
+ because: String(because),
147
+ ...merged ? { extras: merged } : {}
148
+ };
145
149
  try {
146
150
  Object.defineProperty(err, EXPECTED_SYM, {
147
- value: String(because),
151
+ value: mark,
148
152
  enumerable: false,
149
153
  configurable: true
150
154
  });
@@ -225,7 +229,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
225
229
  let kind;
226
230
  if (isViolation(problem)) {
227
231
  errorType = problem.violationName;
228
- message = problem.violationMessage ?? problem.violationName;
232
+ message = problem.violationName;
229
233
  stack = captureCallsiteStack();
230
234
  kind = kindOverride ?? "violation";
231
235
  } else if (problem instanceof Error) {
@@ -308,19 +312,21 @@ function startSeeChain(getProblem, dispatch) {
308
312
  return { causes_the: start, causesThe: start };
309
313
  }
310
314
  function startSeeViolationChain(name, dispatch) {
311
- let msg;
312
- const base = startSeeChain(
313
- () => msg !== void 0 ? violation(name).message(msg) : violation(name),
314
- dispatch
315
- );
316
- const chain = {
317
- ...base,
318
- message(m) {
319
- msg = String(m);
320
- return chain;
315
+ return startSeeChain(() => violation(name), dispatch);
316
+ }
317
+ function startControlFlowChain(err) {
318
+ return {
319
+ because(reason) {
320
+ markExpected(err, reason);
321
+ const tail = {
322
+ extras(x) {
323
+ markExpected(err, reason, x);
324
+ return tail;
325
+ }
326
+ };
327
+ return tail;
321
328
  }
322
329
  };
323
- return chain;
324
330
  }
325
331
  function topStackLine(stack) {
326
332
  if (!stack) return "";
@@ -575,39 +581,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
575
581
  }
576
582
  }
577
583
  if (groups.errors) {
578
- const origOnError = window.onerror;
579
- window.onerror = (msg, source, lineno, _colno, err) => {
580
- if (!isExpected(err)) {
581
- const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
582
- reportSee(
583
- problem,
584
- causesThe("page").to("hit an unhandled error"),
585
- {
586
- source: typeof source === "string" ? source : void 0,
587
- line: lineno ?? void 0
588
- },
589
- "uncaught"
590
- );
591
- }
592
- if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
593
- return false;
594
- };
595
- window.addEventListener("unhandledrejection", (e) => {
596
- const reason = e.reason;
597
- if (isExpected(reason)) return;
598
- reportSee(
599
- reason ?? "Unhandled promise rejection",
600
- causesThe("page").to("hit an unhandled promise rejection"),
601
- void 0,
602
- "unhandled_rejection"
603
- );
604
- });
605
584
  const origFetch = window.fetch;
606
585
  window.fetch = async function(...args) {
607
586
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
608
587
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
609
588
  const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
610
- const bareUrl = url.split("?")[0].slice(0, 200);
611
589
  let corr;
612
590
  if (!ignored && sameOrigin(url) && typeof crypto !== "undefined" && crypto.randomUUID) {
613
591
  corr = crypto.randomUUID();
@@ -619,7 +597,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
619
597
  } catch (err) {
620
598
  if (!ignored && !isExpected(err)) {
621
599
  reportSee(
622
- violation("NetworkError").message(`request to ${bareUrl} failed`),
600
+ violation("NetworkError"),
623
601
  causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
624
602
  { status: 0, url: url.slice(0, 200) },
625
603
  "network"
@@ -630,7 +608,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
630
608
  if (!ignored && res.status >= 500) {
631
609
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
632
610
  reportSee(
633
- violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
611
+ violation("Http5xx"),
634
612
  causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
635
613
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
636
614
  "network",
@@ -1409,7 +1387,7 @@ var see = Object.assign(
1409
1387
  (problem) => startSeeChain(() => problem, dispatchSee),
1410
1388
  {
1411
1389
  Violation: (name) => startSeeViolationChain(name, dispatchSee),
1412
- ControlFlowException: markExpected
1390
+ ControlFlowException: (err) => startControlFlowChain(err)
1413
1391
  }
1414
1392
  );
1415
1393
  var LABEL_MARKER_START = "\uFFF9";
@@ -78,25 +78,29 @@ function causesThe(subject) {
78
78
  };
79
79
  }
80
80
  function violation(name) {
81
- const make = (msg) => ({
82
- __seViolation: true,
83
- violationName: String(name),
84
- ...msg !== void 0 ? { violationMessage: msg } : {},
85
- message(m) {
86
- return make(String(m));
87
- }
88
- });
89
- return make();
81
+ return { __seViolation: true, violationName: String(name) };
90
82
  }
91
83
  function isViolation(p) {
92
84
  return typeof p === "object" && p !== null && p.__seViolation === true;
93
85
  }
94
86
  var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
95
- function markExpected(err, because) {
87
+ function readExpectedMark(err) {
88
+ if (typeof err !== "object" || err === null) return void 0;
89
+ const v = err[EXPECTED_SYM];
90
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
91
+ }
92
+ function markExpected(err, because, extras) {
96
93
  if (typeof err !== "object" || err === null) return;
94
+ const prev = readExpectedMark(err);
95
+ const clean = sanitizeExtras(extras);
96
+ const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
97
+ const mark = {
98
+ because: String(because),
99
+ ...merged ? { extras: merged } : {}
100
+ };
97
101
  try {
98
102
  Object.defineProperty(err, EXPECTED_SYM, {
99
- value: String(because),
103
+ value: mark,
100
104
  enumerable: false,
101
105
  configurable: true
102
106
  });
@@ -177,7 +181,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
177
181
  let kind;
178
182
  if (isViolation(problem)) {
179
183
  errorType = problem.violationName;
180
- message = problem.violationMessage ?? problem.violationName;
184
+ message = problem.violationName;
181
185
  stack = captureCallsiteStack();
182
186
  kind = kindOverride ?? "violation";
183
187
  } else if (problem instanceof Error) {
@@ -260,19 +264,21 @@ function startSeeChain(getProblem, dispatch) {
260
264
  return { causes_the: start, causesThe: start };
261
265
  }
262
266
  function startSeeViolationChain(name, dispatch) {
263
- let msg;
264
- const base = startSeeChain(
265
- () => msg !== void 0 ? violation(name).message(msg) : violation(name),
266
- dispatch
267
- );
268
- const chain = {
269
- ...base,
270
- message(m) {
271
- msg = String(m);
272
- return chain;
267
+ return startSeeChain(() => violation(name), dispatch);
268
+ }
269
+ function startControlFlowChain(err) {
270
+ return {
271
+ because(reason) {
272
+ markExpected(err, reason);
273
+ const tail = {
274
+ extras(x) {
275
+ markExpected(err, reason, x);
276
+ return tail;
277
+ }
278
+ };
279
+ return tail;
273
280
  }
274
281
  };
275
- return chain;
276
282
  }
277
283
  function topStackLine(stack) {
278
284
  if (!stack) return "";
@@ -527,39 +533,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
527
533
  }
528
534
  }
529
535
  if (groups.errors) {
530
- const origOnError = window.onerror;
531
- window.onerror = (msg, source, lineno, _colno, err) => {
532
- if (!isExpected(err)) {
533
- const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
534
- reportSee(
535
- problem,
536
- causesThe("page").to("hit an unhandled error"),
537
- {
538
- source: typeof source === "string" ? source : void 0,
539
- line: lineno ?? void 0
540
- },
541
- "uncaught"
542
- );
543
- }
544
- if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
545
- return false;
546
- };
547
- window.addEventListener("unhandledrejection", (e) => {
548
- const reason = e.reason;
549
- if (isExpected(reason)) return;
550
- reportSee(
551
- reason ?? "Unhandled promise rejection",
552
- causesThe("page").to("hit an unhandled promise rejection"),
553
- void 0,
554
- "unhandled_rejection"
555
- );
556
- });
557
536
  const origFetch = window.fetch;
558
537
  window.fetch = async function(...args) {
559
538
  const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
560
539
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
561
540
  const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
562
- const bareUrl = url.split("?")[0].slice(0, 200);
563
541
  let corr;
564
542
  if (!ignored && sameOrigin(url) && typeof crypto !== "undefined" && crypto.randomUUID) {
565
543
  corr = crypto.randomUUID();
@@ -571,7 +549,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
571
549
  } catch (err) {
572
550
  if (!ignored && !isExpected(err)) {
573
551
  reportSee(
574
- violation("NetworkError").message(`request to ${bareUrl} failed`),
552
+ violation("NetworkError"),
575
553
  causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
576
554
  { status: 0, url: url.slice(0, 200) },
577
555
  "network"
@@ -582,7 +560,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
582
560
  if (!ignored && res.status >= 500) {
583
561
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
584
562
  reportSee(
585
- violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
563
+ violation("Http5xx"),
586
564
  causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
587
565
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
588
566
  "network",
@@ -1361,7 +1339,7 @@ var see = Object.assign(
1361
1339
  (problem) => startSeeChain(() => problem, dispatchSee),
1362
1340
  {
1363
1341
  Violation: (name) => startSeeViolationChain(name, dispatchSee),
1364
- ControlFlowException: markExpected
1342
+ ControlFlowException: (err) => startControlFlowChain(err)
1365
1343
  }
1366
1344
  );
1367
1345
  var LABEL_MARKER_START = "\uFFF9";
@@ -0,0 +1,50 @@
1
+ import { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
2
+
3
+ /** The first-party cookie that carries the stable anonymous bucketing unit. */
4
+ declare const ANON_ID_COOKIE = "__se_anon_id";
5
+ interface AnonIdResult {
6
+ /** The stable bucketing id for this request (existing cookie, or freshly minted). */
7
+ anonId: string;
8
+ /** True when there was no valid cookie and we minted a new id (must be persisted). */
9
+ minted: boolean;
10
+ }
11
+ /**
12
+ * Read + validate the `__se_anon_id` cookie, minting one when absent/invalid.
13
+ * When `requestHeaders` is supplied and we mint, the new id is appended to the
14
+ * forwarded `cookie` header so THIS request's SSR (and any inner middleware)
15
+ * already sees it. Pair with {@link commitAnonId} to persist a minted id.
16
+ */
17
+ declare function readOrMintAnonId(req: NextRequest, requestHeaders?: Headers): AnonIdResult;
18
+ /**
19
+ * Persist a freshly-minted anon id as a first-party cookie on `res` (no-op when
20
+ * `minted` is false). Non-httpOnly by contract — the browser SDK reads it via
21
+ * `document.cookie` to bucket identically to SSR. Returns `res` for chaining.
22
+ */
23
+ declare function commitAnonId(res: NextResponse, result: AnonIdResult, req: NextRequest): NextResponse;
24
+ type ShipeasyMiddleware = (req: NextRequest, event: NextFetchEvent) => NextResponse | Response | undefined | null | void | Promise<NextResponse | Response | undefined | null | void>;
25
+ /**
26
+ * Wrap an existing Next middleware so every matched request also mints the
27
+ * shared `__se_anon_id` cookie. Your middleware runs against a request that
28
+ * already carries the id (its own logic + SSR see it), and the cookie is set on
29
+ * whatever response it returns. Called with no argument, returns a standalone
30
+ * mint-only middleware (what the default {@link middleware} export uses).
31
+ *
32
+ * If your middleware forwards custom request headers via
33
+ * `NextResponse.next({ request: { headers } })`, prefer composing the
34
+ * {@link readOrMintAnonId} + {@link commitAnonId} primitives inside it — that
35
+ * preserves your forwarding verbatim; this wrapper rebuilds the pass-through
36
+ * `next()` and so would drop request-header forwarding the inner handler added.
37
+ */
38
+ declare function withShipeasy(handler?: ShipeasyMiddleware): ShipeasyMiddleware;
39
+ /** Drop-in middleware: `export { middleware, config } from "@shipeasy/sdk/next";` */
40
+ declare const middleware: ShipeasyMiddleware;
41
+ /**
42
+ * Matcher for the drop-in middleware: every HTML route (landing + app), skipping
43
+ * Next internals, API routes, and static files (any path segment with a dot).
44
+ * Define your own `config` if you need a narrower scope.
45
+ */
46
+ declare const config: {
47
+ matcher: string[];
48
+ };
49
+
50
+ export { ANON_ID_COOKIE, type AnonIdResult, type ShipeasyMiddleware, commitAnonId, config, middleware, readOrMintAnonId, withShipeasy };
@@ -0,0 +1,50 @@
1
+ import { NextRequest, NextFetchEvent, NextResponse } from 'next/server';
2
+
3
+ /** The first-party cookie that carries the stable anonymous bucketing unit. */
4
+ declare const ANON_ID_COOKIE = "__se_anon_id";
5
+ interface AnonIdResult {
6
+ /** The stable bucketing id for this request (existing cookie, or freshly minted). */
7
+ anonId: string;
8
+ /** True when there was no valid cookie and we minted a new id (must be persisted). */
9
+ minted: boolean;
10
+ }
11
+ /**
12
+ * Read + validate the `__se_anon_id` cookie, minting one when absent/invalid.
13
+ * When `requestHeaders` is supplied and we mint, the new id is appended to the
14
+ * forwarded `cookie` header so THIS request's SSR (and any inner middleware)
15
+ * already sees it. Pair with {@link commitAnonId} to persist a minted id.
16
+ */
17
+ declare function readOrMintAnonId(req: NextRequest, requestHeaders?: Headers): AnonIdResult;
18
+ /**
19
+ * Persist a freshly-minted anon id as a first-party cookie on `res` (no-op when
20
+ * `minted` is false). Non-httpOnly by contract — the browser SDK reads it via
21
+ * `document.cookie` to bucket identically to SSR. Returns `res` for chaining.
22
+ */
23
+ declare function commitAnonId(res: NextResponse, result: AnonIdResult, req: NextRequest): NextResponse;
24
+ type ShipeasyMiddleware = (req: NextRequest, event: NextFetchEvent) => NextResponse | Response | undefined | null | void | Promise<NextResponse | Response | undefined | null | void>;
25
+ /**
26
+ * Wrap an existing Next middleware so every matched request also mints the
27
+ * shared `__se_anon_id` cookie. Your middleware runs against a request that
28
+ * already carries the id (its own logic + SSR see it), and the cookie is set on
29
+ * whatever response it returns. Called with no argument, returns a standalone
30
+ * mint-only middleware (what the default {@link middleware} export uses).
31
+ *
32
+ * If your middleware forwards custom request headers via
33
+ * `NextResponse.next({ request: { headers } })`, prefer composing the
34
+ * {@link readOrMintAnonId} + {@link commitAnonId} primitives inside it — that
35
+ * preserves your forwarding verbatim; this wrapper rebuilds the pass-through
36
+ * `next()` and so would drop request-header forwarding the inner handler added.
37
+ */
38
+ declare function withShipeasy(handler?: ShipeasyMiddleware): ShipeasyMiddleware;
39
+ /** Drop-in middleware: `export { middleware, config } from "@shipeasy/sdk/next";` */
40
+ declare const middleware: ShipeasyMiddleware;
41
+ /**
42
+ * Matcher for the drop-in middleware: every HTML route (landing + app), skipping
43
+ * Next internals, API routes, and static files (any path segment with a dot).
44
+ * Define your own `config` if you need a narrower scope.
45
+ */
46
+ declare const config: {
47
+ matcher: string[];
48
+ };
49
+
50
+ export { ANON_ID_COOKIE, type AnonIdResult, type ShipeasyMiddleware, commitAnonId, config, middleware, readOrMintAnonId, withShipeasy };
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/next/index.ts
21
+ var next_exports = {};
22
+ __export(next_exports, {
23
+ ANON_ID_COOKIE: () => ANON_ID_COOKIE,
24
+ commitAnonId: () => commitAnonId,
25
+ config: () => config,
26
+ middleware: () => middleware,
27
+ readOrMintAnonId: () => readOrMintAnonId,
28
+ withShipeasy: () => withShipeasy
29
+ });
30
+ module.exports = __toCommonJS(next_exports);
31
+ var import_server = require("next/server");
32
+ var ANON_ID_COOKIE = "__se_anon_id";
33
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
34
+ var ANON_ID_MAX_AGE = 60 * 60 * 24 * 365;
35
+ function mint() {
36
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
37
+ }
38
+ function appendCookie(headers, name, value) {
39
+ const existing = headers.get("cookie") ?? "";
40
+ const parts = existing.split(";").map((c) => c.trim()).filter((c) => c && !c.startsWith(`${name}=`));
41
+ parts.push(`${name}=${value}`);
42
+ headers.set("cookie", parts.join("; "));
43
+ }
44
+ function readOrMintAnonId(req, requestHeaders) {
45
+ const raw = req.cookies.get(ANON_ID_COOKIE)?.value;
46
+ const valid = !!raw && ANON_ID_RX.test(raw);
47
+ const anonId = valid ? raw : mint();
48
+ const minted = !valid;
49
+ if (minted && requestHeaders) appendCookie(requestHeaders, ANON_ID_COOKIE, anonId);
50
+ return { anonId, minted };
51
+ }
52
+ function commitAnonId(res, result, req) {
53
+ if (result.minted) {
54
+ res.cookies.set(ANON_ID_COOKIE, result.anonId, {
55
+ httpOnly: false,
56
+ secure: req.nextUrl.protocol === "https:",
57
+ sameSite: "lax",
58
+ path: "/",
59
+ maxAge: ANON_ID_MAX_AGE
60
+ });
61
+ }
62
+ return res;
63
+ }
64
+ function withShipeasy(handler) {
65
+ return async (req, event) => {
66
+ const requestHeaders = new Headers(req.headers);
67
+ const anon = readOrMintAnonId(req, requestHeaders);
68
+ if (anon.minted) req.cookies.set(ANON_ID_COOKIE, anon.anonId);
69
+ const userRes = handler ? await handler(req, event) : void 0;
70
+ const isPassThrough = !userRes || userRes instanceof import_server.NextResponse && userRes.headers.get("x-middleware-next") === "1";
71
+ if (isPassThrough) {
72
+ const res2 = import_server.NextResponse.next({ request: { headers: requestHeaders } });
73
+ if (userRes instanceof import_server.NextResponse) {
74
+ userRes.cookies.getAll().forEach((c) => res2.cookies.set(c));
75
+ userRes.headers.forEach((v, k) => {
76
+ if (k !== "x-middleware-next") res2.headers.set(k, v);
77
+ });
78
+ }
79
+ return commitAnonId(res2, anon, req);
80
+ }
81
+ const res = userRes instanceof import_server.NextResponse ? userRes : userRes instanceof Response ? new import_server.NextResponse(userRes.body, userRes) : import_server.NextResponse.next({ request: { headers: requestHeaders } });
82
+ return commitAnonId(res, anon, req);
83
+ };
84
+ }
85
+ var middleware = withShipeasy();
86
+ var config = {
87
+ matcher: ["/((?!api/|_next/|.*\\..*).*)"]
88
+ };
89
+ // Annotate the CommonJS export names for ESM import in node:
90
+ 0 && (module.exports = {
91
+ ANON_ID_COOKIE,
92
+ commitAnonId,
93
+ config,
94
+ middleware,
95
+ readOrMintAnonId,
96
+ withShipeasy
97
+ });
@@ -0,0 +1,67 @@
1
+ // src/next/index.ts
2
+ import { NextResponse } from "next/server";
3
+ var ANON_ID_COOKIE = "__se_anon_id";
4
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
5
+ var ANON_ID_MAX_AGE = 60 * 60 * 24 * 365;
6
+ function mint() {
7
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
8
+ }
9
+ function appendCookie(headers, name, value) {
10
+ const existing = headers.get("cookie") ?? "";
11
+ const parts = existing.split(";").map((c) => c.trim()).filter((c) => c && !c.startsWith(`${name}=`));
12
+ parts.push(`${name}=${value}`);
13
+ headers.set("cookie", parts.join("; "));
14
+ }
15
+ function readOrMintAnonId(req, requestHeaders) {
16
+ const raw = req.cookies.get(ANON_ID_COOKIE)?.value;
17
+ const valid = !!raw && ANON_ID_RX.test(raw);
18
+ const anonId = valid ? raw : mint();
19
+ const minted = !valid;
20
+ if (minted && requestHeaders) appendCookie(requestHeaders, ANON_ID_COOKIE, anonId);
21
+ return { anonId, minted };
22
+ }
23
+ function commitAnonId(res, result, req) {
24
+ if (result.minted) {
25
+ res.cookies.set(ANON_ID_COOKIE, result.anonId, {
26
+ httpOnly: false,
27
+ secure: req.nextUrl.protocol === "https:",
28
+ sameSite: "lax",
29
+ path: "/",
30
+ maxAge: ANON_ID_MAX_AGE
31
+ });
32
+ }
33
+ return res;
34
+ }
35
+ function withShipeasy(handler) {
36
+ return async (req, event) => {
37
+ const requestHeaders = new Headers(req.headers);
38
+ const anon = readOrMintAnonId(req, requestHeaders);
39
+ if (anon.minted) req.cookies.set(ANON_ID_COOKIE, anon.anonId);
40
+ const userRes = handler ? await handler(req, event) : void 0;
41
+ const isPassThrough = !userRes || userRes instanceof NextResponse && userRes.headers.get("x-middleware-next") === "1";
42
+ if (isPassThrough) {
43
+ const res2 = NextResponse.next({ request: { headers: requestHeaders } });
44
+ if (userRes instanceof NextResponse) {
45
+ userRes.cookies.getAll().forEach((c) => res2.cookies.set(c));
46
+ userRes.headers.forEach((v, k) => {
47
+ if (k !== "x-middleware-next") res2.headers.set(k, v);
48
+ });
49
+ }
50
+ return commitAnonId(res2, anon, req);
51
+ }
52
+ const res = userRes instanceof NextResponse ? userRes : userRes instanceof Response ? new NextResponse(userRes.body, userRes) : NextResponse.next({ request: { headers: requestHeaders } });
53
+ return commitAnonId(res, anon, req);
54
+ };
55
+ }
56
+ var middleware = withShipeasy();
57
+ var config = {
58
+ matcher: ["/((?!api/|_next/|.*\\..*).*)"]
59
+ };
60
+ export {
61
+ ANON_ID_COOKIE,
62
+ commitAnonId,
63
+ config,
64
+ middleware,
65
+ readOrMintAnonId,
66
+ withShipeasy
67
+ };
@@ -10,15 +10,13 @@ interface Consequence {
10
10
  }
11
11
  /**
12
12
  * Non-exception problem, built by `violation(name)`. A plain branded object
13
- * (not an Error subclass) so `.message()` can be a builder method without
14
- * colliding with `Error.prototype.message`.
13
+ * (not an Error subclass). The name is the whole identity there is no
14
+ * separate message; any variable/context data belongs in `.extras()` on the
15
+ * see chain, never on the violation itself.
15
16
  */
16
17
  interface Violation {
17
18
  readonly __seViolation: true;
18
19
  readonly violationName: string;
19
- readonly violationMessage?: string;
20
- /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
21
- message(msg: string): Violation;
22
20
  }
23
21
  /**
24
22
  * Identity of a problem that see() already reported, carried on the wire as
@@ -87,9 +85,22 @@ interface SeeChain {
87
85
  /** camelCase alias of {@link SeeChain.causes_the}. */
88
86
  causesThe(subject: string): SeeOutcomeStep;
89
87
  }
90
- interface SeeViolationChain extends SeeChain {
91
- /** Free-form detail. Variable data goes here (or extras), never in the name. */
92
- message(msg: string): SeeViolationChain;
88
+ /**
89
+ * Violations share the exception consequence grammar exactly there is no
90
+ * separate `.message()`. Put any variable/context data in `.extras()`.
91
+ */
92
+ type SeeViolationChain = SeeChain;
93
+ interface SeeControlFlowTail {
94
+ /**
95
+ * Optional debugging context for the expected exception. Kept on the mark for
96
+ * local debugging only (expected exceptions are never reported). Callable
97
+ * repeatedly — keys merge, later wins.
98
+ */
99
+ extras(extras: SeeExtras): SeeControlFlowTail;
100
+ }
101
+ interface SeeControlFlowChain {
102
+ /** Document why the exception is expected. The reason should start with "because". */
103
+ because(reason: string): SeeControlFlowTail;
93
104
  }
94
105
 
95
106
  declare const version = "4.0.0";
@@ -367,20 +378,26 @@ interface SeeApi {
367
378
  /**
368
379
  * Report a non-exception problem. Prefer passing a caught Error to `see()`
369
380
  * when one exists. The name is a stable identifier (it participates in the
370
- * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
381
+ * issue fingerprint) — variable data goes in `.extras()`, never the name.
371
382
  *
372
383
  * ```ts
373
384
  * if (results.length > LIMIT) {
374
- * see.Violation("large query").causes_the("search results").to("be trimmed");
385
+ * see.Violation("large query")
386
+ * .causes_the("search results").to("be trimmed").extras({ rows: results.length });
375
387
  * }
376
388
  * ```
377
389
  */
378
390
  Violation(name: string): SeeViolationChain;
379
391
  /**
380
392
  * Mark an exception as expected control flow — documents the expectation and
381
- * reports nothing. The reason must start with "because".
393
+ * reports nothing. Say why with `.because()` (reason should start with
394
+ * "because"); attach optional debug context with `.extras()`.
395
+ *
396
+ * ```ts
397
+ * see.ControlFlowException(e).because("because the metric may not exist yet");
398
+ * ```
382
399
  */
383
- ControlFlowException(err: unknown, because: string): void;
400
+ ControlFlowException(err: unknown): SeeControlFlowChain;
384
401
  }
385
402
  /**
386
403
  * Structured error reporter — the whole grammar hangs off this one import.
@@ -389,4 +406,4 @@ interface SeeApi {
389
406
  */
390
407
  declare const see: SeeApi;
391
408
 
392
- export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
409
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -10,15 +10,13 @@ interface Consequence {
10
10
  }
11
11
  /**
12
12
  * Non-exception problem, built by `violation(name)`. A plain branded object
13
- * (not an Error subclass) so `.message()` can be a builder method without
14
- * colliding with `Error.prototype.message`.
13
+ * (not an Error subclass). The name is the whole identity there is no
14
+ * separate message; any variable/context data belongs in `.extras()` on the
15
+ * see chain, never on the violation itself.
15
16
  */
16
17
  interface Violation {
17
18
  readonly __seViolation: true;
18
19
  readonly violationName: string;
19
- readonly violationMessage?: string;
20
- /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
21
- message(msg: string): Violation;
22
20
  }
23
21
  /**
24
22
  * Identity of a problem that see() already reported, carried on the wire as
@@ -87,9 +85,22 @@ interface SeeChain {
87
85
  /** camelCase alias of {@link SeeChain.causes_the}. */
88
86
  causesThe(subject: string): SeeOutcomeStep;
89
87
  }
90
- interface SeeViolationChain extends SeeChain {
91
- /** Free-form detail. Variable data goes here (or extras), never in the name. */
92
- message(msg: string): SeeViolationChain;
88
+ /**
89
+ * Violations share the exception consequence grammar exactly there is no
90
+ * separate `.message()`. Put any variable/context data in `.extras()`.
91
+ */
92
+ type SeeViolationChain = SeeChain;
93
+ interface SeeControlFlowTail {
94
+ /**
95
+ * Optional debugging context for the expected exception. Kept on the mark for
96
+ * local debugging only (expected exceptions are never reported). Callable
97
+ * repeatedly — keys merge, later wins.
98
+ */
99
+ extras(extras: SeeExtras): SeeControlFlowTail;
100
+ }
101
+ interface SeeControlFlowChain {
102
+ /** Document why the exception is expected. The reason should start with "because". */
103
+ because(reason: string): SeeControlFlowTail;
93
104
  }
94
105
 
95
106
  declare const version = "4.0.0";
@@ -367,20 +378,26 @@ interface SeeApi {
367
378
  /**
368
379
  * Report a non-exception problem. Prefer passing a caught Error to `see()`
369
380
  * when one exists. The name is a stable identifier (it participates in the
370
- * issue fingerprint) — variable data goes in `.message()` or `.extras()`.
381
+ * issue fingerprint) — variable data goes in `.extras()`, never the name.
371
382
  *
372
383
  * ```ts
373
384
  * if (results.length > LIMIT) {
374
- * see.Violation("large query").causes_the("search results").to("be trimmed");
385
+ * see.Violation("large query")
386
+ * .causes_the("search results").to("be trimmed").extras({ rows: results.length });
375
387
  * }
376
388
  * ```
377
389
  */
378
390
  Violation(name: string): SeeViolationChain;
379
391
  /**
380
392
  * Mark an exception as expected control flow — documents the expectation and
381
- * reports nothing. The reason must start with "because".
393
+ * reports nothing. Say why with `.because()` (reason should start with
394
+ * "because"); attach optional debug context with `.extras()`.
395
+ *
396
+ * ```ts
397
+ * see.ControlFlowException(e).because("because the metric may not exist yet");
398
+ * ```
382
399
  */
383
- ControlFlowException(err: unknown, because: string): void;
400
+ ControlFlowException(err: unknown): SeeControlFlowChain;
384
401
  }
385
402
  /**
386
403
  * Structured error reporter — the whole grammar hangs off this one import.
@@ -389,4 +406,4 @@ interface SeeApi {
389
406
  */
390
407
  declare const see: SeeApi;
391
408
 
392
- export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
409
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeControlFlowChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -128,25 +128,29 @@ function causesThe(subject) {
128
128
  };
129
129
  }
130
130
  function violation(name) {
131
- const make = (msg) => ({
132
- __seViolation: true,
133
- violationName: String(name),
134
- ...msg !== void 0 ? { violationMessage: msg } : {},
135
- message(m) {
136
- return make(String(m));
137
- }
138
- });
139
- return make();
131
+ return { __seViolation: true, violationName: String(name) };
140
132
  }
141
133
  function isViolation(p) {
142
134
  return typeof p === "object" && p !== null && p.__seViolation === true;
143
135
  }
144
136
  var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
145
- function markExpected(err, because) {
137
+ function readExpectedMark(err) {
138
+ if (typeof err !== "object" || err === null) return void 0;
139
+ const v = err[EXPECTED_SYM];
140
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
141
+ }
142
+ function markExpected(err, because, extras) {
146
143
  if (typeof err !== "object" || err === null) return;
144
+ const prev = readExpectedMark(err);
145
+ const clean = sanitizeExtras(extras);
146
+ const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
147
+ const mark = {
148
+ because: String(because),
149
+ ...merged ? { extras: merged } : {}
150
+ };
147
151
  try {
148
152
  Object.defineProperty(err, EXPECTED_SYM, {
149
- value: String(because),
153
+ value: mark,
150
154
  enumerable: false,
151
155
  configurable: true
152
156
  });
@@ -227,7 +231,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
227
231
  let kind;
228
232
  if (isViolation(problem)) {
229
233
  errorType = problem.violationName;
230
- message = problem.violationMessage ?? problem.violationName;
234
+ message = problem.violationName;
231
235
  stack = captureCallsiteStack();
232
236
  kind = kindOverride ?? "violation";
233
237
  } else if (problem instanceof Error) {
@@ -310,19 +314,21 @@ function startSeeChain(getProblem, dispatch) {
310
314
  return { causes_the: start, causesThe: start };
311
315
  }
312
316
  function startSeeViolationChain(name, dispatch) {
313
- let msg;
314
- const base = startSeeChain(
315
- () => msg !== void 0 ? violation(name).message(msg) : violation(name),
316
- dispatch
317
- );
318
- const chain = {
319
- ...base,
320
- message(m) {
321
- msg = String(m);
322
- return chain;
317
+ return startSeeChain(() => violation(name), dispatch);
318
+ }
319
+ function startControlFlowChain(err) {
320
+ return {
321
+ because(reason) {
322
+ markExpected(err, reason);
323
+ const tail = {
324
+ extras(x) {
325
+ markExpected(err, reason, x);
326
+ return tail;
327
+ }
328
+ };
329
+ return tail;
323
330
  }
324
331
  };
325
- return chain;
326
332
  }
327
333
  function topStackLine(stack) {
328
334
  if (!stack) return "";
@@ -1027,7 +1033,7 @@ var see = Object.assign(
1027
1033
  (problem) => startSeeChain(() => problem, dispatchSee),
1028
1034
  {
1029
1035
  Violation: (name) => startSeeViolationChain(name, dispatchSee),
1030
- ControlFlowException: markExpected
1036
+ ControlFlowException: (err) => startControlFlowChain(err)
1031
1037
  }
1032
1038
  );
1033
1039
  // Annotate the CommonJS export names for ESM import in node:
@@ -81,25 +81,29 @@ function causesThe(subject) {
81
81
  };
82
82
  }
83
83
  function violation(name) {
84
- const make = (msg) => ({
85
- __seViolation: true,
86
- violationName: String(name),
87
- ...msg !== void 0 ? { violationMessage: msg } : {},
88
- message(m) {
89
- return make(String(m));
90
- }
91
- });
92
- return make();
84
+ return { __seViolation: true, violationName: String(name) };
93
85
  }
94
86
  function isViolation(p) {
95
87
  return typeof p === "object" && p !== null && p.__seViolation === true;
96
88
  }
97
89
  var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
98
- function markExpected(err, because) {
90
+ function readExpectedMark(err) {
91
+ if (typeof err !== "object" || err === null) return void 0;
92
+ const v = err[EXPECTED_SYM];
93
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
94
+ }
95
+ function markExpected(err, because, extras) {
99
96
  if (typeof err !== "object" || err === null) return;
97
+ const prev = readExpectedMark(err);
98
+ const clean = sanitizeExtras(extras);
99
+ const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
100
+ const mark = {
101
+ because: String(because),
102
+ ...merged ? { extras: merged } : {}
103
+ };
100
104
  try {
101
105
  Object.defineProperty(err, EXPECTED_SYM, {
102
- value: String(because),
106
+ value: mark,
103
107
  enumerable: false,
104
108
  configurable: true
105
109
  });
@@ -180,7 +184,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
180
184
  let kind;
181
185
  if (isViolation(problem)) {
182
186
  errorType = problem.violationName;
183
- message = problem.violationMessage ?? problem.violationName;
187
+ message = problem.violationName;
184
188
  stack = captureCallsiteStack();
185
189
  kind = kindOverride ?? "violation";
186
190
  } else if (problem instanceof Error) {
@@ -263,19 +267,21 @@ function startSeeChain(getProblem, dispatch) {
263
267
  return { causes_the: start, causesThe: start };
264
268
  }
265
269
  function startSeeViolationChain(name, dispatch) {
266
- let msg;
267
- const base = startSeeChain(
268
- () => msg !== void 0 ? violation(name).message(msg) : violation(name),
269
- dispatch
270
- );
271
- const chain = {
272
- ...base,
273
- message(m) {
274
- msg = String(m);
275
- return chain;
270
+ return startSeeChain(() => violation(name), dispatch);
271
+ }
272
+ function startControlFlowChain(err) {
273
+ return {
274
+ because(reason) {
275
+ markExpected(err, reason);
276
+ const tail = {
277
+ extras(x) {
278
+ markExpected(err, reason, x);
279
+ return tail;
280
+ }
281
+ };
282
+ return tail;
276
283
  }
277
284
  };
278
- return chain;
279
285
  }
280
286
  function topStackLine(stack) {
281
287
  if (!stack) return "";
@@ -980,7 +986,7 @@ var see = Object.assign(
980
986
  (problem) => startSeeChain(() => problem, dispatchSee),
981
987
  {
982
988
  Violation: (name) => startSeeViolationChain(name, dispatchSee),
983
- ControlFlowException: markExpected
989
+ ControlFlowException: (err) => startControlFlowChain(err)
984
990
  }
985
991
  );
986
992
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "4.4.0",
3
+ "version": "5.0.0",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",
@@ -21,6 +21,9 @@
21
21
  ],
22
22
  "server": [
23
23
  "./dist/server/index.d.ts"
24
+ ],
25
+ "next": [
26
+ "./dist/next/index.d.ts"
24
27
  ]
25
28
  }
26
29
  },
@@ -39,11 +42,17 @@
39
42
  "types": "./dist/client/index.d.ts",
40
43
  "default": "./dist/client/index.js"
41
44
  },
45
+ "./next": {
46
+ "types": "./dist/next/index.d.ts",
47
+ "import": "./dist/next/index.mjs",
48
+ "default": "./dist/next/index.js"
49
+ },
42
50
  "./templates/*": "./templates/*.js"
43
51
  },
44
52
  "files": [
45
53
  "dist/server/",
46
54
  "dist/client/",
55
+ "dist/next/",
47
56
  "templates/",
48
57
  "LICENSE",
49
58
  "README.md"
@@ -58,9 +67,18 @@
58
67
  "dependencies": {
59
68
  "murmurhash-js": "^1.0.0"
60
69
  },
70
+ "peerDependencies": {
71
+ "next": ">=13"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "next": {
75
+ "optional": true
76
+ }
77
+ },
61
78
  "devDependencies": {
62
79
  "@types/murmurhash-js": "^1.0.6",
63
80
  "@types/node": "^20.0.0",
81
+ "next": "^16.2.3",
64
82
  "tsup": "^8.3.0",
65
83
  "typescript": "^5.7.4",
66
84
  "vitest": "^2.1.0",