From 9b7efbe2e29325411eba0afe02b0f2bdce5ee5ba Mon Sep 17 00:00:00 2001 From: James Crosswell Date: Thu, 17 Sep 2026 13:22:03 +1200 Subject: [PATCH] feat!: make Scope.Environment non-nullable When no environment has been set on the scope, the getter now falls back to the environment resolved from the options (options, SENTRY_ENVIRONMENT, then the default), so it never returns null. Closes #5388 Co-Authored-By: Claude Opus 5 --- src/Sentry/Scope.cs | 36 +++++++++++------ ...Populate_RouteData_SetToScope.verified.txt | 1 + ...iApprovalTests.Run.DotNet10_0.verified.txt | 2 +- ...iApprovalTests.Run.DotNet11_0.verified.txt | 2 +- ...piApprovalTests.Run.DotNet8_0.verified.txt | 2 +- ...piApprovalTests.Run.DotNet9_0.verified.txt | 2 +- .../ApiApprovalTests.Run.Net4_8.verified.txt | 2 +- test/Sentry.Tests/Protocol/ScopeTests.cs | 4 +- test/Sentry.Tests/ScopeTests.cs | 39 +++++++++++++++++++ 9 files changed, 71 insertions(+), 19 deletions(-) diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index d062290133..2d8fb973c0 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -144,30 +144,32 @@ public SentryUser User /// public string? Distribution { get; set; } + private string? _environment; + /// - public string? Environment + /// Setting this to null reverts to the environment resolved from the options. + [AllowNull] + public string Environment { - get; + get => _environment ?? Options.SettingLocator.GetEnvironment(); set { - if (field == value) - { - return; - } - if (value is null) { Options.LogDebug("Environment cannot be null. Reverting to default value from the options."); - field = Options.Environment; + value = Options.SettingLocator.GetEnvironment(); } - else + + if (_environment == value) { - field = value; + return; } + _environment = value; + if (Options is { EnableScopeSync: true, ScopeObserver: { } observer }) { - observer.SetEnvironment(field); + observer.SetEnvironment(value); } } } @@ -515,7 +517,17 @@ public void Apply(IEventLike other) other.Release ??= Release; other.Distribution ??= Distribution; - other.Environment ??= Environment; + if (other is Scope otherScope) + { + if (otherScope._environment is null && _environment is not null) + { + otherScope.Environment = _environment; + } + } + else + { + other.Environment ??= Environment; + } other.TransactionName ??= TransactionName; other.Level ??= Level; diff --git a/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.Populate_RouteData_SetToScope.verified.txt b/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.Populate_RouteData_SetToScope.verified.txt index 8ba783b0dd..efda02a581 100644 --- a/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.Populate_RouteData_SetToScope.verified.txt +++ b/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.Populate_RouteData_SetToScope.verified.txt @@ -4,6 +4,7 @@ QueryString: }, User: {}, + Environment: production, TransactionName: GET Ctrl.Actn, Tags: { route.action: Actn, diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 803c8f81b3..efdfc57015 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -404,7 +404,7 @@ namespace Sentry public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } public Sentry.SentryContexts Contexts { get; set; } public string? Distribution { get; set; } - public string? Environment { get; set; } + public string Environment { get; set; } public System.Collections.Generic.IReadOnlyDictionary Extra { get; } public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } public Sentry.SentryLevel? Level { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet11_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet11_0.verified.txt index 803c8f81b3..efdfc57015 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet11_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet11_0.verified.txt @@ -404,7 +404,7 @@ namespace Sentry public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } public Sentry.SentryContexts Contexts { get; set; } public string? Distribution { get; set; } - public string? Environment { get; set; } + public string Environment { get; set; } public System.Collections.Generic.IReadOnlyDictionary Extra { get; } public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } public Sentry.SentryLevel? Level { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 803c8f81b3..efdfc57015 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -404,7 +404,7 @@ namespace Sentry public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } public Sentry.SentryContexts Contexts { get; set; } public string? Distribution { get; set; } - public string? Environment { get; set; } + public string Environment { get; set; } public System.Collections.Generic.IReadOnlyDictionary Extra { get; } public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } public Sentry.SentryLevel? Level { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 803c8f81b3..efdfc57015 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -404,7 +404,7 @@ namespace Sentry public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } public Sentry.SentryContexts Contexts { get; set; } public string? Distribution { get; set; } - public string? Environment { get; set; } + public string Environment { get; set; } public System.Collections.Generic.IReadOnlyDictionary Extra { get; } public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } public Sentry.SentryLevel? Level { get; set; } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 89bf5488a8..e9c923778b 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -392,7 +392,7 @@ namespace Sentry public System.Collections.Generic.IReadOnlyCollection Breadcrumbs { get; } public Sentry.SentryContexts Contexts { get; set; } public string? Distribution { get; set; } - public string? Environment { get; set; } + public string Environment { get; set; } public System.Collections.Generic.IReadOnlyDictionary Extra { get; } public System.Collections.Generic.IReadOnlyList Fingerprint { get; set; } public Sentry.SentryLevel? Level { get; set; } diff --git a/test/Sentry.Tests/Protocol/ScopeTests.cs b/test/Sentry.Tests/Protocol/ScopeTests.cs index 4ac9bfae4d..d6b7f26130 100644 --- a/test/Sentry.Tests/Protocol/ScopeTests.cs +++ b/test/Sentry.Tests/Protocol/ScopeTests.cs @@ -958,7 +958,7 @@ public void Apply_Sdk_SourceNone_TargetSingle_DoesNotModifyTarget() } [Fact] - public void Apply_Environment_Null() + public void Apply_Environment_Null_TargetUsesOptionsEnvironment() { var sut = _fixture.GetSut(); sut.Environment = null; @@ -966,7 +966,7 @@ public void Apply_Environment_Null() var target = _fixture.GetSut(); sut.Apply(target); - Assert.Null(target.Environment); + Assert.Equal(target.Options.SettingLocator.GetEnvironment(), target.Environment); } [Fact] diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index 0e04ec4b34..1ef6924990 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -770,6 +770,45 @@ public void SetEnvironment_Null_ObserverReceivesOptionEnvironment() observer.Received(1).SetEnvironment(Arg.Is(optionsEnvironment)); } + [Fact] + public void Environment_NotSet_ReturnsOptionsEnvironment() + { + var scope = new Scope(new SentryOptions { Environment = "staging" }); + + scope.Environment.Should().Be("staging"); + } + + [Fact] + public void Environment_NotSetInOptions_ReturnsDefaultEnvironment() + { + var scope = new Scope(new SentryOptions()); + + scope.Environment.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public void Apply_EnvironmentNotSetOnScope_EventGetsOptionsEnvironment() + { + var scope = new Scope(new SentryOptions { Environment = "staging" }); + var evt = new SentryEvent(); + + scope.Apply(evt); + + evt.Environment.Should().Be("staging"); + } + + [Fact] + public void Apply_EnvironmentSetOnTarget_TargetEnvironmentPreserved() + { + var options = new SentryOptions { Environment = "production" }; + var source = new Scope(options) { Environment = "staging" }; + var target = new Scope(options) { Environment = "development" }; + + source.Apply(target); + + target.Environment.Should().Be("development"); + } + [Fact] public void SetEnvironment_SameValue_ObserverNotifiedOnce() {