From 90f5ef74fe40e4d981b858f3e945ca1af8be5dbc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 10 May 2026 09:03:41 +0000
Subject: [PATCH 1/5] Initial plan
From 4247864c1976927f46ee0689197a68d87fc4852c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 10 May 2026 09:11:44 +0000
Subject: [PATCH 2/5] fix environment detection precedence and CI metadata
Agent-Logs-Url: https://github.com/xping-dev/sdk-dotnet/sessions/3f753eb5-b070-4f8a-a450-4423ad940fb6
Co-authored-by: xping-admin <180879096+xping-admin@users.noreply.github.com>
---
docs/configuration/configuration-reference.md | 35 ++++-
docs/getting-started/ci-cd-setup.md | 14 +-
.../Configuration/XpingConfiguration.cs | 22 ++-
.../XpingConfigurationBuilder.cs | 11 ++
.../XpingServiceCollectionExtensions.cs | 4 +
.../Environment/IEnvironmentDetector.cs | 3 +-
.../Internals/EnvironmentDetector.cs | 58 +++++++-
.../XpingConfigurationBuilderTests.cs | 2 +
.../Configuration/XpingConfigurationTests.cs | 1 +
.../XpingServiceCollectionExtensionsTests.cs | 15 ++
.../Environment/EnvironmentDetectorTests.cs | 139 ++++++++++++++++++
11 files changed, 291 insertions(+), 13 deletions(-)
create mode 100644 tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs
diff --git a/docs/configuration/configuration-reference.md b/docs/configuration/configuration-reference.md
index 9ec2f38..78c6703 100644
--- a/docs/configuration/configuration-reference.md
+++ b/docs/configuration/configuration-reference.md
@@ -26,6 +26,7 @@ Xping SDK supports multiple configuration methods with the following priority or
| `FlushInterval` | TimeSpan | `30s` | `XPING_FLUSHINTERVAL` | Auto-flush interval |
| `Environment` | string | `Local` | `XPING_ENVIRONMENT` | Environment name |
| `AutoDetectCIEnvironment` | bool | `true` | `XPING_AUTODETECTCIENVIRONMENT` | Auto-detect CI/CD |
+| `CiEnvironmentName` | string | `CI` | `XPING_CIENVIRONMENTNAME` | Label used for auto-detected CI executions |
| `Enabled` | bool | `true` | `XPING_ENABLED` | SDK enabled/disabled |
| `CaptureStackTraces` | bool | `true` | `XPING_CAPTURESTACKTRACES` | Include stack traces |
| `EnableCompression` | bool | `true` | `XPING_ENABLECOMPRESSION` | Compress uploads |
@@ -414,11 +415,12 @@ export XPING_ENVIRONMENT="Staging"
The SDK determines the environment name using the following priority (highest to lowest):
1. **`XPING_ENVIRONMENT` environment variable** - Explicit Xping-specific setting (highest priority)
-2. **Auto-detected CI** - Returns `"CI"` when `AutoDetectCIEnvironment=true` and running in a detected CI/CD platform
+2. **Auto-detected CI** - Returns `CiEnvironmentName` (default `"CI"`) when `AutoDetectCIEnvironment=true` and running in a detected CI/CD platform
3. **`Environment` configuration property** - Value set programmatically or in configuration files
-4. **Default** - Returns `"Local"` when none of the above are set
+4. **Framework environment variables** - `ASPNETCORE_ENVIRONMENT`, then `DOTNET_ENVIRONMENT`
+5. **Default** - Returns `"Local"` when none of the above are set
-**Example:** If you won't specify `Environment` and `AutoDetectCIEnvironment=false`, Xping will use `"Local"` as the environment name. However, setting `XPING_ENVIRONMENT=Staging` will override this and use `"Staging"` instead.
+**Example:** If you don't specify `Environment` and `AutoDetectCIEnvironment=false`, Xping will use `DOTNET_ENVIRONMENT`/`ASPNETCORE_ENVIRONMENT` when available, and otherwise fall back to `"Local"`. Setting `XPING_ENVIRONMENT=Staging` still overrides everything and uses `"Staging"` instead.
---
@@ -428,7 +430,7 @@ The SDK determines the environment name using the following priority (highest to
**Default:** `true`
**Environment Variable:** `XPING_AUTODETECTCIENVIRONMENT`
-Automatically detect when running in CI/CD environments and set `Environment` to `"CI"`. Also captures CI-specific metadata (build numbers, commit SHAs, etc.).
+Automatically detect when running in CI/CD environments and set `Environment` to `CiEnvironmentName` (default `"CI"`). Also captures CI-specific metadata (build numbers, commit SHAs, branch names, etc.).
**Supported CI/CD platforms:**
- GitHub Actions
@@ -462,6 +464,31 @@ export XPING_AUTODETECTCIENVIRONMENT="false"
---
+### CiEnvironmentName
+
+**Type:** `string`
+**Default:** `"CI"`
+**Environment Variable:** `XPING_CIENVIRONMENTNAME`
+
+Overrides the label used when CI/CD is auto-detected. This is useful when you want CI executions grouped under a more specific environment name such as `"BuildPipeline"` or `"PullRequestValidation"` without disabling auto-detection.
+
+**Example:**
+
+```json
+{
+ "Xping": {
+ "AutoDetectCIEnvironment": true,
+ "CiEnvironmentName": "BuildPipeline"
+ }
+}
+```
+
+```bash
+export XPING_CIENVIRONMENTNAME="BuildPipeline"
+```
+
+---
+
## Feature Flags
### Enabled
diff --git a/docs/getting-started/ci-cd-setup.md b/docs/getting-started/ci-cd-setup.md
index 34e760d..1cde630 100644
--- a/docs/getting-started/ci-cd-setup.md
+++ b/docs/getting-started/ci-cd-setup.md
@@ -6,7 +6,7 @@ Learn how to integrate Xping SDK into your CI/CD pipelines for continuous test r
## Overview
-Xping SDK automatically detects CI/CD environments and captures relevant metadata like build numbers, commit SHAs, and branch names. This enables you to:
+Xping SDK automatically detects CI/CD environments and captures relevant metadata like build numbers, commit SHAs, and branch names. It also marks non-CI executions as local developer-machine runs. This enables you to:
- **Track test reliability across builds**
- **Detect flaky tests in your pipeline**
@@ -87,6 +87,7 @@ Xping automatically captures:
- `GITHUB_RUN_NUMBER` - Sequential run number
- `GITHUB_SHA` - Commit SHA
- `GITHUB_REF` - Branch or tag ref
+- `GITHUB_HEAD_REF` / `GITHUB_REF_NAME` - Normalized into `CI.Branch`
- `GITHUB_REPOSITORY` - Repository name
- `GITHUB_ACTOR` - User who triggered the workflow
@@ -216,7 +217,7 @@ Xping automatically captures:
- `CI_PIPELINE_ID` - Unique pipeline ID
- `CI_JOB_ID` - Job ID
- `CI_COMMIT_SHA` - Commit SHA
-- `CI_COMMIT_REF_NAME` - Branch or tag name
+- `CI_COMMIT_BRANCH` / `CI_COMMIT_REF_NAME` - Normalized into `CI.Branch`
- `CI_PROJECT_PATH` - Repository path
- `GITLAB_USER_LOGIN` - User who triggered the pipeline
@@ -547,6 +548,15 @@ Track different branches in different Xping projects:
run: dotnet test
```
+### Custom CI Environment Label
+
+If you want auto-detected CI runs grouped under a label other than the default `CI`, set `XPING_CIENVIRONMENTNAME`:
+
+```yaml
+env:
+ XPING__CIENVIRONMENTNAME: "BuildPipeline"
+```
+
---
## Verification Checklist
diff --git a/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs b/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
index a82ed81..4a5aba6 100644
--- a/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
+++ b/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
@@ -17,6 +17,13 @@ public sealed class XpingConfiguration
///
public const string DefaultEnvironment = "Local";
+ ///
+ /// Represents the default environment name used when CI/CD is auto-detected.
+ ///
+ public const string DefaultCiEnvironment = "CI";
+
+ private string? _environment;
+
///
/// Gets or sets the Xping API endpoint URL.
///
@@ -57,14 +64,25 @@ public sealed class XpingConfiguration
///
/// Gets or sets the environment name (e.g., "Local", "CI", "Staging", "Production").
+ /// When left unset, the SDK falls back to unless a CI or framework-specific
+ /// environment variable takes precedence during environment detection.
///
- public string Environment { get; set; } = DefaultEnvironment;
+ public string Environment
+ {
+ get => string.IsNullOrWhiteSpace(_environment) ? DefaultEnvironment : _environment!;
+ set => _environment = value;
+ }
///
/// Gets or sets a value indicating whether to automatically detect CI/CD environments.
///
public bool AutoDetectCIEnvironment { get; set; } = true;
+ ///
+ /// Gets or sets the environment name to use when CI/CD is auto-detected.
+ ///
+ public string CiEnvironmentName { get; set; } = DefaultCiEnvironment;
+
///
/// Gets or sets a value indicating whether the SDK is enabled.
///
@@ -213,4 +231,6 @@ public bool IsValid()
{
return Validate().Count == 0;
}
+
+ internal bool HasExplicitEnvironment => !string.IsNullOrWhiteSpace(_environment);
}
diff --git a/src/Xping.Sdk.Core/Configuration/XpingConfigurationBuilder.cs b/src/Xping.Sdk.Core/Configuration/XpingConfigurationBuilder.cs
index b4cff7c..28ae120 100644
--- a/src/Xping.Sdk.Core/Configuration/XpingConfigurationBuilder.cs
+++ b/src/Xping.Sdk.Core/Configuration/XpingConfigurationBuilder.cs
@@ -89,6 +89,17 @@ public XpingConfigurationBuilder WithAutoDetectCIEnvironment(bool autoDetect)
return this;
}
+ ///
+ /// Sets the environment name to use when CI/CD is auto-detected.
+ ///
+ /// The CI/CD environment name.
+ /// The builder instance for method chaining.
+ public XpingConfigurationBuilder WithCiEnvironmentName(string ciEnvironmentName)
+ {
+ _configuration.CiEnvironmentName = ciEnvironmentName;
+ return this;
+ }
+
///
/// Sets whether the SDK is enabled.
///
diff --git a/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs b/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs
index 9734131..bd254cc 100644
--- a/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs
+++ b/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs
@@ -492,6 +492,9 @@ private static void BindEnvironmentVariablesWithPrefix(XpingConfiguration config
&& bool.TryParse(autoDetect, out var ad))
config.AutoDetectCIEnvironment = ad;
+ if (GetEnv("CIENVIRONMENTNAME") is { } ciEnvironmentName)
+ config.CiEnvironmentName = ciEnvironmentName;
+
// Feature Flags
if (GetEnv("ENABLED") is { } enabled && bool.TryParse(enabled, out var e))
config.Enabled = e;
@@ -557,6 +560,7 @@ private static void CopyConfiguration(XpingConfiguration source, XpingConfigurat
target.FlushInterval = source.FlushInterval;
target.Environment = source.Environment;
target.AutoDetectCIEnvironment = source.AutoDetectCIEnvironment;
+ target.CiEnvironmentName = source.CiEnvironmentName;
target.Enabled = source.Enabled;
target.CaptureStackTraces = source.CaptureStackTraces;
target.EnableCompression = source.EnableCompression;
diff --git a/src/Xping.Sdk.Core/Services/Environment/IEnvironmentDetector.cs b/src/Xping.Sdk.Core/Services/Environment/IEnvironmentDetector.cs
index 8cadf9d..6bfaf5e 100644
--- a/src/Xping.Sdk.Core/Services/Environment/IEnvironmentDetector.cs
+++ b/src/Xping.Sdk.Core/Services/Environment/IEnvironmentDetector.cs
@@ -52,7 +52,8 @@ public interface IEnvironmentDetector
///
/// Gets the environment name based on configuration and auto-detection rules.
/// Priority:
- /// XPING_ENVIRONMENT > AutoDetectCI > Options.Environment > ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT > "Local"
+ /// XPING_ENVIRONMENT > AutoDetectCI > explicitly configured Options.Environment >
+ /// ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT > "Local"
///
///
/// The environment name is used in confidence calculations, which are performed both globally across all executions
diff --git a/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs b/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
index f699874..870b972 100644
--- a/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
+++ b/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
@@ -347,11 +347,13 @@ private string DetectEnvironmentName(XpingConfiguration configuration)
// Priority 2: Auto-detect CI if enabled
if (configuration.AutoDetectCIEnvironment && _ciPlatform.Value.HasValue)
{
- return "CI";
+ return string.IsNullOrWhiteSpace(configuration.CiEnvironmentName)
+ ? XpingConfiguration.DefaultCiEnvironment
+ : configuration.CiEnvironmentName;
}
- // Priority 3: Use configuration property
- if (!string.IsNullOrWhiteSpace(configuration.Environment))
+ // Priority 3: Use explicitly configured environment property
+ if (configuration.HasExplicitEnvironment)
{
return configuration.Environment;
}
@@ -369,8 +371,8 @@ private string DetectEnvironmentName(XpingConfiguration configuration)
return dotnetEnv!;
}
- // Priority 5: Default to "Local"
- return XpingConfiguration.DefaultEnvironment;
+ // Priority 5: Default to configured/default local environment
+ return configuration.Environment;
}
private bool DetectIsContainer()
@@ -415,6 +417,12 @@ private Dictionary CollectCustomProperties(string operatingSyste
{
Dictionary properties = new Dictionary();
+ properties["ExecutionContext"] = ciPlatform.HasValue ? "CI" : "Local";
+ if (!ciPlatform.HasValue && !isContainer)
+ {
+ properties["IsDeveloperMachine"] = "true";
+ }
+
// Add container information
if (isContainer)
{
@@ -468,6 +476,12 @@ private Dictionary CollectCustomProperties(string operatingSyste
AddIfNotNull(properties, "CI.RunId", GetEnvironmentVariable("GITHUB_RUN_ID"));
AddIfNotNull(properties, "CI.RunNumber", GetEnvironmentVariable("GITHUB_RUN_NUMBER"));
AddIfNotNull(properties, "CI.Ref", GetEnvironmentVariable("GITHUB_REF"));
+ AddIfNotNull(properties, "CI.Branch", GetFirstNonEmptyValue(
+ GetEnvironmentVariable("GITHUB_HEAD_REF"),
+ GetEnvironmentVariable("GITHUB_REF_NAME"),
+ ExtractBranchName(GetEnvironmentVariable("GITHUB_REF"))));
+ AddIfNotNull(properties, "CI.HeadBranch", GetEnvironmentVariable("GITHUB_HEAD_REF"));
+ AddIfNotNull(properties, "CI.BaseBranch", GetEnvironmentVariable("GITHUB_BASE_REF"));
AddIfNotNull(properties, "CI.SHA", GetEnvironmentVariable("GITHUB_SHA"));
AddIfNotNull(properties, "CI.Actor", GetEnvironmentVariable("GITHUB_ACTOR"));
AddIfNotNull(properties, "CI.Workflow", GetEnvironmentVariable("GITHUB_WORKFLOW"));
@@ -478,6 +492,9 @@ private Dictionary CollectCustomProperties(string operatingSyste
AddIfNotNull(properties, "CI.BuildNumber", GetEnvironmentVariable("BUILD_BUILDNUMBER"));
AddIfNotNull(properties, "CI.Repository", GetEnvironmentVariable("BUILD_REPOSITORY_NAME"));
AddIfNotNull(properties, "CI.SourceBranch", GetEnvironmentVariable("BUILD_SOURCEBRANCH"));
+ AddIfNotNull(properties, "CI.Branch", GetFirstNonEmptyValue(
+ GetEnvironmentVariable("BUILD_SOURCEBRANCHNAME"),
+ ExtractBranchName(GetEnvironmentVariable("BUILD_SOURCEBRANCH"))));
AddIfNotNull(properties, "CI.SourceVersion", GetEnvironmentVariable("BUILD_SOURCEVERSION"));
AddIfNotNull(properties, "CI.RequestedFor", GetEnvironmentVariable("BUILD_REQUESTEDFOR"));
break;
@@ -488,6 +505,7 @@ private Dictionary CollectCustomProperties(string operatingSyste
AddIfNotNull(properties, "CI.BuildUrl", GetEnvironmentVariable("BUILD_URL"));
AddIfNotNull(properties, "CI.GitCommit", GetEnvironmentVariable("GIT_COMMIT"));
AddIfNotNull(properties, "CI.GitBranch", GetEnvironmentVariable("GIT_BRANCH"));
+ AddIfNotNull(properties, "CI.Branch", GetEnvironmentVariable("GIT_BRANCH"));
break;
case CIPlatform.GitLabCI:
@@ -496,7 +514,11 @@ private Dictionary CollectCustomProperties(string operatingSyste
AddIfNotNull(properties, "CI.ProjectPath", GetEnvironmentVariable("CI_PROJECT_PATH"));
AddIfNotNull(properties, "CI.CommitSHA", GetEnvironmentVariable("CI_COMMIT_SHA"));
AddIfNotNull(properties, "CI.CommitBranch", GetEnvironmentVariable("CI_COMMIT_BRANCH"));
+ AddIfNotNull(properties, "CI.Branch", GetFirstNonEmptyValue(
+ GetEnvironmentVariable("CI_COMMIT_BRANCH"),
+ GetEnvironmentVariable("CI_COMMIT_REF_NAME")));
AddIfNotNull(properties, "CI.CommitAuthor", GetEnvironmentVariable("CI_COMMIT_AUTHOR"));
+ AddIfNotNull(properties, "CI.Actor", GetEnvironmentVariable("GITLAB_USER_LOGIN"));
break;
case CIPlatform.CircleCI:
@@ -556,6 +578,32 @@ private static void AddIfNotNull(Dictionary dictionary, string k
}
}
+ private static string? GetFirstNonEmptyValue(params string?[] values)
+ {
+ foreach (string? value in values)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ return value;
+ }
+ }
+
+ return null;
+ }
+
+ private static string? ExtractBranchName(string? gitRef)
+ {
+ if (string.IsNullOrWhiteSpace(gitRef))
+ {
+ return null;
+ }
+
+ const string headsPrefix = "refs/heads/";
+ return gitRef!.StartsWith(headsPrefix, StringComparison.OrdinalIgnoreCase)
+ ? gitRef.Substring(headsPrefix.Length)
+ : gitRef;
+ }
+
private static string? GetEnvironmentVariable(string variable)
{
try
diff --git a/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationBuilderTests.cs b/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationBuilderTests.cs
index 48107bc..7b3047f 100644
--- a/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationBuilderTests.cs
+++ b/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationBuilderTests.cs
@@ -39,6 +39,7 @@ public void BuilderShouldAllowSettingAllProperties()
.WithFlushInterval(TimeSpan.FromMinutes(1))
.WithEnvironment("Production")
.WithAutoDetectCIEnvironment(false)
+ .WithCiEnvironmentName("Pipeline")
.WithEnabled(false)
.WithCaptureStackTraces(false)
.WithEnableCompression(false)
@@ -58,6 +59,7 @@ public void BuilderShouldAllowSettingAllProperties()
Assert.Equal(TimeSpan.FromMinutes(1), config.FlushInterval);
Assert.Equal("Production", config.Environment);
Assert.False(config.AutoDetectCIEnvironment);
+ Assert.Equal("Pipeline", config.CiEnvironmentName);
Assert.False(config.Enabled);
Assert.False(config.CaptureStackTraces);
Assert.False(config.EnableCompression);
diff --git a/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationTests.cs b/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationTests.cs
index 1025752..88b8882 100644
--- a/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationTests.cs
+++ b/tests/Xping.Sdk.Core.Tests/Configuration/XpingConfigurationTests.cs
@@ -23,6 +23,7 @@ public void DefaultConfigurationShouldHaveCorrectValues()
Assert.Equal(TimeSpan.FromSeconds(30), config.FlushInterval);
Assert.Equal("Local", config.Environment);
Assert.True(config.AutoDetectCIEnvironment);
+ Assert.Equal("CI", config.CiEnvironmentName);
Assert.True(config.Enabled);
Assert.True(config.CaptureStackTraces);
Assert.True(config.EnableCompression);
diff --git a/tests/Xping.Sdk.Core.Tests/Extensions/XpingServiceCollectionExtensionsTests.cs b/tests/Xping.Sdk.Core.Tests/Extensions/XpingServiceCollectionExtensionsTests.cs
index 689aae2..300a4d3 100644
--- a/tests/Xping.Sdk.Core.Tests/Extensions/XpingServiceCollectionExtensionsTests.cs
+++ b/tests/Xping.Sdk.Core.Tests/Extensions/XpingServiceCollectionExtensionsTests.cs
@@ -646,4 +646,19 @@ public void BindEnvVars_AUTODETECTCIENVIRONMENT_ShouldParseBool()
Assert.False(bound.AutoDetectCIEnvironment);
}
+
+ [Fact]
+ public void BindEnvVars_CIENVIRONMENTNAME_ShouldOverrideConfiguredValue()
+ {
+ using var _key = WithEnv("XPING_APIKEY", "k");
+ using var _proj = WithEnv("XPING_PROJECTID", "p");
+ using var _ = WithEnv("XPING_CIENVIRONMENTNAME", "BuildPipeline");
+
+ var services = new ServiceCollection();
+ services.AddXpingConfigurationFromConfiguration(InMemoryXpingConfig());
+ var bound = services.BuildServiceProvider()
+ .GetRequiredService>().Value;
+
+ Assert.Equal("BuildPipeline", bound.CiEnvironmentName);
+ }
}
diff --git a/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs b/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs
new file mode 100644
index 0000000..af148b1
--- /dev/null
+++ b/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs
@@ -0,0 +1,139 @@
+/*
+ * © 2026 Xping.io. All Rights Reserved.
+ * License: [MIT]
+ */
+
+using Microsoft.Extensions.Options;
+using Moq;
+using Xping.Sdk.Core.Configuration;
+using Xping.Sdk.Core.Models.Environments;
+using Xping.Sdk.Core.Services.Environment;
+using Xping.Sdk.Core.Services.Environment.Internals;
+using Xping.Sdk.Core.Services.Network;
+
+namespace Xping.Sdk.Core.Tests.Services.Environment;
+
+[Collection("Sequential")]
+public sealed class EnvironmentDetectorTests
+{
+ private static readonly string[] _ciVariables =
+ [
+ "CI",
+ "GITHUB_ACTIONS",
+ "TF_BUILD",
+ "JENKINS_URL",
+ "GITLAB_CI",
+ "CIRCLECI",
+ "TRAVIS",
+ "TEAMCITY_VERSION",
+ "BITBUCKET_PIPELINE_UUID",
+ "APPVEYOR",
+ ];
+
+ [Fact]
+ public async Task BuildEnvironmentInfoAsync_WithDotnetEnvironmentAndDefaultConfiguration_UsesDotnetEnvironment()
+ {
+ using var clearedCiVariables = ClearEnvironmentVariables(_ciVariables);
+ using var dotnetEnvironment = new EnvRestorer("DOTNET_ENVIRONMENT", "Development");
+
+ IEnvironmentDetector detector = CreateDetector();
+
+ EnvironmentInfo info = await detector.BuildEnvironmentInfoAsync();
+
+ Assert.Equal("Development", info.EnvironmentName);
+ Assert.False(info.IsCIEnvironment);
+ }
+
+ [Fact]
+ public async Task BuildEnvironmentInfoAsync_WithLocalExecution_MarksDeveloperMachine()
+ {
+ using var clearedCiVariables = ClearEnvironmentVariables(_ciVariables);
+
+ IEnvironmentDetector detector = CreateDetector();
+
+ EnvironmentInfo info = await detector.BuildEnvironmentInfoAsync();
+
+ Assert.False(info.IsCIEnvironment);
+ Assert.Equal("Local", info.EnvironmentName);
+ Assert.Equal("Local", info.CustomProperties["ExecutionContext"]);
+ Assert.Equal("true", info.CustomProperties["IsDeveloperMachine"]);
+ }
+
+ [Fact]
+ public async Task BuildEnvironmentInfoAsync_WithGitHubActions_CapturesNormalizedBranchAndConfiguredCiName()
+ {
+ using var githubActions = new EnvRestorer("GITHUB_ACTIONS", "true");
+ using var githubHeadRef = new EnvRestorer("GITHUB_HEAD_REF", "feature/refactor-environment");
+ using var githubRefName = new EnvRestorer("GITHUB_REF_NAME", "17/merge");
+ using var githubRef = new EnvRestorer("GITHUB_REF", "refs/pull/17/merge");
+ using var githubRepository = new EnvRestorer("GITHUB_REPOSITORY", "xping-dev/sdk-dotnet");
+ using var githubRunId = new EnvRestorer("GITHUB_RUN_ID", "42");
+ using var githubSha = new EnvRestorer("GITHUB_SHA", "abc123");
+ using var githubActor = new EnvRestorer("GITHUB_ACTOR", "octocat");
+
+ IEnvironmentDetector detector = CreateDetector(new XpingConfiguration
+ {
+ CiEnvironmentName = "BuildPipeline",
+ });
+
+ EnvironmentInfo info = await detector.BuildEnvironmentInfoAsync();
+
+ Assert.True(info.IsCIEnvironment);
+ Assert.Equal("BuildPipeline", info.EnvironmentName);
+ Assert.Equal("CI", info.CustomProperties["ExecutionContext"]);
+ Assert.Equal("GitHubActions", info.CustomProperties["CIPlatform"]);
+ Assert.Equal("feature/refactor-environment", info.CustomProperties["CI.Branch"]);
+ Assert.Equal("refs/pull/17/merge", info.CustomProperties["CI.Ref"]);
+ Assert.DoesNotContain("IsDeveloperMachine", info.CustomProperties.Keys);
+ }
+
+ private static EnvironmentDetector CreateDetector(XpingConfiguration? configuration = null)
+ {
+ XpingConfiguration resolvedConfiguration = configuration ?? new XpingConfiguration();
+ resolvedConfiguration.CollectNetworkMetrics = false;
+
+ return new EnvironmentDetector(
+ Options.Create(resolvedConfiguration),
+ Mock.Of());
+ }
+
+ private static CompositeDisposable ClearEnvironmentVariables(IEnumerable variableNames)
+ {
+ List restorers = [];
+ foreach (string variableName in variableNames)
+ {
+ restorers.Add(new EnvRestorer(variableName, null));
+ }
+
+ return new CompositeDisposable(restorers);
+ }
+
+ private sealed class EnvRestorer : IDisposable
+ {
+ private readonly string _name;
+ private readonly string? _originalValue;
+
+ public EnvRestorer(string name, string? value)
+ {
+ _name = name;
+ _originalValue = System.Environment.GetEnvironmentVariable(name);
+ System.Environment.SetEnvironmentVariable(name, value);
+ }
+
+ public void Dispose()
+ {
+ System.Environment.SetEnvironmentVariable(_name, _originalValue);
+ }
+ }
+
+ private sealed class CompositeDisposable(IEnumerable disposables) : IDisposable
+ {
+ public void Dispose()
+ {
+ foreach (IDisposable disposable in disposables.Reverse())
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+}
From ccfe72361700ae5a189c8e5a60d91817a080ab49 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 10 May 2026 09:14:15 +0000
Subject: [PATCH 3/5] chore: address environment detection review feedback
Agent-Logs-Url: https://github.com/xping-dev/sdk-dotnet/sessions/3f753eb5-b070-4f8a-a450-4423ad940fb6
Co-authored-by: xping-admin <180879096+xping-admin@users.noreply.github.com>
---
src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs | 3 ++-
.../Services/Environment/Internals/EnvironmentDetector.cs | 4 +++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs b/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
index 4a5aba6..0e527f8 100644
--- a/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
+++ b/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
@@ -18,7 +18,8 @@ public sealed class XpingConfiguration
public const string DefaultEnvironment = "Local";
///
- /// Represents the default environment name used when CI/CD is auto-detected.
+ /// Represents the default value for when CI/CD is auto-detected
+ /// and no explicit override is configured.
///
public const string DefaultCiEnvironment = "CI";
diff --git a/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs b/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
index 870b972..8724909 100644
--- a/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
+++ b/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
@@ -417,7 +417,9 @@ private Dictionary CollectCustomProperties(string operatingSyste
{
Dictionary properties = new Dictionary();
- properties["ExecutionContext"] = ciPlatform.HasValue ? "CI" : "Local";
+ properties["ExecutionContext"] = ciPlatform.HasValue
+ ? XpingConfiguration.DefaultCiEnvironment
+ : XpingConfiguration.DefaultEnvironment;
if (!ciPlatform.HasValue && !isContainer)
{
properties["IsDeveloperMachine"] = "true";
From d557b1edf17e70637ecd2f12790358e42010f78a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 10 May 2026 09:16:35 +0000
Subject: [PATCH 4/5] chore: finalize environment detection polish
Agent-Logs-Url: https://github.com/xping-dev/sdk-dotnet/sessions/3f753eb5-b070-4f8a-a450-4423ad940fb6
Co-authored-by: xping-admin <180879096+xping-admin@users.noreply.github.com>
---
src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs | 3 +--
.../Services/Environment/Internals/EnvironmentDetector.cs | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs b/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
index 0e527f8..a164355 100644
--- a/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
+++ b/src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
@@ -18,8 +18,7 @@ public sealed class XpingConfiguration
public const string DefaultEnvironment = "Local";
///
- /// Represents the default value for when CI/CD is auto-detected
- /// and no explicit override is configured.
+ /// Represents the default value for when CI/CD is auto-detected and no explicit override is configured.
///
public const string DefaultCiEnvironment = "CI";
diff --git a/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs b/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
index 8724909..7fb974a 100644
--- a/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
+++ b/src/Xping.Sdk.Core/Services/Environment/Internals/EnvironmentDetector.cs
@@ -602,7 +602,7 @@ private static void AddIfNotNull(Dictionary dictionary, string k
const string headsPrefix = "refs/heads/";
return gitRef!.StartsWith(headsPrefix, StringComparison.OrdinalIgnoreCase)
- ? gitRef.Substring(headsPrefix.Length)
+ ? gitRef.Substring(headsPrefix.Length, gitRef.Length - headsPrefix.Length)
: gitRef;
}
From 0bbc3d2aa23ae5fcfc5b1c7c0a05c5f74fd35cd0 Mon Sep 17 00:00:00 2001
From: xping-admin
Date: Sun, 10 May 2026 17:04:59 +0200
Subject: [PATCH 5/5] Fix PR review comments: CopyConfiguration guard, test env
cleanup, doc consistency
- Guard target.Environment assignment in CopyConfiguration with HasExplicitEnvironment
check to prevent overwriting ASPNETCORE_ENVIRONMENT/DOTNET_ENVIRONMENT precedence
- Rename _ciVariables to _environmentVariables and add XPING_ENVIRONMENT,
ASPNETCORE_ENVIRONMENT, DOTNET_ENVIRONMENT to cleanup list to prevent flaky tests
- Fix YAML example in ci-cd-setup.md to use single underscore (XPING_CIENVIRONMENTNAME)
instead of double underscore which is not the SDK custom binding format
---
docs/getting-started/ci-cd-setup.md | 2 +-
.../Extensions/XpingServiceCollectionExtensions.cs | 6 +++++-
.../Services/Environment/EnvironmentDetectorTests.cs | 9 ++++++---
3 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/docs/getting-started/ci-cd-setup.md b/docs/getting-started/ci-cd-setup.md
index 1cde630..1453251 100644
--- a/docs/getting-started/ci-cd-setup.md
+++ b/docs/getting-started/ci-cd-setup.md
@@ -554,7 +554,7 @@ If you want auto-detected CI runs grouped under a label other than the default `
```yaml
env:
- XPING__CIENVIRONMENTNAME: "BuildPipeline"
+ XPING_CIENVIRONMENTNAME: "BuildPipeline"
```
---
diff --git a/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs b/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs
index bd254cc..30d81c8 100644
--- a/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs
+++ b/src/Xping.Sdk.Core/Extensions/XpingServiceCollectionExtensions.cs
@@ -558,7 +558,11 @@ private static void CopyConfiguration(XpingConfiguration source, XpingConfigurat
target.ProjectId = source.ProjectId;
target.BatchSize = source.BatchSize;
target.FlushInterval = source.FlushInterval;
- target.Environment = source.Environment;
+ if (source.HasExplicitEnvironment)
+ {
+ target.Environment = source.Environment;
+ }
+
target.AutoDetectCIEnvironment = source.AutoDetectCIEnvironment;
target.CiEnvironmentName = source.CiEnvironmentName;
target.Enabled = source.Enabled;
diff --git a/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs b/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs
index af148b1..5fbd672 100644
--- a/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs
+++ b/tests/Xping.Sdk.Core.Tests/Services/Environment/EnvironmentDetectorTests.cs
@@ -16,7 +16,7 @@ namespace Xping.Sdk.Core.Tests.Services.Environment;
[Collection("Sequential")]
public sealed class EnvironmentDetectorTests
{
- private static readonly string[] _ciVariables =
+ private static readonly string[] _environmentVariables =
[
"CI",
"GITHUB_ACTIONS",
@@ -28,12 +28,15 @@ public sealed class EnvironmentDetectorTests
"TEAMCITY_VERSION",
"BITBUCKET_PIPELINE_UUID",
"APPVEYOR",
+ "XPING_ENVIRONMENT",
+ "ASPNETCORE_ENVIRONMENT",
+ "DOTNET_ENVIRONMENT",
];
[Fact]
public async Task BuildEnvironmentInfoAsync_WithDotnetEnvironmentAndDefaultConfiguration_UsesDotnetEnvironment()
{
- using var clearedCiVariables = ClearEnvironmentVariables(_ciVariables);
+ using var clearedCiVariables = ClearEnvironmentVariables(_environmentVariables);
using var dotnetEnvironment = new EnvRestorer("DOTNET_ENVIRONMENT", "Development");
IEnvironmentDetector detector = CreateDetector();
@@ -47,7 +50,7 @@ public async Task BuildEnvironmentInfoAsync_WithDotnetEnvironmentAndDefaultConfi
[Fact]
public async Task BuildEnvironmentInfoAsync_WithLocalExecution_MarksDeveloperMachine()
{
- using var clearedCiVariables = ClearEnvironmentVariables(_ciVariables);
+ using var clearedCiVariables = ClearEnvironmentVariables(_environmentVariables);
IEnvironmentDetector detector = CreateDetector();