@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 +44 -213
- package/issue-breadcrumbs.cjs +143 -0
- package/issue-diagnostics.cjs +94 -582
- package/issue-evidence.cjs +256 -0
- package/issue-exception.cjs +263 -0
- package/issue-stack-frames.cjs +220 -0
- package/issue-stack-text.cjs +101 -0
- package/issue-stack.cjs +188 -225
- package/issue-text.cjs +72 -0
- package/log-context.cjs +87 -67
- package/package.json +10 -1
- package/sdk-error.cjs +36 -0
- package/support-ticket.cjs +232 -122
- package/telemetry-context.cjs +343 -251
- package/trace-context.cjs +376 -179
- package/transport-error.cjs +23 -0
- package/validation.cjs +279 -0
- package/winston.cjs +6 -7
package/issue-diagnostics.cjs
CHANGED
|
@@ -1,593 +1,105 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
45
|
+
/**
|
|
46
|
+
* @param {DeepReadonly<IssueStackFrame>} frame
|
|
47
|
+
* @returns {DeepReadonly<IssueStackFrame>}
|
|
48
|
+
*/
|
|
49
|
+
function cloneFrame(frame) {
|
|
50
|
+
return { ...frame };
|
|
51
|
+
}
|
|
567
52
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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 = {
|
|
98
|
+
module.exports = {
|
|
99
|
+
MAX_ISSUE_BREADCRUMBS,
|
|
100
|
+
cloneIssueDiagnostics,
|
|
101
|
+
createIssueException,
|
|
102
|
+
validateIssueBreadcrumb,
|
|
103
|
+
validateIssueDiagnostics,
|
|
104
|
+
validateIssueEvidence
|
|
105
|
+
};
|