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
2 changes: 1 addition & 1 deletion dist/spector.bundle.js

Large diffs are not rendered by default.

11 changes: 6 additions & 5 deletions dist/spector.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,13 +807,15 @@ export declare class Spector {
}): void;
log(value: string): void;
/**
* Intercept all new Worker() calls to auto-inject Spector.
* Best-effort: will fail for CORS, CSP, or module Workers.
* Explicitly opt in to intercepting new Worker() calls to auto-inject Spector.
* Experimental/best-effort: only simple same-origin classic scripts with
* string URLs and no options are candidates. Module Workers stay native.
* Blob wrapping can still change behavior or fail under CSP. Prefer spyWorker().
* @param workerBundleUrl URL to spector.worker.bundle.js
*/
spyWorkers(workerBundleUrl?: string): void;
/**
* Stop intercepting Worker construction.
* Stop intercepting Worker construction without replacing later third-party wrappers.
*/
stopSpyingWorkers(): void;
/**
Expand All @@ -824,8 +826,7 @@ export declare class Spector {
spyWorker(worker: Worker): WorkerBridge;
/**
* Capture a frame from a Worker's WebGL context.
* Uses direct postMessage to bypass the main-thread spy chain,
* which ensures a full frame is captured.
* Uses the Worker's bridge to send the request and publish its result once.
*/
captureWorker(worker: Worker, commandCount?: number, quickCapture?: boolean, fullCapture?: boolean): void;
private captureFrames;
Expand Down
12 changes: 9 additions & 3 deletions documentation/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ And the following list of events:
- ```onError: IEvent<string>```: triggered when an error occured and return the error message.

### Worker / OffscreenCanvas APIs
- ```spyWorkers(workerBundleUrl?: string)``` : Intercept all `new Worker()` calls to auto-inject Spector. Best-effort — may fail with CORS, CSP, or module Workers. Defaults to `"spector.worker.bundle.js"`.
- ```stopSpyingWorkers()``` : Stop intercepting Worker construction.
- ```spyWorker(worker: Worker): WorkerBridge``` : Manually bridge a specific Worker for capture. This is the primary, reliable API. The Worker must have `spector.worker.bundle.js` loaded via `importScripts`.
- ```spyWorkers(workerBundleUrl?: string)``` : Explicitly opt in to experimental, best-effort interception of `new Worker()` calls. Only primitive string URLs for same-origin HTTP(S) classic Workers without options are candidates. URL objects, options (including module Workers), subclasses, redirects, blob/data/cross-origin URLs, strict scripts, and recognizable URL-sensitive sources (`import`, `importScripts`, nested Workers, `location`, `Request`, `fetch`, XHR) use native construction. Inputs are passed to the original constructor without extra coercion or options inspection. Source checks cannot establish safety. Defaults to `"spector.worker.bundle.js"`, resolved against the caller's base URL. Prefer `spyWorker()` when you control the Worker source.
- ```stopSpyingWorkers()``` : Deactivate this interceptor, including retained references to it. Restore the previous constructor only if it is still installed; preserve wrappers installed later by other libraries.
- ```spyWorker(worker: Worker): WorkerBridge``` : Manually bridge a specific Worker for capture. This is the recommended API and does not replace the Worker constructor. The Worker must load `spector.worker.bundle.js` via `importScripts` (classic Workers) or `import` (module Workers).
- ```captureWorker(worker: Worker, commandCount?: number, quickCapture?: boolean, fullCapture?: boolean)``` : Capture a frame from a Worker's WebGL context. If the Worker hasn't been bridged yet, `spyWorker` is called automatically.

The browser extension's **Auto-inject Workers (experimental)** checkbox is separate from `spyWorkers()` and defaults **off**, even when offscreen canvas capture was previously enabled. It uses the same conservative eligibility rules, includes simple classic OffscreenCanvas Workers, and warns that instrumentation may alter behavior. Changes are persisted in page `sessionStorage` (`SPECTOR_WORKERAUTOINJECT`) for the tab/origin session, like the offscreen capture setting, then reload the tab's loaded frames. The MAIN-world content script reads the exact value `"true"` synchronously at `document_start`; it does not wait for asynchronous extension storage. Missing/false values leave `window.Worker` untouched, with no wrapper or preflight fetch. Disabling the checkbox restores that behavior on reload. Main-thread canvas capture and manual Worker APIs remain available.

For OffscreenCanvas capture in the extension, select the instrumented **Worker** entry. A transferred DOM canvas alone does not identify its owning Worker, so selecting its canvas entry reports an error instead of guessing which Worker to capture.

Opt-in is not a compatibility guarantee: source and instrumentation-bundle preflights add synchronous requests, computed API names can evade checks, and blob wrapping still changes the worker global and script error locations. Application responses with CSP or report-only CSP stay native because a blob cannot carry those response policies. Native constructor and preflight failures, including unavailable or non-JavaScript instrumentation bundles, fall back synchronously without creating a blob Worker. A successful XHR preflight does not prove `importScripts` will succeed: it uses different CSP checks and a separate request. Bundle import/execution exceptions are logged in the Worker so the application can still start, but partially executed instrumentation is not rolled back. CSP blocking the blob Worker itself and other asynchronous startup failures cannot transparently replace an already-returned Worker. Module Workers are always native; explicitly import the worker bundle when module capture is needed. No startup message queue or `postMessage` override is installed.

As you notice you could use the capture without displying the UI (this is the orientation we chosed in the browser extension).

## SPECTOR.CaptureMenu
Expand Down
7 changes: 7 additions & 0 deletions documentation/changeLogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@

Please, find below the per release summary of the contribution added to the project per version. Each of the listed versions is having its corresponding tag in the repo.

## v0.9.35

* Fix [relative dynamic imports in Workers](https://github.com/BabylonJS/Spector.js/issues/394) and [nested module Workers](https://github.com/BabylonJS/Spector.js/issues/395) by keeping extension Worker construction native unless experimental auto-injection is explicitly enabled; module Workers always stay native.
* Add a default-off **Auto-inject Workers (experimental)** extension setting, with native fallbacks for ineligible classic Workers, URL-sensitive scripts, detectable CSP restrictions, and failed preflights.
* Fix Worker and OffscreenCanvas capture routing so Worker captures are delivered once and an unlinked transferred canvas cannot select an unrelated Worker.
* Clear saved extension captures on browser restart and handle messages to closed tabs without extension errors.

## v0.9.33

* Improve [shader debugging](https://github.com/BabylonJS/Spector.js/issues/283) with source links, compiler diagnostics, and parser-backed formatting
Expand Down
44 changes: 39 additions & 5 deletions extensions/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,24 @@ browser = (function () {
chrome;
})();

function consumeLastError() {
// Reading runtime.lastError marks an expected one-way delivery failure as
// handled when the popup, tab, or content script has already closed.
if (browser.runtime && browser.runtime.lastError) {
return;
}
};

function sendMessage(message) {
browser.tabs.query({ active: true, currentWindow: true }, function(tabs) {
browser.tabs.sendMessage(tabs[0].id, message, function(response) { });
if (tabs.length > 0) {
browser.tabs.sendMessage(tabs[0].id, message, consumeLastError);
}
});
};

function sendRuntimeMessage(message) {
browser.runtime.sendMessage(message, function(response) { });
browser.runtime.sendMessage(message, consumeLastError);
};

function listenForMessage(callback) {
Expand All @@ -26,14 +36,33 @@ var currentCapture = null;
var currentFrameId = null;
var currentTabId = null;

var clearCaptureSession = function() {
browser.storage.local.remove([
"currentCapture",
"captureHistory",
"currentFrameInfo"
], consumeLastError);
};

// Captures are useful within one browser session for comparison, but should
// not reappear after the browser is relaunched.
if (browser.runtime.onStartup) {
browser.runtime.onStartup.addListener(clearCaptureSession);
}

var refreshCanvases = function() {
var canvasesToSend = { canvases: [], captureOffScreen: false };
var canvasesToSend = { canvases: [], captureOffScreen: false, workerAutoInject: false };
browser.tabs.query({ active: true, currentWindow: true }, function(tabs) {
for (var tabId in tabInfo) {
if (tabId == tabs[0].id) {
for (var frameId in tabInfo[tabId]) {
var infos = tabInfo[tabId][frameId];
canvasesToSend.captureOffScreen = infos.captureOffScreen;
// Subframes may navigate to origins with different session
// settings. The popup reflects the top-level page's choice.
if (infos.isTopFrame) {
canvasesToSend.workerAutoInject = infos.workerAutoInject;
}
for (var i = 0; i < infos.canvases.length; i++) {
var info = infos.canvases[i];
canvasesToSend.canvases.push({
Expand All @@ -60,7 +89,7 @@ browser.action.onClicked.addListener(function (tab) {

listenForMessage(function(request, sender, sendResponse) {
var frameId;
if (sender.frameId) {
if (typeof sender.frameId === "number") {
frameId = sender.frameId;
}
else if (request.uniqueId) {
Expand Down Expand Up @@ -111,7 +140,12 @@ listenForMessage(function(request, sender, sendResponse) {
tabInfo[tabId] = { };
}

tabInfo[tabId][frameId] = { canvases: request.canvases, captureOffScreen: request.captureOffScreen };
tabInfo[tabId][frameId] = {
canvases: request.canvases,
captureOffScreen: request.captureOffScreen,
workerAutoInject: request.workerAutoInject === true,
isTopFrame: sender.frameId === 0
};
}
else if (request.errorString) {
// Close the wait message and may display an error.
Expand Down
Loading
Loading