@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.
Files changed (90) hide show
  1. package/automatic-delivery.cjs +271 -0
  2. package/batch-sender.cjs +164 -0
  3. package/bounded-number.cjs +34 -0
  4. package/browser-breadcrumbs.cjs +346 -136
  5. package/capture-support.cjs +100 -0
  6. package/client-options.cjs +292 -0
  7. package/client-report.cjs +232 -0
  8. package/client.cjs +447 -0
  9. package/console-arguments.cjs +151 -0
  10. package/console-capture.cjs +194 -0
  11. package/core.cjs +30 -2810
  12. package/delivery-health.cjs +133 -0
  13. package/error-causes.cjs +130 -0
  14. package/error-details.cjs +124 -0
  15. package/error-issue.cjs +187 -0
  16. package/event-clone.cjs +55 -0
  17. package/event-metadata.cjs +40 -0
  18. package/event-pipeline.cjs +325 -0
  19. package/event-queue.cjs +306 -0
  20. package/event-store.cjs +237 -0
  21. package/event-validation.cjs +159 -0
  22. package/examples/agent-timeline.cjs +57 -46
  23. package/examples/agent-timeline.mjs +41 -35
  24. package/examples/index.mjs +122 -94
  25. package/examples/readme-example.cjs +36 -27
  26. package/examples/readme-example.mjs +21 -17
  27. package/examples/real-user-smoke.cjs +41 -32
  28. package/examples/real-user-smoke.mjs +26 -22
  29. package/exception-chain.cjs +205 -0
  30. package/index.d.ts +144 -1076
  31. package/index.js +1 -1
  32. package/issue-exception.cjs +13 -8
  33. package/metric-validation.cjs +95 -0
  34. package/network-milestone.cjs +155 -0
  35. package/null-value.cjs +4 -0
  36. package/opentelemetry-bridge.cjs +330 -0
  37. package/opentelemetry-context.cjs +245 -0
  38. package/opentelemetry-exceptions.cjs +69 -0
  39. package/opentelemetry-keys.cjs +183 -0
  40. package/opentelemetry-span.cjs +286 -0
  41. package/opentelemetry-summary.cjs +213 -0
  42. package/opentelemetry-time.cjs +131 -0
  43. package/opentelemetry.cjs +99 -1068
  44. package/package.json +55 -1
  45. package/pino-destination.cjs +108 -0
  46. package/pino-record.cjs +237 -0
  47. package/product-action.cjs +163 -0
  48. package/provided-breadcrumbs.cjs +117 -0
  49. package/random-hex.cjs +29 -0
  50. package/react-native.js +1 -1
  51. package/recording-transport.cjs +68 -0
  52. package/release-artifacts-build.cjs +263 -84
  53. package/release-artifacts-common.js +347 -66
  54. package/release-artifacts-debug-id.js +178 -0
  55. package/release-artifacts-inspect.js +213 -0
  56. package/release-artifacts-issue-event.js +99 -0
  57. package/release-artifacts-manifest-artifact.js +107 -0
  58. package/release-artifacts-manifest.js +77 -0
  59. package/release-artifacts-mappings.js +265 -0
  60. package/release-artifacts-prepare.js +297 -0
  61. package/release-artifacts-source-context.js +210 -0
  62. package/release-artifacts-symbolication.js +272 -421
  63. package/release-artifacts-upload-files.js +122 -0
  64. package/release-artifacts-upload-request.js +246 -0
  65. package/release-artifacts-upload.js +155 -300
  66. package/release-artifacts-urls.js +49 -0
  67. package/release-artifacts.js +189 -662
  68. package/retry-delay.cjs +32 -0
  69. package/span-validation.cjs +191 -0
  70. package/stored-events.cjs +243 -0
  71. package/types/client.d.ts +67 -0
  72. package/types/delivery.d.ts +125 -0
  73. package/types/issues.d.ts +249 -0
  74. package/types/logging.d.ts +175 -0
  75. package/types/opentelemetry.d.ts +205 -0
  76. package/types/product.d.ts +128 -0
  77. package/types/recording-transport.d.ts +15 -0
  78. package/types/sdk-error.d.ts +9 -0
  79. package/types/telemetry.d.ts +171 -0
  80. package/types/trace-context.d.ts +110 -0
  81. package/types/transport-error.d.ts +10 -0
  82. package/utf8.cjs +37 -0
  83. package/validation.cjs +49 -1
  84. package/vite-release-artifacts-config.cjs +120 -0
  85. package/vite-release-artifacts.cjs +199 -99
  86. package/vite-release-artifacts.d.cts +65 -40
  87. package/vite-release-artifacts.d.ts +65 -40
  88. package/vite-release-artifacts.js +2 -2
  89. package/winston-info.cjs +205 -0
  90. 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 MAX_SOURCE_CONTEXT_FILE_BYTES = 1024 * 1024;
14
- const VLQ_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
15
- const VLQ_VALUES = new Map([...VLQ_CHARS].map((char, index) => [char, index]));
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
- function parseStackFrame(stackFrame) {
18
- let line = stackFrame.trim();
19
- if (line.startsWith("at ")) {
20
- line = line.slice(3).trim();
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
- let functionName = null;
23
- let location = line;
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 < 3) {
91
+ if (parts.length < LOCATION_PARTS) {
31
92
  throw new Error("stack frame must end with :line:column");
32
93
  }
33
- const columnText = parts.pop();
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
- filename,
46
- line: generatedLine,
47
- column: generatedColumn,
48
- ...(functionName ? { function: functionName } : {})
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
- let normalized = fileReference(value.trim());
54
- if (normalized.startsWith("file://")) {
55
- normalized = normalized.slice("file://".length);
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
- return normalized;
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
- function artifactMatchesFrame(artifact, frame, buildDir) {
61
- if (!artifact.minifiedSource || typeof artifact.minifiedSource !== "object") {
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 = String(artifact.minifiedSource.path ?? "");
65
- const artifactUrl = String(artifact.minifiedSource.minifiedUrl ?? "");
66
- const normalizedFrame = normalizeReference(String(frame.filename));
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.replace(/^\/+|\/+$/gu, "") === artifactPath.replace(/^\/+|\/+$/gu, "")) {
182
+ if (normalizedFrame.replaceAll(EDGE_SLASHES_RE, "") === artifactPath.replaceAll(EDGE_SLASHES_RE, "")) {
71
183
  return true;
72
184
  }
73
- if (path.isAbsolute(normalizedFrame)) {
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
- if (manifest.artifactType !== "javascript_source_map_manifest") {
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.validation?.status !== "ready") {
197
+ if (objectField(manifest["validation"], "status") !== "ready") {
87
198
  throw new Error("manifest validation status must be ready");
88
199
  }
89
- if (!Array.isArray(manifest.artifacts) || manifest.artifacts.length === 0) {
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
- for (const artifact of manifest.artifacts) {
93
- if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
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.validation?.status !== "ready") {
209
+ if (objectField(artifact["validation"], "status") !== "ready") {
97
210
  throw new Error("all artifact validation statuses must be ready");
98
211
  }
99
- }
100
- return manifest.artifacts;
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.debugId;
115
- return typeof candidateDebugId === "string" && candidateDebugId.toLowerCase() === normalizedDebugId;
223
+ const candidateDebugId = candidate["debugId"];
224
+ return typeof candidateDebugId === "string" && candidateDebugId.toLowerCase() === debugId;
116
225
  });
117
- if (matches.length === 0) {
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 > 1) {
230
+ if (matches.length > ONE_MATCH) {
121
231
  throw new Error("multiple manifest artifacts match the issue event debug ID");
122
232
  }
123
- return matches[0];
233
+ return match;
124
234
  }
125
235
 
126
- function loadManifestSourceMap(artifact, buildDir) {
127
- if (!artifact.sourceMap || typeof artifact.sourceMap !== "object" || !artifact.sourceMap.path) {
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 sourceMapPath = safeResolve(path.join(buildDir, String(artifact.sourceMap.path)), buildDir);
131
- if (!sourceMapPath) {
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
- const payload = readJsonObject(sourceMapPath, "source map");
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.debugId;
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
- function safeOriginalSourceForReport(source) {
217
- if (typeof source !== "string") {
218
- throw new Error("source map segment references an invalid source value");
219
- }
220
- const value = fileReference(source.trim());
221
- if (!value) {
222
- throw new Error("source map segment references an invalid source value");
223
- }
224
- if (value.startsWith("file://") || path.isAbsolute(value) || /^[A-Za-z]:[\\/]/u.test(value)) {
225
- throw new Error("source map source path must be stripped before symbolication proof");
226
- }
227
- return value;
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
- const codeFile = stringOrNull(metadata.releaseArtifactCodeFile) ?? stringOrNull(metadata.errorFrameFile);
440
- if (!codeFile) {
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
- const line = requireMetadataPositiveInteger(metadata, "errorFrameLine");
444
- const column = requireMetadataPositiveInteger(metadata, "errorFrameColumn");
445
- return `${codeFile}:${line}:${column}`;
287
+ return { path: sourceMapPath, payload: { mappings, names, sources } };
446
288
  }
447
289
 
448
- function symbolicationReportForArtifact({ buildDir, manifest, frame, artifact, sourceContext }) {
449
- const sourceMap = loadManifestSourceMap(artifact, buildDir);
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(original, {
452
- ...sourceContext,
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
- status: "resolved",
457
- verifier: { name: "logbrew-js-release-artifact-symbolication-verifier", version: SCRIPT_VERSION },
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
- sourceMap: {
470
- path: artifact.sourceMap.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
- ...(resolvedSourceContext ? { sourceContext: resolvedSourceContext } : {})
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
- export function verifyJavaScriptSymbolication({ buildDir, manifest, stackFrame, sourceContext }) {
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
- return symbolicationReportForArtifact({
481
- buildDir,
482
- manifest,
483
- frame,
484
- artifact: findMatchingArtifact(manifest, frame, buildDir),
485
- sourceContext
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
- export function verifyJavaScriptIssueSymbolication({ buildDir, manifest, issueEvent, sourceContext }) {
490
- const { metadata, input } = metadataFromIssueEvent(issueEvent);
491
- requireMetadataMatchesManifest(metadata, manifest, "release");
492
- requireMetadataMatchesManifest(metadata, manifest, "environment");
493
- requireMetadataMatchesManifest(metadata, manifest, "service");
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
- artifact,
352
+ manifest,
502
353
  sourceContext
503
354
  });
504
- if (typeof report.debugId !== "string" || report.debugId.toLowerCase() !== expectedDebugId) {
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 };