Outage Banner and Statuspage
In one screen. A single banner component has to carry four different meanings, from “GitHub didn’t answer” to “Chrome revoked our access to GitHub.” This page covers what the banner says in each case, when the Statuspage link is shown or hidden, and how the popup gets its read on
summary.jsonwithout ever fetching it itself.
The background is the only context that touches githubstatus.com. The popup mirrors a cached snapshot from chrome.storage.local and decides whether to render a link based on what is in that cache. Same pattern as PR data, same reason: the popup paints from storage, it does not perform network IO of its own.
GitHubStatusClient: cached, fail-open, popup-read-only
Section titled “GitHubStatusClient: cached, fail-open, popup-read-only”GitHubStatusClient wraps the summary.json endpoint at https://www.githubstatus.com/api/v2/summary.json. The contract:
| Concern | Value / behaviour |
|---|---|
| Endpoint | GITHUB_STATUS_API_URL |
| Per-request timeout | GITHUB_STATUS_FETCH_TIMEOUT_MS = 3s |
| Cache TTL | GITHUB_STATUS_CACHE_TTL_MS = 120s |
| Cache storage key | STORAGE_KEY_GITHUB_STATUS_CACHE in chrome.storage.local |
| Fail mode | Open. Every transport or parse failure resolves to { prComponentStatus: 'unknown', globalIndicator: 'unknown' } |
bypassCache semantics | Skip the TTL short-circuit, fetch fresh, overwrite the cache with the new snapshot |
Failing open is the load-bearing design choice. Real transport outages on github.com are caught upstream by GitHubOutageError from GitHubService, and the flaky status endpoint must not silently suppress legitimate notifications by masking a healthy PR fetch as “degraded”. The trade-off is honest: a real PR component degradation that coincides with a status-API blip will not arm the gate; the existing transport-error path still paints the outage banner if PR fetches fail outright.
The PR component name the parser looks for is exactly 'pull requests' (GITHUB_PR_COMPONENT_NAME), case-folded on read. If GitHub renames the component, the snapshot logs a warning and falls back to the global status.indicator. That gives a one-cycle escape hatch before the constant has to be updated.
The bypassCache flag exists for one specific caller. EventService.handleAlarm prefetches one snapshot at the top of every alarm wave with bypassCache: true, and the snapshot is threaded through all three list assessments. The bypass write overwrites the cache entry, so any incidental same-wave non-bypass read picks up the refreshed snapshot rather than triple-hitting summary.json. See List Trust and Suspect Lists for how assess() consumes that wave-scoped snapshot.
What the popup reads
Section titled “What the popup reads”Two hooks. Neither performs network IO; both subscribe to chrome.storage.onChanged so a write in the background reaches React without an explicit refresh.
useGitHubStatusSnapshot
Section titled “useGitHubStatusSnapshot”src/hooks/use-github-status-snapshot.ts mirrors STORAGE_KEY_GITHUB_STATUS_CACHE into local React state. It validates the parsed shape (both prComponentStatus and globalIndicator must belong to known unions) and returns null for a missing or malformed cache. The two-minute staleness on the cache is deliberate: this snapshot is used to gate an informational link, and the alternative (always-on or always-off) is strictly worse.
useGitHubOutage
Section titled “useGitHubOutage”src/hooks/use-github-outage.ts mirrors STORAGE_KEY_GITHUB_OUTAGE plus STORAGE_KEY_LAST_UNTRUSTED_FETCH_AT, and also subscribes to the githubOutage{Detected,Cleared} runtime broadcasts. Two parsing rules are worth calling out:
- Legacy payload fallback. Pre-
reasonbuilds wrote{ detected, timestamp, context }without a discriminator. The hook defaultsreasonto'transport'for those payloads so the banner shows the most generic, never-over-promising copy and the link stays gated byhasCorroboratingStatusCache. - Stale-flag expiry. Payloads whose
lastSeenAtis older thanGITHUB_OUTAGE_STALE_AFTER_MS = 2hare rejected outright. Broadcasts are best-effort and a popup can mount hours after recovery; without the expiry, a missed*Clearedbroadcast would leave the banner pinned.signalGitHubOutagerefresheslastSeenAton every repeat hit, so a genuinely ongoing outage stays visible.
Both hooks treat chrome.storage.local.remove (which surfaces as newValue: undefined on onChanged) as a clear, so a key removal flips the banner off without waiting for the broadcast.
Banner copy by reason
Section titled “Banner copy by reason”github-outage-banner.tsx renders one of four copy variants, keyed on the outage reason. The data-variant-id attribute is stable so support reports and integration tests can match it directly.
reason | data-variant-id | Title | Body |
|---|---|---|---|
transport (default) | outage.transport | ”GitHub didn’t respond. Showing your last known list." | "Pullwatch will retry on its own. If this sticks around, a quick refresh or a check on your connection usually clears it.” |
pr_component_degraded | outage.component-degraded | ”Pullwatch noticed an unusual change in your list." | "Keeping your last known list while things settle. New review requests during this window may not show until the next clean sync.” |
pr_list_churn | outage.list-churn | ”A pull request briefly disappeared and came back." | "Pullwatch held back the bouncing one to avoid duplicate alerts. Other list updates still flow through normally.” |
site_access_blocked | outage.site-access-blocked | ”Chrome is blocking Pullwatch from reaching GitHub." | "Site access for github.com is turned off in chrome://extensions. Pullwatch is showing your last known list, re-enable access to refresh it.” |
The copy is intentionally calm and never names “GitHub” as broken when Pullwatch is not sure GitHub is broken. pr_component_degraded says “an unusual change in your list”, not “GitHub is degraded”, because the local anomaly is what we are confident about. pr_list_churn is the one banner that explicitly explains the suppression: a notification did not fire, that is by design, and the rest of the popup is still alive. site_access_blocked is the only reason where the fix is on the user’s side, so its banner replaces the Statuspage link with a button that opens chrome://extensions/?id=<id> directly (via chrome.tabs.create, because Chrome blocks navigating to a chrome:// URL from a plain anchor click).
Statuspage link gating
Section titled “Statuspage link gating”The link to https://www.githubstatus.com is not always shown. It is gated by reason and by what the cached Statuspage snapshot actually says.
reason | Link visibility |
|---|---|
transport | Shown iff hasCorroboratingStatusCache(snapshot) is true. |
pr_component_degraded | Shown iff hasCorroboratingStatusCache(snapshot) is true. |
pr_list_churn | Always hidden. |
site_access_blocked | No Statuspage link. Shows a “open chrome://extensions” action button instead, because the fix is a local Chrome setting, not a GitHub incident. |
hasCorroboratingStatusCache (in use-github-status-snapshot.ts) returns true when:
prComponentStatusispartial_outageormajor_outage, orglobalIndicatorisminor,major, orcritical.
The reason narrows the banner copy; the snapshot decides whether pointing the user at githubstatus.com would line up with what they will see there. pr_component_degraded includes the gate too because the signal also fires for sub-threshold local shrinks (a merged shrink at the MERGED_SHRINK_SUSPICION_THRESHOLD) that do not require Statuspage corroboration; linking unconditionally would dump those users on an all-green page.
pr_list_churn skips the gate because Statuspage is by definition irrelevant: the integrity signal fires on tombstone resurrection regardless of summary.json. Linking would be misleading.
The “Last check (kept your cached list)” subline
Section titled “The “Last check (kept your cached list)” subline”Below the banner body, the pr_component_degraded variant renders one extra line:
Last check (kept your cached list): N minutes ago
It only ever shows on pr_component_degraded, because only that reason writes STORAGE_KEY_LAST_UNTRUSTED_FETCH_AT (in PRService.persistUntrustedFetchMetadata). Showing the subline for transport would tick against a stale or unrelated timestamp; for pr_list_churn it would be dishonest, because the wave that signalled churn did persist a list.
The relative-time formatter is the same formatLastFetchDetail used by the header tooltip, so staleness reads consistently across the popup. A 30-second tick (OUTAGE_SUBLINE_TICK_MS) keeps the line alive without burning a per-second timer on a non-critical banner. The subline disappears the moment clearGitHubOutage runs, because that call also removes STORAGE_KEY_LAST_UNTRUSTED_FETCH_AT (HealthStatusService.clearGitHubOutage).
Stale-banner expiry
Section titled “Stale-banner expiry”A popup can mount hours after a brief outage. Three layers prevent a phantom banner in that case.
- Storage source of truth.
useGitHubOutagealways reads fromchrome.storage.localon mount, even though the broadcast may have fired in the meantime. The hook’s storage-fallback path is documented to be safe becausesignalGitHubOutageawaitsstorage.local.setbeforeruntime.sendMessage. - 2-hour
lastSeenAtexpiry.parsePayloadrejects any payload whoselastSeenAtis older thanGITHUB_OUTAGE_STALE_AFTER_MS = 2h. A wave that re-asserts the same outage refresheslastSeenAt, so genuinely ongoing outages survive; a flag forgotten in storage after a missed clear broadcast does not. - Listener convergence. Both the
*Detectedand*Clearedbroadcasts and thechrome.storage.onChangedlistener can flip the banner; whichever arrives first wins, the other is a no-op.
End-to-end sequence
Section titled “End-to-end sequence”The sequence below walks one alarm wave that ends in a pr_component_degraded banner, so you can see every actor from the timer to the rendered link in order. Read it top to bottom: the alarm wakes the worker, the worker prefetches the Statuspage snapshot once, then the PR assessment decides the list is suspect and signals the outage. The last four lines are the popup reacting to two independent chrome.storage.onChanged events (the outage payload and the snapshot) and combining them into copy plus a gated link. Notice the worker never pushes the banner; it only writes storage, and the popup derives everything from there.
sequenceDiagram
autonumber
participant Alarm
participant SW as Service worker
participant Status as GitHubStatusClient
participant Statuspage as githubstatus.com
participant PR as PRService
participant Health as HealthStatusService
participant Local as chrome.storage.local
participant Popup
Alarm->>SW: EVENT_FETCH_PRS
SW->>Status: getStatus({ bypassCache: true })
Status->>Statuspage: GET summary.json
Statuspage-->>Status: snapshot
Status->>Local: write github_status_cache
SW->>PR: fetchAndUpdateAssignedPRs(false, true, waveStatus)
PR->>PR: assess() → suspect_partial
PR->>Local: write last_untrusted_fetch_at
PR->>Health: signalGitHubOutage(reason='pr_component_degraded')
Health->>Local: write github_outage payload
Health->>Popup: runtime broadcast githubOutageDetected
Local-->>Popup: onChanged for github_outage
Local-->>Popup: onChanged for github_status_cache
Popup->>Popup: useGitHubOutage parses payload
Popup->>Popup: useGitHubStatusSnapshot reads snapshot
Popup->>Popup: hasCorroboratingStatusCache decides link
Popup-->>Popup: render banner with reason copy + optional link
When the next wave lands a trusted persist, Health.clearGitHubOutage removes github_outage and last_untrusted_fetch_at, broadcasts githubOutageCleared, and the banner unmounts on the next React tick.
See also
Section titled “See also”- GitHub Health and Outages: the hub. The full
GitHubOutageReasontaxonomy and the wave-suppression rule that decides whenclearGitHubOutagemay fire. - List Trust and Suspect Lists: the integrity layer that produces
pr_component_degradedandpr_list_churnsignals. - Notifications and Sound: the sibling suppression layer. Most of its rules are described in terms of the same trust dispatch this banner reflects.
- Data Hydration and Storage: every storage key referenced on this page, in one inventory.