Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 31 additions & 4 deletions docs/configuration/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

---

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions docs/getting-started/ci-cd-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion src/Xping.Sdk.Core/Configuration/XpingConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ public sealed class XpingConfiguration
/// </summary>
public const string DefaultEnvironment = "Local";

/// <summary>
/// Represents the default value for <see cref="CiEnvironmentName"/> when CI/CD is auto-detected and no explicit override is configured.
/// </summary>
public const string DefaultCiEnvironment = "CI";

private string? _environment;

/// <summary>
/// Gets or sets the Xping API endpoint URL.
/// </summary>
Expand Down Expand Up @@ -57,14 +64,25 @@ public sealed class XpingConfiguration

/// <summary>
/// Gets or sets the environment name (e.g., "Local", "CI", "Staging", "Production").
/// When left unset, the SDK falls back to <see cref="DefaultEnvironment"/> unless a CI or framework-specific
/// environment variable takes precedence during environment detection.
/// </summary>
public string Environment { get; set; } = DefaultEnvironment;
public string Environment
{
get => string.IsNullOrWhiteSpace(_environment) ? DefaultEnvironment : _environment!;
set => _environment = value;
}

/// <summary>
/// Gets or sets a value indicating whether to automatically detect CI/CD environments.
/// </summary>
public bool AutoDetectCIEnvironment { get; set; } = true;

/// <summary>
/// Gets or sets the environment name to use when CI/CD is auto-detected.
/// </summary>
public string CiEnvironmentName { get; set; } = DefaultCiEnvironment;

/// <summary>
/// Gets or sets a value indicating whether the SDK is enabled.
/// </summary>
Expand Down Expand Up @@ -213,4 +231,6 @@ public bool IsValid()
{
return Validate().Count == 0;
}

internal bool HasExplicitEnvironment => !string.IsNullOrWhiteSpace(_environment);
}
11 changes: 11 additions & 0 deletions src/Xping.Sdk.Core/Configuration/XpingConfigurationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ public XpingConfigurationBuilder WithAutoDetectCIEnvironment(bool autoDetect)
return this;
}

/// <summary>
/// Sets the environment name to use when CI/CD is auto-detected.
/// </summary>
/// <param name="ciEnvironmentName">The CI/CD environment name.</param>
/// <returns>The builder instance for method chaining.</returns>
public XpingConfigurationBuilder WithCiEnvironmentName(string ciEnvironmentName)
{
_configuration.CiEnvironmentName = ciEnvironmentName;
return this;
}

/// <summary>
/// Sets whether the SDK is enabled.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -555,8 +558,13 @@ 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;
target.CaptureStackTraces = source.CaptureStackTraces;
target.EnableCompression = source.EnableCompression;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public interface IEnvironmentDetector
/// <summary>
/// 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"
/// </summary>
/// <remarks>
/// The environment name is used in confidence calculations, which are performed both globally across all executions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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()
Expand Down Expand Up @@ -415,6 +417,14 @@ private Dictionary<string, string> CollectCustomProperties(string operatingSyste
{
Dictionary<string, string> properties = new Dictionary<string, string>();

properties["ExecutionContext"] = ciPlatform.HasValue
? XpingConfiguration.DefaultCiEnvironment
: XpingConfiguration.DefaultEnvironment;
if (!ciPlatform.HasValue && !isContainer)
{
properties["IsDeveloperMachine"] = "true";
}

// Add container information
if (isContainer)
{
Expand Down Expand Up @@ -468,6 +478,12 @@ private Dictionary<string, string> 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"));
Expand All @@ -478,6 +494,9 @@ private Dictionary<string, string> 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;
Expand All @@ -488,6 +507,7 @@ private Dictionary<string, string> 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:
Expand All @@ -496,7 +516,11 @@ private Dictionary<string, string> 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:
Expand Down Expand Up @@ -556,6 +580,32 @@ private static void AddIfNotNull(Dictionary<string, string> 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.Length - headsPrefix.Length)
: gitRef;
}

private static string? GetEnvironmentVariable(string variable)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public void BuilderShouldAllowSettingAllProperties()
.WithFlushInterval(TimeSpan.FromMinutes(1))
.WithEnvironment("Production")
.WithAutoDetectCIEnvironment(false)
.WithCiEnvironmentName("Pipeline")
.WithEnabled(false)
.WithCaptureStackTraces(false)
.WithEnableCompression(false)
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IOptions<XpingConfiguration>>().Value;

Assert.Equal("BuildPipeline", bound.CiEnvironmentName);
}
}
Loading
Loading