Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Send never waits for the source-map lookup.** The page hands the lookup to the worker with the
note, and the worker resolves it while posting, so navigating right after Send on a production
build can no longer lose the note.

## [0.7.2] — 2026-10-21

### Added
Expand Down
9 changes: 9 additions & 0 deletions extension/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,15 @@ api.runtime.onMessage.addListener((msg, sender, sendResponse) => {
case "submit": {
if (sender.tab) msg.annotation.page = { ...msg.annotation.page, topUrl: sender.tab.url || null, frameId: sender.frameId || 0 };
msg.annotation.context = pageContext(sender.tab?.id, msg.annotation.context);
// The page handed over its source-map probe instead of waiting for it, so a navigation right
// after Send can't lose the note. Resolved here (this worker outlives the page), same rules
// as the "sourcemap" message: local dev pages only, origin from the browser, never the page.
const probe = msg.annotation.mapProbe;
delete msg.annotation.mapProbe;
if (probe && typeof probe.snippet === "string" && Array.isArray(probe.scripts) && sender.url && isLocalDev(sender.url) && globalThis.pinpointSourceMap) {
const r = await globalThis.pinpointSourceMap.resolve({ snippet: probe.snippet.slice(0, 400), scripts: probe.scripts.slice(0, 30) }, sender.url).catch(() => null);
globalThis.pinpointSourceMap.applyToNote(msg.annotation, r, typeof probe.name === "string" ? probe.name : null);
}
const result = await bridgeFetch("/annotations", {
method: "POST",
headers: { "content-type": "application/json" },
Expand Down
25 changes: 10 additions & 15 deletions extension/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -694,30 +694,24 @@
if (!mapLookups.has(el)) mapLookups.set(el, mapSource(el).catch(() => null));
return mapLookups.get(el);
}
async function mapSource(el) {
// The component's minified code and the page's scripts, from inspector.js (synchronous).
function probeOf(el) {
let raw = null;
const on = (e) => { raw = e.detail; };
el.addEventListener("pinpoint:probe-result", on, { once: true });
el.dispatchEvent(new CustomEvent("pinpoint:probe", { bubbles: true }));
el.removeEventListener("pinpoint:probe-result", on);
let probe = null;
try { probe = JSON.parse(raw); } catch {}
if (typeof probe?.snippet !== "string") return null;
return typeof probe?.snippet === "string" ? probe : null;
}
async function mapSource(el) {
const probe = probeOf(el);
if (!probe) return null;
const r = await chrome.runtime.sendMessage({ type: "sourcemap", snippet: probe.snippet, scripts: probe.scripts }).catch(() => null);
if (typeof r?.file !== "string") return null;
return { ...r, fnName: probe.name };
}
// `reuse` (element.reuse) was counted under the same minified name, so it is renamed too.
async function applyMapSource(el, hint, reuse) {
const r = await Promise.race([mapLookup(el), new Promise((res) => setTimeout(res, 4300))]); // the worker stops at 4 s
if (!r) return;
Object.assign(hint, { file: r.file, line: r.line ?? null, column: r.column ?? null, method: "sourcemap" });
if (r.name) {
const i = hint.components.indexOf(r.fnName);
if (i >= 0) hint.components[i] = r.name; else hint.components.unshift(r.name);
if (reuse?.component === r.fnName) reuse.component = r.name;
}
}

// The page's recent uncaught errors, from inspector.js in the MAIN world. Raw and untrusted: the
// worker validates and redacts them before they reach the bridge.
Expand Down Expand Up @@ -1954,8 +1948,9 @@
// A region annotation is an element annotation on its anchor, plus the box. Everything
// downstream — selector, fingerprint, pin re-finding, styles — keeps working unchanged.
if (opts.region) a.region = opts.region;
// Only React and Vue leave a component function to look up; anything else adds no await here.
if (!a.source.file && /^(react|vue)$/.test(a.source.framework || "")) { try { await applyMapSource(el, a.source, a.element.reuse); } catch {} }
// Only React and Vue leave a component function to look up. Send never waits for it: the worker
// resolves the probe while it posts the note, so a navigation right after Send can't lose it.
if (!a.source.file && /^(react|vue)$/.test(a.source.framework || "")) { const p = probeOf(el); if (p) a.mapProbe = p; }
const errors = pageErrors();
if (errors.length) a.context = { errors };
return a;
Expand Down
14 changes: 13 additions & 1 deletion extension/sourcemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,5 +210,17 @@
} catch { return null; } finally { clearTimeout(timer); }
}

globalThis.pinpointSourceMap = { decodeMappings, segmentAt, cleanSource, originalPosition, locate, mapComment, allowed, resolve };
// Put a resolved position into a note: the source hint, and the component's original name where
// the chain (and element.reuse, counted under the same minified name) still has the minified one.
function applyToNote(note, r, fnName) {
if (typeof r?.file !== "string" || !note?.source) return;
Object.assign(note.source, { file: r.file, line: r.line ?? null, column: r.column ?? null, method: "sourcemap" });
if (r.name && typeof r.name === "string") {
const c = Array.isArray(note.source.components) ? note.source.components : (note.source.components = []);
const i = c.indexOf(fnName);
if (i >= 0) c[i] = r.name; else c.unshift(r.name);
if (note.element?.reuse?.component === fnName) note.element.reuse.component = r.name;
}
}
globalThis.pinpointSourceMap = { decodeMappings, segmentAt, cleanSource, originalPosition, locate, mapComment, allowed, resolve, applyToNote };
})();
Loading