feat(scraper): fetch targets directly, delete the API-side scrape proxy - #750
Merged
Conversation
Every upstream failure of a scrape target (PlanetScale metrics.psdb.cloud 502/504s, 29s timeouts) surfaced as a maple-api 5xx on GET /api/internal/prometheus-scrape and paged the org-wide error-rate rule for maple-api as well as scraper, while each branch scrape parked a Worker invocation for up to 29s waiting on the target. The scraper now fetches each target itself: - lib/safe-fetch (@maple/safe-fetch): the SSRF-safe fetch + URL validator moved out of apps/api unchanged so both apps share one guard. - InternalScrapeTarget carries targetType, scrapeUrl (the signed PlanetScale branch URL, otherwise the row url) and authHeaders, decrypted API-side so the master key and the OAuth grant never leave the API. - apps/scraper TargetFetcher does the GET with the same timeout formula, Retry-After parsing and span attributes the proxy had (client span scraper.fetch_target; host and path only, never the signed query). - scrapeUrl/authHeaders are outside the scheduler's loop fingerprint; each scrape reads the latest reconciled copy, so a rotated signature or credential is picked up without restarting the loop. - Timeouts and transport failures back off like the proxy's 502 did; an SSRF-rejected URL reports at interval without backoff. - Deleted: the proxy route, scrapeForCollector, fetchUpstream and the per-isolate row memo. The service exposes authHeaders(row) instead.
🍁 Maple PR previewWarning Preview cleanup could not be confirmed. The Alchemy teardown outcome was Final commit |
Makisuo
added a commit
that referenced
this pull request
Sep 3, 2026
Third version collision in a row. Main landed #738 (agent-session filter columns), taking migration 0026 and local schema v16 — the slots this branch moved into yesterday. As with the previous two, nothing semantic conflicts: two unrelated changes each took the next free number. - `0026_product_events_from_traces.ts` -> `0027_...`, `version: 26` -> `27`, export and every doc reference renamed, docs/product-events-funnels.md included. - `migrations/index.ts` and its test carry 0024, 0025, 0026 and 0027. `clickHouseSchemaVersion` stays "21" — all four are `requiredForIngest: false`. - Local edge re-derived via `local-schema:bump` for v17; the v16->v17 module body is the previous one unchanged apart from version plumbing. - Restored the same four local-store-migration test assertions that taking main's copy of that file reverts on every one of these merges. Schema is 39 tables / 42 MVs, local schema v17, 81 objects. Verified on the merged tree: `bun typecheck` 41/41, `bun run lint` clean, ClickHouse schema / local-manifest / Tinybird gates up to date, apps/api 2585 passed, apps/web 2382, packages/domain 703, query-engine 1373, apps/cli 534. `bun.lock` is byte-identical to main's. Note for the next merge: main's #750 adds a new `@maple/safe-fetch` workspace, so a worktree installed before the merge needs `bun install` again afterwards — without it, `@maple/scraper` typecheck and one ScrapeTargetsService lint rule fail for missing types rather than for anything in the diff. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Tonight's PlanetScale metrics blip (
metrics.psdb.cloud502/504s and 29s timeouts from 21:54 UTC) surfaced as maple-api 5xxs onGET /api/internal/prometheus-scrapeand paged the org-wide "High error rate" rule for maple-api as well as scraper. Every branch scrape also parked a Worker invocation for up to 29s waiting on a third-party target. The proxy predates PlanetScale OAuth and existed to keep credentials server-side; both services already read secrets from the same Infisical project, and PlanetScale's data plane never used the Authorization header (it authenticates with the signed URL minted by discovery), so the proxy no longer bought anything.What changed
lib/safe-fetch(@maple/safe-fetch): the SSRF-safe fetch and URL validator moved out ofapps/apiunchanged so both apps share one guard. The API's five import sites now point at the lib.validateExternalUrlSyncusesURL.canParsebecauselib/**enforcesmaple/no-try-catch.InternalScrapeTargetgainstargetType,scrapeUrl(the signed PlanetScale branch URL, otherwise the row url) andauthHeaders. The API decrypts each row's credential while building the list, so the master key and the OAuth grant never leave the API. A row whose credential fails to decrypt is skipped for that round with a warning rather than failing the whole list.apps/scraperfetches directly through a newTargetFetcherservice: same timeout formula, Retry-After parsing and span attributes the proxy had. The client span isscraper.fetch_target; it recordsserver.addressandurl.pathbut never the signed query.scrapeUrlandauthHeadersare deliberately outside the scheduler's loop fingerprint. Each scrape reads the latest reconciled copy, so a re-signed PlanetScale URL or a rotated credential is picked up mid-loop without re-jittering the cadence.target_error); an SSRF-rejected URL reports at interval without backoff (scrape_failed).scrapeForCollector,fetchUpstream, the per-isolate row memo and its invalidation hooks.ScrapeTargetsServiceexposesauthHeaders(row)instead.Verification
Reviewer notes
ScrapeTargetsService.fetchUpstreamspan and will go silent. New where clause:service.name = "scraper" AND span.name = "scraper.fetch_target" AND attr.maple.scrape.target_type = "planetscale".scraperrather thanmaple-api. Excluding it from the critical rule is a separate decision (the rule'sexcludeServiceNamesfield).lib/safe-fetchis a new workspace, registered withbun install --minimum-release-age=0; the lockfile diff is 17 added lines with no version moves.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.