Lanterna captures profiling data from the generated JavaScript that V8 actually executes. When the app is written in TypeScript (or bundled), the raw file:line coordinates point at compiled output (dist/, build/, bundle chunks) — not at the source you would edit.
Source-map resolution joins each frame back to its original source position so consumers (humans and AI agents) can open the right file and propose patches against the right line.
Source maps are on by default. Disable with:
lanterna run --no-source-maps -- node dist/server.js
lanterna attach --pid 4242 --no-source-mapsDisable when:
- the project ships JS only (no maps to find — keep the integrity counters clean),
- map files are huge and analysis time matters more than precision,
- you want to verify a frame's raw V8 location against the bundle.
For every unique generated URL seen during capture, Lanterna:
- Reads the tail of the JS file looking for a
//# sourceMappingURL=…comment. - If the URL is a sibling file (e.g.
foo.js.map), reads it. - If it is a
data:URL (application/jsonbase64 or uri-encoded), decodes inline. - If it is a remote
http(s)://URL and remote fetching is enabled (--source-map-remote/sourceMapRemote: true), reads it from the pre-fetched cache (see below). Off by default. - Loads the parsed JSON into a
TraceMap(@jridgewell/trace-mapping) for O(log n) lookups.
Maps larger than 50 MiB are skipped.
A bundle deployed to disk can point its //# sourceMappingURL at a CDN (https://…). Fetching that is off by default because it is network egress from the machine running Lanterna. Enable it with --source-map-remote (or "sourceMapRemote": true in .lanterna.json, or sourceMapRemote: true on the programmatic API).
Because frame resolution is synchronous, Lanterna fetches remote maps once, up front (bounded by a 3 s timeout and the 50 MiB cap) into an in-memory cache, then resolves against that cache. A failed or oversized fetch is skipped silently and the frame stays at unsupported-mapping-url. Only enable it for maps you trust — see security-and-privacy.md.
Whenever a frame is mapped successfully, a source object is attached:
{
"file": "src/server.ts",
"line": 42,
"column": 18,
"name": "handleRequest"
}fileis relative to the capture cwd when the source is on disk; otherwise the raw map source URL is kept verbatim (e.g.webpack://app/src/server.ts,vite:/src/server.ts).namecomes from the source map'snamesarray — useful when the generatedfunctionis(anonymous).columnis 1-based (matches Lanterna's frame convention).
source appears on:
profiles.cpu.hotspots[].sourceprofiles.cpu.summary.topCpuCulprit.sourceprofiles.cpu.summary.topRequestEntry.sourceprofiles.cpu.summary.topUserHotspot.sourceprofiles.cpu.hotStacks[].frames[].sourceprofiles.cpu.hotStackClusters[].anchor.sourceprofiles.memory.hotAllocators[].sourceand the memory summaryprofiles.async.*frame-bearing entries such as top operations, chains, orphans, CDP async contexts, and async CPU attributionfindings[].evidence.source
{
"enabled": true,
"applicable": true,
"status": "partial",
"framesResolved": 1842,
"framesUnresolved": 211,
"coverage": 0.897,
"mapsLoaded": 14,
"failures": [
{ "url": "file:///app/dist/legacy.js", "reason": "map-read-failed: ENOENT" }
]
}For plain JavaScript files without a sourceMappingURL, source maps are not applicable: Lanterna reports applicable: false, status: "not-applicable", coverage: 1, and does not treat those frames as unresolved. Missing, unreadable, or invalid referenced maps remain applicable failures.
Use coverage as a quality gate. Below ~0.7, prefer raw file:line and warn the reader. The failures[] array is capped at 20 entries — informative, not exhaustive.
Failure reasons:
| reason | meaning |
|---|---|
not-file-url |
URL was not file:// (e.g. node:internal/...). Filtered out — never appears in failures. |
no-mapping-url |
JS file has no sourceMappingURL comment. Filtered out. |
js-read-failed |
JS file could not be read (permissions, deletion mid-capture). |
map-read-failed |
sourceMappingURL pointed at a missing/unreadable file. |
map-parse-failed |
Map JSON or inline data URL is malformed. |
map-too-large |
Map exceeded the 50 MiB cap. |
unsupported-mapping-url |
Remote scheme (http(s)://) not fetched — either --source-map-remote was off, or the fetch failed/timed out/was too large. Other non-file schemes are also reported here. |
When consuming the JSON:
- Always prefer
source.file:source.lineoverfile:linewhensourceis present. Patches based on the generated coordinates will edit the wrong file (or no file at all). - Fall back gracefully to the generated
file:linewhensourceis absent — that means the frame had no map (e.g. anode:builtin, a stripped bundle). - Treat virtual paths as untrusted. A
source.filewith a bundler scheme (webpack://,vite:/...) is the bundler's logical path; it may not exist on disk. Verify via filesystem before quoting it as a fix location. - Use
source.namewhenfunctionis(anonymous)— the original symbol name often survives in the map. - Check
meta.captureIntegrity.sourceMapsbefore stating "the hotspot is atsrc/foo.ts:42". Ifapplicable !== falseand coverage is low, most frames were not mapped; the few that were may still be misleading without the surrounding context.applicable: falsemeans plain JS without source maps, not degraded mapping.
- report-schema.md — full type definitions
- reading-a-report.md — interpretation playbook
- signal-quality.md — the integrity model