1import { ORCHESTRATION_V2_WS_METHODS, WS_METHODS, type WsRpcGroup } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as References from "effect/References";
5import type * as RpcGroup from "effect/rpc/RpcGroup";
6import * as RpcMiddleware from "effect/rpc/RpcMiddleware";
7
8import { rpcRequestDuration, rpcRequestsTotal, withMetrics } from "./Metrics.ts";
9
10type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];
11
12/**
13 * The `rpc.aggregate` span attribute of every WebSocket RPC. Trace queries and dashboards group by
14 * these labels, so they keep their historical values even where they differ from the method
15 * prefix. Adding an RPC to `WsRpcGroup` without a label is a type error.
16 */
17const RPC_AGGREGATES = {
18 [WS_METHODS.uiSettingsGet]: "settings",
19 [WS_METHODS.uiSettingsPatch]: "settings",
20 [WS_METHODS.uiSettingsSubscribe]: "settings",
21 [ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: "orchestrationV2",
22 [ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: "orchestration",
23 [ORCHESTRATION_V2_WS_METHODS.stopWorkflow]: "orchestration",
24 [ORCHESTRATION_V2_WS_METHODS.getTurnItem]: "orchestration",
25 [ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: "orchestration",
26 [ORCHESTRATION_V2_WS_METHODS.getFullThreadDiff]: "orchestration",
27 [ORCHESTRATION_V2_WS_METHODS.searchThreads]: "orchestration",
28 [ORCHESTRATION_V2_WS_METHODS.searchThread]: "orchestration",
29 [ORCHESTRATION_V2_WS_METHODS.searchThreadStream]: "orchestration",
30 [ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: "orchestration",
31 [ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: "orchestrationV2",
32 [ORCHESTRATION_V2_WS_METHODS.launchThread]: "orchestration",
33 [ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: "orchestration",
34 [ORCHESTRATION_V2_WS_METHODS.subscribeShell]: "orchestrationV2",
35 [ORCHESTRATION_V2_WS_METHODS.subscribeThread]: "orchestrationV2",
36 [WS_METHODS.projectsMutate]: "orchestration",
37 [WS_METHODS.serverProbe]: "server",
38 [WS_METHODS.serverGetConfig]: "server",
39 [WS_METHODS.serverRefreshProviders]: "server",
40 [WS_METHODS.serverUpdateProvider]: "server",
41 [WS_METHODS.providerAuthStart]: "provider",
42 [WS_METHODS.providerConsumeResetCredit]: "provider",
43 [WS_METHODS.providerAuthComplete]: "provider",
44 [WS_METHODS.chatGptReconnectProfile]: "provider",
45 [WS_METHODS.chatGptImportProfile]: "provider",
46 [WS_METHODS.chatGptHandoffSubscribe]: "provider",
47 [WS_METHODS.codexAuthCallbackSubscribe]: "provider",
48 [WS_METHODS.providerAuthRespond]: "provider",
49 [WS_METHODS.providerAuthCancel]: "provider",
50 [WS_METHODS.providerAuthLogout]: "provider",
51 [WS_METHODS.providerAuthSubscribe]: "provider",
52 [WS_METHODS.providerInstallStart]: "provider",
53 [WS_METHODS.providerInstallCancel]: "provider",
54 [WS_METHODS.providerInstallSubscribe]: "provider",
55 [WS_METHODS.providerInstallRemove]: "provider",
56 [WS_METHODS.serverUpdateServer]: "server",
57 [WS_METHODS.serverUpdateServerWithProgress]: "server",
58 [WS_METHODS.serverCommitDesktopUpdate]: "server",
59 [WS_METHODS.serverUpsertKeybinding]: "server",
60 [WS_METHODS.serverRemoveKeybinding]: "server",
61 [WS_METHODS.serverRunStorageCleanup]: "server",
62 [WS_METHODS.serverGetStorageCleanupReport]: "server",
63 [WS_METHODS.serverGetSettings]: "server",
64 [WS_METHODS.serverUpdateSettings]: "server",
65 [WS_METHODS.serverSearchAcpRegistry]: "server",
66 [WS_METHODS.serverPrepareAcpRegistryAgent]: "server",
67 [WS_METHODS.serverUninstallAcpRegistryManagedBinary]: "server",
68 [WS_METHODS.serverAcceptAcpRegistryUrlAuth]: "server",
69 [WS_METHODS.serverListAcpRegistrySessions]: "server",
70 [WS_METHODS.serverImportAcpRegistrySession]: "server",
71 [WS_METHODS.serverDeleteAcpRegistrySession]: "server",
72 [WS_METHODS.serverListAcpRegistryProviders]: "server",
73 [WS_METHODS.serverSetAcpRegistryProvider]: "server",
74 [WS_METHODS.serverDisableAcpRegistryProvider]: "server",
75 [WS_METHODS.serverLogoutAcpRegistry]: "server",
76 [WS_METHODS.serverDiscoverSourceControl]: "server",
77 [WS_METHODS.serverGetTraceDiagnostics]: "server",
78 [WS_METHODS.serverGetProcessDiagnostics]: "server",
79 [WS_METHODS.serverGetHostResources]: "server",
80 [WS_METHODS.serverGetProcessResourceHistory]: "server",
81 [WS_METHODS.serverGetResourceTelemetryHistory]: "server",
82 [WS_METHODS.serverRetryResourceTelemetry]: "server",
83 [WS_METHODS.serverCheckOtlpEndpoint]: "server",
84 [WS_METHODS.serverGetUsageSummary]: "server",
85 [WS_METHODS.serverRefreshUsageRates]: "server",
86 [WS_METHODS.serverSignalProcess]: "server",
87 [WS_METHODS.serverReportClientActivity]: "server",
88 [WS_METHODS.serverReportHostPowerState]: "server",
89 [WS_METHODS.serverGetBackgroundPolicy]: "server",
90 [WS_METHODS.scheduledTasksList]: "scheduledTasks",
91 [WS_METHODS.scheduledTasksSubscribe]: "scheduledTasks",
92 [WS_METHODS.scheduledTasksUpsert]: "scheduledTasks",
93 [WS_METHODS.scheduledTasksSetEnabled]: "scheduledTasks",
94 [WS_METHODS.scheduledTasksDelete]: "scheduledTasks",
95 [WS_METHODS.scheduledTasksRunNow]: "scheduledTasks",
96 [WS_METHODS.scheduledTasksRotateWebhookToken]: "scheduledTasks",
97 [WS_METHODS.scheduledTasksListWebhookDeliveries]: "scheduledTasks",
98 [WS_METHODS.scheduledTasksGetWebhookDelivery]: "scheduledTasks",
99 [WS_METHODS.secretsAnswerRequest]: "secrets",
100 [WS_METHODS.cloudGetRelayClientStatus]: "cloud",
101 [WS_METHODS.cloudInstallRelayClient]: "cloud",
102 [WS_METHODS.mergebotQueue]: "mergebot",
103 [WS_METHODS.mergebotSubscribe]: "mergebot",
104 [WS_METHODS.mergebotOpenPullRequest]: "mergebot",
105 [WS_METHODS.mergebotSetAutoMerge]: "mergebot",
106 [WS_METHODS.mergebotUpdatePullRequest]: "mergebot",
107 [WS_METHODS.mergebotSubscribePullRequestStatus]: "mergebot",
108 [WS_METHODS.mergebotSubscribeDeployments]: "mergebot",
109 [WS_METHODS.mergebotDeployProduction]: "mergebot",
110 [WS_METHODS.jjSubscribeStack]: "vcs",
111 [WS_METHODS.jjSnapshot]: "vcs",
112 [WS_METHODS.jjDiff]: "vcs",
113 [WS_METHODS.jjDescribe]: "vcs",
114 [WS_METHODS.jjSquash]: "vcs",
115 [WS_METHODS.jjPush]: "vcs",
116 [WS_METHODS.pullRequestsList]: "pull-requests",
117 [WS_METHODS.pullRequestsListStats]: "pull-requests",
118 [WS_METHODS.pullRequestsSummary]: "pull-requests",
119 [WS_METHODS.pullRequestsRouting]: "pull-requests",
120 [WS_METHODS.pullRequestsRoutingIdentity]: "pull-requests",
121 [WS_METHODS.pullRequestsStack]: "pull-requests",
122 [WS_METHODS.pullRequestsLinkedThreads]: "pull-requests",
123 [WS_METHODS.pullRequestsDetail]: "pull-requests",
124 [WS_METHODS.pullRequestsPreview]: "pull-requests",
125 [WS_METHODS.pullRequestsChecks]: "pull-requests",
126 [WS_METHODS.pullRequestsActivity]: "pull-requests",
127 [WS_METHODS.pullRequestsReviewGuide]: "pull-requests",
128 [WS_METHODS.pullRequestsGenerateReviewGuide]: "pull-requests",
129 [WS_METHODS.pullRequestsThreadComments]: "pull-requests",
130 [WS_METHODS.pullRequestsDiffFileContents]: "pull-requests",
131 [WS_METHODS.pullRequestsFilesViewed]: "pull-requests",
132 [WS_METHODS.pullRequestsRunAction]: "pull-requests",
133 [WS_METHODS.pullRequestsUpdate]: "pull-requests",
134 [WS_METHODS.pullRequestsComment]: "pull-requests",
135 [WS_METHODS.pullRequestsUpdateComment]: "pull-requests",
136 [WS_METHODS.pullRequestsSubmitReview]: "pull-requests",
137 [WS_METHODS.pullRequestsReplyToThread]: "pull-requests",
138 [WS_METHODS.pullRequestsSetThreadResolution]: "pull-requests",
139 [WS_METHODS.pullRequestsSetReaction]: "pull-requests",
140 [WS_METHODS.pullRequestsSetFilesViewed]: "pull-requests",
141 [WS_METHODS.pullRequestsInvalidate]: "pull-requests",
142 [WS_METHODS.pullRequestsReportState]: "pull-requests",
143 [WS_METHODS.pullRequestsSubscribeRefreshes]: "pull-requests",
144 [WS_METHODS.pullRequestsReviewerCandidates]: "pull-requests",
145 [WS_METHODS.pullRequestsRequestReviewers]: "pull-requests",
146 [WS_METHODS.pullRequestsLabelCandidates]: "pull-requests",
147 [WS_METHODS.pullRequestsSetLabels]: "pull-requests",
148 [WS_METHODS.sourceControlLookupRepository]: "source-control",
149 [WS_METHODS.sourceControlCloneRepository]: "source-control",
150 [WS_METHODS.sourceControlPublishRepository]: "source-control",
151 [WS_METHODS.projectCloneStart]: "source-control",
152 [WS_METHODS.projectCloneCancel]: "source-control",
153 [WS_METHODS.projectCloneRetry]: "source-control",
154 [WS_METHODS.subscribeProjectClones]: "source-control",
155 [WS_METHODS.projectsListEntries]: "workspace",
156 [WS_METHODS.devServerStatus]: "workspace",
157 [WS_METHODS.managedWorkspacesStatus]: "workspace",
158 [WS_METHODS.managedWorkspacesCreate]: "workspace",
159 [WS_METHODS.managedWorkspacesArchiveCompleted]: "workspace",
160 [WS_METHODS.devServerStart]: "workspace",
161 [WS_METHODS.projectsWatch]: "workspace",
162 [WS_METHODS.projectsReadFile]: "workspace",
163 [WS_METHODS.projectsSearchContents]: "workspace",
164 [WS_METHODS.projectsSearchEntries]: "workspace",
165 [WS_METHODS.projectsWriteFile]: "workspace",
166 [WS_METHODS.projectsEnsureScratch]: "orchestration",
167 [WS_METHODS.projectsCreateNew]: "orchestration",
168 [WS_METHODS.shellOpenInEditor]: "workspace",
169 [WS_METHODS.filesystemBrowse]: "workspace",
170 [WS_METHODS.filesystemGetMetadata]: "workspace",
171 [WS_METHODS.agentSessionsScan]: "workspace",
172 [WS_METHODS.agentSessionsImport]: "workspace",
173 [WS_METHODS.assetsCreateUrl]: "workspace",
174 [WS_METHODS.assetsPersistChatAttachments]: "orchestration",
175 [WS_METHODS.attachmentsCreateUploadUrl]: "workspace",
176 [WS_METHODS.attachmentsDelete]: "workspace",
177 [WS_METHODS.providerUploadFeedback]: "provider",
178 [WS_METHODS.mcpAppsCallTool]: "provider",
179 [WS_METHODS.mcpAppsToolInfo]: "provider",
180 [WS_METHODS.mcpAppsUpdateModelContext]: "provider",
181 [WS_METHODS.mcpAppsReadResource]: "provider",
182 [WS_METHODS.subscribeVcsStatus]: "vcs",
183 [WS_METHODS.subscribeWorktreeSetup]: "vcs",
184 [WS_METHODS.worktreeSetupCancel]: "vcs",
185 [WS_METHODS.subscribeResourceTelemetry]: "server",
186 [WS_METHODS.vcsRefreshStatus]: "vcs",
187 [WS_METHODS.vcsPull]: "git",
188 [WS_METHODS.gitRunStackedAction]: "vcs",
189 [WS_METHODS.gitResolvePullRequest]: "git",
190 [WS_METHODS.gitPreparePullRequestThread]: "git",
191 [WS_METHODS.vcsListRefs]: "vcs",
192 [WS_METHODS.vcsCreateWorktree]: "vcs",
193 [WS_METHODS.vcsRemoveWorktree]: "vcs",
194 [WS_METHODS.vcsCreateRef]: "vcs",
195 [WS_METHODS.vcsSwitchRef]: "vcs",
196 [WS_METHODS.vcsInit]: "vcs",
197 [WS_METHODS.reviewGetDiffPreview]: "review",
198 [WS_METHODS.reviewGetDiffFileContents]: "review",
199 [WS_METHODS.terminalOpen]: "terminal",
200 [WS_METHODS.terminalAttach]: "terminal",
201 [WS_METHODS.terminalObserve]: "terminal",
202 [WS_METHODS.terminalWrite]: "terminal",
203 [WS_METHODS.terminalResize]: "terminal",
204 [WS_METHODS.terminalClear]: "terminal",
205 [WS_METHODS.terminalRestart]: "terminal",
206 [WS_METHODS.terminalClose]: "terminal",
207 [WS_METHODS.subscribeTerminalEvents]: "terminal",
208 [WS_METHODS.subscribeTerminalMetadata]: "terminal",
209 [WS_METHODS.subscribeScriptProgress]: "orchestration",
210 [WS_METHODS.previewOpen]: "preview",
211 [WS_METHODS.previewNavigate]: "preview",
212 [WS_METHODS.previewResize]: "preview",
213 [WS_METHODS.previewAdjust]: "preview",
214 [WS_METHODS.previewRefresh]: "preview",
215 [WS_METHODS.previewClose]: "preview",
216 [WS_METHODS.previewList]: "preview",
217 [WS_METHODS.previewClearProfile]: "preview",
218 [WS_METHODS.previewReportProfiles]: "preview",
219 [WS_METHODS.previewReportStatus]: "preview",
220 [WS_METHODS.subscribePreviewEvents]: "preview",
221 [WS_METHODS.subscribeDiscoveredLocalServers]: "preview",
222 [WS_METHODS.deviceConfigure]: "device",
223 [WS_METHODS.deviceTestHost]: "device",
224 [WS_METHODS.deviceList]: "device",
225 [WS_METHODS.deviceOpen]: "device",
226 [WS_METHODS.deviceClose]: "device",
227 [WS_METHODS.deviceShutdown]: "device",
228 [WS_METHODS.deviceDetail]: "device",
229 [WS_METHODS.deviceAction]: "device",
230 [WS_METHODS.subscribeDeviceState]: "device",
231 [WS_METHODS.subscribeServerConfig]: "server",
232 [WS_METHODS.subscribeServerLifecycle]: "server",
233 [WS_METHODS.subscribeAuthAccess]: "auth",
234 [WS_METHODS.subscribeBackgroundPolicy]: "server",
235} as const satisfies Readonly<Record<WsRpcMethod, string>>;
236
237const RPC_SPAN_PREFIX = "ws.rpc";
238const DEFAULT_RPC_SPAN_ATTRIBUTES = {
239 "rpc.transport": "websocket",
240 "rpc.system": "effect-rpc",
241} as const;
242const RPC_METHODS_WITH_TRACING_DISABLED: ReadonlySet<string> = new Set([
243 WS_METHODS.serverGetTraceDiagnostics,
244 WS_METHODS.serverGetProcessDiagnostics,
245 WS_METHODS.serverGetProcessResourceHistory,
246 WS_METHODS.serverSignalProcess,
247]);
248
249/**
250 * Records each WebSocket RPC's span and request metrics. `ws.ts` adds it to the server's group
251 * after `RpcScopeAuthorization`, so it wraps authorization and also records rejected calls.
252 */
253export class RpcInstrumentation extends RpcMiddleware.Service<RpcInstrumentation>()(
254 "t3/server/RpcInstrumentation",
255) {}
256
257/**
258 * Wraps each WebSocket RPC call in its `ws.rpc.<method>` span and records its request counter and
259 * duration. For a stream RPC, the middleware receives the whole stream run, so the span and the
260 * metrics cover the subscription until it ends, fails, or is interrupted. Methods in
261 * `RPC_METHODS_WITH_TRACING_DISABLED` record metrics but no spans, for the call or anything it runs.
262 */
263export const rpcInstrumentationLayer = Layer.succeed(RpcInstrumentation)((effect, { rpc }) => {
264 const method = rpc._tag;
265 const measured = effect.pipe(
266 withMetrics({ counter: rpcRequestsTotal, timer: rpcRequestDuration, attributes: { method } }),
267 );
268
269 if (RPC_METHODS_WITH_TRACING_DISABLED.has(method)) {
270 return measured.pipe(Effect.provideService(References.TracerEnabled, false));
271 }
272 return measured.pipe(
273 Effect.withSpan(`${RPC_SPAN_PREFIX}.${method}`, {
274 attributes: {
275 ...DEFAULT_RPC_SPAN_ATTRIBUTES,
276 "rpc.method": method,
277 "rpc.aggregate": RPC_AGGREGATES[method as WsRpcMethod],
278 },
279 }),
280 );
281});