@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/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
  };
@@ -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
- if (typeof value !== "string" || value.trim() === "") {
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(value).length > maxLength
380
- || hasControlCharacter(value)
381
- || (rejectLocationText && /[?#]/u.test(value))
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 value;
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.13",
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": "node --test",
90
- "smoke": "node ./smoke.js"
89
+ "test": "bun test",
90
+ "smoke": "bun ./smoke.js"
91
91
  }
92
92
  }