diff --git a/src/Sentry/IScopeObserver.cs b/src/Sentry/IScopeObserver.cs index 870a160a30..3d9dcdce90 100644 --- a/src/Sentry/IScopeObserver.cs +++ b/src/Sentry/IScopeObserver.cs @@ -35,6 +35,11 @@ public interface IScopeObserver /// public void SetTrace(SentryId traceId, SpanId parentSpanId); + /// + /// Sets the environment. + /// + public void SetEnvironment(string? environment); + /// /// Adds an attachment. /// diff --git a/src/Sentry/Internal/ScopeObserver.cs b/src/Sentry/Internal/ScopeObserver.cs index 28f4676f84..0ba44c98a5 100644 --- a/src/Sentry/Internal/ScopeObserver.cs +++ b/src/Sentry/Internal/ScopeObserver.cs @@ -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, diff --git a/src/Sentry/Platforms/Android/AndroidScopeObserver.cs b/src/Sentry/Platforms/Android/AndroidScopeObserver.cs index e18b215562..c4d3e91640 100644 --- a/src/Sentry/Platforms/Android/AndroidScopeObserver.cs +++ b/src/Sentry/Platforms/Android/AndroidScopeObserver.cs @@ -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 diff --git a/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs b/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs index f92156a4be..46f8aebf5e 100644 --- a/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs +++ b/src/Sentry/Platforms/Cocoa/CocoaScopeObserver.cs @@ -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 diff --git a/src/Sentry/Platforms/Native/CFunctions.cs b/src/Sentry/Platforms/Native/CFunctions.cs index 5faa149720..72105155e8 100644 --- a/src/Sentry/Platforms/Native/CFunctions.cs +++ b/src/Sentry/Platforms/Native/CFunctions.cs @@ -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 LoadDebugImages(IDiagnosticLogger? logger) { // It only makes sense to load them once because they're cached on the native side anyway. We could force diff --git a/src/Sentry/Platforms/Native/NativeScopeObserver.cs b/src/Sentry/Platforms/Native/NativeScopeObserver.cs index f9034df998..1d6956a9f4 100644 --- a/src/Sentry/Platforms/Native/NativeScopeObserver.cs +++ b/src/Sentry/Platforms/Native/NativeScopeObserver.cs @@ -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 diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 62b926cb7f..d062290133 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -145,7 +145,32 @@ public SentryUser User public string? Distribution { get; set; } /// - public string? Environment { get; set; } + public string? Environment + { + get; + 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; + } + else + { + field = value; + } + + if (Options is { EnableScopeSync: true, ScopeObserver: { } observer }) + { + observer.SetEnvironment(field); + } + } + } // TransactionName is kept for legacy purposes because // SentryEvent still makes use of it. diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index a6e31c57af..e0067fafb8 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -63,6 +63,9 @@ 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(); + // Initialize native platform SDKs here if (options.InitNativeSdks) { diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index 35d2d30f47..1a6c3e737d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -221,6 +221,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); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 35d2d30f47..1a6c3e737d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -221,6 +221,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); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index 35d2d30f47..1a6c3e737d 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -221,6 +221,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); diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index fe7b7cab37..ae30b18e3e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -209,6 +209,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); diff --git a/test/Sentry.Tests/ScopeTests.cs b/test/Sentry.Tests/ScopeTests.cs index ccb1b277bf..0e04ec4b34 100644 --- a/test/Sentry.Tests/ScopeTests.cs +++ b/test/Sentry.Tests/ScopeTests.cs @@ -113,6 +113,21 @@ public void Clone_CopiesFields() Assert.Equal(_sut.Environment, clone.Environment); } + [Fact] + public void Clone_EnvironmentOverridesOptions_OverridePreserved() + { + // Arrange + var options = new SentryOptions { Environment = "production" }; + var scope = new Scope(options); + scope.Environment = "staging"; + + // Act + var clone = scope.Clone(); + + // Assert + clone.Environment.Should().Be("staging"); + } + [Fact] public void TransactionName_TransactionNotStarted_NameIsSet() { @@ -696,6 +711,84 @@ 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(); + 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 optionsEnvironment = "production"; + var scope = new Scope(new SentryOptions { Environment = optionsEnvironment }); + scope.Environment = "staging"; // Override before resetting + + // Act + scope.Environment = null; + + // Assert + scope.Environment.Should().Be(optionsEnvironment); + } + + [Fact] + public void SetEnvironment_Null_ObserverReceivesOptionEnvironment() + { + // Arrange + const string optionsEnvironment = "production"; + var observer = Substitute.For(); + var scope = new Scope(new SentryOptions + { + ScopeObserver = observer, + EnableScopeSync = true, + Environment = optionsEnvironment + }); + scope.Environment = "staging"; // Override before resetting + observer.ClearReceivedCalls(); + + // Act + scope.Environment = null; + + // Assert + observer.Received(1).SetEnvironment(Arg.Is(optionsEnvironment)); + } + + [Fact] + public void SetEnvironment_SameValue_ObserverNotifiedOnce() + { + // Arrange + var observer = Substitute.For(); + 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() {