Skip to content

Move setup code to ScopeDefaults (v7) #5197

Description

@jamescrosswell

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:

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:

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

  1. linear-code commented on May 6, 2026

    @linear-code
  2. added
    Breaking ChangeBinary/Source/Behavioral Breaking Changes.
    Next MajorChanges scheduled for the next Major release.
    and removed on May 20, 2026
  3. jamescrosswell commented on Sep 30, 2026

    @jamescrosswell
    CollaboratorAuthor

    Design proposal: ScopeDefaults

    Current 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:

    • SentryOptions holds what the user configured. SettingLocator resolves each value on demand, in the order option → environment variable → default.
    • Scope also declares Release, Distribution, Environment and Sdk, 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). PushScope clones the current scope.
    • The Enricher runs on every event. It fills in whatever the scope left empty, reading from SettingLocator and 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| Native
    
    Loading

    Problems

    1. 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).
    2. 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."
    3. 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 from SentryOptions or SettingLocator.

    Value Resolved from Scope can override
    Release options → SENTRY_RELEASE → mobile package version → entry assembly version No
    Distribution options → mobile build number No
    Environment options → SENTRY_ENVIRONMENT → debug or production Yes
    Sdk (name, version, packages) the init path (SentrySdk.Init, UseSentry, …), via an internal option Yes
    Tags SentryOptions.DefaultTags Yes, per key
    Contexts OS, 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.

    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| Native
    
    Loading

    How it works:

    • SentrySdk.InitHub builds ScopeDefaults before 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.Apply runs where the Enricher runs today, so event processors and BeforeSend see the same values as now.
    • The Enricher keeps only values that change per event, plus user and server name (see Out of scope).

    ScopeDefaults is deliberately not a Scope. It has no breadcrumbs, attachments, processors, observer calls or Clear(), 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.Release and Scope.Distribution are removed. They move from IEventLike to ITransactionData; SentryEvent already declares them.
    • Scope.Environment stays nullable, and null means "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.Apply sets the SDK name only when the item has none. Today it overwrites it.
    • SENTRY_RELEASE takes precedence over the mobile default release. Today the mobile default wins.

    Out of scope

    • User defaults and server name (User.Id from InstallationId, username, IP address). These belong to the data-collection work (#5428, #5439). A scope can't unset a default, so moving User.Id here would bring back #4172.
    • A public, spec-style global scope, and isolation scopes (#4484).
    • Bugs that can be fixed on main now:
      • Scope.Clone() side effects (#5647).
      • Native AOT crashes missing the default release (#5648).

    Delivery

    1. ScopeDefaults, the precedence rule for every signal, and the public API changes. There are no prerequisites.
    2. 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, SentryLoggerProvider writes scope.Sdk, which would take precedence over the default.
    3. 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).
    4. 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

    1. Are we OK introducing ScopeDefaults instead 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.
  4. jamescrosswell commented on Sep 30, 2026

    @jamescrosswell
    CollaboratorAuthor

    Does moving Release and Distribution off IEventLike cost us code reuse?

    No. Scope.Apply(IEventLike) stops copying them, but that copy only existed because scopes held these values. Here's every place on version7 that 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.Apply
    Options → event MainSentryEventProcessor, duplicating the Enricher Gone
    Options → check-in Enricher.Apply(SentryCheckIn) ScopeDefaults.Apply(SentryCheckIn)
    Tracer → transaction SentryTransaction constructor Unchanged, since ITransactionData keeps the properties

    Every 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. Because IEventLike no longer exposes these values, ScopeDefaults.Apply needs 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 IEventLike as before.

  5. jamescrosswell commented on Sep 30, 2026

    @jamescrosswell
    CollaboratorAuthor

    @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);

  6. changed the title [-]Move setup code to GlobalRootScopeIntegration (v7)[/-] [+]Move setup code to ScopeDefaults (v7)[/+] on Sep 30, 2026
  7. ric-oliv commented on Oct 1, 2026

    @ric-oliv
    Member

    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 .

  8. jamescrosswell commented on Oct 5, 2026

    @jamescrosswell
    CollaboratorAuthor

    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.

  9. added a commit that references this issue on Oct 6, 2026
    d95ae41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETPull requests that update .net codeBreaking ChangeBinary/Source/Behavioral Breaking Changes.ImprovementNext MajorChanges scheduled for the next Major release.

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions