You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The backend emits structured events via structlog from Django views and task handlers. Each emission today has to carry the identifiers it wants to expose — organisation__id, user__id, etc. — as explicit kwargs at the call site. That means either every emit site duplicates context-fetching code, or events lose the identifiers entirely when emitted from a deeply nested service where no org is in scope.
The new events catalogue makes the gap visible — several entries list sparse attribute sets because the surrounding code can't cheaply resolve context.
Proposal
Add a Django middleware that runs after AuthenticationMiddleware and binds request context onto structlog.contextvars so every downstream emit in the request's scope picks it up automatically, without call-site changes.
Bindings:
user.id — request.user.uuid when request.user is an FFAdminUser. Nothing bound for API-key principals (APIKeyUser has no uuid; a separate identifier would mix kinds).
organisation.id — primary request.user.key.organisation_id (APIKeyUser via Master-API-Key), fallback request.user.organisations.first().id (FFAdminUser).
project.id / environment.id — derived from URL kwargs (project_pk, environment_api_key → Environment lookup), bound during process_view so they're available for the view and anything downstream.
Cleanup
Gunicorn's gthread worker (what we run with GUNICORN_THREADS=2) reuses long-lived threads across requests. Python's contextvars bindings persist on a thread's current context until explicitly cleared, so without cleanup, Request B on Thread 1 inherits Request A's bindings.
The middleware wraps the inner call in try / finally clear_contextvars() at the outer __call__. The finally runs even on view exceptions, so one request's bindings never reach the next one on the same thread. No race concerns across threads — each thread has its own context.
Pattern forward-compatible with ASGI / async views, since contextvars is the shared primitive.
Out of scope
SDK flag-eval routes (X-Environment-Key auth) have no user identity to bind. Environment / project bindings from URL path would still apply there.
Frontend-provided baggage (e.g. Amplitude device_id / session_id) is already propagated via W3CBaggagePropagator and copied by StructlogOTelProcessor. No change needed.
Middleware binds the four contextvars above when derivable, clears at response.
Existing emission sites (e.g. code_references.scan.created, launch_darkly.import_failed) drop their now-redundant organisation__id / user__id kwargs where the middleware covers them.
Cross-request leakage test: two requests to the same worker thread, the second asserts no stale bindings from the first.
The events catalogue gains a "Common attributes" preamble describing the globally-bound attributes — follow-up template tweak in flagsmith-common once the convention is settled.
The backend emits structured events via structlog from Django views and task handlers. Each emission today has to carry the identifiers it wants to expose —
organisation__id,user__id, etc. — as explicit kwargs at the call site. That means either every emit site duplicates context-fetching code, or events lose the identifiers entirely when emitted from a deeply nested service where no org is in scope.The new events catalogue makes the gap visible — several entries list sparse attribute sets because the surrounding code can't cheaply resolve context.
Proposal
Add a Django middleware that runs after
AuthenticationMiddlewareand binds request context ontostructlog.contextvarsso every downstream emit in the request's scope picks it up automatically, without call-site changes.Bindings:
user.id—request.user.uuidwhenrequest.useris anFFAdminUser. Nothing bound for API-key principals (APIKeyUser has no uuid; a separate identifier would mix kinds).organisation.id— primaryrequest.user.key.organisation_id(APIKeyUser via Master-API-Key), fallbackrequest.user.organisations.first().id(FFAdminUser).project.id/environment.id— derived from URL kwargs (project_pk,environment_api_key→ Environment lookup), bound duringprocess_viewso they're available for the view and anything downstream.Cleanup
Gunicorn's
gthreadworker (what we run withGUNICORN_THREADS=2) reuses long-lived threads across requests. Python'scontextvarsbindings persist on a thread's current context until explicitly cleared, so without cleanup, Request B on Thread 1 inherits Request A's bindings.The middleware wraps the inner call in
try/finally clear_contextvars()at the outer__call__. Thefinallyruns even on view exceptions, so one request's bindings never reach the next one on the same thread. No race concerns across threads — each thread has its own context.Pattern forward-compatible with ASGI / async views, since
contextvarsis the shared primitive.Out of scope
X-Environment-Keyauth) have no user identity to bind. Environment / project bindings from URL path would still apply there.device_id/session_id) is already propagated viaW3CBaggagePropagatorand copied byStructlogOTelProcessor. No change needed.Acceptance
code_references.scan.created,launch_darkly.import_failed) drop their now-redundantorganisation__id/user__idkwargs where the middleware covers them.