Sentry environment-filter gotcha
The issues-list environment= filter returns issue-level aggregates, not environment-scoped ones
Applies to: Any monitor or query that calls Sentry's issues-list endpoint (/api/0/projects/<org>/<project>/issues/?query=...&environment=<env>) and trusts the returned issue's title, lastSeen, or unresolved status as evidence about that specific environment.
Sentry's issues-list endpoint filters which issues it returns by environment: an issue appears in the result if any of its events match environment=<env>. But the fields that endpoint surfaces on each matched issue — title, lastSeen, unresolved/status — are issue-level aggregates computed across all environments the issue has ever seen, not scoped to the filter. If a single Sentry issue holds events from two environments, a query for one environment can return the issue while its lastSeen and resolution state are actually being driven by the other.
The foot-gun: the environment= query parameter reads like a scope filter on the whole result, but it only filters issue membership — everything the response tells you about a matched issue is unscoped.
What happened (OBJ-3175, 2026-09-01)
The Dave — Staging health check autopilot's Check 2 queried the issues-list endpoint with is:unresolved !level:info and environment=staging, and treated any non-empty result as FAIL.
Sentry issue OBJECTUVE-API-2W ("⚠️ EMAIL CANARY - CANNOT VERIFY", culprit Email::DeliveryCanaryVerifyJob) matched. Its tag breakdown told a different story than the FAIL implied:
environment tag breakdown (11 total events, 2 unique values):
production: 6 events, lastSeen 2026-09-01T07:31:04Z (today)
staging: 5 events, lastSeen 2026-08-29T07:30:52Z (3 days before this run)Staging's occurrences had already self-resolved days earlier (confirmed separately by /health's email_canary sub-check reporting ok with a fresh last_success_at). But because the same Sentry issue kept accumulating live production events, the issue stayed unresolved with a recent lastSeen — so Check 2 read a dead staging symptom as a live staging failure. The false-FAIL cost a full diagnostic chain (OBJ-3175 → this doc) to trace back to a proximate cause that was ultimately a query-shape problem, not an application bug.
A related but distinct mechanism — why one Sentry issue held both environments' events in the first place, despite DeliveryCanaryVerifyJob#send_alert setting an explicit per-environment fingerprint — is tracked separately on OBJ-3176. This doc covers the query-shape fix only; it doesn't restate OBJ-3176's fingerprint investigation.
Fix
Query the events endpoint scoped to the environment with an explicit recency window, and use the issues-list endpoint only for resolution state — never for environment scoping:
# Step A: environment-scoped events in the last 24h
EVENTS_URL="https://sentry.io/api/0/organizations/<org>/events/?dataset=errors&project=<project_id>&statsPeriod=24h&field=issue&field=count()&sort=-count&per_page=100&query=environment%3Astaging%20%21level%3Ainfo"
# Step B (recommended): issue-level resolution state, deliberately with no environment= param —
# used only for unresolved/resolved status, never for environment scoping
ISSUES_URL="https://sentry.io/api/0/projects/<org>/<project>/issues/?query=is%3Aunresolved&limit=100"
# Step C: intersect — flag only an issue that BOTH has a recent environment-scoped event
# AND is still unresolvedFAIL only when an issue appears in both the environment-scoped recent-events result and the unresolved-issues result. A query-shape canary (a wide, unfiltered version of the same events query that must return non-zero results) guards against a malformed query silently reading as "0 results = healthy" — treat any non-200 response, or an empty canary, as blocked/inconclusive, never as PASS.
This exact fix now lives in the Dave — Staging health check autopilot's Check 2 (its description field, not a repo file — Check 2's logic is an inline task prompt, not versioned application code, so there's no source file to link here).
Rule of thumb
Sentry's issues-list
environment=filter answers "does this issue have any event in this environment?" — it does not answer "is this issue'slastSeen/unresolvedstate driven by this environment?" For the second question, query the events endpoint scoped toenvironment:<env>with a recency window, and use the issues-list endpoint only for resolution state.
Related
- Health Check Informational Sub-checks — a different Sentry/health-check gotcha shape:
INFORMATIONAL_CHECKSgates the overall HTTP verdict but not the raw JSON a monitor might read docs/operations/deployment.md— release-train gate context relevant to the underlyingOBJECTUVE-API-2Wincident (production's fix was already merged but awaiting its scheduled deploy window)
Last updated: 2026-09-01