@logbrew/sdk 0.1.36 → 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.
@@ -1,593 +1,105 @@
1
1
  "use strict";
2
2
 
3
- const MAX_ISSUE_BREADCRUMBS = 64;
4
- const MAX_ISSUE_EXCEPTIONS = 8;
5
- const MAX_EXCEPTION_TYPE_LENGTH = 256;
6
- const MAX_EXCEPTION_MESSAGE_LENGTH = 1024;
7
- const MAX_EXCEPTION_MODULE_LENGTH = 512;
8
- const MAX_MECHANISM_TYPE_LENGTH = 64;
9
- const MAX_BREADCRUMB_NAME_LENGTH = 64;
10
- const MAX_BREADCRUMB_MESSAGE_LENGTH = 512;
11
- const MAX_BREADCRUMB_DATA_FIELDS = 8;
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;
17
- const MACHINE_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/u;
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;
20
- const BREADCRUMB_LEVEL_ALIASES = new Map([
21
- ["trace", "debug"],
22
- ["debug", "debug"],
23
- ["info", "info"],
24
- ["log", "info"],
25
- ["warn", "warning"],
26
- ["warning", "warning"],
27
- ["error", "error"],
28
- ["fatal", "critical"],
29
- ["critical", "critical"]
30
- ]);
31
-
32
- function buildIssueDiagnosticsHelpers({ SdkError, requireTimestamp, validateIssueStackFrames }) {
33
- function validationError(message) {
34
- return new SdkError("validation_error", message);
35
- }
36
-
37
- function validateIssueException(exception) {
38
- if (exception === undefined) {
39
- return undefined;
40
- }
41
- requireObject("issue exception", exception);
42
- rejectUnknownKeys("issue exception", exception, new Set(["type", "mechanism"]));
43
- const type = boundedText("issue exception type", exception.type, MAX_EXCEPTION_TYPE_LENGTH, {
44
- rejectLocationText: true
45
- });
46
- const mechanism = validateIssueExceptionMechanism(exception.mechanism);
47
- return {
48
- type,
49
- ...(mechanism === undefined ? {} : { mechanism })
50
- };
51
- }
52
-
53
- function validateIssueExceptionMechanism(mechanism) {
54
- if (mechanism === undefined) {
55
- return undefined;
56
- }
57
- requireObject("issue exception mechanism", mechanism);
58
- rejectUnknownKeys(
59
- "issue exception mechanism",
60
- mechanism,
61
- new Set(["type", "handled"])
62
- );
63
- if (typeof mechanism.type !== "string" || !MACHINE_NAME_PATTERN.test(mechanism.type)) {
64
- throw validationError(
65
- `issue exception mechanism type must match ${MACHINE_NAME_PATTERN}`
66
- );
67
- }
68
- if (Array.from(mechanism.type).length > MAX_MECHANISM_TYPE_LENGTH) {
69
- throw validationError(
70
- `issue exception mechanism type must be at most ${MAX_MECHANISM_TYPE_LENGTH} characters`
71
- );
72
- }
73
- if (typeof mechanism.handled !== "boolean") {
74
- throw validationError("issue exception mechanism handled must be a boolean");
75
- }
76
- return { type: mechanism.type, handled: mechanism.handled };
77
- }
78
-
79
- function createIssueException(type, mechanism, handled) {
80
- return validateIssueException({
81
- type,
82
- mechanism: { type: mechanism, handled }
83
- });
84
- }
85
-
86
- function validateIssueExceptionChain(chain, legacyException, legacyStackFrames) {
87
- if (chain === undefined) {
88
- return undefined;
89
- }
90
- requireObject("issue exceptionChain", chain);
91
- rejectUnknownKeys("issue exceptionChain", chain, new Set(["entries", "truncated"]));
92
- if (!Array.isArray(chain.entries)
93
- || chain.entries.length < 1
94
- || chain.entries.length > MAX_ISSUE_EXCEPTIONS) {
95
- throw validationError(
96
- `issue exceptionChain entries must contain 1-${MAX_ISSUE_EXCEPTIONS} exceptions`
97
- );
98
- }
99
- if (typeof chain.truncated !== "boolean") {
100
- throw validationError("issue exceptionChain truncated must be a boolean");
101
- }
102
- const entries = chain.entries.map((entry, index) => validateIssueExceptionChainEntry(entry, index));
103
- const reportedException = {
104
- type: entries[0].type,
105
- ...(entries[0].mechanism === undefined ? {} : { mechanism: entries[0].mechanism })
106
- };
107
- if (legacyException === undefined
108
- || JSON.stringify(reportedException) !== JSON.stringify(legacyException)) {
109
- throw validationError("issue exceptionChain reported exception must match exception");
110
- }
111
- const reportedFrames = entries[0].stackFrames;
112
- const canonicalLegacyFrames = validateIssueStackFrames(legacyStackFrames);
113
- if (entries[0].stackFramesState === "not_captured") {
114
- if (canonicalLegacyFrames !== undefined) {
115
- throw validationError("issue exceptionChain reported stack must match stackFrames");
116
- }
117
- } else if (JSON.stringify(reportedFrames) !== JSON.stringify(canonicalLegacyFrames)) {
118
- throw validationError("issue exceptionChain reported stack must match stackFrames");
119
- }
120
- return {
121
- entries,
122
- truncated: chain.truncated
123
- };
124
- }
125
-
126
- function validateIssueExceptionChainEntry(entry, index) {
127
- requireObject(`issue exceptionChain entry ${index}`, entry);
128
- rejectUnknownKeys(
129
- `issue exceptionChain entry ${index}`,
130
- entry,
131
- new Set([
132
- "id",
133
- "parentId",
134
- "relationship",
135
- "type",
136
- "message",
137
- "messageState",
138
- "module",
139
- "mechanism",
140
- "stackFrames",
141
- "stackFramesState"
142
- ])
143
- );
144
- if (!Number.isInteger(entry.id) || entry.id !== index) {
145
- throw validationError(`issue exceptionChain entry ${index} id must equal its array index`);
146
- }
147
- const relationship = entry.relationship;
148
- const parentId = entry.parentId;
149
- if (index === 0) {
150
- if (relationship !== "reported" || parentId !== undefined) {
151
- throw validationError("issue exceptionChain entry 0 must be the parentless reported exception");
152
- }
153
- } else if (!new Set(["cause", "context", "aggregate_member", "suppressed"]).has(relationship)
154
- || !Number.isInteger(parentId)
155
- || parentId < 0
156
- || parentId >= index) {
157
- throw validationError(`issue exceptionChain entry ${index} parent relationship is invalid`);
158
- }
159
- const type = boundedText(
160
- `issue exceptionChain entry ${index} type`,
161
- entry.type,
162
- MAX_EXCEPTION_TYPE_LENGTH,
163
- { rejectLocationText: true }
164
- );
165
- const messageState = entry.messageState;
166
- if (!new Set(["captured", "truncated", "redacted", "not_captured"]).has(messageState)) {
167
- throw validationError(`issue exceptionChain entry ${index} messageState is invalid`);
168
- }
169
- const message = entry.message === undefined
170
- ? undefined
171
- : boundedText(
172
- `issue exceptionChain entry ${index} message`,
173
- entry.message,
174
- MAX_EXCEPTION_MESSAGE_LENGTH
175
- );
176
- if ((messageState === "captured" || messageState === "truncated") !== (message !== undefined)) {
177
- throw validationError(`issue exceptionChain entry ${index} message does not match messageState`);
178
- }
179
- const moduleName = entry.module === undefined
180
- ? undefined
181
- : boundedText(
182
- `issue exceptionChain entry ${index} module`,
183
- entry.module,
184
- MAX_EXCEPTION_MODULE_LENGTH,
185
- { rejectLocationText: true }
186
- );
187
- const mechanism = validateIssueExceptionMechanism(entry.mechanism);
188
- const stackFramesState = entry.stackFramesState;
189
- if (!new Set(["captured", "truncated", "not_captured"]).has(stackFramesState)) {
190
- throw validationError(`issue exceptionChain entry ${index} stackFramesState is invalid`);
191
- }
192
- const stackFrames = validateIssueStackFrames(entry.stackFrames);
193
- if ((stackFramesState === "captured" || stackFramesState === "truncated")
194
- !== (stackFrames !== undefined)) {
195
- throw validationError(
196
- `issue exceptionChain entry ${index} stackFrames do not match stackFramesState`
197
- );
198
- }
199
- return {
200
- id: entry.id,
201
- ...(parentId === undefined ? {} : { parentId }),
202
- relationship,
203
- type,
204
- ...(message === undefined ? {} : { message }),
205
- messageState,
206
- ...(moduleName === undefined ? {} : { module: moduleName }),
207
- ...(mechanism === undefined ? {} : { mechanism }),
208
- ...(stackFrames === undefined ? {} : { stackFrames }),
209
- stackFramesState
210
- };
211
- }
212
-
213
- function validateIssueBreadcrumb(breadcrumb, defaultTimestamp) {
214
- requireObject("issue breadcrumb", breadcrumb);
215
- rejectUnknownKeys(
216
- "issue breadcrumb",
217
- breadcrumb,
218
- new Set(["timestamp", "type", "category", "level", "message", "data"])
219
- );
220
- const timestamp = breadcrumb.timestamp ?? defaultTimestamp;
221
- requireTimestamp(timestamp);
222
- if (typeof breadcrumb.category !== "string" || !MACHINE_NAME_PATTERN.test(breadcrumb.category)) {
223
- throw validationError(`issue breadcrumb category must match ${MACHINE_NAME_PATTERN}`);
224
- }
225
- const type = optionalMachineName("issue breadcrumb type", breadcrumb.type);
226
- const level = optionalBreadcrumbLevel(breadcrumb.level);
227
- const message = breadcrumb.message === undefined
228
- ? undefined
229
- : boundedText(
230
- "issue breadcrumb message",
231
- breadcrumb.message,
232
- MAX_BREADCRUMB_MESSAGE_LENGTH
233
- );
234
- const data = validateBreadcrumbData(breadcrumb.data);
235
- return {
236
- timestamp,
237
- ...(type === undefined ? {} : { type }),
238
- category: breadcrumb.category,
239
- ...(level === undefined ? {} : { level }),
240
- ...(message === undefined ? {} : { message }),
241
- ...(data === undefined ? {} : { data })
242
- };
243
- }
244
-
245
- function validateIssueBreadcrumbs(breadcrumbs) {
246
- if (breadcrumbs === undefined) {
247
- return undefined;
248
- }
249
- if (!Array.isArray(breadcrumbs) || breadcrumbs.length < 1 || breadcrumbs.length > MAX_ISSUE_BREADCRUMBS) {
250
- throw validationError(
251
- `issue breadcrumbs must contain 1-${MAX_ISSUE_BREADCRUMBS} entries`
252
- );
253
- }
254
- return breadcrumbs.map((breadcrumb) => validateIssueBreadcrumb(breadcrumb));
255
- }
256
-
257
- function validateIssueDiagnostics(attributes) {
258
- const exception = validateIssueException(attributes.exception);
259
- const exceptionChain = validateIssueExceptionChain(
260
- attributes.exceptionChain,
261
- exception,
262
- attributes.stackFrames
263
- );
264
- const breadcrumbs = validateIssueBreadcrumbs(attributes.breadcrumbs);
265
- const evidence = validateIssueEvidence(attributes.evidence);
266
- if (
267
- attributes.breadcrumbsTruncated !== undefined
268
- && typeof attributes.breadcrumbsTruncated !== "boolean"
269
- ) {
270
- throw validationError("issue breadcrumbsTruncated must be a boolean");
271
- }
272
- return {
273
- ...(exception === undefined ? {} : { exception }),
274
- ...(exceptionChain === undefined ? {} : { exceptionChain }),
275
- ...(breadcrumbs === undefined ? {} : { breadcrumbs }),
276
- ...(attributes.breadcrumbsTruncated === true ? { breadcrumbsTruncated: true } : {}),
277
- ...(evidence === undefined ? {} : { evidence })
278
- };
279
- }
280
-
281
- function cloneIssueDiagnostics(attributes) {
282
- const diagnostics = {};
283
- if (attributes.exception !== undefined) {
284
- diagnostics.exception = {
285
- ...attributes.exception,
286
- ...(attributes.exception.mechanism === undefined
287
- ? {}
288
- : { mechanism: { ...attributes.exception.mechanism } })
289
- };
290
- }
291
- if (attributes.exceptionChain !== undefined) {
292
- diagnostics.exceptionChain = {
293
- entries: attributes.exceptionChain.entries.map((entry) => ({
294
- ...entry,
295
- ...(entry.mechanism === undefined
296
- ? {}
297
- : { mechanism: { ...entry.mechanism } }),
298
- ...(entry.stackFrames === undefined
299
- ? {}
300
- : { stackFrames: entry.stackFrames.map((frame) => ({ ...frame })) })
301
- })),
302
- truncated: attributes.exceptionChain.truncated
303
- };
304
- }
305
- if (Array.isArray(attributes.breadcrumbs)) {
306
- diagnostics.breadcrumbs = attributes.breadcrumbs.map((breadcrumb) => ({
307
- ...breadcrumb,
308
- ...(breadcrumb.data === undefined ? {} : { data: { ...breadcrumb.data } })
309
- }));
310
- }
311
- if (attributes.breadcrumbsTruncated === true) {
312
- diagnostics.breadcrumbsTruncated = true;
313
- }
314
- if (attributes.evidence !== undefined) {
315
- diagnostics.evidence = cloneIssueEvidence(attributes.evidence);
316
- }
317
- return diagnostics;
318
- }
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
-
483
- function validateBreadcrumbData(data) {
484
- if (data === undefined) {
485
- return undefined;
486
- }
487
- requireObject("issue breadcrumb data", data);
488
- const entries = Object.entries(data);
489
- if (entries.length > MAX_BREADCRUMB_DATA_FIELDS) {
490
- throw validationError(
491
- `issue breadcrumb data must contain at most ${MAX_BREADCRUMB_DATA_FIELDS} fields`
492
- );
493
- }
494
- const validated = {};
495
- for (const [key, value] of entries) {
496
- if (!DATA_KEY_PATTERN.test(key)) {
497
- throw validationError(`issue breadcrumb data key must match ${DATA_KEY_PATTERN}`);
498
- }
499
- if (typeof value === "string") {
500
- validated[key] = boundedText(
501
- `issue breadcrumb data value for ${key}`,
502
- value,
503
- MAX_BREADCRUMB_DATA_STRING_LENGTH
504
- );
505
- } else if (typeof value === "number") {
506
- if (!Number.isFinite(value)) {
507
- throw validationError(`issue breadcrumb data value for ${key} must be finite`);
508
- }
509
- validated[key] = value;
510
- } else if (typeof value === "boolean" || value === null) {
511
- validated[key] = value;
512
- } else {
513
- throw validationError(
514
- `issue breadcrumb data value for ${key} must be a string, number, boolean, or null`
515
- );
516
- }
517
- }
518
- return validated;
519
- }
520
-
521
- function optionalMachineName(label, value) {
522
- if (value === undefined) {
523
- return undefined;
524
- }
525
- if (typeof value !== "string" || !MACHINE_NAME_PATTERN.test(value)) {
526
- throw validationError(`${label} must match ${MACHINE_NAME_PATTERN}`);
527
- }
528
- if (Array.from(value).length > MAX_BREADCRUMB_NAME_LENGTH) {
529
- throw validationError(`${label} must be at most ${MAX_BREADCRUMB_NAME_LENGTH} characters`);
530
- }
531
- return value;
532
- }
533
-
534
- function optionalBreadcrumbLevel(value) {
535
- if (value === undefined) {
536
- return undefined;
537
- }
538
- const normalized = typeof value === "string" ? BREADCRUMB_LEVEL_ALIASES.get(value) : undefined;
539
- if (normalized === undefined) {
540
- throw validationError(
541
- `issue breadcrumb level must be one of: ${Array.from(BREADCRUMB_LEVEL_ALIASES.keys()).join(", ")}`
542
- );
543
- }
544
- return normalized;
545
- }
3
+ const { SdkError } = require("./sdk-error.cjs");
4
+ const { MAX_ISSUE_BREADCRUMBS, validateIssueBreadcrumb, validateIssueBreadcrumbs } = require("./issue-breadcrumbs.cjs");
5
+ const { cloneIssueEvidence, validateIssueEvidence } = require("./issue-evidence.cjs");
6
+ const { createIssueException, validateIssueException, validateIssueExceptionChain } = require("./issue-exception.cjs");
7
+ const { isList, optionalEntry } = require("./validation.cjs");
8
+
9
+ /**
10
+ * @import { IssueAttributes, IssueBreadcrumb, IssueExceptionChainEntry, IssueStackFrame } from "./index.cjs"
11
+ * @import { DeepReadonly } from "./validation.cjs"
12
+ * @typedef {Pick<IssueAttributes, "exception" | "exceptionChain" | "breadcrumbs" | "breadcrumbsTruncated" | "evidence">} IssueDiagnostics
13
+ */
14
+
15
+ /**
16
+ * @param {Readonly<Record<string, unknown>>} attributes
17
+ * @returns {boolean | undefined}
18
+ */
19
+ function breadcrumbsTruncated(attributes) {
20
+ const truncated = attributes["breadcrumbsTruncated"];
21
+ if (truncated !== undefined && typeof truncated !== "boolean") {
22
+ throw new SdkError("validation_error", "issue breadcrumbsTruncated must be a boolean");
23
+ }
24
+ return truncated === true ? true : undefined;
25
+ }
546
26
 
547
- function boundedText(label, value, maxLength, { rejectLocationText = false } = {}) {
548
- const normalized = typeof value === "string" ? value.trim() : "";
549
- if (normalized === "") {
550
- throw validationError(`${label} must be non-empty`);
551
- }
552
- if (
553
- Array.from(normalized).length > maxLength
554
- || hasControlCharacter(normalized)
555
- || (rejectLocationText && /[?#]/u.test(normalized))
556
- ) {
557
- throw validationError(`${label} is invalid or exceeds ${maxLength} characters`);
558
- }
559
- return normalized;
560
- }
27
+ /**
28
+ * @param {Readonly<Record<string, unknown>>} attributes
29
+ * @returns {IssueDiagnostics}
30
+ */
31
+ function validateIssueDiagnostics(attributes) {
32
+ const exception = validateIssueException(attributes["exception"]);
33
+ const exceptionChain = validateIssueExceptionChain(attributes["exceptionChain"], exception, attributes["stackFrames"]);
34
+ const breadcrumbs = validateIssueBreadcrumbs(attributes["breadcrumbs"]);
35
+ const evidence = validateIssueEvidence(attributes["evidence"]);
36
+ return {
37
+ ...optionalEntry("exception", exception),
38
+ ...optionalEntry("exceptionChain", exceptionChain),
39
+ ...optionalEntry("breadcrumbs", breadcrumbs),
40
+ ...optionalEntry("breadcrumbsTruncated", breadcrumbsTruncated(attributes)),
41
+ ...optionalEntry("evidence", evidence)
42
+ };
43
+ }
561
44
 
562
- function requireObject(label, value) {
563
- if (!value || Array.isArray(value) || typeof value !== "object") {
564
- throw validationError(`${label} must be an object`);
565
- }
566
- }
45
+ /**
46
+ * @param {DeepReadonly<IssueStackFrame>} frame
47
+ * @returns {DeepReadonly<IssueStackFrame>}
48
+ */
49
+ function cloneFrame(frame) {
50
+ return { ...frame };
51
+ }
567
52
 
568
- function rejectUnknownKeys(label, value, allowed) {
569
- const unknown = Object.keys(value).filter((key) => !allowed.has(key));
570
- if (unknown.length > 0) {
571
- throw validationError(`${label} has unsupported fields: ${unknown.sort().join(", ")}`);
572
- }
573
- }
53
+ /**
54
+ * @param {DeepReadonly<IssueBreadcrumb>} breadcrumb
55
+ * @returns {DeepReadonly<IssueBreadcrumb>}
56
+ */
57
+ function cloneBreadcrumb(breadcrumb) {
58
+ return {
59
+ ...breadcrumb,
60
+ ...optionalEntry("data", breadcrumb.data === undefined ? undefined : { ...breadcrumb.data })
61
+ };
62
+ }
574
63
 
575
- function hasControlCharacter(value) {
576
- return Array.from(value).some((character) => {
577
- const code = character.codePointAt(0);
578
- return code !== undefined && (code <= 31 || (code >= 127 && code <= 159));
579
- });
580
- }
64
+ /**
65
+ * @param {DeepReadonly<IssueExceptionChainEntry>} entry
66
+ * @returns {DeepReadonly<IssueExceptionChainEntry>}
67
+ */
68
+ function cloneChainEntry(entry) {
69
+ const { mechanism, stackFrames } = entry;
70
+ return {
71
+ ...entry,
72
+ ...optionalEntry("mechanism", mechanism === undefined ? undefined : { ...mechanism }),
73
+ ...optionalEntry("stackFrames", stackFrames === undefined ? undefined : stackFrames.map((frame) => cloneFrame(frame)))
74
+ };
75
+ }
581
76
 
77
+ /**
78
+ * @param {DeepReadonly<IssueDiagnostics>} attributes
79
+ * @returns {DeepReadonly<IssueDiagnostics>}
80
+ */
81
+ function cloneIssueDiagnostics(attributes) {
82
+ const { breadcrumbs, evidence, exception, exceptionChain } = attributes;
582
83
  return {
583
- MAX_ISSUE_BREADCRUMBS,
584
- MAX_ISSUE_EXCEPTIONS,
585
- cloneIssueDiagnostics,
586
- createIssueException,
587
- validateIssueBreadcrumb,
588
- validateIssueDiagnostics,
589
- validateIssueEvidence
84
+ ...optionalEntry("exception", exception === undefined ? undefined : {
85
+ ...exception,
86
+ ...optionalEntry("mechanism", exception.mechanism === undefined ? undefined : { ...exception.mechanism })
87
+ }),
88
+ ...optionalEntry("exceptionChain", exceptionChain === undefined ? undefined : {
89
+ entries: exceptionChain.entries.map((entry) => cloneChainEntry(entry)),
90
+ truncated: exceptionChain.truncated
91
+ }),
92
+ ...optionalEntry("breadcrumbs", isList(breadcrumbs) ? breadcrumbs.map((breadcrumb) => cloneBreadcrumb(breadcrumb)) : undefined),
93
+ ...optionalEntry("breadcrumbsTruncated", attributes.breadcrumbsTruncated === true ? true : undefined),
94
+ ...optionalEntry("evidence", evidence === undefined ? undefined : cloneIssueEvidence(evidence))
590
95
  };
591
96
  }
592
97
 
593
- module.exports = { buildIssueDiagnosticsHelpers };
98
+ module.exports = {
99
+ MAX_ISSUE_BREADCRUMBS,
100
+ cloneIssueDiagnostics,
101
+ createIssueException,
102
+ validateIssueBreadcrumb,
103
+ validateIssueDiagnostics,
104
+ validateIssueEvidence
105
+ };