Skip to content

chore: add local testing extension - #1931

Open
daniel-graham-amplitude wants to merge 1 commit into
hackathon-client-configuratorfrom
hackathon-client-configurator-extension
Open

chore: add local testing extension#1931
daniel-graham-amplitude wants to merge 1 commit into
hackathon-client-configuratorfrom
hackathon-client-configurator-extension

Conversation

@daniel-graham-amplitude

@daniel-graham-amplitude daniel-graham-amplitude commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?:

Note

Low Risk
Changes are confined to test-server dev tooling; the extension is explicitly a spike with broad host permissions intended for local use only.

Overview
Adds a local Chrome extension spike so configurator settings can be exercised on any http(s) site, not only the in-repo run page.

The configurator gains a Run on URL flow: extension-bridge.js posts the built runtime config (via buildRuntimeConfig + toJsonSafe for regexes) to a content script on configurator.html, which opens the target tab and injects vendored analytics (and optional session replay) in the main world on webNavigation.onCommitted. A toolbar action toggles instrumentation on the current tab using the last payload or a debug default.

csp.js strips CSP headers per tab with declarativeNetRequest, restores them when instrumentation ends, and heuristically reports what the original policy would have blocked for Amplitude endpoints. inject.js revives config, logs events, and restores any pre-existing window.amplitude. sync-vendor.mjs copies built SDK bundles into vendor/ with UTF-8 non-character escaping for Chromium. Includes README, manifest, and a strict-CSP target page for manual testing.

Reviewed by Cursor Bugbot for commit 4126acd. Bugbot is set up for automated code reviews on this repo. Configure here.

@daniel-graham-amplitude
daniel-graham-amplitude requested a review from a team as a code owner August 12, 2026 16:57
@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
packages/analytics-browser/lib/scripts/amplitude-min.js.gz 61.51 KB (0%)
packages/session-replay-browser/lib/scripts/session-replay-browser-min.js.gz 134.97 KB (0%)
packages/unified/lib/scripts/amplitude-min.umd.js.gz 215.59 KB (0%)
@amplitude/element-selector (gzipped esm) 2.67 KB (0%)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Done

Create PR

Or push these changes by commenting:

@cursor push 998428230a
Preview (998428230a)
diff --git a/test-server/configurator-extension/csp.js b/test-server/configurator-extension/csp.js
--- a/test-server/configurator-extension/csp.js
+++ b/test-server/configurator-extension/csp.js
@@ -42,7 +42,14 @@
 // nobody's attention if session replay is off.
 function requirementsFor({ analytics = {}, sessionReplay, engagement }) {
   const zone = ZONES[analytics.serverZone] ?? ZONES.US;
-  const events = analytics.serverUrl ? new URL(analytics.serverUrl).origin : zone.events;
+  let events = zone.events;
+  if (analytics.serverUrl) {
+    try {
+      events = new URL(analytics.serverUrl).origin;
+    } catch {
+      // Free-form configurator input; an invalid URL must not abort injection.
+    }
+  }
   const requirements = [{ directive: 'connect-src', target: events, reason: 'event uploads' }];
   if (fetchesRemoteConfig(analytics)) {
     requirements.push({ directive: 'connect-src', target: zone.remoteConfig, reason: 'remote configuration' });
@@ -73,11 +80,14 @@
 
 // Tabs whose rule is in place. Rehydrated from the rules themselves, since they are the state that
 // survives the service worker being torn down between a run and the navigation it opened.
+// Await before registering onHeadersReceived so a waking navigation cannot race an empty set.
 const relaxed = new Set();
-chrome.declarativeNetRequest
-  .getSessionRules()
-  .then((rules) => rules.forEach(({ id }) => relaxed.add(id)))
-  .catch(() => undefined);
+try {
+  const rules = await chrome.declarativeNetRequest.getSessionRules();
+  rules.forEach(({ id }) => relaxed.add(id));
+} catch {
+  // Leave relaxed empty; rules can still be added via relaxCsp.
+}
 
 // Only the policy of a tab under instrumentation is worth keeping, and only until the tab goes away.
 const policies = new Map();

diff --git a/test-server/configurator-extension/sync-vendor.mjs b/test-server/configurator-extension/sync-vendor.mjs
--- a/test-server/configurator-extension/sync-vendor.mjs
+++ b/test-server/configurator-extension/sync-vendor.mjs
@@ -10,8 +10,9 @@
 // Run from the repository root: node test-server/configurator-extension/sync-vendor.mjs
 import { mkdir, readFile, writeFile } from 'node:fs/promises';
 import path from 'node:path';
+import { fileURLToPath } from 'node:url';
 
-const HERE = path.dirname(new URL(import.meta.url).pathname);
+const HERE = path.dirname(fileURLToPath(import.meta.url));
 const PACKAGES = path.resolve(HERE, '../../packages');
 
 const BUNDLES = [

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 4126acd. Configure here.

chrome.declarativeNetRequest
.getSessionRules()
.then((rules) => rules.forEach(({ id }) => relaxed.add(id)))
.catch(() => undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSP state races service worker wake

Medium Severity

relaxed is rehydrated with a fire-and-forget promise, so a waking onHeadersReceived can run while the set is still empty and skip recording the policy. CSP removal still works via session rules, but the console and toolbar report that the design depends on is lost after the service worker sleeps.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4126acd. Configure here.

import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';

const HERE = path.dirname(new URL(import.meta.url).pathname);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vendor sync breaks on Windows paths

Medium Severity

HERE is derived from new URL(import.meta.url).pathname instead of fileURLToPath. On Windows that yields a leading-slash path such as /C:/..., and on any OS spaces or non-ASCII segments stay percent-encoded, so the vendor copy step fails before the extension can load bundles.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4126acd. Configure here.

}
// Read before injecting: by the time a navigation commits the response headers have arrived, which is
// where the policy the page was sent is still visible.
const csp = cspReport(tabId, payload);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bad server URL aborts injection

Low Severity

cspReport runs outside the injection try/catch and calls new URL(analytics.serverUrl). An invalid custom server URL from the configurator throws before any script is injected, leaves the badge as on, and skips the error path that would mark err.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4126acd. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant