Skip to content

fix: fetch default attributes from Scope first#5376

Open
bitsandfoxes wants to merge 14 commits into
mainfrom
fix/default-attributes-from-scope
Open

fix: fetch default attributes from Scope first#5376
bitsandfoxes wants to merge 14 commits into
mainfrom
fix/default-attributes-from-scope

Conversation

@bitsandfoxes

@bitsandfoxes bitsandfoxes commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Follows #5365

The issue I see is that

  • We keep the Scope basically unpopulated
  • We let users overwrite things on the Scope, i.e. Environment, or Release
  • To make sure certain stuff is there we let the Enricher backfill (because they were never set on the scope)
    eventLike.Environment ??= _options.SettingLocator.GetEnvironment();
  • And now we have a secondary processing pipeline with the SetDefaultAttributes that lives outside of either mechanism.

Personally, I am heavily in favour of having the scope as the source of truth. Especially, since the scope is used to sync data between the C# and the native layer.
And with this change whatever is set on the scope is at least getting applied where previously, any user set Environment on the Scope would just get ignored.

Behaviour before this PR

Two separate inconsistent pipelines:

1. Events / traces (IEventLike) — scope wins, Options is only a fallback

  • Scope.Environment / Scope.Release are plain settable properties, default null (Scope.cs:142, 148). The constructor does not seed them from Options (Scope.cs:287).
  • Scope.Apply copies scope→event only if unset: other.Environment ??= Environment; other.Release ??= Release; (Scope.cs:491-493).
  • Then the Enricher backfills from Options/env-var/default, again only if still null: eventLike.Environment ??= _options.SettingLocator.GetEnvironment(); (Enricher.cs:75,81).

So if a user sets scope.Environment = "Staging", that wins and Options never overrides it. Options is purely the fallback.

2. Logs / metrics (SetDefaultAttributes) — scope is ignored, Options wins

  • SetDefaultAttributes reads straight from SettingLocator, never looking at the scope: options.SettingLocator.GetEnvironment() / GetRelease() (SentryAttributes.cs:100-109).
  • SettingLocator.GetEnvironment/GetRelease resolve Options.Environment ?? SENTRY_ENVIRONMENT ?? default (SettingLocator.cs:80,105) — and, notably, cache the resolved value back onto Options (_options.Environment = environment).

So for logs, a user-set scope.Environment is silently ignored — which is exactly bitsandfoxes' complaint ("any user set Environment on the Scope would just get ignored"). That statement is true for the logs pipeline, not for events.

What sentry-python and sentry-java do

Both take the opposite stance from "scope as source of truth":

  • sentry-python: Scope has no environment/release fields. They live in client options. On events, _prepare_event fills them only if absent: if event.get(key) is None and self.options[key] is not None: event[key] = ... for release/environment. Logs (set_global_attributes) read directly from options. There is no per-scope override for these fields.
  • sentry-java: Scope has no getEnvironment/setEnvironment/getRelease/setRelease. MainEventProcessor does if (event.getRelease() == null) event.setRelease(options.getRelease()); and likewise for environment. Again, options-level only, no scope override.

In both, environment/release are effectively program-lifetime invariants set at init on options.

Recommendation ???

If we want consistency and to avoid the whole clone/override bug class, the java/python model is the cleaner solution??? @bitsandfoxes what do you think of this?

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Jul 10, 2026
@bitsandfoxes bitsandfoxes changed the title read from scope first fix: fetch default attributes from Scope first Jul 10, 2026
Comment thread src/Sentry/Scope.cs
Comment thread src/Sentry/Scope.cs
PropagationContext = new SentryPropagationContext(propagationContext);

_environment = Options.Environment;
Release = Options.Release;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PushScope ignores custom Release

Medium Severity

Initializing each scope’s Release from Options means cloned scopes already have a non-null release before Apply runs. Parent release overrides set via ConfigureScope are skipped on PushScope, so child scopes keep the options release instead of the parent’s value for default attributes and other scope-backed telemetry.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 55b2d5d. Configure here.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an immediate solution (would require a major version bump) but maybe reinforces this idea:

In the interim, it seems like Options should only be used to populate the root scope. Any scope that gets cloned after that should probably just apply itself (not the options) to the new clone.

Kind of opinionated, but can anyone see a reason to do it otherwise?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then ... I believe ... we have some unintended behavior!

By setting Release = Options.Release; in the Scope's .ctor, when pushing a Scope:

  1. SentryScopeManager.PushScope calls Scope.Clone (when not in Global-Mode and when not Locked)
  2. Scope.Clone instantiates a new Scope by passing Options and PropagationContext
    • Release gets set on the new Scope instance from the Options
  3. Apply the current Scope onto the new Scope
    • which only sets Release when null
    • other.Release ??= Release;
      other.Distribution ??= Distribution;
      other.Environment ??= Environment;
      other.TransactionName ??= TransactionName;
      other.Level ??= Level;

So Release is not only applied to the root Scope, and then copied over from the root Scope to pushed Scopes,
but actually always applied from the Options, so that the current Scope's Release is overwritten.

SentrySdk.Init(options =>
{
    options.Release = "options-release";
});

SentrySdk.ConfigureScope(static scope =>
{
    scope.Release = "scope-release";
});
var scope = SentrySdk.PushScope();
SentrySdk.ConfigureScope(static scope =>
{
    Console.WriteLine(scope.Release); //options-release
});

Perhaps we need a facility, where we have a CreateRootScope factory method, that applies Release and other SentryOptions, and when later pushing a new Scope, we call existing .ctor with existing behavior that does not apply Release from the Options, but from the current Scope instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH it's a bit weird for stuff like Environment and Release to be on the scope at all. Would these ever change during the program's execution? If the SDK user sets these to Staging and 7.6.5 then they would expect those values to be attached to all events/traces/etc (in the .NET, Apple, Android and Native SDKs).

We seem to be using the scope as a vehicle to apply these properties to things that are IEventLike. Is there a real world scenario where the user sets one of these things in the options and then sets it to something else on the scope though? Or where they set them on the root scope and then want to push a new scope that has a different value for these?

Note

sentry-python and sentry-java don't put environment/release on the scope at all — they're options-level settings

So maybe we go the opposite direction? When Environment and Release are set on the Options, that always overrides whatever is on the scope (so these things become invariants)?

And in the next major, we see if we can remove these from the Scope entirely. They should probably never have been added there.

@bitsandfoxes thoughts 🤔

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.19%. Comparing base (52db5d3) to head (4db9f38).

Additional details and impacted files
@@                       Coverage Diff                       @@
##           feat/sync-scope-environment    #5376      +/-   ##
===============================================================
- Coverage                        74.24%   74.19%   -0.06%     
===============================================================
  Files                              509      509              
  Lines                            18436    18404      -32     
  Branches                          3610     3605       -5     
===============================================================
- Hits                             13688    13655      -33     
- Misses                            3874     3875       +1     
  Partials                           874      874              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread test/Sentry.Tests/SentryMetricTests.cs
Comment thread test/Sentry.Tests/SentryLogTests.cs
Comment thread test/Sentry.Tests/SentryMetricTests.cs Outdated
Comment thread test/Sentry.Tests/SentryMetricTests.cs Outdated
Comment thread src/Sentry/Scope.cs
Comment thread src/Sentry/SentrySdk.cs

// This happens before the native SDKs get initialized
options.Environment = options.SettingLocator.GetEnvironment();
options.Release = options.SettingLocator.GetRelease();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏻

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a0babfe. Configure here.

Comment thread src/Sentry/SentrySdk.cs

// This happens before the native SDKs get initialized
options.Environment = options.SettingLocator.GetEnvironment();
options.Release = options.SettingLocator.GetRelease();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release init blocks mobile defaults

High Severity

Calling SettingLocator.GetRelease() before native SDK init eagerly fills options.Release from ApplicationVersionLocator. Android and Cocoa then skip their options.Release ??= GetDefaultReleaseString() defaults, so mobile apps get an assembly-based release instead of the package @version+build format used for release health and debug-file matching.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a0babfe. Configure here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed:

This does now apply SentryOptions.Release for Native:

if (options.Release is not null)
{
options.DiagnosticLogger?.LogDebug("Setting Release: {0}", options.Release);
sentry_options_set_release(cOptions, options.Release);
}

But, it changes existing behavior for Android

options.Release ??= GetDefaultReleaseString();

and also iOS / Mac Catalyst

options.Release ??= GetDefaultReleaseString();

Do we intend this change in behavior for Android/Apple?

@github-actions github-actions Bot added risk: high PR risk score: high and removed risk: medium PR risk score: medium labels Jul 13, 2026
Comment thread CHANGELOG.md
Comment on lines +10 to +13
### Fixes

- fix: When setting `Environment` and `Release` on the `Scope` these are now also getting applied on emitted Metrics and Logs by bitsandfoxes [#5376](https://github.com/getsentry/sentry-dotnet/pull/5376)

@Flash0ver Flash0ver Jul 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: don't edit CHANGELOG.md manually

Sorry for the confusion I've caused ... I meant to add a Changelog Entry indirectly via ### Changelog Entry in the PR description (https://craft.sentry.dev/configuration/#custom-changelog-entries-from-pr-descriptions).

This way we can have "great changelog entries" while avoiding merge conflicts when editing CHANGELOG.md manually.

We just merged new guidance: #5369

Comment thread src/Sentry/Scope.cs

// Skip scope sync with backing field. The native SDKs already received the environment set on the options.
_environment = Options.Environment;
Release = Options.Release;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Distribution

Do we need similar treatment for Distribution ... to apply it from the SentryOptions onto the (root) Scope?

Comment thread src/Sentry/Scope.cs
PropagationContext = new SentryPropagationContext(propagationContext);

_environment = Options.Environment;
Release = Options.Release;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then ... I believe ... we have some unintended behavior!

By setting Release = Options.Release; in the Scope's .ctor, when pushing a Scope:

  1. SentryScopeManager.PushScope calls Scope.Clone (when not in Global-Mode and when not Locked)
  2. Scope.Clone instantiates a new Scope by passing Options and PropagationContext
    • Release gets set on the new Scope instance from the Options
  3. Apply the current Scope onto the new Scope
    • which only sets Release when null
    • other.Release ??= Release;
      other.Distribution ??= Distribution;
      other.Environment ??= Environment;
      other.TransactionName ??= TransactionName;
      other.Level ??= Level;

So Release is not only applied to the root Scope, and then copied over from the root Scope to pushed Scopes,
but actually always applied from the Options, so that the current Scope's Release is overwritten.

SentrySdk.Init(options =>
{
    options.Release = "options-release";
});

SentrySdk.ConfigureScope(static scope =>
{
    scope.Release = "scope-release";
});
var scope = SentrySdk.PushScope();
SentrySdk.ConfigureScope(static scope =>
{
    Console.WriteLine(scope.Release); //options-release
});

Perhaps we need a facility, where we have a CreateRootScope factory method, that applies Release and other SentryOptions, and when later pushing a new Scope, we call existing .ctor with existing behavior that does not apply Release from the Options, but from the current Scope instead.

Comment thread src/Sentry/SentrySdk.cs

// This happens before the native SDKs get initialized
options.Environment = options.SettingLocator.GetEnvironment();
options.Release = options.SettingLocator.GetRelease();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirmed:

This does now apply SentryOptions.Release for Native:

if (options.Release is not null)
{
options.DiagnosticLogger?.LogDebug("Setting Release: {0}", options.Release);
sentry_options_set_release(cOptions, options.Release);
}

But, it changes existing behavior for Android

options.Release ??= GetDefaultReleaseString();

and also iOS / Mac Catalyst

options.Release ??= GetDefaultReleaseString();

Do we intend this change in behavior for Android/Apple?

Base automatically changed from feat/sync-scope-environment to main July 15, 2026 01:17

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might point to a more fundamental difference between how python/java are doing things vs .net/cocoa.

I suspect the java/python approach makes more sense for these properties (these properties only exist on the options for those SDKs - not on the scope).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants