@logbrew/sdk 0.1.13 → 0.1.15
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 +9 -0
- package/core.cjs +21 -10
- package/index.d.cts +1 -999
- package/index.d.ts +36 -0
- package/issue-diagnostics.cjs +182 -7
- package/package.json +3 -3
package/index.d.ts
CHANGED
|
@@ -357,6 +357,38 @@ export type IssueBreadcrumbInput = Omit<IssueBreadcrumb, "timestamp" | "level">
|
|
|
357
357
|
level?: IssueBreadcrumbLevelInput;
|
|
358
358
|
};
|
|
359
359
|
|
|
360
|
+
/** App-reported code location that narrows the smallest likely fix area. */
|
|
361
|
+
export type IssueLikelyFixArea = {
|
|
362
|
+
component?: string;
|
|
363
|
+
module?: string;
|
|
364
|
+
function?: string;
|
|
365
|
+
/** Safe repository-relative source path. */
|
|
366
|
+
file?: string;
|
|
367
|
+
line?: number;
|
|
368
|
+
column?: number;
|
|
369
|
+
inApp?: boolean;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** App-reported user impact without user identities or raw request data. */
|
|
373
|
+
export type IssueImpactEvidence = {
|
|
374
|
+
affectedUserSegment?: string;
|
|
375
|
+
failedAction?: string;
|
|
376
|
+
userVisibleOutcome?: string;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/** Explicit diagnostic evidence for cause, fix area, impact, and capture limitations. */
|
|
380
|
+
export type IssueDiagnosticEvidence = {
|
|
381
|
+
/** App-owned hypothesis. LogBrew presents it as reported, never proven. */
|
|
382
|
+
likelyRootCause?: string;
|
|
383
|
+
likelyFixArea?: IssueLikelyFixArea;
|
|
384
|
+
impact?: IssueImpactEvidence;
|
|
385
|
+
/** Unique bounded field names whose values were captured. */
|
|
386
|
+
capturedFields?: string[];
|
|
387
|
+
missingFields?: string[];
|
|
388
|
+
redactedFields?: string[];
|
|
389
|
+
truncatedFields?: string[];
|
|
390
|
+
};
|
|
391
|
+
|
|
360
392
|
/** Public issue event attributes. */
|
|
361
393
|
export type IssueAttributes = {
|
|
362
394
|
title: string;
|
|
@@ -371,6 +403,8 @@ export type IssueAttributes = {
|
|
|
371
403
|
breadcrumbs?: IssueBreadcrumb[];
|
|
372
404
|
/** True when older or invalid history was omitted before capture. */
|
|
373
405
|
breadcrumbsTruncated?: boolean;
|
|
406
|
+
/** App-reported diagnostic evidence, validated and labeled separately from observed facts. */
|
|
407
|
+
evidence?: IssueDiagnosticEvidence;
|
|
374
408
|
metadata?: Metadata;
|
|
375
409
|
context?: TelemetryContext;
|
|
376
410
|
};
|
|
@@ -403,6 +437,8 @@ export type JavaScriptErrorIssueOptions = {
|
|
|
403
437
|
debugIdMap?: Record<string, string>;
|
|
404
438
|
/** Optional stable app-owned grouping fingerprint. Keep it safe and low-cardinality. */
|
|
405
439
|
fingerprint?: string;
|
|
440
|
+
/** App-reported cause, fix-area, impact, and explicit evidence-state receipt. */
|
|
441
|
+
evidence?: IssueDiagnosticEvidence;
|
|
406
442
|
/** Include raw stack text only when the app has explicitly approved it. Defaults to false. */
|
|
407
443
|
includeErrorStack?: boolean;
|
|
408
444
|
};
|
package/issue-diagnostics.cjs
CHANGED
|
@@ -10,8 +10,13 @@ const MAX_BREADCRUMB_NAME_LENGTH = 64;
|
|
|
10
10
|
const MAX_BREADCRUMB_MESSAGE_LENGTH = 512;
|
|
11
11
|
const MAX_BREADCRUMB_DATA_FIELDS = 8;
|
|
12
12
|
const MAX_BREADCRUMB_DATA_STRING_LENGTH = 256;
|
|
13
|
+
const MAX_DIAGNOSTIC_IDENTITY_LENGTH = 256;
|
|
14
|
+
const MAX_DIAGNOSTIC_CAUSE_LENGTH = 1024;
|
|
15
|
+
const MAX_DIAGNOSTIC_OUTCOME_LENGTH = 512;
|
|
16
|
+
const MAX_DIAGNOSTIC_FIELDS = 32;
|
|
13
17
|
const MACHINE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/u;
|
|
14
18
|
const DATA_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
|
|
19
|
+
const EVIDENCE_FIELD_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u;
|
|
15
20
|
const BREADCRUMB_LEVEL_ALIASES = new Map([
|
|
16
21
|
["trace", "debug"],
|
|
17
22
|
["debug", "debug"],
|
|
@@ -257,6 +262,7 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssu
|
|
|
257
262
|
attributes.stackFrames
|
|
258
263
|
);
|
|
259
264
|
const breadcrumbs = validateIssueBreadcrumbs(attributes.breadcrumbs);
|
|
265
|
+
const evidence = validateIssueEvidence(attributes.evidence);
|
|
260
266
|
if (
|
|
261
267
|
attributes.breadcrumbsTruncated !== undefined
|
|
262
268
|
&& typeof attributes.breadcrumbsTruncated !== "boolean"
|
|
@@ -267,7 +273,8 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssu
|
|
|
267
273
|
...(exception === undefined ? {} : { exception }),
|
|
268
274
|
...(exceptionChain === undefined ? {} : { exceptionChain }),
|
|
269
275
|
...(breadcrumbs === undefined ? {} : { breadcrumbs }),
|
|
270
|
-
...(attributes.breadcrumbsTruncated === true ? { breadcrumbsTruncated: true } : {})
|
|
276
|
+
...(attributes.breadcrumbsTruncated === true ? { breadcrumbsTruncated: true } : {}),
|
|
277
|
+
...(evidence === undefined ? {} : { evidence })
|
|
271
278
|
};
|
|
272
279
|
}
|
|
273
280
|
|
|
@@ -304,9 +311,175 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssu
|
|
|
304
311
|
if (attributes.breadcrumbsTruncated === true) {
|
|
305
312
|
diagnostics.breadcrumbsTruncated = true;
|
|
306
313
|
}
|
|
314
|
+
if (attributes.evidence !== undefined) {
|
|
315
|
+
diagnostics.evidence = cloneIssueEvidence(attributes.evidence);
|
|
316
|
+
}
|
|
307
317
|
return diagnostics;
|
|
308
318
|
}
|
|
309
319
|
|
|
320
|
+
function validateIssueEvidence(evidence) {
|
|
321
|
+
if (evidence === undefined) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
requireObject("issue evidence", evidence);
|
|
325
|
+
const fieldKeys = ["capturedFields", "missingFields", "redactedFields", "truncatedFields"];
|
|
326
|
+
rejectUnknownKeys(
|
|
327
|
+
"issue evidence",
|
|
328
|
+
evidence,
|
|
329
|
+
new Set(["likelyRootCause", "likelyFixArea", "impact", ...fieldKeys])
|
|
330
|
+
);
|
|
331
|
+
const likelyRootCause = evidence.likelyRootCause === undefined
|
|
332
|
+
? undefined
|
|
333
|
+
: boundedText(
|
|
334
|
+
"issue evidence likelyRootCause",
|
|
335
|
+
evidence.likelyRootCause,
|
|
336
|
+
MAX_DIAGNOSTIC_CAUSE_LENGTH
|
|
337
|
+
).trim();
|
|
338
|
+
const likelyFixArea = validateLikelyFixArea(evidence.likelyFixArea);
|
|
339
|
+
const impact = validateImpactEvidence(evidence.impact);
|
|
340
|
+
const fieldLists = Object.fromEntries(
|
|
341
|
+
fieldKeys.map((key) => [key, validateEvidenceFields(key, evidence[key])])
|
|
342
|
+
);
|
|
343
|
+
const present = new Set();
|
|
344
|
+
for (const key of fieldKeys) {
|
|
345
|
+
for (const field of fieldLists[key] ?? []) {
|
|
346
|
+
if (present.has(field)) {
|
|
347
|
+
throw validationError(`issue evidence field ${field} has conflicting states`);
|
|
348
|
+
}
|
|
349
|
+
present.add(field);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const validated = {
|
|
353
|
+
...(likelyRootCause === undefined ? {} : { likelyRootCause }),
|
|
354
|
+
...(likelyFixArea === undefined ? {} : { likelyFixArea }),
|
|
355
|
+
...(impact === undefined ? {} : { impact }),
|
|
356
|
+
...Object.fromEntries(fieldKeys.flatMap((key) => fieldLists[key] === undefined ? [] : [[key, fieldLists[key]]]))
|
|
357
|
+
};
|
|
358
|
+
if (Object.keys(validated).length === 0) {
|
|
359
|
+
throw validationError("issue evidence must contain at least one field");
|
|
360
|
+
}
|
|
361
|
+
return validated;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function validateLikelyFixArea(area) {
|
|
365
|
+
if (area === undefined) {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
requireObject("issue evidence likelyFixArea", area);
|
|
369
|
+
rejectUnknownKeys(
|
|
370
|
+
"issue evidence likelyFixArea",
|
|
371
|
+
area,
|
|
372
|
+
new Set(["component", "module", "function", "file", "line", "column", "inApp"])
|
|
373
|
+
);
|
|
374
|
+
const validated = {};
|
|
375
|
+
for (const key of ["component", "module", "function"]) {
|
|
376
|
+
if (area[key] !== undefined) {
|
|
377
|
+
validated[key] = boundedText(
|
|
378
|
+
`issue evidence likelyFixArea ${key}`,
|
|
379
|
+
area[key],
|
|
380
|
+
MAX_DIAGNOSTIC_IDENTITY_LENGTH,
|
|
381
|
+
{ rejectLocationText: true }
|
|
382
|
+
).trim();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (area.file !== undefined) {
|
|
386
|
+
validated.file = safeRelativeSourcePath(area.file);
|
|
387
|
+
}
|
|
388
|
+
for (const key of ["line", "column"]) {
|
|
389
|
+
if (area[key] !== undefined) {
|
|
390
|
+
if (!Number.isInteger(area[key]) || area[key] < 1 || area[key] > 2147483647) {
|
|
391
|
+
throw validationError(`issue evidence likelyFixArea ${key} must be a positive integer`);
|
|
392
|
+
}
|
|
393
|
+
validated[key] = area[key];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (area.inApp !== undefined) {
|
|
397
|
+
if (typeof area.inApp !== "boolean") {
|
|
398
|
+
throw validationError("issue evidence likelyFixArea inApp must be a boolean");
|
|
399
|
+
}
|
|
400
|
+
validated.inApp = area.inApp;
|
|
401
|
+
}
|
|
402
|
+
if (!Object.keys(validated).some((key) => key !== "inApp")) {
|
|
403
|
+
throw validationError("issue evidence likelyFixArea must identify a code location");
|
|
404
|
+
}
|
|
405
|
+
return validated;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validateImpactEvidence(impact) {
|
|
409
|
+
if (impact === undefined) {
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
requireObject("issue evidence impact", impact);
|
|
413
|
+
rejectUnknownKeys(
|
|
414
|
+
"issue evidence impact",
|
|
415
|
+
impact,
|
|
416
|
+
new Set(["affectedUserSegment", "failedAction", "userVisibleOutcome"])
|
|
417
|
+
);
|
|
418
|
+
const validated = {};
|
|
419
|
+
for (const key of ["affectedUserSegment", "failedAction"]) {
|
|
420
|
+
if (impact[key] !== undefined) {
|
|
421
|
+
validated[key] = boundedText(
|
|
422
|
+
`issue evidence impact ${key}`,
|
|
423
|
+
impact[key],
|
|
424
|
+
MAX_DIAGNOSTIC_IDENTITY_LENGTH,
|
|
425
|
+
{ rejectLocationText: true }
|
|
426
|
+
).trim();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
if (impact.userVisibleOutcome !== undefined) {
|
|
430
|
+
validated.userVisibleOutcome = boundedText(
|
|
431
|
+
"issue evidence impact userVisibleOutcome",
|
|
432
|
+
impact.userVisibleOutcome,
|
|
433
|
+
MAX_DIAGNOSTIC_OUTCOME_LENGTH
|
|
434
|
+
).trim();
|
|
435
|
+
}
|
|
436
|
+
if (Object.keys(validated).length === 0) {
|
|
437
|
+
throw validationError("issue evidence impact must contain at least one field");
|
|
438
|
+
}
|
|
439
|
+
return validated;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function validateEvidenceFields(key, fields) {
|
|
443
|
+
if (fields === undefined) {
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
if (!Array.isArray(fields) || fields.length < 1 || fields.length > MAX_DIAGNOSTIC_FIELDS) {
|
|
447
|
+
throw validationError(`issue evidence ${key} must contain 1-${MAX_DIAGNOSTIC_FIELDS} fields`);
|
|
448
|
+
}
|
|
449
|
+
const unique = new Set(fields);
|
|
450
|
+
if (unique.size !== fields.length || fields.some((field) => typeof field !== "string" || !EVIDENCE_FIELD_PATTERN.test(field))) {
|
|
451
|
+
throw validationError(`issue evidence ${key} fields must be unique bounded identifiers`);
|
|
452
|
+
}
|
|
453
|
+
return [...fields];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function safeRelativeSourcePath(value) {
|
|
457
|
+
const path = boundedText(
|
|
458
|
+
"issue evidence likelyFixArea file",
|
|
459
|
+
value,
|
|
460
|
+
MAX_DIAGNOSTIC_IDENTITY_LENGTH,
|
|
461
|
+
{ rejectLocationText: true }
|
|
462
|
+
).trim().replaceAll("\\", "/");
|
|
463
|
+
const parts = path.split("/");
|
|
464
|
+
if (path.startsWith("/") || /^[A-Za-z]:\//u.test(path) || path.includes("://")
|
|
465
|
+
|| parts.some((part) => part === "" || part === "." || part === "..")) {
|
|
466
|
+
throw validationError("issue evidence likelyFixArea file must be a safe relative path");
|
|
467
|
+
}
|
|
468
|
+
return path;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function cloneIssueEvidence(evidence) {
|
|
472
|
+
return {
|
|
473
|
+
...evidence,
|
|
474
|
+
...(evidence.likelyFixArea === undefined ? {} : { likelyFixArea: { ...evidence.likelyFixArea } }),
|
|
475
|
+
...(evidence.impact === undefined ? {} : { impact: { ...evidence.impact } }),
|
|
476
|
+
...Object.fromEntries(
|
|
477
|
+
["capturedFields", "missingFields", "redactedFields", "truncatedFields"]
|
|
478
|
+
.flatMap((key) => evidence[key] === undefined ? [] : [[key, [...evidence[key]]]])
|
|
479
|
+
)
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
310
483
|
function validateBreadcrumbData(data) {
|
|
311
484
|
if (data === undefined) {
|
|
312
485
|
return undefined;
|
|
@@ -372,17 +545,18 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssu
|
|
|
372
545
|
}
|
|
373
546
|
|
|
374
547
|
function boundedText(label, value, maxLength, { rejectLocationText = false } = {}) {
|
|
375
|
-
|
|
548
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
549
|
+
if (normalized === "") {
|
|
376
550
|
throw validationError(`${label} must be non-empty`);
|
|
377
551
|
}
|
|
378
552
|
if (
|
|
379
|
-
Array.from(
|
|
380
|
-
|| hasControlCharacter(
|
|
381
|
-
|| (rejectLocationText && /[?#]/u.test(
|
|
553
|
+
Array.from(normalized).length > maxLength
|
|
554
|
+
|| hasControlCharacter(normalized)
|
|
555
|
+
|| (rejectLocationText && /[?#]/u.test(normalized))
|
|
382
556
|
) {
|
|
383
557
|
throw validationError(`${label} is invalid or exceeds ${maxLength} characters`);
|
|
384
558
|
}
|
|
385
|
-
return
|
|
559
|
+
return normalized;
|
|
386
560
|
}
|
|
387
561
|
|
|
388
562
|
function requireObject(label, value) {
|
|
@@ -411,7 +585,8 @@ function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssu
|
|
|
411
585
|
cloneIssueDiagnostics,
|
|
412
586
|
createIssueException,
|
|
413
587
|
validateIssueBreadcrumb,
|
|
414
|
-
validateIssueDiagnostics
|
|
588
|
+
validateIssueDiagnostics,
|
|
589
|
+
validateIssueEvidence
|
|
415
590
|
};
|
|
416
591
|
}
|
|
417
592
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logbrew/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Public LogBrew JavaScript SDK for building, validating, and flushing event batches.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"url": "git+https://github.com/LogBrewCo/sdk.git"
|
|
87
87
|
},
|
|
88
88
|
"scripts": {
|
|
89
|
-
"test": "
|
|
90
|
-
"smoke": "
|
|
89
|
+
"test": "bun test",
|
|
90
|
+
"smoke": "bun ./smoke.js"
|
|
91
91
|
}
|
|
92
92
|
}
|