Split platform-induced API calls out of the control telemetry category - #322
Split platform-induced API calls out of the control telemetry category#322archandatta wants to merge 1 commit into
Conversation
205f61a to
f22b32a
Compare
Every documented operation emitted api_call under `control`, so the platform's own traffic — recorder polling, profile save/restore, telemetry publishing — buried the agent's browser actions. Operations now carry `x-telemetry-category` in openapi.yaml and categorygen emits the operation -> category map alongside the event-type map, so a new endpoint without a classification fails generation. Browser-control calls keep emitting api_call under `control`; VM-management calls emit platform_api_call under a new `platform` category, which is opt-in and out of the default set. api_call data also gains an optional `code` field, recorded for executePlaywrightCode and clipped to 8 KB on a rune boundary, so the event says what actually ran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f22b32a to
5659a6a
Compare
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 5659a6a. Configure here.
| {events.Page, b.Page}, | ||
| {events.Interaction, b.Interaction}, | ||
| {events.Control, b.Control}, | ||
| {events.Platform, b.Platform}, |
There was a problem hiding this comment.
Platform-only config disables middleware
High Severity
The TelemetryHTTPMiddleware is currently only enabled when the control telemetry category is active. Since platform_api_call events, now part of the platform category, rely on this middleware, enabling platform without control means these events won't be collected or published.
Reviewed by Cursor Bugbot for commit 5659a6a. Configure here.
Sayan-
left a comment
There was a problem hiding this comment.
Reviewed at 5659a6a. Verified locally: go vet ./... clean, unit suite green apart from the known real-Chromium flake in devtoolsproxy, and go generate ./lib/events/... reproduces category_gen.go byte for byte as claimed.
One functional gap, one policy question, then small stuff.
1. A platform-only config emits nothing
reconcileTelemetryState (cmd/api/api/telemetry.go:126) still enables the api_call middleware only when control is captured, but that middleware is now also the only producer of platform_api_call. With browser.platform.enabled=true and control off, the session starts and the category filter admits platform, but both middleware layers short-circuit on the process toggle, so nothing is published. Reproduced against this branch:
platform only -> TelemetryMiddlewareEnabled() = false
control + platform -> TelemetryMiddlewareEnabled() = true
That is precisely the migration this PR asks platform readers to make, so it seems worth fixing here rather than in the control-plane follow-up. Gating on control-or-platform, plus the doc comment above the function, covers it. The existing assertions at telemetry_test.go:215-231 only exercise control, which is why this isn't caught today; a platform-only case there would pin it.
2. code puts free-form caller content into a category that is captured by default
api_call was metadata only (operation_id, status, duration), and control is in DefaultCategories, so it is what a caller gets when they enable telemetry without naming categories. Submitted Playwright source is not metadata: page.fill('#password', ...), a reset token in a goto URL, PII in typed values. It lands in the S2 stream and in any OTLP destination bound to the session. The content-bearing categories (console, network, screenshot) are opt-in for exactly this reason, and the stacked CDP PR is careful in this same category to record a text length instead of the text and to drop URLs entirely, which is the opposite policy for the same class of data.
Not arguing against capturing it, the gap it closes is real and it is the most useful field in the event. But it reads like something that should carry its own opt-in rather than ride a default-on category, or get the same treatment the content-bearing categories get downstream. Worth an explicit decision from whoever owns that gating before it ships on by default.
3. Smaller
- lib/events/capture.go: "three orders of magnitude below
maxS2RecordBytes" is off. 8 KiB against 1,000,000 is about 122x, so two orders. - lib/cdpmonitor/util.go:
contextCap = CapturedFieldCap / 2ties the 4 KiB non-structured body cap to the captured-field cap. Those are unrelated numbers, and the standalone constant used to say so; deriving one as half the other reads as if they must move together. codelives on the sharedBrowserApiCallEventData, whichBrowserPlatformApiCallEventalso references, so the schema permitscodeon aplatform_api_call. Only the description rules it out.- The generator's new failure modes are only reachable through
make oapi-generate; nothing in CI checks that generated files are current. In practice a new route can't be added without regeneratingoapi.go, and that target runsgo generate ./lib/events/..., so the realistic path is covered. Worth knowing the guardrail isn't enforced.
The classification list reads right to me, processExec as platform and clipboard as control included, and defaulting an unknown operation to platform is the right direction for the failure case.


Summary
openapi.yamldeclaresx-telemetry-category;categorygenemits the operation → category map next to the event-type map, so a new endpoint without a classification fails generation instead of landing somewhere silentlyexecutePlaywrightCode, screenshot, clipboard) keep emittingapi_callundercontrol; VM-management operations emit a newplatform_api_callunder a newplatformcategoryplatformis opt-in: inUserCategories, notDefaultCategories, so enabling telemetry without per-category settings now captures strictly less than beforeBrowserApiCallEventDatagainscode, recorded forexecutePlaywrightCodeand capped by the same helper and constant as every other captured stringoperation_iddescription corrected — the value on the wire is the generated handler name (ProcessExec), andcategorygenrejects any operationId that isn't lowerCamelCase, since that assumption is what makes the mapping safeWhy
api_callfired for every documented operation and always landed incontrol. Most of that traffic is the platform acting on the VM, not an agent acting on the browser:listRecorderspolling, profile save and restore (downloadDirZstd,deleteDirectory,processExec), replay lifecycle, telemetry publishing itself. Readingcontrolto see what an agent did meant paging past all of it, and the one event worth reading —executePlaywrightCode— didn't include the code that ran.Classification calls worth a second look
takeScreenshotandreadClipboard/writeClipboardarecontrol: an agent uses them to see and to move data.patchDisplay,chromiumConfigure,patchChromiumFlagsanduploadExtensionsAndRestartareplatform— browser configuration issued at session setup.processExecisplatformeven though a customer can call it directly, because it isn't browser control. An operation the generated map doesn't know falls back toplatform, so an unclassified route can't dilute the stream callers read to see agent behavior.Sequencing
controlnarrows here with no opt-in path back until the control-plane half lands (kernel#3086), so anyone readingcontrolfor profile-save or replay calls needsplatformafter this release. CDP-level control events are #323, stacked on this branch and shipping in the same release. Docs are kernel/docs#474.Captured-string cap
codereuses what already existed rather than adding a second convention.truncateBodyand the 8 KB structured-body cap moved out oflib/cdpmonitorintolib/eventsasTruncateCapturedandCapturedFieldCap, so response bodies and submitted source now share one helper, one constant and one marker (...[truncated]), and the constant carries the reason for its value: three orders of magnitude belowmaxS2RecordBytes, so no single field can push an envelope past the record limit and null the whole payload. A clipped value is marked in the string, sotruncatedon the envelope keeps meaning only whattruncateIfNeededsets it to. The helper had no tests before; it has them now.Generated handler names
categorygenno longer derives the handler name from the operationId by uppercasing the first letter, which assumed how oapi-codegen spells a name and needed a lowerCamelCase guard to be safe. It now reads the generatedServerInterface, whose methods each document the route they serve, and joins that to the spec on method and path. Nothing about naming is assumed, and generation fails if a classified route has no handler, if a handler has no classification, or if the two counts disagree. The regenerated map is byte-identical to the derived one.Testing
make test-unit—go vet ./...clean, unit suite greentest-server-unitgreen on this commit in CITruncateCaptured(cap, rune boundary, marker, sub-marker caps), for the operation → category map, and for thecodecap end to end through the middlewarecategorygenfailure modes exercised by hand: a spec route with no generated handler, an operation with nox-telemetry-category, and a missing handlers file each exit non-zero with the route namedNote
Medium Risk
Changes telemetry event types and what lands in control by default; consumers and OTLP pipelines that assumed all API calls were control need to opt into platform and update filters.
Overview
Splits VM-management HTTP traffic out of the
controltelemetry stream soapi_callreflects browser-driving operations only, while a newplatform_api_call/platformcategory covers recording, filesystem, process, and config endpoints.Each OpenAPI operation now declares
x-telemetry-category;categorygenbuilds an operation → category map from the generatedServerInterface(not derived fromoperationId), and unclassified routes default to platform so they cannot pollute control.platformis a new opt-in telemetry category (not in default capture).executePlaywrightCoderecords submitted source on the event viaRecordTelemetryCode, using sharedevents.TruncateCaptured/CapturedFieldCap(also used for CDP response bodies).Reviewed by Cursor Bugbot for commit 5659a6a. Bugbot is set up for automated code reviews on this repo. Configure here.