@logbrew/sdk 0.1.2 → 0.1.4
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/README.md +351 -2
- package/examples/agent-timeline.cjs +87 -0
- package/examples/agent-timeline.mjs +79 -0
- package/examples/index.mjs +15 -0
- package/examples/package.json +3 -0
- package/examples/real-user-smoke.cjs +15 -1
- package/examples/real-user-smoke.mjs +15 -1
- package/index.cjs +1358 -28
- package/index.d.cts +461 -6
- package/index.d.ts +461 -6
- package/index.js +13 -0
- package/issue-stack.cjs +149 -0
- package/opentelemetry.cjs +1080 -0
- package/package.json +28 -1
- package/release-artifacts-build.cjs +227 -0
- package/release-artifacts-common.js +82 -0
- package/release-artifacts-symbolication.js +550 -0
- package/release-artifacts-upload.js +380 -0
- package/release-artifacts.js +766 -0
- package/support-ticket.cjs +175 -0
- package/trace-context.cjs +217 -0
- package/vite-release-artifacts.cjs +152 -0
- package/vite-release-artifacts.d.cts +44 -0
- package/vite-release-artifacts.d.ts +44 -0
- package/vite-release-artifacts.js +6 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SCRIPT_VERSION = "0.1.0";
|
|
5
|
+
const MAX_SOURCE_CONTEXT_FILE_BYTES = 1024 * 1024;
|
|
6
|
+
const SOURCE_MAP_DEBUG_ID_KEYS = ["debug_id", "debugId", "debugID", "x_debug_id"];
|
|
7
|
+
const VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
8
|
+
const VLQ_VALUES = new Map([...VLQ_CHARS].map((char, index) => [char, index]));
|
|
9
|
+
|
|
10
|
+
function fileReference(value) {
|
|
11
|
+
return value.split("?", 1)[0].split("#", 1)[0];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toPosix(value) {
|
|
15
|
+
return value.split(path.sep).join("/");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function relativeTo(root, filePath) {
|
|
19
|
+
return toPosix(path.relative(root, filePath));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeResolve(candidate, root) {
|
|
23
|
+
const resolvedRoot = fs.realpathSync(root);
|
|
24
|
+
const resolved = path.resolve(candidate);
|
|
25
|
+
const comparable = fs.existsSync(resolved) ? fs.realpathSync(resolved) : resolved;
|
|
26
|
+
const relative = path.relative(resolvedRoot, comparable);
|
|
27
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))) {
|
|
28
|
+
return comparable;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readJsonObject(filePath, label) {
|
|
34
|
+
let payload;
|
|
35
|
+
try {
|
|
36
|
+
payload = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new Error(`${label} is not valid JSON: ${error.message}`, { cause: error });
|
|
39
|
+
}
|
|
40
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
41
|
+
throw new Error(`${label} must be a JSON object`);
|
|
42
|
+
}
|
|
43
|
+
return payload;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sourceMapDebugId(payload) {
|
|
47
|
+
for (const key of SOURCE_MAP_DEBUG_ID_KEYS) {
|
|
48
|
+
const value = payload[key];
|
|
49
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
50
|
+
return value.trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseStackFrame(stackFrame) {
|
|
57
|
+
let line = stackFrame.trim();
|
|
58
|
+
if (line.startsWith("at ")) {
|
|
59
|
+
line = line.slice(3).trim();
|
|
60
|
+
}
|
|
61
|
+
let functionName = null;
|
|
62
|
+
let location = line;
|
|
63
|
+
if (line.endsWith(")") && line.includes(" (")) {
|
|
64
|
+
const marker = line.lastIndexOf(" (");
|
|
65
|
+
functionName = line.slice(0, marker);
|
|
66
|
+
location = line.slice(marker + 2, -1);
|
|
67
|
+
}
|
|
68
|
+
const parts = location.split(":");
|
|
69
|
+
if (parts.length < 3) {
|
|
70
|
+
throw new Error("stack frame must end with :line:column");
|
|
71
|
+
}
|
|
72
|
+
const columnText = parts.pop();
|
|
73
|
+
const lineText = parts.pop();
|
|
74
|
+
const filename = parts.join(":");
|
|
75
|
+
const generatedLine = Number.parseInt(lineText, 10);
|
|
76
|
+
const generatedColumn = Number.parseInt(columnText, 10);
|
|
77
|
+
if (!Number.isInteger(generatedLine) || !Number.isInteger(generatedColumn)) {
|
|
78
|
+
throw new Error("stack frame line and column must be integers");
|
|
79
|
+
}
|
|
80
|
+
if (generatedLine < 1 || generatedColumn < 1) {
|
|
81
|
+
throw new Error("stack frame line and column must be one-based positive integers");
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
filename,
|
|
85
|
+
line: generatedLine,
|
|
86
|
+
column: generatedColumn,
|
|
87
|
+
...(functionName ? { function: functionName } : {})
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeReference(value) {
|
|
92
|
+
let normalized = fileReference(value.trim());
|
|
93
|
+
if (normalized.startsWith("file://")) {
|
|
94
|
+
normalized = normalized.slice("file://".length);
|
|
95
|
+
}
|
|
96
|
+
return normalized;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function artifactMatchesFrame(artifact, frame, buildDir) {
|
|
100
|
+
if (!artifact.minifiedSource || typeof artifact.minifiedSource !== "object") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const artifactPath = String(artifact.minifiedSource.path ?? "");
|
|
104
|
+
const artifactUrl = String(artifact.minifiedSource.minifiedUrl ?? "");
|
|
105
|
+
const normalizedFrame = normalizeReference(String(frame.filename));
|
|
106
|
+
if (normalizedFrame === normalizeReference(artifactUrl)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (normalizedFrame.replace(/^\/+|\/+$/gu, "") === artifactPath.replace(/^\/+|\/+$/gu, "")) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (path.isAbsolute(normalizedFrame)) {
|
|
113
|
+
const resolved = safeResolve(normalizedFrame, buildDir);
|
|
114
|
+
if (resolved) {
|
|
115
|
+
return relativeTo(buildDir, resolved) === artifactPath;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function requireReadyArtifacts(manifest) {
|
|
122
|
+
if (manifest.artifactType !== "javascript_source_map_manifest") {
|
|
123
|
+
throw new Error("only javascript_source_map_manifest symbolication proof is supported");
|
|
124
|
+
}
|
|
125
|
+
if (!manifest.validation || manifest.validation.status !== "ready") {
|
|
126
|
+
throw new Error("manifest validation status must be ready");
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
|
|
129
|
+
throw new Error("manifest must contain at least one JavaScript source-map artifact");
|
|
130
|
+
}
|
|
131
|
+
for (const artifact of manifest.artifacts) {
|
|
132
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
|
|
133
|
+
throw new Error("artifact entries must be JSON objects");
|
|
134
|
+
}
|
|
135
|
+
if (!artifact.validation || artifact.validation.status !== "ready") {
|
|
136
|
+
throw new Error("all artifact validation statuses must be ready");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return manifest.artifacts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function findMatchingArtifact(manifest, frame, buildDir) {
|
|
143
|
+
const artifact = requireReadyArtifacts(manifest).find((candidate) => artifactMatchesFrame(candidate, frame, buildDir));
|
|
144
|
+
if (!artifact) {
|
|
145
|
+
throw new Error("no manifest artifact matches the minified stack frame filename");
|
|
146
|
+
}
|
|
147
|
+
return artifact;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findArtifactByDebugId(manifest, debugId) {
|
|
151
|
+
const normalizedDebugId = String(debugId).toLowerCase();
|
|
152
|
+
const matches = requireReadyArtifacts(manifest).filter((candidate) => {
|
|
153
|
+
const candidateDebugId = candidate.debugId;
|
|
154
|
+
return typeof candidateDebugId === "string" && candidateDebugId.toLowerCase() === normalizedDebugId;
|
|
155
|
+
});
|
|
156
|
+
if (matches.length === 0) {
|
|
157
|
+
throw new Error("no manifest artifact matches the issue event debug ID");
|
|
158
|
+
}
|
|
159
|
+
if (matches.length > 1) {
|
|
160
|
+
throw new Error("multiple manifest artifacts match the issue event debug ID");
|
|
161
|
+
}
|
|
162
|
+
return matches[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function loadManifestSourceMap(artifact, buildDir) {
|
|
166
|
+
if (!artifact.sourceMap || typeof artifact.sourceMap !== "object" || !artifact.sourceMap.path) {
|
|
167
|
+
throw new Error("matched artifact is missing source map metadata");
|
|
168
|
+
}
|
|
169
|
+
const sourceMapPath = safeResolve(path.join(buildDir, String(artifact.sourceMap.path)), buildDir);
|
|
170
|
+
if (!sourceMapPath) {
|
|
171
|
+
throw new Error("source map path resolves outside the build directory");
|
|
172
|
+
}
|
|
173
|
+
const payload = readJsonObject(sourceMapPath, "source map");
|
|
174
|
+
if ("sourcesContent" in payload) {
|
|
175
|
+
throw new Error("source map still contains sourcesContent; strip it before symbolication proof");
|
|
176
|
+
}
|
|
177
|
+
const artifactDebugId = artifact.debugId;
|
|
178
|
+
const mapDebugId = sourceMapDebugId(payload);
|
|
179
|
+
if (artifactDebugId && mapDebugId && artifactDebugId !== mapDebugId) {
|
|
180
|
+
throw new Error("matched artifact debug ID does not match source map debug ID");
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(payload.sources) || payload.sources.length === 0) {
|
|
183
|
+
throw new Error("source map sources must be a non-empty array");
|
|
184
|
+
}
|
|
185
|
+
if (typeof payload.mappings !== "string" || payload.mappings === "") {
|
|
186
|
+
throw new Error("source map mappings must be a non-empty string");
|
|
187
|
+
}
|
|
188
|
+
return { path: sourceMapPath, payload };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function decodeVlqValues(segment) {
|
|
192
|
+
const values = [];
|
|
193
|
+
let value = 0;
|
|
194
|
+
let shift = 0;
|
|
195
|
+
for (const char of segment) {
|
|
196
|
+
const digit = VLQ_VALUES.get(char);
|
|
197
|
+
if (digit === undefined) {
|
|
198
|
+
throw new Error("source map mappings contain an invalid base64 VLQ character");
|
|
199
|
+
}
|
|
200
|
+
const continuation = digit & 32;
|
|
201
|
+
value += (digit & 31) << shift;
|
|
202
|
+
if (continuation) {
|
|
203
|
+
shift += 5;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
values.push((value & 1 ? -1 : 1) * (value >> 1));
|
|
207
|
+
value = 0;
|
|
208
|
+
shift = 0;
|
|
209
|
+
}
|
|
210
|
+
if (shift) {
|
|
211
|
+
throw new Error("source map mappings contain an unterminated base64 VLQ value");
|
|
212
|
+
}
|
|
213
|
+
return values;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function decodedMappingSegments(mappings) {
|
|
217
|
+
const lines = [];
|
|
218
|
+
let previousSource = 0;
|
|
219
|
+
let previousOriginalLine = 0;
|
|
220
|
+
let previousOriginalColumn = 0;
|
|
221
|
+
let previousName = 0;
|
|
222
|
+
for (const rawLine of mappings.split(";")) {
|
|
223
|
+
let generatedColumn = 0;
|
|
224
|
+
const lineSegments = [];
|
|
225
|
+
if (rawLine) {
|
|
226
|
+
for (const rawSegment of rawLine.split(",")) {
|
|
227
|
+
if (!rawSegment) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const values = decodeVlqValues(rawSegment);
|
|
231
|
+
if (![1, 4, 5].includes(values.length)) {
|
|
232
|
+
throw new Error("source map segment must contain 1, 4, or 5 VLQ fields");
|
|
233
|
+
}
|
|
234
|
+
generatedColumn += values[0];
|
|
235
|
+
if (values.length === 1) {
|
|
236
|
+
lineSegments.push([generatedColumn, null, null, null, null]);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
previousSource += values[1];
|
|
240
|
+
previousOriginalLine += values[2];
|
|
241
|
+
previousOriginalColumn += values[3];
|
|
242
|
+
let nameIndex = null;
|
|
243
|
+
if (values.length === 5) {
|
|
244
|
+
previousName += values[4];
|
|
245
|
+
nameIndex = previousName;
|
|
246
|
+
}
|
|
247
|
+
lineSegments.push([generatedColumn, previousSource, previousOriginalLine, previousOriginalColumn, nameIndex]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
lines.push(lineSegments);
|
|
251
|
+
}
|
|
252
|
+
return lines;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function safeOriginalSourceForReport(source) {
|
|
256
|
+
if (typeof source !== "string") {
|
|
257
|
+
throw new Error("source map segment references an invalid source value");
|
|
258
|
+
}
|
|
259
|
+
const value = fileReference(source.trim());
|
|
260
|
+
if (!value) {
|
|
261
|
+
throw new Error("source map segment references an invalid source value");
|
|
262
|
+
}
|
|
263
|
+
if (value.startsWith("file://") || path.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value)) {
|
|
264
|
+
throw new Error("source map source path must be stripped before symbolication proof");
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function originalPositionFor(payload, generatedLine, generatedColumn) {
|
|
270
|
+
const generatedLineIndex = generatedLine - 1;
|
|
271
|
+
const generatedColumnIndex = generatedColumn - 1;
|
|
272
|
+
const lines = decodedMappingSegments(payload.mappings);
|
|
273
|
+
if (generatedLineIndex >= lines.length) {
|
|
274
|
+
throw new Error("generated line is outside source map mappings");
|
|
275
|
+
}
|
|
276
|
+
let bestSegment = null;
|
|
277
|
+
for (const segment of lines[generatedLineIndex]) {
|
|
278
|
+
if (segment[0] <= generatedColumnIndex) {
|
|
279
|
+
bestSegment = segment;
|
|
280
|
+
} else {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!bestSegment || bestSegment[1] === null || bestSegment[2] === null || bestSegment[3] === null) {
|
|
285
|
+
throw new Error("no original source mapping found for generated frame");
|
|
286
|
+
}
|
|
287
|
+
const sourceIndex = bestSegment[1];
|
|
288
|
+
if (!Number.isInteger(sourceIndex) || sourceIndex < 0 || sourceIndex >= payload.sources.length) {
|
|
289
|
+
throw new Error("source map segment references an invalid source index");
|
|
290
|
+
}
|
|
291
|
+
const original = {
|
|
292
|
+
source: safeOriginalSourceForReport(payload.sources[sourceIndex]),
|
|
293
|
+
line: bestSegment[2] + 1,
|
|
294
|
+
column: bestSegment[3] + 1
|
|
295
|
+
};
|
|
296
|
+
const nameIndex = bestSegment[4];
|
|
297
|
+
if (Array.isArray(payload.names) && Number.isInteger(nameIndex) && nameIndex >= 0 && nameIndex < payload.names.length) {
|
|
298
|
+
const name = payload.names[nameIndex];
|
|
299
|
+
if (typeof name === "string" && name) {
|
|
300
|
+
original.name = name;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return original;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function requireContextLineCount(value) {
|
|
307
|
+
if (value === undefined || value === null) {
|
|
308
|
+
return 2;
|
|
309
|
+
}
|
|
310
|
+
if (!Number.isInteger(value) || value < 0 || value > 10) {
|
|
311
|
+
throw new Error("source context line count must be an integer from 0 to 10");
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sourceRootRelativeCandidates(source) {
|
|
317
|
+
const values = [];
|
|
318
|
+
const add = (candidate) => {
|
|
319
|
+
const cleaned = fileReference(String(candidate ?? "").trim()).replace(/\\/gu, "/");
|
|
320
|
+
if (!cleaned || cleaned.startsWith("file://") || /^[A-Za-z]:[\\/]/u.test(cleaned)) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const normalized = path.posix.normalize(cleaned.replace(/^\/+/u, ""));
|
|
324
|
+
if (!normalized || normalized === "." || normalized.startsWith("../") || normalized === "..") {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
values.push(normalized);
|
|
328
|
+
};
|
|
329
|
+
const addBundlerSuffixes = (candidate) => {
|
|
330
|
+
const cleaned = fileReference(String(candidate ?? "").trim()).replace(/\\/gu, "/");
|
|
331
|
+
for (const marker of ["/./", "!./"]) {
|
|
332
|
+
const markerIndex = cleaned.lastIndexOf(marker);
|
|
333
|
+
if (markerIndex >= 0) {
|
|
334
|
+
add(cleaned.slice(markerIndex + marker.length));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const parts = cleaned.replace(/^\/+/u, "").split("/");
|
|
338
|
+
if (parts.length > 1 && /^\[[^\]/]+\]$/u.test(parts[0])) {
|
|
339
|
+
add(parts.slice(1).join("/"));
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
add(source);
|
|
344
|
+
addBundlerSuffixes(source);
|
|
345
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(source)) {
|
|
346
|
+
try {
|
|
347
|
+
const url = new URL(source);
|
|
348
|
+
add(url.pathname);
|
|
349
|
+
addBundlerSuffixes(url.pathname);
|
|
350
|
+
} catch {
|
|
351
|
+
// Non-standard bundler schemes should still use the direct candidate.
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return [...new Set(values)];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function sourceContextPathCandidates(source, sourceRoot, sourceMapPath) {
|
|
358
|
+
const candidates = [];
|
|
359
|
+
if (sourceMapPath) {
|
|
360
|
+
candidates.push(path.resolve(path.dirname(sourceMapPath), source));
|
|
361
|
+
}
|
|
362
|
+
candidates.push(path.join(sourceRoot, source));
|
|
363
|
+
for (const relativeSource of sourceRootRelativeCandidates(source)) {
|
|
364
|
+
candidates.push(path.join(sourceRoot, relativeSource));
|
|
365
|
+
}
|
|
366
|
+
return [...new Set(candidates)];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function sourceContextForOriginalPosition(original, options = {}) {
|
|
370
|
+
if (!options.sourceRoot) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (typeof options.sourceRoot !== "string" || options.sourceRoot.trim() === "") {
|
|
374
|
+
throw new Error("source context root must be a non-empty directory path");
|
|
375
|
+
}
|
|
376
|
+
const requestedSourceRoot = path.resolve(options.sourceRoot);
|
|
377
|
+
if (!fs.existsSync(requestedSourceRoot) || !fs.statSync(requestedSourceRoot).isDirectory()) {
|
|
378
|
+
throw new Error("source context root must resolve to an existing directory");
|
|
379
|
+
}
|
|
380
|
+
const sourceRoot = fs.realpathSync(requestedSourceRoot);
|
|
381
|
+
let sourcePath = null;
|
|
382
|
+
for (const candidate of sourceContextPathCandidates(original.source, sourceRoot, options.sourceMapPath)) {
|
|
383
|
+
const resolved = safeResolve(candidate, sourceRoot);
|
|
384
|
+
if (resolved && fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
385
|
+
sourcePath = resolved;
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!sourcePath) {
|
|
390
|
+
throw new Error("original source file is not readable under the source context root");
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const contextLines = requireContextLineCount(options.contextLines);
|
|
394
|
+
if (fs.statSync(sourcePath).size > MAX_SOURCE_CONTEXT_FILE_BYTES) {
|
|
395
|
+
throw new Error("source context file is too large for local report output");
|
|
396
|
+
}
|
|
397
|
+
const sourceLines = fs.readFileSync(sourcePath, "utf8").split(/\r?\n/u);
|
|
398
|
+
const startLine = Math.max(1, original.line - contextLines);
|
|
399
|
+
const endLine = Math.min(sourceLines.length, original.line + contextLines);
|
|
400
|
+
const lines = [];
|
|
401
|
+
for (let lineNumber = startLine; lineNumber <= endLine; lineNumber += 1) {
|
|
402
|
+
lines.push({
|
|
403
|
+
line: lineNumber,
|
|
404
|
+
text: sourceLines[lineNumber - 1] ?? "",
|
|
405
|
+
highlighted: lineNumber === original.line
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
source: relativeTo(sourceRoot, sourcePath),
|
|
411
|
+
startLine,
|
|
412
|
+
lines
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function isJsonObject(value) {
|
|
417
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function stringOrNull(value) {
|
|
421
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function metadataFromIssueEvent(issueEvent) {
|
|
425
|
+
if (!isJsonObject(issueEvent)) {
|
|
426
|
+
throw new Error("issue event must be a JSON object");
|
|
427
|
+
}
|
|
428
|
+
if (isJsonObject(issueEvent.attributes) && isJsonObject(issueEvent.attributes.metadata)) {
|
|
429
|
+
return {
|
|
430
|
+
metadata: issueEvent.attributes.metadata,
|
|
431
|
+
input: {
|
|
432
|
+
type: "sdk_issue_event",
|
|
433
|
+
...(stringOrNull(issueEvent.id) ? { issueId: stringOrNull(issueEvent.id) } : {}),
|
|
434
|
+
metadataSource: "attributes.metadata"
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
if (isJsonObject(issueEvent.metadata)) {
|
|
439
|
+
return {
|
|
440
|
+
metadata: issueEvent.metadata,
|
|
441
|
+
input: {
|
|
442
|
+
type: "issue_attributes",
|
|
443
|
+
metadataSource: "metadata"
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
throw new Error("issue event must contain attributes.metadata or metadata");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function requireMetadataString(metadata, name) {
|
|
451
|
+
const value = stringOrNull(metadata[name]);
|
|
452
|
+
if (!value) {
|
|
453
|
+
throw new Error(`issue event metadata is missing ${name}`);
|
|
454
|
+
}
|
|
455
|
+
return value;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function requireMetadataPositiveInteger(metadata, name) {
|
|
459
|
+
const value = metadata[name];
|
|
460
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
461
|
+
throw new Error(`issue event metadata ${name} must be a one-based positive integer`);
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function requireMetadataMatchesManifest(metadata, manifest, name) {
|
|
467
|
+
const value = requireMetadataString(metadata, name);
|
|
468
|
+
if (manifest[name] !== value) {
|
|
469
|
+
throw new Error(`issue event metadata ${name} does not match the manifest`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function stackFrameFromIssueMetadata(metadata) {
|
|
474
|
+
const releaseArtifactType = requireMetadataString(metadata, "releaseArtifactType");
|
|
475
|
+
if (releaseArtifactType !== "sourcemap") {
|
|
476
|
+
throw new Error("issue event releaseArtifactType must be sourcemap");
|
|
477
|
+
}
|
|
478
|
+
const codeFile = stringOrNull(metadata.releaseArtifactCodeFile) ?? stringOrNull(metadata.errorFrameFile);
|
|
479
|
+
if (!codeFile) {
|
|
480
|
+
throw new Error("issue event metadata is missing releaseArtifactCodeFile");
|
|
481
|
+
}
|
|
482
|
+
const line = requireMetadataPositiveInteger(metadata, "errorFrameLine");
|
|
483
|
+
const column = requireMetadataPositiveInteger(metadata, "errorFrameColumn");
|
|
484
|
+
return `${codeFile}:${line}:${column}`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function symbolicationReportForArtifact({ buildDir, manifest, frame, artifact, sourceContext }) {
|
|
488
|
+
const sourceMap = loadManifestSourceMap(artifact, buildDir);
|
|
489
|
+
const original = originalPositionFor(sourceMap.payload, frame.line, frame.column);
|
|
490
|
+
const resolvedSourceContext = sourceContextForOriginalPosition(original, {
|
|
491
|
+
...sourceContext,
|
|
492
|
+
sourceMapPath: sourceMap.path
|
|
493
|
+
});
|
|
494
|
+
return {
|
|
495
|
+
status: "resolved",
|
|
496
|
+
verifier: { name: "logbrew-js-release-artifact-symbolication-verifier", version: SCRIPT_VERSION },
|
|
497
|
+
release: manifest.release,
|
|
498
|
+
environment: manifest.environment,
|
|
499
|
+
service: manifest.service,
|
|
500
|
+
debugId: artifact.debugId,
|
|
501
|
+
generated: {
|
|
502
|
+
path: artifact.minifiedSource.path,
|
|
503
|
+
minifiedUrl: artifact.minifiedSource.minifiedUrl,
|
|
504
|
+
line: frame.line,
|
|
505
|
+
column: frame.column,
|
|
506
|
+
...(frame.function ? { function: frame.function } : {})
|
|
507
|
+
},
|
|
508
|
+
sourceMap: {
|
|
509
|
+
path: artifact.sourceMap.path,
|
|
510
|
+
hasSourcesContent: false
|
|
511
|
+
},
|
|
512
|
+
original,
|
|
513
|
+
...(resolvedSourceContext ? { sourceContext: resolvedSourceContext } : {})
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function verifyJavaScriptSymbolication({ buildDir, manifest, stackFrame, sourceContext }) {
|
|
518
|
+
const frame = parseStackFrame(stackFrame);
|
|
519
|
+
return symbolicationReportForArtifact({
|
|
520
|
+
buildDir,
|
|
521
|
+
manifest,
|
|
522
|
+
frame,
|
|
523
|
+
artifact: findMatchingArtifact(manifest, frame, buildDir),
|
|
524
|
+
sourceContext
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function verifyJavaScriptIssueSymbolication({ buildDir, manifest, issueEvent, sourceContext }) {
|
|
529
|
+
const { metadata, input } = metadataFromIssueEvent(issueEvent);
|
|
530
|
+
requireMetadataMatchesManifest(metadata, manifest, "release");
|
|
531
|
+
requireMetadataMatchesManifest(metadata, manifest, "environment");
|
|
532
|
+
requireMetadataMatchesManifest(metadata, manifest, "service");
|
|
533
|
+
|
|
534
|
+
const expectedDebugId = requireMetadataString(metadata, "releaseArtifactDebugId").toLowerCase();
|
|
535
|
+
const artifact = findArtifactByDebugId(manifest, expectedDebugId);
|
|
536
|
+
const report = symbolicationReportForArtifact({
|
|
537
|
+
buildDir,
|
|
538
|
+
manifest,
|
|
539
|
+
frame: parseStackFrame(stackFrameFromIssueMetadata(metadata)),
|
|
540
|
+
artifact,
|
|
541
|
+
sourceContext
|
|
542
|
+
});
|
|
543
|
+
if (typeof report.debugId !== "string" || report.debugId.toLowerCase() !== expectedDebugId) {
|
|
544
|
+
throw new Error("issue event releaseArtifactDebugId does not match the resolved artifact");
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
...report,
|
|
548
|
+
input
|
|
549
|
+
};
|
|
550
|
+
}
|