Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
### Features ✨

- feat: Add exponential backoff and log deduplication to Spotlight transport by @mattico in [#5025](https://github.com/getsentry/sentry-dotnet/pull/5025)
- feat: The `Environment` set on the `Scope` now gets synchronized to the native layers (`sentry-cocoa` and `sentry-native`) by @bitsandfoxes [#5365](https://github.com/getsentry/sentry-dotnet/pull/5365)

### 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)

Comment on lines +10 to +13

@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

## 6.6.0

Expand Down
5 changes: 5 additions & 0 deletions src/Sentry/IScopeObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public interface IScopeObserver
/// </summary>
public void SetTrace(SentryId traceId, SpanId parentSpanId);

/// <summary>
/// Sets the environment.
/// </summary>
public void SetEnvironment(string? environment);

/// <summary>
/// Adds an attachment.
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions src/Sentry/Internal/ScopeObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ public void SetTrace(SentryId traceId, SpanId parentSpanId)

public abstract void SetTraceImpl(SentryId traceId, SpanId parentSpanId);

public void SetEnvironment(string? environment)
{
_options.DiagnosticLogger?.Log(SentryLevel.Debug,
"{0} Scope Sync - Setting Environment e:\"{1}\"", null, _name, environment);
SetEnvironmentImpl(environment);
}

public abstract void SetEnvironmentImpl(string? environment);

public void AddAttachment(SentryAttachment attachment)
{
_options.DiagnosticLogger?.Log(SentryLevel.Debug,
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Platforms/Android/AndroidScopeObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ public void SetTrace(SentryId traceId, SpanId parentSpanId)
}
}

public void SetEnvironment(string? environment)
{
try
{
// TODO: Missing corresponding scope-level functionality on the Android SDK
}
finally
{
_innerObserver?.SetEnvironment(environment);
}
}

public void AddAttachment(SentryAttachment attachment)
{
try
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,18 @@ public void SetTrace(SentryId traceId, SpanId parentSpanId)
}
}

public void SetEnvironment(string? environment)
{
try
{
SentryCocoaSdk.ConfigureScope(scope => scope.SetEnvironment(environment));
}
finally
{
_innerObserver?.SetEnvironment(environment);
}
}

public void AddAttachment(SentryAttachment attachment)
{
try
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry/Platforms/Native/CFunctions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ internal static string GetCacheDirectory(SentryOptions options)
[DllImport("sentry-native")]
internal static extern void sentry_set_trace(string traceId, string parentSpanId);

[DllImport("sentry-native")]
internal static extern void sentry_set_environment(string? environment);

internal static Dictionary<long, DebugImage> LoadDebugImages(IDiagnosticLogger? logger)
{
// It only makes sense to load them once because they're cached on the native side anyway. We could force
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry/Platforms/Native/NativeScopeObserver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public override void SetUserImpl(SentryUser user)
public override void SetTraceImpl(SentryId traceId, SpanId parentSpanId) =>
C.sentry_set_trace(traceId.ToString(), parentSpanId.ToString());

public override void SetEnvironmentImpl(string? environment) =>
C.sentry_set_environment(environment);

public override void AddAttachmentImpl(SentryAttachment attachment)
{
// TODO: Missing corresponding functionality on the Native SDK
Expand Down
7 changes: 4 additions & 3 deletions src/Sentry/Protocol/SentryAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,18 @@ internal void SetAttribute(string key, int value)
this[key] = new SentryAttribute(value, "integer");
}

internal void SetDefaultAttributes(SentryOptions options, SdkVersion sdk)
internal void SetDefaultAttributes(SentryOptions options, Scope? scope, SdkVersion? sdk = null)
{
var environment = options.SettingLocator.GetEnvironment();
var environment = scope?.Environment ?? options.SettingLocator.GetEnvironment();
SetAttribute("sentry.environment", environment);

var release = options.SettingLocator.GetRelease();
var release = scope?.Release ?? options.SettingLocator.GetRelease();
if (release is not null)
{
SetAttribute("sentry.release", release);
}

sdk ??= scope?.Sdk ?? SdkVersion.Instance;
if (sdk.Name is { } name)
{
SetAttribute("sentry.sdk.name", name);
Expand Down
37 changes: 34 additions & 3 deletions src/Sentry/Scope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,35 @@ public SentryUser User
/// <inheritdoc />
public string? Distribution { get; set; }

private string? _environment;

/// <inheritdoc />
public string? Environment { get; set; }
public string? Environment
{
get => _environment;
set
{
if (_environment == value)
{
return;
}

if (value is null)
{
Options.LogWarning("Environment cannot be null. Reverting to default value from the options.");
_environment = Options.Environment;
}
else
{
_environment = value;
}

if (Options is { EnableScopeSync: true, ScopeObserver: { } observer })
{
observer.SetEnvironment(_environment);
}
}
}

// TransactionName is kept for legacy purposes because
// SentryEvent still makes use of it.
Expand Down Expand Up @@ -288,6 +315,10 @@ internal Scope(SentryOptions? options, SentryPropagationContext? propagationCont
{
Options = options ?? new SentryOptions();
PropagationContext = new SentryPropagationContext(propagationContext);

// 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

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 🤔

Comment thread
Flash0ver marked this conversation as resolved.

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?

}

// For testing. Should explicitly require SentryOptions.
Expand Down Expand Up @@ -404,9 +435,9 @@ public void Clear()
Request = new();
Contexts.Clear();
User = new();
Release = default;
Release = Options.Release;
Distribution = default;
Environment = default;
Environment = Options.Environment; // Restore to keep in sync with native
TransactionName = default;
Transaction = default;
Fingerprint = Array.Empty<string>();
Expand Down
3 changes: 1 addition & 2 deletions src/Sentry/SentryLog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,7 @@ public bool TryGetAttribute(string key, [NotNullWhen(true)] out object? value) =
internal void SetDefaultAttributes(SentryOptions options, Scope? scope, SdkVersion? sdk = null)
{
// Core Attributes
sdk ??= scope?.Sdk ?? SdkVersion.Instance;
Attributes.SetDefaultAttributes(options, sdk);
Attributes.SetDefaultAttributes(options, scope, sdk);

// Server Attributes
if (!string.IsNullOrEmpty(options.ServerName))
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/SentryMetric.Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private static SentryMetric<T> CreateCore<T>(IHub hub, SentryOptions options, IS
};

scope ??= hub.GetScope();
metric.Attributes.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance);
metric.Attributes.SetDefaultAttributes(options, scope);

return metric;
}
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ internal static IHub InitHub(SentryOptions options)
#pragma warning restore 0162
#pragma warning restore CS0162 // Unreachable code detected

// 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.

👍🏻

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?


// Initialize native platform SDKs here
if (options.InitNativeSdks)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ namespace Sentry
void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
void ClearAttachments();
void SetEnvironment(string? environment);
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ namespace Sentry
void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
void ClearAttachments();
void SetEnvironment(string? environment);
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ namespace Sentry
void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
void ClearAttachments();
void SetEnvironment(string? environment);
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ namespace Sentry
void AddAttachment(Sentry.SentryAttachment attachment);
void AddBreadcrumb(Sentry.Breadcrumb breadcrumb);
void ClearAttachments();
void SetEnvironment(string? environment);
void SetExtra(string key, object? value);
void SetTag(string key, string value);
void SetTrace(Sentry.SentryId traceId, Sentry.SpanId parentSpanId);
Expand Down Expand Up @@ -2029,4 +2030,4 @@ public static class SentryExceptionExtensions
public static void AddSentryContext(this System.Exception ex, string name, System.Collections.Generic.IReadOnlyDictionary<string, object> data) { }
public static void AddSentryTag(this System.Exception ex, string name, string value) { }
public static void SetSentryMechanism(this System.Exception ex, string type, string? description = null, bool? handled = default, bool? terminal = default) { }
}
}
99 changes: 99 additions & 0 deletions test/Sentry.Tests/ScopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,29 @@ public void Clear_SetsPropertiesToDefaultValues()
}
}

[Fact]
public void Clear_OptionsHaveEnvironmentAndRelease_RestoresToOptionsValues()
{
// Arrange
var options = new SentryOptions
{
Environment = "options-environment",
Release = "options-release",
};
var sut = new Scope(options);
sut.ApplyFakeValues();

// Act
sut.Clear();

// Assert
using (new AssertionScope())
{
sut.Environment.Should().Be("options-environment");
sut.Release.Should().Be("options-release");
}
}

[Fact]
public void Clear_ResetsPropagationContext()
{
Expand Down Expand Up @@ -696,6 +719,82 @@ public void AddBreadcrumb_ObserverExist_ObserverAddsBreadcrumbIfEnabled(bool obs
observer.Received(expectedCount).AddBreadcrumb(Arg.Is(breadcrumb));
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void SetEnvironment_ObserverExist_ObserverSetsEnvironmentIfEnabled(bool observerEnable)
{
// Arrange
var observer = Substitute.For<IScopeObserver>();
var scope = new Scope(new SentryOptions
{
ScopeObserver = observer,
EnableScopeSync = observerEnable
});
const string expectedEnvironment = "staging";
var expectedCount = observerEnable ? 1 : 0;

// Act
scope.Environment = expectedEnvironment;

// Assert
observer.Received(expectedCount).SetEnvironment(Arg.Is(expectedEnvironment));
}

[Fact]
public void SetEnvironment_Null_EnvironmentSetToOptionEnvironment()
{
// Arrange
const string expectedEnvironment = "staging";
var scope = new Scope(new SentryOptions { Environment = expectedEnvironment });

// Act
scope.Environment = null;

// Assert
scope.Environment.Should().Be(expectedEnvironment);
}

[Fact]
public void SetEnvironment_Null_ObserverReceivesOptionEnvironment()
{
// Arrange
const string expectedEnvironment = "staging";
var observer = Substitute.For<IScopeObserver>();
var scope = new Scope(new SentryOptions
{
ScopeObserver = observer,
EnableScopeSync = true,
Environment = expectedEnvironment
});
observer.ClearReceivedCalls();

// Act
scope.Environment = null;

// Assert
observer.Received(1).SetEnvironment(Arg.Is(expectedEnvironment));
}

[Fact]
public void SetEnvironment_SameValue_ObserverNotifiedOnce()
{
// Arrange
var observer = Substitute.For<IScopeObserver>();
var scope = new Scope(new SentryOptions
{
ScopeObserver = observer,
EnableScopeSync = true
});

// Act
scope.Environment = "staging";
scope.Environment = "staging";

// Assert
observer.Received(1).SetEnvironment(Arg.Is("staging"));
}

[Fact]
public void Filtered_tags_are_not_set()
{
Expand Down
Loading
Loading