Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix Instatus fetcher schema rejects numeric maintenance duration (bluesky) - #2702

Open
polylane[bot] wants to merge 1 commit into
mainfrom
polylane/autofix/tol6e5zdblif
Open

Fix Instatus fetcher schema rejects numeric maintenance duration (bluesky)#2702
polylane[bot] wants to merge 1 commit into
mainfrom
polylane/autofix/tol6e5zdblif

Conversation

@polylane

@polylane polylane Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes: Instatus fetcher schema rejects numeric maintenance duration (bluesky)

The external-status cron failed on every tick while Bluesky's status page was under maintenance: Instatus serves activeMaintenances[].duration as a JSON number, and the instatus fetcher's schema only accepted a string, so each fetch threw a schema-mismatch FetchError to Sentry and Bluesky's entry in the public external-services directory kept its stale pre-maintenance status. This widens the field to accept both shapes, so the fetcher records the maintenance instead of failing, and any other Instatus-hosted page stops hitting the same wall during its next maintenance window.

What changed

  • packages/status-fetcher/src/fetchers/instatus.ts: instatusMaintenanceSchema.duration widened from z.string().optional() to z.union([z.number(), z.string()]).optional(), matching the union idiom already used in custom.ts.
  • packages/status-fetcher/__tests__/fetchers/instatus.test.ts: regression test replaying the exact live status.bsky.app summary.json payload (numeric duration: 60, UNDERMAINTENANCE, INPROGRESS maintenance) and asserting it maps to under_maintenance with the maintenance's name and start time.

Why it's safe

  • duration is never consumed downstream: the fetcher's normalize() maps only page.status, the maintenance's name and start, and activeIncidents; the workflows cron and detect.ts read no other maintenance fields, so accepting a wider type cannot change any mapped output.
  • The union still rejects wrong-typed payloads (objects, null, booleans), so genuinely malformed responses keep throwing the schema-mismatch FetchError exactly as before — the guard is narrowed, not removed.
  • The string branch preserves the documented Instatus shape, so pages that do send a string duration keep parsing; no existing fixture or test asserted the old type.
  • The same exported schema serves both the fetcher and detect.ts, so one widening fixes both paths; a repo-wide grep confirmed no other package references these fields.

Validation

  • bun x deno check --sloppy-imports . in packages/status-fetcher (the package's declared check script): pass
  • bun x deno test --parallel -A --no-check --sloppy-imports in packages/status-fetcher (the declared test script): 19 files passed, 243 steps, 0 failed — includes the new regression test replaying the live Bluesky payload
  • bun x deno check --sloppy-imports src/serve.ts in apps/workflows (its declared check script, covering the consumer): pass
  • oxfmt --check repo-wide: all files correctly formatted
  • oxlint on packages/status-fetcher: 0 warnings, 0 errors
  • node scripts/check-doc-refs.mts: no dangling references
  • not run: turbo run check wrapper — pnpm is not installed in this sandbox, so each package's declared check script was run directly instead (results above)
  • not run: apps/workflows deno test suite — requires a live DATABASE_URL; the change does not touch workflows code and its type gate covers the import surface
Root cause and scoping notes

Root cause

  • Symptom: Sentry alert FetchError: [instatus (bluesky)] schema mismatch: https://status.bsky.app/summary.json, 2 events (19:00:31Z, 19:10:23Z) from the external-status cron on openstatus-workflows.
  • Trigger: Bluesky's "Network Maintenance" began at 18:45:00Z; the first event is the next cron tick. Zero events of this signature in the prior 30 days — the populated-maintenance branch had never been hit before.
  • Mechanism: a live fetch confirmed summary.json returns HTTP 200 with valid Instatus JSON whose activeMaintenances[0].duration is the number 60. The schema declared duration: z.string().optional(); reproduced with zod 4.1.13 (the lockfile version): parse fails with invalid_type, expected string, received number at ['activeMaintenances', 0, 'duration'], which fetchJson rethrows as the FetchError of kind=schema.
  • Root cause is in our schema, not the page: Instatus serves duration as a number on live pages, and the handler treats schema mismatch as an our-side defect (caught, last-known status kept, reported to Sentry). Impact is bounded to the directory display path; no customer monitor depends on this fetcher.
  • Why this fixes it: the union accepts both wire shapes without transforming anything, and duration is never read downstream, so mapped output for every previously-parsing payload is identical. detect.ts reuses the same exported schema, so the detection probe also stops misclassifying live Instatus pages during maintenances.

Out of scope / follow-ups

  • The remaining duration of Bluesky's maintenance window itself: the directory entry will show under_maintenance after this merges, which is the correct rendering of a real maintenance.
  • Sentry alert-rule tuning for the external-status family (the connected token already 400s on alert-rule reads, tracked separately).
Causal chain
  • Signal (alert): FetchError: [instatus (bluesky)] schema mismatch: https://status.bsky.app/summary.json
  • Surfacing site: openstatus-workflows (Fly app) at apps/workflows external-status cron phase=status, reported to Sentry from openstatus-workflows (url tag http://openstatus-workflows.fly.dev/cron/external-status, tags cron:external-status, fetcher:instatus, slug:bluesky, handled:yes)
  • Mechanism: The cron fetched https://status.bsky.app/summary.json, which returns HTTP 200 with valid Instatus JSON. Instatus's activeMaintenances[].duration is a JSON number (60), but instatusMaintenanceSchema in packages/status-fetcher/src/fetchers/instatus.ts declared duration: z.string().optional(). Zod 4.1.13 (the lockfile version) rejected the payload with invalid_type at path ['activeMaintenances', 0, 'duration'], which fetchJson rethrew as FetchError kind=schema: '[instatus (bluesky)] schema mismatch'.
  • Producer: instatusMaintenanceSchema in packages/status-fetcher, executed by the external-status cron in apps/workflows on openstatus-workflows, instance openstatus-workflows (single Fly app, server_name 85e906a41411e8), tenant The OpenStatus external-services directory (its own dogfood dataset), entry slug 'bluesky', at openstatusHQ/openstatus:packages/status-fetcher/src/fetchers/instatus.ts#instatusMaintenanceSchema (line 28 pre-fix)
  • Trigger: Bluesky's scheduled maintenance 'Network Maintenance' started at 2026-09-10T18:45:00.000Z, flipping page.status to UNDERMAINTENANCE and populating activeMaintenances. The 19:00:31Z event is the first cron tick after that moment; with no active maintenance before today, the fetcher had never hit the populated-maintenance branch.
  • What happens to the failed unit today: The cron handler wraps every fetch in try/catch (handled=yes): a schema mismatch keeps the last-known status for the entry, increments a failure count, and reports a FetchError of kind=schema to Sentry. The unit is dropped for that tick and re-fetched on the next one; nothing retries within a tick and nothing dead-letters. The handler's own semantics treat a schema mismatch as an our-side defect.
  • Cadence check: Confirmed: the ~10-minute cron explains the two events (19:00:31Z, 19:10:23Z). The 30-day window shows zero prior events of this signature, which the hypothesis predicts: the defect exists on every tick but only bites when a maintenance exposes the numeric field. The maintenance's stated duration is 60 minutes, so roughly six events would accrue without the fix.
  • Blast radius: 0 other resource(s), 0 other tenant(s); data at risk: The external directory's status snapshot for any Instatus-hosted page goes stale while the page is under maintenance (last-known status kept, no refresh, no updated_at). Today it hit Bluesky; every other Instatus-hosted entry is one scheduled maintenance away from the same failure.
  • Producer evidence:
    • Live fetch of https://status.bsky.app/summary.json: HTTP 200, page.status UNDERMAINTENANCE, activeMaintenances[0].duration = 60 (number) on entry 'Network Maintenance', status INPROGRESS, start 2026-09-10T18:45:00.000Z
    • Reproduced the parse failure in isolation with zod 4.1.13, the exact lockfile version: success false, issues [{code invalid_type, expected string, received number, path ['activeMaintenances', 0, 'duration']}]
    • packages/status-fetcher/src/fetchers/instatus.ts:28 (pre-fix) declared duration: z.string().optional(); one caller today, the external-status cron at apps/workflows/src/cron/external-status.ts, one directory entry (bluesky) on this fetcher; packages/status-fetcher/src/detect.ts:63 reuses the same schema for provider detection and benefits from the same fix
    • Sentry 30-day scan: exactly 2 events of this signature, timestamps 2026-09-10T19:00:31Z and 19:10:23Z, one per cron tick, first occurrence 15 minutes after the maintenance began

Detection outcome

The fingerprinted signal FetchError: [instatus (bluesky)] schema mismatch: https://status.bsky.app/summary.json stops firing once this deploys. On the cron's next tick, summary.json's activeMaintenances[0].duration (a JSON number while the maintenance is live) now parses against the widened union, so the fetch completes: the entry records UNDERMAINTENANCE / under_maintenance with the maintenance's name and start time instead of throwing and keeping a stale snapshot. The same holds for any other Instatus-hosted page that runs a maintenance: the schema shared by detect.ts and the fetcher no longer rejects the payload shape Instatus actually serves.

Fix chosen

Chosen: cause (removes the mechanism that produces the failure): Widen instatusMaintenanceSchema's duration field from z.string() to z.union([z.number(), z.string()]), so the payload Instatus actually serves parses; this removes the mechanism that produces the schema-mismatch FetchError.

Considered and not chosen:

  • suppress (silences, downgrades, or reroutes the signal without changing what produces it): Downgrade or filter the schema-mismatch FetchError in Sentry so it stops paging. Not chosen: The handler's own semantics are that a schema mismatch is an our-side defect; silencing it would hide a real producer, and every other Instatus-hosted page would keep failing during its next maintenance with no signal.
  • disable (turns a feature, guard, check, test, or telemetry off): Re-point the bluesky directory entry from the instatus fetcher to another provider. Not chosen: The page is genuinely Instatus-hosted and its API serves valid Instatus JSON live; the data is correct, the schema is what is wrong.
  • loosen (raises a limit, threshold, or timeout, or widens a retry): Preprocess duration with z.coerce or a transform so both shapes normalize to one type. Not chosen: Coercion transforms a field no code path reads and would silently accept non-numeric strings; the union accepts exactly the two wire shapes Instatus serves without transforming either, mirroring the existing custom.ts idiom.
2 files changed (+32/-1)
  • packages/status-fetcher/__tests__/fetchers/instatus.test.ts: modified, +30/-0
  • packages/status-fetcher/src/fetchers/instatus.ts: modified, +2/-1

Repository conventions present in the repository: AGENTS.md, CLAUDE.md, apps/checker/AGENTS.md, apps/dashboard/AGENTS.md, apps/server/AGENTS.md, apps/status-page/AGENTS.md, apps/web/AGENTS.md, apps/workflows/AGENTS.md, packages/services/AGENTS.md, packages/ui/AGENTS.md.

Repository lint: pnpm run lint (declared in package.json) could not run in the sandbox because its tool is not installed there; run it before merging.

view-autofix view-investigation view-issue


Generated by Polylane.

Review in cubic

@vercel
vercel Bot temporarily deployed to Preview – openstatus-status-page September 10, 2026 19:38 Inactive
@polylane polylane Bot added the polylane label Sep 10, 2026
@vercel
vercel Bot temporarily deployed to Preview – openstatus-web September 10, 2026 19:38 Inactive
@vercel
vercel Bot temporarily deployed to Preview – openstatus-dashboard September 10, 2026 19:38 Inactive
@polylane polylane Bot added the polylane label Sep 10, 2026
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated
openstatus-dashboard Skipped Skipped Sep 10, 2026 7:38pm UTC
openstatus-status-page Skipped Skipped Sep 10, 2026 7:38pm UTC
openstatus-web Skipped Skipped Sep 10, 2026 7:38pm UTC

Request Review

@polylane polylane Bot added the severity:medium Polylane autofix severity: medium label Sep 10, 2026
@polylane
polylane Bot requested a review from mxkaske September 10, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

polylane severity:medium Polylane autofix severity: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants