Repository navigation
Move setup code to ScopeDefaults (v7) #5197
Description
Activity
- added.NETPull requests that update .net codePull requests that update .net code
on May 11, 2026 - addedBreaking ChangeBinary/Source/Behavioral Breaking Changes.Binary/Source/Behavioral Breaking Changes.Next MajorChanges scheduled for the next Major release.Changes scheduled for the next Major release.and removed
on May 20, 2026 jamescrosswell commented
on Sep 30, 2026 CollaboratorAuthorMore actionsDesign proposal:
ScopeDefaultsCurrent design
Some values are the same for everything an app sends: release, distribution, environment, SDK name, default tags, and OS and runtime contexts. Today they come from three places:
SentryOptionsholds what the user configured.SettingLocatorresolves each value on demand, in the order option → environment variable → default.Scopealso declaresRelease,Distribution,EnvironmentandSdk, and users can set them. Scopes live on a scope stack: one per process in global mode (desktop, mobile), otherwise one per async flow (ASP.NET Core) or per request (ASP.NET classic).PushScopeclones the current scope.- The
Enricherruns on every event. It fills in whatever the scope left empty, reading fromSettingLocatorand computing SDK info and contexts.
Each kind of telemetry combines these differently:
Loadingflowchart LR Options["SentryOptions"] --> Locator["SettingLocator<br/>resolves on demand"] Scope["Current scope"] Locator --> Enricher["Enricher<br/>runs on every event"] Scope --> Events["Events, transactions,<br/>feedback"] Enricher -->|fills gaps| Events Locator --> Logs["Logs, metrics"] Scope -->|SDK name only| Logs Locator --> Other["Sessions, DSC,<br/>check-ins"] Options -->|at native init| Native["Native SDKs"] Scope -->|scope observer| NativeProblems
- Signals disagree. An environment set on a scope reaches events, but not logs, metrics, sessions, the dynamic sampling context (DSC) or check-ins (#5376). SDK names are wrong or missing on logs and metrics (#5497, #5506, #5613).
- Release can be set on a scope, which the spec doesn't allow. The configuration spec says release "Can only be set in options, not on scopes."
- Native SDKs miss values.
- Contexts are never synced, so a native crash before the first managed event has none (#2923).
- Default tags reach native only through a special-case push in
SentrySdk.InitHub.
Proposed design
A small internal type,
ScopeDefaults, holds the static values. It is built once per SDK init, and after that nothing reads them fromSentryOptionsorSettingLocator.Value Resolved from Scope can override Releaseoptions → SENTRY_RELEASE→ mobile package version → entry assembly versionNo Distributionoptions → mobile build number No Environmentoptions → SENTRY_ENVIRONMENT→debugorproductionYes Sdk(name, version, packages)the init path ( SentrySdk.Init,UseSentry, …), via an internal optionYes TagsSentryOptions.DefaultTagsYes, per key ContextsOS, runtime and device boot time, computed once Yes, per context: runtime and OS are added only when missing; boot time fills an existing device context - Release and Distribution follow the spec: only options can set them.
- Environment stays overridable because game SDKs change it after login (#5387, getsentry/sentry-java#5769). Cocoa and Native already allow this.
Every signal resolves each value the same way: the telemetry item's own value wins, then the current scope, then
ScopeDefaults, each layer only filling blanks.Loadingflowchart LR Options["SentryOptions"] -->|resolved once at init| Defaults[("ScopeDefaults")] Scope["Current scope"] --> Rule{{"Precedence rule<br/>item, then scope, then defaults"}} Defaults --> Rule Rule --> Events["Events, transactions,<br/>feedback"] Rule --> Logs["Logs, metrics"] Rule --> Other["Sessions, DSC,<br/>check-ins"] Defaults -->|at native init| Native["Native SDKs"] Scope -->|scope observer| NativeHow it works:
SentrySdk.InitHubbuildsScopeDefaultsbefore initialising native SDKs, then hands it to the Hub. The Hub shares it with the client and scope manager before integrations register, and syncs the default tags to native.ScopeDefaults.Applyruns where theEnricherruns today, so event processors andBeforeSendsee the same values as now.- The
Enricherkeeps only values that change per event, plus user and server name (see Out of scope).
ScopeDefaultsis deliberately not aScope. It has no breadcrumbs, attachments, processors, observer calls orClear(), and it is never cloned. It is the process-wide layer that the scopes spec calls the global scope. The name avoids "global", which already means global mode in this SDK.Why not set these on the root scope at init? Outside global mode there is no single root scope: the scope manager creates an empty root for each async flow that has no scope stack, and ASP.NET classic creates one per request. It also wouldn't change the signals that ignore the scope.
What changes
Public API:
Scope.ReleaseandScope.Distributionare removed. They move fromIEventLiketoITransactionData;SentryEventalready declares them.Scope.Environmentstays nullable, andnullmeans "use the default". Setting it syncs to native; clearing it re-syncs the default. This makes #5388 / #5587 unnecessary.
Behaviour:
- Logs, metrics, sessions, DSC and check-ins honour a scope's environment override.
Scope.Applysets the SDK name only when the item has none. Today it overwrites it.SENTRY_RELEASEtakes precedence over the mobile default release. Today the mobile default wins.
Out of scope
- User defaults and server name (
User.IdfromInstallationId, username, IP address). These belong to the data-collection work (#5428, #5439). A scope can't unset a default, so movingUser.Idhere would bring back #4172. - A public, spec-style global scope, and isolation scopes (#4484).
- Bugs that can be fixed on
mainnow:
Delivery
ScopeDefaults, the precedence rule for every signal, and the public API changes. There are no prerequisites.- SDK name from each init path, including Blazor WebAssembly, which has none today.
- This replaces the ASP.NET Core middleware and the MAUI, gRPC, Google Cloud Functions and ASP.NET event processors that set it today.
- It waits for the rest of the #5245 stack (#5585, #5592, #5595). Until #5595 merges,
SentryLoggerProviderwritesscope.Sdk, which would take precedence over the default.
- Initial native sync of contexts (#2923), after step 1. It isn't needed for 7.0: the gap predates
ScopeDefaults, and an internal interface for the built-in scope observers keeps it non-breaking (plan). - Android environment sync (#5387). It is independent of the steps above and can happen once sentry-java supports it (getsentry/sentry-java#5769, getsentry/sentry-java#5772).
Open questions
- Are we OK introducing
ScopeDefaultsinstead of the Global Scope that some other SDKs use for this... Implementing an additional Global Scope would introduce all manner of complexities to solve problems we don't really have - so I'd rather this simpler approach if we can.
jamescrosswell commented
on Sep 30, 2026 CollaboratorAuthorMore actionsDoes moving
ReleaseandDistributionoffIEventLikecost us code reuse?No.
Scope.Apply(IEventLike)stops copying them, but that copy only existed because scopes held these values. Here's every place onversion7that writes Release or Distribution onto a telemetry item:Copy Today After Scope → event, transaction or cloned scope Scope.Apply(IEventLike)Gone: scopes no longer hold these values Options → event or transaction Enricher.Apply(IEventLike)ScopeDefaults.ApplyOptions → event MainSentryEventProcessor, duplicating theEnricherGone Options → check-in Enricher.Apply(SentryCheckIn)ScopeDefaults.Apply(SentryCheckIn)Tracer → transaction SentryTransactionconstructorUnchanged, since ITransactionDatakeeps the propertiesEvery other reference (JSON deserialisation, the profiler, sessions) uses a concrete type, not
IEventLike.Apart from the unchanged tracer copy, four copy sites become two, both in
ScopeDefaults. BecauseIEventLikeno longer exposes these values,ScopeDefaults.Applyneeds a small type check for them:switch (item) { case SentryEvent e: e.Release ??= Release; e.Distribution ??= Distribution; break; case ITransactionData t: t.Release ??= Release; t.Distribution ??= Distribution; break; }
Environment, SDK, tags and contexts still go through
IEventLikeas before.jamescrosswell commented
on Sep 30, 2026 CollaboratorAuthorMore actions@bitsandfoxes just a heads up - I don't think it would change anything for you - you'd still be able to override the Environment on the scope (this simply provides a mechanism for setting the defaults when nothing is set by Unity/Godot or by the SDK user);
- changed the title
[-]Move setup code to GlobalRootScopeIntegration (v7)[/-][+]Move setup code to ScopeDefaults (v7)[/+]on Sep 30, 2026 I generally like the proposal very much! I think it should work well, and my only concern so far would be that we implement it in a way that makes our way into the Span-First initiative easier rather than harder
In terms of "not implementing the Global Scope as the spec defines", we can probably make that work (making it internal, and exposing the methods the spec defines), but I'd delegate the final decision to someone else xD Maybe @dingsdax? But I'm happy to also bring this up for discussion if wanted/needed .
jamescrosswell commented
on Oct 5, 2026 CollaboratorAuthorMore actionsOK let's go with ScopeDefaults. We can always evolve/replace these with a new model for ScopeStackContainers at some point in the future if we need.
- linked a pull request that will close this issuefeat!: resolve release, environment and SDK name once at init for all telemetry #5671
on Oct 5, 2026 - added a commit that references this issue
on Oct 6, 2026
Originally posted by @jamescrosswell in #5039
Some of these things can currently be set via the SentryOptions:
sentry-dotnet/src/Sentry/SentrySdk.cs
Lines 67 to 68 in 55b2d5d
But that in itself is confusing since now it's hard to tell which one should win (Scope or Options).
See:
And it seems we're always copying them from the options into the Scope anyway 😕.
It might be easier if we removed those things from the options and just configured them in a root scope (or allowed the user to configure them in a root scope)... so there was only ever one place these were configured (on the scope).