@logbrew/sdk 0.1.37 → 0.1.38

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/core.cjs CHANGED
@@ -1,10 +1,48 @@
1
- const { buildCreateSupportTicketDraft } = require("./support-ticket.cjs");
2
- const { buildIssueDiagnosticsHelpers } = require("./issue-diagnostics.cjs");
3
- const { buildIssueStackHelpers } = require("./issue-stack.cjs");
4
- const { buildLogContextHelpers } = require("./log-context.cjs");
1
+ const {
2
+ MAX_ISSUE_BREADCRUMBS,
3
+ cloneIssueDiagnostics,
4
+ createIssueException,
5
+ validateIssueBreadcrumb,
6
+ validateIssueDiagnostics,
7
+ validateIssueEvidence
8
+ } = require("./issue-diagnostics.cjs");
9
+ const { javascriptStackEvidence, validateIssueStackFrames, validateNativeStackFrames } = require("./issue-stack.cjs");
10
+ const {
11
+ compactMetadata,
12
+ isMetadataValue,
13
+ normalizeLogTraceContext,
14
+ traceFromProvider,
15
+ traceMetadataFromLogContext
16
+ } = require("./log-context.cjs");
5
17
  const { buildOpenTelemetryHelpers } = require("./opentelemetry.cjs");
6
- const { buildTelemetryContextHelpers } = require("./telemetry-context.cjs");
7
- const { buildTraceContextHelpers } = require("./trace-context.cjs");
18
+ const {
19
+ cloneTelemetryContext,
20
+ mergeTelemetryContexts,
21
+ validateTelemetryContext
22
+ } = require("./telemetry-context.cjs");
23
+ const { SdkError, retryAfterMsOrUndefined } = require("./sdk-error.cjs");
24
+ const { createSupportTicketDraft } = require("./support-ticket.cjs");
25
+ const { TransportError } = require("./transport-error.cjs");
26
+ const {
27
+ createBaggage,
28
+ createTraceContextHeaders,
29
+ createTraceparent,
30
+ createTraceparentHeaders,
31
+ createTracestate,
32
+ parseBaggage,
33
+ parseTraceparent,
34
+ parseTracestate
35
+ } = require("./trace-context.cjs");
36
+ const {
37
+ requireAllowedValue,
38
+ requireFiniteNumber,
39
+ requireNonEmpty,
40
+ requireNonNegativeInteger,
41
+ requirePositiveInteger,
42
+ requireSpanId,
43
+ requireTimestamp,
44
+ requireTraceId
45
+ } = require("./validation.cjs");
8
46
 
9
47
  const SEVERITY_ALIASES = new Map([
10
48
  ["trace", "info"],
@@ -80,10 +118,6 @@ const PINO_SENSITIVE_CONTEXT_FIELDS = new Set([
80
118
  "uri",
81
119
  "url"
82
120
  ]);
83
- const TRACEPARENT_PATTERN = /^([0-9a-fA-F]{2})-([0-9a-fA-F]{32})-([0-9a-fA-F]{16})-([0-9a-fA-F]{2})$/u;
84
- const ZERO_TRACE_ID = "00000000000000000000000000000000";
85
- const ZERO_SPAN_ID = "0000000000000000";
86
- const RFC3339_TIMESTAMP_PATTERN = /^(\d{4})-(\d{2})-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
87
121
  const DEFAULT_MAX_QUEUE_SIZE = 1000;
88
122
  const DEFAULT_MAX_QUEUE_BYTES = 4 * 1024 * 1024;
89
123
  const DEFAULT_MAX_BATCH_EVENTS = 100;
@@ -129,60 +163,7 @@ const EVENT_VALIDATORS = new Map([
129
163
  ["action", validateAction],
130
164
  ["metric", validateMetric]
131
165
  ]);
132
- class SdkError extends Error {
133
- constructor(code, message, details = {}) {
134
- super(message);
135
- this.name = "SdkError";
136
- this.code = code;
137
- const retryAfterMs = retryAfterMsOrUndefined(details?.retryAfterMs);
138
- if (retryAfterMs !== undefined) {
139
- this.retryAfterMs = retryAfterMs;
140
- }
141
- if (details?.retryable === true) {
142
- this.retryable = true;
143
- }
144
- }
145
- }
146
166
 
147
- const {
148
- compactMetadata,
149
- isMetadataValue,
150
- normalizeLogTraceContext,
151
- traceFromProvider,
152
- traceMetadataFromLogContext
153
- } = buildLogContextHelpers({ SdkError });
154
-
155
- const {
156
- javascriptStackEvidence,
157
- validateIssueStackFrames,
158
- validateNativeStackFrames
159
- } = buildIssueStackHelpers({ SdkError });
160
- const {
161
- MAX_ISSUE_BREADCRUMBS,
162
- cloneIssueDiagnostics,
163
- createIssueException,
164
- validateIssueBreadcrumb,
165
- validateIssueDiagnostics,
166
- validateIssueEvidence
167
- } = buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssueStackFrames });
168
- const {
169
- cloneTelemetryContext,
170
- mergeTelemetryContexts,
171
- validateTelemetryContext
172
- } = buildTelemetryContextHelpers({ SdkError });
173
-
174
- class TransportError extends Error {
175
- constructor(code, message, retryable = false) {
176
- super(message);
177
- this.name = "TransportError";
178
- this.code = code;
179
- this.retryable = retryable;
180
- }
181
-
182
- static network(message) {
183
- return new TransportError("network_failure", message, true);
184
- }
185
- }
186
167
 
187
168
  class RecordingTransport {
188
169
  constructor(scriptedResponses = [{ statusCode: 202 }]) {
@@ -2086,68 +2067,6 @@ function createNetworkMilestoneAttributes(request, options = {}) {
2086
2067
  };
2087
2068
  }
2088
2069
 
2089
- function parseTraceparent(traceparent) {
2090
- if (typeof traceparent !== "string" || traceparent.trim() === "") {
2091
- throw new SdkError("validation_error", "traceparent must be non-empty");
2092
- }
2093
-
2094
- const match = TRACEPARENT_PATTERN.exec(traceparent.trim());
2095
- if (!match) {
2096
- throw new SdkError("validation_error", "traceparent must use W3C version-traceId-parentSpanId-traceFlags format");
2097
- }
2098
-
2099
- const version = match[1].toLowerCase();
2100
- const traceId = match[2].toLowerCase();
2101
- const parentSpanId = match[3].toLowerCase();
2102
- const traceFlags = match[4].toLowerCase();
2103
- if (version === "ff") {
2104
- throw new SdkError("validation_error", "traceparent version ff is not allowed");
2105
- }
2106
- if (traceId === ZERO_TRACE_ID) {
2107
- throw new SdkError("validation_error", "traceparent traceId must not be all zeros");
2108
- }
2109
- if (parentSpanId === ZERO_SPAN_ID) {
2110
- throw new SdkError("validation_error", "traceparent parentSpanId must not be all zeros");
2111
- }
2112
-
2113
- return {
2114
- version,
2115
- traceId,
2116
- parentSpanId,
2117
- traceFlags,
2118
- sampled: (Number.parseInt(traceFlags, 16) & 1) === 1
2119
- };
2120
- }
2121
-
2122
- function createTraceparent({ traceId, spanId, traceFlags = "01" }) {
2123
- requireTraceId(traceId);
2124
- requireSpanId("spanId", spanId);
2125
- requireTraceFlags(traceFlags);
2126
- return `00-${traceId.toLowerCase()}-${spanId.toLowerCase()}-${traceFlags.toLowerCase()}`;
2127
- }
2128
-
2129
- function createTraceparentHeaders(input) {
2130
- return { traceparent: createTraceparent(input) };
2131
- }
2132
-
2133
- const {
2134
- createBaggage,
2135
- createTraceContextHeaders,
2136
- createTracestate,
2137
- parseBaggage,
2138
- parseTracestate
2139
- } = buildTraceContextHelpers({
2140
- SdkError,
2141
- createTraceparent
2142
- });
2143
-
2144
- const createSupportTicketDraft = buildCreateSupportTicketDraft({
2145
- SdkError,
2146
- requireAllowedValue,
2147
- requireNonEmpty,
2148
- requireTraceId
2149
- });
2150
-
2151
2070
  const {
2152
2071
  createLogBrewOpenTelemetrySpanExporter,
2153
2072
  createLogBrewOpenTelemetrySpanProcessor,
@@ -2444,94 +2363,6 @@ function addPinoErrorMetadata(metadata, error, includeErrorStack) {
2444
2363
  }
2445
2364
  }
2446
2365
 
2447
- function requireNonEmpty(label, value) {
2448
- if (typeof value !== "string" || value.trim() === "") {
2449
- throw new SdkError("validation_error", `${label} must be non-empty`);
2450
- }
2451
- }
2452
-
2453
- function requireAllowedValue(label, value, allowedValues) {
2454
- requireNonEmpty(label, value);
2455
- if (!allowedValues.has(value)) {
2456
- throw new SdkError(
2457
- "validation_error",
2458
- `${label} must be one of: ${Array.from(allowedValues).join(", ")}`
2459
- );
2460
- }
2461
- }
2462
-
2463
- function requireFiniteNumber(label, value) {
2464
- if (typeof value !== "number" || !Number.isFinite(value)) {
2465
- throw new SdkError("validation_error", `${label} must be a finite number`);
2466
- }
2467
- }
2468
-
2469
- function requirePositiveInteger(label, value) {
2470
- if (!Number.isSafeInteger(value) || value <= 0) {
2471
- throw new SdkError("validation_error", `${label} must be a positive integer`);
2472
- }
2473
- }
2474
-
2475
- function requireNonNegativeInteger(label, value) {
2476
- if (!Number.isSafeInteger(value) || value < 0) {
2477
- throw new SdkError("validation_error", `${label} must be a non-negative integer`);
2478
- }
2479
- }
2480
-
2481
- function retryAfterMsOrUndefined(value) {
2482
- if (value === undefined) {
2483
- return undefined;
2484
- }
2485
- return Number.isSafeInteger(value) && value >= 0 ? value : undefined;
2486
- }
2487
-
2488
- function requireTraceId(traceId) {
2489
- if (typeof traceId !== "string" || !/^[0-9a-fA-F]{32}$/u.test(traceId)) {
2490
- throw new SdkError("validation_error", "traceId must be 32 lowercase or uppercase hex characters");
2491
- }
2492
- if (traceId.toLowerCase() === ZERO_TRACE_ID) {
2493
- throw new SdkError("validation_error", "traceId must not be all zeros");
2494
- }
2495
- }
2496
-
2497
- function requireSpanId(label, spanId) {
2498
- if (typeof spanId !== "string" || !/^[0-9a-fA-F]{16}$/u.test(spanId)) {
2499
- throw new SdkError("validation_error", `${label} must be 16 lowercase or uppercase hex characters`);
2500
- }
2501
- if (spanId.toLowerCase() === ZERO_SPAN_ID) {
2502
- throw new SdkError("validation_error", `${label} must not be all zeros`);
2503
- }
2504
- }
2505
-
2506
- function requireTraceFlags(traceFlags) {
2507
- if (typeof traceFlags !== "string" || !/^[0-9a-fA-F]{2}$/u.test(traceFlags)) {
2508
- throw new SdkError("validation_error", "traceFlags must be 2 lowercase or uppercase hex characters");
2509
- }
2510
- }
2511
-
2512
- function requireTimestamp(timestamp) {
2513
- requireNonEmpty("timestamp", timestamp);
2514
- const timePortion = timestamp.split("T")[1];
2515
- if (!timestamp.endsWith("Z")
2516
- && !(timePortion && (timePortion.includes("+") || /.+-.+/.test(timePortion)))) {
2517
- throw new SdkError(
2518
- "validation_error",
2519
- `timestamp must include a timezone offset: ${timestamp}`
2520
- );
2521
- }
2522
- const match = timestamp.match(RFC3339_TIMESTAMP_PATTERN);
2523
- const calendar = new Date(0);
2524
- calendar.setUTCHours(0, 0, 0, 0);
2525
- calendar.setUTCFullYear(Number(match?.[1]), Number(match?.[2]) - 1, Number(match?.[3]));
2526
- if (!match
2527
- || Number(match[1]) === 0
2528
- || calendar.getUTCFullYear() !== Number(match[1])
2529
- || calendar.getUTCMonth() !== Number(match[2]) - 1
2530
- || calendar.getUTCDate() !== Number(match[3])) {
2531
- throw new SdkError("validation_error", `invalid timestamp: ${timestamp}`);
2532
- }
2533
- }
2534
-
2535
2366
  function cloneMetadata(metadata) {
2536
2367
  if (metadata === undefined) {
2537
2368
  return undefined;
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { boundedText, optionalText, requireMachineName } = require("./issue-text.cjs");
5
+ const { optionalEntry, rejectUnknownKeys, requireObject, requireTimestamp } = require("./validation.cjs");
6
+
7
+ /**
8
+ * @import { IssueBreadcrumb, IssueBreadcrumbDataValue, IssueBreadcrumbLevel } from "./index.cjs"
9
+ */
10
+
11
+ const MAX_ISSUE_BREADCRUMBS = 64;
12
+ const MIN_ISSUE_BREADCRUMBS = 1;
13
+ const MAX_BREADCRUMB_MESSAGE_LENGTH = 512;
14
+ const MAX_BREADCRUMB_DATA_FIELDS = 8;
15
+ const MAX_BREADCRUMB_DATA_STRING_LENGTH = 256;
16
+ const DATA_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
17
+ const BREADCRUMB_KEYS = new Set(["timestamp", "type", "category", "level", "message", "data"]);
18
+ /** @type {ReadonlyMap<string, IssueBreadcrumbLevel>} */
19
+ const BREADCRUMB_LEVEL_ALIASES = new Map([
20
+ ["trace", "debug"],
21
+ ["debug", "debug"],
22
+ ["info", "info"],
23
+ ["log", "info"],
24
+ ["warn", "warning"],
25
+ ["warning", "warning"],
26
+ ["error", "error"],
27
+ ["fatal", "critical"],
28
+ ["critical", "critical"]
29
+ ]);
30
+
31
+ /**
32
+ * @param {unknown} value
33
+ * @returns {IssueBreadcrumbLevel | undefined}
34
+ */
35
+ function optionalBreadcrumbLevel(value) {
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ const normalized = typeof value === "string" ? BREADCRUMB_LEVEL_ALIASES.get(value) : undefined;
40
+ if (normalized === undefined) {
41
+ throw new SdkError(
42
+ "validation_error",
43
+ `issue breadcrumb level must be one of: ${[...BREADCRUMB_LEVEL_ALIASES.keys()].join(", ")}`
44
+ );
45
+ }
46
+ return normalized;
47
+ }
48
+
49
+ /**
50
+ * @param {string} key
51
+ * @returns {string}
52
+ */
53
+ function requireDataKey(key) {
54
+ if (!DATA_KEY_PATTERN.test(key)) {
55
+ throw new SdkError("validation_error", `issue breadcrumb data key must match ${DATA_KEY_PATTERN}`);
56
+ }
57
+ return key;
58
+ }
59
+
60
+ /**
61
+ * @param {string} key
62
+ * @param {unknown} value
63
+ * @returns {IssueBreadcrumbDataValue}
64
+ */
65
+ function breadcrumbDataValue(key, value) {
66
+ if (typeof value === "string") {
67
+ return boundedText(`issue breadcrumb data value for ${key}`, value, MAX_BREADCRUMB_DATA_STRING_LENGTH);
68
+ }
69
+ if (typeof value === "number") {
70
+ if (!Number.isFinite(value)) {
71
+ throw new SdkError("validation_error", `issue breadcrumb data value for ${key} must be finite`);
72
+ }
73
+ return value;
74
+ }
75
+ if (typeof value === "boolean" || value === null) {
76
+ return value;
77
+ }
78
+ throw new SdkError(
79
+ "validation_error",
80
+ `issue breadcrumb data value for ${key} must be a string, number, boolean, or null`
81
+ );
82
+ }
83
+
84
+ /**
85
+ * @param {unknown} data
86
+ * @returns {Record<string, IssueBreadcrumbDataValue> | undefined}
87
+ */
88
+ function validateBreadcrumbData(data) {
89
+ if (data === undefined) {
90
+ return undefined;
91
+ }
92
+ /** @type {readonly (readonly [string, unknown])[]} */
93
+ const entries = Object.entries(requireObject("issue breadcrumb data", data));
94
+ if (entries.length > MAX_BREADCRUMB_DATA_FIELDS) {
95
+ throw new SdkError(
96
+ "validation_error",
97
+ `issue breadcrumb data must contain at most ${MAX_BREADCRUMB_DATA_FIELDS} fields`
98
+ );
99
+ }
100
+ return Object.fromEntries(entries.map(([key, value]) => [requireDataKey(key), breadcrumbDataValue(key, value)]));
101
+ }
102
+
103
+ /**
104
+ * @param {unknown} breadcrumb
105
+ * @param {unknown} [defaultTimestamp]
106
+ * @returns {IssueBreadcrumb}
107
+ */
108
+ function validateIssueBreadcrumb(breadcrumb, defaultTimestamp) {
109
+ const source = requireObject("issue breadcrumb", breadcrumb);
110
+ rejectUnknownKeys("issue breadcrumb", source, BREADCRUMB_KEYS);
111
+ const timestamp = requireTimestamp(source["timestamp"] ?? defaultTimestamp);
112
+ const category = requireMachineName("issue breadcrumb category", source["category"]);
113
+ const type = source["type"] === undefined ? undefined : requireMachineName("issue breadcrumb type", source["type"]);
114
+ const level = optionalBreadcrumbLevel(source["level"]);
115
+ const message = optionalText("issue breadcrumb message", source["message"], MAX_BREADCRUMB_MESSAGE_LENGTH);
116
+ const data = validateBreadcrumbData(source["data"]);
117
+ return {
118
+ timestamp,
119
+ ...optionalEntry("type", type),
120
+ category,
121
+ ...optionalEntry("level", level),
122
+ ...optionalEntry("message", message),
123
+ ...optionalEntry("data", data)
124
+ };
125
+ }
126
+
127
+ /**
128
+ * @param {unknown} breadcrumbs
129
+ * @returns {IssueBreadcrumb[] | undefined}
130
+ */
131
+ function validateIssueBreadcrumbs(breadcrumbs) {
132
+ if (breadcrumbs === undefined) {
133
+ return undefined;
134
+ }
135
+ if (!Array.isArray(breadcrumbs) || breadcrumbs.length < MIN_ISSUE_BREADCRUMBS || breadcrumbs.length > MAX_ISSUE_BREADCRUMBS) {
136
+ throw new SdkError("validation_error", `issue breadcrumbs must contain 1-${MAX_ISSUE_BREADCRUMBS} entries`);
137
+ }
138
+ /** @type {readonly unknown[]} */
139
+ const list = breadcrumbs;
140
+ return list.map((breadcrumb) => validateIssueBreadcrumb(breadcrumb));
141
+ }
142
+
143
+ module.exports = { MAX_ISSUE_BREADCRUMBS, validateIssueBreadcrumb, validateIssueBreadcrumbs };