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