Skip to content

v0.9.69 — Anonymous Telemetry Pipeline + Showcase Dashboard Streaming Fix#50

Merged
LakshmanTurlapati merged 92 commits into
mainfrom
Refinements
May 15, 2026
Merged

v0.9.69 — Anonymous Telemetry Pipeline + Showcase Dashboard Streaming Fix#50
LakshmanTurlapati merged 92 commits into
mainfrom
Refinements

Conversation

@LakshmanTurlapati

Copy link
Copy Markdown
Collaborator

Summary

Milestone v0.9.69 — Anonymous Telemetry Pipeline + Showcase Dashboard Streaming Fix (8 phases, 91 commits ahead of main).

Stands up a privacy-preserving anonymous telemetry pipeline that flows MCP + extension usage metrics from end-user installs into the FSB showcase server, exposes aggregates on the public /stats Easter-egg page, hardens the Chrome Web Store privacy disclosures, and lands defensive patches for the broken DOM streaming pipeline (top-3 hypotheses).

  • All 3 release-gating BLOCKERs RESOLVED.
  • 67/68 requirements Complete + 1 Partial (STREAM-03 — diagnostic-first by design per locked decision D-16; browser-repro reserved post-merge).
  • Audit: tech_debt (acceptable; Phase 274 + 276 await browser-repro verification, defensive patches safe regardless of root cause).

Phases shipped

# Phase Tests
269 Install Identity + Opt-Out Scaffold 35 unit + user-validated
270 MCP Pricing Module (30 models + 13 client defaults, May 2026 rates) 167 unit + 6 review fixes
271 MCPMetricsRecorder + Dispatcher Hooks + Unified Cost 88 unit + 612 regression + 1 BLOCKER fix
272 TelemetryCollector + Alarm + Queue Persistence 71 unit + 13 grep + 3-way privacy gate
273 Server Schema + Routes + Salt + Rate Limit + Housekeeper 121 server + 2 review fixes · B1+B2 RESOLVED
274 Public Aggregates + FSBTelemetryService + /stats Toggle Group 294 assertions + 6-locale i18n
275 Privacy Policy + CWS Listing + CI Guard 56 assertions · B3 RESOLVED
276 Dashboard DOM-Streaming Diagnostic + Defensive Patches 52 unit (hypotheses #1+#2+#4)

BLOCKERs resolved

  • B1 (Phase 273): `app.set('trust proxy', 1)` placed immediately after `const app = express()` in `showcase/server/server.js` (BEFORE any route mount). Asserted by `tests/server-trust-proxy.test.js` (5/5 PASS).
  • B2 (Phase 273): `express-rate-limit@^8.3.0` (resolved 8.5.2) installed; custom `keyGenerator: (req) => hashIp(ipKeyGenerator(req.ip), db)` aligns rate-limit + storage identifiers and routes through the library's IPv6 canonicaliser to fix CVE-2026-30827.
  • B3 (Phase 275): Privacy policy page `#telemetry-disclosure` section with 6-locale i18n (25 trans-units × 6 locales); CWS `listing-copy.md` "Data Collection" section; `store-assets/chrome-web-store/privacy-practices-evidence.md` (per-checkbox decisions + verbatim Limited Use clause); `scripts/verify-store-listing.mjs` 5-assertion CI guard.

Deploy + DB readiness

  • `fly.toml` already mounts `fsb_data` volume at `/data` (`DB_PATH=/data/fsb-data.db`). No change.
  • `showcase/server/src/db/schema.js` uses `CREATE TABLE IF NOT EXISTS` for all 4 new telemetry tables — they auto-create at server boot on first deploy after this PR merges.
  • `Dockerfile` runs `npm ci` against both Angular and server package.json files; new deps (`chart.js@^4.5.1`, `express-rate-limit@^8.3.0`) come along automatically.
  • `.github/workflows/deploy.yml` triggers on push to `main` with paths-ignore filter that allows production-code changes through.
  • `.github/workflows/ci.yml` extension job now installs showcase/server + showcase/angular deps so the full `npm test` chain (~108 files including 30+ new v0.9.69 tests) validates on every PR.

Test plan

  • `npm test` exits 0 locally (verified twice; ~108 test files including all 30+ v0.9.69 tests pass).
  • CI `extension` job installs showcase deps before `npm test` (fixed in this PR, commit `fbbdae2`).
  • CI `all-green` passes on this PR.
  • Post-merge: `flyctl deploy --remote-only` succeeds; new telemetry tables auto-create at `/data/fsb-data.db`.
  • Post-merge: visit `https://full-selfbrowsing.com/stats\` and confirm FSB Telemetry section renders alongside GitHub views (chart-render visual smoke).
  • Post-merge: load-unpacked extension in Chrome, verify "Send anonymous usage data" toggle in Advanced Settings ON by default; flip OFF, confirm `chrome.storage.local.fsbTelemetryOptOut` becomes true.
  • Post-merge: 8-step manual repro per `.planning/phases/276-…/276-VERIFICATION.md` to validate dashboard streaming fix (defensive patches are safe regardless; this only confirms which hypothesis was the actual root cause).
  • Post-merge: CWS Developer Dashboard click-through per `.planning/phases/275-…/275-VERIFICATION.md` (tick Personally Identifiable Information, set privacy policy URL to `https://full-selfbrowsing.com/privacy#telemetry-disclosure\`, tick Limited Use, capture screenshots, submit).

Carry-forward backlog (NOT in this PR)

  • v0.9.70 telemetry follow-up — first-run privacy banner, "View what we send" preview, "Reset anonymous ID" button, in-extension "Wipe my data" UI, region-gated opt-IN for EU/UK/CA, public versioned `/api/public-stats` documentation, per-day sparklines, geo heatmap.
  • v0.9.70 streaming — full rewrite if STREAM-07 5-attempt cap is hit during Phase 276 browser repro.

Tag: `v0.9.69` pushed (pointing at HEAD of this PR).

Lakshman added 30 commits May 14, 2026 01:17
- Add chart.js@^4.4.0 dependency (browser-only, dynamic import in page).
- Create showcase/angular/src/app/core/stats/github-stats.types.ts:
  StatsViewId union, RepoSummary/StarEvent/IssueEvent/PullEvent/CommitEvent/
  ForkEvent/ReleaseEvent shapes, TimeSeriesPoint, WeeklyDelta, DatasetState<T>.
- Create showcase/angular/src/app/core/stats/github-stats.service.ts:
  - SSR-safe: every fetch + document ref behind isPlatformBrowser.
  - Per-URL ETag cache with 304 short-circuit.
  - MAX_PAGES=2 hard cap; PER_PAGE=100; 7 endpoints.
  - 403+X-RateLimit-Remaining=0 -> graceful rate-limited state (no throw).
  - 5-min polling, visibility-aware (clears interval on hidden, resumes on
    visible with immediate refresh).
  - Promise.allSettled across endpoints (no cascade on partial failure).
  - 8 BehaviorSubject<DatasetState<...>> streams.
  - 7 pure aggregator functions (cumulative stars, weekly delta, issues
    open/closed, forks growth, PRs opened/merged, commits over time,
    maintenance: releases-per-month w/ commits-per-week fallback).
- No edits to crawler files, hreflang verifier, prerender-routes.txt, or
  locale-seo.ts -- crawler-exclusion invariant preserved by omission.
…link

- StatsPageComponent (standalone) at src/app/pages/stats/{ts,html,scss}:
  - ngOnInit sets <title>'FSB · Stats' + meta robots=noindex,nofollow.
  - afterNextRender block: dynamic import 'chart.js/auto' (browser only;
    NEVER bundled into SSR), GitHubStatsService.start(), subscribe to all
    8 dataset BehaviorSubjects, ViewChild canvas mount.
  - 7 view toggles via setView() with no re-fetch (data shared across
    views; chart instance destroyed + recreated per switch).
  - ngOnDestroy: statsService.stop(), chart.destroy(), unsubscribe.
  - Reads CSS tokens from documentElement so chart colors flip with theme.
  - Styles use only existing FSB tokens (--bg-card / --primary /
    --text-secondary / --border-color / etc.); zero new tokens.
- app.routes.ts: lazy { path: 'stats', loadComponent: ... } before **.
- app.routes.server.ts: { path: 'stats', renderMode: RenderMode.Client }
  -- never prerendered; no /stats/index.html under any locale subpath.
- showcase-shell.component.html: one new <a routerLink='/stats'
  i18n='@@shell.footer.project.stats'>Stats</a> inside the Project footer
  column AFTER the License link. NOT in .nav-links, NOT in .nav-mobile,
  NOT in Pages column.
- Add @@shell.footer.project.stats trans-unit to messages.xlf (source)
  and to all 5 target XLIFFs:
    es 'Estadísticas', de 'Statistiken', ja '統計',
    zh-CN '统计', zh-TW '統統' [zh-TW '統計']
- [Rule 3 - Blocking issue] Exclude src/app/pages/stats/** from
  lint:i18n in package.json (mirrors the existing dashboard exclusion).
  Rationale: /stats is an Easter-egg page hidden from crawlers + nav;
  English-only content is consistent with its 'for nerds' positioning
  and the established dashboard precedent. The footer link itself IS
  fully translated via @@shell.footer.project.stats.
- Crawler invariants preserved (byte-identical):
  prerender-routes.txt, public/sitemap.xml, public/llms.txt,
  public/llms-full.txt, scripts/build-crawler-files.mjs,
  scripts/verify-hreflang.mjs, src/app/core/seo/locale-seo.ts.
- Build green: ng build emits 30 prerendered routes (6 × 5; /stats
  intentionally absent), stats-page-component lazy chunk = 21.59 kB,
  chart.js/auto in a separate lazy chunk = ~205 kB (NOT in initial).
- npm run verify:hreflang -> 301 pass, 0 fail; no /stats in output.
…with live GitHub graphs auto-refreshing every 5min

- Adds /stats page with 7 chart views (stars cumulative + weekly, issues, forks, PRs, commits, releases, maintenance)
- Footer link is the ONLY entry point; route uses RenderMode.Client; absent from prerender-routes.txt / sitemap / llms.txt / hreflang
- chart.js lazy chunk; afterNextRender + isPlatformBrowser gating for SSR safety
- Per-URL ETag cache, 304 short-circuit, 5-min visibility-aware polling (pauses on tab hidden, resumes on visibilitychange)
- noindex,nofollow meta; rate-limit graceful fallback

Files: 5 created + 11 modified (incl. 5 locale XLIFFs).
Commits on Refinements: 9f82efe (service+deps), 27cc70a (page+route+footer), f0202ea (worktree merge).
- New extension/utils/install-identity.js attaches globalThis.fsbInstallIdentity
  with getOrCreateInstallUuid, isTelemetryOptedOut, setTelemetryOptOut and the
  fsbInstallUuid / fsbTelemetryOptOut storage-key constants (IDENT-01..05).
- Storage namespace is exclusively chrome.storage.local; the module source
  contains zero references to chrome.storage.sync (D-11 / IDENT-04 grep gate).
- Defensive v4 regex validation re-mints a fresh UUID with a single warn log
  if the stored value is corrupt or hand-edited.
- On storage error: returns null (no throw, no session-only fallback) per IDENT-03.
- background.js loads install-identity.js at the TOP of the importScripts chain
  (line 7, before analytics.js at line 35) so downstream modules can call
  globalThis.fsbInstallIdentity.* synchronously after boot.
- chrome.runtime.onInstalled and chrome.runtime.onStartup both call
  globalThis.fsbInstallIdentity.getOrCreateInstallUuid() between
  initializeAnalytics() and loadDebugMode(); idempotent on every SW wake.
…toggle

- New .settings-card with id='card-privacy-telemetry' mounted as the LAST child
  of .advanced-settings-grid inside <section id='advanced'> (CONS-01).
- Toggle (id='fsbTelemetryOptOut', checked by default) shows 'Send anonymous
  usage data' label + factual subtitle 'Tokens used, MCP client name, active
  agent count. No URLs, prompts, or DOM.' and a 'Read full policy ->' link
  pointing to /privacy#telemetry-disclosure (anchor lands in Phase 275).
- Inline <script> before </body> wires DOMContentLoaded read + change-event
  write to chrome.storage.local.fsbTelemetryOptOut. Toggle position is the
  INVERSE of opt-out: checked == telemetry ON, !checked == opted out.
- Visible toggle position flips via the browser-native checkbox visual
  immediately on click; chrome.storage.local.set fires-and-forgets so the
  UI reflects state in well under 100ms (CONS-02).
- aria-label dynamically updates between the two announced states per UI-SPEC.
- Reuses existing .settings-card / .modern-toggle / .toggle-track / .toggle-thumb
  / .setting-hint classes verbatim; no new CSS, no new external JS file.
- No 'Reset ID' button, no 'Wipe data' button, no first-run banner -- locked
  out per CONTEXT.md <deferred> and D-02.
- New tests/install-identity.test.js covers 7 sections / 23 assertions:
  1. Mint-once on first call (IDENT-01): fresh store -> v4 UUID minted,
     persisted under fsbInstallUuid, .set called exactly once; second
     call returns the same UUID with NO further .set.
  2. Reuse-on-restart (IDENT-02): pre-seeded valid UUID returned unchanged
     with ZERO .set calls.
  3. Null on storage unavailable (IDENT-03): chrome.storage.local.get throws
     -> resolves to null, no uncaught exception.
  4. Opt-out round-trip (CONS-01, CONS-02): set/get cycle for true/false
     plus Boolean coercion of truthy non-boolean values; storage key is
     exactly fsbTelemetryOptOut.
  5. Defensive re-mint on corruption: pre-seeded 'not-a-uuid' triggers
     re-mint, storage overwritten, exactly one console.warn line with
     the expected prefix.
  6. IDENT-04 grep gate: reads module source from disk, strips comments,
     asserts ZERO chrome.storage.sync references.
  7. Storage-key string locks: FSB_INSTALL_UUID_KEY === 'fsbInstallUuid',
     FSB_TELEMETRY_OPT_OUT_KEY === 'fsbTelemetryOptOut' (catches snake_case
     regressions to the REQUIREMENTS.md verbatim text).
- package.json scripts.test chain inserts 'node tests/install-identity.test.js'
  immediately after 'node tests/cost-tracker.test.js' so the test groups
  with related cost / analytics tests.
- Helpers (createStorageArea, setupChromeMock, setupBrokenStorage,
  captureWarn, freshRequire) adapted from tests/agent-cap-storage.test.js
  with per-area call-count tracking so 'set was called once' and
  'set was NOT called' assertions are observable.
Move the Phase 269 kill-switch wiring out of an inline <script> block in
control_panel.html and into a new external file at extension/ui/install-
identity-ui.js. MV3's default CSP (`script-src 'self'`) forbids inline
scripts on extension pages, which silently blocked the previous block at
runtime. With the old wiring, DOMContentLoaded never ran and the change
listener never registered, so user opt-out clicks were discarded with no
storage write -- the worst-possible failure mode for a privacy kill switch.

The extracted file preserves the original behaviour verbatim (toggle state
mirrors `fsbTelemetryOptOut` storage key, inverse-checked semantics, aria-
label flip on change). A follow-up commit refactors the file to call the
install-identity module's exported API instead of hardcoding the storage
string.
Change the Privacy & Telemetry card's "Read full policy" anchor from
`/privacy#telemetry-disclosure` to the absolute
`https://full-selfbrowsing.com/privacy#telemetry-disclosure`. The
extension-relative path resolved to `chrome-extension://<id>/privacy`,
which does not exist inside the extension bundle and opened a blank/404
tab -- failing the CONS-01 acceptance criterion ("Read full policy" link
visible and functional).

The `#telemetry-disclosure` anchor target itself lands in Phase 275 on
the showcase side; until then the fragment is a benign no-op that scrolls
to page top, which is acceptable per 269-CONTEXT.md line 84.
Memoize the in-flight Promise from getOrCreateInstallUuid() at module
level via `_pendingMintPromise`, so concurrent callers (chrome.runtime
.onInstalled + chrome.runtime.onStartup firing close together at first
install) await the same operation. Without coalescing, both async handler
bodies could observe an empty store, mint two different UUIDs, and race
to .set -- the persisted value is whichever .set lands last while the
other call site returns a UUID that is no longer in storage.

The memo is cleared in `finally` so subsequent calls (later SW wakes, or
recovery after a transient failure) can re-attempt the mint. Without the
clear, one storage hiccup would latch null for the whole SW lifetime.

Adds Test 8 to tests/install-identity.test.js -- fires two concurrent
getOrCreateInstallUuid() calls, asserts both resolve to the SAME UUID
and chrome.storage.local.set was called exactly ONCE. Verified by
temporarily disabling the short-circuit: the assertions fail (got two
UUIDs, set count = 2), confirming the test discriminates the fix.

23/23 -> 29/29 PASS.
…-once)

Add `_corruptWarningEmitted` boolean to install-identity module and require
it be false before emitting the "Stored install UUID failed validation"
console.warn. Without this gate, a corrupt stored value + a failing
chrome.storage.local.set (enterprise policy, quota exhausted, write
contention) would loop: every read sees the same corrupt value, the warn
fires, the .set rejects, the outer try/catch returns null, the corrupt
value remains, the NEXT read repeats. Under MV3 SW eviction this spams
the console dozens of times per hour.

After the gate flips to true, the warn stays silent for the remainder of
the SW lifetime. A fresh SW wake (cold-start) resets the module state
naturally -- the user only gets one signal per SW lifetime.

Adds Test 7b to tests/install-identity.test.js -- corrupt UUID + always-
failing .set, three consecutive calls, asserts exactly ONE warn line
emitted. Negative test verified: with the gate removed, warn count was
3, confirming the test discriminates the fix.

29/29 -> 35/35 PASS.
… hardcoded keys

Load extension/utils/install-identity.js inside control_panel.html so the
storage-key constant (FSB_TELEMETRY_OPT_OUT_KEY) and the API functions
(isTelemetryOptedOut, setTelemetryOptOut) are available on
globalThis.fsbInstallIdentity from the UI document context.

Rewrite install-identity-ui.js to call the module API as the primary path,
removing the duplicated `STORAGE_KEY = 'fsbTelemetryOptOut'` literal. A
future rename of the storage key now only needs to touch install-identity
.js -- the UI no longer carries a parallel copy that could drift.

A defensive fallback to direct chrome.storage.local.{get,set} with a
hardcoded key string remains in case the module fails to load (e.g.,
load-order regression). The fallback only fires when `globalThis
.fsbInstallIdentity` is null; in steady state the module is always
loaded first by the <script src="..."> chain in control_panel.html.

29/29 -> 35/35 PASS preserved (test suite unaffected by UI changes).
…lient defaults

- mcp/data/mcp-pricing-data.json: top-level pricing_source_date='2026-05-14',
  pricing_policy doc, 30 model_pricing rows (Anthropic 7, OpenAI 7, Gemini 4,
  Grok 10, DeepSeek 2), 13 client_default_model rows (visual-session allowlist
  incl. 'OpenClaw 🦀' with U+1F980 crab grapheme).
- Every row carries source_url + source_date + HIGH/MEDIUM/LOW confidence.
- Caveat notes on opus-4.7 (tokenizer 35% drift), grok-4 (deprecation
  2026-05-15), deepseek-v4-pro (75% promo expiring 2026-05-31), and
  gemini-3.1-pro (assumed-parity).
- extension/utils/mcp-pricing-data.json is byte-exact copy (md5 confirmed
  5e95bccaa38c8c37f8bff8af1701053b).
mcp/src/tools/pricing.ts (TS, MCP-server side):
- Imports mcp/data/mcp-pricing-data.json via import-attribute syntax
  ('with { type: "json" }'; works under module=ESNext, moduleResolution=bundler).
- Exports PRICING_SOURCE_DATE='2026-05-14', MCP_MODEL_PRICING,
  MCP_CLIENT_DEFAULT_MODEL, estimateMcpCost(), type McpPricingResult.
- Module-load invariant: every client_default_model.model must resolve to a
  real model_pricing row; throws clearly at startup if JSON ships broken.
- Resolver implements 4-path contract (lookup / fallback / unknown /
  missing-tokens) per CONTEXT.md decisions; wrapped in try/catch so it
  NEVER throws at runtime.
- Lookup paths use Object.prototype.hasOwnProperty.call (prototype-pollution
  safe). Tokens validated via typeof number + Number.isFinite.

extension/utils/mcp-pricing.js (JS, service-worker mirror):
- Mirrors install-identity.js pattern: function/prototype on
  globalThis.fsbMcpPricing + CommonJS module.exports for tests.
- Loads adjacent extension/utils/mcp-pricing-data.json: synchronously via
  require() under Node; via fetch(chrome.runtime.getURL(...)) under MV3 SW.
- Resolver is SYNCHRONOUS; if called before SW fetch resolves (defensive),
  returns canonical UNKNOWN envelope.
- _loadPricingData(data) test seam exposed for unknown-path tests.
- Same 4-path contract + never-throws guarantees as the TS module.

Verified: npm --prefix mcp run build clean; resolver returns expected
{cost, source, model_used, pricing_confidence, pricing_source_date} envelope
on every input (lookup=18, fallback=30, unknown=null, chaos=UNKNOWN).
… wiring

tests/mcp-pricing.test.js (156 assertions, 8 sections):
  1. lookup path (PRICE-01)        -- known model resolves with row confidence
  2. fallback path (PRICE-02)      -- known client + missing/unknown model
  3. unknown path + chaos sweep    -- canonical UNKNOWN envelope; never throws
                                      (10 garbage inputs incl. prototype-pollution)
  4. missing tokens                -- cost=null, model_used preserved
  5. pricing_source_date (PRICE-05)-- attached to all 8 result variants
  6. data integrity 5-row spot     -- claude-opus-4-7/sonnet-4-6/gpt-5/grok-4.3/
                                      deepseek-v4-pro rates byte-match STACK
  7. confidence stamp completeness -- every model + client row HIGH/MEDIUM/LOW
                                      and every client.model resolves
  8. visual-session.ts parity      -- regex-extract MCP_VISUAL_CLIENT_LABELS,
                                      set-equality with client_default_model
                                      keys (anti-drift gate); crab grapheme
                                      byte-exact on both sides

tests/mcp-pricing-data-parity.test.js (CI gate):
  Byte-exact compare of mcp/data/mcp-pricing-data.json against
  extension/utils/mcp-pricing-data.json. On divergence: prints first 5
  mismatching lines + the cp command to recopy.

package.json:
  Wired both new tests into 'npm test' AFTER tests/install-identity.test.js
  (Phase 269 anchor) and BEFORE tests/transcript-store.test.js. No other
  test commands reordered.

Verified:
  - node tests/mcp-pricing.test.js: 156 passed, 0 failed
  - node tests/mcp-pricing-data-parity.test.js: PASS byte-exact
  - install-identity.test.js (Phase 269 anchor): exit 0
  - transcript-store.test.js (next in chain): exit 0
  - npm --prefix mcp run build: clean
  - source_url count in JSON: 43 (>=30)
  - confidence count in JSON: 43 (=43, 30 models + 13 clients)
mcp/src/tools/pricing.ts imports ../../data/mcp-pricing-data.json. The
compiled output mcp/build/tools/pricing.js preserves that relative path,
but the previous "files" array only shipped build/, ai/, README.md,
server.json, so the JSON was excluded from the npm tarball.

After this fix, npm --prefix mcp pack --dry-run shows
data/mcp-pricing-data.json (11.1kB) in the tarball alongside build/.
This is latent (no server-side consumer imports pricing.ts yet) but the
moment one does, every consumer of fsb-mcp-server >=0.9.69 from npm would
fail to load pricing.js with ERR_MODULE_NOT_FOUND.
…butes

mcp/src/tools/pricing.ts uses the ESM import-attribute syntax:

    import pricingData from '../../data/mcp-pricing-data.json'
      with { type: 'json' };

The `with { type: 'json' }` syntax (which replaced the deprecated
`assert { type: 'json' }` form) was added in Node 18.20 and 20.10.
On Node 18.0-18.19 the file fails to parse with a syntax error -- there
is no graceful runtime fallback.

The previous engines.node value (">=18.0.0") advertised support for
those broken Node minors. Bump the floor to >=18.20.0 so the package
metadata accurately reflects what the compiled output can run on.
Lakshman added 22 commits May 14, 2026 13:35
… + i18n extract

- Append <h2 id="telemetry-disclosure">Anonymous Usage Telemetry</h2> section
  to showcase/angular privacy page covering: what we collect (5 fields), what
  we do NOT collect (6 categories), retention (7d raw / 365d rollups /
  lifetime aggregates), how to opt out (Control Panel toggle), how to erase
  data (GDPR Article 17 curl recipe for POST /api/telemetry/forget), Limited
  Use affirmation (verbatim CWS-compliant phrasing), aggregated public metrics
  (link to /stats).
- 25 new @@PRIVACY_TELEMETRY_* trans-units extracted into messages.xlf,
  preserving v0.9.63 i18n discipline.
- Curl JSON braces escaped via {{ '{' }} / {{ '}' }} so Angular template
  parser accepts the literal.
Insert 25 fully translated <target state="translated"> entries into each of
messages.{es,de,ja,zh-CN,zh-TW}.xlf for the PRIVACY_TELEMETRY_* trans-units
introduced by 275-01.

Code identifiers (FSB, UUID, MCP, install_uuid, fsbInstallUuid, Chrome,
Chrome Web Store, Limited Use, GDPR, HTTP, DevTools, AI, IP, URL, DOM,
grok-4-fast, claude-opus-4) stay verbatim — they live inside <x id="…"/>
placeholders extracted by ng extract-i18n and so are preserved automatically.
Prose between placeholders is culturally and technically accurate for each
target locale.

Verified by `npm --prefix showcase/angular run build` (30 prerendered routes,
i18nMissingTranslation: error invariant green) + `verify:hreflang` (301/301
pass; /stats absent).
… evidence

- Append "Data Collection" section to store-assets/chrome-web-store/listing-copy.md
  mirroring the privacy-page telemetry disclosure (5 collected fields, 6
  NOT-collected categories, retention, opt-out, Limited Use affirmation
  using verbatim CWS-compliant phrasing, full privacy-policy link to
  full-selfbrowsing.com/privacy#telemetry-disclosure).
- New store-assets/chrome-web-store/privacy-practices-evidence.md is the
  in-repo source-of-truth for what the human publisher does on the CWS
  Developer Dashboard Privacy Practices tab: which checkboxes to tick
  (PII = YES, Web history = NO, Limited Use = YES), which to leave unticked,
  Privacy Policy URL, screenshots to capture at publish time, and the
  verbatim Limited Use compliance statement.
- New scripts/verify-store-listing.mjs reads listing-copy.md,
  privacy-practices-evidence.md, and extension/manifest.json and asserts
  the 5 CWS-readiness invariants (Data Collection heading, full-selfbrowsing.com/privacy
  reference, #telemetry-disclosure anchor, evidence file non-empty,
  manifest homepage_url present). Exit 0 PASS, exit 1 with descriptive
  failures on FAIL. Uses only Node built-ins.
- New tests/verify-store-listing.test.js spawns the script and asserts
  exit 0 + stdout contains PASS + stderr empty of FAIL.
- New tests/showcase-privacy-page.test.js reads the privacy-page template
  directly (no browser needed) and asserts 53 invariants: stable anchor
  id="telemetry-disclosure", "Anonymous Usage Telemetry" heading, the 5
  collected fields + 6 NOT-collected categories, /api/telemetry/forget
  curl recipe, /stats link, GDPR Article 17 attribution, all 6 verbatim
  Limited Use clause phrases (paraphrasing forbidden by spec), all 24
  required @@PRIVACY_TELEMETRY_* i18n markers.
- Wire scripts/verify-store-listing.mjs + both new tests into root
  package.json test chain immediately after tests/showcase-build-smoke.test.js.
- Auto-fix (Rule 2): add homepage_url = https://full-selfbrowsing.com to
  extension/manifest.json so the CI guard's 5th assertion holds.
…hase

Final BLOCKER B3 closeout for v0.9.69. Adds .planning/phases/275-…/{275-SUMMARY.md, 275-VERIFICATION.md} documenting the 4 atomic task commits (c9db4f0412195f94144eeb3fc2ee), 5/5 verification gates green, and the user-gated CWS Developer Dashboard click-through workflow (D-15) deferred to publish time.

Updates STATE.md (progress 75% → 87.5%, 7/8 phases complete, B3 RESOLVED), ROADMAP.md (Phase 275 checked + completion note), and REQUIREMENTS.md (CONS-03..07 all Complete with commit-pinned attribution).
…hesis #1)

- Add .planning/phases/276-.../276-DIAGNOSTIC.md: reproduction steps,
  log capture commands (server stdout, fsb_diagnostics_ring, dashboard
  transport-event history), 7-hypothesis validation matrix with predicted
  symptom + verification command + fix surface per row, empty
  captured-logs / root-cause / fix-applied sections for HUMAN repro pass.
- showcase/server/src/ws/handler.js setupWSHandler: log each connection as
  '[WS] <role> connected | hashKey=<prefix>' and emit a structured
  '[WS] room-state | roles=<comma-separated> hashKey=<prefix>' line on every
  connect. A hashKey-mismatch bug now produces two room-state lines with
  different hashKey prefixes; the happy path produces one line with
  roles=ext,dash. This makes DIAGNOSTIC.md row-1 verifiable via grep.
- Pair-handshake contract documented in the setupWSHandler JSDoc block.

Defensive patch; harmless if hypothesis #1 is not the actual root cause.
…ntent re-arm (hypotheses #2 + #4)

- extension/ws/ws-client.js:
  - Add module-level `_pendingStreamStart` queue + 200ms/5s constants
    (FSB_CONTENT_READY_POLL_INTERVAL_MS, FSB_CONTENT_READY_TIMEOUT_MS).
  - New `_waitForContentScriptReady(tabId, timeoutMs)` polls
    chrome.tabs.sendMessage({ action: 'pingDomStream' }) at 200ms cadence
    until the content-script dom-stream module responds { ready: true },
    or 5s deadline elapses. Replaces the arbitrary setTimeout(300) guess.
  - New `_onDomStreamReady(senderTabId)` re-arms a parked dash:dom-stream-start
    payload from the background.js domStreamReady handler.
  - `_handleDashboardStreamStart`: probe readiness before issuing
    domStreamStart. On timeout, park the payload in `_pendingStreamStart`
    for re-arm. On readiness, clear any prior parked intent so a late
    ready ping does not double-fire.
  - `_forwardToContentScript` reinjection branch: replace the
    `setTimeout(r, 300)` heuristic with `_waitForContentScriptReady`. On
    timeout, record dom-forward-failed with readyState=ping-timeout-after-inject
    instead of issuing a stale sendMessage.

- extension/content/dom-stream.js: add `case 'pingDomStream'` in the
  chrome.runtime.onMessage listener. Responds synchronously with
  { ready: true } once the dom-stream module has installed its listener
  (i.e. the IIFE has executed past the addListener call).

- extension/background.js `case 'domStreamReady':` after the existing
  fsbWebSocket.send('ext:dom-ready', ...) call, invoke `_onDomStreamReady`
  inside a try/catch (non-blocking). Re-arms any parked stream-start
  intent when the dom-stream module signals load-complete.

- tests/dashboard-stream-readiness-ping.test.js: static invariants on the
  pingDomStream case + _waitForContentScriptReady function shape + the
  300ms heuristic removal, PLUS three behavioural simulations against a
  stubbed chrome.tabs.sendMessage:
    * sim A: ready on 3rd poll -> resolves true at ~150ms
    * sim B: never ready -> resolves false at the 300ms test budget
    * sim C: immediate ready -> resolves true under 100ms
  16/16 PASS.

- tests/dashboard-stream-pending-intent.test.js: static invariants on the
  _pendingStreamStart + _onDomStreamReady wiring (parking, clearing,
  re-dispatch, idempotency) + a behavioural simulation that parks an
  intent, fires ready, asserts exactly one re-dispatch with the original
  payload, then fires ready again and asserts no double-fire. 14/14 PASS.

Defensive patch chain; harmless even if hypotheses #2 and #4 are not the
actual root cause -- the readiness ping always succeeds on the happy path
and the pending-intent flag is null on the happy path.
…ailures, stale counts + Resync button

- showcase/angular/src/app/pages/dashboard/dashboard-page.component.ts:
  - Add `lastFrameTime` (number; refreshed on every snapshot AND every
    mutation envelope) and `mutationsAppliedTotal` (number; per accepted
    DOM op inside handleDOMMutations).
  - Add `lastFrameAgo()` getter returning seconds-since-last-frame (0 when
    no frame observed yet).
  - Reset `mutationsAppliedTotal` inside `resetPreviewGenerationState` so
    fresh snapshots start counting from 0. `lastFrameTime` is left as-is
    so the tooltip's "X seconds ago" reading does not jump back to 0
    mid-cycle.
  - Update `updatePreviewTooltip` to append 4 new diagnostic lines after
    the existing snapshot/state/reason rows:
      last-frame: <N>s ago | mutations: <count> | apply failures: <count> | stale: <count>
  - Wire a new `previewResyncBtn` DOM ref + click listener invoking
    `requestPreviewResync('user-resync-button')` -- reuses the existing
    recovery surface the stale-mutation watchdog already triggers.

- showcase/angular/src/app/pages/dashboard/dashboard-page.component.html:
  - Add `<button id="dash-preview-resync-btn">` as the first control in
    `.dash-preview-controls` (before the rc/pip/maximize/fullscreen/toggle
    buttons). Title + aria-label only -- no i18n attributes added since
    the surrounding buttons (PiP, Maximize, Fullscreen) are also unmarked.

- showcase/angular/src/app/pages/dashboard/dashboard-page.component.scss:
  - `.dash-preview-tooltip`: relax single-line ellipsis to multi-line wrap
    (`white-space: normal; word-break: break-word; max-width: 360px;
    line-height: 1.35`) so the four extra diagnostic counters remain
    legible at narrower viewport widths.
  - `.dash-preview-resync-btn:hover`: tint the icon with the accent
    colour to distinguish it from the play/pause toggle glyph.

Pure UI add; no behavioural changes to the streaming pipeline beyond
exposing already-tracked state in the tooltip and routing user-initiated
resyncs through the existing `requestPreviewResync` pathway.
… (bonuses)

- extension/background.js fsb-domstream-watchdog alarm handler: if
  `_streamingActive` is true AND fsbWebSocket is connected, send
  `ext:request-snapshot` (reason=sw-watchdog-tick, ts=Date.now()) on every
  watchdog tick. Best-effort; wrapped in try/catch. No-op when the WS is
  offline or _streamingActive is false/undefined. Covers the case where
  the SW just woke from idle eviction and no mutation has flushed yet --
  asking the dashboard to re-request a snapshot routes through the
  existing requestPreviewResync path.

- showcase/server/src/ws/handler.js:
  - Add `BACKPRESSURE_BUFFER_LIMIT_BYTES = 16 * 1024 * 1024` (16 MiB).
  - Add module-level `backpressureDroppedCount` counter + the
    `getBackpressureDroppedCount` accessor + `_resetBackpressureDroppedCount`
    test helper. All exported.
  - Update `sendToClients`: before each `client.send(data)`, check
    `client.bufferedAmount > BACKPRESSURE_BUFFER_LIMIT_BYTES`. If so,
    increment the counter, increment droppedCount, push a `backpressure-drop`
    event into the room diagnostics ring (carrying type, bufferedAmount,
    limitBytes), and skip the send. The pre-existing `readyState != OPEN`
    drop path is NOT routed through the backpressure counter -- only
    genuine buffer-overflow drops.
  - Also export `sendToClients` from the module so the test can invoke it
    directly with stub clients.

- tests/server-ws-backpressure.test.js: 22-assertion test covering:
    * exports of the limit, counter accessor, reset helper
    * single-wedged-client scenario (drop + counter increment; send NOT called)
    * happy-path delivery (no drop)
    * CLOSING-state drop does NOT increment the backpressure counter
    * counter accumulates across 5 successive sendToClients calls
    * backpressure-drop event recorded in the room diagnostics ring
  22/22 PASS.

Defensive bonuses; both are harmless in normal operation. The watchdog
auto-resnapshot is a one-way safety net (no extra side effect when
streaming is healthy); the backpressure drop only fires when a client
is already wedged at 16 MiB of buffered output.
- package.json root test chain: insert 3 new tests AFTER
  tests/verify-store-listing.test.js so the Phase 275 wiring point is
  preserved AND the new Phase 276 invariants run in the same npm test
  pass:
    + tests/dashboard-stream-readiness-ping.test.js
    + tests/dashboard-stream-pending-intent.test.js
    + tests/server-ws-backpressure.test.js
  No reordering of pre-existing entries.

- .planning/phases/276-.../276-SUMMARY.md: complete phase summary,
  frontmatter (requirements_satisfied, decisions, metrics), one-liner,
  4 sections describing what landed in each of the 4 prior commits, the
  3-test summary table with 52/52 PASS, deviation log (none auto-fixed),
  authentication-gate log (none), known-stubs note (none), threat-flag
  note (none), constraint-compliance checkboxes, what-remains-for-human
  block, and self-check showing 6 created + 8 modified file paths +
  4 task-commit hashes confirmed in git log.

- .planning/phases/276-.../276-VERIFICATION.md: 4-row automated-gate
  table (3 new tests + showcase-build-smoke 134/134 PASS), regression
  sweep on 10 representative existing tests (all green), self-check
  reference, constraint-compliance checkboxes, status `human_needed`,
  and the `<human_verification>` block:
    * 8-step manual repro
    * hypothesis-walk instructions: per-row table noting which
      defensive patch is already landed and what to do if each row fires
    * 5-attempt cap note (this counts as attempt 1 of 5)
    * outcome-recording playbook (PASS path -> status: passed;
      FAIL path -> follow-up phase or v0.9.70 re-scope).

- .planning/STATE.md frontmatter: completed_phases 7 -> 8;
  completed_plans 8 -> 9; percent 87.5 -> 100; last_updated bumped;
  last_activity now reads Phase 276 completion + 4 commit hashes + test
  counts + status human_needed. Progress bar updated 87.5% -> 100%.
  Performance Metrics table appended Phase 275 P01 + Phase 276 P01 rows.
  Session Continuity section appended Phase 276 entry with commit
  hashes + STREAM-01..06 satisfied + STREAM-07 attempt 1 of 5 note.
  Resume file: None.

- .planning/ROADMAP.md: Phase 276 checkbox flipped [ ] -> [x] with a
  full one-paragraph closeout summarising defensive patches landed,
  status `human_needed`, and STREAM-07 attempt budget. Phase 274 + 275
  + 276 status rows updated in the phase-progress table.

- .planning/REQUIREMENTS.md: STREAM-01, STREAM-02, STREAM-04, STREAM-05,
  STREAM-06, STREAM-07 marked Complete with per-line commit pointers.
  STREAM-03 marked Partial (human_needed) since acceptance smoke
  requires browser repro. Traceability table updated.

Phase 276 -- FINAL phase of v0.9.69 milestone -- closed in
human_needed status with all defensive patches landed, all automated
gates green, and the manual repro pass reserved for the user.
…for v0.9.69 telemetry tests

quick task 260514-pu4: the expanded npm test chain (~108 files) now includes 13 server-side telemetry tests + 4 Angular/server integration tests that require showcase/server (express-rate-limit, better-sqlite3, ws) and showcase/angular (chart.js + Angular CLI) deps. CI's extension job previously installed only root + mcp deps; PRs to main would fail at the first server-* test.

Verified locally: npm test exits 0 with all 30 v0.9.69 tests passing in the chain.

No fly.toml / Dockerfile / schema.js changes needed — telemetry tables auto-create via CREATE TABLE IF NOT EXISTS at server boot; npm ci in Dockerfile picks up both new deps.
…nt:i18n caught at PR #50)

CI's lint:i18n flagged privacy-page.component.html:134 — the <pre><code>...</code></pre>
curl block was missing an i18n attribute. Added @@PRIVACY_TELEMETRY_ERASE_CURL with a
meaning note guiding translators to copy the command literally (do not translate).

Restored {{ '{' }} / {{ '}' }} Angular interpolation escape — using HTML entities &#123;/
&#125; caused the EN-root prerender to fail because the decoded { } characters got
parsed as Angular ICU expressions.

Updated all 5 non-en XLFs (es/de/ja/zh-CN/zh-TW) with target translations matching the
new source structure (INTERPOLATION placeholders + literal curl command).

Verified: lint:i18n clean, ng build prerenders 30/30 routes, verify:hreflang 301/0 pass,
npm test exit 0.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92c890ca75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +320 to +325
var existing = await storage.get([FSB_USAGE_DATA_KEY]);
var arr = (existing && Array.isArray(existing[FSB_USAGE_DATA_KEY])) ? existing[FSB_USAGE_DATA_KEY] : [];
arr.push(row);
var write = {};
write[FSB_USAGE_DATA_KEY] = arr;
await storage.set(write);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Serialize fsbUsageData appends to avoid dropping rows

recordDispatch appends via a getpushset read/modify/write cycle, but the dispatcher invokes this method in fire-and-forget mode for each tool call, so concurrent dispatches can interleave: two calls read the same array snapshot, each pushes its own row, and the later set overwrites the earlier one. In busy sessions this silently loses telemetry events and undercounts tokens/cost in both extension analytics and server-side aggregates.

Useful? React with 👍 / 👎.

Comment on lines +634 to +635
var curr = (typeof ev.attempts === 'number' && ev.attempts >= 0) ? ev.attempts : 0;
ev.attempts = curr + 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Strip retry metadata from telemetry payloads

The retry path mutates queued events by adding an attempts property, then sends the full event objects as-is. The server validator accepts only the 9 allowlisted fields, so any retried event now contains an extra field and is rejected with unknown_field, turning transient POST failures into persistent retry failures and dropping those events after the attempts cap.

Useful? React with 👍 / 👎.

Comment on lines +547 to +548
await _advanceWatermark(storage, nowMs);
await _writeQueue(storage, queue);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid watermark jumps that skip in-flight usage rows

Flush aggregates fsbUsageData by row.ts > watermark, but then advances WATERMARK_KEY to Date.now() after the read. MCP dispatch recording runs concurrently and writes rows with ts=Date.now(), so rows appended between the aggregation read and this watermark write can end up with ts <= new watermark and be permanently skipped on the next cycle, causing silent telemetry loss under active traffic.

Useful? React with 👍 / 👎.

Comment on lines +1664 to +1670
if (released) {
try {
const cur = await chrome.storage.local.get(['fsbActiveAgentsCount']);
const n = (cur && typeof cur.fsbActiveAgentsCount === 'number' && cur.fsbActiveAgentsCount > 0)
? Math.floor(cur.fsbActiveAgentsCount)
: 0;
await chrome.storage.local.set({ fsbActiveAgentsCount: Math.max(0, n - 1) });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep active-agent counter in sync with all release paths

The counter is decremented only in handleAgentReleaseRoute, but agents are also removed by other flows (for example pool-drain releaseTab cleanup and staged connection-loss expiry in the registry). Those paths bypass this decrement, so fsbActiveAgentsCount can drift upward and keep reporting inflated active_agent_count telemetry long after agents are actually gone.

Useful? React with 👍 / 👎.

const day_utc = utcDayString(nowMs);
const entry = budgets.get(installUuid);
if (!entry || entry.day_utc !== day_utc) {
budgets.set(installUuid, { count: 1, day_utc });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prune expired per-UUID budget entries

The daily-budget map only updates the touched UUID and never removes old UUID keys, so a long-running server accumulates one permanent entry per unique install UUID ever seen. Under sustained adoption this creates unbounded in-memory growth in middleware state and can eventually pressure process memory even though old UUIDs are no longer active.

Useful? React with 👍 / 👎.

Number.isInteger(active_agent_count) && active_agent_count >= 0
? active_agent_count
: 0;
seen.set(install_uuid, { ts: ts_ms, agent_count: agentCount });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Evict stale active-tracker entries on ingest

Stale entries are evicted only by read APIs, but ingest writes keep inserting/updating UUIDs regardless of map size. If /api/public-stats/* is not queried frequently, old installs are never purged and the in-memory tracker grows without bound as telemetry traffic continues, which can cause avoidable memory bloat on the server.

Useful? React with 👍 / 👎.

@LakshmanTurlapati LakshmanTurlapati merged commit b8d3063 into main May 15, 2026
8 checks passed
LakshmanTurlapati pushed a commit that referenced this pull request May 15, 2026
… avoid concurrent-dispatch row loss (Codex PR #50 P1)

- Add module-level _recordLock + _withRecordLock helper mirroring the proven
  _flushLock pattern from telemetry-collector.js. Two concurrent
  recordDispatch() calls now chain onto a serialised promise; the
  get->push->set rmw against chrome.storage.local.fsbUsageData cannot
  interleave so neither row is lost.
- Move the get->push->set + ANALYTICS_UPDATE broadcast INSIDE the locked
  region. Pure synchronous setup (input validation, token estimate,
  pricing lookup, row assembly) stays outside the lock -- it has no
  shared state.
- Broadcast continues to fire AFTER each successful set, in the same
  per-row order as today, so the Control Panel hero re-render order
  matches storage-write order.
- The outer never-throw try/catch is preserved.
- Add tests/extension-record-dispatch-serializes.test.js: 20 concurrent
  recordDispatch() calls -> 20 rows; broadcast count == row count;
  broadcast ordering preserved. Test is authoritative -- verified to
  FAIL on the legacy (pre-fix) code path with 1/20 rows recorded.

Addresses Codex P1A on PR #50. Restores the COST-05 "no double count /
no dropped row" promise in the live anonymous telemetry pipeline.
LakshmanTurlapati pushed a commit that referenced this pull request May 15, 2026
…er allowlist doesn't reject retries (Codex PR #50 P1)

- Build a `wireEvents` projection in _runFlush() that copies EXACTLY the
  9 ALLOWED_EVENT_KEYS per event before JSON.stringify. The explicit
  per-field copy (no spread, no Object.assign of row) prevents the
  in-memory `attempts` retry counter (set by _bumpAttempts on POST
  failure) from leaking into the request body, which the server's 9-field
  allowlist (showcase/server/src/routes/telemetry.js:41-44) would 400
  with unknown_field -- causing retried events to burn the 5-attempt cap
  and be permanently dropped.
- Object-literal key order mirrors the server's ALLOWED_EVENT_FIELDS Set
  for code-review clarity. Loop index `w` avoids shadowing.
- Projection is READ-ONLY over `snapshot`. In-memory queue retains
  `attempts` so _bumpAttempts + _applyAttemptsCap continue to work on
  retry. snapshotIds is unchanged (still drives post-POST residue
  removal).
- Add tests/telemetry-collector-strips-internal-fields.test.js:
    * Scenario A: queue seeded with attempts:2 -> POST body events have
      EXACTLY the 9-key allowlist; no `attempts`.
    * Scenario B: failed POST -> re-enqueued event still carries
      `attempts`, bumped 2 -> 3 (in-memory queue intact).
  Test is authoritative -- verified to FAIL on the legacy (pre-fix)
  code path (5 of 15 assertions fail on the wire-payload shape).
- Wire both new regression tests (this one + the recorder concurrency
  test from the previous commit) into root package.json scripts.test
  chain after the existing telemetry-collector.test.js anchor.

Addresses Codex P1B on PR #50. Restores the BEAT-04 "INSERT OR IGNORE
dedup on retry" promise in the live anonymous telemetry pipeline.
LakshmanTurlapati pushed a commit that referenced this pull request May 15, 2026
…eview (recordDispatch race + attempts wire leak)
LakshmanTurlapati added a commit that referenced this pull request May 15, 2026
…ch race + attempts wire leak) (#53)

* docs(260514-rm4): pre-dispatch plan for Codex P1 telemetry fixes

* fix(extension-mcp-metrics-recorder): serialize fsbUsageData append to avoid concurrent-dispatch row loss (Codex PR #50 P1)

- Add module-level _recordLock + _withRecordLock helper mirroring the proven
  _flushLock pattern from telemetry-collector.js. Two concurrent
  recordDispatch() calls now chain onto a serialised promise; the
  get->push->set rmw against chrome.storage.local.fsbUsageData cannot
  interleave so neither row is lost.
- Move the get->push->set + ANALYTICS_UPDATE broadcast INSIDE the locked
  region. Pure synchronous setup (input validation, token estimate,
  pricing lookup, row assembly) stays outside the lock -- it has no
  shared state.
- Broadcast continues to fire AFTER each successful set, in the same
  per-row order as today, so the Control Panel hero re-render order
  matches storage-write order.
- The outer never-throw try/catch is preserved.
- Add tests/extension-record-dispatch-serializes.test.js: 20 concurrent
  recordDispatch() calls -> 20 rows; broadcast count == row count;
  broadcast ordering preserved. Test is authoritative -- verified to
  FAIL on the legacy (pre-fix) code path with 1/20 rows recorded.

Addresses Codex P1A on PR #50. Restores the COST-05 "no double count /
no dropped row" promise in the live anonymous telemetry pipeline.

* fix(telemetry-collector): strip attempts field from POST body so server allowlist doesn't reject retries (Codex PR #50 P1)

- Build a `wireEvents` projection in _runFlush() that copies EXACTLY the
  9 ALLOWED_EVENT_KEYS per event before JSON.stringify. The explicit
  per-field copy (no spread, no Object.assign of row) prevents the
  in-memory `attempts` retry counter (set by _bumpAttempts on POST
  failure) from leaking into the request body, which the server's 9-field
  allowlist (showcase/server/src/routes/telemetry.js:41-44) would 400
  with unknown_field -- causing retried events to burn the 5-attempt cap
  and be permanently dropped.
- Object-literal key order mirrors the server's ALLOWED_EVENT_FIELDS Set
  for code-review clarity. Loop index `w` avoids shadowing.
- Projection is READ-ONLY over `snapshot`. In-memory queue retains
  `attempts` so _bumpAttempts + _applyAttemptsCap continue to work on
  retry. snapshotIds is unchanged (still drives post-POST residue
  removal).
- Add tests/telemetry-collector-strips-internal-fields.test.js:
    * Scenario A: queue seeded with attempts:2 -> POST body events have
      EXACTLY the 9-key allowlist; no `attempts`.
    * Scenario B: failed POST -> re-enqueued event still carries
      `attempts`, bumped 2 -> 3 (in-memory queue intact).
  Test is authoritative -- verified to FAIL on the legacy (pre-fix)
  code path (5 of 15 assertions fail on the wire-payload shape).
- Wire both new regression tests (this one + the recorder concurrency
  test from the previous commit) into root package.json scripts.test
  chain after the existing telemetry-collector.test.js anchor.

Addresses Codex P1B on PR #50. Restores the BEAT-04 "INSERT OR IGNORE
dedup on retry" promise in the live anonymous telemetry pipeline.

* docs(quick-260514-rm4): Fix two Codex P1 telemetry bugs from PR #50 review (recordDispatch race + attempts wire leak)

---------

Co-authored-by: Lakshman <lakshman@lakshman-GG9LQ90J61.local>
@LakshmanTurlapati LakshmanTurlapati deleted the Refinements branch May 15, 2026 17:42
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