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

Merge remote-tracking branch 'origin/main' into feat/vercel-integration

9d247e5
Select commit
Loading
Failed to load commit list.
Maple Review Bot / Maple / review completed Oct 20, 2026 in 7m 9s

Confidence 1/10 · 7 issues to address

🔴 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.

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.

Annotations

Check warning on line 38 in apps/ingest/src/ai_session/vercel_ai_gateway.rs

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: `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.

Check warning on line 1218 in packages/backend/src/services/integrations/VercelIntegrationService.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: 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.

Check warning on line 257 in apps/web/src/page/integrations/view-vercel.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: 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.

Check warning on line 181 in apps/web/src/components/integrations/vercel-model.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: `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.

Check warning on line 640 in apps/web/src/page/infra-integrations/vercel/project/view.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: 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.

Check warning on line 416 in apps/web/src/page/integrations/view-vercel-configure.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: 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.

Check notice on line 136 in apps/web/src/page/infra-integrations/vercel/list/view.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

correctness: 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.

Check notice on line 36 in packages/query-engine-integrations/src/vercel/vercel-infra.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

performance: `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.

Check failure on line 116 in apps/web/src/page/integrations/view-vercel-configure.ts

See this annotation in the file changed.

@maple-review-bot maple-review-bot / Maple / review

security · PII-01: 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.