@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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { boundedIdentity, optionalText } = require("./issue-text.cjs");
|
|
5
|
+
const { optionalEntry, rejectUnknownKeys, requireObject } = require("./validation.cjs");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @import { IssueDiagnosticEvidence, IssueImpactEvidence, IssueLikelyFixArea } from "./index.cjs"
|
|
9
|
+
* @import { DeepReadonly } from "./validation.cjs"
|
|
10
|
+
* @typedef {"capturedFields" | "missingFields" | "redactedFields" | "truncatedFields"} EvidenceFieldKey
|
|
11
|
+
* @typedef {Pick<IssueDiagnosticEvidence, EvidenceFieldKey>} EvidenceFieldLists
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MAX_DIAGNOSTIC_IDENTITY_LENGTH = 256;
|
|
15
|
+
const MAX_DIAGNOSTIC_CAUSE_LENGTH = 1024;
|
|
16
|
+
const MAX_DIAGNOSTIC_OUTCOME_LENGTH = 512;
|
|
17
|
+
const MAX_DIAGNOSTIC_FIELDS = 32;
|
|
18
|
+
const MIN_DIAGNOSTIC_FIELDS = 1;
|
|
19
|
+
const MIN_COORDINATE = 1;
|
|
20
|
+
const MAX_COORDINATE = 2_147_483_647;
|
|
21
|
+
const NO_FIELDS = 0;
|
|
22
|
+
const EVIDENCE_FIELD_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/u;
|
|
23
|
+
const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:\//u;
|
|
24
|
+
/** @type {readonly EvidenceFieldKey[]} */
|
|
25
|
+
const EVIDENCE_FIELD_KEYS = ["capturedFields", "missingFields", "redactedFields", "truncatedFields"];
|
|
26
|
+
const EVIDENCE_KEYS = new Set(["likelyRootCause", "likelyFixArea", "impact", ...EVIDENCE_FIELD_KEYS]);
|
|
27
|
+
const FIX_AREA_KEYS = new Set(["component", "module", "function", "file", "line", "column", "inApp"]);
|
|
28
|
+
const IMPACT_KEYS = new Set(["affectedUserSegment", "failedAction", "userVisibleOutcome"]);
|
|
29
|
+
/** @type {readonly ["component", "module", "function"]} */
|
|
30
|
+
const FIX_AREA_IDENTITY_KEYS = ["component", "module", "function"];
|
|
31
|
+
/** @type {readonly ["affectedUserSegment", "failedAction"]} */
|
|
32
|
+
const IMPACT_IDENTITY_KEYS = ["affectedUserSegment", "failedAction"];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @template {string} Field
|
|
36
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
37
|
+
* @param {readonly Field[]} fields
|
|
38
|
+
* @param {string} label
|
|
39
|
+
* @returns {{ [Key in Field]?: string }}
|
|
40
|
+
*/
|
|
41
|
+
function identityFields(source, fields, label) {
|
|
42
|
+
/** @type {{ [Key in Field]?: string }} */
|
|
43
|
+
const validated = {};
|
|
44
|
+
for (const field of fields) {
|
|
45
|
+
const value = source[field];
|
|
46
|
+
if (value !== undefined) {
|
|
47
|
+
validated[field] = boundedIdentity(`${label} ${field}`, value, MAX_DIAGNOSTIC_IDENTITY_LENGTH);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return validated;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {unknown} value
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function safeRelativeSourcePath(value) {
|
|
58
|
+
const path = boundedIdentity("issue evidence likelyFixArea file", value, MAX_DIAGNOSTIC_IDENTITY_LENGTH).replaceAll("\\", "/");
|
|
59
|
+
if (
|
|
60
|
+
path.startsWith("/")
|
|
61
|
+
|| WINDOWS_DRIVE_PATTERN.test(path)
|
|
62
|
+
|| path.includes("://")
|
|
63
|
+
|| path.split("/").some((part) => part === "" || part === "." || part === "..")
|
|
64
|
+
) {
|
|
65
|
+
throw new SdkError("validation_error", "issue evidence likelyFixArea file must be a safe relative path");
|
|
66
|
+
}
|
|
67
|
+
return path;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {"line" | "column"} key
|
|
72
|
+
* @param {unknown} value
|
|
73
|
+
* @returns {number | undefined}
|
|
74
|
+
*/
|
|
75
|
+
function optionalCoordinate(key, value) {
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < MIN_COORDINATE || value > MAX_COORDINATE) {
|
|
80
|
+
throw new SdkError("validation_error", `issue evidence likelyFixArea ${key} must be a positive integer`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {unknown} value
|
|
87
|
+
* @returns {boolean | undefined}
|
|
88
|
+
*/
|
|
89
|
+
function optionalInApp(value) {
|
|
90
|
+
if (value !== undefined && typeof value !== "boolean") {
|
|
91
|
+
throw new SdkError("validation_error", "issue evidence likelyFixArea inApp must be a boolean");
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
98
|
+
* @returns {Omit<IssueLikelyFixArea, "inApp">}
|
|
99
|
+
*/
|
|
100
|
+
function fixAreaLocation(source) {
|
|
101
|
+
const identities = identityFields(source, FIX_AREA_IDENTITY_KEYS, "issue evidence likelyFixArea");
|
|
102
|
+
const file = source["file"] === undefined ? undefined : safeRelativeSourcePath(source["file"]);
|
|
103
|
+
const line = optionalCoordinate("line", source["line"]);
|
|
104
|
+
const column = optionalCoordinate("column", source["column"]);
|
|
105
|
+
return {
|
|
106
|
+
...identities,
|
|
107
|
+
...optionalEntry("file", file),
|
|
108
|
+
...optionalEntry("line", line),
|
|
109
|
+
...optionalEntry("column", column)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {unknown} area
|
|
115
|
+
* @returns {IssueLikelyFixArea | undefined}
|
|
116
|
+
*/
|
|
117
|
+
function validateLikelyFixArea(area) {
|
|
118
|
+
if (area === undefined) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const source = requireObject("issue evidence likelyFixArea", area);
|
|
122
|
+
rejectUnknownKeys("issue evidence likelyFixArea", source, FIX_AREA_KEYS);
|
|
123
|
+
const location = fixAreaLocation(source);
|
|
124
|
+
const inApp = optionalInApp(source["inApp"]);
|
|
125
|
+
if (Object.keys(location).length === NO_FIELDS) {
|
|
126
|
+
throw new SdkError("validation_error", "issue evidence likelyFixArea must identify a code location");
|
|
127
|
+
}
|
|
128
|
+
return { ...location, ...optionalEntry("inApp", inApp) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {unknown} impact
|
|
133
|
+
* @returns {IssueImpactEvidence | undefined}
|
|
134
|
+
*/
|
|
135
|
+
function validateImpactEvidence(impact) {
|
|
136
|
+
if (impact === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const source = requireObject("issue evidence impact", impact);
|
|
140
|
+
rejectUnknownKeys("issue evidence impact", source, IMPACT_KEYS);
|
|
141
|
+
const validated = {
|
|
142
|
+
...identityFields(source, IMPACT_IDENTITY_KEYS, "issue evidence impact"),
|
|
143
|
+
...optionalEntry(
|
|
144
|
+
"userVisibleOutcome",
|
|
145
|
+
optionalText("issue evidence impact userVisibleOutcome", source["userVisibleOutcome"], MAX_DIAGNOSTIC_OUTCOME_LENGTH)
|
|
146
|
+
)
|
|
147
|
+
};
|
|
148
|
+
if (Object.keys(validated).length === NO_FIELDS) {
|
|
149
|
+
throw new SdkError("validation_error", "issue evidence impact must contain at least one field");
|
|
150
|
+
}
|
|
151
|
+
return validated;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {unknown} field
|
|
156
|
+
* @returns {field is string}
|
|
157
|
+
*/
|
|
158
|
+
function isEvidenceField(field) {
|
|
159
|
+
return typeof field === "string" && EVIDENCE_FIELD_PATTERN.test(field);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @param {EvidenceFieldKey} key
|
|
164
|
+
* @param {unknown} fields
|
|
165
|
+
* @returns {string[] | undefined}
|
|
166
|
+
*/
|
|
167
|
+
function validateEvidenceFields(key, fields) {
|
|
168
|
+
if (fields === undefined) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
if (!Array.isArray(fields) || fields.length < MIN_DIAGNOSTIC_FIELDS || fields.length > MAX_DIAGNOSTIC_FIELDS) {
|
|
172
|
+
throw new SdkError("validation_error", `issue evidence ${key} must contain 1-${MAX_DIAGNOSTIC_FIELDS} fields`);
|
|
173
|
+
}
|
|
174
|
+
/** @type {readonly unknown[]} */
|
|
175
|
+
const list = fields;
|
|
176
|
+
const identifiers = list.filter((field) => isEvidenceField(field));
|
|
177
|
+
if (new Set(list).size !== list.length || identifiers.length !== list.length) {
|
|
178
|
+
throw new SdkError("validation_error", `issue evidence ${key} fields must be unique bounded identifiers`);
|
|
179
|
+
}
|
|
180
|
+
return identifiers;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {DeepReadonly<EvidenceFieldLists>} lists
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
function rejectConflictingFields(lists) {
|
|
188
|
+
const present = new Set();
|
|
189
|
+
for (const field of Object.values(lists).flat()) {
|
|
190
|
+
if (present.has(field)) {
|
|
191
|
+
throw new SdkError("validation_error", `issue evidence field ${field} has conflicting states`);
|
|
192
|
+
}
|
|
193
|
+
present.add(field);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
199
|
+
* @returns {EvidenceFieldLists}
|
|
200
|
+
*/
|
|
201
|
+
function evidenceFieldLists(source) {
|
|
202
|
+
/** @type {EvidenceFieldLists} */
|
|
203
|
+
const lists = {};
|
|
204
|
+
for (const key of EVIDENCE_FIELD_KEYS) {
|
|
205
|
+
const fields = validateEvidenceFields(key, source[key]);
|
|
206
|
+
if (fields !== undefined) {
|
|
207
|
+
lists[key] = fields;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
rejectConflictingFields(lists);
|
|
211
|
+
return lists;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param {unknown} evidence
|
|
216
|
+
* @returns {IssueDiagnosticEvidence | undefined}
|
|
217
|
+
*/
|
|
218
|
+
function validateIssueEvidence(evidence) {
|
|
219
|
+
if (evidence === undefined) {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
const source = requireObject("issue evidence", evidence);
|
|
223
|
+
rejectUnknownKeys("issue evidence", source, EVIDENCE_KEYS);
|
|
224
|
+
const validated = {
|
|
225
|
+
...optionalEntry(
|
|
226
|
+
"likelyRootCause",
|
|
227
|
+
optionalText("issue evidence likelyRootCause", source["likelyRootCause"], MAX_DIAGNOSTIC_CAUSE_LENGTH)
|
|
228
|
+
),
|
|
229
|
+
...optionalEntry("likelyFixArea", validateLikelyFixArea(source["likelyFixArea"])),
|
|
230
|
+
...optionalEntry("impact", validateImpactEvidence(source["impact"])),
|
|
231
|
+
...evidenceFieldLists(source)
|
|
232
|
+
};
|
|
233
|
+
if (Object.keys(validated).length === NO_FIELDS) {
|
|
234
|
+
throw new SdkError("validation_error", "issue evidence must contain at least one field");
|
|
235
|
+
}
|
|
236
|
+
return validated;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {DeepReadonly<IssueDiagnosticEvidence>} evidence
|
|
241
|
+
* @returns {DeepReadonly<IssueDiagnosticEvidence>}
|
|
242
|
+
*/
|
|
243
|
+
function cloneIssueEvidence(evidence) {
|
|
244
|
+
const { capturedFields, impact, likelyFixArea, missingFields, redactedFields, truncatedFields } = evidence;
|
|
245
|
+
return {
|
|
246
|
+
...evidence,
|
|
247
|
+
...optionalEntry("likelyFixArea", likelyFixArea === undefined ? undefined : { ...likelyFixArea }),
|
|
248
|
+
...optionalEntry("impact", impact === undefined ? undefined : { ...impact }),
|
|
249
|
+
...optionalEntry("capturedFields", capturedFields === undefined ? undefined : [...capturedFields]),
|
|
250
|
+
...optionalEntry("missingFields", missingFields === undefined ? undefined : [...missingFields]),
|
|
251
|
+
...optionalEntry("redactedFields", redactedFields === undefined ? undefined : [...redactedFields]),
|
|
252
|
+
...optionalEntry("truncatedFields", truncatedFields === undefined ? undefined : [...truncatedFields])
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { cloneIssueEvidence, validateIssueEvidence };
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { SdkError } = require("./sdk-error.cjs");
|
|
4
|
+
const { validateIssueStackFrames } = require("./issue-stack-frames.cjs");
|
|
5
|
+
const { boundedIdentity, optionalIdentity, optionalText, requireMachineName } = require("./issue-text.cjs");
|
|
6
|
+
const { isAllowedValue, optionalEntry, rejectUnknownKeys, requireObject } = require("./validation.cjs");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @import {
|
|
10
|
+
* IssueException,
|
|
11
|
+
* IssueExceptionChain,
|
|
12
|
+
* IssueExceptionChainEntry,
|
|
13
|
+
* IssueExceptionMechanism,
|
|
14
|
+
* IssueExceptionMessageState,
|
|
15
|
+
* IssueExceptionRelationship,
|
|
16
|
+
* IssueExceptionStackFramesState
|
|
17
|
+
* } from "./index.cjs"
|
|
18
|
+
* @import { DeepReadonly } from "./validation.cjs"
|
|
19
|
+
* @typedef {Pick<IssueExceptionChainEntry, "id" | "parentId" | "relationship">} EntryLineage
|
|
20
|
+
* @typedef {Pick<IssueExceptionChainEntry, "message" | "messageState">} EntryMessage
|
|
21
|
+
* @typedef {Pick<IssueExceptionChainEntry, "stackFrames" | "stackFramesState">} EntryStack
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const MAX_ISSUE_EXCEPTIONS = 8;
|
|
25
|
+
const MAX_EXCEPTION_TYPE_LENGTH = 256;
|
|
26
|
+
const MAX_EXCEPTION_MESSAGE_LENGTH = 1024;
|
|
27
|
+
const MAX_EXCEPTION_MODULE_LENGTH = 512;
|
|
28
|
+
const ROOT_INDEX = 0;
|
|
29
|
+
const NEXT_INDEX = 1;
|
|
30
|
+
const EXCEPTION_KEYS = new Set(["type", "mechanism"]);
|
|
31
|
+
const MECHANISM_KEYS = new Set(["type", "handled"]);
|
|
32
|
+
const CHAIN_KEYS = new Set(["entries", "truncated"]);
|
|
33
|
+
const ENTRY_KEYS = new Set([
|
|
34
|
+
"id",
|
|
35
|
+
"parentId",
|
|
36
|
+
"relationship",
|
|
37
|
+
"type",
|
|
38
|
+
"message",
|
|
39
|
+
"messageState",
|
|
40
|
+
"module",
|
|
41
|
+
"mechanism",
|
|
42
|
+
"stackFrames",
|
|
43
|
+
"stackFramesState"
|
|
44
|
+
]);
|
|
45
|
+
/** @type {ReadonlySet<IssueExceptionRelationship>} */
|
|
46
|
+
const CHILD_RELATIONSHIPS = new Set(["cause", "context", "aggregate_member", "suppressed"]);
|
|
47
|
+
/** @type {ReadonlySet<IssueExceptionMessageState>} */
|
|
48
|
+
const MESSAGE_STATES = new Set(["captured", "truncated", "redacted", "not_captured"]);
|
|
49
|
+
/** @type {ReadonlySet<IssueExceptionStackFramesState>} */
|
|
50
|
+
const STACK_FRAMES_STATES = new Set(["captured", "truncated", "not_captured"]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {unknown} mechanism
|
|
54
|
+
* @returns {IssueExceptionMechanism | undefined}
|
|
55
|
+
*/
|
|
56
|
+
function validateIssueExceptionMechanism(mechanism) {
|
|
57
|
+
if (mechanism === undefined) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const source = requireObject("issue exception mechanism", mechanism);
|
|
61
|
+
rejectUnknownKeys("issue exception mechanism", source, MECHANISM_KEYS);
|
|
62
|
+
const type = requireMachineName("issue exception mechanism type", source["type"]);
|
|
63
|
+
const { handled } = source;
|
|
64
|
+
if (typeof handled !== "boolean") {
|
|
65
|
+
throw new SdkError("validation_error", "issue exception mechanism handled must be a boolean");
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
type,
|
|
69
|
+
|
|
70
|
+
handled
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {unknown} exception
|
|
76
|
+
* @returns {IssueException | undefined}
|
|
77
|
+
*/
|
|
78
|
+
function validateIssueException(exception) {
|
|
79
|
+
if (exception === undefined) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const source = requireObject("issue exception", exception);
|
|
83
|
+
rejectUnknownKeys("issue exception", source, EXCEPTION_KEYS);
|
|
84
|
+
const type = boundedIdentity("issue exception type", source["type"], MAX_EXCEPTION_TYPE_LENGTH);
|
|
85
|
+
const mechanism = validateIssueExceptionMechanism(source["mechanism"]);
|
|
86
|
+
return { type, ...optionalEntry("mechanism", mechanism) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} type
|
|
91
|
+
* @param {string} mechanism
|
|
92
|
+
* @param {boolean} handled
|
|
93
|
+
* @returns {IssueException | undefined}
|
|
94
|
+
*/
|
|
95
|
+
function createIssueException(type, mechanism, handled) {
|
|
96
|
+
return validateIssueException({ mechanism: { handled, type: mechanism }, type });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {unknown} parentId
|
|
101
|
+
* @param {number} index
|
|
102
|
+
* @returns {parentId is number}
|
|
103
|
+
*/
|
|
104
|
+
function isParentIndex(parentId, index) {
|
|
105
|
+
return typeof parentId === "number" && Number.isInteger(parentId) && parentId >= ROOT_INDEX && parentId < index;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
110
|
+
* @param {number} index
|
|
111
|
+
* @returns {EntryLineage}
|
|
112
|
+
*/
|
|
113
|
+
function entryLineage(source, index) {
|
|
114
|
+
const { id, parentId, relationship } = source;
|
|
115
|
+
if (typeof id !== "number" || !Number.isInteger(id) || id !== index) {
|
|
116
|
+
throw new SdkError("validation_error", `issue exceptionChain entry ${index} id must equal its array index`);
|
|
117
|
+
}
|
|
118
|
+
if (index === ROOT_INDEX) {
|
|
119
|
+
if (relationship !== "reported" || parentId !== undefined) {
|
|
120
|
+
throw new SdkError("validation_error", "issue exceptionChain entry 0 must be the parentless reported exception");
|
|
121
|
+
}
|
|
122
|
+
return { id, relationship };
|
|
123
|
+
}
|
|
124
|
+
if (typeof relationship !== "string" || !isAllowedValue(relationship, CHILD_RELATIONSHIPS) || !isParentIndex(parentId, index)) {
|
|
125
|
+
throw new SdkError("validation_error", `issue exceptionChain entry ${index} parent relationship is invalid`);
|
|
126
|
+
}
|
|
127
|
+
return { id, parentId, relationship };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
132
|
+
* @param {number} index
|
|
133
|
+
* @returns {EntryMessage}
|
|
134
|
+
*/
|
|
135
|
+
function entryMessage(source, index) {
|
|
136
|
+
const { messageState } = source;
|
|
137
|
+
if (typeof messageState !== "string" || !isAllowedValue(messageState, MESSAGE_STATES)) {
|
|
138
|
+
throw new SdkError("validation_error", `issue exceptionChain entry ${index} messageState is invalid`);
|
|
139
|
+
}
|
|
140
|
+
const message = optionalText(`issue exceptionChain entry ${index} message`, source["message"], MAX_EXCEPTION_MESSAGE_LENGTH);
|
|
141
|
+
if ((messageState === "captured" || messageState === "truncated") !== (message !== undefined)) {
|
|
142
|
+
throw new SdkError("validation_error", `issue exceptionChain entry ${index} message does not match messageState`);
|
|
143
|
+
}
|
|
144
|
+
return { ...optionalEntry("message", message), messageState };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {Readonly<Record<string, unknown>>} source
|
|
149
|
+
* @param {number} index
|
|
150
|
+
* @returns {EntryStack}
|
|
151
|
+
*/
|
|
152
|
+
function entryStack(source, index) {
|
|
153
|
+
const { stackFramesState } = source;
|
|
154
|
+
if (typeof stackFramesState !== "string" || !isAllowedValue(stackFramesState, STACK_FRAMES_STATES)) {
|
|
155
|
+
throw new SdkError("validation_error", `issue exceptionChain entry ${index} stackFramesState is invalid`);
|
|
156
|
+
}
|
|
157
|
+
const stackFrames = validateIssueStackFrames(source["stackFrames"]);
|
|
158
|
+
if ((stackFramesState === "captured" || stackFramesState === "truncated") !== (stackFrames !== undefined)) {
|
|
159
|
+
throw new SdkError("validation_error", `issue exceptionChain entry ${index} stackFrames do not match stackFramesState`);
|
|
160
|
+
}
|
|
161
|
+
return { ...optionalEntry("stackFrames", stackFrames), stackFramesState };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {unknown} entry
|
|
166
|
+
* @param {number} index
|
|
167
|
+
* @returns {IssueExceptionChainEntry}
|
|
168
|
+
*/
|
|
169
|
+
function validateIssueExceptionChainEntry(entry, index) {
|
|
170
|
+
const label = `issue exceptionChain entry ${index}`;
|
|
171
|
+
const source = requireObject(label, entry);
|
|
172
|
+
rejectUnknownKeys(label, source, ENTRY_KEYS);
|
|
173
|
+
const lineage = entryLineage(source, index);
|
|
174
|
+
const type = boundedIdentity(`${label} type`, source["type"], MAX_EXCEPTION_TYPE_LENGTH);
|
|
175
|
+
const message = entryMessage(source, index);
|
|
176
|
+
const moduleName = optionalIdentity(`${label} module`, source["module"], MAX_EXCEPTION_MODULE_LENGTH);
|
|
177
|
+
const mechanism = validateIssueExceptionMechanism(source["mechanism"]);
|
|
178
|
+
const stack = entryStack(source, index);
|
|
179
|
+
return {
|
|
180
|
+
...lineage,
|
|
181
|
+
type,
|
|
182
|
+
...message,
|
|
183
|
+
...optionalEntry("module", moduleName),
|
|
184
|
+
...optionalEntry("mechanism", mechanism),
|
|
185
|
+
...stack
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {readonly unknown[]} entries
|
|
191
|
+
* @returns {entries is readonly [unknown, ...unknown[]]}
|
|
192
|
+
*/
|
|
193
|
+
function hasChainEntries(entries) {
|
|
194
|
+
return entries.length >= NEXT_INDEX && entries.length <= MAX_ISSUE_EXCEPTIONS;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {unknown} entries
|
|
199
|
+
* @returns {readonly [unknown, ...unknown[]]}
|
|
200
|
+
*/
|
|
201
|
+
function requireChainEntries(entries) {
|
|
202
|
+
if (Array.isArray(entries)) {
|
|
203
|
+
/** @type {readonly unknown[]} */
|
|
204
|
+
const list = entries;
|
|
205
|
+
if (hasChainEntries(list)) {
|
|
206
|
+
return list;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
throw new SdkError("validation_error", `issue exceptionChain entries must contain 1-${MAX_ISSUE_EXCEPTIONS} exceptions`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {unknown} truncated
|
|
214
|
+
* @returns {boolean}
|
|
215
|
+
*/
|
|
216
|
+
function requireChainTruncated(truncated) {
|
|
217
|
+
if (typeof truncated !== "boolean") {
|
|
218
|
+
throw new SdkError("validation_error", "issue exceptionChain truncated must be a boolean");
|
|
219
|
+
}
|
|
220
|
+
return truncated;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {DeepReadonly<IssueExceptionChainEntry>} reported
|
|
225
|
+
* @param {DeepReadonly<IssueException> | undefined} legacyException
|
|
226
|
+
* @param {unknown} legacyStackFrames
|
|
227
|
+
* @returns {void}
|
|
228
|
+
*/
|
|
229
|
+
function requireReportedMatch(reported, legacyException, legacyStackFrames) {
|
|
230
|
+
const reportedException = { type: reported.type, ...optionalEntry("mechanism", reported.mechanism) };
|
|
231
|
+
if (legacyException === undefined || JSON.stringify(reportedException) !== JSON.stringify(legacyException)) {
|
|
232
|
+
throw new SdkError("validation_error", "issue exceptionChain reported exception must match exception");
|
|
233
|
+
}
|
|
234
|
+
const canonicalLegacyFrames = validateIssueStackFrames(legacyStackFrames);
|
|
235
|
+
const framesMatch = reported.stackFramesState === "not_captured"
|
|
236
|
+
? canonicalLegacyFrames === undefined
|
|
237
|
+
: JSON.stringify(reported.stackFrames) === JSON.stringify(canonicalLegacyFrames);
|
|
238
|
+
if (!framesMatch) {
|
|
239
|
+
throw new SdkError("validation_error", "issue exceptionChain reported stack must match stackFrames");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {unknown} chain
|
|
245
|
+
* @param {DeepReadonly<IssueException> | undefined} legacyException
|
|
246
|
+
* @param {unknown} legacyStackFrames
|
|
247
|
+
* @returns {IssueExceptionChain | undefined}
|
|
248
|
+
*/
|
|
249
|
+
function validateIssueExceptionChain(chain, legacyException, legacyStackFrames) {
|
|
250
|
+
if (chain === undefined) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
const source = requireObject("issue exceptionChain", chain);
|
|
254
|
+
rejectUnknownKeys("issue exceptionChain", source, CHAIN_KEYS);
|
|
255
|
+
const [first, ...rest] = requireChainEntries(source["entries"]);
|
|
256
|
+
const truncated = requireChainTruncated(source["truncated"]);
|
|
257
|
+
const reported = validateIssueExceptionChainEntry(first, ROOT_INDEX);
|
|
258
|
+
const entries = [reported, ...rest.map((entry, offset) => validateIssueExceptionChainEntry(entry, offset + NEXT_INDEX))];
|
|
259
|
+
requireReportedMatch(reported, legacyException, legacyStackFrames);
|
|
260
|
+
return { entries, truncated };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { createIssueException, validateIssueException, validateIssueExceptionChain };
|