From 6f4f13670bfd0d1b18d5004234eecc591eafcb08 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 22:30:47 -0500 Subject: [PATCH 1/9] Add deployment environments to events --- README.md | 9 +++++++++ .../Configuration/ExceptionlessConfiguration.cs | 8 ++++++++ .../Extensions/EventBuilderExtensions.cs | 8 +++++++- .../ExceptionlessConfigurationExtensions.cs | 15 ++++++++++++++- src/Exceptionless/Models/Client/Event.cs | 16 ++++++++++++++-- .../Default/015_ConfigurationDefaultsPlugin.cs | 6 +++++- .../Utility/DeploymentEnvironment.cs | 11 +++++++++++ .../ExceptionlessExtensions.cs | 7 ++++++- .../Serializer/StorageSerializerTestBase.cs | 1 + .../Platforms/HostingExtensionsTests.cs | 13 +++++++++++++ .../015_ConfigurationDefaultsPluginTests.cs | 16 ++++++++++++++++ .../Plugins/910_DuplicateCheckerPluginTests.cs | 15 +++++++++++++++ .../Serializer/Models/EventSerializerTests.cs | 14 ++++++++++++++ 13 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 src/Exceptionless/Utility/DeploymentEnvironment.cs diff --git a/README.md b/README.md index 63d5f844..2410b010 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,15 @@ The definition of the word exceptionless is: to be without exception. [Exception ## Using Exceptionless +Set the deployment environment once at startup, with an optional override on each event: + +```csharp +client.Configuration.SetEnvironment("production"); +client.CreateLog("Deployment complete").SetEnvironment("staging").Submit(); +``` + +The hosting integration falls back to `IHostEnvironment.EnvironmentName`. Explicit configuration wins; `Exceptionless:Environment` and `Exceptionless__Environment` are also supported. Names are trimmed, lowercased, and limited to 64 characters. Missing or invalid values remain unspecified. The top-level event `environment` is separate from machine diagnostics in `data.@environment`. Stacks and fixed versions remain shared across environments. + Refer to the Exceptionless documentation here: [Exceptionless Docs](https://exceptionless.com/docs/). ## Getting Started (Development) diff --git a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs index 348ddae2..722d3333 100644 --- a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs +++ b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs @@ -13,6 +13,14 @@ namespace Exceptionless { public class ExceptionlessConfiguration { + private string _environment; + + /// The default deployment environment for every event. + public string Environment { + get => _environment; + set => _environment = Utility.DeploymentEnvironment.Normalize(value); + } + private const string DEFAULT_SERVER_URL = "https://collector.exceptionless.io"; private const string DEFAULT_CONFIG_SERVER_URL = "https://config.exceptionless.io"; private const string DEFAULT_HEARTBEAT_SERVER_URL = "https://heartbeat.exceptionless.io"; diff --git a/src/Exceptionless/Extensions/EventBuilderExtensions.cs b/src/Exceptionless/Extensions/EventBuilderExtensions.cs index 58323a38..559ba4c4 100644 --- a/src/Exceptionless/Extensions/EventBuilderExtensions.cs +++ b/src/Exceptionless/Extensions/EventBuilderExtensions.cs @@ -4,6 +4,12 @@ namespace Exceptionless { public static class EventBuilderExtensions { + /// Overrides the deployment environment for this event. + public static EventBuilder SetEnvironment(this EventBuilder builder, string environment) { + builder.Target.Environment = environment; + return builder; + } + /// /// Sets the user's identity (ie. email address, username, user id) that the event happened to. /// @@ -109,4 +115,4 @@ public static EventBuilder AddRecentTraceLogEntries(this EventBuilder builder, D return builder; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs index 6050d569..64d9f825 100644 --- a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs +++ b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs @@ -27,6 +27,11 @@ namespace Exceptionless { public static class ExceptionlessConfigurationExtensions { + /// Sets the default deployment environment for every event. + public static void SetEnvironment(this ExceptionlessConfiguration config, string environment) { + config.Environment = environment; + } + private const string INSTALL_ID_KEY = "ExceptionlessInstallId"; /// @@ -412,6 +417,9 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config, throw new ArgumentNullException(nameof(settings)); var section = settings.GetSection("Exceptionless"); + if (section["Environment"] != null) { + config.Environment = section["Environment"]; + } if (Boolean.TryParse(section["Enabled"], out bool enabled) && !enabled) config.Enabled = false; @@ -483,6 +491,11 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config, /// /// The configuration object you want to apply the attribute settings to. public static void ReadFromEnvironmentalVariables(this ExceptionlessConfiguration config) { + string environment = GetEnvironmentalVariable("Exceptionless:Environment") ?? GetEnvironmentalVariable("Exceptionless__Environment"); + if (environment != null) { + config.Environment = environment; + } + string apiKey = GetEnvironmentalVariable("Exceptionless:ApiKey") ?? GetEnvironmentalVariable("Exceptionless__ApiKey"); if (IsValidApiKey(apiKey)) config.ApiKey = apiKey; @@ -576,4 +589,4 @@ private static bool IsValidApiKey(string apiKey) { return !String.IsNullOrEmpty(apiKey) && apiKey != "API_KEY_HERE"; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Models/Client/Event.cs b/src/Exceptionless/Models/Client/Event.cs index 31d233d9..555685c0 100644 --- a/src/Exceptionless/Models/Client/Event.cs +++ b/src/Exceptionless/Models/Client/Event.cs @@ -4,6 +4,15 @@ namespace Exceptionless.Models { [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Event : IData { + private string _environment; + + /// The deployment environment, such as production or staging. + [Json.JsonProperty(NullValueHandling = Json.NullValueHandling.Ignore)] + public string Environment { + get => _environment; + set => _environment = Utility.DeploymentEnvironment.Normalize(value); + } + public Event() { Tags = new TagSet(); Data = new DataDictionary(); @@ -60,7 +69,7 @@ public Event() { public string ReferenceId { get; set; } protected bool Equals(Event other) { - return string.Equals(Type, other.Type) && string.Equals(Source, other.Source) && Tags.CollectionEquals(other.Tags) && string.Equals(Message, other.Message) && string.Equals(Geo, other.Geo) && Value == other.Value && Equals(Data, other.Data); + return string.Equals(Environment, other.Environment) && string.Equals(Type, other.Type) && string.Equals(Source, other.Source) && Tags.CollectionEquals(other.Tags) && string.Equals(Message, other.Message) && string.Equals(Geo, other.Geo) && Value == other.Value && Equals(Data, other.Data); } public override bool Equals(object obj) { @@ -84,6 +93,9 @@ public override int GetHashCode() { hashCode = (hashCode * 397) ^ (Geo == null ? 0 : Geo.GetHashCode()); hashCode = (hashCode * 397) ^ Value.GetHashCode(); hashCode = (hashCode * 397) ^ (Data == null ? 0 : Data.GetCollectionHashCode(_exclusions)); + if (Environment != null) { + hashCode = (hashCode * 397) ^ Environment.GetHashCode(); + } return hashCode; } } @@ -115,4 +127,4 @@ public static class KnownDataKeys { public const string ManualStackingInfo = "@stack"; } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs b/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs index e2257bdc..dfc151f7 100644 --- a/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs +++ b/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs @@ -2,6 +2,10 @@ [Priority(15)] public class ConfigurationDefaultsPlugin : IEventPlugin { public void Run(EventPluginContext context) { + if (context.Event.Environment == null) { + context.Event.Environment = context.Client.Configuration.Environment; + } + foreach (string tag in context.Client.Configuration.DefaultTags) context.Event.Tags.Add(tag); @@ -11,4 +15,4 @@ public void Run(EventPluginContext context) { } } } -} \ No newline at end of file +} diff --git a/src/Exceptionless/Utility/DeploymentEnvironment.cs b/src/Exceptionless/Utility/DeploymentEnvironment.cs new file mode 100644 index 00000000..a0d7f2cc --- /dev/null +++ b/src/Exceptionless/Utility/DeploymentEnvironment.cs @@ -0,0 +1,11 @@ +using System; +using System.Linq; + +namespace Exceptionless.Utility { + internal static class DeploymentEnvironment { + public static string Normalize(string value) { + string name = value?.Trim(); + return String.IsNullOrEmpty(name) || name.Length > 64 || name.Any(Char.IsControl) ? null : name.ToLowerInvariant(); + } + } +} diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index b224111f..cfeaf036 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -60,7 +60,10 @@ public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuil /// The pre-configured instance /// public static IServiceCollection AddExceptionless(this IServiceCollection services, ExceptionlessClient client) { - return services.AddSingleton(client); + return services.AddSingleton(sp => { + client.Configuration.Environment ??= sp.GetService()?.EnvironmentName; + return client; + }); } /// @@ -90,6 +93,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromEnvironmentalVariables(); configure?.Invoke(client.Configuration); + client.Configuration.Environment ??= sp.GetService()?.EnvironmentName; return client; }); @@ -110,6 +114,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromConfiguration(configuration); configure?.Invoke(client.Configuration); + client.Configuration.Environment ??= sp.GetService()?.EnvironmentName; return client; }); diff --git a/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs b/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs index 39bb0db0..9044adc1 100644 --- a/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs +++ b/test/Exceptionless.TestHarness/Serializer/StorageSerializerTestBase.cs @@ -27,6 +27,7 @@ private Event CreateSimpleEvent() { var ev= new Event { Date = DateTime.Now, Message = "Testing", + Environment = "production", Type = Event.KnownTypes.Log, Source = "StorageSerializer" }; diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index 6a783446..a8b15454 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -7,6 +7,19 @@ namespace Exceptionless.Tests.Platforms { public class HostingExtensionsTests { + [Theory] + [InlineData(null, "staging")] + [InlineData("production", "production")] + public void AddExceptionless_DeploymentEnvironment_UsesHostAsFallback(string? configuredEnvironment, string expectedEnvironment) { + var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Staging" }); + var client = new ExceptionlessClient(); + client.Configuration.Environment = configuredEnvironment; + builder.AddExceptionless(client); + + using var services = builder.Services.BuildServiceProvider(); + Assert.Equal(expectedEnvironment, services.GetRequiredService().Configuration.Environment); + } + [Fact] public void AddExceptionless_WhenCalled_RegistersClientAndLifetimeService() { // Arrange diff --git a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs index 5d6da120..9b87ab6b 100644 --- a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs @@ -8,6 +8,22 @@ namespace Exceptionless.Tests.Plugins { public class ConfigurationDefaultsPluginTests : PluginTestBase { + [Fact] + public void Run_DeploymentEnvironment_UsesDefaultAndPreservesEventOverride() { + var client = CreateClient(); + client.Configuration.SetEnvironment(" Production "); + var plugin = new ConfigurationDefaultsPlugin(); + var context = new EventPluginContext(client, new Event()); + plugin.Run(context); + Assert.Equal("production", context.Event.Environment); + + var builder = client.CreateLog("test", "message").SetEnvironment(" Staging "); + var overridden = new EventPluginContext(client, builder.Target); + plugin.Run(overridden); + Assert.Equal("staging", overridden.Event.Environment); + Assert.Empty(overridden.Event.Data); + } + public ConfigurationDefaultsPluginTests(ITestOutputHelper output) : base(output) { } [Fact] diff --git a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs index ef479fc0..fe471345 100644 --- a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs @@ -14,6 +14,21 @@ namespace Exceptionless.Tests.Plugins { public class DuplicateCheckerPluginTests : PluginTestBase { public DuplicateCheckerPluginTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Run_SameEventAcrossEnvironments_OnlyMergesWithinEnvironment() { + var client = CreateClient(); + using (var plugin = new DuplicateCheckerPlugin(TimeSpan.FromMinutes(1))) { + foreach (bool duplicate in new[] { false, true }) { + foreach (string environment in new[] { "production", "staging", null }) { + var builder = client.CreateLog("Environment test").SetEnvironment(environment); + var context = new EventPluginContext(client, builder.Target, builder.PluginContextData); + plugin.Run(context); + Assert.Equal(duplicate, context.Cancel); + } + } + } + } [Fact] public void CanRemoveDuplicateExceptions() { diff --git a/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs b/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs index 3d20f279..719545cd 100644 --- a/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs +++ b/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs @@ -6,6 +6,20 @@ namespace Exceptionless.Tests.Serializer.Models { public class EventSerializerTests : SerializerTestBase { + [Fact] + public void Serialize_DeploymentEnvironment_RoundTripsSeparatelyFromRuntimeMetadata() { + var model = new Event { Environment = " Production " }; + model.Data[Event.KnownDataKeys.EnvironmentInfo] = new EnvironmentInfo { MachineName = "worker-1" }; + string json = Serialize(model); + Assert.Contains("\"environment\":\"production\"", json); + var result = Deserialize(json); + Assert.Equal("production", result.Environment); + Assert.Equal("worker-1", result.GetEnvironmentInfo().MachineName); + Assert.Null(new Event { Environment = new string('x', 65) }.Environment); + Assert.Null(new Event { Environment = " " }.Environment); + Assert.NotEqual(new Event { Environment = "production" }, new Event { Environment = "staging" }); + } + /* lang=json */ private const string MinimalJson = """{"type":"log","source":"app","date":"0001-01-01T00:00:00+00:00","tags":[],"message":null,"geo":null,"value":null,"count":null,"data":{},"reference_id":null}"""; /* lang=json */ From 5713934220378f505017c5cb6a6f0bb03ee00d22 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 22:35:48 -0500 Subject: [PATCH 2/9] Update SourceLink patch to unblock SDK validation --- build/common.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/common.props b/build/common.props index 904a13ee..49ec682b 100644 --- a/build/common.props +++ b/build/common.props @@ -41,7 +41,7 @@ - + From 2ac945eb94542d99d17a99594ef27c659dba27de Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 22:45:39 -0500 Subject: [PATCH 3/9] Preserve client ownership and environment override semantics --- README.md | 2 +- .../Configuration/ExceptionlessSection.cs | 3 ++ .../ExceptionlessConfigurationExtensions.cs | 23 ++++++++-- src/Exceptionless/Models/Client/Event.cs | 7 +++- .../015_ConfigurationDefaultsPlugin.cs | 2 +- .../Utility/DeploymentEnvironment.cs | 11 ++++- .../ExceptionlessExtensions.cs | 6 +-- ...DeploymentEnvironmentConfigurationTests.cs | 42 +++++++++++++++++++ .../Exceptionless.Tests.csproj | 1 + .../Platforms/HostingExtensionsTests.cs | 17 ++++++++ .../015_ConfigurationDefaultsPluginTests.cs | 14 +++++++ 11 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs diff --git a/README.md b/README.md index 2410b010..32bb4277 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ client.Configuration.SetEnvironment("production"); client.CreateLog("Deployment complete").SetEnvironment("staging").Submit(); ``` -The hosting integration falls back to `IHostEnvironment.EnvironmentName`. Explicit configuration wins; `Exceptionless:Environment` and `Exceptionless__Environment` are also supported. Names are trimmed, lowercased, and limited to 64 characters. Missing or invalid values remain unspecified. The top-level event `environment` is separate from machine diagnostics in `data.@environment`. Stacks and fixed versions remain shared across environments. +The hosting integration falls back to `IHostEnvironment.EnvironmentName`. Explicit configuration wins; `Exceptionless:Environment` and `Exceptionless__Environment` are also supported. .NET Framework applications can use the `Exceptionless:Environment` app setting or the `environment` attribute on the `` configuration section. Names are trimmed, lowercased, and limited to 64 characters. Missing or invalid values remain unspecified. The top-level event `environment` is separate from machine diagnostics in `data.@environment`. Stacks and fixed versions remain shared across environments. Refer to the Exceptionless documentation here: [Exceptionless Docs](https://exceptionless.com/docs/). diff --git a/src/Exceptionless/Configuration/ExceptionlessSection.cs b/src/Exceptionless/Configuration/ExceptionlessSection.cs index 9209a1ff..86e0b74f 100644 --- a/src/Exceptionless/Configuration/ExceptionlessSection.cs +++ b/src/Exceptionless/Configuration/ExceptionlessSection.cs @@ -11,6 +11,9 @@ internal class ExceptionlessSection : ConfigurationSection { [ConfigurationProperty("apiKey", IsRequired = true)] public string ApiKey { get { return base["apiKey"] as string; } set { base["apiKey"] = value; } } + [ConfigurationProperty("environment")] + public string Environment { get { return base["environment"] as string; } set { base["environment"] = value; } } + [ConfigurationProperty("serverUrl")] public string ServerUrl { get { return base["serverUrl"] as string; } set { base["serverUrl"] = value; } } diff --git a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs index 64d9f825..46f76699 100644 --- a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs +++ b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs @@ -20,6 +20,7 @@ #endif #if NET45 +using System.Collections.Specialized; using System.Configuration; using Exceptionless.Extensions; using Exceptionless.Utility; @@ -293,9 +294,16 @@ public static void ReadFromConfigSection(this ExceptionlessConfiguration config) config.Resolver.GetLog().Error(typeof(ExceptionlessConfigurationExtensions), ex, String.Concat("Error retrieving configuration section: ", ex.Message)); } + config.ReadFromConfigSection(section); + } + + internal static void ReadFromConfigSection(this ExceptionlessConfiguration config, ExceptionlessSection section) { if (section == null) return; + if (section.ElementInformation.Properties["environment"].ValueOrigin != PropertyValueOrigin.Default) + config.Environment = section.Environment; + if (!section.Enabled) config.Enabled = false; @@ -385,18 +393,25 @@ public static void ReadFromConfigSection(this ExceptionlessConfiguration config) /// /// The configuration object you want to apply the attribute settings to. public static void ReadFromAppSettings(this ExceptionlessConfiguration config) { - string apiKey = ConfigurationManager.AppSettings["Exceptionless:ApiKey"]; + config.ReadFromAppSettings(ConfigurationManager.AppSettings); + } + + internal static void ReadFromAppSettings(this ExceptionlessConfiguration config, NameValueCollection settings) { + if (settings["Exceptionless:Environment"] != null) + config.Environment = settings["Exceptionless:Environment"]; + + string apiKey = settings["Exceptionless:ApiKey"]; if (IsValidApiKey(apiKey)) config.ApiKey = apiKey; - if (Boolean.TryParse(ConfigurationManager.AppSettings["Exceptionless:Enabled"], out bool enabled) && !enabled) + if (Boolean.TryParse(settings["Exceptionless:Enabled"], out bool enabled) && !enabled) config.Enabled = false; - string serverUrl = ConfigurationManager.AppSettings["Exceptionless:ServerUrl"]; + string serverUrl = settings["Exceptionless:ServerUrl"]; if (!String.IsNullOrEmpty(serverUrl)) config.ServerUrl = serverUrl; - string defaultTags = ConfigurationManager.AppSettings["Exceptionless:DefaultTags"]; + string defaultTags = settings["Exceptionless:DefaultTags"]; if (!String.IsNullOrEmpty(defaultTags)) foreach (var tag in defaultTags.SplitAndTrim(',').Where(tag => !String.IsNullOrEmpty(tag))) config.DefaultTags.Add(tag); diff --git a/src/Exceptionless/Models/Client/Event.cs b/src/Exceptionless/Models/Client/Event.cs index 555685c0..66785849 100644 --- a/src/Exceptionless/Models/Client/Event.cs +++ b/src/Exceptionless/Models/Client/Event.cs @@ -10,9 +10,14 @@ public class Event : IData { [Json.JsonProperty(NullValueHandling = Json.NullValueHandling.Ignore)] public string Environment { get => _environment; - set => _environment = Utility.DeploymentEnvironment.Normalize(value); + set { + _environment = Utility.DeploymentEnvironment.Normalize(value); + HasEnvironmentOverride = value != null; + } } + internal bool HasEnvironmentOverride { get; private set; } + public Event() { Tags = new TagSet(); Data = new DataDictionary(); diff --git a/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs b/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs index dfc151f7..3a9a12bc 100644 --- a/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs +++ b/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs @@ -2,7 +2,7 @@ [Priority(15)] public class ConfigurationDefaultsPlugin : IEventPlugin { public void Run(EventPluginContext context) { - if (context.Event.Environment == null) { + if (!context.Event.HasEnvironmentOverride) { context.Event.Environment = context.Client.Configuration.Environment; } diff --git a/src/Exceptionless/Utility/DeploymentEnvironment.cs b/src/Exceptionless/Utility/DeploymentEnvironment.cs index a0d7f2cc..d1982ec6 100644 --- a/src/Exceptionless/Utility/DeploymentEnvironment.cs +++ b/src/Exceptionless/Utility/DeploymentEnvironment.cs @@ -1,11 +1,18 @@ using System; -using System.Linq; namespace Exceptionless.Utility { internal static class DeploymentEnvironment { public static string Normalize(string value) { string name = value?.Trim(); - return String.IsNullOrEmpty(name) || name.Length > 64 || name.Any(Char.IsControl) ? null : name.ToLowerInvariant(); + if (String.IsNullOrEmpty(name) || name.Length > 64) + return null; + + foreach (char character in name) { + if (Char.IsControl(character)) + return null; + } + + return name.ToLowerInvariant(); } } } diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index cfeaf036..2092c433 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -30,6 +30,7 @@ public static IHostApplicationBuilder UseExceptionless(this IHostApplicationBuil /// Adds the given pre-configured to the host builder and registers lifecycle hooks. /// public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, ExceptionlessClient client) { + client.Configuration.Environment ??= builder.Environment.EnvironmentName; builder.Services.AddExceptionless(client); builder.Services.AddExceptionlessLifetimeService(); return builder; @@ -60,10 +61,7 @@ public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuil /// The pre-configured instance /// public static IServiceCollection AddExceptionless(this IServiceCollection services, ExceptionlessClient client) { - return services.AddSingleton(sp => { - client.Configuration.Environment ??= sp.GetService()?.EnvironmentName; - return client; - }); + return services.AddSingleton(client); } /// diff --git a/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs b/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs new file mode 100644 index 00000000..2bfad2e9 --- /dev/null +++ b/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs @@ -0,0 +1,42 @@ +#if NET45 +using System.Collections.Specialized; +using System.Configuration; +using System.IO; +using Xunit; + +namespace Exceptionless.Tests.Configuration { + public class DeploymentEnvironmentConfigurationTests { + [Fact] + public void ReadFromConfigSection_LoadsEnvironmentAttribute() { + string path = Path.GetTempFileName(); + try { + File.WriteAllText(path, "
"); + var mapped = ConfigurationManager.OpenMappedExeConfiguration(new ExeConfigurationFileMap { ExeConfigFilename = path }, ConfigurationUserLevel.None); + using var client = new ExceptionlessClient(); + client.Configuration.ReadFromConfigSection((ExceptionlessSection)mapped.GetSection("exceptionless")); + Assert.Equal("staging", client.Configuration.Environment); + } finally { + File.Delete(path); + } + } + + [Fact] + public void ReadFromConfigSection_MissingEnvironment_PreservesConfiguredValue() { + using var client = new ExceptionlessClient(); + client.Configuration.Environment = "staging"; + client.Configuration.ReadFromConfigSection(new ExceptionlessSection()); + Assert.Equal("staging", client.Configuration.Environment); + } + + [Fact] + public void ReadFromAppSettings_LoadsEnvironmentAndPreservesMissingSetting() { + using var client = new ExceptionlessClient(); + client.Configuration.Environment = "staging"; + client.Configuration.ReadFromAppSettings(new NameValueCollection()); + Assert.Equal("staging", client.Configuration.Environment); + client.Configuration.ReadFromAppSettings(new NameValueCollection { ["Exceptionless:Environment"] = " Production " }); + Assert.Equal("production", client.Configuration.Environment); + } + } +} +#endif diff --git a/test/Exceptionless.Tests/Exceptionless.Tests.csproj b/test/Exceptionless.Tests/Exceptionless.Tests.csproj index e1c0134d..97f0ccb9 100644 --- a/test/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/test/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -51,6 +51,7 @@ + diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index a8b15454..11424fee 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -1,8 +1,11 @@ #if NET10_0_OR_GREATER using System.Linq; +using Exceptionless.Configuration; +using Exceptionless.Dependency; using Exceptionless.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Moq; using Xunit; namespace Exceptionless.Tests.Platforms { @@ -20,6 +23,20 @@ public void AddExceptionless_DeploymentEnvironment_UsesHostAsFallback(string? co Assert.Equal(expectedEnvironment, services.GetRequiredService().Configuration.Environment); } + [Fact] + public void AddExceptionless_ProvidedClient_RemainsOwnedByCaller() { + var resolver = new Mock(); + using var client = new ExceptionlessClient(new ExceptionlessConfiguration(resolver.Object)); + var builder = Host.CreateApplicationBuilder(); + builder.AddExceptionless(client); + + using (var services = builder.Services.BuildServiceProvider()) { + Assert.Same(client, services.GetRequiredService()); + } + + resolver.Verify(r => r.Dispose(), Times.Never); + } + [Fact] public void AddExceptionless_WhenCalled_RegistersClientAndLifetimeService() { // Arrange diff --git a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs index 9b87ab6b..e82f5dd2 100644 --- a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs @@ -24,6 +24,20 @@ public void Run_DeploymentEnvironment_UsesDefaultAndPreservesEventOverride() { Assert.Empty(overridden.Event.Data); } + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("prod\ninvalid")] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + public void Run_InvalidDeploymentEnvironment_DoesNotUseDefault(string environment) { + var client = CreateClient(); + client.Configuration.SetEnvironment("production"); + var builder = client.CreateLog("test", "message").SetEnvironment(environment); + var context = new EventPluginContext(client, builder.Target); + new ConfigurationDefaultsPlugin().Run(context); + Assert.Null(context.Event.Environment); + } + public ConfigurationDefaultsPluginTests(ITestOutputHelper output) : base(output) { } [Fact] From 5f875dcdd6c5dd2888135f85e13e46f5a4f42987 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 22:51:50 -0500 Subject: [PATCH 4/9] Keep explicitly invalid host environments unspecified --- .../Configuration/ExceptionlessConfiguration.cs | 8 +++++++- .../ExceptionlessExtensions.cs | 9 ++++++--- .../Platforms/HostingExtensionsTests.cs | 6 +++++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs index 722d3333..4a1eddab 100644 --- a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs +++ b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs @@ -18,9 +18,15 @@ public class ExceptionlessConfiguration { /// The default deployment environment for every event. public string Environment { get => _environment; - set => _environment = Utility.DeploymentEnvironment.Normalize(value); + set { + _environment = Utility.DeploymentEnvironment.Normalize(value); + IsEnvironmentConfigured = value != null; + } } + /// Whether an environment was explicitly configured, including an invalid value that remains unspecified. + public bool IsEnvironmentConfigured { get; private set; } + private const string DEFAULT_SERVER_URL = "https://collector.exceptionless.io"; private const string DEFAULT_CONFIG_SERVER_URL = "https://config.exceptionless.io"; private const string DEFAULT_HEARTBEAT_SERVER_URL = "https://heartbeat.exceptionless.io"; diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index 2092c433..c140a24d 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -30,7 +30,8 @@ public static IHostApplicationBuilder UseExceptionless(this IHostApplicationBuil /// Adds the given pre-configured to the host builder and registers lifecycle hooks. ///
public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, ExceptionlessClient client) { - client.Configuration.Environment ??= builder.Environment.EnvironmentName; + if (!client.Configuration.IsEnvironmentConfigured) + client.Configuration.Environment = builder.Environment.EnvironmentName; builder.Services.AddExceptionless(client); builder.Services.AddExceptionlessLifetimeService(); return builder; @@ -91,7 +92,8 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromEnvironmentalVariables(); configure?.Invoke(client.Configuration); - client.Configuration.Environment ??= sp.GetService()?.EnvironmentName; + if (!client.Configuration.IsEnvironmentConfigured) + client.Configuration.Environment = sp.GetService()?.EnvironmentName; return client; }); @@ -112,7 +114,8 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromConfiguration(configuration); configure?.Invoke(client.Configuration); - client.Configuration.Environment ??= sp.GetService()?.EnvironmentName; + if (!client.Configuration.IsEnvironmentConfigured) + client.Configuration.Environment = sp.GetService()?.EnvironmentName; return client; }); diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index 11424fee..71223172 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -13,7 +13,11 @@ public class HostingExtensionsTests { [Theory] [InlineData(null, "staging")] [InlineData("production", "production")] - public void AddExceptionless_DeploymentEnvironment_UsesHostAsFallback(string? configuredEnvironment, string expectedEnvironment) { + [InlineData("", null)] + [InlineData(" ", null)] + [InlineData("prod\ninvalid", null)] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", null)] + public void AddExceptionless_DeploymentEnvironment_UsesHostAsFallback(string? configuredEnvironment, string? expectedEnvironment) { var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Staging" }); var client = new ExceptionlessClient(); client.Configuration.Environment = configuredEnvironment; From fca9d0e0b34d80553a6bedeba8b3b255601ad1e4 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 22:59:49 -0500 Subject: [PATCH 5/9] Keep host defaults distinct from explicit environment settings --- .../Configuration/ExceptionlessConfiguration.cs | 6 ++++++ .../ExceptionlessExtensions.cs | 9 +++------ .../Platforms/HostingExtensionsTests.cs | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs index 4a1eddab..27e5e0e5 100644 --- a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs +++ b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs @@ -27,6 +27,12 @@ public string Environment { /// Whether an environment was explicitly configured, including an invalid value that remains unspecified. public bool IsEnvironmentConfigured { get; private set; } + /// Applies a deployment environment fallback without replacing an explicitly configured value. + public void SetDefaultEnvironment(string environment) { + if (!IsEnvironmentConfigured) + _environment = Utility.DeploymentEnvironment.Normalize(environment); + } + private const string DEFAULT_SERVER_URL = "https://collector.exceptionless.io"; private const string DEFAULT_CONFIG_SERVER_URL = "https://config.exceptionless.io"; private const string DEFAULT_HEARTBEAT_SERVER_URL = "https://heartbeat.exceptionless.io"; diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index c140a24d..5ba0b6b9 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -30,8 +30,7 @@ public static IHostApplicationBuilder UseExceptionless(this IHostApplicationBuil /// Adds the given pre-configured to the host builder and registers lifecycle hooks. /// public static IHostApplicationBuilder AddExceptionless(this IHostApplicationBuilder builder, ExceptionlessClient client) { - if (!client.Configuration.IsEnvironmentConfigured) - client.Configuration.Environment = builder.Environment.EnvironmentName; + client.Configuration.SetDefaultEnvironment(builder.Environment.EnvironmentName); builder.Services.AddExceptionless(client); builder.Services.AddExceptionlessLifetimeService(); return builder; @@ -92,8 +91,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromEnvironmentalVariables(); configure?.Invoke(client.Configuration); - if (!client.Configuration.IsEnvironmentConfigured) - client.Configuration.Environment = sp.GetService()?.EnvironmentName; + client.Configuration.SetDefaultEnvironment(sp.GetService()?.EnvironmentName); return client; }); @@ -114,8 +112,7 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic client.Configuration.ReadFromConfiguration(configuration); configure?.Invoke(client.Configuration); - if (!client.Configuration.IsEnvironmentConfigured) - client.Configuration.Environment = sp.GetService()?.EnvironmentName; + client.Configuration.SetDefaultEnvironment(sp.GetService()?.EnvironmentName); return client; }); diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index 71223172..ab137cee 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -27,6 +27,20 @@ public void AddExceptionless_DeploymentEnvironment_UsesHostAsFallback(string? co Assert.Equal(expectedEnvironment, services.GetRequiredService().Configuration.Environment); } + [Fact] + public void AddExceptionless_HostFallback_DoesNotBecomeExplicitConfiguration() { + using var client = new ExceptionlessClient(); + var staging = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Staging" }); + staging.AddExceptionless(client); + Assert.Equal("staging", client.Configuration.Environment); + Assert.False(client.Configuration.IsEnvironmentConfigured); + + var production = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Production" }); + production.AddExceptionless(client); + Assert.Equal("production", client.Configuration.Environment); + Assert.False(client.Configuration.IsEnvironmentConfigured); + } + [Fact] public void AddExceptionless_ProvidedClient_RemainsOwnedByCaller() { var resolver = new Mock(); From cb3f2150eff7b4a4758545f5315fd42cfaddd59f Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 23:07:07 -0500 Subject: [PATCH 6/9] Preserve environment overrides when splitting aggregate exceptions --- src/Exceptionless/Models/Client/Event.cs | 2 +- .../005_HandleAggregateExceptionsPlugin.cs | 5 ++++- .../005_HandleAggregateExceptionsPluginTests.cs | 15 +++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Exceptionless/Models/Client/Event.cs b/src/Exceptionless/Models/Client/Event.cs index 66785849..23c3e77d 100644 --- a/src/Exceptionless/Models/Client/Event.cs +++ b/src/Exceptionless/Models/Client/Event.cs @@ -16,7 +16,7 @@ public string Environment { } } - internal bool HasEnvironmentOverride { get; private set; } + internal bool HasEnvironmentOverride { get; set; } public Event() { Tags = new TagSet(); diff --git a/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs b/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs index 245dc1a3..d3faae5e 100644 --- a/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs +++ b/src/Exceptionless/Plugins/Default/005_HandleAggregateExceptionsPlugin.cs @@ -21,7 +21,10 @@ public void Run(EventPluginContext context) { ctx.SetException(ex); var serializer = context.Resolver.GetJsonSerializer(); - context.Client.SubmitEvent(serializer.Deserialize(serializer.Serialize(context.Event), typeof(Event)) as Event, ctx); + var child = serializer.Deserialize(serializer.Serialize(context.Event), typeof(Event)) as Event; + if (child != null) + child.HasEnvironmentOverride = context.Event.HasEnvironmentOverride; + context.Client.SubmitEvent(child, ctx); } context.Cancel = true; diff --git a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs index 0f05689c..e758b510 100644 --- a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs @@ -37,23 +37,30 @@ public void SingleInnerException() { Assert.True(context.Cancel); } - [Fact] - public async Task MultipleInnerException() { + [Theory] + [InlineData(null, "production")] + [InlineData("Staging", "staging")] + [InlineData("", null)] + [InlineData("prod\ninvalid", null)] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", null)] + public async Task MultipleInnerException(string? environment, string? expectedEnvironment) { var submissionClient = new InMemorySubmissionClient(); - var client = new ExceptionlessClient("LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"); + using var client = new ExceptionlessClient("LhhP1C9gijpSKCslHHCvwdSIz298twx271nTest"); client.Configuration.Resolver.Register(submissionClient); + client.Configuration.Environment = "production"; var plugin = new HandleAggregateExceptionsPlugin(); var exceptionOne = new Exception("one"); var exceptionTwo = new Exception("two"); - var context = new EventPluginContext(client, new Event()); + var context = new EventPluginContext(client, new Event { Environment = environment }); context.ContextData.SetException(new AggregateException(exceptionOne, exceptionTwo)); plugin.Run(context); Assert.True(context.Cancel); await client.ProcessQueueAsync(); Assert.Equal(2, submissionClient.Events.Count); + Assert.All(submissionClient.Events, ev => Assert.Equal(expectedEnvironment, ev.Environment)); } } } From 609fe96b8d85ffad10e47e73cc3eff296170e654 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Wed, 9 Sep 2026 23:16:50 -0500 Subject: [PATCH 7/9] Apply deployment environments before event exclusions --- .../001_DeploymentEnvironmentPlugin.cs | 9 +++ .../015_ConfigurationDefaultsPlugin.cs | 6 +- .../Plugins/EventPluginManager.cs | 1 + .../001_DeploymentEnvironmentPluginTests.cs | 60 +++++++++++++++++++ .../015_ConfigurationDefaultsPluginTests.cs | 30 ---------- 5 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs create mode 100644 test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs diff --git a/src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs b/src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs new file mode 100644 index 00000000..4c16f622 --- /dev/null +++ b/src/Exceptionless/Plugins/Default/001_DeploymentEnvironmentPlugin.cs @@ -0,0 +1,9 @@ +namespace Exceptionless.Plugins.Default { + [Priority(1)] + public sealed class DeploymentEnvironmentPlugin : IEventPlugin { + public void Run(EventPluginContext context) { + if (!context.Event.HasEnvironmentOverride) + context.Event.Environment = context.Client.Configuration.Environment; + } + } +} diff --git a/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs b/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs index 3a9a12bc..e2257bdc 100644 --- a/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs +++ b/src/Exceptionless/Plugins/Default/015_ConfigurationDefaultsPlugin.cs @@ -2,10 +2,6 @@ [Priority(15)] public class ConfigurationDefaultsPlugin : IEventPlugin { public void Run(EventPluginContext context) { - if (!context.Event.HasEnvironmentOverride) { - context.Event.Environment = context.Client.Configuration.Environment; - } - foreach (string tag in context.Client.Configuration.DefaultTags) context.Event.Tags.Add(tag); @@ -15,4 +11,4 @@ public void Run(EventPluginContext context) { } } } -} +} \ No newline at end of file diff --git a/src/Exceptionless/Plugins/EventPluginManager.cs b/src/Exceptionless/Plugins/EventPluginManager.cs index 3f9dfd1b..b9c36f7d 100644 --- a/src/Exceptionless/Plugins/EventPluginManager.cs +++ b/src/Exceptionless/Plugins/EventPluginManager.cs @@ -24,6 +24,7 @@ public static void Run(EventPluginContext context) { } public static void AddDefaultPlugins(ExceptionlessConfiguration config) { + config.AddPlugin(); config.AddPlugin(); config.AddPlugin(); config.AddPlugin(); diff --git a/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs b/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs new file mode 100644 index 00000000..52b97514 --- /dev/null +++ b/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs @@ -0,0 +1,60 @@ +using Exceptionless.Models; +using Exceptionless.Plugins; +using Exceptionless.Plugins.Default; +using Xunit; + +namespace Exceptionless.Tests.Plugins { + public class DeploymentEnvironmentPluginTests : PluginTestBase { + public DeploymentEnvironmentPluginTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Run_DeploymentEnvironment_UsesDefaultAndPreservesEventOverride() { + var client = CreateClient(); + client.Configuration.SetEnvironment(" Production "); + var plugin = new DeploymentEnvironmentPlugin(); + var context = new EventPluginContext(client, new Event()); + plugin.Run(context); + Assert.Equal("production", context.Event.Environment); + + var builder = client.CreateLog("test", "message").SetEnvironment(" Staging "); + var overridden = new EventPluginContext(client, builder.Target); + plugin.Run(overridden); + Assert.Equal("staging", overridden.Event.Environment); + Assert.Empty(overridden.Event.Data); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("prod\ninvalid")] + [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] + public void Run_InvalidDeploymentEnvironment_DoesNotUseDefault(string environment) { + var client = CreateClient(); + client.Configuration.SetEnvironment("production"); + var builder = client.CreateLog("test", "message").SetEnvironment(environment); + var context = new EventPluginContext(client, builder.Target); + new DeploymentEnvironmentPlugin().Run(context); + Assert.Null(context.Event.Environment); + } + + [Theory] + [InlineData(null, "development", true)] + [InlineData("production", "production", false)] + [InlineData("", null, false)] + public void Pipeline_ExclusionCallbacks_SeeEffectiveEnvironment(string? environment, string? expectedEnvironment, bool cancelled) { + using var client = CreateClient(); + client.Configuration.Environment = "development"; + string? observedEnvironment = null; + client.Configuration.AddEventExclusion(ev => { + observedEnvironment = ev.Environment; + return ev.Environment != "development"; + }); + var context = new EventPluginContext(client, new Event { Type = Event.KnownTypes.FeatureUsage, Environment = environment }); + + EventPluginManager.Run(context); + + Assert.Equal(expectedEnvironment, observedEnvironment); + Assert.Equal(cancelled, context.Cancel); + } + } +} diff --git a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs index e82f5dd2..5d6da120 100644 --- a/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/015_ConfigurationDefaultsPluginTests.cs @@ -8,36 +8,6 @@ namespace Exceptionless.Tests.Plugins { public class ConfigurationDefaultsPluginTests : PluginTestBase { - [Fact] - public void Run_DeploymentEnvironment_UsesDefaultAndPreservesEventOverride() { - var client = CreateClient(); - client.Configuration.SetEnvironment(" Production "); - var plugin = new ConfigurationDefaultsPlugin(); - var context = new EventPluginContext(client, new Event()); - plugin.Run(context); - Assert.Equal("production", context.Event.Environment); - - var builder = client.CreateLog("test", "message").SetEnvironment(" Staging "); - var overridden = new EventPluginContext(client, builder.Target); - plugin.Run(overridden); - Assert.Equal("staging", overridden.Event.Environment); - Assert.Empty(overridden.Event.Data); - } - - [Theory] - [InlineData("")] - [InlineData(" ")] - [InlineData("prod\ninvalid")] - [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")] - public void Run_InvalidDeploymentEnvironment_DoesNotUseDefault(string environment) { - var client = CreateClient(); - client.Configuration.SetEnvironment("production"); - var builder = client.CreateLog("test", "message").SetEnvironment(environment); - var context = new EventPluginContext(client, builder.Target); - new ConfigurationDefaultsPlugin().Run(context); - Assert.Null(context.Event.Environment); - } - public ConfigurationDefaultsPluginTests(ITestOutputHelper output) : base(output) { } [Fact] From 400e18dbabb5197e45206ac49e2a2218a75e8983 Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Thu, 10 Sep 2026 00:10:21 -0500 Subject: [PATCH 8/9] Preserve supplied environment casing --- README.md | 2 +- src/Exceptionless/Utility/DeploymentEnvironment.cs | 2 +- .../DeploymentEnvironmentConfigurationTests.cs | 4 ++-- .../Exceptionless.Tests/Platforms/HostingExtensionsTests.cs | 6 +++--- .../Plugins/001_DeploymentEnvironmentPluginTests.cs | 4 ++-- .../Plugins/910_DuplicateCheckerPluginTests.cs | 2 +- .../Serializer/Models/EventSerializerTests.cs | 5 +++-- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 32bb4277..11361029 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ client.Configuration.SetEnvironment("production"); client.CreateLog("Deployment complete").SetEnvironment("staging").Submit(); ``` -The hosting integration falls back to `IHostEnvironment.EnvironmentName`. Explicit configuration wins; `Exceptionless:Environment` and `Exceptionless__Environment` are also supported. .NET Framework applications can use the `Exceptionless:Environment` app setting or the `environment` attribute on the `` configuration section. Names are trimmed, lowercased, and limited to 64 characters. Missing or invalid values remain unspecified. The top-level event `environment` is separate from machine diagnostics in `data.@environment`. Stacks and fixed versions remain shared across environments. +The hosting integration falls back to `IHostEnvironment.EnvironmentName`. Explicit configuration wins; `Exceptionless:Environment` and `Exceptionless__Environment` are also supported. .NET Framework applications can use the `Exceptionless:Environment` app setting or the `environment` attribute on the `` configuration section. Names are trimmed and limited to 64 characters, preserving the supplied casing. The server filters case-insensitively and normalizes aggregation keys. Missing or invalid values remain unspecified. The top-level event `environment` is separate from machine diagnostics in `data.@environment`. Stacks and fixed versions remain shared across environments. Refer to the Exceptionless documentation here: [Exceptionless Docs](https://exceptionless.com/docs/). diff --git a/src/Exceptionless/Utility/DeploymentEnvironment.cs b/src/Exceptionless/Utility/DeploymentEnvironment.cs index d1982ec6..743a1e39 100644 --- a/src/Exceptionless/Utility/DeploymentEnvironment.cs +++ b/src/Exceptionless/Utility/DeploymentEnvironment.cs @@ -12,7 +12,7 @@ public static string Normalize(string value) { return null; } - return name.ToLowerInvariant(); + return name; } } } diff --git a/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs b/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs index 2bfad2e9..5ab8dce5 100644 --- a/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs +++ b/test/Exceptionless.Tests/Configuration/DeploymentEnvironmentConfigurationTests.cs @@ -14,7 +14,7 @@ public void ReadFromConfigSection_LoadsEnvironmentAttribute() { var mapped = ConfigurationManager.OpenMappedExeConfiguration(new ExeConfigurationFileMap { ExeConfigFilename = path }, ConfigurationUserLevel.None); using var client = new ExceptionlessClient(); client.Configuration.ReadFromConfigSection((ExceptionlessSection)mapped.GetSection("exceptionless")); - Assert.Equal("staging", client.Configuration.Environment); + Assert.Equal("Staging", client.Configuration.Environment); } finally { File.Delete(path); } @@ -35,7 +35,7 @@ public void ReadFromAppSettings_LoadsEnvironmentAndPreservesMissingSetting() { client.Configuration.ReadFromAppSettings(new NameValueCollection()); Assert.Equal("staging", client.Configuration.Environment); client.Configuration.ReadFromAppSettings(new NameValueCollection { ["Exceptionless:Environment"] = " Production " }); - Assert.Equal("production", client.Configuration.Environment); + Assert.Equal("Production", client.Configuration.Environment); } } } diff --git a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs index ab137cee..3db62467 100644 --- a/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs +++ b/test/Exceptionless.Tests/Platforms/HostingExtensionsTests.cs @@ -11,7 +11,7 @@ namespace Exceptionless.Tests.Platforms { public class HostingExtensionsTests { [Theory] - [InlineData(null, "staging")] + [InlineData(null, "Staging")] [InlineData("production", "production")] [InlineData("", null)] [InlineData(" ", null)] @@ -32,12 +32,12 @@ public void AddExceptionless_HostFallback_DoesNotBecomeExplicitConfiguration() { using var client = new ExceptionlessClient(); var staging = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Staging" }); staging.AddExceptionless(client); - Assert.Equal("staging", client.Configuration.Environment); + Assert.Equal("Staging", client.Configuration.Environment); Assert.False(client.Configuration.IsEnvironmentConfigured); var production = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { EnvironmentName = "Production" }); production.AddExceptionless(client); - Assert.Equal("production", client.Configuration.Environment); + Assert.Equal("Production", client.Configuration.Environment); Assert.False(client.Configuration.IsEnvironmentConfigured); } diff --git a/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs b/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs index 52b97514..491a6ffa 100644 --- a/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/001_DeploymentEnvironmentPluginTests.cs @@ -14,12 +14,12 @@ public void Run_DeploymentEnvironment_UsesDefaultAndPreservesEventOverride() { var plugin = new DeploymentEnvironmentPlugin(); var context = new EventPluginContext(client, new Event()); plugin.Run(context); - Assert.Equal("production", context.Event.Environment); + Assert.Equal("Production", context.Event.Environment); var builder = client.CreateLog("test", "message").SetEnvironment(" Staging "); var overridden = new EventPluginContext(client, builder.Target); plugin.Run(overridden); - Assert.Equal("staging", overridden.Event.Environment); + Assert.Equal("Staging", overridden.Event.Environment); Assert.Empty(overridden.Event.Data); } diff --git a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs index fe471345..35a73735 100644 --- a/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/910_DuplicateCheckerPluginTests.cs @@ -20,7 +20,7 @@ public void Run_SameEventAcrossEnvironments_OnlyMergesWithinEnvironment() { var client = CreateClient(); using (var plugin = new DuplicateCheckerPlugin(TimeSpan.FromMinutes(1))) { foreach (bool duplicate in new[] { false, true }) { - foreach (string environment in new[] { "production", "staging", null }) { + foreach (string environment in new[] { "Production", "production", "staging", null }) { var builder = client.CreateLog("Environment test").SetEnvironment(environment); var context = new EventPluginContext(client, builder.Target, builder.PluginContextData); plugin.Run(context); diff --git a/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs b/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs index 719545cd..ed05b026 100644 --- a/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs +++ b/test/Exceptionless.Tests/Serializer/Models/EventSerializerTests.cs @@ -11,13 +11,14 @@ public void Serialize_DeploymentEnvironment_RoundTripsSeparatelyFromRuntimeMetad var model = new Event { Environment = " Production " }; model.Data[Event.KnownDataKeys.EnvironmentInfo] = new EnvironmentInfo { MachineName = "worker-1" }; string json = Serialize(model); - Assert.Contains("\"environment\":\"production\"", json); + Assert.Contains("\"environment\":\"Production\"", json); var result = Deserialize(json); - Assert.Equal("production", result.Environment); + Assert.Equal("Production", result.Environment); Assert.Equal("worker-1", result.GetEnvironmentInfo().MachineName); Assert.Null(new Event { Environment = new string('x', 65) }.Environment); Assert.Null(new Event { Environment = " " }.Environment); Assert.NotEqual(new Event { Environment = "production" }, new Event { Environment = "staging" }); + Assert.NotEqual(new Event { Environment = "Production" }, new Event { Environment = "production" }); } /* lang=json */ From 1e984cd78139b92172c5373608ea075e3103f09a Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Thu, 10 Sep 2026 00:16:45 -0500 Subject: [PATCH 9/9] Preserve casing in aggregate exception expectations --- .../Plugins/005_HandleAggregateExceptionsPluginTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs index e758b510..f6b42b24 100644 --- a/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs +++ b/test/Exceptionless.Tests/Plugins/005_HandleAggregateExceptionsPluginTests.cs @@ -39,7 +39,7 @@ public void SingleInnerException() { [Theory] [InlineData(null, "production")] - [InlineData("Staging", "staging")] + [InlineData("Staging", "Staging")] [InlineData("", null)] [InlineData("prod\ninvalid", null)] [InlineData("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", null)]