@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.
- package/LICENSE +9 -0
- package/README.md +291 -0
- package/bin/cli.cjs +51 -0
- package/bin/cli.js +2 -0
- package/bin/symbolize.cjs +654 -0
- package/bin/viewer.cjs +143 -0
- package/dist/capturers/console.d.ts +18 -0
- package/dist/capturers/console.d.ts.map +1 -0
- package/dist/capturers/errors.d.ts +22 -0
- package/dist/capturers/errors.d.ts.map +1 -0
- package/dist/capturers/index.d.ts +8 -0
- package/dist/capturers/index.d.ts.map +1 -0
- package/dist/capturers/navigation.d.ts +20 -0
- package/dist/capturers/navigation.d.ts.map +1 -0
- package/dist/capturers/network.d.ts +30 -0
- package/dist/capturers/network.d.ts.map +1 -0
- package/dist/capturers/performance.d.ts +15 -0
- package/dist/capturers/performance.d.ts.map +1 -0
- package/dist/capturers/route-matcher.d.ts +10 -0
- package/dist/capturers/route-matcher.d.ts.map +1 -0
- package/dist/capturers/rrweb.d.ts +18 -0
- package/dist/capturers/rrweb.d.ts.map +1 -0
- package/dist/capturers/sanitizer.d.ts +37 -0
- package/dist/capturers/sanitizer.d.ts.map +1 -0
- package/dist/core/flight-recorder.d.ts +75 -0
- package/dist/core/flight-recorder.d.ts.map +1 -0
- package/dist/core/state-machine.d.ts +55 -0
- package/dist/core/state-machine.d.ts.map +1 -0
- package/dist/index.cjs +1803 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5763 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/batch-writer.d.ts +57 -0
- package/dist/storage/batch-writer.d.ts.map +1 -0
- package/dist/storage/db.d.ts +39 -0
- package/dist/storage/db.d.ts.map +1 -0
- package/dist/storage/incident-manager.d.ts +107 -0
- package/dist/storage/incident-manager.d.ts.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/retention.d.ts +41 -0
- package/dist/storage/retention.d.ts.map +1 -0
- package/dist/storage/session.d.ts +32 -0
- package/dist/storage/session.d.ts.map +1 -0
- package/dist/types/artifact.d.ts +72 -0
- package/dist/types/artifact.d.ts.map +1 -0
- package/dist/types/chunk.d.ts +32 -0
- package/dist/types/chunk.d.ts.map +1 -0
- package/dist/types/health.d.ts +16 -0
- package/dist/types/health.d.ts.map +1 -0
- package/dist/types/incident.d.ts +35 -0
- package/dist/types/incident.d.ts.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/options.d.ts +84 -0
- package/dist/types/options.d.ts.map +1 -0
- package/dist/types/timeline.d.ts +74 -0
- package/dist/types/timeline.d.ts.map +1 -0
- package/dist/types/transport.d.ts +6 -0
- package/dist/types/transport.d.ts.map +1 -0
- package/dist/utils/compression.d.ts +28 -0
- package/dist/utils/compression.d.ts.map +1 -0
- package/dist/utils/gist-uploader.d.ts +12 -0
- package/dist/utils/gist-uploader.d.ts.map +1 -0
- package/dist/utils/markdown.d.ts +12 -0
- package/dist/utils/markdown.d.ts.map +1 -0
- package/dist/validation/validate.d.ts +27 -0
- package/dist/validation/validate.d.ts.map +1 -0
- package/dist/viewer/assets/backtrack-favico-Ds-TMW85.png +0 -0
- package/dist/viewer/assets/index-D6J82Kcf.js +191 -0
- package/dist/viewer/assets/index-cVrM07fr.css +1 -0
- package/dist/viewer/index.html +19 -0
- package/dist/widget/annotator.d.ts +85 -0
- package/dist/widget/annotator.d.ts.map +1 -0
- package/dist/widget/styles.d.ts +2 -0
- package/dist/widget/styles.d.ts.map +1 -0
- package/dist/widget/widget.d.ts +78 -0
- package/dist/widget/widget.d.ts.map +1 -0
- 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
|
+
};
|