Skip to content

feat(vercel): Vercel and AI Gateway integration - #1583

Open
JeremyFunk wants to merge 17 commits into
mainfrom
feat/vercel-integration
Open

JeremyFunk wants to merge 17 commits into
mainfrom
feat/vercel-integration

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Vercel and Vercel AI Gateway as an infrastructure integration.

An admin pastes a Vercel access token on Integrations → Vercel, picks a team, projects, a sampling rate, and logs and/or traces. Maple then creates two drains on that team:

  • Maple logs: NDJSON → new ingest route POST /v1/logpush/vercel (Bearer org ingest key, same auth, billing and limits as /v1/logs), mapped to OTel logs with vercel.*, http.* and url.* attributes
  • Maple traces: OTLP → existing /v1/traces, which also carries AI Gateway traces (service.name=ai-gateway)

What's in it

  • DB: vercel_connections, vercel_projects, vercel_deployments (one migration); the token is AES-GCM encrypted
  • API: internal /internal/integrations/vercel (status, discover, connect, settings, sync, disconnect, deployments)
  • Poller: new alerting lane vercel on cron 2-59/5 * * * *
    • Hourly project discovery and deployments every tick
    • Each tick it reconciles Maple's drains on the team, which it finds by a ?maple_org= marker on the endpoint. It deletes duplicates, patches drift and ingest-key rotations, and recreates missing drains, so concurrent connects or interrupted runs settle within one tick
    • A drain the team disabled is only turned back on by a settings save
  • Ingest (Rust):
    • vercel_logs.rs mapper: JSON array or NDJSON, gzip; bad lines are skipped
    • Agent Sessions vendor vercel_ai_gateway: cost from vercel.ai_gateway.cost.total, only server spans count, one session per request
  • Web:
    • Integrations card: connect dialog, quick metrics, drains panel, projects with production deploys
    • Infrastructure → Vercel: projects table, AI Gateway view (spend, tokens, by model and project), project page with requests, status, cache, top paths, top errors, deployments and AI Gateway use
    • Lab pages /lab/vercel and /lab/infra
  • Also:
    • "Vercel" dashboard template
    • integrations/vercel.md docs
    • Integration inventory and setup audit CFG-INTEG-07
    • MCP: list_infra/inspect_infra kinds and sync_vercel
    • vercel service-map platform

Out of V1: Observability Plus metrics API, a Vercel OAuth/Marketplace app, the AI Gateway reporting API.

Verification

  • tsrs --noEmit passes in web, api, ai, alerting, backend, domain, db, query-engine-integrations, ui and local-ui
  • Vitest, scoped: backend 183, domain 157, db parity 5, query-engine-integrations 459, alerting 17, ai/mcp 761, web 950, ui 226 (Vercel service suite now 21)
  • Rust: cargo test passes for the vercel and ai_session suites (112) and the route test (NDJSON, gzip, [] → 200, malformed → 400, no key → 401)
  • ClickHouse analyzer sweep (CLICKHOUSE_E2E=1 catalog e2e) passes 482/482, including all 11 new Vercel queries
  • Four review passes (correctness ×2, Effect conventions, web/queries); every finding fixed
  • Not verified yet: real Vercel drains and real payloads. That needs a Pro team and runs on the PR preview next

Screenshots (lab fixtures)

Not connected Connect flow
Connected Drain deleted on Vercel
Token rejected Infra: projects
Infra: AI Gateway Infra: project
Traces only Read failures
Mobile

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Summary by CodeRabbit

  • New Features
    • Added Vercel integration setup and configuration for teams, projects, log and trace collection, and sampling.
    • Added Vercel infrastructure views with project requests, errors, cache performance, deployments, and AI Gateway usage, including charts and project-level details.
    • Added Vercel data to integration health, service maps, dashboards, and AI session displays.
    • Added Vercel log collection and support for AI Gateway traces and usage.
    • Added admin sync controls and scheduled integration polling.
  • Documentation
    • Added a Vercel integration guide covering setup, collected data, and troubleshooting.

@JeremyFunk JeremyFunk added the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 19, 2026
@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🔴 Confidence 1/10 · do not merge
Five confirmed defects around the new ingest route, drain reconciliation and Vercel connection; lab fixtures, ingest route body and web tests went unread.
7 warnings −7 · tests covered · risk high −1 · 1/3 new units observable −1 · 32 files not read

Adds a Vercel/AI Gateway integration end to end: encrypted token store, drain reconciliation in a new alerting lane, a /v1/logpush/vercel ingest route, warehouse queries and UI. Mergeable once the drain-prune and ingest-attribute defects below are fixed. None of the open WarehouseUpstreamError/PlanetScaleForbiddenError issues in apps/alerting/src/scheduled.ts are addressed by this diff. Not read: the lab fixtures under apps/web/src/lab/, the ingest route body in main.rs, the web test files and the migration snapshot.

  • VercelIntegrationService reconciles Maple's drains on the team every 5 minutes
  • POST /v1/logpush/vercel maps Vercel drain NDJSON to OTel logs
  • 11 new ClickHouse queries behind /vercel-infra-* and /vercel-ai-gateway
  • vercel_connections stores the AES-GCM token plus drain state

Before merge

Findings

🟠 Warning · F1 · Drain logs carry maple.ingest.source, a second spelling of maple_ingest_source

convention · apps/ingest/src/vercel_logs.rs:154

decode_vercel_logs calls enrich_logs_request, whose enrich_resource_attributes (apps/ingest/src/main.rs:5782) upserts maple_ingest_source = "maple-ingest-gateway" on the same resource, so every drain log carries both keys and the attribute every other connector uses (gcp_logging.rs:172, main.rs:5375) reports the drain as plain gateway OTLP. The rule is one spelling per key (README of rules/span-attributes.md): set maple_ingest_source through the same upsert_string_attribute helper and drop the dotted key, updating the test at line 385.

        attribute("maple_ingest_source", INGEST_SOURCE),
🟠 Warning · F2 · Pruning skips when every selected project was deleted, leaving dead ids in the drain

correctness · packages/backend/src/services/integrations/VercelIntegrationService.ts:1141

prune requires kept.length > 0, so with projectScope: "some" and the org's whole selection deleted upstream, kept is empty, nothing is written, and forwarding.projectIds keeps the deleted ids. Vercel drops deleted projects from the drain's project list, so the drain reads as drifted every tick and is PATCHed with dead ids — the "drifted forever" state the comment on line 1137 exists to prevent. Drop the kept.length > 0 guard so the saved selection converges to empty.

						const prune = projects.success.complete && kept.length < claimed.projectIds.length
🟠 Warning · F3 · A failed Vercel inventory read in vercelInventoryDetails is swallowed silently

observability · STAT-01 · apps/ai/src/mcp/tools/inspect-infra.ts:80

Effect.orElseSucceed(() => ({})) turns a Postgres or service failure into an empty details map with no log and no span event, so inspect_infra kind=vercel_project silently loses Framework, Forwarded and both deployment fields and the model cannot tell "no deployment" from "read failed". Record the failure before falling back, as list-infra.ts does for connectedProviderKinds.

Pipe `Effect.tapError((error) => Effect.logWarning("inspect_infra could not read the Vercel inventory", error.message))` before the `Effect.orElseSucceed`.
🟠 Warning · F4 · A failed Vercel projects read renders as permanent loading, not as an error

correctness · apps/web/src/components/integrations/vercel-integration-card.tsx:274

VercelConnectedView maps the "failed" state of vercelInfraProjects to byProject = null, so VercelProjects renders a <Skeleton> in the Requests and 5xx cells of every row forever and VercelQuickMetrics returns null — a failed read is indistinguishable from a slow one. The AI Gateway stat has the same collapse at line 119, where a failed vercelAiGateway read prints "No AI Gateway requests yet". Carry the failure through and render an explicit error/retry affordance, as statusResult failure already does.

Keep the `"failed"` state on `VercelUsage`, and in the projects table and the stat rail render an error with a retry instead of the skeleton and the empty-state sentence.
🟠 Warning · F5 · proxy.path lands on url.path with its query string

convention · apps/ingest/src/vercel_logs.rs:38

Vercel's proxy.path includes the query (this PR's own test asserts url.path == "/api/users?page=1"), but semconv's url.path excludes it, and route extraction from url.path turns every distinct query string into its own route — the values also feed vercelProjectTopPathsSQL (packages/query-engine-integrations/src/vercel/vercel-infra.ts:93), so /search?q=... splits into one path per search. Split the value into url.path and url.query.

Set `url.path` to the part before `?` and put the remainder on `url.query`.
🟠 Warning · F6 · Resource grouping scans every existing group per entry, quadratic on a large drain batch

performance · apps/ingest/src/vercel_logs.rs:117

groups.iter_mut().find(|(attributes, _)| *attributes == resource) runs once per entry, and deploymentId/environment/branch are resource attributes, so a delivery with one distinct resource per entry — up to hundreds of thousands of small entries in a 20 MB body — does billions of Vec<KeyValue> comparisons synchronously inside the async handler, pinning a tokio worker for the whole request. Accumulate into a hash map keyed by the resource attributes and build the ResourceLogs from its values.

🟠 Warning · F7 · A failed settings save is reported underneath the confirm dialog, so the admin sees nothing

correctness · apps/web/src/components/integrations/vercel-configure.tsx:622

apply sets error and returns false, which keeps ConfirmDialog open on top of the DialogPanel that renders the <Alert> with errorMessage(...). Turning logs off or lowering sampling and getting a failed PATCH leaves the admin with a dialog that will not close and no visible reason, and the drains are not updated. Surface the failure inside the dialog before resolving, or close it so the alert is readable.

🤖 Prompt to fix all 7 findings with an AI agent
Findings from an automated review of commit dd8bf24a7696b9538b152a30f2f34c218674eb30. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · convention · apps/ingest/src/vercel_logs.rs:154
Drain logs carry `maple.ingest.source`, a second spelling of `maple_ingest_source`
`decode_vercel_logs` calls `enrich_logs_request`, whose `enrich_resource_attributes` (`apps/ingest/src/main.rs:5782`) upserts `maple_ingest_source = "maple-ingest-gateway"` on the same resource, so every drain log carries both keys and the attribute every other connector uses (`gcp_logging.rs:172`, `main.rs:5375`) reports the drain as plain gateway OTLP. The rule is one spelling per key (README of `rules/span-attributes.md`): set `maple_ingest_source` through the same `upsert_string_attribute` helper and drop the dotted key, updating the test at line 385.
Replace those lines with:
        attribute("maple_ingest_source", INGEST_SOURCE),

---

F2 · Warning · correctness · packages/backend/src/services/integrations/VercelIntegrationService.ts:1141
Pruning skips when every selected project was deleted, leaving dead ids in the drain
`prune` requires `kept.length > 0`, so with `projectScope: "some"` and the org's whole selection deleted upstream, `kept` is empty, nothing is written, and `forwarding.projectIds` keeps the deleted ids. Vercel drops deleted projects from the drain's project list, so the drain reads as drifted every tick and is PATCHed with dead ids — the "drifted forever" state the comment on line 1137 exists to prevent. Drop the `kept.length > 0` guard so the saved selection converges to empty.
Replace those lines with:
						const prune = projects.success.complete && kept.length < claimed.projectIds.length

---

F3 · Warning · observability · STAT-01 · apps/ai/src/mcp/tools/inspect-infra.ts:80
A failed Vercel inventory read in `vercelInventoryDetails` is swallowed silently
`Effect.orElseSucceed(() => ({}))` turns a Postgres or service failure into an empty details map with no log and no span event, so `inspect_infra kind=vercel_project` silently loses Framework, Forwarded and both deployment fields and the model cannot tell "no deployment" from "read failed". Record the failure before falling back, as `list-infra.ts` does for `connectedProviderKinds`.
Suggested fix: Pipe `Effect.tapError((error) => Effect.logWarning("inspect_infra could not read the Vercel inventory", error.message))` before the `Effect.orElseSucceed`.

---

F4 · Warning · correctness · apps/web/src/components/integrations/vercel-integration-card.tsx:274
A failed Vercel projects read renders as permanent loading, not as an error
`VercelConnectedView` maps the `"failed"` state of `vercelInfraProjects` to `byProject = null`, so `VercelProjects` renders a `<Skeleton>` in the Requests and 5xx cells of every row forever and `VercelQuickMetrics` returns `null` — a failed read is indistinguishable from a slow one. The AI Gateway stat has the same collapse at line 119, where a failed `vercelAiGateway` read prints "No AI Gateway requests yet". Carry the failure through and render an explicit error/retry affordance, as `statusResult` failure already does.
Suggested fix: Keep the `"failed"` state on `VercelUsage`, and in the projects table and the stat rail render an error with a retry instead of the skeleton and the empty-state sentence.

---

F5 · Warning · convention · apps/ingest/src/vercel_logs.rs:38
`proxy.path` lands on `url.path` with its query string
Vercel's `proxy.path` includes the query (this PR's own test asserts `url.path == "/api/users?page=1"`), but semconv's `url.path` excludes it, and route extraction from `url.path` turns every distinct query string into its own route — the values also feed `vercelProjectTopPathsSQL` (`packages/query-engine-integrations/src/vercel/vercel-infra.ts:93`), so `/search?q=...` splits into one path per search. Split the value into `url.path` and `url.query`.
Suggested fix: Set `url.path` to the part before `?` and put the remainder on `url.query`.

---

F6 · Warning · performance · apps/ingest/src/vercel_logs.rs:117
Resource grouping scans every existing group per entry, quadratic on a large drain batch
`groups.iter_mut().find(|(attributes, _)| *attributes == resource)` runs once per entry, and `deploymentId`/`environment`/`branch` are resource attributes, so a delivery with one distinct resource per entry — up to hundreds of thousands of small entries in a 20 MB body — does billions of `Vec<KeyValue>` comparisons synchronously inside the async handler, pinning a tokio worker for the whole request. Accumulate into a hash map keyed by the resource attributes and build the `ResourceLogs` from its values.

---

F7 · Warning · correctness · apps/web/src/components/integrations/vercel-configure.tsx:622
A failed settings save is reported underneath the confirm dialog, so the admin sees nothing
`apply` sets `error` and returns `false`, which keeps `ConfirmDialog` open on top of the `DialogPanel` that renders the `<Alert>` with `errorMessage(...)`. Turning logs off or lowering sampling and getting a failed PATCH leaves the admin with a dialog that will not close and no visible reason, and the drains are not updated. Surface the failure inside the dialog before resolving, or close it so the alert is readable.

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 955 GET 819/day, 2.9% err
GET 136/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 955 GET 819/day, 2.9% err
GET 136/day, 0.1% err
packages/domain/src/http/integrations.ts 277 GET /v2/integrations/gcp 277/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 10 gcpInfraPresence 10/day, 0.0% err
apps/api/src/routes/queries.ts 10 gcpInfraPresence 10/day, 0.0% err
packages/domain/src/http/query-engine.ts 10 gcpInfraPresence 10/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:443
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:455
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:456
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:528
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:543
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:564
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:588
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:595
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:645
  • ➕ span name VercelIntegrationService.getStatus · packages/backend/src/services/integrations/VercelIntegrationService.ts:720
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:821
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:900
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:905
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1010
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1041
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1057
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1074
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1212
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1213
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1214
  • ➕ span name VercelIntegrationService.pollAllOrgs · packages/backend/src/services/integrations/VercelIntegrationService.ts:1242
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1297
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
What was checked
  • Every new Vercel query filters OrgId (vercel-infra.ts:31, vercel-ai-gateway.ts:34)
  • New query payloads use TinybirdDateTime/BucketSeconds at the HTTP boundary (http/query-engine.ts:501-536)
  • Token never appears in a status response schema; VercelIntegrationStatus carries only team, scope and drain state
Observability coverage: 1 of 3 changes observable
Change Kind Observable Evidence
POST /v1/logpush/vercel (log drain ingest) HTTP route no not read in this pass; only grepped for the ingest-source attribute
vercel lane in the alerting cron (drain reconcile + discovery) cron yes reconcile annotates the span with maple.vercel.team_id (VercelIntegrationService.ts:1090)
Vercel REST calls (projects, deployments, drains) outbound HTTP no vercel/api.ts not read in this pass
Files not reviewed (32)

The review ended before it read these diffs, so nothing above vouches for them.

  • apps/ai/src/mcp/__evals__/provider-world.ts
  • apps/alerting/src/scheduled.test.ts
  • apps/local-ui/src/hooks/use-local-service-map.test.ts
  • apps/web/src/components/dashboard/nav-items.test.ts
  • apps/web/src/components/infra/overview/summaries.test.ts
  • apps/web/src/components/infra/vercel/list-model.test.ts
  • apps/web/src/components/integrations/vercel-model.test.ts
  • apps/web/src/lab/bench/service-map-bench.tsx
  • apps/web/src/lab/infra-lab.tsx
  • apps/web/src/lab/registry.ts
  • apps/web/src/lab/vercel-infra-fixtures.ts
  • apps/web/src/lab/vercel-integration-lab.tsx
  • apps/web/src/lib/agent-sessions/vendor-color.test.ts
  • apps/web/src/lib/agent-sessions/vendor-icon.ts
  • apps/web/src/lib/agent-sessions/vendor-label.ts
  • apps/web/src/lib/docs.ts
  • apps/web/src/routes/lab/vercel.tsx
  • packages/backend/src/dashboard-templates/index.ts
  • packages/backend/src/dashboard-templates/infrastructure/vercel.test.ts
  • packages/backend/src/dashboard-templates/infrastructure/vercel.ts
  • packages/backend/src/services/integrations/IntegrationInventoryService.test.ts
  • packages/backend/src/services/integrations/VercelIntegrationService.test.ts
  • packages/domain/src/gen-ai.ts
  • packages/query-engine-integrations/src/vercel/vercel.test.ts
  • packages/ui/src/components/icons/index.ts
  • packages/ui/src/components/icons/vercel.tsx
  • packages/ui/src/components/service-map/service-map-canvas.tsx
  • packages/ui/src/components/service-map/service-map-node.tsx
  • packages/ui/src/components/service-map/service-map-types.ts
  • packages/ui/src/components/service-map/service-map-utils.ts
  • and 2 more

dd8bf24 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one. Check ids refer to Maple's instrumentation audit.

maple-review-bot[bot]

This comment was marked as resolved.

@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

🍁 Maple PR preview

The preview is ready:

Service Preview URL
Web app-pr-1583.maple.dev
API api-pr-1583.maple.dev
Electric Sync sync-pr-1583.maple.dev
Ingest (OTLP) ingest-pr-1583.maple.dev

Smoke test: passed

Sign in with the PR preview form on the sign-in page and the preview access key (PREVIEW_SIGNIN_KEY in Infisical), or run bun run preview:signin 1583 for a signed-in link.

Deployed from 9d247e5 · View workflow run

@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🟡 Confidence 5/10 · needs attention
Kept at 5: ordinary warnings and soft signals never read below 5.
7 warnings −7 · tests partial −1 · risk high −1 · floor 5

The only change since the previous review raises the web startup JS budget from 707.5 to 710 KB; the seven open findings are untouched. The alerting warehouse, persistence and PlanetScale errors in apps/alerting/src/scheduled.ts are not addressed by this pull request.

Before merge

Still open from earlier reviews

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 960 GET 824/day, 2.8% err
GET 136/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 960 GET 824/day, 2.8% err
GET 136/day, 0.1% err
packages/domain/src/http/integrations.ts 277 GET /v2/integrations/gcp 277/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 10 gcpInfraPresence 10/day, 0.0% err
apps/api/src/routes/queries.ts 10 gcpInfraPresence 10/day, 0.0% err
packages/domain/src/http/query-engine.ts 10 gcpInfraPresence 10/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:443
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:455
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:456
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:528
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:543
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:564
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:588
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:595
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:645
  • ➕ span name VercelIntegrationService.getStatus · packages/backend/src/services/integrations/VercelIntegrationService.ts:720
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:821
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:900
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:905
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1010
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1041
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1057
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1074
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1212
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1213
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1214
  • ➕ span name VercelIntegrationService.pollAllOrgs · packages/backend/src/services/integrations/VercelIntegrationService.ts:1242
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1297
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
What was checked
  • maxGzipBytes has no second copy to keep in sync; only check-bundle-budget.ts:129 and its comparisons at 154/163 read it
  • All seven open findings re-read at head and still present: vercel_logs.rs:38,117,154, VercelIntegrationService.ts:1141, inspect-infra.ts:80, vercel-integration-card.tsx:274, `vercel-configure.…
  • The new comment's CI figure of 709.4 KB sits inside the 710 KB budget and follows the file's existing per-bump measurement convention

c5cdf2a · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 355e89d7-1aad-4fbf-ad3b-d93cc90013ae
📥 Commits

Reviewing files that changed from the base of the PR and between c5cdf2a and 890d5ff.

📒 Files selected for processing: 15
  • apps/ai/src/mcp/__evals__/provider-world.ts
  • apps/ai/src/mcp/tools/__tests__/provider-infra-tools.test.ts
  • apps/ai/src/mcp/tools/inspect-infra.ts
  • apps/ingest/src/main.rs
  • apps/ingest/src/vercel_logs.rs
  • apps/landing/src/content/docs/integrations/vercel.md
  • apps/web/src/components/infra/vercel/vercel-list-page.tsx
  • apps/web/src/components/integrations/vercel-configure.tsx
  • apps/web/src/components/integrations/vercel-integration-card.tsx
  • apps/web/src/lab/vercel-integration-lab.tsx
  • packages/backend/src/services/integrations/VercelIntegrationService.test.ts
  • packages/backend/src/services/integrations/VercelIntegrationService.ts
  • packages/domain/src/integration-inventory.ts
  • packages/ui/src/lib/__tests__/format.test.ts
  • packages/ui/src/lib/format.ts
🚧 Files skipped from review as they are similar to previous changes: 2
  • apps/landing/src/content/docs/integrations/vercel.md
  • apps/web/src/components/infra/vercel/vercel-list-page.tsx

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

📝 Walkthrough

This pull request adds Vercel integration management, log-drain ingestion, AI Gateway telemetry, infrastructure queries and views, MCP tools, scheduled polling, persistence, and supporting dashboard and documentation surfaces.

Changes: Vercel integration and telemetry

Layer / File(s) Summary
Connection, persistence, and polling
packages/db/..., packages/domain/..., packages/backend/src/services/integrations/..., apps/api/..., apps/alerting/...
Adds connection, project, and deployment storage; Vercel API access; drain reconciliation; deployment synchronization; HTTP handlers; scheduled polling; inventory status; and audit records.
Log-drain and AI Gateway ingestion
apps/ingest/...
Adds Vercel log parsing into OTLP logs and identifies, normalizes, and stamps Vercel AI Gateway request spans.
Analytics and MCP support
packages/query-engine-integrations/..., apps/api/..., apps/ai/..., apps/web/src/api/warehouse/...
Adds Vercel request, latency, path, error, and AI Gateway queries. MCP tools can list and inspect Vercel projects and run administrator-only synchronization.
Integration and infrastructure UI
apps/web/src/components/integrations/..., apps/web/src/components/infra/vercel/..., apps/web/src/routes/infra/vercel/...
Adds connection and settings flows, drain status, project metrics, deployments, AI Gateway views, project details, and retryable error states.
Dashboards, labs, and supporting surfaces
packages/backend/src/dashboard-templates/..., apps/web/src/lab/..., apps/landing/..., packages/ui/...
Adds a Vercel dashboard template, integration guide, fixture labs and routes, icons, platform labels, service-map support, and USD formatting updates.

Priority: ➖ Normal

Estimated code review effort: 5 (Critical) | ~120 minutes

Change: Feature

Suggested reviewers: makisuo

Merge Risk: ⚪ Minimal · up to 890d5

No actionable issue is established before merge. Live Vercel drain and payload behavior still needs the planned preview validation.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.74% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 136 functions across 56 files. (1 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely identifies the primary change: adding Vercel and Vercel AI Gateway integrations.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.74% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 136 functions across 56 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
⚔️ Resolve merge conflicts: 💡
  • Resolve merge conflict in branch feat/vercel-integration
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge
All seven earlier findings are fixed at this head and pinned by tests; only real Vercel deliveries stay unverified.
no findings · tests covered · risk medium · 3/3 new units observable

The follow-up commits fix every open finding: drain reconciliation by org marker, pruning when every project is deleted, indexed resource grouping, the url.path/url.query split, visible read and save failures, and maple_ingest_source. Nothing new is broken; safe to merge.

  • decode_vercel_logs upserts maple_ingest_source = vercel_log_drain over the gateway's value
  • build_logs_request groups by resource through a HashMap index instead of a linear scan
  • proxy.path splits into url.path and url.query
  • inspect_infra drills down Vercel logs by vercel.project.id, not the name

Before merge

Fixed since the last review

  • ✅ F1 · Drain logs carry maple.ingest.source, a second spelling of maple_ingest_source
  • ✅ F2 · Pruning skips when every selected project was deleted, leaving dead ids in the drain
  • ✅ F3 · A failed Vercel inventory read in vercelInventoryDetails is swallowed silently
  • ✅ F4 · A failed Vercel projects read renders as permanent loading, not as an error
  • ✅ F5 · proxy.path lands on url.path with its query string
  • ✅ F6 · Resource grouping scans every existing group per entry, quadratic on a large drain batch
  • ✅ F7 · A failed settings save is reported underneath the confirm dialog, so the admin sees nothing

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 966 GET 831/day, 2.9% err
GET 135/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 966 GET 831/day, 2.9% err
GET 135/day, 0.1% err
packages/domain/src/http/integrations.ts 310 GET /v2/integrations/gcp 310/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 10 gcpInfraPresence 10/day, 0.0% err
apps/api/src/routes/queries.ts 10 gcpInfraPresence 10/day, 0.0% err
packages/domain/src/http/query-engine.ts 10 gcpInfraPresence 10/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:443
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:455
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:456
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:531
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:546
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:567
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:591
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:598
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:648
  • ➕ span name VercelIntegrationService.getStatus · packages/backend/src/services/integrations/VercelIntegrationService.ts:723
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:824
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:903
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:908
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1013
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1044
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1060
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1077
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1214
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1215
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1216
  • ➕ span name VercelIntegrationService.pollAllOrgs · packages/backend/src/services/integrations/VercelIntegrationService.ts:1244
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1299
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
What was checked
  • Pruning now writes an empty selection (VercelIntegrationService.ts:1143) and skips the drain body, pinned by a test asserting zero PATCHes and two creates
  • ConfirmDialog closes when apply resolves (confirm-dialog.tsx:74), so a failed settings save is visible under the closed dialog
  • toLocaleString with maximumSignificantDigits: 2 prints $0.00000024, no exponent (ran in node)
Observability coverage: 3 of 3 changes observable
Change Kind Observable Evidence
POST /v1/logpush/vercel ingest route inbound route yes handle_signal opens the ingest span named POST /v1/logpush/vercel and records payload format and stage errors (apps/ingest/src/main.rs:4291-4305, 4642)
Vercel project inventory read inside inspect_infra db read yes VercelIntegrationService.getStatus is an Effect.fn span (VercelIntegrationService.ts:723); the failure is logged at inspect-infra.ts:80
vercel alerting poller lane cron worker yes lane on 2-59/5 * * * *; poll, reconcileDrains and reconcileDrain run as named Effect.fn spans (VercelIntegrationService.ts:443, 531, 1036)

890d5ff · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

# Conflicts:
#	apps/ai/src/mcp/resources/instructions.ts
#	apps/ingest/src/main.rs
#	apps/web/perf/check-bundle-budget.ts
@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
Backend, ingest, query-engine, API and MCP hunks read clean; the Vercel web UI, alerting lane, dashboard template and DB table files went unread, so this is a partial pass.
no findings · tests covered · risk high −1 · 2/2 new units observable

Adds Vercel and AI Gateway as a managed integration: encrypted token storage, a drain-reconciling poller lane, a /v1/logpush/vercel NDJSON ingest route, warehouse queries and UI. No defect in the hunks I read; several web, alerting and DB files went unread, so merge after those are reviewed.

  • VercelIntegrationService reconciles Maple's log and trace drains on a 5-minute alerting lane
  • POST /v1/logpush/vercel maps drained NDJSON to OTel logs
  • New vercel_* tables with one migration
  • sync_vercel MCP tool re-runs the poller on demand

Before merge

  • Migration · Check it is safe on production data and with the code still deployed · packages/db/drizzle/20261019200241_vercel_integration/migration.sql
  • Manual · Run the 20261019200241_vercel_integration migration through the prd alchemy deploy
  • Manual · Add the Vercel token encryption secret to the prd and dev secret stores
  • Manual · Confirm the new vercel alerting lane is in the deployed cron 2-59/5 * * * *

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 969 GET 833/day, 2.8% err
GET 136/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 969 GET 833/day, 2.8% err
GET 136/day, 0.1% err
packages/domain/src/http/integrations.ts 316 GET /v2/integrations/gcp 316/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 10 gcpInfraPresence 10/day, 0.0% err
apps/api/src/routes/queries.ts 10 gcpInfraPresence 10/day, 0.0% err
packages/domain/src/http/query-engine.ts 10 gcpInfraPresence 10/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:443
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:455
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:456
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:531
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:546
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:567
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:591
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:598
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:648
  • ➕ span name VercelIntegrationService.getStatus · packages/backend/src/services/integrations/VercelIntegrationService.ts:723
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:824
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:903
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:908
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1013
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1044
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1060
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1077
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1214
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1215
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1216
  • ➕ span name VercelIntegrationService.pollAllOrgs · packages/backend/src/services/integrations/VercelIntegrationService.ts:1244
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1299
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
What was checked
  • vercel_logs.rs hex-id and timestamp parsing rejects malformed and all-zero ids
  • New Vercel warehouse queries filter OrgId and stringify UInt64 identity columns
  • integration-sources.ts Vercel read is filtered by orgId
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
POST /v1/logpush/vercel HTTP route yes Rust route reuses the existing ingest request path in apps/ingest/src/main.rs
vercel lane (pollAllOrgs) cron yes makeTick in apps/alerting/src/scheduled.ts reports per-lane counts

d6699b2 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@JeremyFunk JeremyFunk removed the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 19, 2026
@JeremyFunk JeremyFunk added the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 19, 2026
@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
One confirmed gap: the new Vercel source is never stamped on its own telemetry, and no test covers that attribution.
1 warning −1 · tests partial −1 · risk medium

Reviewed the seven files changed since the last pass: the Vercel dashboard template, the Vercel source listing, and platform/docs copy. The template is sound; the new Vercel source row has no telemetry attribution, so its usage always reads zero. The open alerting errors (WarehouseUpstreamError, ErrorPersistenceError, PlanetScaleForbiddenError, PrReviewTelemetryReadError) are untouched by this change and are not addressed.

  • vercelTemplate adds logs and AI Gateway SQL widgets with macro-expanded, org-scoped queries
  • readIntegrationLinks now lists a Vercel connection as a pull source
  • vercel joins PULL_SOURCE_PLATFORMS, platformMeta and the source-health comment

Before merge

Findings

🟠 Warning · F8 · Vercel source is listed but its telemetry never carries maple_source_id

correctness · packages/backend/src/services/org/integration-sources.ts:198-203

Listing the Vercel connection as a source gives it a row and inventory-backed health, but the drains authenticate with the org's private ingest key (VercelIntegrationService.ts:366) and no poller ever calls ensureIntegrationSource/stampableSourceId for Vercel, so no Vercel record carries maple_source_id — a private key only keeps sender-supplied claims it can verify. The source's "Last 24h" card (apps/web/src/components/sources/source-overview.tsx:94, shown because vercel is not in METRICS_ONLY) reads sources usage, which filters ResourceAttributes['maple_source_id'] (packages/query-engine/src/ch/queries/source-usage.ts:4), so it always shows 0 spans · 0 logs while drain traffic is flowing, and the source id advertised by list_sources filters nothing for this platform.

Stamp the connection's source on Vercel traffic (e.g. a `src_` id in the drain endpoint/headers that the logpush and OTLP paths accept), or add `"vercel"` to `METRICS_ONLY` in `source-overview.tsx` until attribution exists so the usage card is not shown as zero.
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit 2ee28cd325aac779932712c3f3c17ee9459aac74. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F8 · Warning · correctness · packages/backend/src/services/org/integration-sources.ts:198-203
Vercel source is listed but its telemetry never carries `maple_source_id`
Listing the Vercel connection as a source gives it a row and inventory-backed health, but the drains authenticate with the org's private ingest key (`VercelIntegrationService.ts:366`) and no poller ever calls `ensureIntegrationSource`/`stampableSourceId` for Vercel, so no Vercel record carries `maple_source_id` — a private key only keeps sender-supplied claims it can verify. The source's "Last 24h" card (`apps/web/src/components/sources/source-overview.tsx:94`, shown because `vercel` is not in `METRICS_ONLY`) reads `sources` `usage`, which filters `ResourceAttributes['maple_source_id']` (`packages/query-engine/src/ch/queries/source-usage.ts:4`), so it always shows 0 spans · 0 logs while drain traffic is flowing, and the source id advertised by `list_sources` filters nothing for this platform.
Suggested fix: Stamp the connection's source on Vercel traffic (e.g. a `src_` id in the drain endpoint/headers that the logpush and OTLP paths accept), or add `"vercel"` to `METRICS_ONLY` in `source-overview.tsx` until attribution exists so the usage card is not shown as zero.

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 977 GET 841/day, 2.8% err
GET 136/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 977 GET 841/day, 2.8% err
GET 136/day, 0.1% err
packages/domain/src/http/integrations.ts 330 GET /v2/integrations/gcp 330/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 10 gcpInfraPresence 10/day, 0.0% err
apps/api/src/routes/queries.ts 10 gcpInfraPresence 10/day, 0.0% err
packages/domain/src/http/query-engine.ts 10 gcpInfraPresence 10/day, 0.0% err
packages/backend/src/services/integrations/VercelIntegrationService.ts 6 VercelIntegrationService.getStatus 6/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:443
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:455
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:456
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:531
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:546
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:567
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:591
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:598
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:648
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:824
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:903
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:908
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1013
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1044
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1060
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1077
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1214
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1215
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1216
  • ➕ span name VercelIntegrationService.pollAllOrgs · packages/backend/src/services/integrations/VercelIntegrationService.ts:1244
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1299
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
  • ➕ span name VercelApi.listDeployments · packages/backend/src/services/integrations/vercel/api.ts:253
What was checked
  • Every Vercel template SQL widget passes prepareRawSql: $__orgFilter, $__timeGroup, $__startTime/$__interval_s all expand (vercel.test.ts:25)
  • attr.http.request.method exists matches only request logs: the mapper omits the attribute on non-request entries (vercel_logs.rs:432)
  • The new vercel read filters orgId (integration-sources.ts:152) and teamName is non-null in packages/db/src/tables/vercel.ts:16

2ee28cd · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

maple-review-bot[bot]

This comment was marked as resolved.

@JeremyFunk

Copy link
Copy Markdown
Collaborator Author

Preview test (pr-1583, synthetic Vercel data)

A separate agent tested the preview with drain-format logs and AI Gateway/deployment traces sent straight to ingest-pr-1583. No real Vercel team was connected in this run (see "Not yet verified").

Ingest

Request Result
NDJSON, 49 entries 200
gzip array 200
[] / empty body 200
malformed 400
no key / bad key 401
1 good + 2 bad NDJSON lines 200, 1 row
all lines bad 400
OTLP traces 200

Log mapping

  • Resource: service.name, cloud.provider=vercel, vercel.project.id, vercel.deployment.id, deployment.environment.name, vcs.ref.head.name, maple_ingest_source=vercel_log_drain
  • Records: url.path without the query, url.query, method, status, vercel.cache, client and user agent
  • Severity follows the level; function logs link to their trace and span

AI Gateway

  • 3 requests show in Agent Sessions as "Vercel AI Gateway" with model, tokens and cost
  • Cost equals what was sent (not doubled); the failed request shows an error marker

Connect flow: a fake token gives an inline "Vercel rejected this token" error, no crash.

Dashboard template: the preview run found that its raw-SQL time series drew nothing for a single burst. Fixed in 2ee28cd (WITH FILL zero-fills empty buckets; the error-log table gets readable timestamps). Re-checked on the preview after the fix:

Connect, bad token Logs
Request log attributes AI Gateway root span
Agent Sessions Session detail

Traces (Maple self-telemetry, deployment.environment=pr-1583, 20:20–20:50 UTC)

Span n p50 p95 errors
VercelIntegrationService.getStatus (one SELECT) 35 21 ms 66 ms 0
alerting.vercel_tick → pollAllOrgs 3 55 ms 62 ms 0
VercelIntegrationService.discover → VercelApi.listTeams 1 308 ms – 1 (expected: fake token)
RawSql.execute (dashboard widgets) 23 58 ms 212 ms 0
  • No N+1 queries or waterfalls
  • The preview ingest gateway sends no traces of its own, so route timing for /v1/logpush/vercel comes from the Rust tests, not traces

Not yet verified

  • Real Vercel drains and real payloads (connect, drain creation, the poller against a Pro team, real AI Gateway and @vercel/otel traces). Pending a test token.

@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
The new drain route and source-claim header are well tested; one warning remains in updateSettings, whose reconcile runs without the disconnect check pollOrg uses.
1 warning −1 · tests covered · risk high −1 · 1/1 new units observable

Adds POST /v1/logpush/vercel, an x-maple-source-id header claim and a reworked Vercel drain reconciler. The ingest side is sound and tested; one gap: updateSettings can leave orphan drains on a concurrent disconnect. The open alerting errors (WarehouseUpstreamError, ErrorPersistenceError, PlanetScaleForbiddenError, PrReviewTelemetryReadError) are untouched by this diff.

  • Ingress enum routes drain bodies to decode_vercel_logs, always forwarded as protobuf
  • apply_header_source_claim lets a private ingest key name the source per request
  • stamp_vercel_log_drain_source replaces the gateway's maple_ingest_source on drain logs
  • pollOrg prunes deleted project ids and skips the drain write when nothing is picked

Before merge

  • Migration · Check it is safe on production data and with the code still deployed · packages/db/drizzle/20261019200241_vercel_integration/migration.sql
  • Manual · Connect a real Pro team on the preview and confirm both drains deliver logs and OTLP traces
  • Manual · Confirm the vercel alerting lane's 2-59/5 * * * * cron is deployed before merge

Findings

🟠 Warning · F9 · updateSettings ignores saveOutcomes returning false, so a concurrent disconnect leaves Maple's drains on Vercel

correctness · packages/backend/src/services/integrations/VercelIntegrationService.ts:1082-1085

saveOutcomes returns false when the connection row is gone or moved team (its updateConnection matches 0 rows), and updateSettings discards that value. If a disconnect (or a reconnect to another team) commits while this handler is reconciling, the drains it just created stay on the team, and Maple can never find or delete them again — the connection, and with it the token, is deleted. pollOrg handles exactly this case at :1235 by calling removeDrains; connect has the same gap at :1036.

			const saved = yield* saveOutcomes(connection, outcomes, ingestKey.hash, now)
			// Disconnected or moved team while this ran: take back what it may have created.
			if (!saved) yield* removeDrains(connection, token)
			const failure = firstError(outcomes)
			if (failure !== null) return yield* toDrainError(failure)
			return yield* getStatus(orgId)
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit c45d3d3c723eb0e8a7350d4578f72ff40380a609. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F9 · Warning · correctness · packages/backend/src/services/integrations/VercelIntegrationService.ts:1082-1085
`updateSettings` ignores `saveOutcomes` returning false, so a concurrent disconnect leaves Maple's drains on Vercel
`saveOutcomes` returns false when the connection row is gone or moved team (its `updateConnection` matches 0 rows), and `updateSettings` discards that value. If a disconnect (or a reconnect to another team) commits while this handler is reconciling, the drains it just created stay on the team, and Maple can never find or delete them again — the connection, and with it the token, is deleted. `pollOrg` handles exactly this case at `:1235` by calling `removeDrains`; `connect` has the same gap at `:1036`.
Replace those lines with:
			const saved = yield* saveOutcomes(connection, outcomes, ingestKey.hash, now)
			// Disconnected or moved team while this ran: take back what it may have created.
			if (!saved) yield* removeDrains(connection, token)
			const failure = firstError(outcomes)
			if (failure !== null) return yield* toDrainError(failure)
			return yield* getStatus(orgId)

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 974 GET 840/day, 2.8% err
GET 134/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 974 GET 840/day, 2.8% err
GET 134/day, 0.1% err
packages/domain/src/http/integrations.ts 337 GET /v2/integrations/gcp 337/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 11 gcpInfraPresence 11/day, 0.0% err
apps/api/src/routes/queries.ts 11 gcpInfraPresence 11/day, 0.0% err
packages/domain/src/http/query-engine.ts 11 gcpInfraPresence 11/day, 0.0% err
packages/backend/src/services/integrations/VercelIntegrationService.ts 7 VercelIntegrationService.getStatus 7/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:487
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:499
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:500
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:575
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:590
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:611
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:635
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:642
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:692
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:868
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:947
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:952
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1057
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1088
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1104
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1121
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1258
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1259
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1260
  • ➕ span name VercelIntegrationService.pollAllOrgs · packages/backend/src/services/integrations/VercelIntegrationService.ts:1288
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1343
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
  • ➕ span name VercelApi.listDeployments · packages/backend/src/services/integrations/vercel/api.ts:253
What was checked
  • Drain claims are verified against the org: a foreign x-maple-source-id falls back to the default source (main.rs:13700-13731)
  • accepts_source_claims admits private keys only, so the drains can claim while a source token cannot (main.rs:1395)
  • pollOrg's lease, budget and disconnected paths still release the lease and remove the drains it created (:1235-1285)
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
POST /v1/logpush/vercel inbound HTTP route yes handle_signal opens the ingest span named POST /v1/logpush/vercel and records item counts (main.rs:4668-4699)

c45d3d3 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

maple-review-bot[bot]

This comment was marked as resolved.

@maple-review-bot

maple-review-bot Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge
Both call sites now take back drains when the connection row is gone, pinned by a race test that fails without the fix.
no findings · tests covered · risk medium

The delta since the last review is ten lines: connect and updateSettings now check saveOutcomes and call removeDrains when the org disconnected or moved team mid-run, with a race test. Safe to merge. None of the listed alerting scheduled.ts error issues (WarehouseUpstreamError, PlanetScaleForbiddenError, ErrorPersistenceError, PrReviewTelemetryReadError) are addressed by this change.

  • updateSettings takes back the drains it created when the connection row is gone
  • connect does the same on its first-connect reconcile path
  • New test holds the drain listing across a concurrent disconnect

Before merge

Fixed since the last review

  • ✅ F9 · updateSettings ignores saveOutcomes returning false, so a concurrent disconnect leaves Maple's drains on Vercel

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 981 GET 847/day, 2.7% err
GET 134/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 981 GET 847/day, 2.7% err
GET 134/day, 0.1% err
packages/domain/src/http/integrations.ts 338 GET /v2/integrations/gcp 338/day, 0.0% err
packages/backend/src/services/integrations/VercelIntegrationService.ts 12 VercelIntegrationService.getStatus 7/day, 0.0% err
VercelIntegrationService.pollAllOrgs 5/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 11 gcpInfraPresence 11/day, 0.0% err
apps/api/src/routes/queries.ts 11 gcpInfraPresence 11/day, 0.0% err
packages/domain/src/http/query-engine.ts 11 gcpInfraPresence 11/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name QueryEngine.getVercelProjects · apps/web/src/api/warehouse/vercel-infra.ts:38
  • ➕ span name QueryEngine.getVercelRequestTimeseries · apps/web/src/api/warehouse/vercel-infra.ts:62
  • ➕ span name QueryEngine.getVercelProjectDetail · apps/web/src/api/warehouse/vercel-infra.ts:77
  • ➕ span name QueryEngine.getVercelAiGateway · apps/web/src/api/warehouse/vercel-infra.ts:94
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:487
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:499
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:500
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:575
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:590
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:611
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:635
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:642
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:692
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:868
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:947
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:952
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1060
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1094
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1110
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1127
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1264
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1265
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1266
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1349
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
  • ➕ span name VercelApi.listDeployments · packages/backend/src/services/integrations/vercel/api.ts:253
  • ➕ attribute maple.vercel.deployments · packages/backend/src/services/integrations/vercel/api.ts:293
What was checked
  • updateConnection returns false only on 0 matched rows, so the new branch fires exactly on a disconnect or team move (VercelIntegrationService.ts:291)
  • removeDrains catches and logs its own failures and returns false, so taking drains back cannot turn the save into a drain error (VercelIntegrationService.ts:590)
  • The new race test asserts fake.drains.size === 0 after the held listing is released, so it fails without the fix

8f7361e · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

# Conflicts:
#	apps/local-ui/src/hooks/use-local-service-map.test.ts
#	apps/local-ui/src/hooks/use-local-service-map.ts
#	apps/web/perf/check-bundle-budget.ts
#	apps/web/src/components/dashboard-builder/templates/template-icons.ts
#	apps/web/src/components/dashboard/app-sidebar.tsx
#	apps/web/src/components/dashboard/nav-items.test.ts
#	apps/web/src/components/dashboard/nav-items.ts
#	apps/web/src/components/infra/overview/infra-overview.tsx
#	apps/web/src/components/infra/overview/summaries.test.ts
#	apps/web/src/components/infra/overview/summaries.ts
#	apps/web/src/components/infra/platform-label.tsx
#	apps/web/src/components/integrations/integration-catalog.tsx
#	apps/web/src/hooks/use-infra-surfaces.ts
#	apps/web/src/lab/bench/service-map-bench.tsx
#	apps/web/src/lab/infra-lab.tsx
#	apps/web/src/lib/agent-sessions/vendor-icon.ts
#	apps/web/src/lib/services/atoms/warehouse-query-atoms.ts
#	apps/web/src/page/sources/kit/platforms.ts
#	apps/web/src/routeTree.gen.ts
#	apps/web/src/routes/integrations.tsx
#	packages/web-data/src/warehouse/vercel-infra.ts
@JeremyFunk JeremyFunk removed the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 20, 2026
@maple-review-bot

maple-review-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Maple review

🔴 Confidence 1/10 · do not merge
Eight confirmed defects in new Vercel paths; the MCP tool diffs, packages/ui, and much of the lab and test diff went unread when the budget ran out.
1 critical −3 · 6 warnings −6 · 2 notes · tests covered · risk high −1 · 40 files not read

Adds Vercel and AI Gateway as an integration: new tables, a poller lane, the /v1/logpush/vercel ingest route, queries, pages and MCP tools. Broad and mostly tested, but the session cost, the deployment watermark and several web read-state paths are wrong as written. Not read: the MCP write tool sync_vercel and the lab/fixture files beyond their diffs.

  • VercelIntegrationService reconciles Maple's two drains per org on the new alerting vercel lane
  • POST /v1/logpush/vercel maps drain NDJSON to OTel logs; vercel_ai_gateway derives AI Gateway sessions
  • One migration adds vercel_connections, vercel_projects, vercel_deployments, purged on org delete
  • New Infrastructure → Vercel pages, the Vercel dashboard template and the sync_vercel MCP write

Before merge

Findings

🔴 Critical · F18 · Credential configure.token written to a span attribute

security · PII-01 · apps/web/src/page/integrations/view-vercel-configure.ts:116

Anyone who can read the organization's telemetry can read it, for the whole retention window. Drop it, or record only whether it is set, and rotate any value already sent.

🟠 Warning · F10 · normalize stamps a non-USD gateway cost as gen_ai.usage.cost

correctness · apps/ingest/src/ai_session/vercel_ai_gateway.rs:38

vercel.ai_gateway.cost.total is copied into gen_ai.usage.cost without reading vercel.ai_gateway.cost.currency, so a request billed in any other currency is restated as USD and summed into maple_ai.usage.cost and the session/model cost totals. This PR's own vercelAiGatewaySummarySQL guards the same field with sumIf(..., inList(currency, ["", "USD"])), so the session cost and Infrastructure → Vercel disagree for the same request.

Read `vercel.ai_gateway.cost.currency` and only emit `gen_ai.usage.cost` when it is absent or `USD`; add a test with `cost.currency: "EUR"` asserting no cost attribute.
🟠 Warning · F11 · Deployment watermark advances past the page cap, dropping unread deployments

correctness · packages/backend/src/services/integrations/VercelIntegrationService.ts:1218

The poll writes the newest listed deployment as deploymentsWatermarkAt even when listDeployments stopped at its page cap, so on the first pass (since 7 days back, newest-first listing) a team with more deployments than the cap only ever sees the newest pages and every older deployment is skipped for good. Those projects keep an empty deployment list on the Infrastructure → Vercel pages.

Have `listDeployments` report whether the last page was reached, and only advance the watermark when the listing was complete (or advance it to the oldest fetched page so the next tick resumes).
🟠 Warning · F12 · A failed revalidation leaves the Vercel card's quick metrics as permanent skeletons

correctness · apps/web/src/page/integrations/view-vercel.ts:257

When a read that previously succeeded fails, Keyed.accept leaves AsyncData.Stale({ data, error }); readOf maps that to "failed" and discards the data, while failureOf/usageFailure only match AsyncData.isFailure, so quickMetrics renders statRailLoading forever with no error text and no retry. The admin sees a card that never finishes loading instead of the retained numbers plus an error.

Treat `onStale` as stale-with-data: pass the retained `data` to the value function and surface `error` through `usageFailure`/`errorState`.
🟠 Warning · F13 · vercelHealth ignores status.lastError, so a failed first sync reads as "First sync pending"

correctness · apps/web/src/components/integrations/vercel-model.ts:181

The backend records lastError on a failed pass without advancing lastSuccessAt, but the derivation keys only on lastSuccessAt, and isVercelSettling stops the 15 s re-reads once connectedAt is ten minutes old — so an org whose token is rejected or whose poll keeps timing out shows "First sync pending", no issue row and no retry, forever. apps/web/src/page/integrations/derive.ts:295 derives the same wrong state for the hub row.

Return `"attention"` plus an issue when `status.lastError !== null` (or when `lastSuccessAt === null` and `connectedAt` is older than the settling window), and apply the same rule in `derive.ts`'s `vercelHealth`.
🟠 Warning · F14 · A failed vercelStatus read renders as "No production deployment recorded"

correctness · apps/web/src/page/infra-integrations/vercel/project/view.ts:640

projectStatusOf returns null for both "loading" and "the read failed", so a failed LoadVercelProjectStatus makes the header claim the project has no production deployment, drops the framework chip and leaves the raw prj_… id as the page title. The model's Message union has no retry for this slot, so the read can never be re-issued without reloading the page; the list page shows an errorState with a retry for the same data.

Give the status slot a failure branch like the list page's plus a `ClickedRetryStatus` message, and keep the "No production deployment recorded" line for loaded data only.
🟠 Warning · F15 · The sampling select shows 100% for a connection saved at any rate outside VERCEL_SAMPLING_RATES

correctness · apps/web/src/page/integrations/view-vercel-configure.ts:416

Vercel accepts any rate in 0.01–1 and the settings PATCH stores what it is sent, so a connection saved at e.g. samplingRate: 0.3 produces defaultValue: "0.3", which matches no nativeOption; the browser selects the first entry ("Everything") while the drains panel prints "30% of events" for the same connection. changedSampling (update-vercel.ts:304) also drops any value outside the list, so the admin cannot re-pick the stored rate.

Append the saved rate as an option when it is not in `VERCEL_SAMPLING_RATES` so the control always reflects `draft.samplingRate`.
🔵 Note · F16 · The "waiting for first logs" empty state is gated on rows.length === 0, so it shows in the opposite cases

correctness · apps/web/src/page/infra-integrations/vercel/list/view.ts:136

vercelProjectRows keeps every forwarded project even with no telemetry, so a team that forwards projects but has received nothing renders a table of zeros and never the "waiting for Vercel's first logs" copy, while a connection that forwards nothing (projectScope: "some" with an empty selection) has rows.length === 0 and is told to wait for logs that can never arrive.

Branch the waiting copy on the telemetry rows (`projects.length === 0`) and choose its title from whether any `status.projects` entry has `forwarded: true`.
🔵 Note · F17 · vercelLogsWhere tests the cloud.provider value without a key guard, so the resource-attribute index never prunes

performance · packages/query-engine-integrations/src/vercel/vercel-infra.ts:36

Every /infra/vercel read filters Vercel logs on the cloud.provider value alone, so the org's whole log volume in the window is decompressed and the map lookups evaluated per row instead of only Vercel granules. The same file's traces query (vercel-infra.ts:151) adds the mapContains(ResourceAttributes, "cloud.provider") key test it says the value test alone cannot replace.

Add `CH.mapContains($.ResourceAttributes, "cloud.provider")` to `vercelLogsWhere` so `idx_resource_attr_keys` can skip granules.
🤖 Prompt to fix all 9 findings with an AI agent
Findings from an automated review of commit 9d247e52431b461b3dc28ac77bee6a913c54ce03. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F18 · Critical · security · PII-01 · apps/web/src/page/integrations/view-vercel-configure.ts:116
Credential `configure.token` written to a span attribute
Anyone who can read the organization's telemetry can read it, for the whole retention window. Drop it, or record only whether it is set, and rotate any value already sent.

---

F10 · Warning · correctness · apps/ingest/src/ai_session/vercel_ai_gateway.rs:38
`normalize` stamps a non-USD gateway cost as `gen_ai.usage.cost`
`vercel.ai_gateway.cost.total` is copied into `gen_ai.usage.cost` without reading `vercel.ai_gateway.cost.currency`, so a request billed in any other currency is restated as USD and summed into `maple_ai.usage.cost` and the session/model cost totals. This PR's own `vercelAiGatewaySummarySQL` guards the same field with `sumIf(..., inList(currency, ["", "USD"]))`, so the session cost and Infrastructure → Vercel disagree for the same request.
Suggested fix: Read `vercel.ai_gateway.cost.currency` and only emit `gen_ai.usage.cost` when it is absent or `USD`; add a test with `cost.currency: "EUR"` asserting no cost attribute.

---

F11 · Warning · correctness · packages/backend/src/services/integrations/VercelIntegrationService.ts:1218
Deployment watermark advances past the page cap, dropping unread deployments
The poll writes the newest listed deployment as `deploymentsWatermarkAt` even when `listDeployments` stopped at its page cap, so on the first pass (`since` 7 days back, newest-first listing) a team with more deployments than the cap only ever sees the newest pages and every older deployment is skipped for good. Those projects keep an empty deployment list on the Infrastructure → Vercel pages.
Suggested fix: Have `listDeployments` report whether the last page was reached, and only advance the watermark when the listing was complete (or advance it to the oldest fetched page so the next tick resumes).

---

F12 · Warning · correctness · apps/web/src/page/integrations/view-vercel.ts:257
A failed revalidation leaves the Vercel card's quick metrics as permanent skeletons
When a read that previously succeeded fails, `Keyed.accept` leaves `AsyncData.Stale({ data, error })`; `readOf` maps that to `"failed"` and discards the data, while `failureOf`/`usageFailure` only match `AsyncData.isFailure`, so `quickMetrics` renders `statRailLoading` forever with no error text and no retry. The admin sees a card that never finishes loading instead of the retained numbers plus an error.
Suggested fix: Treat `onStale` as stale-with-data: pass the retained `data` to the value function and surface `error` through `usageFailure`/`errorState`.

---

F13 · Warning · correctness · apps/web/src/components/integrations/vercel-model.ts:181
`vercelHealth` ignores `status.lastError`, so a failed first sync reads as "First sync pending"
The backend records `lastError` on a failed pass without advancing `lastSuccessAt`, but the derivation keys only on `lastSuccessAt`, and `isVercelSettling` stops the 15 s re-reads once `connectedAt` is ten minutes old — so an org whose token is rejected or whose poll keeps timing out shows "First sync pending", no issue row and no retry, forever. `apps/web/src/page/integrations/derive.ts:295` derives the same wrong state for the hub row.
Suggested fix: Return `"attention"` plus an issue when `status.lastError !== null` (or when `lastSuccessAt === null` and `connectedAt` is older than the settling window), and apply the same rule in `derive.ts`'s `vercelHealth`.

---

F14 · Warning · correctness · apps/web/src/page/infra-integrations/vercel/project/view.ts:640
A failed `vercelStatus` read renders as "No production deployment recorded"
`projectStatusOf` returns `null` for both "loading" and "the read failed", so a failed `LoadVercelProjectStatus` makes the header claim the project has no production deployment, drops the framework chip and leaves the raw `prj_…` id as the page title. The model's `Message` union has no retry for this slot, so the read can never be re-issued without reloading the page; the list page shows an `errorState` with a retry for the same data.
Suggested fix: Give the status slot a failure branch like the list page's plus a `ClickedRetryStatus` message, and keep the "No production deployment recorded" line for loaded data only.

---

F15 · Warning · correctness · apps/web/src/page/integrations/view-vercel-configure.ts:416
The sampling select shows 100% for a connection saved at any rate outside `VERCEL_SAMPLING_RATES`
Vercel accepts any rate in 0.01–1 and the settings PATCH stores what it is sent, so a connection saved at e.g. `samplingRate: 0.3` produces `defaultValue: "0.3"`, which matches no `nativeOption`; the browser selects the first entry ("Everything") while the drains panel prints "30% of events" for the same connection. `changedSampling` (`update-vercel.ts:304`) also drops any value outside the list, so the admin cannot re-pick the stored rate.
Suggested fix: Append the saved rate as an option when it is not in `VERCEL_SAMPLING_RATES` so the control always reflects `draft.samplingRate`.

---

F16 · Note · correctness · apps/web/src/page/infra-integrations/vercel/list/view.ts:136
The "waiting for first logs" empty state is gated on `rows.length === 0`, so it shows in the opposite cases
`vercelProjectRows` keeps every forwarded project even with no telemetry, so a team that forwards projects but has received nothing renders a table of zeros and never the "waiting for Vercel's first logs" copy, while a connection that forwards nothing (`projectScope: "some"` with an empty selection) has `rows.length === 0` and is told to wait for logs that can never arrive.
Suggested fix: Branch the waiting copy on the telemetry rows (`projects.length === 0`) and choose its title from whether any `status.projects` entry has `forwarded: true`.

---

F17 · Note · performance · packages/query-engine-integrations/src/vercel/vercel-infra.ts:36
`vercelLogsWhere` tests the `cloud.provider` value without a key guard, so the resource-attribute index never prunes
Every `/infra/vercel` read filters Vercel logs on the `cloud.provider` value alone, so the org's whole log volume in the window is decompressed and the map lookups evaluated per row instead of only Vercel granules. The same file's traces query (`vercel-infra.ts:151`) adds the `mapContains(ResourceAttributes, "cloud.provider")` key test it says the value test alone cannot replace.
Suggested fix: Add `CH.mapContains($.ResourceAttributes, "cloud.provider")` to `vercelLogsWhere` so `idx_resource_attr_keys` can skip granules.

Production impact

Open errors in the changed files
Issue Service Occurrences File
@maple/http/errors/WarehouseUpstreamError alerting 80k apps/alerting/src/scheduled.ts
@maple/http/errors/WarehouseUpstreamError alerting 20k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 2.6k apps/alerting/src/scheduled.ts
@maple/backend/services/pr-review/PrReviewTelemetryReadError alerting 2.0k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/backend/integrations/PlanetScaleForbiddenError alerting 1.6k apps/alerting/src/scheduled.ts
@maple/http/errors/ErrorPersistenceError alerting 1.2k apps/alerting/src/scheduled.ts

After this merges, Maple checks whether they stop.

Production traffic of the changed files (last 7 days)
File Calls/day Busiest operations
apps/ingest/src/vercel_logs.rs 1.0k GET 902/day, 2.6% err
GET 132/day, 0.1% err
packages/query-engine-integrations/src/benchmark/index.ts 1.0k GET 902/day, 2.6% err
GET 132/day, 0.1% err
packages/domain/src/http/integrations.ts 360 GET /v2/integrations/gcp 360/day, 0.0% err
packages/backend/src/services/integrations/VercelIntegrationService.ts 43 VercelIntegrationService.pollAllOrgs 35/day, 0.0% err
VercelIntegrationService.getStatus 8/day, 0.0% err
apps/web/src/shell/data.ts 20 LoadCloudflareStatus 10/day, 0.0% err
LoadPlanetscaleStatus 10/day, 0.0% err
apps/api/src/routes/internal/query-engine.http.ts 11 gcpInfraPresence 11/day, 0.0% err
apps/api/src/routes/queries.ts 11 gcpInfraPresence 11/day, 0.0% err
packages/domain/src/http/query-engine.ts 11 gcpInfraPresence 11/day, 0.0% err
Telemetry this change adds and removes (30)
  • ➕ span name McpTool.syncVercel · apps/ai/src/mcp/tools/integration-writes.ts:157
  • ➕ span name VercelIntegrationService.reconcileDrain · packages/backend/src/services/integrations/VercelIntegrationService.ts:487
  • ➕ attribute maple.vercel.drain_kind · packages/backend/src/services/integrations/VercelIntegrationService.ts:499
  • ➕ attribute maple.vercel.drains_found · packages/backend/src/services/integrations/VercelIntegrationService.ts:500
  • ➕ span name VercelIntegrationService.reconcileDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:575
  • ➕ span name VercelIntegrationService.removeDrains · packages/backend/src/services/integrations/VercelIntegrationService.ts:590
  • ➕ attribute maple.vercel.drains_removed · packages/backend/src/services/integrations/VercelIntegrationService.ts:611
  • ➕ span name VercelIntegrationService.storeProjects · packages/backend/src/services/integrations/VercelIntegrationService.ts:635
  • ➕ attribute maple.vercel.projects · packages/backend/src/services/integrations/VercelIntegrationService.ts:642
  • ➕ span name VercelIntegrationService.storeDeployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:692
  • ➕ span name VercelIntegrationService.discover · packages/backend/src/services/integrations/VercelIntegrationService.ts:868
  • ➕ span name VercelIntegrationService.connect · packages/backend/src/services/integrations/VercelIntegrationService.ts:947
  • ➕ attribute maple.vercel.team_id · packages/backend/src/services/integrations/VercelIntegrationService.ts:952
  • ➕ span name VercelIntegrationService.updateSettings · packages/backend/src/services/integrations/VercelIntegrationService.ts:1060
  • ➕ span name VercelIntegrationService.disconnect · packages/backend/src/services/integrations/VercelIntegrationService.ts:1094
  • ➕ span name VercelIntegrationService.deployments · packages/backend/src/services/integrations/VercelIntegrationService.ts:1110
  • ➕ span name VercelIntegrationService.pollOrg · packages/backend/src/services/integrations/VercelIntegrationService.ts:1127
  • ➕ attribute maple.vercel.deployments_upserted · packages/backend/src/services/integrations/VercelIntegrationService.ts:1264
  • ➕ attribute maple.vercel.drains_repaired · packages/backend/src/services/integrations/VercelIntegrationService.ts:1265
  • ➕ attribute maple.vercel.failures · packages/backend/src/services/integrations/VercelIntegrationService.ts:1266
  • ➕ span name VercelIntegrationService.sync · packages/backend/src/services/integrations/VercelIntegrationService.ts:1349
  • ➕ span name VercelApi.listTeams · packages/backend/src/services/integrations/vercel/api.ts:110
  • ➕ span name VercelApi.listProjects · packages/backend/src/services/integrations/vercel/api.ts:171
  • ➕ attribute maple.vercel.projects_complete · packages/backend/src/services/integrations/vercel/api.ts:198
  • ➕ span name VercelApi.listDeployments · packages/backend/src/services/integrations/vercel/api.ts:253
  • ➕ attribute maple.vercel.deployments · packages/backend/src/services/integrations/vercel/api.ts:293
  • ➕ span name VercelApi.createDrain · packages/backend/src/services/integrations/vercel/api.ts:333
  • ➕ span name VercelApi.updateDrain · packages/backend/src/services/integrations/vercel/api.ts:348
  • ➕ span name VercelApi.listDrains · packages/backend/src/services/integrations/vercel/api.ts:416
  • ➕ attribute maple.vercel.drains · packages/backend/src/services/integrations/vercel/api.ts:422
What was checked
  • Migration adds three tables; OrgScopedTable purge covers all three, including the encrypted token
  • New ClickHouse queries filter OrgId and bound time; sql_baseline regenerated
  • integration-inventory.ts:417 maps an unknown drain status to pending, not failing
Files not reviewed (40)

The review ended before it read these diffs, so nothing above vouches for them.

  • apps/ai/src/mcp/__evals__/provider-world.ts
  • apps/ai/src/mcp/tools/__tests__/integration-tools.test.ts
  • apps/ai/src/mcp/tools/__tests__/provider-infra-tools.test.ts
  • apps/alerting/src/scheduled.test.ts
  • apps/local-ui/src/page/service-map/data.ts
  • apps/local-ui/src/page/service-map/rows.ts
  • apps/web/src/components/infra/vercel/list-model.test.ts
  • apps/web/src/components/integrations/vercel-model.test.ts
  • apps/web/src/components/mcp/mcp-tool-groups.ts
  • apps/web/src/lab/vercel-lab.test.ts
  • apps/web/src/lab/vercel-lab.ts
  • apps/web/src/lib/agent-sessions/vendor-color.test.ts
  • apps/web/src/lib/agent-sessions/vendor-color.ts
  • apps/web/src/lib/agent-sessions/vendor-label.ts
  • apps/web/src/lib/docs.ts
  • apps/web/src/page/agent-sessions/kit/vendor.ts
  • apps/web/src/page/dashboards/templates/derive.ts
  • apps/web/src/page/infra-integrations/vercel/list/vercel.scene.test.ts
  • apps/web/src/page/infra-integrations/vercel/project/project.scene.test.ts
  • apps/web/src/page/infra/kubernetes/workloads/workloads.scene.test.ts
  • apps/web/src/page/integrations/vercel.scene.test.ts
  • apps/web/src/page/settings/setup-health/derive.ts
  • apps/web/src/page/sources/kit/platforms.ts
  • apps/web/src/page/sources/kit/shared.ts
  • apps/web/src/service-map/map/node-card.ts
  • packages/backend/src/dashboard-templates/infrastructure/vercel.test.ts
  • packages/backend/src/services/integrations/IntegrationInventoryService.test.ts
  • packages/backend/src/services/integrations/VercelIntegrationService.test.ts
  • packages/backend/src/services/integrations/VercelIntegrationService.ts
  • packages/domain/src/integration-inventory.test.ts
  • and 10 more

9d247e5 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one. Check ids refer to Maple's instrumentation audit.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +1214 to +1218
const newest = Math.max(
claimed.deploymentsWatermarkAt ?? 0,
...listed.success.map((d) => d.createdAt),
)
if (newest > 0) yield* write({ deploymentsWatermarkAt: newest })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Deployment history loses older pages

When more than 500 deployments need backfilling, pollOrg advances the watermark from the first five pages. Subsequent polls start near that watermark, permanently omitting older deployments.

Learn more

The deployment API lists newest entries first, and listDeployments stops after the supplied page cap without returning a continuation cursor. A newly connected team with more than five pages of recent deployments only stores those first pages. Advancing deploymentsWatermarkAt to their newest timestamp makes the next poll start an hour before that timestamp, so older pages never enter its query window.

Example: A team created 600 deployments this week. The first poll stores the newest 500 and sets its watermark to the newest deployment. The next poll begins an hour before that deployment, so the remaining 100 older deployments stay absent.

Recommended fix: Persist pagination progress or a separate backfill frontier, continue paging on later ticks until the initial lookback is fully covered, and only then use the newest-created watermark for incremental polling. Include a test with more than DEPLOYMENT_PAGES_PER_TICK * PAGE_SIZE deployments.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +1230 to +1231
const reconciled = yield* Effect.result(
reconcileDrains(claimed, token, forwarding, ingestKey.success, false),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Concurrent poll restores old drain settings

If settings change during pollOrg, its stale claimed snapshot can overwrite the new drain configuration. The saved settings then disagree with Vercel until another poll repairs them.

Learn more

A poll claims a lease and holds a snapshot of the forwarding settings. updateSettings does not wait for that lease: it saves and reconciles new settings concurrently. When the older poll later calls reconcileDrains using claimed, it patches the drains back to the old settings, and saveOutcomes records the old outcome. The next successful poll can eventually restore the saved configuration, but telemetry in the interval uses the wrong settings.

Example: The poll reads logsEnabled=true; an admin disables logs and the save deletes the log drain. The poll then creates that log drain again from its old snapshot, although the status says logsEnabled=false.

Recommended fix: Serialize settings changes with polling on the same connection, or reload forwarding settings and validate their revision before reconciling and saving outcomes. Test a poll paused before listing drains while a settings save completes.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +522 to +526
// Every picked project was deleted on Vercel: there is nothing to forward, and no valid body.
const nothingPicked = forwarding.projectScope === "some" && forwarding.projectIds.length === 0
const write = nothingPicked
? undefined
: kept === undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Empty project selection leaves idle drains

When discovery removes every selected project, nothingPicked skips reconciliation but retains enabled drains. The integration reports healthy drains although they no longer forward any projects.

Learn more

The discovery pass prunes deleted project IDs, so a some scope can become empty. reconcileDrain then leaves existing drains in place and reports their old status rather than deleting or marking them as unable to forward. The health display treats these enabled drains as healthy, even though no project remains selected.

Example: An admin selects only prj_old; Vercel deletes that project. On the next hourly discovery the saved IDs become [], while both drains remain enabled and the card shows healthy.

Recommended fix: Represent an empty selection as an explicit attention state or disable/delete the drains when no projects remain, and expose a recoverable state in the status and UI.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@JeremyFunk JeremyFunk added the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 20, 2026

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 inline notes from Maple's review. The score and summary are in the review comment above.

if span.attributes.iter().any(|attr| attr.key == COST) {
return;
}
let cost = span.attributes.iter().find_map(|attr| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

normalize stamps a non-USD gateway cost as gen_ai.usage.cost

F10 · Warning · correctness

vercel.ai_gateway.cost.total is copied into gen_ai.usage.cost without reading vercel.ai_gateway.cost.currency, so a request billed in any other currency is restated as USD and summed into maple_ai.usage.cost and the session/model cost totals. This PR's own vercelAiGatewaySummarySQL guards the same field with sumIf(..., inList(currency, ["", "USD"])), so the session cost and Infrastructure → Vercel disagree for the same request.

Read `vercel.ai_gateway.cost.currency` and only emit `gen_ai.usage.cost` when it is absent or `USD`; add a test with `cost.currency: "EUR"` asserting no cost attribute.
🤖 Prompt to fix with an AI agent
In `apps/ingest/src/ai_session/vercel_ai_gateway.rs:38`: `normalize` stamps a non-USD gateway cost as `gen_ai.usage.cost`.

`vercel.ai_gateway.cost.total` is copied into `gen_ai.usage.cost` without reading `vercel.ai_gateway.cost.currency`, so a request billed in any other currency is restated as USD and summed into `maple_ai.usage.cost` and the session/model cost totals. This PR's own `vercelAiGatewaySummarySQL` guards the same field with `sumIf(..., inList(currency, ["", "USD"]))`, so the session cost and Infrastructure → Vercel disagree for the same request.

Suggested fix: Read `vercel.ai_gateway.cost.currency` and only emit `gen_ai.usage.cost` when it is absent or `USD`; add a test with `cost.currency: "EUR"` asserting no cost attribute.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

claimed.deploymentsWatermarkAt ?? 0,
...listed.success.map((d) => d.createdAt),
)
if (newest > 0) yield* write({ deploymentsWatermarkAt: newest })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Deployment watermark advances past the page cap, dropping unread deployments

F11 · Warning · correctness

The poll writes the newest listed deployment as deploymentsWatermarkAt even when listDeployments stopped at its page cap, so on the first pass (since 7 days back, newest-first listing) a team with more deployments than the cap only ever sees the newest pages and every older deployment is skipped for good. Those projects keep an empty deployment list on the Infrastructure → Vercel pages.

Have `listDeployments` report whether the last page was reached, and only advance the watermark when the listing was complete (or advance it to the oldest fetched page so the next tick resumes).
🤖 Prompt to fix with an AI agent
In `packages/backend/src/services/integrations/VercelIntegrationService.ts:1218`: Deployment watermark advances past the page cap, dropping unread deployments.

The poll writes the newest listed deployment as `deploymentsWatermarkAt` even when `listDeployments` stopped at its page cap, so on the first pass (`since` 7 days back, newest-first listing) a team with more deployments than the cap only ever sees the newest pages and every older deployment is skipped for good. Those projects keep an empty deployment list on the Infrastructure → Vercel pages.

Suggested fix: Have `listDeployments` report whether the last page was reached, and only advance the watermark when the listing was complete (or advance it to the oldest fetched page so the next tick resumes).

Verify the problem exists at that location before changing it, and keep the fix to those lines.

onIdle: () => null,
onLoading: () => null,
onFailure: () => "failed" as const,
onStale: () => "failed" as const,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

A failed revalidation leaves the Vercel card's quick metrics as permanent skeletons

F12 · Warning · correctness

When a read that previously succeeded fails, Keyed.accept leaves AsyncData.Stale({ data, error }); readOf maps that to "failed" and discards the data, while failureOf/usageFailure only match AsyncData.isFailure, so quickMetrics renders statRailLoading forever with no error text and no retry. The admin sees a card that never finishes loading instead of the retained numbers plus an error.

Treat `onStale` as stale-with-data: pass the retained `data` to the value function and surface `error` through `usageFailure`/`errorState`.
🤖 Prompt to fix with an AI agent
In `apps/web/src/page/integrations/view-vercel.ts:257`: A failed revalidation leaves the Vercel card's quick metrics as permanent skeletons.

When a read that previously succeeded fails, `Keyed.accept` leaves `AsyncData.Stale({ data, error })`; `readOf` maps that to `"failed"` and discards the data, while `failureOf`/`usageFailure` only match `AsyncData.isFailure`, so `quickMetrics` renders `statRailLoading` forever with no error text and no retry. The admin sees a card that never finishes loading instead of the retained numbers plus an error.

Suggested fix: Treat `onStale` as stale-with-data: pass the retained `data` to the value function and surface `error` through `usageFailure`/`errorState`.

Verify the problem exists at that location before changing it, and keep the fix to those lines.


export function vercelHealth(status: VercelIntegrationStatus): VercelHealth {
if (vercelIssues(status).length > 0) return "attention"
return status.lastSuccessAt === null ? "pending" : "healthy"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

vercelHealth ignores status.lastError, so a failed first sync reads as "First sync pending"

F13 · Warning · correctness

The backend records lastError on a failed pass without advancing lastSuccessAt, but the derivation keys only on lastSuccessAt, and isVercelSettling stops the 15 s re-reads once connectedAt is ten minutes old — so an org whose token is rejected or whose poll keeps timing out shows "First sync pending", no issue row and no retry, forever. apps/web/src/page/integrations/derive.ts:295 derives the same wrong state for the hub row.

Return `"attention"` plus an issue when `status.lastError !== null` (or when `lastSuccessAt === null` and `connectedAt` is older than the settling window), and apply the same rule in `derive.ts`'s `vercelHealth`.
🤖 Prompt to fix with an AI agent
In `apps/web/src/components/integrations/vercel-model.ts:181`: `vercelHealth` ignores `status.lastError`, so a failed first sync reads as "First sync pending".

The backend records `lastError` on a failed pass without advancing `lastSuccessAt`, but the derivation keys only on `lastSuccessAt`, and `isVercelSettling` stops the 15 s re-reads once `connectedAt` is ten minutes old — so an org whose token is rejected or whose poll keeps timing out shows "First sync pending", no issue row and no retry, forever. `apps/web/src/page/integrations/derive.ts:295` derives the same wrong state for the hub row.

Suggested fix: Return `"attention"` plus an issue when `status.lastError !== null` (or when `lastSuccessAt === null` and `connectedAt` is older than the settling window), and apply the same rule in `derive.ts`'s `vercelHealth`.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

project?.framework === null || project === null ? undefined : heroChip(h, project.framework),
meta:
production === null
? [h.span([h.Class("text-2xs text-muted-foreground")], ["No production deployment recorded"])]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

A failed vercelStatus read renders as "No production deployment recorded"

F14 · Warning · correctness

projectStatusOf returns null for both "loading" and "the read failed", so a failed LoadVercelProjectStatus makes the header claim the project has no production deployment, drops the framework chip and leaves the raw prj_… id as the page title. The model's Message union has no retry for this slot, so the read can never be re-issued without reloading the page; the list page shows an errorState with a retry for the same data.

Give the status slot a failure branch like the list page's plus a `ClickedRetryStatus` message, and keep the "No production deployment recorded" line for loaded data only.
🤖 Prompt to fix with an AI agent
In `apps/web/src/page/infra-integrations/vercel/project/view.ts:640`: A failed `vercelStatus` read renders as "No production deployment recorded".

`projectStatusOf` returns `null` for both "loading" and "the read failed", so a failed `LoadVercelProjectStatus` makes the header claim the project has no production deployment, drops the framework chip and leaves the raw `prj_…` id as the page title. The model's `Message` union has no retry for this slot, so the read can never be re-issued without reloading the page; the list page shows an `errorState` with a retry for the same data.

Suggested fix: Give the status slot a failure branch like the list page's plus a `ClickedRetryStatus` message, and keep the "No production deployment recorded" line for loaded data only.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

h,
{
className: "w-44",
defaultValue: String(draft.samplingRate),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

The sampling select shows 100% for a connection saved at any rate outside VERCEL_SAMPLING_RATES

F15 · Warning · correctness

Vercel accepts any rate in 0.01–1 and the settings PATCH stores what it is sent, so a connection saved at e.g. samplingRate: 0.3 produces defaultValue: "0.3", which matches no nativeOption; the browser selects the first entry ("Everything") while the drains panel prints "30% of events" for the same connection. changedSampling (update-vercel.ts:304) also drops any value outside the list, so the admin cannot re-pick the stored rate.

Append the saved rate as an option when it is not in `VERCEL_SAMPLING_RATES` so the control always reflects `draft.samplingRate`.
🤖 Prompt to fix with an AI agent
In `apps/web/src/page/integrations/view-vercel-configure.ts:416`: The sampling select shows 100% for a connection saved at any rate outside `VERCEL_SAMPLING_RATES`.

Vercel accepts any rate in 0.01–1 and the settings PATCH stores what it is sent, so a connection saved at e.g. `samplingRate: 0.3` produces `defaultValue: "0.3"`, which matches no `nativeOption`; the browser selects the first entry ("Everything") while the drains panel prints "30% of events" for the same connection. `changedSampling` (`update-vercel.ts:304`) also drops any value outside the list, so the admin cannot re-pick the stored rate.

Suggested fix: Append the saved rate as an option when it is not in `VERCEL_SAMPLING_RATES` so the control always reflects `draft.samplingRate`.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

h.Autocomplete("off"),
h.Spellcheck(false),
h.Placeholder("Paste a token"),
...Field.controlAttributes(h, configure.token, (message) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Credential configure.token written to a span attribute

F18 · Critical · security · PII-01

Anyone who can read the organization's telemetry can read it, for the whole retention window. Drop it, or record only whether it is set, and rotate any value already sent.

🤖 Prompt to fix with an AI agent
In `apps/web/src/page/integrations/view-vercel-configure.ts:116`: Credential `configure.token` written to a span attribute.

Anyone who can read the organization's telemetry can read it, for the whole retention window. Drop it, or record only whether it is set, and rotate any value already sent.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

@JeremyFunk JeremyFunk removed the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 20, 2026
@JeremyFunk JeremyFunk added the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 20, 2026
@Makisuo Makisuo removed the preview Deploy a full PR preview stack (Cloudflare + AWS ingest); removing it tears it down label Oct 22, 2026
@Makisuo

Makisuo commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Removed the preview label to free a Tinybird branch slot (the workspace is at its branch cap) for #1682's preview. Re-add it to redeploy this preview.

This branch was previously deployed

1 inactive deployment
pr-preview — 9d247e52 Deployed Oct 20, 2026 by JeremyFunk via deploy-pr-preview #5792
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.

2 participants