fix: fetch default attributes from Scope first#5376
Conversation
…entry/sentry-dotnet into feat/sync-scope-environment
…entry/sentry-dotnet into feat/sync-scope-environment
Scope first
| PropagationContext = new SentryPropagationContext(propagationContext); | ||
|
|
||
| _environment = Options.Environment; | ||
| Release = Options.Release; |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 55b2d5d. Configure here.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Then ... I believe ... we have some unintended behavior!
By setting Release = Options.Release; in the Scope's .ctor, when pushing a Scope:
SentryScopeManager.PushScopecallsScope.Clone(when not in Global-Mode and when not Locked)Scope.Cloneinstantiates a newScopeby passingOptionsandPropagationContextReleasegets set on the newScopeinstance from theOptions
Applythe currentScopeonto the newScope- which only sets
Releasewhennull sentry-dotnet/src/Sentry/Scope.cs
Lines 522 to 526 in 8efd196
- which only sets
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.
There was a problem hiding this comment.
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
|
|
||
| // This happens before the native SDKs get initialized | ||
| options.Environment = options.SettingLocator.GetEnvironment(); | ||
| options.Release = options.SettingLocator.GetRelease(); |
Co-authored-by: Stefan Pölz <38893694+Flash0ver@users.noreply.github.com>
…m/getsentry/sentry-dotnet into fix/default-attributes-from-scope
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
|
|
||
| // This happens before the native SDKs get initialized | ||
| options.Environment = options.SettingLocator.GetEnvironment(); | ||
| options.Release = options.SettingLocator.GetRelease(); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit a0babfe. Configure here.
There was a problem hiding this comment.
confirmed:
This does now apply SentryOptions.Release for Native:
sentry-dotnet/src/Sentry/Platforms/Native/CFunctions.cs
Lines 89 to 93 in cc9de07
But, it changes existing behavior for Android
and also iOS / Mac Catalyst
Do we intend this change in behavior for Android/Apple?
| ### 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) | ||
|
|
There was a problem hiding this comment.
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
|
|
||
| // Skip scope sync with backing field. The native SDKs already received the environment set on the options. | ||
| _environment = Options.Environment; | ||
| Release = Options.Release; |
There was a problem hiding this comment.
question: Distribution
Do we need similar treatment for Distribution ... to apply it from the SentryOptions onto the (root) Scope?
| PropagationContext = new SentryPropagationContext(propagationContext); | ||
|
|
||
| _environment = Options.Environment; | ||
| Release = Options.Release; |
There was a problem hiding this comment.
Then ... I believe ... we have some unintended behavior!
By setting Release = Options.Release; in the Scope's .ctor, when pushing a Scope:
SentryScopeManager.PushScopecallsScope.Clone(when not in Global-Mode and when not Locked)Scope.Cloneinstantiates a newScopeby passingOptionsandPropagationContextReleasegets set on the newScopeinstance from theOptions
Applythe currentScopeonto the newScope- which only sets
Releasewhennull sentry-dotnet/src/Sentry/Scope.cs
Lines 522 to 526 in 8efd196
- which only sets
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.
|
|
||
| // This happens before the native SDKs get initialized | ||
| options.Environment = options.SettingLocator.GetEnvironment(); | ||
| options.Release = options.SettingLocator.GetRelease(); |
There was a problem hiding this comment.
confirmed:
This does now apply SentryOptions.Release for Native:
sentry-dotnet/src/Sentry/Platforms/Native/CFunctions.cs
Lines 89 to 93 in cc9de07
But, it changes existing behavior for Android
and also iOS / Mac Catalyst
Do we intend this change in behavior for Android/Apple?
There was a problem hiding this comment.
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).


Follows #5365
The issue I see is that
Scopebasically unpopulatedScope, i.e.Environment, orReleaseEnricherbackfill (because they were never set on the scope)sentry-dotnet/src/Sentry/Internal/Enricher.cs
Line 81 in ebef6ff
SetDefaultAttributesthat 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
Environmenton the Scope would just get ignored.Behaviour before this PR
Two separate inconsistent pipelines:
1. Events / traces (
IEventLike) — scope wins, Options is only a fallbackScope.Environment/Scope.Releaseare plain settable properties, defaultnull(Scope.cs:142, 148). The constructor does not seed them from Options (Scope.cs:287).Scope.Applycopies scope→event only if unset:other.Environment ??= Environment; other.Release ??= Release;(Scope.cs:491-493).Enricherbackfills 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 winsSetDefaultAttributesreads straight fromSettingLocator, never looking at the scope:options.SettingLocator.GetEnvironment()/GetRelease()(SentryAttributes.cs:100-109).SettingLocator.GetEnvironment/GetReleaseresolveOptions.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.Environmentis 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":
Scopehas noenvironment/releasefields. They live in clientoptions. On events,_prepare_eventfills them only if absent:if event.get(key) is None and self.options[key] is not None: event[key] = ...forrelease/environment. Logs (set_global_attributes) read directly fromoptions. There is no per-scope override for these fields.Scopehas nogetEnvironment/setEnvironment/getRelease/setRelease.MainEventProcessordoesif (event.getRelease() == null) event.setRelease(options.getRelease());and likewise for environment. Again, options-level only, no scope override.In both,
environment/releaseare 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?