Skip to content

feat: add client id parameter into bw query#7823

Merged
limitofzero merged 10 commits into
developfrom
feat/add-client-id-for-bw
Jul 10, 2026
Merged

feat: add client id parameter into bw query#7823
limitofzero merged 10 commits into
developfrom
feat/add-client-id-for-bw

Conversation

@limitofzero

@limitofzero limitofzero commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Attach a browser-scoped client_id to every balances-watcher request. Backend indexes sessions by (chainId, owner, clientId). Backend doesn't support session-id yet, the frontend pr should be merged first otherwise it breaks the session.

To Test

  1. Open the swap UI on any EVM chain with a connected wallet
  • POST /sessions/* request in DevTools Network tab has an X-Client-Id header
  • /sse/*/balances/* request URL includes ?client_id=<uuid> query parameter
  • Both values are the same UUID
  • Refresh the page — the same client_id value is used (persisted)
  1. In DevTools Application tab → Local Storage
  • Key balances-watcher-client-id exists
  • Value is JSON {"id": "<uuid>", "createdAt": <timestamp>}
  1. TTL rotation (manual)
  • Overwrite createdAt in localStorage to a value older than 24h, refresh page
  • Next request uses a new client_id; localStorage entry is replaced with a fresh createdAt
  1. Storage-disabled fallback (private context / storage blocked)
  • Open the page in Safari private tab (or Chrome with cookies/storage blocked)
  • Requests still carry an X-Client-Id / ?client_id= value
  • Value stays stable while the tab is open, changes after closing/reopening

Background

Native EventSource does not support custom request headers per the WHATWG spec. Adding a polyfill (event-source-polyfill etc.) would let us mirror the POST header on SSE but adds a runtime dependency and a known set of reconnect/readyState bugs that we do not want to trade for pure cosmetic consistency. Query string is equally opaque and equally protected by TLS; the backend already accepts both.

The 1-day TTL bounds the long-term correlation window of a browser to a stable session key and gives orphan sessions on the backend an upper lifetime. Rotation happens only on the next call after a new tab opens or an existing one is refreshed, so a long-running tab keeps the same client_id for the whole session.

Summary by CodeRabbit

  • New Features
    • Balance watcher requests now include an X-Client-Id header, and the balances SSE stream now includes a client_id query parameter.
    • Added a stable, browser-persisted client identifier reused for up to one day.
  • Bug Fixes
    • When browser storage is unavailable, the identifier falls back to a consistent in-tab value.
    • Legacy/unexpected stored values are ignored and a new identifier is generated.
  • Tests
    • Added/updated coverage for identifier persistence, expiration, storage-failure fallback, and inclusion in headers/query parameters.

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cowfi Ready Ready Preview Jul 10, 2026 3:52pm
explorer-dev Ready Ready Preview Jul 10, 2026 3:52pm
sdk-tools Ready Ready Preview Jul 10, 2026 3:52pm
storybook Ready Ready Preview Jul 10, 2026 3:52pm
swap-dev Ready Ready Preview Jul 10, 2026 3:52pm
widget-configurator Ready Ready Preview Jul 10, 2026 3:52pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
cosmos Ignored Ignored Jul 10, 2026 3:52pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e42b7d00-e520-4c4b-b37b-aee93ef71c7a

📥 Commits

Reviewing files that changed from the base of the PR and between 8d9b92d and 0f7b696.

📒 Files selected for processing (1)
  • apps/cowswap-frontend/src/locales/en-US.po

Walkthrough

Adds a persisted balances-watcher client ID with TTL and in-memory fallback, sends it in session and SSE requests, adds coverage for the behavior, and updates related translation entries.

Changes

Balances Watcher Client ID

Layer / File(s) Summary
Client ID persistence module and tests
libs/balances-and-allowances/src/balancesWatcher/clientId.ts, libs/balances-and-allowances/src/balancesWatcher/clientId.test.ts
Adds validated JSON storage, TTL rotation, legacy-value rejection, and stable in-memory fallback behavior, with tests covering persistence and storage failures.
Session creation header wiring
libs/balances-and-allowances/src/balancesWatcher/createSession.ts, libs/balances-and-allowances/src/balancesWatcher/createSession.test.ts
Adds an X-Client-Id header sourced from getBalancesWatcherClientId() and verifies it in tests.
SSE subscription query parameter
libs/balances-and-allowances/src/balancesWatcher/subscribeToBalancesEvents.ts, libs/balances-and-allowances/src/balancesWatcher/subscribeToBalancesEvents.test.ts
Builds the SSE URL with URL, appends client_id, and verifies the resulting URLs in tests.

Translation entries

Layer / File(s) Summary
Confirmation modal translation entries
apps/cowswap-frontend/src/locales/en-US.po
Updates token and total-fee source references and adds the “Insufficient {inputSymbol} balance” entry.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: alfetopito, fairlighteth, shoom3301, Danziger

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant getBalancesWatcherClientId
  participant localStorage
  participant Server

  App->>getBalancesWatcherClientId: request client id
  getBalancesWatcherClientId->>localStorage: read or persist client id
  getBalancesWatcherClientId-->>App: return client id
  App->>Server: POST session with X-Client-Id
  App->>Server: open SSE stream with client_id
Loading

Poem

A UUID rests in storage bright,
Then travels through the stream tonight. 🐇
Headers and URLs bear its glow,
While memory keeps it when stores say no.
Hop, hop — one ID stays in flow! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and matches the main change, though it mentions only the query parameter and not the header addition.
Description check ✅ Passed The description includes the required Summary, To Test, and Background sections with clear implementation and verification details.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-client-id-for-bw

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying explorer-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0f7b696
Status: ✅  Deploy successful!
Preview URL: https://c3783044.explorer-dev-dxz.pages.dev
Branch Preview URL: https://feat-add-client-id-for-bw.explorer-dev-dxz.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying swap-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0f7b696
Status: ✅  Deploy successful!
Preview URL: https://9ff318da.swap-dev-5u6.pages.dev
Branch Preview URL: https://feat-add-client-id-for-bw.swap-dev-5u6.pages.dev

View logs

@elena-zh elena-zh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Works as described!

Comment thread libs/balances-and-allowances/src/balancesWatcher/clientId.ts Outdated

@fairlighteth fairlighteth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ AI Review (Codex GPT-5, worked 4m): client_id can rotate between POST and SSE

Finding: [BLOCKING] Pin the balances-watcher client_id across the POST → SSE handshake

  • Location: libs/balances-and-allowances/src/hooks/balancesWatcherSessionController.ts:128
  • This one is important: the PR says the backend indexes sessions by (chainId, owner, clientId), and the checklist requires the POST X-Client-Id and SSE client_id to be the same UUID.
  • Current code calls getBalancesWatcherClientId() independently in createBalancesWatcherSession and subscribeToBalancesEvents. If the stored ID is still fresh when the POST starts but crosses the 24h TTL before the POST resolves and openStream() runs, the POST registers the old ID while the SSE URL rotates to a new ID.
  • Result: the SSE stream can look up a different backend session than the one just created.

Suggested fix

  • Generate/read the client ID once per session attempt and pass that same value into both helpers, or make getBalancesWatcherClientId() cache the active tab ID so TTL is only re-evaluated on a new page load.
  • Add a regression test where the stored ID expires between createBalancesWatcherSession and subscribeToBalancesEvents, and assert both requests still use the same ID.
Review scope and related context
  • Existing review context did not cover this: CodeRabbit reported no actionable comments, and there are no review threads.
  • The existing tests cover fresh storage, expired storage, POST header, and SSE query parameter separately, but not the controller boundary where the same session attempt must keep one ID across both requests.
🤖 Prompt for AI agents
Verify this finding against current code. Fix only if still valid, keep the change minimal, and validate with targeted tests.

Context:
- createSessionController calls createBalancesWatcherSession(), waits for it to resolve, then opens subscribeToBalancesEvents().
- createBalancesWatcherSession and subscribeToBalancesEvents each call getBalancesWatcherClientId() separately.
- If the 24h TTL boundary is crossed between those calls, POST and SSE can use different client IDs.
- Expected fix: pin one client ID per session attempt or cache the active tab ID until reload, then add a regression test for the TTL-boundary handshake.

Generated using the pr-review skill from the CoW Protocol skills repo.

@limitofzero

Copy link
Copy Markdown
Contributor Author

⚠️ AI Review (Codex GPT-5, worked 4m): client_id can rotate between POST and SSE

done

@limitofzero limitofzero requested a review from Danziger July 10, 2026 13:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
libs/balances-and-allowances/src/balancesWatcher/clientId.test.ts (1)

10-19: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider avoiding the non-null assertion in loadModule.

Line 14 uses mod! to satisfy TypeScript. While this is test code (not production), the coding guidelines discourage non-null assertions in .ts files. Since jest.isolateModules runs its callback synchronously, the assignment is guaranteed, but a default initialization avoids the assertion entirely.

♻️ Optional refactor to avoid non-null assertion
-  let mod!: typeof import('./clientId')
+  let mod: typeof import('./clientId') | undefined
   jest.isolateModules(() => {
     mod = require('./clientId')
   })
-  return mod
+  return mod!

Alternatively, restructure to return from inside the callback via a wrapper:

 function loadModule(): typeof import('./clientId') {
-  let mod!: typeof import('./clientId')
-  jest.isolateModules(() => {
-    mod = require('./clientId')
-  })
-  return mod
+  // jest.isolateModules runs synchronously, so this is safe
+  const scope: { mod?: typeof import('./clientId') } = {}
+  jest.isolateModules(() => {
+    scope.mod = require('./clientId')
+  })
+  return scope.mod!
 }

Note: fully eliminating ! here is awkward because jest.isolateModules doesn't return a value. If the team considers test files exempt from the guideline, this can be skipped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@libs/balances-and-allowances/src/balancesWatcher/clientId.test.ts` around
lines 10 - 19, Replace the non-null assertion initialization in loadModule with
a safe default initialization or equivalent wrapper that avoids !, while
preserving the synchronous jest.isolateModules behavior and returning the loaded
clientId module.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@libs/balances-and-allowances/src/balancesWatcher/clientId.test.ts`:
- Around line 10-19: Replace the non-null assertion initialization in loadModule
with a safe default initialization or equivalent wrapper that avoids !, while
preserving the synchronous jest.isolateModules behavior and returning the loaded
clientId module.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 594f12a1-2b7d-4c62-9517-8e37199a7ee2

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab1cac and 754c98a.

📒 Files selected for processing (2)
  • libs/balances-and-allowances/src/balancesWatcher/clientId.test.ts
  • libs/balances-and-allowances/src/balancesWatcher/clientId.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/balances-and-allowances/src/balancesWatcher/clientId.ts

@limitofzero limitofzero enabled auto-merge (squash) July 10, 2026 15:47
@limitofzero limitofzero merged commit 1ab0190 into develop Jul 10, 2026
18 of 19 checks passed
@limitofzero limitofzero deleted the feat/add-client-id-for-bw branch July 10, 2026 15:55
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 10, 2026
@fairlighteth

Copy link
Copy Markdown
Contributor
AI Review (Codex GPT-5, worked 4m): follow-up addressed

Rechecked

  • Author response: marked as done.
  • Code path: libs/balances-and-allowances/src/balancesWatcher/clientId.ts:18.
  • Test coverage: clientId.test.ts:90 now crosses the TTL boundary and asserts the ID remains stable. The balances-and-allowances CI suite passed all 8 suites and 72 tests.

Result: Fixed. The first call now caches the selected ID for the tab lifetime, so the later SSE call cannot rotate away from the ID used by the POST. Rotation occurs only after a page/module reload, matching the updated PR contract. No new non-duplicate findings surfaced.

🤖 Verification notes for AI agents
Verify the prior finding against current code only.

Prior finding:
- POST and SSE called getBalancesWatcherClientId() separately, allowing TTL rotation between them.

Current verification:
- A module-scoped cachedClientId is populated on the first call and returned before subsequent storage/TTL checks.
- The regression test advances time beyond one day and confirms both calls return the same ID.
- The balances-and-allowances CI suite passed.

Generated using the pr-review skill from the CoW Protocol skills repo.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants