feat(analytics): threads and turns now know which client started them - #7774
feat(analytics): threads and turns now know which client started them#7774t3dotgg wants to merge 1 commit into
Conversation
Clients announce surface (web/desktop/mobile) and app version as optional query params on the /ws upgrade. The server stamps them onto PostHog events (client.connected, client.thread.started, client.turn.requested), refreshes auth_sessions on every connect, and records the origin in orchestration event metadata. All fields optional both ways: old clients and old servers keep working unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
One convention issue found: the new AnalyticsService imports use a named tag import instead of the local service-module namespace import used everywhere else in this repo (server.ts, serverRuntimeStartup.ts, provider/Layers/ProviderService.ts). Everything else in the diff (dispatch origin plumbing, SessionStore.recordClientConnection, AuthSessionRepository.setClientConnection, migration 041, contracts schemas) follows the service, error, and dependency-acquisition conventions.
Posted via Macroscope — Effect Service Conventions
| import * as ResourceAttribution from "./resourceTelemetry/ResourceAttribution.ts"; | ||
| import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; | ||
| import * as UsageService from "./usage/UsageService.ts"; | ||
| import { AnalyticsService } from "./telemetry/AnalyticsService.ts"; |
There was a problem hiding this comment.
Same namespace convention here; AnalyticsService.layerTest at line 837 keeps working since the module re-exports it.
| import { AnalyticsService } from "./telemetry/AnalyticsService.ts"; | |
| import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; |
Posted via Macroscope — Effect Service Conventions
| import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; | ||
| import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; | ||
| import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; | ||
| import { AnalyticsService } from "./telemetry/AnalyticsService.ts"; |
There was a problem hiding this comment.
This is a service boundary, so consider importing the service module as a namespace and using its public module shape (AnalyticsService.AnalyticsService), matching server.ts, serverRuntimeStartup.ts, and provider/Layers/ProviderService.ts — and every other local service import in this file.
-import { AnalyticsService } from "./telemetry/AnalyticsService.ts";
+import * as AnalyticsService from "./telemetry/AnalyticsService.ts";The two acquisition sites then become yield* AnalyticsService.AnalyticsService (lines 394 and 2393).
Posted via Macroscope — Effect Service Conventions
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 36694fb. Configure here.
| ORCHESTRATION_WS_METHODS.dispatchCommand, | ||
| Effect.gen(function* () { | ||
| const normalizedCommand = yield* normalizeDispatchCommand(command); | ||
| yield* recordClientCommandAnalytics(normalizedCommand); |
There was a problem hiding this comment.
Analytics fires before dispatch succeeds
Medium Severity
recordClientCommandAnalytics runs before dispatchNormalizedCommand completes. Failed creates, turns, and bootstrap flows (including cleanup that deletes a just-created thread) still emit client.thread.started and client.turn.requested, so PostHog surface counts no longer match persisted threads and turns.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 36694fb. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a new client origin tracking feature with database schema changes, new analytics events, and modifications to the core orchestration dispatch flow. Additionally, there is an unresolved medium-severity finding about analytics firing before dispatch succeeds. Human review is warranted. You can add or adjust custom eligibility rules. Learn more. |


We can't answer basic product questions like "how many people use mobile" or "are threads started on desktop or phone": the server never learns which client sent a request. Pairing metadata lumps web and desktop together as "desktop", nothing carries an app version, and events, turns, and PostHog all have zero client attribution.
Now every client announces itself with two optional query params on the /ws upgrade, next to the existing wsTicket: clientSurface (web / desktop / mobile) and clientAppVersion. The server applies them in three places:
client.connected,client.thread.started, andclient.turn.requestedcarry surface and app version. Client rotation, multi-client users, and per-surface thread starts become plain PostHog queries.client_surfaceandclient_app_versioncolumns, refreshed on every connect, so rows track the client's current build instead of freezing at pairing.origininto the metadata of every event a client-dispatched command produces. The decider stays pure; local per-thread attribution is now queryable in SQLite.Every new field is optional on both ends. Old clients send nothing and everything behaves as before; old servers ignore the unknown params. No UI changes on any surface.
Tested: engine origin stamping, session store partial-update semantics (COALESCE), migration columns, plus the full server integration suite (125 tests) and typechecks across contracts, client-runtime, server, web, and mobile.
Built by Claude Fable 5 running in Claude Code.
Note
Medium Risk
Touches WebSocket handshake, auth_sessions schema, and orchestration event metadata. Attribution is optional and best-effort so connect/dispatch should not fail, but a bad migration or origin stamping bug would affect persistence and telemetry.
Overview
Clients now send optional
clientSurfaceandclientAppVersionon the/wsupgrade so the server can tell web, desktop, and mobile apart (pairing previously lumped web and desktop asdesktop).On connect the server records those fields on
auth_sessions(new nullable columns, COALESCE so partial reports never wipe earlier values), emitsclient.connected, and stamps the same origin onto every event produced by commands from that connection. Thread create / turn start also fireclient.thread.startedandclient.turn.requested.Web, desktop, and mobile populate
surface/appVersionin presentation metadata; old clients and old servers stay compatible because the params and eventmetadata.originare optional.Reviewed by Cursor Bugbot for commit 36694fb. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Track client
surfaceandappVersionthrough websocket connections to orchestration eventsClientSurface(web|desktop|mobile) andappVersionto client presentation metadata; web, desktop, and mobile clients now report both on connectclientSurface/clientAppVersionquery params to websocket URLs inresolveRemoteWebSocketConnectionUrlandresolveRemoteDpopWebSocketConnectionUrl/wsupgrade request, persists it toauth_sessionsviaSessionStore.recordClientConnection, and passes it into the RPC layerOrchestrationEngine.dispatchstampsoriginonto planned event metadata; analytics events forclient.connectedand selected commands now include origin propertiesclient_surfaceandclient_app_versioncolumns toauth_sessionsusingCOALESCEto preserve prior non-null valuesrecordClientConnectionis best-effort and logs a warning on failure, but reviewers should verify theCOALESCElogic insetClientConnectionRow(041_AuthSessionClientConnection.ts) does not overwrite existing values when new fields are absentMacroscope summarized 36694fb.