Skip to content

fix(auth): share the in-flight me() request - #245

Open
guyofeck wants to merge 1 commit into
mainfrom
fix/single-flight-me-analytics-session
Open

fix(auth): share the in-flight me() request#245
guyofeck wants to merge 1 commit into
mainfrom
fix/single-flight-me-analytics-session

Conversation

@guyofeck

@guyofeck guyofeck commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Problem

BUG-837: an app is ~190ms slower on every cold load because the SDK issues a second GET /entities/User/me at client construction while the app is already issuing its own. Chrome serializes the two identical GETs, so the second pays the first's full latency.

The source is the analytics module: createAnalyticsModule runs at client construction, trackInitializationEvent queues an event, startAnalyticsProcessor sends the first batch before the throttleTime sleep, and flush() awaits getSessionContext()auth.me().

Fix

Share the in-flight request in auth.me(). Both the app and analytics call this same method, so collapsing it there is what removes the duplicate — deduping inside the analytics module would not help, and getSessionContext already shares its own promise.

The pending promise is cleared as soon as the request settles, so no resolved user is ever retained. Caching identity across requests would leave an app rendering a logged-in view after logout or a session swap. setToken and logout also drop the pending request: a me() already in flight must not resolve into callers that run after the identity changed.

This also collapses apps that call me() twice on their own.

Two related fixes in the analytics module

  • Stale user_id. The session context memoized the resolved user_id for the lifetime of the session and nothing ever reset it — not logout, not setToken. A visitor who loaded a page anonymously and then logged in kept reporting user_id: null on every subsequent event. It is now cleared on any identity change. This is a data-quality bug, not a latency one.
  • Heartbeat outside the browser. startHeartBeatProcessor was not window-guarded, unlike the other automatic events here (initialization, session duration, visibility). On a long-lived server-side client it fired a me() every 60s and its interval kept the Node event loop alive. Explicit analytics.track() calls from backend functions are unaffected.

Scope — what this does not fix

Prod data (24h) shows ~88% of the 5.34M daily User/me calls come from the server-side SDK in backend functions, not browsers. This PR removes one request per cold browser load; it does not touch that server-side share, and it does not touch the ~33ms of per-request app-metadata reload that dominates the endpoint's 40ms p50. That work is separate, in apper.

The Datadog p50 will not move on merge day. The SDK is pinned at ^0.8.35 in the app templates, so apps pick this up on their next install or rebuild; the deployed fleet trails.

Testing

npm run test:unit — 199 pass (7 new). Lint, type-tests, and build clean.

The concurrency test uses a single nock interceptor with disableNetConnect, so a second GET fails the test; verified it fails against the unfixed me().

Not covered: no browser-level test that the two GETs actually stop serializing — the fix is asserted at the request-count level.

Noticed, not fixed

flush()'s beacon branch looks inverted: beaconRequest returns true when the beacon cannot be used, so if (!options.isBeacon || !beaconRequest(events)) skips the axios fallback exactly when the beacon failed, and double-sends when it succeeded. Left alone as unrelated to this fix — worth its own look.

🤖 Generated with Claude Code

The analytics module resolves its session context through `auth.me()` at
client construction, which is the same moment most apps issue their own
`me()`. Browsers serialize the two identical GETs, so the second pays the
first's full latency — ~190ms on a cold load (BUG-837).

`auth.me()` now shares its pending promise between concurrent callers.
The promise is cleared as soon as the request settles, so no resolved user
is ever retained: caching identity across requests would leave an app
rendering a logged-in view after logout or a session swap. `setToken` and
`logout` drop the pending request for the same reason — a `me()` already in
flight must not resolve into callers that run after the identity changed.

Deduping inside the analytics module would not have helped; the duplicate
is between analytics and the app, and both go through `auth.me()`.

Two related fixes in the analytics module:

- The session context memoized the resolved `user_id` for the lifetime of
  the session and was never reset, so a visitor who loaded a page
  anonymously and then logged in kept reporting `user_id: null` on every
  subsequent event. It is now cleared on any identity change.
- The heartbeat is browser-only, matching the other automatic events here.
  Outside a browser it fired a `me()` every 60s for the lifetime of a
  long-lived server-side client and kept the Node event loop alive.
  Explicit `analytics.track()` calls are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@guyofeck
guyofeck requested review from OmerKat and roymiloh August 9, 2026 09:59
@guyofeck
guyofeck marked this pull request as ready for review August 9, 2026 09:59
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