Skip to content
Draft
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
36 changes: 30 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0

Expand All @@ -38,6 +38,30 @@ jobs:
name: NuGet
path: .\artifacts

- name: Create Test Results Directory
shell: pwsh
run: New-Item -ItemType Directory -Force -Path artifacts/test-results

- name: Test
shell: pwsh
run: |
cd src
dotnet run --project Uno.DevTools.Telemetry.Tests/Uno.DevTools.Telemetry.Tests.csproj --configuration Release -- --report-trx --results-directory ../artifacts/test-results

- name: Upload Test Results
uses: actions/upload-artifact@v4
with:
name: test-results
path: artifacts/test-results/*.trx

- name: Publish Test Results
uses: dorny/test-reporter@v2
if: always()
with:
name: Unit Tests
path: artifacts/test-results/*.trx
reporter: dotnet-trx

sign:
name: Sign Package
if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }}
Expand All @@ -51,7 +75,7 @@ jobs:
- build
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4

- name: Download Artifacts
uses: actions/download-artifact@v4
Expand All @@ -60,14 +84,14 @@ jobs:
path: artifacts\NuGet

- name: Setup .NET SDK
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'

- name: Setup SignClient
run: dotnet tool install --tool-path . sign --version 0.9.1-beta.25278.1

# Login to Azure using a ServicePrincipal configured to authenticate agaist a GitHub Action
# Login to Azure using a ServicePrincipal configured to authenticate against a GitHub Action
- name: 'Az CLI login'
uses: azure/login@v1
with:
Expand All @@ -83,8 +107,8 @@ jobs:
./sign code azure-key-vault
artifacts\NuGet\*.nupkg
--publisher-name "Uno.Devtools.Telemetry"
--description "Uno.Devtools.Telemetryk"
--description-url "https://github.com/${{ env.GITHUB_REPOSITORY }}"
--description "Uno.Devtools.Telemetry"
--azure-key-vault-managed-identity true
--azure-key-vault-url "${{ secrets.SIGN_KEY_VAULT_URL }}"
--azure-key-vault-certificate "${{ secrets.SIGN_KEY_VAULT_CERTIFICATE_ID }}"
Expand Down Expand Up @@ -136,4 +160,4 @@ jobs:
- name: NuGet Push
shell: pwsh
run: |
dotnet nuget push artifacts\*.nupkg -s https://api.nuget.org/v3/index.json -k "${{ secrets.NUGET_ORG_API_KEY }}"
dotnet nuget push artifacts\*.nupkg -s https://api.nuget.org/v3/index.json -k "${{ secrets.NUGET_ORG_API_KEY }}"
152 changes: 148 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,151 @@
# Uno Platform Developer tools for Telemetry
# Uno.DevTools.Telemetry

Learn how to [get started with Uno Platform](https://aka.platform.uno/get-started).
Uno.DevTools.Telemetry is a .NET library for collecting and sending telemetry data, designed for integration with Uno Platform development tools and other .NET applications.

This repository contains the telemetry source used through Uno Platform's developer tools, derived from the existing `dotnet.exe` telemetry.
## Features
- Optional but easy integration with .NET dependency injection (`IServiceCollection`)
- Supports Microsoft Application Insights
- Extensible and testable architecture
- File-based telemetry channel for local development
- High-performance, immutable code style
- Modern C# features and best practices

## Installation
Add the NuGet package to your project:

```shell
dotnet add package Uno.DevTools.Telemetry
```

## Usage

### Without DI

```csharp
const string instrumentationKey = ""; // GUID from AppInsight
const string analyticsEventPrefix = "uno/my-project"; // prefix for every analytics event

var telemetry = new Telemetry(
instrumentationKey,
analyticsEventPrefix,
typeof(MyProject).Assembly
);
```

### Registering Telemetry in DI

```csharp
using Uno.DevTools.Telemetry;

var services = new ServiceCollection();
services.AddTelemetry(
instrumentationKey: "<your-app-insights-key>",
eventNamePrefix: "MyApp/Telemetry",
versionAssembly: typeof(MyProject).Assembly,
sessionId: "optional-session-id"
);
```

#### Parameters for `AddTelemetry`
- `instrumentationKey` (**required**): Application Insights instrumentation key (GUID).
- `eventNamePrefix` (optional): Prefix for all telemetry events (e.g. `uno/my-project`).
- `versionAssembly` (optional): Assembly used for version info (defaults to calling assembly).
- `sessionId` (optional): Custom session id.

### Tracking Events

```csharp
var telemetry = serviceProvider.GetRequiredService<ITelemetry>();
telemetry.TrackEvent("AppStarted");
telemetry.TrackEvent("UserAction", new Dictionary<string, string> { { "Action", "Clicked" } }, null);
```

> **Warning:**
> When using `ITelemetry` (including `FileTelemetry` or any implementation), do not modify any dictionary or list after passing it as a parameter to telemetry methods (such as `TrackEvent`).
> All collections passed to telemetry should be considered owned by the telemetry system and must not be mutated by the caller after the call. Mutating collections after passing them may cause race conditions or undefined behavior.

### File-based Telemetry
By default, telemetry is persisted locally before being sent. You can configure the storage location and behavior by customizing the `Telemetry` constructor.

## Environment Variables
- `UNO_PLATFORM_TELEMETRY_OPTOUT`: Set to `true` to disable telemetry.
- `UNO_PLATFORM_TELEMETRY_FILE`: If set, telemetry will be logged to the specified file path using `FileTelemetry` instead of Application Insights. On .NET 8+, FileTelemetry uses `TimeProvider` for testable timestamps; on .NET Standard 2.0, it falls back to system time.
- `UNO_PLATFORM_TELEMETRY_SESSIONID`: (optional) Override the session id for telemetry events.

> **Note:**
> - The use of `UNO_PLATFORM_TELEMETRY_FILE` is intended for testing, debugging, or local development scenarios. To activate file-based telemetry, you must use the DI extension (`AddTelemetry`) so that the environment variable is detected and the correct implementation is injected.
> - FileTelemetry is thread-safe and writes events as single-line JSON, prefixed by context if provided.
> - Multi-framework support: On .NET 8+ and .NET 9+, `FileTelemetry` uses `TimeProvider` for testable timestamps. On netstandard2.0, it falls back to `DateTime.Now`.

## FileTelemetry & Testing

When `UNO_PLATFORM_TELEMETRY_FILE` is set, all telemetry events are written to the specified file. This is especially useful for automated tests, CI validation, or debugging telemetry output locally. The file will contain one JSON object per line, optionally prefixed by the context (e.g., session or connection id).

Example output:
```
global: {"Timestamp":"2025-07-07T12:34:56.789Z","EventName":"AppStarted","Properties":{},"Measurements":null}
```

## Crash/Exception Reporting

Crash and exception reporting is planned for a future release. For now, only explicit event tracking is supported. See `todos.md` for roadmap.

## Multi-framework Support

Uno.DevTools.Telemetry targets:
- .NET Standard 2.0 (broad compatibility)
- .NET 8.0
- .NET 9.0

All features are available on .NET 8+; some features (like testable time via `TimeProvider`) are not available on netstandard2.0 and will fallback to system time.

---

*For more details, see the code and comments in the repository.*

### Example: Using FileTelemetry from the Command Line (PowerShell)

To log telemetry events to a file for testing or debugging, set the environment variable before launching your application (the application must use Uno.DevTools.Telemetry via DI):

```powershell
$env:UNO_PLATFORM_TELEMETRY_FILE = "telemetry.log"
dotnet run --project path/to/YourApp.csproj
```

Replace `path/to/YourApp.csproj` with the path to your application's project file. All telemetry events will be written to `telemetry.log` in the working directory.

> **Tip:** You can also specify an absolute path for the log file (e.g., `C:\temp\telemetry.log`).

## Advanced usage: typed/contextual telemetry with DI

To inject contextualized telemetry by type:

```csharp
// In your Startup or Program.cs
services.AddTelemetry();

// Somewhere in your assembly containing the service
[assembly: Telemetry("instrumentation-key", "prefix")]

// In an application class
public class MyService
{
private readonly ITelemetry<MyService> _telemetry;
public MyService(ITelemetry<MyService> telemetry) // Telemetry will be properly configured using the [assembly: Telemetry] attribute
{
_telemetry = telemetry;
}
public void DoSomething()
{
_telemetry.TrackEvent("Action", new Dictionary<string, string> { { "key", "value" } }, null);
}
}
```

The injected instance will automatically use the assembly-level configuration of `MyService` (the `[Telemetry]` attribute).

- If the `UNO_PLATFORM_TELEMETRY_FILE` environment variable is set, the instance will be a `FileTelemetry`.
- Otherwise, the instance will be a `Telemetry` (Application Insights).

> **Note:** You can inject `ITelemetry<T>` for any type, and resolution will be automatic via the DI container.

The package generated from this repository is not integrated into end-user apps, and should be not be referenced directly.
5 changes: 4 additions & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<GenerateSBOM>true</GenerateSBOM>
<NoWarn>$(NoWarn);NU1507</NoWarn>
<NoWarn>$(NoWarn);NU1507;NU5030</NoWarn>

<Deterministic>true</Deterministic>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

<Authors>Uno Platform</Authors>
<Copyright>Copyright (c) Uno Platform 2015-$([System.DateTime]::Now.ToString(`yyyy`))</Copyright>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Expand Down
19 changes: 12 additions & 7 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
<Project>
<ItemGroup>
<PackageVersion Include="Microsoft.Sbom.Targets" Version="3.0.0" />
<PackageVersion Include="Microsoft.ApplicationInsights" Version="2.20.0" />
<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1"/>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Sbom.Targets" Version="3.0.0" />
<PackageVersion Include="Microsoft.ApplicationInsights" Version="2.20.0" />
<PackageVersion Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
<PackageVersion Include="PolySharp" Version="1.15.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="1.7.3" />
<PackageVersion Include="System.Text.Json" Version="9.0.6" />
<PackageVersion Include="AwesomeAssertions" Version="9.0.0" />
</ItemGroup>
</Project>
104 changes: 104 additions & 0 deletions src/Uno.DevTools.Telemetry.Tests/FileTelemetryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace Uno.DevTools.Telemetry.Tests
{
[TestClass]
public class FileTelemetryTests
{
private string GetTempFilePath() => Path.Combine(Path.GetTempPath(), $"telemetry_test_{Guid.NewGuid():N}.log");

[TestMethod]
public void Given_FileTelemetry_When_TrackEvent_Then_WritesToFile()
{
// Arrange
var filePath = GetTempFilePath();
var telemetry = new FileTelemetry(filePath, "test");

// Act
telemetry.TrackEvent("TestEvent", new Dictionary<string, string> { { "foo", "bar" } }, null);
telemetry.Flush();

// Assert
var lines = File.ReadAllLines(filePath);
lines.Should().HaveCount(1);
lines[0].Should().Contain("TestEvent");
lines[0].Should().Contain("foo");
lines[0].Should().Contain("bar");
}

[TestMethod]
public void Given_FileTelemetry_When_TrackEvent_MultipleEvents_Then_AllAreWritten()
{
// Arrange
var filePath = GetTempFilePath();
var telemetry = new FileTelemetry(filePath, "multi");

// Act
for (var i = 0; i < 5; i++)
{
telemetry.TrackEvent($"Event{i}", (IDictionary<string, string>?)null, (IDictionary<string, double>?)null);
}
telemetry.Flush();

// Assert
var lines = File.ReadAllLines(filePath);
lines.Should().HaveCount(5);
for (var i = 0; i < 5; i++)
{
lines[i].Should().Contain($"Event{i}");
}
}

[TestMethod]
public void Given_FileTelemetry_When_TrackEvent_MultiThreaded_Then_AllEventsAreWrittenWithoutError()
{
// Arrange
var filePath = GetTempFilePath();
var telemetry = new FileTelemetry(filePath, "stress");
var threadCount = 16;
var eventsPerThread = 800;
var totalEvents = threadCount * eventsPerThread;
var threads = new List<Thread>();
var exceptions = new List<Exception>();
var startEvent = new ManualResetEventSlim(false);

for (var t = 0; t < threadCount; t++)
{
threads.Add(new Thread(() =>
{
try
{
startEvent.Wait(); // Ensure all threads start together
for (var i = 0; i < eventsPerThread; i++)
{
telemetry.TrackEvent($"StressEvent", new Dictionary<string, string> { { "thread", Thread.CurrentThread.ManagedThreadId.ToString() }, { "i", i.ToString() } }, null);
}
}
catch (Exception ex)
{
lock (exceptions) { exceptions.Add(ex); }
}
}));
}

// Act
threads.ForEach(t => t.Start());
startEvent.Set();
threads.ForEach(t => t.Join());
telemetry.Flush();

// Assert
exceptions.Should().BeEmpty();
var lines = File.ReadAllLines(filePath);
lines.Should().HaveCount(totalEvents);
foreach (var line in lines)
{
line.Should().Contain("StressEvent");
line.Should().Contain("thread");
line.Should().Contain("i");
}
}
}
}
7 changes: 7 additions & 0 deletions src/Uno.DevTools.Telemetry.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Global using directives

global using System;
global using AwesomeAssertions;
global using Microsoft.VisualStudio.TestTools.UnitTesting;
global using Uno.DevTools.Telemetry.PersistenceChannel;
global using Microsoft.Extensions.DependencyInjection;
Loading