Skip to content

feat(gcp): query Google Cloud metrics live with PromQL - #1475

Open
JeremyFunk wants to merge 35 commits into
feat/gcp-infra-pagefrom
feat/gcp-promql-query
Open

JeremyFunk wants to merge 35 commits into
feat/gcp-infra-pagefrom
feat/gcp-promql-query

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Query a customer's Google Cloud Monitoring metrics live, through Google's PromQL-compatible API. Nothing is ingested on this path; the 5-minute poller is unchanged. First of a 3-PR stack: this PR is the backend; the dashboard widget (web) and the MCP tools (apps/ai) build on the contract below.

What it does

  • Targets. A connection is a target once its setup script has reported the read-only account in place (applied_metrics_enabled = true): true with the metrics switch on today, and true for every set-up connection after the sibling access PR. Other connections are left out silently while at least one qualifies. When none does, GcpPromQlFailedError lists each connection with what to do (run the setup script; the guide's section on domain-restricted sharing if a policy blocks it). A project-scope connection is its project. A folder or organization connection is the active projects a live Cloud Asset search finds under it (Project asset type only, up to 2,000 projects, cached 1 h per connection in the edge cache). gcp_resources is not read: only the poller fills it. Projects are deduplicated across connections; overlapping connections read a project through the oldest one.
  • Fan-out. One Google call per metrics scope (form-encoded POST, so long queries don't make long URLs), 4 at a time, each with its connection's read-only account and x-goog-user-project: <host project>. Google evaluates a project URL over that project's whole metrics scope. So each target's scope is looked up first (v1 locations/global/metricsScopes/{project}, cached 1 h per project; needs resourcemanager.projects.get, which Monitoring Viewer holds). A target that another target's scope already holds is not read again; it answers through that scope (widest scopes first). Without projects, more than 20 projects is refused with GcpPromQlTooManyProjectsError, which lists them so the caller can choose. projects must be a subset of the targets. If a connection's project listing failed, a named project that may sit under it fails with GcpPromQlFailedError and the listing failure, not a 400. Project ids are validated (GcpProjectId) before they reach a URL path.
  • Results. A series without project_id gets the project it was read from, so identical label sets from two projects never merge. When the project's metrics scope holds several projects, it gets metrics_scope=<project> instead: an aggregation over that scope is not one project's. Google's own project_id is kept. resultType is Google's: an instant query of a range selector answers a matrix. NaN and ±Inf points are dropped. A scalar answer (1 + 1) comes back as a vector of one series. A string result is refused as an invalid query.
  • Failures. What answered is returned beside a failures list: one entry per project whose query failed, or per connection that could not sign in or list its projects. They use the poller's cause-and-action texts, moved to gcp/explain.ts (the poller's output is byte-identical; its tests pass unchanged). When nothing answers, GcpPromQlFailedError carries the list. Google refusing the query itself (400 bad_data) fails the request with GcpPromQlInvalidQueryError carrying Google's message. Some failures count as that project's alone, with Google's text and "Narrow the query or widen the step": a 422 execution error (too many samples), and a 503 timeout. A 429 says the answer may have passed Google's size limit and to narrow the query. If Maple's own sign-in fails, the request fails with IntegrationsUpstreamError and the cause goes to the log, not to the customer. That covers the key, Google OAuth, and IAM Credentials refusing Maple's project with SERVICE_DISABLED / BILLING_DISABLED / USER_PROJECT_DENIED.
  • Auth. Reuses importServiceAccountKey, fetchMapleAccessToken and impersonateReader. Maple's token and each connection's reader token are kept per isolate and reused for 7 of their 10 minutes, which leaves room for the slowest fan-out. Only settled tokens are kept, never a pending sign-in shared across requests. Readers are acquired one connection at a time before the fan-out, so a cold isolate signs in as Maple once.
  • Caching (edge cache, orgId in every key):
    • query results: key = org, sorted project set, query, step-aligned window or minute-aligned instant. TTL 60 s when the window ends within the last 5 minutes or something failed, else 10 minutes. A request where nothing answered is not cached.
    • metric names: 1 h per project.
    • project lists: 1 h per connection.

Contract for the web widget and MCP tools

Endpoints in IntegrationsApiGroup (/internal/integrations, session auth, any org member):

Endpoint Path Request Response
gcpPromQlQueryRange POST /gcp/promql/query_range GcpPromQlRangeRequest { query, startTime, endTime, stepSeconds, projects? } GcpPromQlResult
gcpPromQlQuery POST /gcp/promql/query GcpPromQlInstantRequest { query, time, projects? } GcpPromQlResult
gcpMetricSearch GET /gcp/metrics/search ?search=&limit=&project= GcpMetricSearchResponse { projectId, metrics }
gcpMetricDescribe GET /gcp/metrics/describe ?type=&project= GcpMetricDescriptor
  • All schemas are in @maple/domain/http (packages/domain/src/http/gcp-promql.ts).
  • Times are TinybirdDateTime (UTC YYYY-MM-DD HH:mm:ss), like the query engine.
  • stepSeconds is a whole number from 60 to 86,400, with at most 11,000 points per series on the step-aligned range Google is sent (Google's limit, refused as a 400). query is trimmed, 1 to 4,000 characters. projects holds 1 to 20 GcpProjectId. limit is 1 to 200 (default 50).
  • GcpPromQlResult: { resultType: "matrix" | "vector", series: [{ labels: Record<string,string>, points: [[epochSeconds, number]] }], projects: GcpProjectId[] /* the ones that answered, directly or through another's metrics scope */, failures: [{ scope, message }] }. scope is projects/<id>, folders/<n> or organizations/<n>. labels carries project_id, or metrics_scope for an aggregation over a multi-project scope.
  • GcpMetricSearchResponse.metrics: PromQL UTF-8 names (each legacy a_b_c:d twin dropped; Prometheus-native names, colons included, are kept), case-insensitive substring match, name order. Distributions appear as _bucket / _count / _sum.
  • GcpMetricDescriptor: { type, metricKind, valueType, unit, description, monitoredResourceTypes, labels: [{ key, description }] }. A _bucket / _count / _sum name resolves to its distribution's descriptor. More than one resource type means a starter query needs a monitored_resource matcher.
  • Discovery reads one project: project if given (it must be one of the targets), else the first target.

Service, for apps/ai: GcpPromQlService (@maple/backend/services/integrations/GcpPromQlService, static readonly layer; needs Database, Env, EdgeCacheService from the app root):

queryRange(orgId, GcpPromQlRangeRequest): Effect<GcpPromQlResult, GcpPromQlError>
queryInstant(orgId, GcpPromQlInstantRequest): Effect<GcpPromQlResult, GcpPromQlError>
searchMetrics(orgId, { search, limit?, project? }): Effect<GcpMetricSearchResponse, GcpPromQlError>
describeMetric(orgId, { type, project? }): Effect<GcpMetricDescriptor, GcpPromQlError | IntegrationsNotFoundError>

Errors (GcpPromQlError):

Error Status When
@maple/http/errors/GcpPromQlUnavailableError 409 MAPLE_GCP_SERVICE_ACCOUNT_KEY is unset
@maple/http/errors/IntegrationsNotConnectedError 409 the org has no connection
@maple/http/errors/IntegrationsValidationError 400 a project outside the targets was named
@maple/http/errors/GcpPromQlInvalidQueryError 400 Google refused the query; message is Google's text, plus projectId
@maple/http/errors/GcpPromQlTooManyProjectsError 400 more than 20 projects and none chosen; projects lists them
@maple/http/errors/GcpPromQlTooManySeriesError 400 more than 500 series across the projects, checked as each project answers, or one project's answer larger than 8 MB (read as a stream and abandoned past the cap). { message }; the message says to aggregate (sum by / avg by), narrow the selector, or widen the step. Never truncated.
@maple/http/errors/GcpPromQlFailedError 409 nothing answered; failures
@maple/http/errors/IntegrationsUpstreamError 502 Maple could not sign in to Google
@maple/http/errors/IntegrationsPersistenceError 503 database

The five new tags are in the regenerated anticipated-errors list.

Decisions and deviations

  • failures entries are { scope, message }, not { projectId, message } (design decision 7). A connection that cannot sign in or list its folder's projects has no single project to name.
  • Two query endpoints (range and instant), mirroring Prometheus, rather than one endpoint with a mode switch.
  • Discovery takes an optional project, so a caller can move off a first project that fails (no billing, say).
  • Descriptor lookups are not cached: they are free, and a widget makes one when a metric is picked.
  • The Google timeout parameter is 18s, so Google answers before the client's existing 20 s request timeout gives up.
  • gcp/api.ts call gained an optional refuse hook, which turns a failed answer into the caller's own typed error. The poller's error behaviour is unchanged.
  • Not handled: Prometheus string results. Only an explicit string literal query produces one, and it fails as an unexpected body.

Known limits, left for later

  • One answer holds at most 500 series across its projects (GCP_PROMQL_MAX_SERIES), checked as each project answers. Each live-query answer (PromQL, metric names, descriptor, metrics scope, live project search) is read up to 8 MB (MAX_BODY_BYTES). A PromQL answer past it is refused with GcpPromQlTooManySeriesError and the rest is left unread; on the other live calls it is a per-project failure. The poller and resource sync read uncapped, as before. With 4 projects in flight that bounds raw bodies at 32 MB per request.
  • Metrics scopes: overlap is resolved one level deep, widest scope first. Two custom scopes that each monitor a different part of the other can still count a project twice.
  • A project reached by two connections is read through the oldest one. If that one can't sign in, the newer one is not tried.
  • A new project under a folder shows up within the hour (the project list is cached 1 h).
  • The isolate's token map has no eviction. It is bounded by the number of connections the isolate serves.
  • GcpPromQlFailedError is retry: never. Its failures mix setup problems, which a retry doesn't fix, with rate limits, which it does. Each failure's message says which.

Env / infra

  • MAPLE_GCP_SERVICE_ACCOUNT_KEY is now bound on the api Worker (optionalSecret, the same way as on alerting), for prd and previews. Infisical dev already has it. apps/ai is not touched; the MCP PR binds it there.

Not verified without a live system

  • The decoders were checked against live answers from a development project (matrix, vector, scalar, bad_data 400, label/__name__/values, v3 metricDescriptors). The tests use anonymised versions of those shapes.
  • Not exercised live: the impersonated reader path end to end, the Cloud Asset project search for a folder, and the edge cache on Workers.
  • Smoke after deploy, as a member of an org with a connection:
    1. POST /internal/integrations/gcp/promql/query_range with sum by (service) (increase({"serviceruntime.googleapis.com/api/request_count", monitored_resource="consumed_api"}[10m])) and step 600 over 2 h: series carry project_id.
    2. The same query without the monitored_resource matcher: 400 GcpPromQlInvalidQueryError with Google's text.
    3. GET /internal/integrations/gcp/metrics/search?search=run.googleapis and GET .../gcp/metrics/describe?type=run.googleapis.com/request_latencies_bucket.
    4. A connection whose setup script has not run since the change in the sibling PR: its failure entry asks for the script to be run again.

@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e763df23-c635-44a1-88c5-2cea9d094ba1

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
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Held at 8 because 1 reviewed file was not read.
quality 100/100 · no findings · tests covered · risk medium · 5/5 new units observable · 1 file not read

Adds live Google Cloud PromQL: four session-authed internal endpoints, one Google call per target project with per-connection reader tokens, edge-cached results, and the poller's cause texts moved into a shared gcp/explain.ts. The test files were read in the sandbox, not as diffs; no defects found, safe to merge once the api app has the service-account key.

  • GcpPromQlService fans a PromQL query out over the org's Google projects
  • Google cause texts move to gcp/explain.ts, now picked by API name
  • PromQL requests add timeout=18s; call decodes outside the timeout
  • Four endpoints added to the internal IntegrationsApiGroup

Before merge

  • Secret · Set MAPLE_GCP_SERVICE_ACCOUNT_KEY in the api app's secret store (prd, dev, and preview if previews query live), reusing the alerting worker's existing value · apps/api/src/resources/env.ts:84

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:306
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:396
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:413
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:442
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:474
What was checked
  • Poller sentences unchanged: describeGcpApiError in non-live mode emits the same three fixed texts (gcp/explain.ts:301-441)
  • Cache keys all carry orgId, and named projects are checked against the target map before any Google call (GcpPromQlService.ts:291,383)
  • PROMQL_TIMEOUT 18s < REQUEST_TIMEOUT 20s and token reuse 7 min < 10 min lifetime (gcp/api.ts:30,402)
Observability coverage: 5 of 5 changes observable
Change Kind Observable Evidence
POST /gcp/promql/query_range handler inbound yes Effect.fn("GcpPromQlService.queryRange") span with maple.gcp.projects (GcpPromQlService.ts:396,306)
POST /gcp/promql/query handler inbound yes Effect.fn("GcpPromQlService.queryInstant") (GcpPromQlService.ts:413)
GET /gcp/metrics/search handler inbound yes Effect.fn("GcpPromQlService.searchMetrics") (GcpPromQlService.ts:442)
GET /gcp/metrics/describe handler inbound yes Effect.fn("GcpPromQlService.describeMetric") (GcpPromQlService.ts:474)
Google Cloud Monitoring PromQL / metric-list / descriptor calls outbound yes peer.service: google-cloud annotated in call on the HTTP client span (gcp/api.ts:471); x-goog-user-project set per reader (gcp/api.ts:266)
Files not reviewed (1)

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

  • packages/backend/src/services/integrations/GcpPromQlService.test.ts

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
No defect found in the fan-out, caps or tenant scoping, all pinned by the 972-line suite; the widest-metrics-scope covering logic in runQuery is the part worth a careful read.
quality 100/100 · no findings · tests covered · risk medium · 4/4 new units observable

Adds GcpPromQlService and four internal endpoints that read a customer's Cloud Monitoring metrics live through Google's PromQL API: one call per metrics scope, per-connection readers, a capped answer, and what failed returned beside what answered. Nothing is stored and the poller's path is untouched, so it is safe to merge.

  • GcpPromQlService reads live PromQL per metrics scope with each connection's reader, deduplicating overlapping scopes
  • Four GcpPromQl* endpoints added under /internal/integrations/gcp/*
  • Answers past 500 series or 8 MB per project are refused, never truncated
  • explain.ts moves the poller's cause/action texts out and adds the live-query variants

Before merge

  • Secret · Add MAPLE_GCP_SERVICE_ACCOUNT_KEY (the Google service-account key) to the prd and dev secret stores; unset answers live queries as GcpPromQlUnavailableError · apps/api/src/resources/env.ts:84

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Tenant scoping: projects come only from the org's own connectors (GcpPromQlService.ts:244) and every cache key leads with orgId (:277, :382, :482)
  • The four new endpoints sit inside the group's SessionAuthorization middleware (packages/domain/src/http/integrations.ts:1521)
  • Poller output unchanged: impersonateReader no longer strips reason, and gcp/explain.ts:22 drops it for IAM Credentials
Observability coverage: 4 of 4 changes observable
Change Kind Observable Evidence
POST /internal/integrations/gcp/promql/query_range and /promql/query http-inbound yes HttpApiBuilder group endpoints (integrations.http.ts:477-486) onto Effect.fn("GcpPromQlService.queryRange") / queryInstant (GcpPromQlService.ts:495, :510)
GET /internal/integrations/gcp/metrics/search and /metrics/describe http-inbound yes Effect.fn("GcpPromQlService.searchMetrics") / describeMetric (GcpPromQlService.ts:542, :574)
Google Cloud calls (PromQL, metrics scope, project listing, descriptors, tokens) http-outbound yes HttpClient span per request plus Effect.annotateSpans("peer.service", "google-cloud") (gcp/api.ts:198)
Postgres read of the org's GCP connectors db yes makeDbExecute(database, "GcpPromQlService", ...) (GcpPromQlService.ts:144) around the PG.from(GcpConnectors) select

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 7/10 · likely safe to merge
The fan-out, caching and poller-text refactor all check out, but searchMetrics/describeMetric and the capped-read error path are thinly exercised by tests.
quality 90/100 · 1 warning · tests partial · risk medium · 4/4 new units observable

Adds live Google Cloud PromQL querying (range, instant, metric search/describe) to the internal integrations API, one Cloud Monitoring call per metrics scope with per-connection read-only tokens, results cached and failures reported beside answers. One misleading error path; otherwise safe to merge.

  • GcpPromQlService fans out one PromQL call per metrics scope, four at a time
  • Scopes and query answers are cached (1 h per scope, 60/600 s per query)
  • call gains an 8 MB body cap; the poller's pages stay uncapped
  • The poller's error texts move to gcp/explain.ts unchanged

Before merge

  • Secret · Set MAPLE_GCP_SERVICE_ACCOUNT_KEY in the prd and dev secret stores; live queries answer gcp_promql_unavailable without it · apps/api/src/resources/env.ts:84

Findings

🟠 Warning · F1 · call reports a non-JSON body as larger than 8 MB

correctness · packages/backend/src/services/integrations/gcp/api.ts:163-180

On a capped call body === undefined is the only signal for the cap, but parseJson also yields None on a body that is not JSON (an intermediary's HTML error page, an empty body). A project answering an HTML 502 makes promQlQuery raise GcpPromQlTooManySeriesError, telling the user to aggregate or widen the step, and the discovery calls answer "Cloud Monitoring answered with more than 8 MB" for a response that was small.

Keep the overflow in a flag (`const overflowed = size > MAX_BODY_BYTES`) and branch on that: when the read did not overflow but `parseJson` returns `None`, fail with `failed("answered with an unexpected body")` as before, and only the overflow path should produce the caller's `tooLarge`.
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit 7be72a24c900b02a31272cb9ec13eb7b72d03267. 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 · correctness · packages/backend/src/services/integrations/gcp/api.ts:163-180
`call` reports a non-JSON body as larger than 8 MB
On a capped call `body === undefined` is the only signal for the cap, but `parseJson` also yields `None` on a body that is not JSON (an intermediary's HTML error page, an empty body). A project answering an HTML 502 makes `promQlQuery` raise `GcpPromQlTooManySeriesError`, telling the user to aggregate or widen the step, and the discovery calls answer "Cloud Monitoring answered with more than 8 MB" for a response that was small.
Suggested fix: Keep the overflow in a flag (`const overflowed = size > MAX_BODY_BYTES`) and branch on that: when the read did not overflow but `parseJson` returns `None`, fail with `failed("answered with an unexpected body")` as before, and only the overflow path should produce the caller's `tooLarge`.

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Poller error strings are byte-identical (gcp/explain.ts:24-58 against the removed explainApiError)
  • Every connector read filters OrgId (GcpPromQlService.ts:244); only org-scoped projects reach a Google URL
  • The MAX_BODY_BYTES cap never reaches the poller's pages (api.ts:407, pinned by api.test.ts:15)
Observability coverage: 4 of 4 changes observable
Change Kind Observable Evidence
POST /internal/integrations/gcp/promql/query_range and /query Server yes handlers on the traced IntegrationsApiGroup (integrations.http.ts:478), service methods wrapped in Effect.fn("GcpPromQlService.queryRange")
GET gcp/metrics/search and gcp/metrics/describe Server yes integrations.http.ts:488; Effect.fn("GcpPromQlService.searchMetrics") / describeMetric
Cloud Monitoring, Cloud Asset and IAM Credentials calls Client yes call annotates peer.service=google-cloud (api.ts:198) on every request
GcpConnectors read in targetsOf Client yes dbExecute/effect-orm over MAPLE_DB (GcpPromQlService.ts:233)

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

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

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

Comment thread packages/backend/src/services/integrations/gcp/api.ts
@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Every hunk read and the fan-out, tokens, cache isolation and failures are pinned by tests; the open warning on the capped body path is unchanged.
quality 90/100 · 1 warning · tests covered · risk medium · 2/2 new units observable

Adds GcpPromQlService: live PromQL over the org's connected Google Cloud projects, one Google call per metrics scope with each connection's reader, plus four internal routes and metric discovery. The earlier warning on the capped-body path is unchanged.

  • GcpPromQlService fans out one query per project's metrics scope, 4 at a time
  • Live answers refuse past 8 MB or 500 series, never truncated
  • Four internal routes: promql query, query_range, metric search, metric describe
  • Poller error texts moved to gcp/explain.ts

Before merge

  • Secret · Set MAPLE_GCP_SERVICE_ACCOUNT_KEY (base64 key file) in the api deploy's secret stores; unset answers these four routes as unavailable · apps/api/src/resources/env.ts:84

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
gcpPromql HTTP routes (/gcp/promql/query, /gcp/promql/query_range, /gcp/metrics/search, /gcp/metrics/describe) server yes handlers registered in integrations.http.ts; routes are built with HttpApiBuilder like the existing integrations endpoints
Google Cloud Monitoring / Asset Inventory fan-out (PromQL, metrics scopes, metric names, project listing) client yes api.ts annotates every call with peer.service=google-cloud`; the service also names its spans (GcpPromQlService.queryRange) and annotates maple.gcp.projects

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 7/10 · likely safe to merge
The fan-out, tenancy and token reuse are pinned by twenty tests, but the open cap-vs-non-JSON defect is unchanged at this head.
quality 90/100 · 1 warning · tests covered · risk high · 2/2 new units observable

Adds live PromQL over an org's connected GCP projects: per-connection readers, metrics-scope fan-out with caching, failure lists, and metric search/describe over four internal routes. Not safe to merge as is — the one open defect from the earlier review is unchanged at this head.

  • GcpPromQlService fans one Google query out per metrics scope, 4 at a time, cached per org
  • call gains a capped read path (8 MB) and per-query refuse handling in gcp/api.ts
  • gcp/explain.ts replaces the poller's local describeApiError with describeGcpApiError
  • Adds GcpPromQlRangeRequest/GcpPromQlInstantRequest and five PromQL error classes

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • F1 is still present: on a capped call a non-JSON body reaches tooLarge (gcp/api.ts:163-179)
  • explain.ts reproduces the removed describeApiError wording, including the other/7 and IAM Credentials branches
  • Org scoping: targetsOf filters orgId and every edge-cache key is prefixed with it (GcpPromQlService.ts:244,277,382,482,552)
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
GET /internal/integrations/gcp/metrics/search and /describe, POST /gcp/promql/query and /query_range inbound yes handlers apps/api/src/routes/internal/integrations.http.ts:478-497; service spans Effect.fn("GcpPromQlService.queryRange") GcpPromQlService.ts:495
Google Cloud Monitoring, IAM Credentials and Cloud Asset calls outbound yes peer.service annotated in gcp/api.ts:198, one untraced HTTP client span per request

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
Fan-out, caps and refusals are pinned by GcpPromQlService.test.ts, and F1 does not reproduce at api.ts:166.
quality 100/100 · no findings · tests covered · risk medium · 2/2 new units observable

Adds a live PromQL read path for Google Cloud connections: four internal endpoints, one Google call per metrics scope fanned out 4 at a time, per-project failures returned beside the answers, and an 8 MB/500-series cap. The new logic is tested and tenant-scoped; safe to merge.

  • Four internal GCP endpoints: PromQL query, query_range, metric search, metric describe
  • GcpPromQlService fans out per metrics scope, 4 at a time, cached per isolate and edge cache
  • Answers past 8 MB or 500 series are refused, never truncated
  • Poller failure texts move to gcp/explain.ts with a live variant

Before merge

Fixed since the last review

  • ✅ F1 · call reports a non-JSON body as larger than 8 MB

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • F1 does not reproduce: a non-JSON body yields null at api.ts:166, never the cap's undefined (api.ts:176)
  • projects must be a subset of the org's targets, and every read resolves through that org's connectors
  • The poller's wording is unchanged by describeGcpApiError for Cloud Monitoring and Cloud Asset reasons (live=false keeps RETRIES)
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
GCP PromQL/metric endpoints (gcpPromQlQueryRange, gcpPromQlQuery, gcpMetricSearch, gcpMetricDescribe) server yes Handlers join the existing HttpIntegrationsLive group, whose routes apps/api traces through ingest self-tracing; no span in the handler itself
Outbound Google calls: PromQL query, metrics scope, metric names, descriptors, Cloud Asset project search client yes queryRange/queryInstant/searchMetrics/describeMetric are Effect.fn spans, and call annotates them peer.service=google-cloud (api.ts:198)

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 9/10 · safe to merge
The delta since the last review is one CI budget constant and its comment, which follows the file's documented per-PR convention.
quality 100/100 · no findings · tests not needed · risk low

Raises the web startup bundle budget to 701.5 KB for the PromQL query contract, on top of the GCP PromQL backend work reviewed earlier. The delta is a single constant with the required justification comment; safe to merge.

Before merge

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Budget raise carries the file's required per-PR justification comment, matching every prior entry (check-bundle-budget.ts:110)
  • The lazy-only startup guard and the over-budget failure path are untouched (check-bundle-budget.ts:119-146)

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Fan-out, dedup, token reuse and failure accounting are pinned by the service test; the one defect left is the undefined body sentinel in gcp/api.ts:163.
quality 90/100 · 1 warning · tests covered · risk medium · 3/3 new units observable

Adds a live Google Cloud PromQL path — range, instant, metric search, descriptor — behind the internal integrations routes, with per-metrics-scope fan-out, 1 h caching and typed failures. The fan-out, dedup and failure accounting read correctly; one error-reporting conflation in gcp/api.ts remains.

  • GcpPromQlService reads each metrics scope once and answers covered targets through it
  • New POST /gcp/promql/query_range, /gcp/promql/query and GET /gcp/metrics/search routes
  • call gains a capped read with a tooLarge hook; poller failure texts move to gcp/explain.ts

Before merge

Findings

🟠 Warning · F2 · undefined body still means both "over the cap" and "did not parse"

correctness · packages/backend/src/services/integrations/gcp/api.ts:163-166

Option.getOrNull(parseJson(...)) yields undefined when the body does not parse, which is the same value the over-cap exit produces, so the body === undefined branch at line 176 cannot tell them apart: a small non-JSON answer (an empty body, or Google's HTML 429/503) makes a PromQL read fail as GcpPromQlTooManySeriesError and every other capped call fail as answered with more than 8 MB, instead of reporting Google's status through refuse/errorReason, and a project's real failure is listed to the caller as a size refusal it never hit. Keep a body that did not parse distinct from one past MAX_BODY_BYTES so only the latter takes that branch.

		const parsed = Option.getOrNull(parseJson(Buffer.concat(chunks).toString("utf8")))
		// Only a body past the cap is `undefined`; one that did not parse stays `null`, so the
		// status branch below reports Google's answer instead of a size that was not exceeded.
		const body = size > MAX_BODY_BYTES ? undefined : (parsed ?? null)
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit 0977b2b6c817d43617b4a69ba30ddfc2562ca25f. 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.

---

F2 · Warning · correctness · packages/backend/src/services/integrations/gcp/api.ts:163-166
`undefined` body still means both "over the cap" and "did not parse"
`Option.getOrNull(parseJson(...))` yields `undefined` when the body does not parse, which is the same value the over-cap exit produces, so the `body === undefined` branch at line 176 cannot tell them apart: a small non-JSON answer (an empty body, or Google's HTML 429/503) makes a PromQL read fail as `GcpPromQlTooManySeriesError` and every other capped call fail as `answered with more than 8 MB`, instead of reporting Google's status through `refuse`/`errorReason`, and a project's real failure is listed to the caller as a size refusal it never hit. Keep a body that did not parse distinct from one past `MAX_BODY_BYTES` so only the latter takes that branch.
Replace those lines with:
		const parsed = Option.getOrNull(parseJson(Buffer.concat(chunks).toString("utf8")))
		// Only a body past the cap is `undefined`; one that did not parse stays `null`, so the
		// status branch below reports Google's answer instead of a size that was not exceeded.
		const body = size > MAX_BODY_BYTES ? undefined : (parsed ?? null)

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Scope dedup seeds covered from monitored plus the scope's own number, widest scope first (GcpPromQlService.ts:362)
  • metrics_scope is set only when the read's scope holds several projects, so two projects never merge (GcpPromQlService.ts:470)
  • Project ids reaching URL paths come from the connector's target set and isProjectId, never raw request text (GcpPromQlService.ts:440)
Observability coverage: 3 of 3 changes observable
Change Kind Observable Evidence
POST /gcp/promql/query_range, POST /gcp/promql/query, GET /gcp/metrics/search server yes handler spans from Effect.fn on queryRange/queryInstant/searchMetrics (GcpPromQlService.ts:495,510,542,574)
Google Monitoring / Cloud Asset calls from call client yes traced httpClient with annotateCurrentSpan({ "peer.service": ... }) (gcp/api.ts:198)
GcpConnectors read per request client yes dbExecute inside the request's Effect.fn span (packages/backend/src/platform/db-execute.ts:67)

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

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

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

Comment thread packages/backend/src/services/integrations/gcp/api.ts
# Conflicts:
#	apps/web/perf/check-bundle-budget.ts
@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 7/10 · likely safe to merge
The head diff is one budget constant, but the open undefined-body conflation at gcp/api.ts:163-179 is still present and unfixed.
quality 90/100 · 1 warning · tests not needed · risk low

The change since the last review only raises the web startup gzip budget to 706.5 KB for the new PromQL query contract; the constant and its comment agree with the CI measurements. The earlier call conflation still stands, so the PR should not merge until that is fixed.

  • maxGzipBytes in the web perf budget rises from 705.5 KB to 706.5 KB

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Budget comment and constant agree: 702.5 KB PromQL / 706.5 KB with feat(infra): Infrastructure → Google Cloud page #1336 merged (apps/web/perf/check-bundle-budget.ts:111-114)
  • body === undefined at gcp/api.ts:163-179 still cannot tell a non-JSON body from an over-cap one, so F2 stays open

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
The code under the open finding is unchanged since the earlier review and Option.getOrNull returns null, so the cap sentinel is sound; the four routes read only by org-scoped keys.
quality 100/100 · no findings · tests covered · risk medium · 3/3 new units observable

Backend for live PromQL over connected Google Cloud projects: a fan-out per metrics scope with each connection's reader token, four new internal endpoints, and failure texts shared with the poller. Safe to merge.

  • GcpPromQlService reads one PromQL call per metrics scope, 4 at a time, with per-connection reader tokens
  • Four internal endpoints: /gcp/promql/query, /gcp/promql/query_range, /gcp/metrics/search, /gcp/metrics/describe
  • gcp/explain.ts holds the poller's cause-and-action texts, now shared by live queries
  • Live-answer bodies capped at 8 MB; the poller's reads stay uncapped

Before merge

Fixed since the last review

  • ✅ F2 · undefined body still means both "over the cap" and "did not parse"

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Option.getOrNull yields null (see packages/backend/src/queries/query-runner.ts wrapping it in Schema.NullOr), so a non-JSON body is not reported as over the 8 MB cap
  • Tenant scoping: the connector query filters orgId and every cache key is prefixed with it (GcpPromQlService.ts:244, :277)
  • Project ids are validated (GcpProjectId) before they reach a URL path or the Cloud Asset scope (GcpPromQlService.ts:211), and GcpMetricType admits no quote for the descriptor filter
Observability coverage: 3 of 3 changes observable
Change Kind Observable Evidence
HTTP entrypoints gcpPromQlQuery / gcpPromQlQueryRange / gcpMetricSearch / gcpMetricDescribe inbound yes service methods are Effect.fn("GcpPromQlService.*") spans annotating orgId; routes sit in the traced internal HTTP group like gcpResources
Outbound Google Cloud Monitoring and Cloud Asset calls outbound yes gcp/api.ts call annotates peer.service=google-cloud on the effect/http client span
GcpConnectors read for the target list db yes goes through makeDbExecute(database, "GcpPromQlService", ...)

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

# Conflicts:
#	apps/web/perf/check-bundle-budget.ts
@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
One info finding on the poller's IAM Credentials text; the PromQL fan-out, its caps, tenancy and cache keys were read and are tested.
quality 98/100 · 1 note · tests partial · risk medium · 2/2 new units observable

Adds live PromQL over a customer's Google Cloud metrics: a per-org fan-out service, four internal endpoints, and the poller's failure texts moved to gcp/explain.ts. The fan-out is capped and tenant-scoped throughout; one poller message now misattributes a Maple-side failure to the customer.

  • GcpPromQlService queries Google's Prometheus-compatible API once per metrics scope, each project read with its connection's reader
  • Four internal endpoints expose range/instant queries, metric search and metric describe
  • gcp/explain.ts moves the poller's texts and adds a live variant
  • MAPLE_GCP_SERVICE_ACCOUNT_KEY gates the feature; unset answers GcpPromQlUnavailableError

Before merge

  • Secret · Add MAPLE_GCP_SERVICE_ACCOUNT_KEY (the Maple service-account JSON key) to the Infisical prd and dev secret stores, or every live query answers GcpPromQlUnavailableError · apps/api/src/resources/env.ts:86

Findings

🔵 Note · F3 · Poller shows a connection problem for Maple-side IAM Credentials failures

correctness · packages/backend/src/services/integrations/gcp/explain.ts:22

hostReason drops the reason for IAM Credentials, so an IAM Credentials 403 whose reason is SERVICE_DISABLED or BILLING_DISABLED now falls through to the granted-account text: the poller's lastMetricsError tells the customer the account or grant is gone and to run the setup script again, when the disabled API or billing is in Maple's own project (impersonateReader sets no x-goog-user-project, and MAPLE_SIDE_REASONS in GcpPromQlService.ts:66 treats exactly these reasons as Maple's). Nothing the customer runs can clear it. Give these reasons their own text, as the live path already does by failing with IntegrationsUpstreamError.

Add a branch before the `denied` one, e.g. `if (error.api === "IAM Credentials" && MAPLE_SIDE_REASONS.has(error.reason ?? "")) return "Google refused Maple's own sign-in: the API is switched off, or billing is off, in the project Maple signs in from. This is a Maple-side problem; write to support@maple.dev."
🤖 Prompt to fix this finding with an AI agent
Findings from an automated review of commit e9d3883aa3a67e960d3b5e98810a2c7eeb567724. 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.

---

F3 · Note · correctness · packages/backend/src/services/integrations/gcp/explain.ts:22
Poller shows a connection problem for Maple-side `IAM Credentials` failures
`hostReason` drops the reason for `IAM Credentials`, so an `IAM Credentials` 403 whose reason is `SERVICE_DISABLED` or `BILLING_DISABLED` now falls through to the granted-account text: the poller's `lastMetricsError` tells the customer the account or grant is gone and to run the setup script again, when the disabled API or billing is in Maple's own project (`impersonateReader` sets no `x-goog-user-project`, and `MAPLE_SIDE_REASONS` in `GcpPromQlService.ts:66` treats exactly these reasons as Maple's). Nothing the customer runs can clear it. Give these reasons their own text, as the live path already does by failing with `IntegrationsUpstreamError`.
Suggested fix: Add a branch before the `denied` one, e.g. `if (error.api === "IAM Credentials" && MAPLE_SIDE_REASONS.has(error.reason ?? "")) return "Google refused Maple's own sign-in: the API is switched off, or billing is off, in the project Maple signs in from. This is a Maple-side problem; write to support@maple.dev."

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • PromQlProjects caps projects at 20 (gcp-promql.ts:45), so naming projects cannot bypass GCP_PROMQL_MAX_PROJECTS
  • A project connector cannot carry another host (v2/integrations-gcp.ts:176), so projects: [connector.projectId] reads the connected project
  • type reaches the Cloud Monitoring filter only through GcpMetricType (integrations.ts:1379)
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
Per-project PromQL / metrics-scope / metric-name / descriptor calls to Google outbound HTTP yes call annotates peer.service=google-cloud (gcp/api.ts:198) and the HTTP client records one span per request
gcpPromQlQueryRange, gcpPromQlQuery, gcpMetricSearch, gcpMetricDescribe inbound HTTP endpoint yes every sibling handler in HttpIntegrationsLive has no route-local span; the API self-traces through ingest

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Org scoping, per-connection auth, the 8 MB cap and scope dedup are read and pinned by tests; the open IAM Credentials finding in explain.ts is byte-identical to the reviewed head.
quality 98/100 · 1 note · tests covered · risk medium · 4/4 new units observable

Adds live Google Cloud PromQL: GcpPromQlService reads one call per metrics scope across an organization's connections, behind four new endpoints, capped at 8 MB and 500 series. The poller's reads, its error texts and the poller itself are unchanged.

  • GcpPromQlService fans one PromQL query per metrics scope, each with its connection's reader
  • Four session-scoped endpoints added: /gcp/promql/query, /query_range, /gcp/metrics/search, /describe
  • call in gcp/api.ts streams live-query answers up to 8 MB and passes Google's PromQL errors on
  • The poller's cause-and-action texts move verbatim to gcp/explain.ts

Before merge

  • Secret · Set MAPLE_GCP_SERVICE_ACCOUNT_KEY in the api app's dev and prd secret stores: live queries answer 409 without it · apps/api/src/resources/env.ts:86

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Tenancy: targetsOf filters orgId and every edge-cache key is org-prefixed (GcpPromQlService.ts:244, :277, :382)
  • Injection: GcpProjectId and GcpMetricType gate every value reaching a URL path or the descriptor filter
  • The poller's reads stay uncapped while live reads are capped, pinned by gcp/api.test.ts
Observability coverage: 4 of 4 changes observable
Change Kind Observable Evidence
gcpPromQlQueryRange / gcpPromQlQuery / gcpMetricSearch / gcpMetricDescribe inbound endpoint yes HttpApiBuilder group handlers; the repo's http api layer auto-instruments the group, as the sibling gcpResources handler relies on
Google calls: promQlQuery, getMetricsScope, listPromQlMetricNames, getMetricDescriptor, searchProjectsPage outbound HTTP yes call annotates peer.service: google-cloud (gcp/api.ts:198) and the effect HTTP client emits a span per request
Tagged failures (GcpPromQlInvalidQueryError, TooManyProjects, TooManySeries, Failed, Unavailable) error surface yes rendered-failure.ts annotates error.type/error.message for HttpTaggedError responses
Edge-cache reads for project lists, metrics scopes, metric names and query answers cache yes cache.hit/cache.bucket are live span attributes and getOrCompute reports them per lookup

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
The changed-since-last-review files are base-branch content; the PR's backend hunks are unchanged since 233ba63 and were not re-read here.
quality 98/100 · 1 note · tests not needed · risk low

The only delta since the previous review is the base branch merged in: the six web files at this head are byte-identical to feat/gcp-infra-page, and pr_file_diff reports the PR does not change them. No new defect found; the open explain.ts finding still stands.

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • git diff b5a69fd..a58c95b lists only the 15 PR files; the card, lab, registry and route-tree files come from b5a69fd itself
  • routeTree.gen.ts, routes/lab/gcp-connections.tsx and the registry.ts entry at line 72 agree on /lab/gcp-connections
  • packages/backend/src/services/integrations/gcp/explain.ts is unchanged since 233ba63, so F3's IAM Credentials branch is still present

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Fan-out, caps and label rules are pinned by fetch-stubbed tests; the poller's Maple-side IAM message (F3) is unchanged at this head.
quality 98/100 · 1 note · tests covered · risk medium · 4/4 new units observable

Adds the backend live PromQL path: connection and project discovery, a 4-way Google fan-out with per-project failures returned beside the series, and edge-cached results. Safe to merge; the only outstanding item is the already-filed poller message.

  • queryRange/queryInstant read one Google call per metrics scope, four at a time
  • Projects are discovered per connection, deduplicated, and scope-covered targets are not read twice
  • Results cached per org and query at 60 s or 600 s, failures beside the series
  • Cause-and-action texts moved to gcp/explain.ts, split into live and poller wording

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Reviewed the 15 files this PR changes; the web paths the kickoff lists are base-branch merges, not this diff
  • Every new query filters OrgId and each read uses its own connector's reader (GcpPromQlService.ts:244)
  • 8 MB body cap, 500-series cap and scope-covering rule are pinned by fetch-stubbed tests (GcpPromQlService.test.ts:640, :675)
Observability coverage: 4 of 4 changes observable
Change Kind Observable Evidence
HTTP routes gcpPromQlRange / gcpPromQlInstant / gcpMetricSearch / gcpMetricDescribe Server yes Handled by HttpIntegrationsLive in apps/api under the existing HTTP tracing middleware
Google calls: PromQL query, impersonation, metricsScopes, metric names and descriptors Client yes call annotates peer.service=google-cloud and httpClient.execute emits the client span (gcp/api.ts)
Edge-cache reads (gcp-projects, gcp-metrics-scopes, gcp-promql, gcp-metric-names) Client yes getOrCompute emits spans carrying cache.read_status (lib/cache/src/edge-cache.ts)
Postgres read of gcp_connectors per request Database yes dbExecute / makeDbExecute wraps the org-filtered select (GcpPromQlService.ts:233)

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 7/10 · likely safe to merge
F3 is unfixed at this head, and I did not re-read the rest of GcpPromQlService, the domain schema or the internal route, which earlier passes covered.
quality 98/100 · 1 note · tests covered · risk medium · 1/1 new units observable

This PR adds the backend for live Google Cloud PromQL queries: an org-scoped fan-out service, the GCP API client, shared error explanations and the internal route. Nothing changed in it since the last review except base-branch merges, and the one open finding still stands.

  • GcpPromQlService signs in per connection and queries Google PromQL once per metrics scope
  • explainGcpApiError moved out of the poller and now takes a live flag

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • F3 at the head: a poller IAM Credentials denial still reaches the connection text (explain.ts:33-45), so it stays open
  • The two files listed as changed since fad4902 are base-branch merges: git diff fad4902 e674226 shows them, pr_changed_files does not list them
  • The dialog's new cap of 20 matches the schema (packages/domain/src/query-engine.ts:486)
Observability coverage: 1 of 1 changes observable
Change Kind Observable Evidence
Google Cloud PromQL / Monitoring calls from GcpPromQlService outbound yes packages/backend/src/services/integrations/gcp/api.ts:198 sets peer.service=google-cloud on every call; the file header documents the HTTP client span per request

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
Nothing changed since the last review; the open poller finding at explain.ts:22 still stands, a Maple-side IAM failure is recorded against the connection.
quality 98/100 · 1 note · tests covered · risk medium · 3/3 new units observable

Adds live Google Cloud PromQL querying to the API: org-scoped targets built from GCP connections, one Google call per metrics scope with cached tokens, and typed errors for invalid, oversized or unreachable queries. Backend-only and safe to merge; the poller still records Maple-side sign-in failures against the connection.

  • GcpPromQlService reads a metrics scope per project, four Google calls at a time
  • New org-scoped v1 endpoints: range, instant, metric search, metric describe
  • Maple's and each connection's reader tokens are cached per isolate for 7 minutes
  • Answers ride beside a failures list; nothing answering is GcpPromQlFailedError

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Files changed since the last review are base-branch merges; pr_file_diff reports 0 diffs for them
  • MAPLE_GCP_SERVICE_ACCOUNT_KEY is a new optional secret, unset answers unavailable (env.ts:86)
  • Caches are org-scoped and no-store, pinned by a cross-org test (GcpPromQlService.test.ts:596)
Observability coverage: 3 of 3 changes observable
Change Kind Observable Evidence
gcp/promql/queryRange and queryInstant endpoints Server yes handlers call gcp.queryRange/queryInstant, which open Effect.fn("GcpPromQlService.queryRange") spans (GcpPromQlService.ts:487)
Google Monitoring PromQL POST per metrics scope Client yes Effect.withSpan("GcpPromQlService.promQlQuery", { kind: "client" }) with server.address (GcpPromQlService.ts:139)
Google OAuth mint, reader impersonation, Cloud Asset project search Client yes spans on mapleAccessToken, readerFor and describeMetric (api.ts:70, api.ts:189, GcpPromQlService.ts:172)

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 7/10 · likely safe to merge
The head differs from the reviewed commit only by a base-branch merge; the open F3 in explain.ts is unchanged and still stands.
quality 98/100 · 1 note · tests covered · risk medium · 2/2 new units observable

Live PromQL over a customer's connected Google Cloud projects: GcpPromQlService fans one query out per metrics scope with each connection's read-only account, and four internal routes expose it. Nothing in the pull request's own diff changed since the last review; one earlier finding in explain.ts is still open.

  • GcpPromQlService reads Google Cloud metrics live, one call per metrics scope
  • Four internal routes (gcpPromQlQueryRange, gcpPromQlQuery, gcpMetricSearch, gcpMetricDescribe) added
  • call gained capped reads and PromQL error refusal; explain.ts now owns the poller's error text

Before merge

  • Secret · Add MAPLE_GCP_SERVICE_ACCOUNT_KEY to the api secret stores (prd and dev); unset answers live queries as unavailable · apps/api/src/resources/env.ts:86

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • explain.ts is byte-identical between the reviewed commit 6a665cc and this head (git diff 6a665cc 3a5537f); only a base-branch web route file differs and it is not in this PR's diff
  • Every connector read filters the org (GcpPromQlService.ts:244), and each edge-cache key is prefixed with orgId
  • Project ids are schema-validated (isProjectId, GcpProjectId) before reaching a URL path or the asset filter
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
Internal HTTP routes gcpPromQlQueryRange / gcpPromQlQuery / gcpMetricSearch / gcpMetricDescribe inbound entrypoint yes Each handler delegates to an Effect.fn("GcpPromQlService.*") span (GcpPromQlService.ts:495, 510, 542, 574)
Google Cloud Monitoring / IAM / Asset calls (promQlQuery, getMetricsScope, searchProjectsPage, listPromQlMetricNames, getMetricDescriptor) outbound HTTP yes call annotates peer.service=google-cloud and the HTTP client records a span per request (gcp/api.ts:198)

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

@maple-review-bot

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

Copy link
Copy Markdown

Maple review

🟢 Confidence 8/10 · likely safe to merge
explain.ts is untouched since the last review, so F3's wrong poller text stands; the two web files the kickoff names are not in this diff.
quality 98/100 · 1 note · tests covered · risk medium · 2/2 new units observable

Adds live Google Cloud PromQL: tenant-scoped internal endpoints, a service that fans out one Google call per metrics scope with each connection's read-only account, and the request/result contract. Nothing new is filed; F3 stays open at this head.

  • GcpPromQlService reads each connection's projects live, one Google call per metrics scope
  • Four internal /gcp/promql/* and /gcp/metrics/* endpoints over the new org-scoped contract
  • Poller error texts move to gcp/explain.ts, gaining a live-query variant
  • Answers cache per org and refuse more than 500 series or an 8 MB project answer

Before merge

Still open from earlier reviews

Production impact

Telemetry this change adds and removes (5)
  • ➕ attribute maple.gcp.projects · packages/backend/src/services/integrations/GcpPromQlService.ts:351
  • ➕ span name GcpPromQlService.queryRange · packages/backend/src/services/integrations/GcpPromQlService.ts:495
  • ➕ span name GcpPromQlService.queryInstant · packages/backend/src/services/integrations/GcpPromQlService.ts:510
  • ➕ span name GcpPromQlService.searchMetrics · packages/backend/src/services/integrations/GcpPromQlService.ts:542
  • ➕ span name GcpPromQlService.describeMetric · packages/backend/src/services/integrations/GcpPromQlService.ts:574
What was checked
  • Over-cap is the only way body is undefined on a capped call (api.ts:163-167), so F1/F2 are fixed
  • Every connector read filters OrgId and cache keys carry orgId; a test covers cross-org isolation
  • GcpMetricType refuses quotes and backslashes before the descriptor filter is built
Observability coverage: 2 of 2 changes observable
Change Kind Observable Evidence
gcpPromQlQueryRange/Query and gcpMetricSearch/Describe HTTP endpoints server yes apps/api registers HttpMiddleware.TracerDisabledWhen, so HttpMiddleware.tracer spans every route; handlers delegate to Effect.fn("GcpPromQlService.*") spans that annotate orgId
Google Cloud Monitoring, Metrics Scope and Cloud Asset calls client yes call in gcp/api.ts runs through the traced HttpClient and annotates peer.service=google-cloud (api.ts:198)

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

@maple-review-bot

Copy link
Copy Markdown

Note

⏳ Maple is reviewing this pull request at a011fce. This comment updates with the review when it finishes.

This branch has not been deployed

No deployments
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.

1 participant