@backtrack-js/browser 0.5.2 → 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.
Files changed (106) hide show
  1. package/README.md +56 -54
  2. package/bin/cli.cjs +5 -2
  3. package/bin/export-context.cjs +50 -0
  4. package/bin/mcp.cjs +19 -0
  5. package/bin/symbolize.cjs +214 -66
  6. package/dist/capturers/browser.d.ts +18 -0
  7. package/dist/capturers/console.d.ts +0 -1
  8. package/dist/capturers/errors.d.ts +3 -2
  9. package/dist/capturers/index.d.ts +0 -1
  10. package/dist/capturers/navigation.d.ts +0 -1
  11. package/dist/capturers/network.d.ts +11 -2
  12. package/dist/capturers/performance.d.ts +0 -1
  13. package/dist/capturers/route-matcher.d.ts +0 -1
  14. package/dist/capturers/rrweb.d.ts +2 -1
  15. package/dist/capturers/sanitizer.d.ts +1 -1
  16. package/dist/core/flight-recorder.d.ts +9 -1
  17. package/dist/core/state-machine.d.ts +0 -1
  18. package/dist/dev-server/index.d.ts +0 -1
  19. package/dist/dev-server/middleware.d.ts +0 -1
  20. package/dist/dev-server.cjs +3 -7
  21. package/dist/dev-server.js +128 -91
  22. package/dist/incident-reports-9iXHIaBD.js +1242 -0
  23. package/dist/incident-reports-Cjos7N-u.cjs +51 -0
  24. package/dist/index.cjs +53 -39
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.js +2357 -2080
  27. package/dist/mcp/server.d.ts +5 -0
  28. package/dist/mcp.cjs +2 -0
  29. package/dist/mcp.js +203 -0
  30. package/dist/storage/batch-writer.d.ts +6 -1
  31. package/dist/storage/db.d.ts +0 -1
  32. package/dist/storage/incident-manager.d.ts +10 -7
  33. package/dist/storage/index.d.ts +0 -1
  34. package/dist/storage/retention.d.ts +0 -1
  35. package/dist/storage/session.d.ts +0 -1
  36. package/dist/types/artifact.d.ts +50 -1
  37. package/dist/types/browser-context.d.ts +34 -0
  38. package/dist/types/chunk.d.ts +0 -1
  39. package/dist/types/health.d.ts +0 -1
  40. package/dist/types/incident.d.ts +0 -1
  41. package/dist/types/index.d.ts +1 -1
  42. package/dist/types/options.d.ts +15 -1
  43. package/dist/types/timeline.d.ts +11 -1
  44. package/dist/types/transport.d.ts +0 -1
  45. package/dist/types/visual-proposal.d.ts +1 -0
  46. package/dist/utils/compression.d.ts +0 -1
  47. package/dist/utils/context-package.d.ts +4 -0
  48. package/dist/utils/gist-uploader.d.ts +0 -1
  49. package/dist/utils/incident-query.d.ts +93 -0
  50. package/dist/utils/incident-reports.d.ts +18 -0
  51. package/dist/utils/interactions.d.ts +13 -0
  52. package/dist/utils/markdown.d.ts +3 -1
  53. package/dist/validation/structure.d.ts +11 -0
  54. package/dist/validation/validate.d.ts +3 -2
  55. package/dist/viewer/assets/index-BNTvj2zy.js +337 -0
  56. package/dist/viewer/assets/{index-092olPra.css → index-DXHvcqkw.css} +1 -1
  57. package/dist/viewer/index.html +2 -2
  58. package/dist/widget/annotator.d.ts +0 -1
  59. package/dist/widget/element-editor.d.ts +7 -0
  60. package/dist/widget/styles.d.ts +0 -1
  61. package/dist/widget/widget.d.ts +9 -3
  62. package/package.json +113 -90
  63. package/dist/capturers/console.d.ts.map +0 -1
  64. package/dist/capturers/errors.d.ts.map +0 -1
  65. package/dist/capturers/index.d.ts.map +0 -1
  66. package/dist/capturers/navigation.d.ts.map +0 -1
  67. package/dist/capturers/network.d.ts.map +0 -1
  68. package/dist/capturers/performance.d.ts.map +0 -1
  69. package/dist/capturers/route-matcher.d.ts.map +0 -1
  70. package/dist/capturers/rrweb.d.ts.map +0 -1
  71. package/dist/capturers/sanitizer.d.ts.map +0 -1
  72. package/dist/core/flight-recorder.d.ts.map +0 -1
  73. package/dist/core/state-machine.d.ts.map +0 -1
  74. package/dist/dev-server/index.d.ts.map +0 -1
  75. package/dist/dev-server/middleware.d.ts.map +0 -1
  76. package/dist/dev-server.cjs.map +0 -1
  77. package/dist/dev-server.js.map +0 -1
  78. package/dist/index.cjs.map +0 -1
  79. package/dist/index.d.ts.map +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/markdown-Ao5Ah9MF.cjs +0 -9
  82. package/dist/markdown-Ao5Ah9MF.cjs.map +0 -1
  83. package/dist/markdown-D3C94OlY.js +0 -494
  84. package/dist/markdown-D3C94OlY.js.map +0 -1
  85. package/dist/storage/batch-writer.d.ts.map +0 -1
  86. package/dist/storage/db.d.ts.map +0 -1
  87. package/dist/storage/incident-manager.d.ts.map +0 -1
  88. package/dist/storage/index.d.ts.map +0 -1
  89. package/dist/storage/retention.d.ts.map +0 -1
  90. package/dist/storage/session.d.ts.map +0 -1
  91. package/dist/types/artifact.d.ts.map +0 -1
  92. package/dist/types/chunk.d.ts.map +0 -1
  93. package/dist/types/health.d.ts.map +0 -1
  94. package/dist/types/incident.d.ts.map +0 -1
  95. package/dist/types/index.d.ts.map +0 -1
  96. package/dist/types/options.d.ts.map +0 -1
  97. package/dist/types/timeline.d.ts.map +0 -1
  98. package/dist/types/transport.d.ts.map +0 -1
  99. package/dist/utils/compression.d.ts.map +0 -1
  100. package/dist/utils/gist-uploader.d.ts.map +0 -1
  101. package/dist/utils/markdown.d.ts.map +0 -1
  102. package/dist/validation/validate.d.ts.map +0 -1
  103. package/dist/viewer/assets/index-D8m3bwRf.js +0 -197
  104. package/dist/widget/annotator.d.ts.map +0 -1
  105. package/dist/widget/styles.d.ts.map +0 -1
  106. package/dist/widget/widget.d.ts.map +0 -1
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.1. 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. A documentação ainda precisa de um destino de publicação.
51
-
52
- ## Licença
53
-
54
- [Backtrack Free-Use License](LICENSE)
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 result = require('./symbolize.cjs').run(process.argv.slice(3));
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
- function walk(currentDir) {
149
- let entries;
150
- try {
151
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
152
- } catch {
153
- return;
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
- const fullPath = path.join(currentDir, entry.name);
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
- walk(fullPath);
160
- } else if (entry.isFile()) {
161
- const name = entry.name;
162
- let bundleName = null;
163
- if (name.endsWith('.js.map')) {
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 bundleMapFiles = findSourceMaps(sourceMapsDir);
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.readFileSync(mapPath, 'utf-8');
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 = fs.readFileSync(incidentPath);
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
- const valResult = validate(artifact);
589
- if (!valResult.success) {
590
- console.error('[Backtrack] Erro: Artefato inválido:');
591
- for (const errorMsg of valResult.errors) {
592
- console.error(` - ${errorMsg}`);
593
- }
594
- process.exitCode = 1;
595
- return 1;
596
- }
597
- } else {
598
- if (
599
- !artifact ||
600
- typeof artifact !== 'object' ||
601
- !Array.isArray(artifact.timeline) ||
602
- !artifact.incident ||
603
- typeof artifact.incident.id !== 'string'
604
- ) {
605
- console.error('[Backtrack] Erro: Artefato inválido: estrutura básica incorreta.');
606
- process.exitCode = 1;
607
- return 1;
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
- if (totalResolved === 0 && totalUnresolved === 0) {
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,