@backtrack-js/browser 0.5.3 → 0.5.4
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/README.md +56 -54
- package/bin/cli.cjs +5 -2
- package/bin/export-context.cjs +50 -0
- package/bin/mcp.cjs +19 -0
- package/bin/symbolize.cjs +214 -66
- package/dist/capturers/browser.d.ts +18 -0
- package/dist/capturers/errors.d.ts +3 -1
- package/dist/capturers/network.d.ts +11 -1
- package/dist/capturers/rrweb.d.ts +2 -0
- package/dist/capturers/sanitizer.d.ts +1 -0
- package/dist/core/flight-recorder.d.ts +9 -0
- package/dist/dev-server.cjs +3 -6
- package/dist/dev-server.js +128 -90
- package/dist/incident-reports-9iXHIaBD.js +1242 -0
- package/dist/incident-reports-Cjos7N-u.cjs +51 -0
- package/dist/index.cjs +53 -38
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2357 -2079
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp.cjs +2 -0
- package/dist/mcp.js +203 -0
- package/dist/storage/batch-writer.d.ts +6 -0
- package/dist/storage/incident-manager.d.ts +10 -6
- package/dist/types/artifact.d.ts +50 -0
- package/dist/types/browser-context.d.ts +34 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/options.d.ts +15 -0
- package/dist/types/timeline.d.ts +11 -0
- package/dist/types/visual-proposal.d.ts +1 -0
- package/dist/utils/context-package.d.ts +4 -0
- package/dist/utils/incident-query.d.ts +93 -0
- package/dist/utils/incident-reports.d.ts +18 -0
- package/dist/utils/interactions.d.ts +13 -0
- package/dist/utils/markdown.d.ts +3 -0
- package/dist/validation/structure.d.ts +11 -0
- package/dist/validation/validate.d.ts +3 -1
- package/dist/viewer/assets/index-BNTvj2zy.js +337 -0
- package/dist/viewer/assets/{index-092olPra.css → index-DXHvcqkw.css} +1 -1
- package/dist/viewer/index.html +2 -2
- package/dist/widget/element-editor.d.ts +7 -0
- package/dist/widget/widget.d.ts +9 -2
- package/package.json +113 -99
- package/dist/markdown-Ao5Ah9MF.cjs +0 -8
- package/dist/markdown-D3C94OlY.js +0 -493
- package/dist/viewer/assets/index-BkV3C4kX.js +0 -197
package/README.md
CHANGED
|
@@ -1,54 +1,56 @@
|
|
|
1
|
-
# Backtrack JS
|
|
2
|
-
|
|
3
|
-
Flight recorder para aplicações web: captura uma janela recente da sessão, salva incidentes localmente e ajuda a investigar replay, rede e stack traces.
|
|
4
|
-
|
|
5
|
-
## Instalação
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm install @backtrack-js/browser
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Inicialize uma vez no cliente:
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
import { Backtrack } from '@backtrack-js/browser';
|
|
15
|
-
|
|
16
|
-
void Backtrack.init({
|
|
17
|
-
bufferMinutes: 5,
|
|
18
|
-
showWidget: true,
|
|
19
|
-
network: { capturePayloads: false }
|
|
20
|
-
}).catch(error => {
|
|
21
|
-
console.warn('Backtrack não pôde iniciar', error);
|
|
22
|
-
});
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
O widget permite salvar uma gravação. Abra o artefato no visualizador para investigar. A biblioteca não envia gravações para um servidor por padrão.
|
|
26
|
-
|
|
27
|
-
## Documentação
|
|
28
|
-
|
|
29
|
-
[English documentation](site/docs/content/en/overview.md) · [Português](site/docs/content/pt/overview.md)
|
|
30
|
-
|
|
31
|
-
- [Primeiros passos](site/docs/content/pt/quickstart.md) e [integrações](site/docs/content/pt/integrations.md).
|
|
32
|
-
- [Configuração](site/docs/content/pt/configuration.md), [privacidade e sanitização](site/docs/content/pt/privacy.md), [rede e disparos automáticos](site/docs/content/pt/network.md).
|
|
33
|
-
- [Visualizador](site/docs/content/pt/viewer.md), [source maps](site/docs/content/pt/source-maps.md), [exportação local](site/docs/content/pt/local-export.md) e [transportes](site/docs/content/pt/transport.md).
|
|
34
|
-
- [Referência da API](site/docs/content/pt/api.md), [solução de problemas](site/docs/content/pt/troubleshooting.md) e [versões e limitações](site/docs/content/pt/releases.md).
|
|
35
|
-
|
|
36
|
-
Os guias descrevem o contrato local da versão 0.5.
|
|
37
|
-
|
|
38
|
-
## Desenvolvimento
|
|
39
|
-
|
|
40
|
-
```sh
|
|
41
|
-
npm install
|
|
42
|
-
npm run build
|
|
43
|
-
npm run build:viewer
|
|
44
|
-
npm run build:site
|
|
45
|
-
npm run preview:site
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Documentação local: http://localhost:4174/docs/
|
|
49
|
-
|
|
50
|
-
[Como editar e publicar o site](site/README.md). A saída do site é `dist/site`; a do visualizador permanece separada.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
# Backtrack JS
|
|
2
|
+
|
|
3
|
+
Flight recorder para aplicações web: captura uma janela recente da sessão, salva incidentes localmente e ajuda a investigar replay, rede e stack traces.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @backtrack-js/browser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Inicialize uma vez no cliente:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { Backtrack } from '@backtrack-js/browser';
|
|
15
|
+
|
|
16
|
+
void Backtrack.init({
|
|
17
|
+
bufferMinutes: 5,
|
|
18
|
+
showWidget: true,
|
|
19
|
+
network: { capturePayloads: false }
|
|
20
|
+
}).catch(error => {
|
|
21
|
+
console.warn('Backtrack não pôde iniciar', error);
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Inicialize antes das interações: a captura retroativa usa apenas o histórico já registrado. O widget permite salvar uma gravação. Abra o artefato no visualizador para investigar. A biblioteca não envia gravações para um servidor por padrão.
|
|
26
|
+
|
|
27
|
+
## Documentação
|
|
28
|
+
|
|
29
|
+
[English documentation](site/docs/content/en/overview.md) · [Português](site/docs/content/pt/overview.md)
|
|
30
|
+
|
|
31
|
+
- [Primeiros passos](site/docs/content/pt/quickstart.md) e [integrações](site/docs/content/pt/integrations.md).
|
|
32
|
+
- [Configuração](site/docs/content/pt/configuration.md), [privacidade e sanitização](site/docs/content/pt/privacy.md), [rede e disparos automáticos](site/docs/content/pt/network.md).
|
|
33
|
+
- [Visualizador](site/docs/content/pt/viewer.md), [source maps](site/docs/content/pt/source-maps.md), [exportação local](site/docs/content/pt/local-export.md) e [transportes](site/docs/content/pt/transport.md).
|
|
34
|
+
- [Referência da API](site/docs/content/pt/api.md), [solução de problemas](site/docs/content/pt/troubleshooting.md) e [versões e limitações](site/docs/content/pt/releases.md).
|
|
35
|
+
|
|
36
|
+
Os guias descrevem o contrato local da versão 0.5.3. Recursos ainda não implementados são identificados explicitamente. Dados sintéticos não desativam automaticamente a sanitização fixa.
|
|
37
|
+
|
|
38
|
+
## Desenvolvimento
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm install
|
|
42
|
+
npm run build
|
|
43
|
+
npm run build:viewer
|
|
44
|
+
npm run build:site
|
|
45
|
+
npm run preview:site
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Documentação local: http://localhost:4174/docs/
|
|
49
|
+
|
|
50
|
+
[Como editar e publicar o site](site/README.md). A saída do site é `dist/site`; a do visualizador permanece separada. Destinos: [backtrack-site.netlify.app](https://backtrack-site.netlify.app) (documentação) e [backtrack-viewer.netlify.app](https://backtrack-viewer.netlify.app) (visualizador).
|
|
51
|
+
|
|
52
|
+
Mudanças por versão: [CHANGELOG.md](CHANGELOG.md).
|
|
53
|
+
|
|
54
|
+
## Licença
|
|
55
|
+
|
|
56
|
+
[Backtrack Free-Use License](LICENSE)
|
package/bin/cli.cjs
CHANGED
|
@@ -7,6 +7,8 @@ Uso: backtrack [comando] [opções]
|
|
|
7
7
|
Comandos:
|
|
8
8
|
viewer Inicia o servidor local do Backtrack Viewer (padrão)
|
|
9
9
|
symbolize <incidente> [opções] Resolve stack traces usando source maps
|
|
10
|
+
export-context <incidente> Exporta relatórios locais em um pacote .tar
|
|
11
|
+
mcp --directory <pasta> Inicia MCP stdio local somente leitura
|
|
10
12
|
|
|
11
13
|
Opções globais:
|
|
12
14
|
-h, --help Mostra esta mensagem de ajuda
|
|
@@ -22,8 +24,9 @@ if (command === '--help' || command === '-h') {
|
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
if (command === 'symbolize') {
|
|
26
|
-
const
|
|
27
|
+
if (command === 'symbolize' || command === 'export-context' || command === 'mcp') {
|
|
28
|
+
const runner = command === 'symbolize' ? require('./symbolize.cjs') : command === 'mcp' ? require('./mcp.cjs') : require('./export-context.cjs');
|
|
29
|
+
const result = runner.run(process.argv.slice(3));
|
|
27
30
|
if (result instanceof Promise) {
|
|
28
31
|
result.then((code) => {
|
|
29
32
|
if (typeof code === 'number') {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { readLimitedFile } = require('./symbolize.cjs');
|
|
4
|
+
|
|
5
|
+
async function run(args = []) {
|
|
6
|
+
let input, output;
|
|
7
|
+
const options = {};
|
|
8
|
+
const flags = {
|
|
9
|
+
'--max-events': 'maxEvents', '--max-bytes': 'maxSizeBytes',
|
|
10
|
+
'--before': 'windowBeforeSeconds', '--after': 'windowAfterSeconds'
|
|
11
|
+
};
|
|
12
|
+
try {
|
|
13
|
+
for (let index = 0; index < args.length; index++) {
|
|
14
|
+
const arg = args[index];
|
|
15
|
+
if (arg === '--help' || arg === '-h') {
|
|
16
|
+
console.log('Uso: backtrack export-context <incidente.json[.gz]> [--output <arquivo.tar>] [--max-events <1..1000>] [--max-bytes <4096..1048576>] [--before <segundos>] [--after <segundos>]\nExporta relatórios locais mascarados; não executa source maps, não inclui a gravação original e não modifica AGENTS.md. Saídas existentes nunca são sobrescritas.');
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
if (arg === '--output' || Object.hasOwn(flags, arg)) {
|
|
20
|
+
const value = args[++index];
|
|
21
|
+
if (!value || value.startsWith('-')) throw new Error('Opção exige um valor.');
|
|
22
|
+
if (arg === '--output') output = value;
|
|
23
|
+
else {
|
|
24
|
+
if (!/^\d+(?:\.\d+)?$/.test(value) || !Number.isFinite(Number(value))) throw new Error('Limite numérico inválido.');
|
|
25
|
+
options[flags[arg]] = Number(value);
|
|
26
|
+
}
|
|
27
|
+
} else if (!arg.startsWith('-') && !input) input = arg;
|
|
28
|
+
else throw new Error('Argumento desconhecido ou duplicado.');
|
|
29
|
+
}
|
|
30
|
+
if (!input) throw new Error('Informe o incidente de entrada.');
|
|
31
|
+
const inputPath = path.resolve(input);
|
|
32
|
+
const outputPath = path.resolve(output || input.replace(/(?:\.ffr)?\.json(?:\.gz)?$/i, '') + '.context.tar');
|
|
33
|
+
if (fs.existsSync(outputPath)) throw new Error('O arquivo de saída já existe. Escolha outro caminho.');
|
|
34
|
+
const mod = require('../dist/index.cjs');
|
|
35
|
+
if (typeof mod.createIncidentContextPackage !== 'function' || typeof mod.decompressArtifact !== 'function') throw new Error('Build indisponível ou desatualizado. Execute o build da biblioteca.');
|
|
36
|
+
const artifact = await mod.decompressArtifact(new Uint8Array(readLimitedFile(inputPath)));
|
|
37
|
+
const bytes = mod.createIncidentContextPackage(artifact, options);
|
|
38
|
+
// Exclusive creation prevents replacement of input, links and unrelated files.
|
|
39
|
+
const fd = fs.openSync(outputPath, 'wx');
|
|
40
|
+
try { fs.writeFileSync(fd, bytes); }
|
|
41
|
+
catch (error) { fs.closeSync(fd); fs.unlinkSync(outputPath); throw error; }
|
|
42
|
+
fs.closeSync(fd);
|
|
43
|
+
console.log('[Backtrack] Pacote de contexto exportado. Extraia e comece em incident.md.');
|
|
44
|
+
return 0;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('[Backtrack] Falha na exportação: ' + error.message);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
module.exports = { run };
|
package/bin/mcp.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { readProjectFile } = require('./symbolize.cjs');
|
|
3
|
+
|
|
4
|
+
async function run(args = []) {
|
|
5
|
+
if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
|
|
6
|
+
console.log('Uso: backtrack mcp --directory <pasta-absoluta-de-incidentes>\nMCP stdio somente leitura. Lê apenas <incidentId>.json nessa pasta; não executa comandos, não resolve mapas e não modifica arquivos.');
|
|
7
|
+
return 0;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
if (args.length !== 2 || args[0] !== '--directory' || !path.isAbsolute(args[1])) throw new Error('Informe --directory absoluto.');
|
|
11
|
+
const { runIncidentMcp } = require('../dist/mcp.cjs');
|
|
12
|
+
await runIncidentMcp(args[1], readProjectFile);
|
|
13
|
+
return 0;
|
|
14
|
+
} catch {
|
|
15
|
+
console.error('[Backtrack MCP] Falha: confira --directory absoluto, pasta regular e build atualizado.');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
module.exports = { run };
|
package/bin/symbolize.cjs
CHANGED
|
@@ -1,12 +1,79 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const zlib = require('zlib');
|
|
4
|
+
const { execFileSync } = require('child_process');
|
|
4
5
|
const {
|
|
5
6
|
TraceMap,
|
|
6
7
|
originalPositionFor,
|
|
7
8
|
sourceContentFor
|
|
8
9
|
} = require('@jridgewell/trace-mapping');
|
|
9
10
|
|
|
11
|
+
const MAX_LOCAL_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
function withinProject(root, file) {
|
|
14
|
+
const relative = path.relative(root, file);
|
|
15
|
+
return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith('..' + path.sep));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function projectMapsDirectory(projectRoot, directory) {
|
|
19
|
+
const root = fs.realpathSync(projectRoot);
|
|
20
|
+
const candidate = path.resolve(directory);
|
|
21
|
+
if (!withinProject(root, candidate)) throw new Error('Diretório fora do projeto autorizado.');
|
|
22
|
+
const real = fs.realpathSync(candidate);
|
|
23
|
+
if (!withinProject(root, real) || !fs.statSync(real).isDirectory()) {
|
|
24
|
+
throw new Error('Diretório de mapas escapa do projeto autorizado.');
|
|
25
|
+
}
|
|
26
|
+
return { root, directory: real };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Bounded read from an explicitly selected regular file; rejects growth and replacement. */
|
|
30
|
+
function readLimitedFile(file, maxBytes = MAX_LOCAL_BYTES) {
|
|
31
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error('Limite de leitura inválido.');
|
|
32
|
+
const expected = fs.statSync(file);
|
|
33
|
+
if (!expected.isFile() || expected.size > maxBytes) throw new Error('Arquivo excede o limite de leitura ou não é regular.');
|
|
34
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
35
|
+
try {
|
|
36
|
+
const actual = fs.fstatSync(fd);
|
|
37
|
+
if (!actual.isFile() || actual.dev !== expected.dev || actual.ino !== expected.ino || actual.size !== expected.size) {
|
|
38
|
+
throw new Error('Arquivo mudou durante a leitura.');
|
|
39
|
+
}
|
|
40
|
+
const buffer = Buffer.alloc(actual.size + 1);
|
|
41
|
+
let size = 0;
|
|
42
|
+
while (size < buffer.length) {
|
|
43
|
+
const read = fs.readSync(fd, buffer, size, buffer.length - size, null);
|
|
44
|
+
if (!read) break;
|
|
45
|
+
size += read;
|
|
46
|
+
}
|
|
47
|
+
if (size > maxBytes || size > actual.size) throw new Error('Arquivo excede o limite ou mudou durante a leitura.');
|
|
48
|
+
return buffer.subarray(0, size);
|
|
49
|
+
} finally {
|
|
50
|
+
fs.closeSync(fd);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Relative references are data. Only this explicit local resolver may read them. */
|
|
55
|
+
function isProjectReference(reference) {
|
|
56
|
+
return typeof reference === 'string' && reference.length > 0 && !reference.includes('\0') &&
|
|
57
|
+
!path.posix.isAbsolute(reference) && !path.win32.isAbsolute(reference) && !reference.includes(':') &&
|
|
58
|
+
!reference.split(/[/\\]/).includes('..');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sourceMapProjectReference(reference) {
|
|
62
|
+
const relative = reference.replace(/^webpack:\/\/\/(?:\.\/)?/, '').replace(/^\.\//, '');
|
|
63
|
+
// Empty disables lookup; never turn an absolute/traversing map source into a local filename.
|
|
64
|
+
return isProjectReference(relative) ? relative : '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readProjectFile(projectRoot, reference, maxBytes = MAX_LOCAL_BYTES) {
|
|
68
|
+
if (!isProjectReference(reference)) throw new Error('Referência de arquivo inválida para o projeto.');
|
|
69
|
+
const root = fs.realpathSync(projectRoot);
|
|
70
|
+
const candidate = path.resolve(root, reference.replace(/\\/g, '/'));
|
|
71
|
+
if (!withinProject(root, candidate)) throw new Error('Arquivo fora do projeto autorizado.');
|
|
72
|
+
const real = fs.realpathSync(candidate);
|
|
73
|
+
if (!withinProject(root, real)) throw new Error('Arquivo escapa do projeto autorizado.');
|
|
74
|
+
return readLimitedFile(real, maxBytes).toString('utf8');
|
|
75
|
+
}
|
|
76
|
+
|
|
10
77
|
const FRAME_WITH_FUNCTION = /^\s*at\s+(.+?)\s+\((.+):(\d+):(\d+)\)\s*$/;
|
|
11
78
|
const FRAME_WITHOUT_FUNCTION_PARENS = /^\s*at\s+\((.+):(\d+):(\d+)\)\s*$/;
|
|
12
79
|
const FRAME_WITHOUT_FUNCTION = /^\s*at\s+(.+):(\d+):(\d+)\s*$/;
|
|
@@ -142,46 +209,30 @@ function normalizeSourceFile(rawSource) {
|
|
|
142
209
|
* @param {string} dir
|
|
143
210
|
* @returns {Map<string, string[]>}
|
|
144
211
|
*/
|
|
145
|
-
function findSourceMaps(dir) {
|
|
212
|
+
function findSourceMaps(dir, root) {
|
|
146
213
|
const mapFilesByBundle = new Map();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
214
|
+
const pending = [{ directory: dir, depth: 0 }];
|
|
215
|
+
let entriesCount = 0;
|
|
216
|
+
while (pending.length) {
|
|
217
|
+
const current = pending.pop();
|
|
218
|
+
if (current.depth > 100) throw new Error('Profundidade de mapas excede o limite.');
|
|
219
|
+
const checked = projectMapsDirectory(root, current.directory);
|
|
220
|
+
const entries = fs.readdirSync(checked.directory, { withFileTypes: true });
|
|
221
|
+
entriesCount += entries.length;
|
|
222
|
+
if (entriesCount > 100000) throw new Error('Quantidade de arquivos de mapas excede o limite.');
|
|
156
223
|
for (const entry of entries) {
|
|
157
|
-
|
|
224
|
+
// Do not follow links during discovery, including directory junctions.
|
|
225
|
+
if (entry.isSymbolicLink()) continue;
|
|
226
|
+
const fullPath = path.join(checked.directory, entry.name);
|
|
158
227
|
if (entry.isDirectory()) {
|
|
159
|
-
|
|
160
|
-
} else if (entry.isFile()) {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
bundleName = name.slice(0, -4);
|
|
165
|
-
} else if (name.endsWith('.mjs.map')) {
|
|
166
|
-
bundleName = name.slice(0, -4);
|
|
167
|
-
} else if (name.endsWith('.cjs.map')) {
|
|
168
|
-
bundleName = name.slice(0, -4);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (bundleName) {
|
|
172
|
-
if (!mapFilesByBundle.has(bundleName)) {
|
|
173
|
-
mapFilesByBundle.set(bundleName, []);
|
|
174
|
-
}
|
|
175
|
-
mapFilesByBundle.get(bundleName).push(fullPath);
|
|
176
|
-
}
|
|
228
|
+
pending.push({ directory: fullPath, depth: current.depth + 1 });
|
|
229
|
+
} else if (entry.isFile() && /\.(?:js|mjs|cjs)\.map$/.test(entry.name)) {
|
|
230
|
+
const bundleName = entry.name.slice(0, -4);
|
|
231
|
+
if (!mapFilesByBundle.has(bundleName)) mapFilesByBundle.set(bundleName, []);
|
|
232
|
+
mapFilesByBundle.get(bundleName).push(fullPath);
|
|
177
233
|
}
|
|
178
234
|
}
|
|
179
235
|
}
|
|
180
|
-
|
|
181
|
-
if (dir && typeof dir === 'string' && fs.existsSync(dir)) {
|
|
182
|
-
walk(dir);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
236
|
return mapFilesByBundle;
|
|
186
237
|
}
|
|
187
238
|
|
|
@@ -211,6 +262,10 @@ function extractContext(map, rawSource, originalLine, contextLinesOption) {
|
|
|
211
262
|
};
|
|
212
263
|
}
|
|
213
264
|
|
|
265
|
+
return extractTextContext(content, originalLine, contextLinesOption);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function extractTextContext(content, originalLine, contextLinesOption) {
|
|
214
269
|
const allLines = content.split(/\r?\n/);
|
|
215
270
|
const lineIdx = originalLine - 1; // 1-based para 0-based
|
|
216
271
|
|
|
@@ -252,7 +307,7 @@ function extractContext(map, rawSource, originalLine, contextLinesOption) {
|
|
|
252
307
|
/**
|
|
253
308
|
* Resolve os frames de uma stack V8 utilizando os source maps disponíveis.
|
|
254
309
|
* @param {string} stack
|
|
255
|
-
* @param {{ sourceMapsDirectory: string, contextLines?: number }} options
|
|
310
|
+
* @param {{ sourceMapsDirectory: string, projectRoot?: string, contextLines?: number }} options
|
|
256
311
|
* @returns {{
|
|
257
312
|
* frames: Array<{
|
|
258
313
|
* generatedFile: string,
|
|
@@ -284,12 +339,23 @@ function symbolicateStack(stack, options) {
|
|
|
284
339
|
const sourceMapsDir = options?.sourceMapsDirectory;
|
|
285
340
|
const contextLines = options?.contextLines;
|
|
286
341
|
|
|
287
|
-
const
|
|
342
|
+
const projectRoot = options?.projectRoot ?? process.cwd();
|
|
343
|
+
let bundleMapFiles = new Map();
|
|
344
|
+
try {
|
|
345
|
+
const checked = projectMapsDirectory(projectRoot, sourceMapsDir);
|
|
346
|
+
bundleMapFiles = findSourceMaps(checked.directory, checked.root);
|
|
347
|
+
} catch (error) {
|
|
348
|
+
result.warnings.push(error.message);
|
|
349
|
+
}
|
|
288
350
|
const traceMapCache = new Map();
|
|
289
351
|
|
|
290
352
|
const lines = stack.split(/\r?\n/);
|
|
291
353
|
|
|
292
354
|
for (const line of lines) {
|
|
355
|
+
if (result.frames.length >= 100) {
|
|
356
|
+
result.warnings.push('Resolução limitada a 100 frames; a stack original permanece preservada.');
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
293
359
|
const parsed = parseStackFrame(line);
|
|
294
360
|
if (!parsed) {
|
|
295
361
|
continue;
|
|
@@ -324,7 +390,7 @@ function symbolicateStack(stack, options) {
|
|
|
324
390
|
let cached = traceMapCache.get(mapPath);
|
|
325
391
|
if (!cached) {
|
|
326
392
|
try {
|
|
327
|
-
const rawContent = fs.
|
|
393
|
+
const rawContent = readProjectFile(projectRoot, path.relative(fs.realpathSync(projectRoot), mapPath));
|
|
328
394
|
const parsedJson = JSON.parse(rawContent);
|
|
329
395
|
const declaredBundle = parsedJson.file && typeof parsedJson.file === 'string'
|
|
330
396
|
? extractBundleName(parsedJson.file)
|
|
@@ -377,6 +443,7 @@ function symbolicateStack(stack, options) {
|
|
|
377
443
|
generatedLine,
|
|
378
444
|
generatedColumn,
|
|
379
445
|
sourceFile: normalizeSourceFile(original.source),
|
|
446
|
+
sourceReference: sourceMapProjectReference(original.source),
|
|
380
447
|
sourceLine: original.line,
|
|
381
448
|
sourceColumn: original.column,
|
|
382
449
|
...((original.name ?? functionName) ? { functionName: original.name ?? functionName } : {})
|
|
@@ -385,6 +452,7 @@ function symbolicateStack(stack, options) {
|
|
|
385
452
|
const { context, warning } = extractContext(traceMap, original.source, original.line, contextLines);
|
|
386
453
|
if (context) {
|
|
387
454
|
resolvedFrame.context = context;
|
|
455
|
+
resolvedFrame.contextOrigin = 'source_map';
|
|
388
456
|
}
|
|
389
457
|
if (warning) {
|
|
390
458
|
result.warnings.push(warning);
|
|
@@ -402,6 +470,59 @@ function symbolicateStack(stack, options) {
|
|
|
402
470
|
* @param {Buffer} buffer
|
|
403
471
|
* @returns {boolean}
|
|
404
472
|
*/
|
|
473
|
+
/** Compare complete identities only; imported strings never become Git arguments. */
|
|
474
|
+
function compareCommits(captureCommit, localCommit, workingTree = 'unknown') {
|
|
475
|
+
const fullCommit = value => typeof value === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i.test(value);
|
|
476
|
+
const commitMatch = fullCommit(captureCommit) && fullCommit(localCommit)
|
|
477
|
+
? (captureCommit.toLowerCase() === localCommit.toLowerCase() ? 'matched' : 'divergent')
|
|
478
|
+
: 'unknown';
|
|
479
|
+
return { version: 1, ...(captureCommit !== undefined ? { captureCommit } : {}),
|
|
480
|
+
...(fullCommit(localCommit) ? { localCommit } : {}), commitMatch, workingTree };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function inspectLocalVersion(projectRoot, captureCommit) {
|
|
484
|
+
// Fixed, bounded, read-only Git operations; do not inherit Git routing overrides.
|
|
485
|
+
const env = { ...process.env };
|
|
486
|
+
for (const key of Object.keys(env)) if (key.toUpperCase().startsWith('GIT_')) delete env[key];
|
|
487
|
+
const options = { cwd: projectRoot, encoding: 'utf8', timeout: 5000,
|
|
488
|
+
maxBuffer: 1024 * 1024, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env };
|
|
489
|
+
let localCommit;
|
|
490
|
+
let workingTree = 'unknown';
|
|
491
|
+
try {
|
|
492
|
+
localCommit = execFileSync('git', ['-c', 'core.fsmonitor=false', 'rev-parse', '--verify', 'HEAD'], options).trim();
|
|
493
|
+
try {
|
|
494
|
+
workingTree = execFileSync('git', ['-c', 'core.fsmonitor=false', 'status', '--porcelain', '--untracked-files=normal'], options).trim()
|
|
495
|
+
? 'modified' : 'clean';
|
|
496
|
+
} catch {}
|
|
497
|
+
} catch {}
|
|
498
|
+
return compareCommits(captureCommit, localCommit, workingTree);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function enrichLocalFrames(frames, projectRoot, contextLines, warnings) {
|
|
502
|
+
return frames?.map(frame => {
|
|
503
|
+
// Refresh checkout excerpts on reruns; preserve imported/map context independently.
|
|
504
|
+
const { localContext: _previous, localContextTruncated: _truncated, ...reference } = frame;
|
|
505
|
+
if (!frame.sourceFile || !Number.isSafeInteger(frame.sourceLine)) return reference;
|
|
506
|
+
const rawReference = frame.sourceReference ?? frame.sourceFile;
|
|
507
|
+
const localReference = rawReference.replace(/^webpack:\/\/\/(?:\.\/)?/, '').replace(/^\.\//, '');
|
|
508
|
+
try {
|
|
509
|
+
const content = readProjectFile(projectRoot, localReference, 1024 * 1024);
|
|
510
|
+
const { context } = extractTextContext(content, frame.sourceLine, contextLines);
|
|
511
|
+
if (!context) {
|
|
512
|
+
warnings.push('Linha local indisponível: ' + frame.sourceFile + ':' + frame.sourceLine);
|
|
513
|
+
return reference;
|
|
514
|
+
}
|
|
515
|
+
const allLines = content.split(/\r?\n/);
|
|
516
|
+
const radius = Math.max(0, Math.min(10, Number.isFinite(contextLines) ? contextLines : 3));
|
|
517
|
+
const selected = allLines.slice(Math.max(0, frame.sourceLine - 1 - radius), frame.sourceLine + radius);
|
|
518
|
+
return { ...reference, localContext: context, localContextTruncated: selected.some(line => line.length > 2000) };
|
|
519
|
+
} catch {
|
|
520
|
+
warnings.push('Trecho local indisponível ou referência bloqueada: ' + frame.sourceFile);
|
|
521
|
+
return reference;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
405
526
|
function isGzip(buffer) {
|
|
406
527
|
return Buffer.isBuffer(buffer) && buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
|
|
407
528
|
}
|
|
@@ -441,6 +562,7 @@ Argumentos obrigatórios:
|
|
|
441
562
|
--sourcemaps <diretório> Diretório contendo os arquivos .map
|
|
442
563
|
|
|
443
564
|
Opções adicionais:
|
|
565
|
+
--project-root <diretório> Projeto autorizado para leitura de mapas (padrão: diretório atual)
|
|
444
566
|
--release <versão> Versão da release para validação de compatibilidade
|
|
445
567
|
--output <arquivo> Caminho do arquivo de saída (padrão: <incidente>.symbolicated.ffr.json)
|
|
446
568
|
--context-lines <número> Quantidade de linhas de contexto antes e depois do erro (0-10, padrão: 3)
|
|
@@ -478,6 +600,7 @@ async function run(args = []) {
|
|
|
478
600
|
let outputPath = null;
|
|
479
601
|
let releaseArg = null;
|
|
480
602
|
let contextLines = 3;
|
|
603
|
+
let projectRoot = process.cwd();
|
|
481
604
|
|
|
482
605
|
for (let i = 0; i < args.length; i++) {
|
|
483
606
|
const arg = args[i];
|
|
@@ -487,6 +610,13 @@ async function run(args = []) {
|
|
|
487
610
|
}
|
|
488
611
|
if (arg === '--sourcemaps') {
|
|
489
612
|
sourcemapsDir = args[++i];
|
|
613
|
+
} else if (arg === '--project-root') {
|
|
614
|
+
projectRoot = args[++i];
|
|
615
|
+
if (!projectRoot || projectRoot.startsWith('-')) {
|
|
616
|
+
console.error('[Backtrack] Erro: Opção --project-root exige um diretório.');
|
|
617
|
+
process.exitCode = 1;
|
|
618
|
+
return 1;
|
|
619
|
+
}
|
|
490
620
|
} else if (arg === '--output') {
|
|
491
621
|
outputPath = args[++i];
|
|
492
622
|
} else if (arg === '--release') {
|
|
@@ -549,7 +679,7 @@ async function run(args = []) {
|
|
|
549
679
|
|
|
550
680
|
let bytes;
|
|
551
681
|
try {
|
|
552
|
-
bytes =
|
|
682
|
+
bytes = readLimitedFile(incidentPath);
|
|
553
683
|
} catch (err) {
|
|
554
684
|
console.error(`[Backtrack] Erro ao ler o arquivo de incidente: ${err.message}`);
|
|
555
685
|
process.exitCode = 1;
|
|
@@ -559,7 +689,7 @@ async function run(args = []) {
|
|
|
559
689
|
let json;
|
|
560
690
|
try {
|
|
561
691
|
json = isGzip(bytes)
|
|
562
|
-
? zlib.gunzipSync(bytes).toString('utf8')
|
|
692
|
+
? zlib.gunzipSync(bytes, { maxOutputLength: MAX_LOCAL_BYTES }).toString('utf8')
|
|
563
693
|
: bytes.toString('utf8');
|
|
564
694
|
} catch (err) {
|
|
565
695
|
console.error(`[Backtrack] Erro ao descompactar incidente: ${err.message}`);
|
|
@@ -584,31 +714,31 @@ async function run(args = []) {
|
|
|
584
714
|
}
|
|
585
715
|
|
|
586
716
|
const validate = await loadValidator();
|
|
587
|
-
if (validate) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
}
|
|
717
|
+
if (!validate) {
|
|
718
|
+
console.error('[Backtrack] Erro: Validador indisponível. Execute o build antes de importar incidentes.');
|
|
719
|
+
process.exitCode = 1;
|
|
720
|
+
return 1;
|
|
721
|
+
}
|
|
722
|
+
const valResult = validate(artifact);
|
|
723
|
+
if (!valResult.success) {
|
|
724
|
+
console.error('[Backtrack] Erro: Artefato inválido:');
|
|
725
|
+
for (const errorMsg of valResult.errors) console.error(' - ' + errorMsg);
|
|
726
|
+
process.exitCode = 1;
|
|
727
|
+
return 1;
|
|
728
|
+
}
|
|
729
|
+
artifact = valResult.data;
|
|
730
|
+
try {
|
|
731
|
+
const checked = projectMapsDirectory(projectRoot, sourcemapsDir);
|
|
732
|
+
projectRoot = checked.root;
|
|
733
|
+
sourcemapsDir = checked.directory;
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error('[Backtrack] Erro: ' + error.message);
|
|
736
|
+
process.exitCode = 1;
|
|
737
|
+
return 1;
|
|
609
738
|
}
|
|
610
739
|
|
|
611
740
|
const allWarnings = [];
|
|
741
|
+
const codeContext = inspectLocalVersion(projectRoot, artifact.environment.gitCommit);
|
|
612
742
|
|
|
613
743
|
const artifactRelease = artifact.context && typeof artifact.context.release === 'string'
|
|
614
744
|
? artifact.context.release
|
|
@@ -649,7 +779,7 @@ async function run(args = []) {
|
|
|
649
779
|
const hasStack = typeof event.stack === 'string' && event.stack.trim().length > 0;
|
|
650
780
|
const hasComponentStack = typeof event.componentStack === 'string' && event.componentStack.trim().length > 0;
|
|
651
781
|
|
|
652
|
-
if (!hasStack && !hasComponentStack) {
|
|
782
|
+
if (!hasStack && !hasComponentStack && !event.resolvedStack?.length && !event.resolvedComponentStack?.length) {
|
|
653
783
|
return event;
|
|
654
784
|
}
|
|
655
785
|
|
|
@@ -660,6 +790,7 @@ async function run(args = []) {
|
|
|
660
790
|
if (hasStack) {
|
|
661
791
|
const stackResult = symbolicateStack(event.stack, {
|
|
662
792
|
sourceMapsDirectory: sourcemapsDir,
|
|
793
|
+
projectRoot,
|
|
663
794
|
contextLines
|
|
664
795
|
});
|
|
665
796
|
stackResolvedCount = stackResult.resolvedCount;
|
|
@@ -676,6 +807,7 @@ async function run(args = []) {
|
|
|
676
807
|
if (hasComponentStack) {
|
|
677
808
|
const componentResult = symbolicateStack(event.componentStack, {
|
|
678
809
|
sourceMapsDirectory: sourcemapsDir,
|
|
810
|
+
projectRoot,
|
|
679
811
|
contextLines
|
|
680
812
|
});
|
|
681
813
|
componentResolvedCount = componentResult.resolvedCount;
|
|
@@ -702,10 +834,10 @@ async function run(args = []) {
|
|
|
702
834
|
return {
|
|
703
835
|
...event,
|
|
704
836
|
...(resolvedStack && resolvedStack.length > 0
|
|
705
|
-
? { resolvedStack }
|
|
837
|
+
? { resolvedStack: enrichLocalFrames(resolvedStack, projectRoot, contextLines, allWarnings) }
|
|
706
838
|
: {}),
|
|
707
839
|
...(resolvedComponentStack && resolvedComponentStack.length > 0
|
|
708
|
-
? { resolvedComponentStack }
|
|
840
|
+
? { resolvedComponentStack: enrichLocalFrames(resolvedComponentStack, projectRoot, contextLines, allWarnings) }
|
|
709
841
|
: {})
|
|
710
842
|
};
|
|
711
843
|
});
|
|
@@ -713,7 +845,9 @@ async function run(args = []) {
|
|
|
713
845
|
const totalResolved = exceptionResolvedFrames + reactResolvedFrames;
|
|
714
846
|
const totalUnresolved = exceptionUnresolvedFrames + reactUnresolvedFrames;
|
|
715
847
|
|
|
716
|
-
|
|
848
|
+
const hasExistingReferences = newTimeline.some(event => event.type === 'error' &&
|
|
849
|
+
(event.resolvedStack?.length || event.resolvedComponentStack?.length));
|
|
850
|
+
if (totalResolved === 0 && totalUnresolved === 0 && !hasExistingReferences) {
|
|
717
851
|
console.log('[Backtrack] Nenhuma stack compatível foi encontrada.');
|
|
718
852
|
process.exitCode = 0;
|
|
719
853
|
return 0;
|
|
@@ -728,6 +862,8 @@ async function run(args = []) {
|
|
|
728
862
|
}
|
|
729
863
|
|
|
730
864
|
console.log(`Incidente: ${artifact.incident.id}`);
|
|
865
|
+
console.log(`Commits: captura=${codeContext.captureCommit ?? 'desconhecido'}; local=${codeContext.localCommit ?? 'desconhecido'}; correspondência=${codeContext.commitMatch}; checkout=${codeContext.workingTree}`);
|
|
866
|
+
console.log('Referências são pontos de investigação; trechos locais têm correspondência não confirmada.');
|
|
731
867
|
if (releaseStatusMsg) {
|
|
732
868
|
console.log(releaseStatusMsg);
|
|
733
869
|
}
|
|
@@ -747,8 +883,15 @@ async function run(args = []) {
|
|
|
747
883
|
|
|
748
884
|
const symbolicatedArtifact = {
|
|
749
885
|
...artifact,
|
|
750
|
-
timeline: newTimeline
|
|
886
|
+
timeline: newTimeline,
|
|
887
|
+
codeContext
|
|
751
888
|
};
|
|
889
|
+
const outputValidation = validate(symbolicatedArtifact);
|
|
890
|
+
if (!outputValidation.success) {
|
|
891
|
+
console.error('[Backtrack] Erro: enriquecimento excede os limites ou produziu dados inválidos.');
|
|
892
|
+
process.exitCode = 1;
|
|
893
|
+
return 1;
|
|
894
|
+
}
|
|
752
895
|
|
|
753
896
|
const tempPath = `${resolvedOutput}.tmp`;
|
|
754
897
|
try {
|
|
@@ -786,6 +929,11 @@ if (require.main === module) {
|
|
|
786
929
|
module.exports = {
|
|
787
930
|
parseStackFrame,
|
|
788
931
|
symbolicateStack,
|
|
932
|
+
compareCommits,
|
|
933
|
+
inspectLocalVersion,
|
|
934
|
+
enrichLocalFrames,
|
|
935
|
+
readProjectFile,
|
|
936
|
+
readLimitedFile,
|
|
789
937
|
FRAME_WITH_FUNCTION,
|
|
790
938
|
FRAME_WITHOUT_FUNCTION,
|
|
791
939
|
extractBundleName,
|