@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,40 +1,40 @@
1
1
  #!/usr/bin/env node
2
- import { Buffer } from "node:buffer";
3
- import crypto from "node:crypto";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
-
7
2
  import {
8
- byteSize,
9
- fileReference,
3
+ errorMessage,
10
4
  normalizeProjectId,
11
5
  parseOptions,
6
+ pathExists,
12
7
  printJson,
13
8
  readJsonObject,
14
- relativeTo,
15
- requireOption,
16
9
  requireBuildDir,
17
- safeResolve,
18
- sha256File,
19
- sourceMapDebugId,
20
- SOURCE_MAP_DEBUG_ID_KEYS,
21
- sortJson,
22
- stableJson
10
+ requireOption
23
11
  } from "./release-artifacts-common.js";
24
- import { runUploadJs } from "./release-artifacts-upload.js";
25
12
  import { verifyJavaScriptIssueSymbolication, verifyJavaScriptSymbolication } from "./release-artifacts-symbolication.js";
13
+ import { createDebugIdPlan } from "./release-artifacts-prepare.js";
14
+ import { createManifest } from "./release-artifacts-manifest.js";
15
+ import path from "node:path";
16
+ import { runUploadJs } from "./release-artifacts-upload.js";
26
17
 
27
- const DEBUG_ID_NAMESPACE = "16f4a837-7e0b-4d7c-97d9-8a7af1fd2768";
28
- const DEBUG_ID_RE = /(?:\/\/#|\/\*#)\s*debugId=([A-Za-z0-9._:-]+)/giu;
29
- const DEBUG_ID_REGISTRY_NAME = "logbrew.release-artifact.debug-ids";
30
- const DEBUG_ID_REGISTRY_EXPRESSION = `Symbol.for(${JSON.stringify(DEBUG_ID_REGISTRY_NAME)})`;
31
- const HERMES_RUNTIME_DEBUG_ID_RE = /\/\*logbrew-runtime-debug-id\*\/\/\/# debugId=([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12})/iu;
32
- const RUNTIME_DEBUG_ID_MARKER = "/*logbrew-runtime-debug-id*/";
33
- const MINIFIED_SOURCE_SUFFIXES = [".js", ".mjs", ".bundle", ".jsbundle"];
34
- const SCRIPT_VERSION = "0.1.0";
35
- const SOURCE_MAPPING_COMMENT_RE = /(?:\/\/#|\/\*#)\s*sourceMappingURL=[^\r\n]*/giu;
36
- const SOURCE_MAPPING_RE = /(?:\/\/#|\/\*#)\s*sourceMappingURL=([^\s*]+)/iu;
18
+ /**
19
+ * @import { CliOptions } from "./release-artifacts-common.js"
20
+ * @import { SourceContextOptions } from "./release-artifacts-source-context.js"
21
+ */
37
22
 
23
+ const SCRIPT_VERSION = "0.1.0";
24
+ const DEFAULT_CONTEXT_LINES = 2;
25
+ const MAX_CONTEXT_LINES = 10;
26
+ const MIN_CONTEXT_LINES = 0;
27
+ const DECIMAL = 10;
28
+ const EXIT_OK = 0;
29
+ const EXIT_BLOCKED = 1;
30
+ const EXIT_USAGE = 2;
31
+ const ARGUMENT_OFFSET = 2;
32
+ const NON_NEGATIVE_INTEGER_RE = /^\d+$/u;
33
+ const VERIFIER = { name: "logbrew-js-release-artifact-symbolication-verifier", version: SCRIPT_VERSION };
34
+
35
+ /**
36
+ * @returns {string}
37
+ */
38
38
  function usage() {
39
39
  return [
40
40
  "Usage:",
@@ -48,691 +48,218 @@ function usage() {
48
48
  ].join("\n");
49
49
  }
50
50
 
51
- function optionalSourceContextOptions(options) {
52
- const hasSourceRoot = typeof options["source-root"] === "string" && options["source-root"].trim() !== "";
53
- const hasContextLines = typeof options["context-lines"] === "string" && options["context-lines"].trim() !== "";
54
- if (!hasSourceRoot && !hasContextLines) {
55
- return undefined;
56
- }
57
- if (!hasSourceRoot) {
58
- throw new Error("--source-root is required when --context-lines is provided");
59
- }
60
- let contextLines = 2;
61
- if (hasContextLines) {
62
- const value = options["context-lines"].trim();
63
- if (!/^\d+$/u.test(value)) {
64
- throw new Error("--context-lines must be an integer from 0 to 10");
65
- }
66
- contextLines = Number.parseInt(value, 10);
67
- if (contextLines < 0 || contextLines > 10) {
68
- throw new Error("--context-lines must be an integer from 0 to 10");
69
- }
70
- }
71
- return {
72
- sourceRoot: path.resolve(requireOption(options, "source-root")),
73
- contextLines
74
- };
75
- }
76
-
77
- function readText(filePath) {
78
- return fs.readFileSync(filePath, "utf8");
79
- }
80
-
81
- function writeText(filePath, value) {
82
- fs.writeFileSync(filePath, value, "utf8");
83
- }
84
-
85
- function readSourceMap(filePath) {
86
- let payload;
87
- try {
88
- payload = JSON.parse(readText(filePath));
89
- } catch (error) {
90
- return [null, [`source map is not valid JSON: ${error.message}`]];
91
- }
92
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
93
- return [null, ["source map must be a JSON object"]];
94
- }
95
- return [payload, []];
96
- }
97
-
98
- function findLastMatch(source, regex) {
99
- let result = null;
100
- regex.lastIndex = 0;
101
- for (const match of source.matchAll(regex)) {
102
- result = match[1]?.trim() ?? null;
103
- }
104
- return result;
105
- }
106
-
107
- function findDebugId(source) {
108
- const runtimeDebugId = source.match(HERMES_RUNTIME_DEBUG_ID_RE)?.[1];
109
- return runtimeDebugId ?? findLastMatch(source, DEBUG_ID_RE);
110
- }
111
-
112
- function findSourceMappingUrl(source) {
113
- const match = source.match(SOURCE_MAPPING_RE);
114
- return match?.[1]?.trim() ?? null;
115
- }
116
-
117
- function resolveSourceMapPath(jsPath, buildDir, sourceMappingUrl) {
118
- const warnings = [];
119
- const errors = [];
120
- if (!sourceMappingUrl) {
121
- const fallback = `${jsPath}.map`;
122
- warnings.push("sourceMappingURL comment missing; checked sibling .map fallback");
123
- return [fs.existsSync(fallback) ? fallback : null, warnings, errors];
124
- }
125
-
126
- const reference = fileReference(sourceMappingUrl);
127
- if (reference.startsWith("data:")) {
128
- errors.push("inline source maps are not accepted for release artifact manifests");
129
- return [null, warnings, errors];
130
- }
131
- if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(reference)) {
132
- errors.push("external sourceMappingURL cannot be validated from the local build directory");
133
- return [null, warnings, errors];
134
- }
135
-
136
- const candidate = reference.startsWith("/")
137
- ? path.join(buildDir, reference.slice(1))
138
- : path.join(path.dirname(jsPath), reference);
139
- const resolved = safeResolve(candidate, buildDir);
140
- if (!resolved) {
141
- errors.push("sourceMappingURL resolves outside the build directory");
142
- }
143
- return [resolved, warnings, errors];
144
- }
145
-
146
- function isMinifiedSource(filePath) {
147
- return !filePath.endsWith(".map") && MINIFIED_SOURCE_SUFFIXES.some((suffix) => filePath.endsWith(suffix));
148
- }
149
-
150
- function walkFiles(root) {
151
- const results = [];
152
- for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
153
- const entryPath = path.join(root, entry.name);
154
- if (entry.isDirectory()) {
155
- results.push(...walkFiles(entryPath));
156
- } else if (entry.isFile()) {
157
- results.push(entryPath);
158
- }
159
- }
160
- return results.sort();
161
- }
162
-
163
- function iterMinifiedSourceFiles(buildDir) {
164
- return walkFiles(buildDir).filter(isMinifiedSource);
165
- }
166
-
167
- function canonicalSourceWithoutDebugId(source) {
168
- return source.replace(DEBUG_ID_RE, "");
169
- }
170
-
171
- function canonicalSourceMapWithoutDebugId(payload) {
172
- const copy = { ...payload };
173
- for (const key of SOURCE_MAP_DEBUG_ID_KEYS) {
174
- delete copy[key];
175
- }
176
- return stableJson(copy);
177
- }
178
-
179
- function uuidBytes(value) {
180
- return Buffer.from(value.replaceAll("-", ""), "hex");
181
- }
182
-
183
- function formatUuid(bytes) {
184
- const hex = Buffer.from(bytes).toString("hex");
185
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
186
- }
187
-
188
- function uuidV5(namespace, name) {
189
- const bytes = crypto
190
- .createHash("sha1")
191
- .update(uuidBytes(namespace))
192
- .update(Buffer.from(name, "utf8"))
193
- .digest()
194
- .subarray(0, 16);
195
- bytes[6] = (bytes[6] & 0x0f) | 0x50;
196
- bytes[8] = (bytes[8] & 0x3f) | 0x80;
197
- return formatUuid(bytes);
198
- }
199
-
200
- function generateDebugId(relativeJsPath, jsSource, sourceMapPayload) {
201
- const digest = crypto.createHash("sha256");
202
- digest.update(relativeJsPath);
203
- digest.update("\0");
204
- digest.update(canonicalSourceWithoutDebugId(jsSource));
205
- digest.update("\0");
206
- digest.update(canonicalSourceMapWithoutDebugId(sourceMapPayload));
207
- return uuidV5(DEBUG_ID_NAMESPACE, digest.digest("hex"));
208
- }
209
-
210
- function executableShebang(source) {
211
- return source.startsWith("#!") ? source.match(/^#![^\r\n]*(?:\r?\n|$)/u)?.[0] ?? "" : "";
212
- }
213
-
214
- function sourceStartsWithDirective(source, shebang) {
215
- const body = source.slice(shebang.length).replace(/^(?:\s|\/\/[^\r\n]*(?:\r?\n|$)|\/\*[\s\S]*?\*\/)+/u, "");
216
- return body.startsWith("\"") || body.startsWith("'");
217
- }
218
-
219
- function sourceWithDebugId(source, debugId, relativePath) {
220
- const registryLine = `;try{${RUNTIME_DEBUG_ID_MARKER}(function(){var g=globalThis,s=${DEBUG_ID_REGISTRY_EXPRESSION},r=g[s];if(!r||typeof r!=="object")r=g[s]=Object.create(null);r[${JSON.stringify(relativePath)}]=${JSON.stringify(debugId)}})()}catch(_){}\n`;
221
- const shebang = executableShebang(source);
222
- const prepared = source.includes(RUNTIME_DEBUG_ID_MARKER)
223
- ? source
224
- : `${shebang}${shebang === source && shebang ? "\n" : ""}${registryLine}${source.slice(shebang.length)}`;
225
- if (findDebugId(prepared)) {
226
- return prepared;
227
- }
228
- const debugLine = `//# debugId=${debugId}\n`;
229
- const matches = [...prepared.matchAll(SOURCE_MAPPING_COMMENT_RE)];
230
- if (matches.length === 0) {
231
- return `${prepared}${prepared.endsWith("\n") ? "" : "\n"}${debugLine}`;
232
- }
233
- const last = matches.at(-1);
234
- const prefix = prepared.slice(0, last.index);
235
- const separator = prefix.endsWith("\n") || prefix.endsWith("\r") ? "" : "\n";
236
- return `${prefix}${separator}${debugLine}${prepared.slice(last.index)}`;
237
- }
238
-
239
- function normalizeSourcePrefixes(values) {
240
- const prefixes = [];
241
- for (const value of values ?? []) {
242
- for (const candidate of [path.resolve(value), fs.existsSync(value) ? fs.realpathSync(value) : path.resolve(value)]) {
243
- const normalized = candidate.split(path.sep).join("/").replace(/\/+$/u, "");
244
- if (normalized && !prefixes.includes(normalized)) {
245
- prefixes.push(normalized);
246
- }
247
- }
248
- }
249
- return prefixes;
250
- }
251
-
252
- function sourceWithoutPrefix(source, prefixes) {
253
- const normalized = source.replaceAll("\\", "/");
254
- for (const prefix of prefixes) {
255
- const marker = `${prefix}/`;
256
- if (normalized === prefix) {
257
- return path.posix.basename(prefix);
258
- }
259
- if (normalized.startsWith(marker)) {
260
- return normalized.slice(marker.length);
261
- }
262
- }
263
- return source;
264
- }
265
-
266
- function sourceMapSourcesWithoutPrefixes(payload, prefixes) {
267
- if (prefixes.length === 0 || !Array.isArray(payload.sources)) {
268
- return null;
269
- }
270
- const updated = payload.sources.map((source) => (typeof source === "string" ? sourceWithoutPrefix(source, prefixes) : source));
271
- return JSON.stringify(updated) === JSON.stringify(payload.sources) ? null : updated;
272
- }
273
-
274
- function sourceMapPayloadForDebugId(payload, { stripSourcesContent, sourcePrefixes }) {
275
- const updatedSources = sourceMapSourcesWithoutPrefixes(payload, sourcePrefixes);
276
- if (!stripSourcesContent && !updatedSources) {
277
- return payload;
278
- }
279
- const updated = updatedSources ? { ...payload, sources: updatedSources } : { ...payload };
280
- if (stripSourcesContent) {
281
- delete updated.sourcesContent;
282
- }
283
- return updated;
284
- }
285
-
286
- function sourceMapWithPrivacyUpdates(payload, debugId, options) {
287
- const privacyUpdated = sourceMapPayloadForDebugId(payload, options);
288
- const { shiftGeneratedLines } = options;
289
- if (sourceMapDebugId(payload)
290
- && privacyUpdated === payload
291
- && !shiftGeneratedLines) {
292
- return payload;
293
- }
294
- const updated = { ...privacyUpdated };
295
- if (!sourceMapDebugId(updated)) {
296
- updated.debug_id = debugId;
297
- }
298
- if (shiftGeneratedLines && typeof updated.mappings === "string") {
299
- updated.mappings = `;${updated.mappings}`;
300
- }
301
- return updated;
302
- }
303
-
304
- function inspectArtifactFiles(jsPath, buildDir) {
305
- const errors = [];
306
- const warnings = [];
307
- const relJs = relativeTo(buildDir, jsPath);
308
- const jsSize = byteSize(jsPath);
309
-
310
- if (jsSize === 0) {
311
- errors.push("minified source file is empty");
312
- }
313
-
314
- const jsSource = readText(jsPath);
315
- const jsDebugId = findDebugId(jsSource);
316
- const sourceMappingUrl = findSourceMappingUrl(jsSource);
317
- const [sourceMapPath, mapWarnings, mapErrors] = resolveSourceMapPath(jsPath, buildDir, sourceMappingUrl);
318
- warnings.push(...mapWarnings);
319
- errors.push(...mapErrors);
320
-
321
- let sourceMapPayload = null;
322
- let mapDebugId = null;
323
- let sourceMapRel = null;
324
- let sourceMapSize = null;
325
- if (!sourceMapPath) {
326
- errors.push("source map file is missing");
327
- } else if (!fs.existsSync(sourceMapPath)) {
328
- errors.push(`source map file is missing: ${relativeTo(buildDir, sourceMapPath)}`);
329
- } else if (byteSize(sourceMapPath) === 0) {
330
- errors.push(`source map file is empty: ${relativeTo(buildDir, sourceMapPath)}`);
331
- } else {
332
- sourceMapRel = relativeTo(buildDir, sourceMapPath);
333
- sourceMapSize = byteSize(sourceMapPath);
334
- const [payload, sourceMapErrors] = readSourceMap(sourceMapPath);
335
- errors.push(...sourceMapErrors);
336
- if (payload) {
337
- sourceMapPayload = payload;
338
- mapDebugId = sourceMapDebugId(payload);
339
- }
340
- }
341
-
342
- if (jsDebugId && mapDebugId && jsDebugId !== mapDebugId) {
343
- errors.push("minified source debugId does not match source map debugId");
344
- }
345
-
346
- return {
347
- errors,
348
- warnings,
349
- relJs,
350
- jsSize,
351
- jsSource,
352
- jsDebugId,
353
- sourceMappingUrl,
354
- sourceMapPath,
355
- sourceMapPayload,
356
- sourceMapRel,
357
- sourceMapSize,
358
- mapDebugId
359
- };
360
- }
361
-
362
- function buildArtifactPlan(jsPath, buildDir, options) {
363
- const {
364
- errors,
365
- warnings,
366
- relJs,
367
- jsSource,
368
- jsDebugId,
369
- sourceMapPayload,
370
- sourceMapRel,
371
- mapDebugId
372
- } = inspectArtifactFiles(jsPath, buildDir);
373
- const changes = [];
374
- let debugId = jsDebugId || mapDebugId;
375
- const needsRuntimeDebugId = !jsSource.includes(RUNTIME_DEBUG_ID_MARKER);
376
- if (needsRuntimeDebugId && sourceStartsWithDirective(jsSource, executableShebang(jsSource))) {
377
- errors.push("runtime Debug ID injection cannot precede a script directive");
378
- }
379
- if (needsRuntimeDebugId
380
- && sourceMapPayload
381
- && (typeof sourceMapPayload.mappings !== "string" || sourceMapPayload.mappings === "")) {
382
- errors.push("source map mappings must be a non-empty string before runtime Debug ID injection");
383
- }
384
- if (errors.length === 0 && sourceMapPayload) {
385
- if (needsRuntimeDebugId) {
386
- changes.push("minifiedSource.runtimeDebugId");
387
- }
388
- if (!debugId) {
389
- debugId = generateDebugId(
390
- relJs,
391
- jsSource,
392
- sourceMapPayloadForDebugId(sourceMapPayload, options)
393
- );
394
- changes.push("minifiedSource.debugId", "sourceMap.debug_id");
395
- } else {
396
- if (!jsDebugId) {
397
- changes.push("minifiedSource.debugId");
398
- }
399
- if (!mapDebugId) {
400
- changes.push("sourceMap.debug_id");
401
- }
402
- }
403
- if (options.stripSourcesContent && "sourcesContent" in sourceMapPayload) {
404
- changes.push("sourceMap.sourcesContent");
405
- }
406
- if (sourceMapSourcesWithoutPrefixes(sourceMapPayload, options.sourcePrefixes)) {
407
- changes.push("sourceMap.sources");
408
- }
409
- }
410
-
411
- return {
412
- path: relJs,
413
- ...(sourceMapRel ? { sourceMapPath: sourceMapRel } : {}),
414
- ...(debugId ? { debugId } : {}),
415
- changes,
416
- validation: {
417
- status: errors.length > 0 ? "blocked" : "ready",
418
- errors,
419
- warnings
420
- }
421
- };
422
- }
423
-
424
- function applyArtifactPlan(artifact, buildDir, options) {
425
- const debugId = artifact.debugId;
426
- const jsPath = path.join(buildDir, artifact.path);
427
- const sourceMapPath = path.join(buildDir, artifact.sourceMapPath);
428
- const jsSource = readText(jsPath);
429
- const shiftGeneratedLines = !jsSource.includes(RUNTIME_DEBUG_ID_MARKER);
430
- const updatedSource = sourceWithDebugId(jsSource, debugId, artifact.path);
431
- if (updatedSource !== jsSource) {
432
- writeText(jsPath, updatedSource);
433
- }
434
- const [payload, errors] = readSourceMap(sourceMapPath);
435
- if (!payload || errors.length > 0) {
436
- throw new Error(`${artifact.path}: source map became unreadable before write`);
437
- }
438
- const updatedPayload = sourceMapWithPrivacyUpdates(payload, debugId, {
439
- ...options,
440
- shiftGeneratedLines
441
- });
442
- if (stableJson(updatedPayload) !== stableJson(payload)) {
443
- writeText(sourceMapPath, `${JSON.stringify(sortJson(updatedPayload), null, 2)}\n`);
444
- }
445
- }
446
-
447
- function createDebugIdPlan({ buildDir, write, stripSourcesContent, stripSourcePrefixes }) {
448
- const sourcePrefixes = normalizeSourcePrefixes(stripSourcePrefixes);
449
- const artifactOptions = { stripSourcesContent, sourcePrefixes };
450
- const artifacts = iterMinifiedSourceFiles(buildDir).map((filePath) => buildArtifactPlan(filePath, buildDir, artifactOptions));
451
- const errors = artifacts.length === 0 ? ["no JavaScript release artifact files found in build directory"] : [];
452
- const warnings = [];
453
- for (const artifact of artifacts) {
454
- errors.push(...artifact.validation.errors.map((message) => `${artifact.path}: ${message}`));
455
- warnings.push(...artifact.validation.warnings.map((message) => `${artifact.path}: ${message}`));
456
- }
457
- const status = errors.length > 0 ? "blocked" : "ready";
458
- if (write && status === "ready") {
459
- for (const artifact of artifacts) {
460
- applyArtifactPlan(artifact, buildDir, artifactOptions);
461
- }
462
- }
463
- return {
464
- manifestVersion: 1,
465
- tool: { name: "logbrew-js-release-artifact-debug-id-prep", version: SCRIPT_VERSION },
466
- stripSourcesContent,
467
- stripSourcePrefixCount: stripSourcePrefixes?.length ?? 0,
468
- writeApplied: Boolean(write && status === "ready"),
469
- artifacts,
470
- validation: { status, errors, warnings }
471
- };
51
+ /**
52
+ * @param {CliOptions} options
53
+ * @param {string} name
54
+ * @returns {string | undefined}
55
+ */
56
+ function optionalOption(options, name) {
57
+ const value = options[name];
58
+ return typeof value === "string" ? value : undefined;
472
59
  }
473
60
 
474
- function validateSourceMapPayload(payload, allowSourcesContent) {
475
- const errors = [];
476
- const warnings = [];
477
- if (payload.version === undefined) {
478
- errors.push("source map version is required");
479
- }
480
- if (!Array.isArray(payload.sources) || payload.sources.length === 0) {
481
- errors.push("source map sources must be a non-empty array");
482
- }
483
- if (typeof payload.mappings !== "string" || payload.mappings === "") {
484
- errors.push("source map mappings must be a non-empty string");
485
- }
486
- if ("sourcesContent" in payload) {
487
- if (allowSourcesContent) {
488
- warnings.push("source map contains sourcesContent; ensure app policy permits source upload");
489
- } else {
490
- errors.push("source map contains sourcesContent; rerun with --allow-sources-content only if policy permits it");
491
- }
492
- }
493
- return [errors, warnings];
61
+ /**
62
+ * @param {CliOptions} options
63
+ * @param {string} name
64
+ * @returns {boolean}
65
+ */
66
+ function hasOption(options, name) {
67
+ const value = options[name];
68
+ return typeof value === "string" && value.trim() !== "";
494
69
  }
495
70
 
496
- function normalizeUrlOrPath(value) {
71
+ /**
72
+ * @param {string} value
73
+ * @returns {number}
74
+ */
75
+ function parseContextLines(value) {
497
76
  const trimmed = value.trim();
498
- if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(trimmed)) {
499
- const parsed = new URL(trimmed);
500
- const normalizedPath = path.posix.normalize(parsed.pathname || "/").replace(/\/+$/u, "") || "/";
501
- return parsed.host
502
- ? `${parsed.protocol}//${parsed.host}${normalizedPath}`
503
- : `${parsed.protocol}//${normalizedPath}`;
504
- }
505
- return trimmed.split("?", 1)[0].split("#", 1)[0].trim().replace(/\/+$/u, "");
506
- }
507
-
508
- function joinUrlOrPath(prefix, relativePath) {
509
- const normalizedPrefix = normalizeUrlOrPath(prefix);
510
- const normalizedRelative = relativePath.replaceAll("\\", "/").replace(/^\/+/u, "");
511
- if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//u.test(normalizedPrefix)) {
512
- const parsed = new URL(normalizedPrefix);
513
- const joinedPath = path.posix.join(parsed.pathname.replace(/\/+$/u, ""), normalizedRelative);
514
- const finalPath = joinedPath.startsWith("/") ? joinedPath : `/${joinedPath}`;
515
- return parsed.host
516
- ? `${parsed.protocol}//${parsed.host}${finalPath}`
517
- : `${parsed.protocol}//${finalPath}`;
518
- }
519
- return normalizedPrefix ? `${normalizedPrefix.replace(/\/+$/u, "")}/${normalizedRelative}` : normalizedRelative;
520
- }
521
-
522
- function buildManifestArtifact(jsPath, buildDir, minifiedPathPrefix, allowSourcesContent) {
523
- const {
524
- errors,
525
- warnings,
526
- relJs,
527
- jsSize,
528
- jsDebugId,
529
- sourceMappingUrl,
530
- sourceMapPath,
531
- sourceMapPayload,
532
- sourceMapRel,
533
- sourceMapSize,
534
- mapDebugId
535
- } = inspectArtifactFiles(jsPath, buildDir);
536
- let sourceMapEntry = null;
537
- if (sourceMapPayload && sourceMapPath && sourceMapRel) {
538
- const [payloadErrors, payloadWarnings] = validateSourceMapPayload(sourceMapPayload, allowSourcesContent);
539
- errors.push(...payloadErrors);
540
- warnings.push(...payloadWarnings);
541
- sourceMapEntry = {
542
- path: sourceMapRel,
543
- artifactSha256: sha256File(sourceMapPath),
544
- byteSize: sourceMapSize,
545
- sourceCount: Array.isArray(sourceMapPayload.sources) ? sourceMapPayload.sources.length : 0,
546
- hasSourcesContent: "sourcesContent" in sourceMapPayload,
547
- ...(mapDebugId ? { debugId: mapDebugId } : {})
548
- };
77
+ if (!NON_NEGATIVE_INTEGER_RE.test(trimmed)) {
78
+ throw new Error("--context-lines must be an integer from 0 to 10");
549
79
  }
550
- if (!jsDebugId && !mapDebugId) {
551
- warnings.push("no debugId found; backend matching must rely on release/environment/service and minified path");
80
+ const contextLines = Number.parseInt(trimmed, DECIMAL);
81
+ if (contextLines < MIN_CONTEXT_LINES || contextLines > MAX_CONTEXT_LINES) {
82
+ throw new Error("--context-lines must be an integer from 0 to 10");
552
83
  }
553
-
554
- const debugId = jsDebugId || mapDebugId;
555
- return {
556
- artifactType: "javascript_source_map",
557
- ...(debugId ? { debugId } : {}),
558
- minifiedSource: {
559
- path: relJs,
560
- minifiedUrl: joinUrlOrPath(minifiedPathPrefix, relJs),
561
- artifactSha256: sha256File(jsPath),
562
- byteSize: jsSize,
563
- ...(jsDebugId ? { debugId: jsDebugId } : {}),
564
- ...(sourceMappingUrl ? { sourceMappingUrl } : {})
565
- },
566
- sourceMap: sourceMapEntry,
567
- validation: {
568
- status: errors.length > 0 ? "blocked" : "ready",
569
- errors,
570
- warnings
571
- }
572
- };
84
+ return contextLines;
573
85
  }
574
86
 
575
- function createManifest({ buildDir, projectId, release, environment, service, minifiedPathPrefix, allowSourcesContent, repositoryUrl, commitSha }) {
576
- const normalizedPrefix = normalizeUrlOrPath(minifiedPathPrefix.trim());
577
- const artifacts = iterMinifiedSourceFiles(buildDir).map((filePath) =>
578
- buildManifestArtifact(filePath, buildDir, normalizedPrefix, allowSourcesContent)
579
- );
580
- const errors = artifacts.length === 0 ? ["no JavaScript release artifact files found in build directory"] : [];
581
- const warnings = [];
582
- for (const artifact of artifacts) {
583
- const relPath = artifact.minifiedSource.path;
584
- errors.push(...artifact.validation.errors.map((message) => `${relPath}: ${message}`));
585
- warnings.push(...artifact.validation.warnings.map((message) => `${relPath}: ${message}`));
586
- }
587
- const git = {};
588
- if (repositoryUrl) {
589
- git.repositoryUrl = repositoryUrl.trim();
87
+ /**
88
+ * @param {CliOptions} options
89
+ * @returns {SourceContextOptions | undefined}
90
+ */
91
+ function optionalSourceContextOptions(options) {
92
+ const hasSourceRoot = hasOption(options, "source-root");
93
+ const contextLinesOption = optionalOption(options, "context-lines");
94
+ const hasContextLines = hasOption(options, "context-lines");
95
+ if (!hasSourceRoot && !hasContextLines) {
96
+ return undefined;
590
97
  }
591
- if (commitSha) {
592
- git.commitSha = commitSha.trim();
98
+ if (!hasSourceRoot) {
99
+ throw new Error("--source-root is required when --context-lines is provided");
593
100
  }
594
101
  return {
595
- manifestVersion: 1,
596
- ...(projectId ? { projectId } : {}),
597
- release,
598
- environment,
599
- service,
600
- artifactType: "javascript_source_map_manifest",
601
- minifiedPathPrefix: normalizedPrefix,
602
- uploader: { name: "logbrew-js-release-artifact-manifest", version: SCRIPT_VERSION },
603
- ...(Object.keys(git).length > 0 ? { git } : {}),
604
- artifacts,
605
- validation: {
606
- status: errors.length > 0 ? "blocked" : "ready",
607
- errors,
608
- warnings
609
- }
102
+ contextLines: hasContextLines ? parseContextLines(contextLinesOption ?? "") : DEFAULT_CONTEXT_LINES,
103
+ sourceRoot: path.resolve(requireOption(options, "source-root"))
610
104
  };
611
105
  }
612
106
 
613
- function runPrepareJs(args) {
107
+ /**
108
+ * @param {readonly string[]} args
109
+ * @returns {Promise<number>}
110
+ */
111
+ async function runPrepareJs(args) {
614
112
  const options = parseOptions(args, {
615
113
  "build-dir": "string",
616
- write: "boolean",
114
+ "strip-source-prefix": "repeat",
617
115
  "strip-sources-content": "boolean",
618
- "strip-source-prefix": "repeat"
116
+ write: "boolean"
619
117
  });
620
- const buildDir = requireBuildDir(requireOption(options, "build-dir"));
621
- const plan = createDebugIdPlan({
118
+ const buildDir = await requireBuildDir(requireOption(options, "build-dir"));
119
+ const stripSourcePrefixes = options["strip-source-prefix"];
120
+ const plan = await createDebugIdPlan({
622
121
  buildDir,
623
- write: Boolean(options.write),
624
- stripSourcesContent: Boolean(options["strip-sources-content"]),
625
- stripSourcePrefixes: options["strip-source-prefix"] ?? []
122
+ stripSourcePrefixes: Array.isArray(stripSourcePrefixes) ? stripSourcePrefixes : [],
123
+ stripSourcesContent: options["strip-sources-content"] === true,
124
+ write: options["write"] === true
626
125
  });
627
126
  printJson(plan);
628
- return plan.validation.status === "blocked" ? 1 : 0;
127
+ return plan.validation.status === "blocked" ? EXIT_BLOCKED : EXIT_OK;
629
128
  }
630
129
 
631
- function runManifestJs(args) {
130
+ /**
131
+ * @param {readonly string[]} args
132
+ * @returns {Promise<number>}
133
+ */
134
+ async function runManifestJs(args) {
632
135
  const options = parseOptions(args, {
136
+ "allow-sources-content": "boolean",
633
137
  "build-dir": "string",
634
- "project-id": "string",
635
- release: "string",
138
+ "commit-sha": "string",
636
139
  environment: "string",
637
- service: "string",
638
140
  "minified-path-prefix": "string",
141
+ "project-id": "string",
142
+ release: "string",
639
143
  "repository-url": "string",
640
- "commit-sha": "string",
641
- "allow-sources-content": "boolean"
144
+ service: "string"
642
145
  });
643
- const manifest = createManifest({
644
- buildDir: requireBuildDir(requireOption(options, "build-dir")),
645
- projectId: normalizeProjectId(options["project-id"], "--project-id must be a UUID"),
646
- release: requireOption(options, "release"),
647
- environment: requireOption(options, "environment"),
648
- service: requireOption(options, "service"),
649
- minifiedPathPrefix: requireOption(options, "minified-path-prefix"),
650
- allowSourcesContent: Boolean(options["allow-sources-content"]),
651
- repositoryUrl: options["repository-url"],
652
- commitSha: options["commit-sha"]
146
+ const buildDir = await requireBuildDir(requireOption(options, "build-dir"));
147
+ const projectId = normalizeProjectId(options["project-id"], "--project-id must be a UUID");
148
+ const release = requireOption(options, "release");
149
+ const environment = requireOption(options, "environment");
150
+ const service = requireOption(options, "service");
151
+ const minifiedPathPrefix = requireOption(options, "minified-path-prefix");
152
+ const manifest = await createManifest({
153
+ allowSourcesContent: options["allow-sources-content"] === true,
154
+ buildDir,
155
+ commitSha: optionalOption(options, "commit-sha"),
156
+ environment,
157
+ minifiedPathPrefix,
158
+ projectId,
159
+ release,
160
+ repositoryUrl: optionalOption(options, "repository-url"),
161
+ service
653
162
  });
654
163
  printJson(manifest);
655
- return manifest.validation.status === "blocked" ? 1 : 0;
164
+ return manifest.validation.status === "blocked" ? EXIT_BLOCKED : EXIT_OK;
165
+ }
166
+
167
+ /**
168
+ * @param {CliOptions} options
169
+ * @param {string} name
170
+ * @param {string} label
171
+ * @returns {Promise<string>}
172
+ */
173
+ async function requireExistingFile(options, name, label) {
174
+ const filePath = path.resolve(requireOption(options, name));
175
+ if (!(await pathExists(filePath))) {
176
+ throw new Error(`${label} file does not exist: ${String(options[name])}`);
177
+ }
178
+ return filePath;
179
+ }
180
+
181
+ /**
182
+ * @param {CliOptions} options
183
+ * @returns {Promise<number>}
184
+ */
185
+ async function symbolicateFromOptions(options) {
186
+ const buildDir = await requireBuildDir(requireOption(options, "build-dir"));
187
+ const manifestPath = await requireExistingFile(options, "manifest", "manifest");
188
+ const issueEvent = hasOption(options, "issue-event");
189
+ if (hasOption(options, "stack-frame") === issueEvent) {
190
+ throw new Error("provide exactly one of --stack-frame or --issue-event");
191
+ }
192
+ const manifest = await readJsonObject(manifestPath, "manifest");
193
+ const sourceContext = optionalSourceContextOptions(options);
194
+ const report = issueEvent
195
+ ? await verifyJavaScriptIssueSymbolication({
196
+ buildDir,
197
+ issueEvent: await readJsonObject(await requireExistingFile(options, "issue-event", "issue event"), "issue event"),
198
+ manifest,
199
+ sourceContext
200
+ })
201
+ : await verifyJavaScriptSymbolication({ buildDir, manifest, sourceContext, stackFrame: requireOption(options, "stack-frame") });
202
+ printJson(report);
203
+ return EXIT_OK;
656
204
  }
657
205
 
658
- function runSymbolicateJs(args) {
206
+ /**
207
+ * @param {readonly string[]} args
208
+ * @returns {Promise<number>}
209
+ */
210
+ async function runSymbolicateJs(args) {
659
211
  const options = parseOptions(args, {
660
212
  "build-dir": "string",
661
- manifest: "string",
662
- "stack-frame": "string",
213
+ "context-lines": "string",
663
214
  "issue-event": "string",
215
+ manifest: "string",
664
216
  "source-root": "string",
665
- "context-lines": "string"
217
+ "stack-frame": "string"
666
218
  });
667
219
  try {
668
- const buildDir = requireBuildDir(requireOption(options, "build-dir"));
669
- const manifestPath = path.resolve(requireOption(options, "manifest"));
670
- if (!fs.existsSync(manifestPath)) {
671
- throw new Error(`manifest file does not exist: ${options.manifest}`);
672
- }
673
- const stackFrame = typeof options["stack-frame"] === "string" && options["stack-frame"].trim() !== "";
674
- const issueEvent = typeof options["issue-event"] === "string" && options["issue-event"].trim() !== "";
675
- if (stackFrame === issueEvent) {
676
- throw new Error("provide exactly one of --stack-frame or --issue-event");
677
- }
678
- const manifest = readJsonObject(manifestPath, "manifest");
679
- const sourceContext = optionalSourceContextOptions(options);
680
- if (issueEvent) {
681
- const issueEventPath = path.resolve(requireOption(options, "issue-event"));
682
- if (!fs.existsSync(issueEventPath)) {
683
- throw new Error(`issue event file does not exist: ${options["issue-event"]}`);
684
- }
685
- const report = verifyJavaScriptIssueSymbolication({
686
- buildDir,
687
- manifest,
688
- issueEvent: readJsonObject(issueEventPath, "issue event"),
689
- sourceContext
690
- });
691
- printJson(report);
692
- return 0;
693
- }
694
- const report = verifyJavaScriptSymbolication({
695
- buildDir,
696
- manifest,
697
- stackFrame: requireOption(options, "stack-frame"),
698
- sourceContext
699
- });
700
- printJson(report);
701
- return 0;
220
+ return await symbolicateFromOptions(options);
702
221
  } catch (error) {
703
- printJson({
704
- status: "validation_failed",
705
- verifier: { name: "logbrew-js-release-artifact-symbolication-verifier", version: SCRIPT_VERSION },
706
- validation: { errors: [error.message] }
707
- });
708
- return 1;
222
+ printJson({ status: "validation_failed", validation: { errors: [errorMessage(error)] }, verifier: VERIFIER });
223
+ return EXIT_BLOCKED;
224
+ }
225
+ }
226
+
227
+ const COMMANDS = new Map([
228
+ ["manifest-js", runManifestJs],
229
+ ["prepare-js", runPrepareJs],
230
+ ["symbolicate-js", runSymbolicateJs],
231
+ ["upload-js", runUploadJs]
232
+ ]);
233
+
234
+ /**
235
+ * @param {string} command
236
+ * @param {readonly string[]} args
237
+ * @returns {Promise<number>}
238
+ */
239
+ async function runCommand(command, args) {
240
+ const run = COMMANDS.get(command);
241
+ if (run === undefined) {
242
+ throw new Error(`unknown command: ${command}`);
709
243
  }
244
+ return run(args);
710
245
  }
711
246
 
247
+ /**
248
+ * @param {readonly string[]} argv
249
+ * @returns {Promise<number>}
250
+ */
712
251
  async function main(argv) {
713
- const [command, ...args] = argv;
714
- if (!command || command === "--help" || command === "-h") {
252
+ const [command = "", ...args] = argv;
253
+ if (command === "" || command === "--help" || command === "-h") {
715
254
  process.stdout.write(`${usage()}\n`);
716
- return command ? 0 : 1;
255
+ return command === "" ? EXIT_BLOCKED : EXIT_OK;
717
256
  }
718
257
  try {
719
- if (command === "prepare-js") {
720
- return runPrepareJs(args);
721
- }
722
- if (command === "manifest-js") {
723
- return runManifestJs(args);
724
- }
725
- if (command === "symbolicate-js") {
726
- return runSymbolicateJs(args);
727
- }
728
- if (command === "upload-js") {
729
- return await runUploadJs(args);
730
- }
731
- throw new Error(`unknown command: ${command}`);
258
+ return await runCommand(command, args);
732
259
  } catch (error) {
733
- process.stderr.write(`${error.message}\n\n${usage()}\n`);
734
- return 2;
260
+ process.stderr.write(`${errorMessage(error)}\n\n${usage()}\n`);
261
+ return EXIT_USAGE;
735
262
  }
736
263
  }
737
264
 
738
- process.exitCode = await main(process.argv.slice(2));
265
+ process.exitCode = await main(process.argv.slice(ARGUMENT_OFFSET));