---
phase: 04-harden-clean-up-optional
plan: 01
subsystem: content-script
tags: [audit-p1-polish, p1-11-fetch-url, p1-14-nav-url, p1-15-rrweb-timestamps, tdd, content-script, charter-d-p4-02]

# Dependency graph
requires:
  - phase: 03-spec-10-smoke-verification-dom-event-log-verification
    provides: REQ-user-event-log validated (A30 harness; 5 UserEvent types — click + input + navigation + js_error + network_error captured under canonical assertion shape)
  - phase: 01-stabilize-video-pipeline
    provides: tests/background/start-video-capture-no-tab.test.ts scaffold pattern (Plan 01-09 — pure-function-with-vi.fn-stub mirror for tests/content/)
provides:
  - Audit P1 #11 fetch URL extraction fix — fetch(new Request(url)) now captures Request.url in events.json instead of literal '[object Request]'
  - Audit P1 #14 navigation URL tracking fix — module-level `let previousUrl` tracker replaces stale `history.state?.url || 'unknown'` emit so meta.previousUrl carries the operator's prior URL verbatim
  - Audit P1 #15 rrweb timestamp normalization — rrweb emit-side timestamps normalized to Date.now() Unix-epoch ms so cleanupOldEvents (now - event.timestamp) arithmetic is meaningful
  - tests/content/ NEW directory mirroring src/content/ — establishes the content-script unit-test placement convention for Phase 4+
  - 9 new vitest tests (171 baseline → 180 GREEN) pinning the three contracts above
affects: [04-02 (CSP/dead-code hardening; same Phase 4 wave), 04-04 (A34 fetch+XHR empirical harness will exercise the P1 #11 fix end-to-end), 04-07 (Phase 4 closure aggregator)]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Content-script unit-test scaffold: vi.mock('rrweb') + globalThis stubs for window/document/history/Request before await import('../../src/content/index'); userEvents+rrwebEvents read back via GET_RRWEB_EVENTS chrome.runtime.onMessage handler. Established at tests/content/ for Phase 4+ content-script work."
    - "Type-narrow over implicit-coercion at trust boundaries: `args[0] instanceof Request ? args[0].url : String(args[0])` replaces `args[0]?.toString()` at the fetch wrapper sites. Discipline applied at both ok-branch (line ~190) AND catch-branch (line ~210) — twin sites carry the same narrow."
    - "Module-level state for closure-time inter-event tracking: `let previousUrl` mutated swap-then-emit inside handleNavigation. Mirrors the existing rrwebEvents + userEvents module-level pattern at lines 21+24."
    - "Wrapper-side timestamp normalization at emit time: `event.timestamp = Date.now()` prepended in rrweb record() emit callback. Same Unix-epoch convention as the existing addUserEvent normalization at line 54."

key-files:
  created:
    - "tests/content/fetch-interception.test.ts"
    - "tests/content/navigation-tracking.test.ts"
    - "tests/content/rrweb-timestamps.test.ts"
  modified:
    - "src/content/index.ts (3 surgical edits — module-level previousUrl declaration at line 29; handleNavigation rewrite at lines 106-122; fetch URL narrow at lines 190 + 210; rrweb emit timestamp normalization at line 315)"
    - ".planning/config.json (use_worktrees: false — orchestrator-staged flip from earlier in this session; folded into Task 1 commit)"

key-decisions:
  - "Task 1 (RED) added 4 fetch tests instead of the plan's nominal 3: Tests A+C are paired with new Tests B+D so both fetch interception sites (ok-branch line ~190 + catch-branch line ~210) get an explicit Request-arg test against the same canonical narrow. Plan acceptance allows ≥3 it() in fetch-interception.test.ts (min_lines: 60); 4 strengthens the regression net."
  - "Used `(typeof window !== 'undefined')` guard in the module-level previousUrl initializer so the content script remains importable under Node-env vitest (vitest.config.ts environment: 'node'). Production content-script realm always has window; the guard is a one-line defensive bootstrap mirroring the project's import-time discipline (same guard pattern Plan 01-03 established for chrome.* in src/offscreen/recorder.ts)."
  - "Did NOT introduce a helper function for the Request-or-string narrow. The inline form `(args[0] instanceof Request ? args[0].url : String(args[0])) || 'unknown'` reads clearly at the two call sites and avoids a new module-scope name. Optionally extractable in a future plan if a third call site appears."

patterns-established:
  - "tests/content/ as the canonical content-script unit-test directory (parallel to existing tests/background/, tests/i18n/, tests/offscreen/, tests/build/)"
  - "Reading content-script module state via the production GET_RRWEB_EVENTS chrome.runtime.onMessage path doubles as a contract pin for the archive export surface"
  - "Audit P1 polish in src/content/index.ts is now closed end-to-end via a TDD pair (RED scaffolds + GREEN edits in cohesive sibling commits)"

requirements-completed: []

# Metrics
duration: ~30 min
completed: 2026-05-21
---

# Phase 4 Plan 01: audit-p1-polish-content-script Summary

**Three audit P1 correctness fixes in src/content/index.ts — fetch(new Request(url)) URL extraction at both fetch-wrapper sites, module-level previousUrl tracker replacing the always-"unknown" navigation emit, and Unix-epoch rrweb emit timestamp normalization — gated by 9 new RED→GREEN unit tests under tests/content/.**

## Performance

- **Duration:** ~30 min (RED scaffold + GREEN edits + bundle gates + SUMMARY)
- **Started:** 2026-05-21T11:55:00Z (after orchestrator respawn in foreground mode)
- **Completed:** 2026-05-21T12:25:00Z
- **Tasks:** 2 (Wave 0 RED + Wave 1 GREEN per plan's tdd type)
- **Files modified:** 4 (3 new test files + 1 source file edit + 1 config flip)

## Accomplishments

- Audit P1 #11 (fetch URL extraction) — `fetch(new Request(url))` now records the canonical `request.url` in events.json instead of the literal string `'[object Request]'`. Twin fix at both ok-branch (line ~190) and catch-branch (line ~210).
- Audit P1 #14 (navigation URL tracking) — module-level `let previousUrl` declared at line 29; handleNavigation rewritten to swap-then-emit so `meta.previousUrl` carries the operator's actual prior URL instead of the literal `'unknown'` produced by the stale `history.state?.url || 'unknown'` lookup.
- Audit P1 #15 (rrweb emit timestamps) — `event.timestamp = Date.now()` prepended in the rrweb `record({ emit })` callback at line 315; cleanupOldEvents `(now - event.timestamp) < RRWEB_RETENTION_MS` arithmetic at line 33 is now meaningful (both sides Unix-epoch ms).
- 9 new vitest tests across `tests/content/` pin all three contracts; baseline preserved (171 → 180/180 GREEN). tsc-clean preserved.

## Task Commits

Each task was committed atomically per the plan's TDD cycle:

1. **Task 1: Wave 0 RED — content-script test scaffolds** — `3dbc51c` (test)
   - 7 of 9 tests RED at commit (B+D fetch, A+B+C navigation, A+B rrweb); 2 tests pass-as-baseline (A+C fetch — String(string)===string identity).
2. **Task 2: Wave 1 GREEN — 3 surgical edits in src/content/index.ts** — `7da30af` (feat)
   - All 9 tests flip GREEN; full vitest baseline preserved at 180/180.

**Plan metadata commit:** to follow this SUMMARY landing.

## Files Created/Modified

- `tests/content/fetch-interception.test.ts` (NEW; 4 tests pinning P1 #11 — fetch(stringUrl) ok-branch + fetch(new Request(url)) ok-branch + fetch(stringUrl) catch-branch + fetch(new Request(url)) catch-branch)
- `tests/content/navigation-tracking.test.ts` (NEW; 3 tests pinning P1 #14 — popstate + hashchange + history.pushState wrap all read meta.previousUrl from module-level state)
- `tests/content/rrweb-timestamps.test.ts` (NEW; 2 tests pinning P1 #15 — Test A normalization to >1e12 + Test B cleanupOldEvents arithmetic regression)
- `src/content/index.ts` (3 surgical edits — see Decisions Made for line-level details)
- `.planning/config.json` (use_worktrees flipped to false earlier in this session by the orchestrator; folded into Task 1 commit per orchestrator instruction)

## Decisions Made

**Inline narrow over extracted helper for P1 #11:** Considered factoring out `function extractFetchUrl(arg: unknown): string` at module scope; chose to keep the narrow inline at both call sites for the same reason the project consistently inlines `event.timestamp = Date.now();` in the existing addUserEvent path — the discipline reads at the call site, the call sites are two-and-only-two, and a future plan can refactor if a third caller appears.

**typeof window guard for module-level previousUrl:** Production content-script realm always has `window`; the `(typeof window !== 'undefined')` guard at line 29 keeps the module importable under vitest's Node environment, mirroring the defensive bootstrap pattern Plan 01-03 established for chrome.* in src/offscreen/recorder.ts.

**Test count bumped 8 → 9:** Plan nominal was 3+3+2=8 tests. Added a 4th fetch test (catch-branch + new Request) so both fetch-wrapper sites have an explicit Request-arg pin. Plan acceptance permits ≥3 it() per file; this strengthens the regression net for the catch-branch fix at line ~210 which would otherwise have a single regression-guard test (Test C with a string arg).

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 3 - Blocking] vi.mock('rrweb') in fetch + navigation tests**
- **Found during:** Task 1 (first npm test run)
- **Issue:** Initial RED-test scaffold for fetch-interception + navigation-tracking did not stub rrweb. The content script's `import { record } from 'rrweb'` pulled rrweb's lib/*.js through vitest's CJS transform pipeline; rrweb 2.0.0-alpha.4 ships ESM-only, so the import crashed with "exports is not defined in ES module scope" before any test body ran.
- **Fix:** Added `vi.mock('rrweb', () => ({ record: (_opts) => () => {} }))` at the top of both fetch-interception.test.ts and navigation-tracking.test.ts. The rrweb-timestamps test already mocked rrweb (it needs the captured emit callback), so this completes the pattern across all 3 files.
- **Files modified:** tests/content/fetch-interception.test.ts, tests/content/navigation-tracking.test.ts
- **Verification:** All 9 content tests imported cleanly post-fix; 7 RED + 2 baseline-pass as expected for Task 1's RED gate.
- **Committed in:** `3dbc51c` (Task 1 commit; deviation absorbed into the same Wave 0 RED commit because the fix was in the test files themselves, pre-RED-gate).

**2. [Rule 1 - Bug] Removed `static prototype: unknown` from FakeXHR stubs**
- **Found during:** Task 1 (first npm test run — vite:oxc parse error)
- **Issue:** Initial scaffold included `static prototype: unknown = { open: () => {}, send: () => {} };` on the FakeXHR class stub. The TC39 class fields proposal reserves `prototype` as a non-writable class property; oxc (Vite's parser) correctly rejects it with `Classes may not have a static property named 'prototype'`.
- **Fix:** Removed the `static prototype` line from all 3 test files. The instance-level open/send/addEventListener methods on FakeXHR suffice — the content script only assigns to `XMLHttpRequest.prototype.open` and `XMLHttpRequest.prototype.send` (production prototype chain), which is satisfied by the auto-generated class prototype.
- **Files modified:** tests/content/fetch-interception.test.ts, tests/content/navigation-tracking.test.ts, tests/content/rrweb-timestamps.test.ts
- **Verification:** Files parsed clean post-fix.
- **Committed in:** `3dbc51c` (Task 1 commit).

---

**Total deviations:** 2 auto-fixed (1 blocking, 1 bug — both in the new test files, both resolved pre-commit during the Task 1 RED iteration).
**Impact on plan:** Zero scope creep; deviations were test-side mechanics that surfaced during the first RED-gate run and got rolled into the same Task 1 commit before the RED gate was claimed. Source-side edits (Task 2) landed exactly as RESEARCH §"Specifics" and 04-PATTERNS.md spelled out.

## Issues Encountered

**Pre-existing vitest flakes** — 3 unrelated tests intermittently fail across baseline runs (`tests/background/blob-url-download.test.ts` 5000ms timeout race, `tests/background/webm-remux.test.ts` ffprobe frame count, `tests/offscreen/webm-playback.test.ts` ffmpeg dry-run). These are documented Phase 4 deferred items per `03-VERIFICATION.md` "Forward-Looking Deferred Items" + 04-CONTEXT.md §"Flake stabilization" — owned by Plan 04-03 (A29 race rewrite + parallel-vitest race + 2 ffprobe flakes). Confirmed pre-existing via `git stash` baseline runs (171/171 only on lucky runs; 168/171 or 165/171 on flaky runs). Plan 04-01 introduces no new flakes — final clean baseline run was 180/180 GREEN.

## Pre-Checkpoint Bundle Gates

Per saved memory `feedback-pre-checkpoint-bundle-gates.md` (6/6 standard inventory):

1. **Tier-1 FORBIDDEN_HOOK_STRINGS** — `tests/background/no-test-hooks-in-prod-bundle.test.ts` 13/13 GREEN; inventory unchanged at 12 strings (Plan 04-01 added no harness hooks; pure source-side polish).
2. **SW CSP-safety grep** — 1 `new Function("")` in SW chunk `dist/assets/index.ts-8LkXuqac.js`. Pre-existing `vite-plugin-node-polyfills` setimmediate polyfill; documented at `.planning/phases/01-stabilize-video-pipeline/deferred-items.md`; Plan 04-02 owns the polyfill replacement. NOT a Plan 04-01 regression.
3. **Node-globals grep** — `Buffer.copy / .isView / .length / .push / .shift` in SW chunk — all from JSZip internals (lowercase-b `buffer` field names + JSZip stream pipeline). Pre-existing across all Phase 1+2+3 builds.
4. **DOM-globals grep** — `document.createElement / .createTextNode / .documentElement / .F` + `window.Math / .console / .localStorage / .process` in SW chunk — pre-existing shimmed-DOM references inside JSZip's text encoder fallback paths.
5. **manifest.json** — present at `dist/manifest.json`; `name: "__MSG_extName__"` (chrome.i18n message resolution intact). Plan 04-01 did NOT touch `_locales/` so en↔ru parity is untouched.
6. **Build itself** — `npm run build` exits 0 in 4.66s; all chunks ship.

## Self-Check

- **Files created:**
  - tests/content/fetch-interception.test.ts → FOUND
  - tests/content/navigation-tracking.test.ts → FOUND
  - tests/content/rrweb-timestamps.test.ts → FOUND
- **Files modified:**
  - src/content/index.ts → FOUND (diff shows 3 surgical edits)
  - .planning/config.json → FOUND (use_worktrees: false)
- **Commits:**
  - 3dbc51c → FOUND (test(04-01): Wave 0 RED)
  - 7da30af → FOUND (feat(04-01): Wave 1 GREEN)
- **Verification commands all green:**
  - npm test -- tests/content/ --run → 9 passed (9)
  - npm test -- --run → 180 passed (180) [confirmed twice; once on flaky run with 1 pre-existing failure, once 180/180 clean]
  - npx tsc --noEmit → exit 0
  - All 7 acceptance grep checks pass per Task 2 commit body

## Self-Check: PASSED

## Next Phase Readiness

- Plan 04-02 (build hygiene: vite.config.ts polyfill replacement + generate-icons ESM/CJS + dead-code grep + deferred-items.md update) is **unblocked** and queued NEXT in Wave 1. It modifies `vite.config.ts` + `src/background/index.ts` + `generate-icons.{js,cjs}` + 2 build tests + the Phase 1 deferred-items.md; zero overlap with Plan 04-01's `src/content/index.ts` surface.
- Plan 04-04 (A34 fetch+XHR network_error empirical harness; ROADMAP SC #2) will validate Plan 04-01's P1 #11 fix end-to-end against a real probe page in Wave 3.
- Audit P1 polish backlog: **CLOSED 3/3** (items #11 + #14 + #15 all GREEN with regression coverage).

---
*Phase: 04-harden-clean-up-optional*
*Completed: 2026-05-21*
