@logbrew/sdk 0.1.9 → 0.1.10

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
@@ -102,7 +102,7 @@ npx logbrew-release-artifacts upload-js \
102
102
 
103
103
  Non-loopback endpoints require `--allow-hosted`, a UUID `projectId` created by `manifest-js --project-id`, HTTPS, and no embedded auth values, query strings, or fragments. Local loopback preparation remains valid without a project ID. The upload command never uses normal SDK ingest keys or account/session API auth values. Full backend-symbolicated issue support is separate from artifact upload until your project has completed hosted symbolication for its release.
104
104
 
105
- When you capture a JavaScript error, use `createIssueAttributesFromError()` to keep error metadata structured and source-map-friendly without sending raw stack text by default. Pass a Debug ID map from your app-owned build setup when you want the issue event to carry release-artifact metadata:
105
+ When you capture a JavaScript error, use `createIssueAttributesFromError()` to keep error metadata structured and source-map-friendly without sending raw stack text by default. The helper also follows `Error.cause` and `AggregateError.errors` into a bounded parent-first exception graph. Automatic messages are marked redacted, every node reports whether frames were captured, truncated, or unavailable, and unsafe accessors, cycles, or the eight-node cap mark the graph truncated instead of inventing evidence. React, browser, Node, Next.js, and React Native helpers reuse this same core projection. See the shared [exception-chain contract](../../docs/exception-chain-evidence.md). Pass a Debug ID map from your app-owned build setup when you want the issue event to carry release-artifact metadata:
106
106
 
107
107
  ```js
108
108
  import { createIssueAttributesFromError, LogBrewClient } from "@logbrew/sdk";
package/core.cjs CHANGED
@@ -129,14 +129,17 @@ const {
129
129
  traceMetadataFromLogContext
130
130
  } = buildLogContextHelpers({ SdkError });
131
131
 
132
- const { javascriptStackFrames, validateIssueStackFrames } = buildIssueStackHelpers({ SdkError });
132
+ const {
133
+ javascriptStackEvidence,
134
+ validateIssueStackFrames
135
+ } = buildIssueStackHelpers({ SdkError });
133
136
  const {
134
137
  MAX_ISSUE_BREADCRUMBS,
135
138
  cloneIssueDiagnostics,
136
139
  createIssueException,
137
140
  validateIssueBreadcrumb,
138
141
  validateIssueDiagnostics
139
- } = buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp });
142
+ } = buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssueStackFrames });
140
143
  const {
141
144
  cloneTelemetryContext,
142
145
  mergeTelemetryContexts,
@@ -1363,7 +1366,8 @@ function createIssueAttributesFromError(error, options = {}) {
1363
1366
  throw new SdkError("validation_error", "error issue options must be an object");
1364
1367
  }
1365
1368
  const details = errorDetails(error);
1366
- const stackFrames = javascriptStackFrames(details.stack, options.debugIdMap);
1369
+ const stackEvidence = javascriptStackEvidence(details.stack, options.debugIdMap);
1370
+ const stackFrames = stackEvidence.frames;
1367
1371
  const frame = stackFrames[0] ?? null;
1368
1372
  const source = stringOrUndefined(options.source) ?? "javascript.error";
1369
1373
  const metadata = {
@@ -1388,33 +1392,151 @@ function createIssueAttributesFromError(error, options = {}) {
1388
1392
  ...(options.includeErrorStack === true && details.stack ? { errorStack: details.stack } : {})
1389
1393
  };
1390
1394
 
1395
+ const exception = createIssueException(
1396
+ boundedIssueExceptionType(details.name),
1397
+ stringOrUndefined(options.mechanism) ?? "javascript.error",
1398
+ options.handled === undefined ? true : options.handled
1399
+ );
1400
+ const exceptionChain = createJavaScriptExceptionChain(error, {
1401
+ details,
1402
+ exception,
1403
+ stackEvidence,
1404
+ debugIdMap: options.debugIdMap
1405
+ });
1391
1406
  return {
1392
1407
  title: stringOrUndefined(options.title) ?? details.name,
1393
1408
  level: normalizeSeverity("issue level", options.level ?? "error"),
1394
1409
  ...(stringOrUndefined(options.message) ? { message: options.message } : details.message ? { message: details.message } : {}),
1395
- exception: createIssueException(
1396
- boundedIssueExceptionType(details.name),
1397
- stringOrUndefined(options.mechanism) ?? "javascript.error",
1398
- options.handled === undefined ? true : options.handled
1399
- ),
1410
+ exception,
1411
+ exceptionChain,
1400
1412
  ...(stackFrames.length > 0 ? { stackFrames } : {}),
1401
1413
  metadata: compactMetadata(metadata)
1402
1414
  };
1403
1415
  }
1404
1416
 
1417
+ function createJavaScriptExceptionChain(error, root) {
1418
+ const state = {
1419
+ entries: [],
1420
+ seen: new Set(),
1421
+ truncated: false,
1422
+ debugIdMap: root.debugIdMap
1423
+ };
1424
+ if (isObjectLike(error)) {
1425
+ state.seen.add(error);
1426
+ }
1427
+ appendJavaScriptExceptionNode(error, undefined, "reported", state, root);
1428
+ collectJavaScriptExceptionChildren(error, 0, state);
1429
+ return { entries: state.entries, truncated: state.truncated };
1430
+ }
1431
+
1432
+ function appendJavaScriptExceptionNode(value, parentId, relationship, state, known = undefined) {
1433
+ if (state.entries.length >= 8) {
1434
+ state.truncated = true;
1435
+ return undefined;
1436
+ }
1437
+ const details = known?.details ?? errorDetails(value);
1438
+ const stackEvidence = known?.stackEvidence
1439
+ ?? javascriptStackEvidence(details.stack, state.debugIdMap);
1440
+ const mechanism = known?.exception?.mechanism ?? {
1441
+ type: relationship === "aggregate_member" ? "javascript.aggregate_member" : "javascript.cause",
1442
+ handled: true
1443
+ };
1444
+ const messageEvidence = exceptionMessageEvidence(details.message);
1445
+ const id = state.entries.length;
1446
+ state.entries.push({
1447
+ id,
1448
+ ...(parentId === undefined ? {} : { parentId }),
1449
+ relationship,
1450
+ type: known?.exception?.type ?? errorCauseType(value),
1451
+ ...messageEvidence,
1452
+ mechanism,
1453
+ ...(stackEvidence.frames.length === 0 ? {} : { stackFrames: stackEvidence.frames }),
1454
+ stackFramesState: stackEvidence.frames.length === 0
1455
+ ? "not_captured"
1456
+ : stackEvidence.truncated
1457
+ ? "truncated"
1458
+ : "captured"
1459
+ });
1460
+ return id;
1461
+ }
1462
+
1463
+ function collectJavaScriptExceptionChildren(value, parentId, state) {
1464
+ if (!isObjectLike(value)) {
1465
+ return;
1466
+ }
1467
+ const cause = safeProperty(value, "cause");
1468
+ if (cause.available && cause.value !== undefined && cause.value !== null) {
1469
+ collectJavaScriptExceptionChild(cause.value, parentId, "cause", state);
1470
+ } else if (!cause.available) {
1471
+ state.truncated = true;
1472
+ }
1473
+ const errors = safeProperty(value, "errors");
1474
+ if (!errors.available) {
1475
+ state.truncated = true;
1476
+ } else if (Array.isArray(errors.value)) {
1477
+ for (const child of errors.value) {
1478
+ collectJavaScriptExceptionChild(child, parentId, "aggregate_member", state);
1479
+ }
1480
+ }
1481
+ }
1482
+
1483
+ function collectJavaScriptExceptionChild(value, parentId, relationship, state) {
1484
+ if (state.entries.length >= 8) {
1485
+ state.truncated = true;
1486
+ return;
1487
+ }
1488
+ if (isObjectLike(value)) {
1489
+ if (state.seen.has(value)) {
1490
+ state.truncated = true;
1491
+ return;
1492
+ }
1493
+ state.seen.add(value);
1494
+ }
1495
+ const id = appendJavaScriptExceptionNode(value, parentId, relationship, state);
1496
+ if (id !== undefined) {
1497
+ collectJavaScriptExceptionChildren(value, id, state);
1498
+ }
1499
+ }
1500
+
1501
+ function exceptionMessageEvidence(value) {
1502
+ if (typeof value !== "string") {
1503
+ return { messageState: "not_captured" };
1504
+ }
1505
+ const normalized = value.replace(/\s+/gu, " ").trim();
1506
+ if (normalized === "") {
1507
+ return { messageState: "not_captured" };
1508
+ }
1509
+ return { messageState: "redacted" };
1510
+ }
1511
+
1405
1512
  function errorDetails(error) {
1406
1513
  if (error instanceof Error) {
1514
+ const name = safeProperty(error, "name");
1515
+ const message = safeProperty(error, "message");
1516
+ const stack = safeProperty(error, "stack");
1407
1517
  return {
1408
- name: stringOrUndefined(error.name) ?? "Error",
1409
- message: stringOrUndefined(error.message),
1410
- stack: typeof error.stack === "string" && error.stack.trim() !== "" ? error.stack : undefined
1518
+ name: name.available ? stringOrUndefined(name.value) ?? "Error" : "Error",
1519
+ message: message.available ? stringOrUndefined(message.value) : undefined,
1520
+ stack: stack.available && typeof stack.value === "string" && stack.value.trim() !== ""
1521
+ ? stack.value
1522
+ : undefined
1411
1523
  };
1412
1524
  }
1413
1525
  if (error && typeof error === "object") {
1414
- const name = typeof error.name === "string" && error.name.trim() !== "" ? error.name : "Error";
1415
- const message = typeof error.message === "string" && error.message.trim() !== "" ? error.message : undefined;
1416
- const stack = typeof error.stack === "string" && error.stack.trim() !== "" ? error.stack : undefined;
1417
- return { name, message, stack };
1526
+ const name = safeProperty(error, "name");
1527
+ const message = safeProperty(error, "message");
1528
+ const stack = safeProperty(error, "stack");
1529
+ return {
1530
+ name: name.available && typeof name.value === "string" && name.value.trim() !== ""
1531
+ ? name.value
1532
+ : "Error",
1533
+ message: message.available && typeof message.value === "string" && message.value.trim() !== ""
1534
+ ? message.value
1535
+ : undefined,
1536
+ stack: stack.available && typeof stack.value === "string" && stack.value.trim() !== ""
1537
+ ? stack.value
1538
+ : undefined
1539
+ };
1418
1540
  }
1419
1541
  if (typeof error === "string" && error.trim() !== "") {
1420
1542
  return { name: "Error", message: error };
@@ -1422,6 +1544,14 @@ function errorDetails(error) {
1422
1544
  return { name: "Error" };
1423
1545
  }
1424
1546
 
1547
+ function safeProperty(value, name) {
1548
+ try {
1549
+ return { available: true, value: value[name] };
1550
+ } catch {
1551
+ return { available: false, value: undefined };
1552
+ }
1553
+ }
1554
+
1425
1555
  function boundedIssueExceptionType(value) {
1426
1556
  const normalized = typeof value === "string" ? value.trim() : "";
1427
1557
  const characters = Array.from(normalized);
@@ -1488,12 +1618,18 @@ function collectNestedErrorCauses(parent, state) {
1488
1618
  if (!isObjectLike(parent)) {
1489
1619
  return;
1490
1620
  }
1491
- if ("cause" in parent) {
1492
- collectErrorCause(parent.cause, "cause", state);
1621
+ const cause = safeProperty(parent, "cause");
1622
+ if (!cause.available) {
1623
+ state.truncated = true;
1624
+ } else if (cause.value !== undefined && cause.value !== null) {
1625
+ collectErrorCause(cause.value, "cause", state);
1493
1626
  }
1494
- if (Array.isArray(parent.errors)) {
1627
+ const errors = safeProperty(parent, "errors");
1628
+ if (!errors.available) {
1629
+ state.truncated = true;
1630
+ } else if (Array.isArray(errors.value)) {
1495
1631
  state.sawExceptionGroup = true;
1496
- for (const [index, child] of parent.errors.entries()) {
1632
+ for (const [index, child] of errors.value.entries()) {
1497
1633
  collectErrorCause(child, `errors[${index}]`, state);
1498
1634
  }
1499
1635
  }
@@ -1523,12 +1659,21 @@ function collectErrorCause(value, source, state) {
1523
1659
 
1524
1660
  function errorCauseType(value) {
1525
1661
  if (isObjectLike(value)) {
1526
- const constructorName = safeCauseTypeName(value.constructor?.name);
1662
+ const constructor = safeProperty(value, "constructor");
1663
+ const constructorNameValue = constructor.available && isObjectLike(constructor.value)
1664
+ ? safeProperty(constructor.value, "name")
1665
+ : { available: false, value: undefined };
1666
+ const constructorName = constructorNameValue.available
1667
+ ? safeCauseTypeName(constructorNameValue.value)
1668
+ : undefined;
1527
1669
  if (value instanceof Error) {
1528
1670
  if (constructorName && constructorName !== "Error") {
1529
1671
  return constructorName;
1530
1672
  }
1531
- const builtinName = BUILTIN_ERROR_NAMES.has(value.name) ? value.name : undefined;
1673
+ const name = safeProperty(value, "name");
1674
+ const builtinName = name.available && BUILTIN_ERROR_NAMES.has(name.value)
1675
+ ? name.value
1676
+ : undefined;
1532
1677
  return builtinName ?? constructorName ?? "Error";
1533
1678
  }
1534
1679
  return constructorName ?? "Object";
package/index.d.cts CHANGED
@@ -298,6 +298,40 @@ export type IssueException = {
298
298
  mechanism?: IssueExceptionMechanism;
299
299
  };
300
300
 
301
+ export type IssueExceptionRelationship =
302
+ | "reported"
303
+ | "cause"
304
+ | "context"
305
+ | "aggregate_member"
306
+ | "suppressed";
307
+ export type IssueExceptionMessageState = "captured" | "truncated" | "redacted" | "not_captured";
308
+ export type IssueExceptionStackFramesState = "captured" | "truncated" | "not_captured";
309
+
310
+ /** One parent-first runtime exception with its own message and structured stack state. */
311
+ export type IssueExceptionChainEntry = {
312
+ /** Contiguous zero-based node identity. */
313
+ id: number;
314
+ /** Earlier parent node. Omitted only for the reported root exception. */
315
+ parentId?: number;
316
+ relationship: IssueExceptionRelationship;
317
+ type: string;
318
+ /** Bounded message only when messageState is captured or truncated. */
319
+ message?: string;
320
+ messageState: IssueExceptionMessageState;
321
+ module?: string;
322
+ mechanism?: IssueExceptionMechanism;
323
+ /** This exact exception's bounded structured frames. */
324
+ stackFrames?: IssueStackFrame[];
325
+ stackFramesState: IssueExceptionStackFramesState;
326
+ };
327
+
328
+ /** At most eight parent-first runtime exceptions. */
329
+ export type IssueExceptionChain = {
330
+ entries: IssueExceptionChainEntry[];
331
+ /** True when a cycle or node cap omitted additional exceptions. */
332
+ truncated: boolean;
333
+ };
334
+
301
335
  export type IssueBreadcrumbLevel = "debug" | "info" | "warning" | "error" | "critical";
302
336
  export type IssueBreadcrumbLevelInput = IssueBreadcrumbLevel | "trace" | "log" | "warn" | "fatal";
303
337
  export type IssueBreadcrumbDataValue = string | number | boolean | null;
@@ -329,6 +363,8 @@ export type IssueAttributes = {
329
363
  level: SeverityInput;
330
364
  message?: string;
331
365
  exception?: IssueException;
366
+ /** Parent-first runtime exception evidence; the first node agrees with legacy exception/stackFrames. */
367
+ exceptionChain?: IssueExceptionChain;
332
368
  /** Ordered privacy-bounded generated frames, capped at 32. */
333
369
  stackFrames?: IssueStackFrame[];
334
370
  /** Oldest-to-newest issue history, capped at the most recent 64 entries. */
@@ -845,7 +881,10 @@ export declare function createNetworkMilestoneAttributes(
845
881
  /** Build a local-only, token-free support-ticket create payload draft without calling backend routes. */
846
882
  export declare function createSupportTicketDraft(input: SupportTicketDraftInput): SupportTicketDraft;
847
883
 
848
- /** Convert a JavaScript Error-like value into safe issue attributes with optional source-map Debug ID metadata. */
884
+ /**
885
+ * Convert a JavaScript Error-like value into safe issue attributes with optional source-map Debug ID metadata.
886
+ * Parent-first cause and AggregateError evidence is bounded to the public exception-chain contract.
887
+ */
849
888
  export declare function createIssueAttributesFromError(
850
889
  error: unknown,
851
890
  options?: JavaScriptErrorIssueOptions
package/index.d.ts CHANGED
@@ -298,6 +298,40 @@ export type IssueException = {
298
298
  mechanism?: IssueExceptionMechanism;
299
299
  };
300
300
 
301
+ export type IssueExceptionRelationship =
302
+ | "reported"
303
+ | "cause"
304
+ | "context"
305
+ | "aggregate_member"
306
+ | "suppressed";
307
+ export type IssueExceptionMessageState = "captured" | "truncated" | "redacted" | "not_captured";
308
+ export type IssueExceptionStackFramesState = "captured" | "truncated" | "not_captured";
309
+
310
+ /** One parent-first runtime exception with its own message and structured stack state. */
311
+ export type IssueExceptionChainEntry = {
312
+ /** Contiguous zero-based node identity. */
313
+ id: number;
314
+ /** Earlier parent node. Omitted only for the reported root exception. */
315
+ parentId?: number;
316
+ relationship: IssueExceptionRelationship;
317
+ type: string;
318
+ /** Bounded message only when messageState is captured or truncated. */
319
+ message?: string;
320
+ messageState: IssueExceptionMessageState;
321
+ module?: string;
322
+ mechanism?: IssueExceptionMechanism;
323
+ /** This exact exception's bounded structured frames. */
324
+ stackFrames?: IssueStackFrame[];
325
+ stackFramesState: IssueExceptionStackFramesState;
326
+ };
327
+
328
+ /** At most eight parent-first runtime exceptions. */
329
+ export type IssueExceptionChain = {
330
+ entries: IssueExceptionChainEntry[];
331
+ /** True when a cycle or node cap omitted additional exceptions. */
332
+ truncated: boolean;
333
+ };
334
+
301
335
  export type IssueBreadcrumbLevel = "debug" | "info" | "warning" | "error" | "critical";
302
336
  export type IssueBreadcrumbLevelInput = IssueBreadcrumbLevel | "trace" | "log" | "warn" | "fatal";
303
337
  export type IssueBreadcrumbDataValue = string | number | boolean | null;
@@ -329,6 +363,8 @@ export type IssueAttributes = {
329
363
  level: SeverityInput;
330
364
  message?: string;
331
365
  exception?: IssueException;
366
+ /** Parent-first runtime exception evidence; the first node agrees with legacy exception/stackFrames. */
367
+ exceptionChain?: IssueExceptionChain;
332
368
  /** Ordered privacy-bounded generated frames, capped at 32. */
333
369
  stackFrames?: IssueStackFrame[];
334
370
  /** Oldest-to-newest issue history, capped at the most recent 64 entries. */
@@ -845,7 +881,10 @@ export declare function createNetworkMilestoneAttributes(
845
881
  /** Build a local-only, token-free support-ticket create payload draft without calling backend routes. */
846
882
  export declare function createSupportTicketDraft(input: SupportTicketDraftInput): SupportTicketDraft;
847
883
 
848
- /** Convert a JavaScript Error-like value into safe issue attributes with optional source-map Debug ID metadata. */
884
+ /**
885
+ * Convert a JavaScript Error-like value into safe issue attributes with optional source-map Debug ID metadata.
886
+ * Parent-first cause and AggregateError evidence is bounded to the public exception-chain contract.
887
+ */
849
888
  export declare function createIssueAttributesFromError(
850
889
  error: unknown,
851
890
  options?: JavaScriptErrorIssueOptions
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  const MAX_ISSUE_BREADCRUMBS = 64;
4
+ const MAX_ISSUE_EXCEPTIONS = 8;
4
5
  const MAX_EXCEPTION_TYPE_LENGTH = 256;
6
+ const MAX_EXCEPTION_MESSAGE_LENGTH = 1024;
7
+ const MAX_EXCEPTION_MODULE_LENGTH = 512;
5
8
  const MAX_MECHANISM_TYPE_LENGTH = 64;
6
9
  const MAX_BREADCRUMB_NAME_LENGTH = 64;
7
10
  const MAX_BREADCRUMB_MESSAGE_LENGTH = 512;
@@ -21,7 +24,7 @@ const BREADCRUMB_LEVEL_ALIASES = new Map([
21
24
  ["critical", "critical"]
22
25
  ]);
23
26
 
24
- function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
27
+ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssueStackFrames }) {
25
28
  function validationError(message) {
26
29
  return new SdkError("validation_error", message);
27
30
  }
@@ -75,6 +78,133 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
75
78
  });
76
79
  }
77
80
 
81
+ function validateIssueExceptionChain(chain, legacyException, legacyStackFrames) {
82
+ if (chain === undefined) {
83
+ return undefined;
84
+ }
85
+ requireObject("issue exceptionChain", chain);
86
+ rejectUnknownKeys("issue exceptionChain", chain, new Set(["entries", "truncated"]));
87
+ if (!Array.isArray(chain.entries)
88
+ || chain.entries.length < 1
89
+ || chain.entries.length > MAX_ISSUE_EXCEPTIONS) {
90
+ throw validationError(
91
+ `issue exceptionChain entries must contain 1-${MAX_ISSUE_EXCEPTIONS} exceptions`
92
+ );
93
+ }
94
+ if (typeof chain.truncated !== "boolean") {
95
+ throw validationError("issue exceptionChain truncated must be a boolean");
96
+ }
97
+ const entries = chain.entries.map((entry, index) => validateIssueExceptionChainEntry(entry, index));
98
+ const reportedException = {
99
+ type: entries[0].type,
100
+ ...(entries[0].mechanism === undefined ? {} : { mechanism: entries[0].mechanism })
101
+ };
102
+ if (legacyException === undefined
103
+ || JSON.stringify(reportedException) !== JSON.stringify(legacyException)) {
104
+ throw validationError("issue exceptionChain reported exception must match exception");
105
+ }
106
+ const reportedFrames = entries[0].stackFrames;
107
+ const canonicalLegacyFrames = validateIssueStackFrames(legacyStackFrames);
108
+ if (entries[0].stackFramesState === "not_captured") {
109
+ if (canonicalLegacyFrames !== undefined) {
110
+ throw validationError("issue exceptionChain reported stack must match stackFrames");
111
+ }
112
+ } else if (JSON.stringify(reportedFrames) !== JSON.stringify(canonicalLegacyFrames)) {
113
+ throw validationError("issue exceptionChain reported stack must match stackFrames");
114
+ }
115
+ return {
116
+ entries,
117
+ truncated: chain.truncated
118
+ };
119
+ }
120
+
121
+ function validateIssueExceptionChainEntry(entry, index) {
122
+ requireObject(`issue exceptionChain entry ${index}`, entry);
123
+ rejectUnknownKeys(
124
+ `issue exceptionChain entry ${index}`,
125
+ entry,
126
+ new Set([
127
+ "id",
128
+ "parentId",
129
+ "relationship",
130
+ "type",
131
+ "message",
132
+ "messageState",
133
+ "module",
134
+ "mechanism",
135
+ "stackFrames",
136
+ "stackFramesState"
137
+ ])
138
+ );
139
+ if (!Number.isInteger(entry.id) || entry.id !== index) {
140
+ throw validationError(`issue exceptionChain entry ${index} id must equal its array index`);
141
+ }
142
+ const relationship = entry.relationship;
143
+ const parentId = entry.parentId;
144
+ if (index === 0) {
145
+ if (relationship !== "reported" || parentId !== undefined) {
146
+ throw validationError("issue exceptionChain entry 0 must be the parentless reported exception");
147
+ }
148
+ } else if (!new Set(["cause", "context", "aggregate_member", "suppressed"]).has(relationship)
149
+ || !Number.isInteger(parentId)
150
+ || parentId < 0
151
+ || parentId >= index) {
152
+ throw validationError(`issue exceptionChain entry ${index} parent relationship is invalid`);
153
+ }
154
+ const type = boundedText(
155
+ `issue exceptionChain entry ${index} type`,
156
+ entry.type,
157
+ MAX_EXCEPTION_TYPE_LENGTH,
158
+ { rejectLocationText: true }
159
+ );
160
+ const messageState = entry.messageState;
161
+ if (!new Set(["captured", "truncated", "redacted", "not_captured"]).has(messageState)) {
162
+ throw validationError(`issue exceptionChain entry ${index} messageState is invalid`);
163
+ }
164
+ const message = entry.message === undefined
165
+ ? undefined
166
+ : boundedText(
167
+ `issue exceptionChain entry ${index} message`,
168
+ entry.message,
169
+ MAX_EXCEPTION_MESSAGE_LENGTH
170
+ );
171
+ if ((messageState === "captured" || messageState === "truncated") !== (message !== undefined)) {
172
+ throw validationError(`issue exceptionChain entry ${index} message does not match messageState`);
173
+ }
174
+ const moduleName = entry.module === undefined
175
+ ? undefined
176
+ : boundedText(
177
+ `issue exceptionChain entry ${index} module`,
178
+ entry.module,
179
+ MAX_EXCEPTION_MODULE_LENGTH,
180
+ { rejectLocationText: true }
181
+ );
182
+ const mechanism = validateIssueExceptionMechanism(entry.mechanism);
183
+ const stackFramesState = entry.stackFramesState;
184
+ if (!new Set(["captured", "truncated", "not_captured"]).has(stackFramesState)) {
185
+ throw validationError(`issue exceptionChain entry ${index} stackFramesState is invalid`);
186
+ }
187
+ const stackFrames = validateIssueStackFrames(entry.stackFrames);
188
+ if ((stackFramesState === "captured" || stackFramesState === "truncated")
189
+ !== (stackFrames !== undefined)) {
190
+ throw validationError(
191
+ `issue exceptionChain entry ${index} stackFrames do not match stackFramesState`
192
+ );
193
+ }
194
+ return {
195
+ id: entry.id,
196
+ ...(parentId === undefined ? {} : { parentId }),
197
+ relationship,
198
+ type,
199
+ ...(message === undefined ? {} : { message }),
200
+ messageState,
201
+ ...(moduleName === undefined ? {} : { module: moduleName }),
202
+ ...(mechanism === undefined ? {} : { mechanism }),
203
+ ...(stackFrames === undefined ? {} : { stackFrames }),
204
+ stackFramesState
205
+ };
206
+ }
207
+
78
208
  function validateIssueBreadcrumb(breadcrumb, defaultTimestamp) {
79
209
  requireObject("issue breadcrumb", breadcrumb);
80
210
  rejectUnknownKeys(
@@ -121,6 +251,11 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
121
251
 
122
252
  function validateIssueDiagnostics(attributes) {
123
253
  const exception = validateIssueException(attributes.exception);
254
+ const exceptionChain = validateIssueExceptionChain(
255
+ attributes.exceptionChain,
256
+ exception,
257
+ attributes.stackFrames
258
+ );
124
259
  const breadcrumbs = validateIssueBreadcrumbs(attributes.breadcrumbs);
125
260
  if (
126
261
  attributes.breadcrumbsTruncated !== undefined
@@ -130,6 +265,7 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
130
265
  }
131
266
  return {
132
267
  ...(exception === undefined ? {} : { exception }),
268
+ ...(exceptionChain === undefined ? {} : { exceptionChain }),
133
269
  ...(breadcrumbs === undefined ? {} : { breadcrumbs }),
134
270
  ...(attributes.breadcrumbsTruncated === true ? { breadcrumbsTruncated: true } : {})
135
271
  };
@@ -145,6 +281,20 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
145
281
  : { mechanism: { ...attributes.exception.mechanism } })
146
282
  };
147
283
  }
284
+ if (attributes.exceptionChain !== undefined) {
285
+ diagnostics.exceptionChain = {
286
+ entries: attributes.exceptionChain.entries.map((entry) => ({
287
+ ...entry,
288
+ ...(entry.mechanism === undefined
289
+ ? {}
290
+ : { mechanism: { ...entry.mechanism } }),
291
+ ...(entry.stackFrames === undefined
292
+ ? {}
293
+ : { stackFrames: entry.stackFrames.map((frame) => ({ ...frame })) })
294
+ })),
295
+ truncated: attributes.exceptionChain.truncated
296
+ };
297
+ }
148
298
  if (Array.isArray(attributes.breadcrumbs)) {
149
299
  diagnostics.breadcrumbs = attributes.breadcrumbs.map((breadcrumb) => ({
150
300
  ...breadcrumb,
@@ -257,6 +407,7 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp }) {
257
407
 
258
408
  return {
259
409
  MAX_ISSUE_BREADCRUMBS,
410
+ MAX_ISSUE_EXCEPTIONS,
260
411
  cloneIssueDiagnostics,
261
412
  createIssueException,
262
413
  validateIssueBreadcrumb,
package/issue-stack.cjs CHANGED
@@ -7,22 +7,28 @@ const SAFE_DEBUG_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-
7
7
  const LOCAL_ABSOLUTE_PATH_PATTERN = /^(?:\/(?:Users|home|private|tmp|var|Volumes)\/|[A-Za-z]:[\\/])/u;
8
8
 
9
9
  function buildIssueStackHelpers({ SdkError }) {
10
- function javascriptStackFrames(stack, debugIdMap) {
10
+ function javascriptStackEvidence(stack, debugIdMap) {
11
11
  if (typeof stack !== "string" || stack.trim() === "") {
12
- return [];
12
+ return { frames: [], truncated: false };
13
13
  }
14
14
  const frames = [];
15
+ let truncated = false;
15
16
  for (const rawLine of stack.split(/\r?\n/u)) {
16
17
  const parsed = parseJavaScriptStackFrame(rawLine);
17
18
  if (parsed) {
18
- const debugId = debugIdForFrame(parsed.filename, debugIdMap, SdkError);
19
- frames.push({ ...parsed, ...(debugId ? { debugId } : {}) });
20
19
  if (frames.length === MAX_ISSUE_STACK_FRAMES) {
20
+ truncated = true;
21
21
  break;
22
22
  }
23
+ const debugId = debugIdForFrame(parsed.filename, debugIdMap, SdkError);
24
+ frames.push({ ...parsed, ...(debugId ? { debugId } : {}) });
23
25
  }
24
26
  }
25
- return frames;
27
+ return { frames, truncated };
28
+ }
29
+
30
+ function javascriptStackFrames(stack, debugIdMap) {
31
+ return javascriptStackEvidence(stack, debugIdMap).frames;
26
32
  }
27
33
 
28
34
  function validateIssueStackFrames(stackFrames) {
@@ -84,7 +90,7 @@ function buildIssueStackHelpers({ SdkError }) {
84
90
  });
85
91
  }
86
92
 
87
- return { javascriptStackFrames, validateIssueStackFrames };
93
+ return { javascriptStackEvidence, javascriptStackFrames, validateIssueStackFrames };
88
94
  }
89
95
 
90
96
  function parseJavaScriptStackFrame(rawLine) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logbrew/sdk",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
5
5
  "type": "module",
6
6
  "main": "./index.cjs",