From da6fca037839228e3912305e8fdc65cf0b87c653 Mon Sep 17 00:00:00 2001 From: Gowtham Kumar Solleti Date: Wed, 21 Oct 2026 08:58:38 -0500 Subject: [PATCH] Send never waits for the source-map lookup: the worker resolves it while posting On a production React/Vue page the note waited up to 4 s for the lookup before it was posted, so a navigation right after Send lost it (Bug 6, macOS CI). The page now sends its probe with the note; the worker, which outlives the page, resolves it and fills in the file and original names before it posts. Same rules as before: local dev pages only, the origin from the browser. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ extension/background.js | 9 +++++++++ extension/content.js | 25 ++++++++++--------------- extension/sourcemap.js | 14 +++++++++++++- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1dc77d..66b27bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/extension/background.js b/extension/background.js index 48f71d4..a946655 100644 --- a/extension/background.js +++ b/extension/background.js @@ -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" }, diff --git a/extension/content.js b/extension/content.js index 2d47847..91c775d 100644 --- a/extension/content.js +++ b/extension/content.js @@ -694,7 +694,8 @@ 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 }); @@ -702,22 +703,15 @@ 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. @@ -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; diff --git a/extension/sourcemap.js b/extension/sourcemap.js index 120637f..bd2f852 100644 --- a/extension/sourcemap.js +++ b/extension/sourcemap.js @@ -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 }; })();