Repository navigation
Move setup code to ScopeDefaults (v7) #5197
Description
We could potentially set more stuff here. There are lots of scope properties that never change and which we currently set on the scope every time we capture an event. These are all candidates, I think:
sentry-dotnet/src/Sentry/Scope.cs
Lines 498 to 517 in f50b360
Contexts.CopyTo(other.Contexts); Request.CopyTo(other.Request); User.CopyTo(other.User); other.Release ??= Release; other.Distribution ??= Distribution; other.Environment ??= Environment; other.TransactionName ??= TransactionName; other.Level ??= Level; if (Sdk.Name is not null && Sdk.Version is not null) { other.Sdk.Name = Sdk.Name; other.Sdk.Version = Sdk.Version; } foreach (var package in Sdk.InternalPackages) { other.Sdk.AddPackage(package); } More of a performance improvement but might result in a subtle behavioural change for some users (perhaps code that checks/expects things to not be set in certain circumstances and sets them conditionally on this basis ). To be safe, perhaps we delay a change like that until the next major release.
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
| options.Environment = options.SettingLocator.GetEnvironment(); | |
| options.Release = options.SettingLocator.GetRelease(); |
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).
Activity
linear-code commentedon May 6, 2026
NET-676
jamescrosswell commentedon Sep 30, 2026
Design 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.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:
flowchart 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
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.ReleaseSENTRY_RELEASE→ mobile package version → entry assembly versionDistributionEnvironmentSENTRY_ENVIRONMENT→debugorproductionSdk(name, version, packages)SentrySdk.Init,UseSentry, …), via an internal optionTagsSentryOptions.DefaultTagsContextsEvery 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.flowchart 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.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:
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.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.mainnow:Scope.Clone()side effects (#5647).Delivery
ScopeDefaults, the precedence rule for every signal, and the public API changes. There are no prerequisites.SentryLoggerProviderwritesscope.Sdk, which would take precedence over the default.ScopeDefaults, and an internal interface for the built-in scope observers keeps it non-breaking (plan).Open questions
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 commentedon Sep 30, 2026
Does 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:Scope.Apply(IEventLike)Enricher.Apply(IEventLike)ScopeDefaults.ApplyMainSentryEventProcessor, duplicating theEnricherEnricher.Apply(SentryCheckIn)ScopeDefaults.Apply(SentryCheckIn)SentryTransactionconstructorITransactionDatakeeps 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:Environment, SDK, tags and contexts still go through
IEventLikeas before.jamescrosswell commentedon Sep 30, 2026
@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);
[-]Move setup code to GlobalRootScopeIntegration (v7)[/-][+]Move setup code to ScopeDefaults (v7)[/+]ric-oliv commentedon Oct 1, 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 commentedon Oct 5, 2026
OK 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.
feat!: resolve release, environment and SDK name once at init for all…