Skip to content

Commit b96d451

Browse files
committed
feat(telemetry): include CI provider and invocation source in payload
Detect the current CI environment and identify whether the CLI was invoked directly or via GitVersion.MsBuild to better understand usage patterns.
1 parent 672a6a2 commit b96d451

9 files changed

Lines changed: 245 additions & 15 deletions

File tree

docs/input/docs/usage/cli/telemetry.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Each payload includes:
2929
| --- | --- | --- |
3030
| `toolVersion` | The GitVersion CLI version | Helps correlate behavior with a released version |
3131
| `parserImplementation` | `ArgumentParser` or `LegacyArgumentParser` | Helps understand adoption of the legacy parser |
32+
| `continuousIntegrationProvider` | The detected CI provider such as `github-actions`, or `unknown` when GitVersion is not running in a recognized CI environment | Helps understand where CLI usage comes from without collecting repository data |
33+
| `invocationSource` | `gitversion-msbuild` when the CLI is invoked internally by the `GitVersion.MsBuild` package, otherwise `direct` | Helps distinguish direct CLI usage from package-driven usage |
3234
| `command` | The CLI command name (`gitversion`) | Identifies the invoked entry point |
3335
| `subcommand` | Reserved for future CLI command trees; currently `null` in the stable CLI | Keeps the payload shape stable |
3436
| `arguments[].name` | Canonical argument name | Shows which switches/options are used |

src/GitVersion.App.Tests/ArgumentParserTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ public void TelemetryPayloadIncludesParserVersionAndRedactedArguments()
131131
arguments.Telemetry.ParserImplementation.ShouldBe(nameof(ArgumentParser));
132132
arguments.Telemetry.Command.ShouldBe("gitversion");
133133
arguments.Telemetry.ToolVersion.ShouldNotBeNullOrWhiteSpace();
134+
arguments.Telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
135+
arguments.Telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
134136
arguments.Telemetry.Arguments.ShouldContain(argument =>
135137
argument.Name == TelemetryArgumentNames.Path && argument.Values.Single() == "<redacted:path>");
136138
arguments.Telemetry.Arguments.ShouldContain(argument =>

src/GitVersion.App.Tests/LegacyArgumentParserTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ public void TelemetryPayloadIncludesParserVersionAndRedactedArguments()
121121
arguments.Telemetry.ParserImplementation.ShouldBe(nameof(LegacyArgumentParser));
122122
arguments.Telemetry.Command.ShouldBe("gitversion");
123123
arguments.Telemetry.ToolVersion.ShouldNotBeNullOrWhiteSpace();
124+
arguments.Telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
125+
arguments.Telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
124126
arguments.Telemetry.Arguments.ShouldContain(argument =>
125127
argument.Name == TelemetryArgumentNames.Path && argument.Values.Single() == "<redacted:path>");
126128
arguments.Telemetry.Arguments.ShouldContain(argument =>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using GitVersion.OutputVariables;
2+
3+
namespace GitVersion.App.Tests;
4+
5+
[TestFixture]
6+
public class TelemetryContextEnricherTests
7+
{
8+
[Test]
9+
public void EnrichMapsSupportedContinuousIntegrationProviders()
10+
{
11+
var enricher = new TelemetryContextEnricher(new GitHubActions(), new TestEnvironment());
12+
13+
var telemetry = enricher.Enrich(CreateTelemetry());
14+
15+
telemetry.ContinuousIntegrationProvider.ShouldBe("github-actions");
16+
telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
17+
}
18+
19+
[Test]
20+
public void EnrichDetectsGitVersionMsBuildInvocations()
21+
{
22+
var environment = new TestEnvironment();
23+
environment.SetEnvironmentVariable(TelemetryContextValues.InternalCallerEnvironmentVariable, "GitVersion.MsBuild");
24+
var enricher = new TelemetryContextEnricher(new LocalBuild(), environment);
25+
26+
var telemetry = enricher.Enrich(CreateTelemetry());
27+
28+
telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
29+
telemetry.InvocationSource.ShouldBe(TelemetryContextValues.GitVersionMsBuild);
30+
}
31+
32+
[Test]
33+
public void EnrichDefaultsInvocationSourceToDirectForUnsupportedContext()
34+
{
35+
var enricher = new TelemetryContextEnricher(new UnsupportedBuildAgent(), new TestEnvironment());
36+
37+
var telemetry = enricher.Enrich(CreateTelemetry());
38+
39+
telemetry.ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
40+
telemetry.InvocationSource.ShouldBe(TelemetryContextValues.Direct);
41+
}
42+
43+
private static CommandLineTelemetry CreateTelemetry() => new(
44+
"1.2.3",
45+
nameof(ArgumentParser),
46+
TelemetryContextValues.Unknown,
47+
TelemetryContextValues.Direct,
48+
"gitversion",
49+
null,
50+
[]);
51+
52+
private sealed class TestEnvironment : IEnvironment
53+
{
54+
private readonly Dictionary<string, string?> variables = new(StringComparer.Ordinal);
55+
56+
public string? GetEnvironmentVariable(string variableName) =>
57+
this.variables.TryGetValue(variableName, out var value) ? value : null;
58+
59+
public void SetEnvironmentVariable(string variableName, string? value)
60+
{
61+
if (value == null)
62+
{
63+
this.variables.Remove(variableName);
64+
return;
65+
}
66+
67+
this.variables[variableName] = value;
68+
}
69+
}
70+
71+
private sealed class GitHubActions : GitVersion.Agents.ICurrentBuildAgent
72+
{
73+
public bool IsDefault => false;
74+
public bool CanApplyToCurrentContext() => true;
75+
public string? GetCurrentBranch(bool usingDynamicRepos) => null;
76+
public bool PreventFetch() => true;
77+
public bool ShouldCleanUpRemotes() => false;
78+
public void WriteIntegration(Action<string?> writer, GitVersionVariables variables, bool updateBuildNumber = true)
79+
{
80+
}
81+
}
82+
83+
private sealed class LocalBuild : GitVersion.Agents.ICurrentBuildAgent
84+
{
85+
public bool IsDefault => true;
86+
public bool CanApplyToCurrentContext() => true;
87+
public string? GetCurrentBranch(bool usingDynamicRepos) => null;
88+
public bool PreventFetch() => true;
89+
public bool ShouldCleanUpRemotes() => false;
90+
public void WriteIntegration(Action<string?> writer, GitVersionVariables variables, bool updateBuildNumber = true)
91+
{
92+
}
93+
}
94+
95+
private sealed class UnsupportedBuildAgent : GitVersion.Agents.ICurrentBuildAgent
96+
{
97+
public bool IsDefault => false;
98+
public bool CanApplyToCurrentContext() => true;
99+
public string? GetCurrentBranch(bool usingDynamicRepos) => null;
100+
public bool PreventFetch() => true;
101+
public bool ShouldCleanUpRemotes() => false;
102+
public void WriteIntegration(Action<string?> writer, GitVersionVariables variables, bool updateBuildNumber = true)
103+
{
104+
}
105+
}
106+
}

src/GitVersion.App.Tests/TelemetryReporterTests.cs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,23 @@ public void ReportWritesNoticeOnceAndEmitsTelemetryWhenSinkIsEnabled()
1111
var output = new StringBuilder();
1212
var sink = new TestTelemetrySink();
1313
var reporter = new TelemetryReporter(
14-
new TestConsoleAdapter(output), new TestTelemetryNoticeState(), sink, new TestTelemetryReleaseDateProvider(true, new(2026, 04, 26)),
14+
new TestConsoleAdapter(output), new TestTelemetryNoticeState(), sink, new TestTelemetryContextEnricher("github-actions", TelemetryContextValues.GitVersionMsBuild),
15+
new TestTelemetryReleaseDateProvider(true, new(2026, 04, 26)),
1516
new TestTelemetryUtcDateProvider(new(2026, 04, 26)));
1617
var arguments = new Arguments
1718
{
18-
Telemetry = new CommandLineTelemetry("1.2.3", nameof(ArgumentParser), "gitversion", null, []),
19+
Telemetry = CreateTelemetry(),
1920
TelemetryOptOut = false
2021
};
2122

2223
reporter.Report(arguments);
2324
reporter.Report(arguments);
2425

2526
sink.Payloads.Count.ShouldBe(2);
27+
sink.Payloads[0].ContinuousIntegrationProvider.ShouldBe("github-actions");
28+
sink.Payloads[0].InvocationSource.ShouldBe(TelemetryContextValues.GitVersionMsBuild);
29+
sink.Payloads[1].ContinuousIntegrationProvider.ShouldBe("github-actions");
30+
sink.Payloads[1].InvocationSource.ShouldBe(TelemetryContextValues.GitVersionMsBuild);
2631
output.ToString().Split("Telemetry", StringSplitOptions.None).Length.ShouldBe(2);
2732
}
2833

@@ -31,11 +36,12 @@ public void ReportSkipsOptedOutInvocations()
3136
{
3237
var sink = new TestTelemetrySink();
3338
var reporter = new TelemetryReporter(
34-
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryReleaseDateProvider(true, new(2026, 04, 26)),
39+
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryContextEnricher(),
40+
new TestTelemetryReleaseDateProvider(true, new(2026, 04, 26)),
3541
new TestTelemetryUtcDateProvider(new(2026, 04, 26)));
3642
var arguments = new Arguments
3743
{
38-
Telemetry = new CommandLineTelemetry("1.2.3", nameof(ArgumentParser), "gitversion", null, []),
44+
Telemetry = CreateTelemetry(),
3945
TelemetryOptOut = true
4046
};
4147

@@ -49,11 +55,12 @@ public void ReportSkipsTelemetryWhenReleaseDateIsMissing()
4955
{
5056
var sink = new TestTelemetrySink();
5157
var reporter = new TelemetryReporter(
52-
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryReleaseDateProvider(false, default),
58+
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryContextEnricher(),
59+
new TestTelemetryReleaseDateProvider(false, default),
5360
new TestTelemetryUtcDateProvider(new(2026, 04, 26)));
5461
var arguments = new Arguments
5562
{
56-
Telemetry = new CommandLineTelemetry("1.2.3", nameof(ArgumentParser), "gitversion", null, []),
63+
Telemetry = CreateTelemetry(),
5764
TelemetryOptOut = false
5865
};
5966

@@ -67,11 +74,12 @@ public void ReportSkipsTelemetryWhenReleaseWindowHasExpired()
6774
{
6875
var sink = new TestTelemetrySink();
6976
var reporter = new TelemetryReporter(
70-
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryReleaseDateProvider(true, new(2026, 01, 01)),
77+
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryContextEnricher(),
78+
new TestTelemetryReleaseDateProvider(true, new(2026, 01, 01)),
7179
new TestTelemetryUtcDateProvider(new(2026, 04, 01)));
7280
var arguments = new Arguments
7381
{
74-
Telemetry = new CommandLineTelemetry("1.2.3", nameof(ArgumentParser), "gitversion", null, []),
82+
Telemetry = CreateTelemetry(),
7583
TelemetryOptOut = false
7684
};
7785

@@ -80,6 +88,35 @@ public void ReportSkipsTelemetryWhenReleaseWindowHasExpired()
8088
sink.Payloads.ShouldBeEmpty();
8189
}
8290

91+
[Test]
92+
public void ReportDefaultsInvocationSourceToDirectWhenNoCallerIsDetected()
93+
{
94+
var sink = new TestTelemetrySink();
95+
var reporter = new TelemetryReporter(
96+
new TestConsoleAdapter(new StringBuilder()), new TestTelemetryNoticeState(), sink, new TestTelemetryContextEnricher(),
97+
new TestTelemetryReleaseDateProvider(true, new(2026, 04, 26)),
98+
new TestTelemetryUtcDateProvider(new(2026, 04, 26)));
99+
var arguments = new Arguments
100+
{
101+
Telemetry = CreateTelemetry(),
102+
TelemetryOptOut = false
103+
};
104+
105+
reporter.Report(arguments);
106+
107+
sink.Payloads.Single().ContinuousIntegrationProvider.ShouldBe(TelemetryContextValues.Unknown);
108+
sink.Payloads.Single().InvocationSource.ShouldBe(TelemetryContextValues.Direct);
109+
}
110+
111+
private static CommandLineTelemetry CreateTelemetry() => new(
112+
"1.2.3",
113+
nameof(ArgumentParser),
114+
TelemetryContextValues.Unknown,
115+
TelemetryContextValues.Direct,
116+
"gitversion",
117+
null,
118+
[]);
119+
83120
private sealed class TestTelemetrySink : ITelemetrySink
84121
{
85122
public bool IsEnabled => true;
@@ -107,6 +144,18 @@ public bool TryGetReleaseDate(out DateOnly value)
107144
}
108145
}
109146

147+
private sealed class TestTelemetryContextEnricher(
148+
string continuousIntegrationProvider = TelemetryContextValues.Unknown,
149+
string invocationSource = TelemetryContextValues.Direct
150+
) : ITelemetryContextEnricher
151+
{
152+
public CommandLineTelemetry Enrich(CommandLineTelemetry telemetry) => telemetry with
153+
{
154+
ContinuousIntegrationProvider = continuousIntegrationProvider,
155+
InvocationSource = invocationSource
156+
};
157+
}
158+
110159
private sealed class TestTelemetryUtcDateProvider(DateOnly utcToday) : ITelemetryUtcDateProvider
111160
{
112161
public DateOnly UtcToday { get; } = utcToday;

src/GitVersion.App/GitVersionTelemetryModule.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public void RegisterTypes(IServiceCollection services)
66
{
77
services.AddSingleton<ITelemetryReleaseDateProvider, AssemblyTelemetryReleaseDateProvider>();
88
services.AddSingleton<ITelemetryUtcDateProvider, TelemetryUtcDateProvider>();
9+
services.AddSingleton<ITelemetryContextEnricher, TelemetryContextEnricher>();
910
services.AddSingleton<ITelemetrySink, NoOpTelemetrySink>();
1011
services.AddSingleton<ITelemetryNoticeState, FileTelemetryNoticeState>();
1112
services.AddSingleton<ITelemetryReporter, TelemetryReporter>();

src/GitVersion.App/TelemetryModels.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,21 @@ public static bool IsWithinWindow(DateOnly releaseDate, DateOnly utcToday) =>
6666
utcToday < releaseDate.AddMonths(3);
6767
}
6868

69+
internal static class TelemetryContextValues
70+
{
71+
public const string Unknown = "unknown";
72+
public const string Direct = "direct";
73+
public const string GitVersionMsBuild = "gitversion-msbuild";
74+
public const string InternalCallerEnvironmentVariable = "GITVERSION_INTERNAL_CALLER";
75+
}
76+
6977
internal sealed record TelemetryArgument(string Name, IReadOnlyList<string> Values);
7078

7179
internal sealed record CommandLineTelemetry(
7280
string ToolVersion,
7381
string ParserImplementation,
82+
string ContinuousIntegrationProvider,
83+
string InvocationSource,
7484
string Command,
7585
string? Subcommand,
7686
IReadOnlyList<TelemetryArgument> Arguments
@@ -119,6 +129,8 @@ public void AddValues(string name, IEnumerable<string>? values, TelemetryValueKi
119129
public CommandLineTelemetry Build() => new(
120130
ToolVersion: TelemetryVersionProvider.GetCurrentVersion(),
121131
ParserImplementation: parserImplementation,
132+
ContinuousIntegrationProvider: TelemetryContextValues.Unknown,
133+
InvocationSource: TelemetryContextValues.Direct,
122134
Command: CommandName,
123135
Subcommand: null,
124136
Arguments: this.arguments

0 commit comments

Comments
 (0)