Repository navigation
feat(vercel): Vercel and AI Gateway integration - #1583
JeremyFunk wants to merge 17 commits into
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.
VercelIntegrationServicereconciles Maple's two drains per org on the new alertingvercellanePOST /v1/logpush/vercelmaps drain NDJSON to OTel logs;vercel_ai_gatewayderives 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_vercelMCP write
Before merge
- Migration · Check it is safe on production data and with the code still deployed ·
packages/db/drizzle/20261020142059_vercel_integration/migration.sql - Manual · Verify drain payloads against a real Pro team; the PR says this is not yet done
- Manual · Confirm the ingest URL the poller writes into drains per environment
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% errGET 132/day, 0.1% err |
packages/query-engine-integrations/src/benchmark/index.ts |
1.0k | GET 902/day, 2.6% errGET 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% errVercelIntegrationService.getStatus 8/day, 0.0% err |
apps/web/src/shell/data.ts |
20 | LoadCloudflareStatus 10/day, 0.0% errLoadPlanetscaleStatus 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;
OrgScopedTablepurge covers all three, including the encrypted token - New ClickHouse queries filter
OrgIdand bound time;sql_baselineregenerated integration-inventory.ts:417maps an unknown drain status topending, notfailing
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.tsapps/ai/src/mcp/tools/__tests__/integration-tools.test.tsapps/ai/src/mcp/tools/__tests__/provider-infra-tools.test.tsapps/alerting/src/scheduled.test.tsapps/local-ui/src/page/service-map/data.tsapps/local-ui/src/page/service-map/rows.tsapps/web/src/components/infra/vercel/list-model.test.tsapps/web/src/components/integrations/vercel-model.test.tsapps/web/src/components/mcp/mcp-tool-groups.tsapps/web/src/lab/vercel-lab.test.tsapps/web/src/lab/vercel-lab.tsapps/web/src/lib/agent-sessions/vendor-color.test.tsapps/web/src/lib/agent-sessions/vendor-color.tsapps/web/src/lib/agent-sessions/vendor-label.tsapps/web/src/lib/docs.tsapps/web/src/page/agent-sessions/kit/vendor.tsapps/web/src/page/dashboards/templates/derive.tsapps/web/src/page/infra-integrations/vercel/list/vercel.scene.test.tsapps/web/src/page/infra-integrations/vercel/project/project.scene.test.tsapps/web/src/page/infra/kubernetes/workloads/workloads.scene.test.tsapps/web/src/page/integrations/vercel.scene.test.tsapps/web/src/page/settings/setup-health/derive.tsapps/web/src/page/sources/kit/platforms.tsapps/web/src/page/sources/kit/shared.tsapps/web/src/service-map/map/node-card.tspackages/backend/src/dashboard-templates/infrastructure/vercel.test.tspackages/backend/src/services/integrations/IntegrationInventoryService.test.tspackages/backend/src/services/integrations/VercelIntegrationService.test.tspackages/backend/src/services/integrations/VercelIntegrationService.tspackages/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
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
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
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
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
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
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
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
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
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.