@backtrack-js/browser 0.4.0

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 (81) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +291 -0
  3. package/bin/cli.cjs +51 -0
  4. package/bin/cli.js +2 -0
  5. package/bin/symbolize.cjs +654 -0
  6. package/bin/viewer.cjs +143 -0
  7. package/dist/capturers/console.d.ts +18 -0
  8. package/dist/capturers/console.d.ts.map +1 -0
  9. package/dist/capturers/errors.d.ts +22 -0
  10. package/dist/capturers/errors.d.ts.map +1 -0
  11. package/dist/capturers/index.d.ts +8 -0
  12. package/dist/capturers/index.d.ts.map +1 -0
  13. package/dist/capturers/navigation.d.ts +20 -0
  14. package/dist/capturers/navigation.d.ts.map +1 -0
  15. package/dist/capturers/network.d.ts +30 -0
  16. package/dist/capturers/network.d.ts.map +1 -0
  17. package/dist/capturers/performance.d.ts +15 -0
  18. package/dist/capturers/performance.d.ts.map +1 -0
  19. package/dist/capturers/route-matcher.d.ts +10 -0
  20. package/dist/capturers/route-matcher.d.ts.map +1 -0
  21. package/dist/capturers/rrweb.d.ts +18 -0
  22. package/dist/capturers/rrweb.d.ts.map +1 -0
  23. package/dist/capturers/sanitizer.d.ts +37 -0
  24. package/dist/capturers/sanitizer.d.ts.map +1 -0
  25. package/dist/core/flight-recorder.d.ts +75 -0
  26. package/dist/core/flight-recorder.d.ts.map +1 -0
  27. package/dist/core/state-machine.d.ts +55 -0
  28. package/dist/core/state-machine.d.ts.map +1 -0
  29. package/dist/index.cjs +1803 -0
  30. package/dist/index.cjs.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +5763 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/storage/batch-writer.d.ts +57 -0
  36. package/dist/storage/batch-writer.d.ts.map +1 -0
  37. package/dist/storage/db.d.ts +39 -0
  38. package/dist/storage/db.d.ts.map +1 -0
  39. package/dist/storage/incident-manager.d.ts +107 -0
  40. package/dist/storage/incident-manager.d.ts.map +1 -0
  41. package/dist/storage/index.d.ts +6 -0
  42. package/dist/storage/index.d.ts.map +1 -0
  43. package/dist/storage/retention.d.ts +41 -0
  44. package/dist/storage/retention.d.ts.map +1 -0
  45. package/dist/storage/session.d.ts +32 -0
  46. package/dist/storage/session.d.ts.map +1 -0
  47. package/dist/types/artifact.d.ts +72 -0
  48. package/dist/types/artifact.d.ts.map +1 -0
  49. package/dist/types/chunk.d.ts +32 -0
  50. package/dist/types/chunk.d.ts.map +1 -0
  51. package/dist/types/health.d.ts +16 -0
  52. package/dist/types/health.d.ts.map +1 -0
  53. package/dist/types/incident.d.ts +35 -0
  54. package/dist/types/incident.d.ts.map +1 -0
  55. package/dist/types/index.d.ts +8 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/options.d.ts +84 -0
  58. package/dist/types/options.d.ts.map +1 -0
  59. package/dist/types/timeline.d.ts +74 -0
  60. package/dist/types/timeline.d.ts.map +1 -0
  61. package/dist/types/transport.d.ts +6 -0
  62. package/dist/types/transport.d.ts.map +1 -0
  63. package/dist/utils/compression.d.ts +28 -0
  64. package/dist/utils/compression.d.ts.map +1 -0
  65. package/dist/utils/gist-uploader.d.ts +12 -0
  66. package/dist/utils/gist-uploader.d.ts.map +1 -0
  67. package/dist/utils/markdown.d.ts +12 -0
  68. package/dist/utils/markdown.d.ts.map +1 -0
  69. package/dist/validation/validate.d.ts +27 -0
  70. package/dist/validation/validate.d.ts.map +1 -0
  71. package/dist/viewer/assets/backtrack-favico-Ds-TMW85.png +0 -0
  72. package/dist/viewer/assets/index-D6J82Kcf.js +191 -0
  73. package/dist/viewer/assets/index-cVrM07fr.css +1 -0
  74. package/dist/viewer/index.html +19 -0
  75. package/dist/widget/annotator.d.ts +85 -0
  76. package/dist/widget/annotator.d.ts.map +1 -0
  77. package/dist/widget/styles.d.ts +2 -0
  78. package/dist/widget/styles.d.ts.map +1 -0
  79. package/dist/widget/widget.d.ts +78 -0
  80. package/dist/widget/widget.d.ts.map +1 -0
  81. package/package.json +82 -0
@@ -0,0 +1,654 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const zlib = require('zlib');
4
+ const {
5
+ TraceMap,
6
+ originalPositionFor,
7
+ sourceContentFor
8
+ } = require('@jridgewell/trace-mapping');
9
+
10
+ // ponytail: parser inicial cobre stacks V8; adicionar Firefox/Safari quando fixtures reais exigirem.
11
+ const FRAME_WITH_FUNCTION = /^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)\s*$/;
12
+ const FRAME_WITHOUT_FUNCTION = /^\s*at\s+(.+):(\d+):(\d+)\s*$/;
13
+
14
+ /**
15
+ * Remove query string, hash, diretórios e decodifica URL para obter o nome do bundle.
16
+ * @param {string} generatedFile
17
+ * @returns {string}
18
+ */
19
+ function extractBundleName(generatedFile) {
20
+ if (!generatedFile || typeof generatedFile !== 'string') return '';
21
+ let cleaned = generatedFile;
22
+ // Remove hash
23
+ cleaned = cleaned.split('#')[0];
24
+ // Remove query string
25
+ cleaned = cleaned.split('?')[0];
26
+ // URL decode
27
+ try {
28
+ cleaned = decodeURIComponent(cleaned);
29
+ } catch {}
30
+ // Extrai nome do arquivo
31
+ const segments = cleaned.split(/[/\\]/);
32
+ return segments[segments.length - 1] || '';
33
+ }
34
+
35
+ /**
36
+ * Normaliza o caminho do arquivo de origem para evitar vazar caminhos absolutos locais.
37
+ * Salva preferencialmente a partir de src/ e trata prefixos como webpack:///./
38
+ * @param {string} rawSource
39
+ * @returns {string | undefined}
40
+ */
41
+ function normalizeSourceFile(rawSource) {
42
+ if (!rawSource || typeof rawSource !== 'string') return undefined;
43
+ let normalized = rawSource.replace(/\\/g, '/');
44
+
45
+ // Tratar prefixos comuns de bundlers
46
+ normalized = normalized.replace(/^webpack:\/\/\/\.\//, '');
47
+ normalized = normalized.replace(/^webpack:\/\/\//, '');
48
+ normalized = normalized.replace(/^\.\//, '');
49
+
50
+ // Caminhos absolutos: salvar preferencialmente a partir de src/
51
+ const srcIdx = normalized.lastIndexOf('src/');
52
+ if (srcIdx !== -1) {
53
+ return normalized.slice(srcIdx);
54
+ }
55
+
56
+ // Detectar caminhos absolutos: file://, POSIX (/...) ou Windows (C:/... ou //UNC)
57
+ const isFileUri = /^file:\/\//i.test(rawSource) || /^file:\/\//i.test(normalized);
58
+ const isPosixAbsolute = normalized.startsWith('/');
59
+ const isWindowsAbsolute = /^[a-zA-Z]:\//.test(normalized) || normalized.startsWith('//');
60
+
61
+ if (isFileUri || isPosixAbsolute || isWindowsAbsolute) {
62
+ // Se for absoluto e não tiver src/, retornar apenas o nome do arquivo
63
+ const segments = normalized.split(/[/\\]/);
64
+ const fileName = segments[segments.length - 1];
65
+ return fileName || normalized;
66
+ }
67
+
68
+ // Caminhos relativos legítimos não são alterados
69
+ return normalized;
70
+ }
71
+
72
+ /**
73
+ * Percorre recursivamente o diretório em busca de arquivos .js.map, .mjs.map e .cjs.map.
74
+ * @param {string} dir
75
+ * @returns {Map<string, string[]>}
76
+ */
77
+ function findSourceMaps(dir) {
78
+ const mapFilesByBundle = new Map();
79
+
80
+ function walk(currentDir) {
81
+ let entries;
82
+ try {
83
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
84
+ } catch {
85
+ return;
86
+ }
87
+
88
+ for (const entry of entries) {
89
+ const fullPath = path.join(currentDir, entry.name);
90
+ if (entry.isDirectory()) {
91
+ walk(fullPath);
92
+ } else if (entry.isFile()) {
93
+ const name = entry.name;
94
+ let bundleName = null;
95
+ if (name.endsWith('.js.map')) {
96
+ bundleName = name.slice(0, -4);
97
+ } else if (name.endsWith('.mjs.map')) {
98
+ bundleName = name.slice(0, -4);
99
+ } else if (name.endsWith('.cjs.map')) {
100
+ bundleName = name.slice(0, -4);
101
+ }
102
+
103
+ if (bundleName) {
104
+ if (!mapFilesByBundle.has(bundleName)) {
105
+ mapFilesByBundle.set(bundleName, []);
106
+ }
107
+ mapFilesByBundle.get(bundleName).push(fullPath);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ if (dir && typeof dir === 'string' && fs.existsSync(dir)) {
114
+ walk(dir);
115
+ }
116
+
117
+ return mapFilesByBundle;
118
+ }
119
+
120
+ /**
121
+ * Extrai linhas de contexto do código-fonte original contido no .map.
122
+ * @param {any} map
123
+ * @param {string} rawSource
124
+ * @param {number} originalLine
125
+ * @param {number | undefined} contextLinesOption
126
+ * @returns {{ context?: { before: string[], line: string, after: string[] }, warning?: string }}
127
+ */
128
+ function extractContext(map, rawSource, originalLine, contextLinesOption) {
129
+ let content = null;
130
+ try {
131
+ content = sourceContentFor(map, rawSource, true);
132
+ } catch {
133
+ return {
134
+ context: undefined,
135
+ warning: `sourcesContent não disponível para ${rawSource}.`
136
+ };
137
+ }
138
+
139
+ if (!content || typeof content !== 'string') {
140
+ return {
141
+ context: undefined,
142
+ warning: `sourcesContent não disponível para ${rawSource}.`
143
+ };
144
+ }
145
+
146
+ const allLines = content.split(/\r?\n/);
147
+ const lineIdx = originalLine - 1; // 1-based para 0-based
148
+
149
+ if (lineIdx < 0 || lineIdx >= allLines.length) {
150
+ return { context: undefined, warning: undefined };
151
+ }
152
+
153
+ let cl = typeof contextLinesOption === 'number' ? contextLinesOption : 3;
154
+ cl = Math.max(0, Math.min(10, cl)); // Limites: mínimo 0, máximo 10
155
+
156
+ const startIdx = Math.max(0, lineIdx - cl);
157
+ const endIdx = Math.min(allLines.length - 1, lineIdx + cl);
158
+
159
+ const truncate = (str) => {
160
+ if (str.length > 2000) {
161
+ return str.slice(0, 2000);
162
+ }
163
+ return str;
164
+ };
165
+
166
+ const before = [];
167
+ for (let i = startIdx; i < lineIdx; i++) {
168
+ before.push(truncate(allLines[i]));
169
+ }
170
+
171
+ const line = truncate(allLines[lineIdx]);
172
+
173
+ const after = [];
174
+ for (let i = lineIdx + 1; i <= endIdx; i++) {
175
+ after.push(truncate(allLines[i]));
176
+ }
177
+
178
+ return {
179
+ context: { before, line, after },
180
+ warning: undefined
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Resolve os frames de uma stack V8 utilizando os source maps disponíveis.
186
+ * @param {string} stack
187
+ * @param {{ sourceMapsDirectory: string, contextLines?: number }} options
188
+ * @returns {{
189
+ * frames: Array<{
190
+ * generatedFile: string,
191
+ * generatedLine: number,
192
+ * generatedColumn: number,
193
+ * sourceFile?: string,
194
+ * sourceLine?: number,
195
+ * sourceColumn?: number,
196
+ * functionName?: string,
197
+ * context?: { before: string[], line: string, after: string[] }
198
+ * }>,
199
+ * resolvedCount: number,
200
+ * unresolvedCount: number,
201
+ * warnings: string[]
202
+ * }}
203
+ */
204
+ function symbolicateStack(stack, options) {
205
+ const result = {
206
+ frames: [],
207
+ resolvedCount: 0,
208
+ unresolvedCount: 0,
209
+ warnings: []
210
+ };
211
+
212
+ if (!stack || typeof stack !== 'string') {
213
+ return result;
214
+ }
215
+
216
+ const sourceMapsDir = options?.sourceMapsDirectory;
217
+ const contextLines = options?.contextLines;
218
+
219
+ const bundleMapFiles = findSourceMaps(sourceMapsDir);
220
+ const traceMapCache = new Map();
221
+
222
+ const lines = stack.split(/\r?\n/);
223
+ // Ignora a primeira linha contendo a mensagem do erro (se não for um frame)
224
+ const candidateLines = lines.length > 1 && !lines[0].trim().startsWith('at ')
225
+ ? lines.slice(1)
226
+ : lines;
227
+
228
+ for (const line of candidateLines) {
229
+ let parsedFunctionName = undefined;
230
+ let generatedFile = undefined;
231
+ let lineStr = undefined;
232
+ let colStr = undefined;
233
+
234
+ const withFnMatch = line.match(FRAME_WITH_FUNCTION);
235
+ if (withFnMatch) {
236
+ parsedFunctionName = withFnMatch[1];
237
+ generatedFile = withFnMatch[2];
238
+ lineStr = withFnMatch[3];
239
+ colStr = withFnMatch[4];
240
+ } else {
241
+ const withoutFnMatch = line.match(FRAME_WITHOUT_FUNCTION);
242
+ if (withoutFnMatch) {
243
+ generatedFile = withoutFnMatch[1];
244
+ lineStr = withoutFnMatch[2];
245
+ colStr = withoutFnMatch[3];
246
+ }
247
+ }
248
+
249
+ if (!generatedFile || !lineStr || !colStr) {
250
+ // Ignora linhas que não correspondam a um frame
251
+ continue;
252
+ }
253
+
254
+ const generatedLine = parseInt(lineStr, 10);
255
+ const generatedColumn = parseInt(colStr, 10);
256
+
257
+ // Valida que linha e coluna são inteiros positivos
258
+ if (!Number.isInteger(generatedLine) || generatedLine <= 0 ||
259
+ !Number.isInteger(generatedColumn) || generatedColumn <= 0) {
260
+ continue;
261
+ }
262
+
263
+ const bundleName = extractBundleName(generatedFile);
264
+ const matchingMaps = bundleMapFiles.get(bundleName);
265
+
266
+ const unresolvedFrame = {
267
+ generatedFile,
268
+ generatedLine,
269
+ generatedColumn,
270
+ ...(parsedFunctionName ? { functionName: parsedFunctionName } : {})
271
+ };
272
+
273
+ if (!matchingMaps || matchingMaps.length === 0) {
274
+ result.frames.push(unresolvedFrame);
275
+ result.unresolvedCount++;
276
+ continue;
277
+ }
278
+
279
+ if (matchingMaps.length > 1) {
280
+ result.frames.push(unresolvedFrame);
281
+ result.unresolvedCount++;
282
+ result.warnings.push(`Mais de um source map encontrado para ${bundleName}.`);
283
+ continue;
284
+ }
285
+
286
+ const mapPath = matchingMaps[0];
287
+ let traceMap = traceMapCache.get(mapPath);
288
+ if (!traceMap) {
289
+ try {
290
+ const rawContent = fs.readFileSync(mapPath, 'utf-8');
291
+ const parsedJson = JSON.parse(rawContent);
292
+ traceMap = new TraceMap(parsedJson);
293
+ traceMapCache.set(mapPath, traceMap);
294
+ } catch (err) {
295
+ result.frames.push(unresolvedFrame);
296
+ result.unresolvedCount++;
297
+ result.warnings.push(`Erro ao carregar source map ${mapPath}: ${err.message}`);
298
+ continue;
299
+ }
300
+ }
301
+
302
+ // A coluna da stack do navegador (V8) é 1-based, enquanto o TraceMap espera coluna 0-based
303
+ const lookupColumn = generatedColumn > 0 ? generatedColumn - 1 : 0;
304
+ let original = null;
305
+ try {
306
+ original = originalPositionFor(traceMap, {
307
+ line: generatedLine,
308
+ column: lookupColumn
309
+ });
310
+ } catch {
311
+ result.frames.push(unresolvedFrame);
312
+ result.unresolvedCount++;
313
+ continue;
314
+ }
315
+
316
+ if (!original || !original.source || original.line == null) {
317
+ result.frames.push(unresolvedFrame);
318
+ result.unresolvedCount++;
319
+ continue;
320
+ }
321
+
322
+ const resolvedFrame = {
323
+ generatedFile,
324
+ generatedLine,
325
+ generatedColumn,
326
+ sourceFile: normalizeSourceFile(original.source),
327
+ sourceLine: original.line,
328
+ sourceColumn: original.column,
329
+ functionName: original.name ?? parsedFunctionName
330
+ };
331
+
332
+ const { context, warning } = extractContext(traceMap, original.source, original.line, contextLines);
333
+ if (context) {
334
+ resolvedFrame.context = context;
335
+ }
336
+ if (warning) {
337
+ result.warnings.push(warning);
338
+ }
339
+
340
+ result.frames.push(resolvedFrame);
341
+ result.resolvedCount++;
342
+ }
343
+
344
+ return result;
345
+ }
346
+
347
+ /**
348
+ * Detecta se o buffer possui o cabeçalho mágico de gzip (0x1f, 0x8b).
349
+ * @param {Buffer} buffer
350
+ * @returns {boolean}
351
+ */
352
+ function isGzip(buffer) {
353
+ return Buffer.isBuffer(buffer) && buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
354
+ }
355
+
356
+ /**
357
+ * Calcula o caminho de saída padrão para o incidente simbolizado.
358
+ * Ex: bug.ffr.json -> bug.symbolicated.ffr.json
359
+ * bug.ffr.json.gz -> bug.symbolicated.ffr.json
360
+ * @param {string} inputPath
361
+ * @returns {string}
362
+ */
363
+ function computeDefaultOutputPath(inputPath) {
364
+ const dir = path.dirname(inputPath);
365
+ let base = path.basename(inputPath);
366
+
367
+ if (base.endsWith('.ffr.json.gz')) {
368
+ base = base.slice(0, -'.ffr.json.gz'.length);
369
+ } else if (base.endsWith('.ffr.json')) {
370
+ base = base.slice(0, -'.ffr.json'.length);
371
+ } else if (base.endsWith('.json.gz')) {
372
+ base = base.slice(0, -'.json.gz'.length);
373
+ } else if (base.endsWith('.json')) {
374
+ base = base.slice(0, -'.json'.length);
375
+ } else if (base.endsWith('.gz')) {
376
+ base = base.slice(0, -'.gz'.length);
377
+ }
378
+
379
+ return path.join(dir, `${base}.symbolicated.ffr.json`);
380
+ }
381
+
382
+ function printSymbolizeHelp() {
383
+ console.log(`
384
+ Uso: backtrack symbolize <incidente> --sourcemaps <diretório> [opções]
385
+
386
+ Argumentos obrigatórios:
387
+ <incidente> Caminho para o arquivo do incidente (.ffr.json, .ffr.json.gz, etc.)
388
+ --sourcemaps <diretório> Diretório contendo os arquivos .map
389
+
390
+ Opções adicionais:
391
+ --output <arquivo> Caminho do arquivo de saída (padrão: <incidente>.symbolicated.ffr.json)
392
+ --context-lines <número> Quantidade de linhas de contexto antes e depois do erro (0-10, padrão: 3)
393
+ -h, --help Mostra esta mensagem de ajuda
394
+ `.trim());
395
+ }
396
+
397
+ /**
398
+ * Tenta carregar validateFlightRecorderArtifact do build gerado.
399
+ */
400
+ async function loadValidator() {
401
+ try {
402
+ const mod = require('../dist/index.cjs');
403
+ if (mod && typeof mod.validateFlightRecorderArtifact === 'function') {
404
+ return mod.validateFlightRecorderArtifact;
405
+ }
406
+ } catch {}
407
+ try {
408
+ const mod = await import('../dist/index.js');
409
+ if (mod && typeof mod.validateFlightRecorderArtifact === 'function') {
410
+ return mod.validateFlightRecorderArtifact;
411
+ }
412
+ } catch {}
413
+ return null;
414
+ }
415
+
416
+ /**
417
+ * Executa o comando symbolize com os argumentos fornecidos na CLI.
418
+ * @param {string[]} args
419
+ * @returns {Promise<number>} Código de saída (0 ou 1)
420
+ */
421
+ async function run(args = []) {
422
+ let incidentPath = null;
423
+ let sourcemapsDir = null;
424
+ let outputPath = null;
425
+ let contextLines = 3;
426
+
427
+ for (let i = 0; i < args.length; i++) {
428
+ const arg = args[i];
429
+ if (arg === '--help' || arg === '-h') {
430
+ printSymbolizeHelp();
431
+ return 0;
432
+ }
433
+ if (arg === '--sourcemaps') {
434
+ sourcemapsDir = args[++i];
435
+ } else if (arg === '--output') {
436
+ outputPath = args[++i];
437
+ } else if (arg === '--context-lines') {
438
+ const val = parseInt(args[++i], 10);
439
+ if (!Number.isNaN(val)) {
440
+ contextLines = val;
441
+ }
442
+ } else if (!arg.startsWith('-') && !incidentPath) {
443
+ incidentPath = arg;
444
+ } else {
445
+ console.error(`[Backtrack] Argumento desconhecido ou inválido: ${arg}`);
446
+ process.exitCode = 1;
447
+ return 1;
448
+ }
449
+ }
450
+
451
+ if (!incidentPath) {
452
+ console.error('[Backtrack] Erro: Caminho do arquivo de incidente não informado.');
453
+ process.exitCode = 1;
454
+ return 1;
455
+ }
456
+
457
+ if (!sourcemapsDir) {
458
+ console.error('[Backtrack] Erro: Opção --sourcemaps é obrigatória.');
459
+ process.exitCode = 1;
460
+ return 1;
461
+ }
462
+
463
+ if (!fs.existsSync(incidentPath)) {
464
+ console.error(`[Backtrack] Erro: Arquivo de incidente não encontrado: ${incidentPath}`);
465
+ process.exitCode = 1;
466
+ return 1;
467
+ }
468
+
469
+ if (!fs.existsSync(sourcemapsDir) || !fs.statSync(sourcemapsDir).isDirectory()) {
470
+ console.error(`[Backtrack] Erro: Diretório de source maps não encontrado: ${sourcemapsDir}`);
471
+ process.exitCode = 1;
472
+ return 1;
473
+ }
474
+
475
+ const finalOutputPath = outputPath || computeDefaultOutputPath(incidentPath);
476
+ const resolvedInput = path.resolve(incidentPath);
477
+ const resolvedOutput = path.resolve(finalOutputPath);
478
+
479
+ if (
480
+ resolvedInput === resolvedOutput ||
481
+ (process.platform === 'win32' && resolvedInput.toLowerCase() === resolvedOutput.toLowerCase())
482
+ ) {
483
+ console.error('[Backtrack] Erro: o arquivo de saída não pode sobrescrever o incidente original.');
484
+ process.exitCode = 1;
485
+ return 1;
486
+ }
487
+
488
+ let bytes;
489
+ try {
490
+ bytes = fs.readFileSync(incidentPath);
491
+ } catch (err) {
492
+ console.error(`[Backtrack] Erro ao ler o arquivo de incidente: ${err.message}`);
493
+ process.exitCode = 1;
494
+ return 1;
495
+ }
496
+
497
+ let json;
498
+ try {
499
+ json = isGzip(bytes)
500
+ ? zlib.gunzipSync(bytes).toString('utf8')
501
+ : bytes.toString('utf8');
502
+ } catch (err) {
503
+ console.error(`[Backtrack] Erro ao descompactar incidente: ${err.message}`);
504
+ process.exitCode = 1;
505
+ return 1;
506
+ }
507
+
508
+ const MAX_BYTES = 50 * 1024 * 1024;
509
+ if (Buffer.byteLength(json, 'utf8') > MAX_BYTES) {
510
+ console.error('[Backtrack] Erro: Tamanho do incidente descompactado excede o limite de 50 MB.');
511
+ process.exitCode = 1;
512
+ return 1;
513
+ }
514
+
515
+ let artifact;
516
+ try {
517
+ artifact = JSON.parse(json);
518
+ } catch (err) {
519
+ console.error(`[Backtrack] Erro ao analisar JSON do incidente: ${err.message}`);
520
+ process.exitCode = 1;
521
+ return 1;
522
+ }
523
+
524
+ const validate = await loadValidator();
525
+ if (validate) {
526
+ const valResult = validate(artifact);
527
+ if (!valResult.success) {
528
+ console.error('[Backtrack] Erro: Artefato inválido:');
529
+ for (const errorMsg of valResult.errors) {
530
+ console.error(` - ${errorMsg}`);
531
+ }
532
+ process.exitCode = 1;
533
+ return 1;
534
+ }
535
+ } else {
536
+ if (
537
+ !artifact ||
538
+ typeof artifact !== 'object' ||
539
+ !Array.isArray(artifact.timeline) ||
540
+ !artifact.incident ||
541
+ typeof artifact.incident.id !== 'string'
542
+ ) {
543
+ console.error('[Backtrack] Erro: Artefato inválido: estrutura básica incorreta.');
544
+ process.exitCode = 1;
545
+ return 1;
546
+ }
547
+ }
548
+
549
+ let errorEventsCount = 0;
550
+ let totalResolvedFrames = 0;
551
+ let totalUnresolvedFrames = 0;
552
+ const allWarnings = [];
553
+
554
+ const newTimeline = artifact.timeline.map((event) => {
555
+ if (event.type !== 'error' || !event.stack || typeof event.stack !== 'string') {
556
+ return event;
557
+ }
558
+
559
+ errorEventsCount++;
560
+ const result = symbolicateStack(event.stack, {
561
+ sourceMapsDirectory: sourcemapsDir,
562
+ contextLines
563
+ });
564
+
565
+ totalResolvedFrames += result.resolvedCount;
566
+ totalUnresolvedFrames += result.unresolvedCount;
567
+ if (result.warnings && result.warnings.length > 0) {
568
+ allWarnings.push(...result.warnings);
569
+ }
570
+
571
+ return {
572
+ ...event,
573
+ ...(result.frames.length > 0
574
+ ? { resolvedStack: result.frames }
575
+ : {})
576
+ };
577
+ });
578
+
579
+ if (totalResolvedFrames === 0 && totalUnresolvedFrames === 0) {
580
+ console.log('[Backtrack] Nenhuma stack compatível foi encontrada.');
581
+ process.exitCode = 0;
582
+ return 0;
583
+ }
584
+
585
+ const uniqueWarnings = Array.from(new Set(allWarnings));
586
+
587
+ if (totalUnresolvedFrames > 0) {
588
+ console.log('[Backtrack] Symbolication parcial.\n');
589
+ } else {
590
+ console.log('[Backtrack] Symbolication concluída.\n');
591
+ }
592
+
593
+ console.log(`Incidente: ${artifact.incident.id}`);
594
+ console.log(`Erros encontrados: ${errorEventsCount}`);
595
+ console.log(`Frames resolvidos: ${totalResolvedFrames}`);
596
+ console.log(`Frames não resolvidos: ${totalUnresolvedFrames}`);
597
+ console.log(`Saída: ${resolvedOutput}`);
598
+
599
+ if (uniqueWarnings.length > 0) {
600
+ console.log('\nAvisos:');
601
+ for (const w of uniqueWarnings) {
602
+ console.log(`- ${w}`);
603
+ }
604
+ }
605
+
606
+ const symbolicatedArtifact = {
607
+ ...artifact,
608
+ timeline: newTimeline
609
+ };
610
+
611
+ const tempPath = `${resolvedOutput}.tmp`;
612
+ try {
613
+ fs.writeFileSync(tempPath, JSON.stringify(symbolicatedArtifact, null, 2), 'utf8');
614
+ if (fs.existsSync(resolvedOutput)) {
615
+ fs.unlinkSync(resolvedOutput);
616
+ }
617
+ fs.renameSync(tempPath, resolvedOutput);
618
+ } catch (err) {
619
+ if (fs.existsSync(tempPath)) {
620
+ try {
621
+ fs.unlinkSync(tempPath);
622
+ } catch {}
623
+ }
624
+ console.error(`[Backtrack] Erro ao gravar arquivo de saída: ${err.message}`);
625
+ process.exitCode = 1;
626
+ return 1;
627
+ }
628
+
629
+ process.exitCode = 0;
630
+ return 0;
631
+ }
632
+
633
+ if (require.main === module) {
634
+ run(process.argv.slice(2)).then((code) => {
635
+ if (typeof code === 'number') {
636
+ process.exitCode = code;
637
+ }
638
+ }).catch((err) => {
639
+ console.error(err);
640
+ process.exitCode = 1;
641
+ });
642
+ }
643
+
644
+ module.exports = {
645
+ symbolicateStack,
646
+ FRAME_WITH_FUNCTION,
647
+ FRAME_WITHOUT_FUNCTION,
648
+ extractBundleName,
649
+ normalizeSourceFile,
650
+ isGzip,
651
+ computeDefaultOutputPath,
652
+ loadValidator,
653
+ run
654
+ };