@logbrew/sdk 0.1.9 → 0.1.11
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 +1 -1
- package/core.cjs +166 -21
- package/index.d.cts +40 -1
- package/index.d.ts +40 -1
- package/issue-diagnostics.cjs +152 -1
- package/issue-stack.cjs +12 -6
- package/package.json +5 -1
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 {
|
|
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
|
|
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
|
|
1396
|
-
|
|
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(
|
|
1409
|
-
message: stringOrUndefined(
|
|
1410
|
-
stack: typeof
|
|
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 =
|
|
1415
|
-
const message =
|
|
1416
|
-
const stack =
|
|
1417
|
-
return {
|
|
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
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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/issue-diagnostics.cjs
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./react-native.d.ts",
|
|
16
16
|
"default": "./react-native.js"
|
|
17
17
|
},
|
|
18
|
+
"browser": {
|
|
19
|
+
"types": "./react-native.d.ts",
|
|
20
|
+
"default": "./react-native.js"
|
|
21
|
+
},
|
|
18
22
|
"import": {
|
|
19
23
|
"types": "./index.d.ts",
|
|
20
24
|
"default": "./index.js"
|