Context
Per the OAuth scope drift RFC's pre-flight #4, PostHog's AS doesn't emit invalid_scope rejections to the standard log stream. They come back as HTTP 302 redirects with ?error=invalid_scope in the redirect URL (per RFC 6749 § 4.1.2.1), but nothing logs the rejection on our side.
This means:
Confirmed via query-logs against posthog-web-django: zero results for searchTerm invalid_scope over the past 24h, despite the in-flight #57509 PR being the resolution to a customer-reported invalid_scope event in the same window.
Change
In posthog/api/oauth/views.py, add structured logging at the point where the AS returns an invalid_scope redirect.
Two places to instrument depending on where DOT raises:
- The
OAuthAuthorizationView (or its parent in DOT) — when validate_authorization_request() raises InvalidScopeError
- The exception handler that converts the OAuth error into the redirect URL
Add:
import structlog
import posthoganalytics
logger = structlog.get_logger(__name__)
# At the rejection point:
logger.warning(
\"oauth.authorize.invalid_scope\",
requested_scopes=requested_scopes_list,
grantable_scopes=grantable_scopes_list,
missing_scopes=list(set(requested_scopes_list) - set(grantable_scopes_list)),
client_id=request.GET.get(\"client_id\", \"\"),
user_agent=request.headers.get(\"User-Agent\", \"\")[:200],
)
posthoganalytics.capture_exception(
Exception(\"OAuth invalid_scope rejection\"),
properties={
\"missing_scopes\": list(set(requested_scopes_list) - set(grantable_scopes_list)),
\"client_id\": request.GET.get(\"client_id\", \"\"),
},
)
Confirm the WARN-severity Loki query produces results within 7 days of deploy.
Acceptance
- A query like
SELECT count() FROM logs WHERE service.name = 'posthog-web-django' AND severity_text = 'warn' AND body ILIKE '%oauth.authorize.invalid_scope%' produces non-zero results once a real rejection occurs
- The baseline metric for the RFC's success criterion is available within 7 days of this landing
- Capture in error tracking surfaces the spike if there's a regression
Why
You can't measure what you don't observe. The RFC commits to "zero invalid_scope over 30 days" as the success metric for the structural cleanup; this issue makes that measurable.
Tracking
Parent: #57524
Project: https://github.com/orgs/PostHog/projects/194
RFC: https://github.com/PostHog/requests-for-comments-internal/blob/matt/oauth-scope-drift-rfc/engineering/2026-05-04-oauth-scope-drift.md
Context
Per the OAuth scope drift RFC's pre-flight #4, PostHog's AS doesn't emit
invalid_scoperejections to the standard log stream. They come back as HTTP 302 redirects with?error=invalid_scopein the redirect URL (per RFC 6749 § 4.1.2.1), but nothing logs the rejection on our side.This means:
invalid_scopeover 30-day window") is unmeasurable todayConfirmed via
query-logsagainstposthog-web-django: zero results for searchTerminvalid_scopeover the past 24h, despite the in-flight #57509 PR being the resolution to a customer-reportedinvalid_scopeevent in the same window.Change
In
posthog/api/oauth/views.py, add structured logging at the point where the AS returns aninvalid_scoperedirect.Two places to instrument depending on where DOT raises:
OAuthAuthorizationView(or its parent in DOT) — whenvalidate_authorization_request()raisesInvalidScopeErrorAdd:
Confirm the
WARN-severity Loki query produces results within 7 days of deploy.Acceptance
SELECT count() FROM logs WHERE service.name = 'posthog-web-django' AND severity_text = 'warn' AND body ILIKE '%oauth.authorize.invalid_scope%'produces non-zero results once a real rejection occursWhy
You can't measure what you don't observe. The RFC commits to "zero
invalid_scopeover 30 days" as the success metric for the structural cleanup; this issue makes that measurable.Tracking
Parent: #57524
Project: https://github.com/orgs/PostHog/projects/194
RFC: https://github.com/PostHog/requests-for-comments-internal/blob/matt/oauth-scope-drift-rfc/engineering/2026-05-04-oauth-scope-drift.md