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()
{