Skip to content

Commit 074e50c

Browse files
committed
feat(telemetry): add EventsPrefix support to FileTelemetry and propagate from ServiceCollectionExtensions
1 parent 014cd9e commit 074e50c

File tree

4 files changed

+133
-33
lines changed

4 files changed

+133
-33
lines changed

src/Uno.UI.RemoteControl.DevServer.Tests/Helpers/DevServerTestHelper.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,14 @@ public async Task<bool> StartAsync(CancellationToken ct, int timeout = 30000)
123123
{
124124
foreach (var variable in _environmentVariables)
125125
{
126-
// Only add if not already present to preserve existing values
127-
if (!startInfo.Environment.ContainsKey(variable.Key))
126+
// For testing purposes, allow overriding environment variables
127+
// This is especially important for telemetry redirection in tests
128+
if (startInfo.Environment.ContainsKey(variable.Key))
128129
{
129-
startInfo.Environment[variable.Key] = variable.Value;
130-
}
131-
else
132-
{
133-
_logger.LogWarning("Environment variable {Key} already exists, skipping override",
130+
_logger.LogWarning("Environment variable {Key} already exists, overriding for test",
134131
variable.Key);
135132
}
133+
startInfo.Environment[variable.Key] = variable.Value;
136134
}
137135
}
138136

src/Uno.UI.RemoteControl.DevServer.Tests/Telemetry/ServerTelemetryTests.cs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,69 @@ public async Task Telemetry_Server_LogsConnectionEvents()
4848
await helper.StopAsync(CT);
4949
if (File.Exists(filePath))
5050
{
51-
try { File.Delete(filePath); } catch { }
51+
try { File.Delete(filePath); }
52+
catch { }
53+
}
54+
}
55+
}
56+
57+
[TestMethod]
58+
public async Task Telemetry_FileTelemetry_AppliesEventsPrefix()
59+
{
60+
var solution = SolutionHelper!;
61+
62+
// Arrange
63+
var fileName = GetTestTelemetryFileName("eventsprefix");
64+
var tempDir = Path.GetTempPath();
65+
var filePath = Path.Combine(tempDir, fileName);
66+
await solution.CreateSolutionFile();
67+
await using var helper = CreateTelemetryHelperWithExactPath(filePath, solutionPath: solution.SolutionFile);
68+
69+
try
70+
{
71+
// Act - Start server which will trigger DevServer.Startup event
72+
var started = await helper.StartAsync(CT);
73+
helper.EnsureStarted();
74+
75+
// Wait a bit for telemetry to be written
76+
await Task.Delay(1000, CT);
77+
await helper.AttemptGracefulShutdown(CT);
78+
79+
var fileExists = File.Exists(filePath);
80+
var fileContent = fileExists ? await File.ReadAllTextAsync(filePath, CT) : string.Empty;
81+
var events = fileContent.Length > 0 ? ParseTelemetryEvents(fileContent) : new();
82+
83+
// Assert
84+
started.Should().BeTrue();
85+
fileExists.Should().BeTrue();
86+
events.Should().NotBeEmpty();
87+
88+
// Verify that events have the EventsPrefix applied
89+
// The EventsPrefix should be "uno/dev-server" based on the TelemetryAttribute
90+
var hasEventWithPrefix = events.Any(e =>
91+
e.Json.RootElement.TryGetProperty("EventName", out var eventName) &&
92+
eventName.GetString()?.StartsWith("uno/dev-server/") == true);
93+
94+
hasEventWithPrefix.Should()
95+
.BeTrue("Events should have the EventsPrefix 'uno/dev-server/' applied to event names");
96+
97+
// Log some events for debugging
98+
Console.WriteLine($"[DEBUG_LOG] Found {events.Count} telemetry events:");
99+
foreach (var (prefix, json) in events.Take(5))
100+
{
101+
if (json.RootElement.TryGetProperty("EventName", out var eventName))
102+
{
103+
Console.WriteLine($"[DEBUG_LOG] Prefix: {prefix}, EventName: {eventName.GetString()}");
104+
}
105+
}
106+
}
107+
finally
108+
{
109+
await helper.StopAsync(CT);
110+
if (File.Exists(filePath))
111+
{
112+
try { File.Delete(filePath); }
113+
catch { }
52114
}
53115
}
54116
}

src/Uno.UI.RemoteControl.Server/Helpers/ServiceCollectionExtensions.cs

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
using Microsoft.Extensions.DependencyInjection;
66
using Uno.UI.RemoteControl.Server.Telemetry;
77

8-
[assembly: Telemetry("DevServer", EventsPrefix = "uno/dev-server")]
8+
#if DEBUG
9+
[assembly: Telemetry("81286976-e3a4-49fb-b03b-30315092dbc4", EventsPrefix = "uno/dev-server")]
10+
#else
11+
[assembly: Telemetry("9a44058e-1913-4721-a979-9582ab8bedce", EventsPrefix = "uno/dev-server")]
12+
#endif
913

1014
namespace Uno.UI.RemoteControl.Server.Helpers
1115
{
@@ -20,8 +24,7 @@ public static IServiceCollection AddGlobalTelemetry(this IServiceCollection serv
2024
// Register root telemetry session as singleton
2125
services.AddSingleton<TelemetrySession>(svc => new TelemetrySession
2226
{
23-
SessionType = TelemetrySessionType.Root,
24-
CreatedAt = DateTime.UtcNow
27+
SessionType = TelemetrySessionType.Root, CreatedAt = DateTime.UtcNow
2528
});
2629

2730
// Register global telemetry service as singleton
@@ -44,7 +47,8 @@ public static IServiceCollection AddConnectionTelemetry(this IServiceCollection
4447
services.AddScoped<TelemetrySession>(svc => CreateConnectionTelemetrySession(svc));
4548

4649
// Register connection-specific telemetry service as scoped
47-
services.AddScoped<ITelemetry>(svc => CreateTelemetry(typeof(ITelemetry).Assembly, svc.GetRequiredService<TelemetrySession>().Id.ToString("N")));
50+
services.AddScoped<ITelemetry>(svc => CreateTelemetry(typeof(ITelemetry).Assembly,
51+
svc.GetRequiredService<TelemetrySession>().Id.ToString("N")));
4852
services.AddScoped(typeof(ITelemetry<>), typeof(TelemetryAdapter<>));
4953

5054
return services;
@@ -63,7 +67,8 @@ public static IServiceCollection AddTelemetry(this IServiceCollection services)
6367
// Register TelemetrySession as scoped with connection context integration
6468
services.AddScoped<TelemetrySession>(svc => CreateTelemetrySession(svc));
6569

66-
services.AddScoped<ITelemetry>(svc => CreateTelemetry(typeof(ITelemetry).Assembly, svc.GetRequiredService<TelemetrySession>().Id.ToString("N")));
70+
services.AddScoped<ITelemetry>(svc => CreateTelemetry(typeof(ITelemetry).Assembly,
71+
svc.GetRequiredService<TelemetrySession>().Id.ToString("N")));
6772
services.AddScoped(typeof(ITelemetry<>), typeof(TelemetryAdapter<>));
6873

6974
services.AddSingleton<ITelemetry>(svc => CreateTelemetry(typeof(ITelemetry).Assembly));
@@ -86,8 +91,10 @@ private static TelemetrySession CreateConnectionTelemetrySession(IServiceProvide
8691
};
8792

8893
// Add connection metadata to the telemetry session
89-
session.AddMetadata("RemoteIpAddress", TelemetryHashHelper.Hash(connectionContext.RemoteIpAddress?.ToString() ?? "Unknown"));
90-
session.AddMetadata("ConnectedAt", connectionContext.ConnectedAt.ToString("yyyy-MM-dd HH:mm:ss UTC", DateTimeFormatInfo.InvariantInfo));
94+
session.AddMetadata("RemoteIpAddress",
95+
TelemetryHashHelper.Hash(connectionContext.RemoteIpAddress?.ToString() ?? "Unknown"));
96+
session.AddMetadata("ConnectedAt",
97+
connectionContext.ConnectedAt.ToString("yyyy-MM-dd HH:mm:ss UTC", DateTimeFormatInfo.InvariantInfo));
9198

9299
if (!string.IsNullOrEmpty(connectionContext.UserAgent))
93100
{
@@ -111,7 +118,8 @@ private static TelemetrySession CreateTelemetrySession(IServiceProvider svc)
111118
var connectionContext = svc.GetService<ConnectionContext>();
112119
var session = new TelemetrySession
113120
{
114-
SessionType = connectionContext != null ? TelemetrySessionType.Connection : TelemetrySessionType.Root,
121+
SessionType =
122+
connectionContext != null ? TelemetrySessionType.Connection : TelemetrySessionType.Root,
115123
ConnectionId = connectionContext?.ConnectionId,
116124
CreatedAt = DateTime.UtcNow
117125
};
@@ -120,7 +128,9 @@ private static TelemetrySession CreateTelemetrySession(IServiceProvider svc)
120128
if (connectionContext != null)
121129
{
122130
session.AddMetadata("RemoteIpAddress", connectionContext.RemoteIpAddress?.ToString() ?? "Unknown");
123-
session.AddMetadata("ConnectedAt", connectionContext.ConnectedAt.ToString("yyyy-MM-dd HH:mm:ss UTC", DateTimeFormatInfo.InvariantInfo));
131+
session.AddMetadata("ConnectedAt",
132+
connectionContext.ConnectedAt.ToString("yyyy-MM-dd HH:mm:ss UTC",
133+
DateTimeFormatInfo.InvariantInfo));
124134

125135
if (!string.IsNullOrEmpty(connectionContext.UserAgent))
126136
{
@@ -142,31 +152,36 @@ private static TelemetrySession CreateTelemetrySession(IServiceProvider svc)
142152
/// </summary>
143153
private static ITelemetry CreateTelemetry(Assembly asm, string? sessionId = null)
144154
{
155+
// Get telemetry configuration first
156+
if (asm.GetCustomAttribute<TelemetryAttribute>() is not { } config)
157+
{
158+
throw new InvalidOperationException($"No telemetry config found for assembly {asm}.");
159+
}
160+
161+
var eventsPrefix = config.EventsPrefix ?? $"uno/{asm.GetName().Name?.ToLowerInvariant()}";
162+
145163
// Check for telemetry redirection environment variable
146164
var telemetryFilePath = Environment.GetEnvironmentVariable("UNO_PLATFORM_TELEMETRY_FILE");
147165
if (!string.IsNullOrEmpty(telemetryFilePath))
148166
{
149-
// New behavior: use contextual naming
167+
// New behavior: use contextual naming with events prefix
150168
if (string.IsNullOrEmpty(sessionId))
151169
{
152170
// Global telemetry - use contextual naming
153-
return new FileTelemetry(telemetryFilePath, "global");
171+
return new FileTelemetry(telemetryFilePath, "global", eventsPrefix);
154172
}
155173
else
156174
{
157175
// Connection telemetry - use session ID as context
158176
var shortSessionId = sessionId.Length > 8 ? sessionId.Substring(0, 8) : sessionId;
159-
return new FileTelemetry(telemetryFilePath, $"connection-{shortSessionId}");
177+
return new FileTelemetry(telemetryFilePath, $"connection-{shortSessionId}", eventsPrefix);
160178
}
161179
}
162180

163-
if (asm.GetCustomAttribute<TelemetryAttribute>() is { } config)
164-
{
165-
var telemetry = new Uno.DevTools.Telemetry.Telemetry(config.InstrumentationKey, config.EventsPrefix ?? $"uno/{asm.GetName().Name?.ToLowerInvariant()}", asm, sessionId);
166-
return new TelemetryWrapper(telemetry);
167-
}
168-
169-
throw new InvalidOperationException($"No telemetry config found for assembly {asm}.");
181+
// Use normal telemetry
182+
var telemetry =
183+
new Uno.DevTools.Telemetry.Telemetry(config.InstrumentationKey, eventsPrefix, asm, sessionId);
184+
return new TelemetryWrapper(telemetry);
170185
}
171186
}
172187
}

src/Uno.UI.RemoteControl.Server/Telemetry/FileTelemetry.cs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,30 @@ public class FileTelemetry : ITelemetry
2121

2222
private readonly string _filePath;
2323
private readonly string _contextPrefix;
24+
private readonly string? _eventsPrefix;
2425
private readonly object _lock = new();
2526

2627
/// <summary>
2728
/// Creates a FileTelemetry instance with a contextual file name or prefix.
2829
/// </summary>
2930
/// <param name="baseFilePath">The base file path (with or without extension)</param>
3031
/// <param name="context">The telemetry context (e.g., "global", "connection", session ID)</param>
31-
public FileTelemetry(string baseFilePath, string context)
32+
/// <param name="eventsPrefix">The events prefix to prepend to event names (e.g., "uno/dev-server")</param>
33+
public FileTelemetry(string baseFilePath, string context, string? eventsPrefix = null)
3234
{
3335
if (string.IsNullOrEmpty(baseFilePath))
3436
{
3537
throw new ArgumentNullException(nameof(baseFilePath));
3638
}
39+
3740
if (string.IsNullOrEmpty(context))
3841
{
3942
throw new ArgumentNullException(nameof(context));
4043
}
4144

4245
_filePath = baseFilePath;
4346
_contextPrefix = context; // Save the context as a prefix for events
47+
_eventsPrefix = eventsPrefix;
4448
EnsureDirectoryExists(_filePath);
4549
}
4650

@@ -53,6 +57,19 @@ private static void EnsureDirectoryExists(string filePath)
5357
}
5458
}
5559

60+
/// <summary>
61+
/// Applies the events prefix to the event name if configured.
62+
/// </summary>
63+
private string ApplyEventsPrefix(string eventName)
64+
{
65+
if (string.IsNullOrEmpty(_eventsPrefix))
66+
{
67+
return eventName;
68+
}
69+
70+
return $"{_eventsPrefix}/{eventName}";
71+
}
72+
5673
public bool Enabled => true;
5774

5875
public void Dispose()
@@ -67,12 +84,14 @@ public void Flush()
6784

6885
public Task FlushAsync(CancellationToken ct) => Task.CompletedTask;
6986

70-
public void ThreadBlockingTrackEvent(string eventName, IDictionary<string, string> properties, IDictionary<string, double> measurements)
87+
public void ThreadBlockingTrackEvent(string eventName, IDictionary<string, string> properties,
88+
IDictionary<string, double> measurements)
7189
{
7290
TrackEvent(eventName, properties, measurements);
7391
}
7492

75-
public void TrackEvent(string eventName, (string key, string value)[]? properties, (string key, double value)[]? measurements)
93+
public void TrackEvent(string eventName, (string key, string value)[]? properties,
94+
(string key, double value)[]? measurements)
7695
{
7796
var propertiesDict = properties != null ? new Dictionary<string, string>() : null;
7897
if (properties != null)
@@ -95,20 +114,26 @@ public void TrackEvent(string eventName, (string key, string value)[]? propertie
95114
TrackEvent(eventName, propertiesDict, measurementsDict);
96115
}
97116

98-
public void TrackEvent(string eventName, IDictionary<string, string>? properties, IDictionary<string, double>? measurements)
117+
public void TrackEvent(string eventName, IDictionary<string, string>? properties,
118+
IDictionary<string, double>? measurements)
99119
{
120+
// Apply events prefix to event name
121+
var prefixedEventName = ApplyEventsPrefix(eventName);
122+
100123
// Add the context prefix to properties if specified
101124
var finalProperties = properties;
102125
if (!string.IsNullOrEmpty(_contextPrefix))
103126
{
104127
// Clone properties and add context
105-
finalProperties = properties != null ? new Dictionary<string, string>(properties) : new Dictionary<string, string>();
128+
finalProperties = properties != null
129+
? new Dictionary<string, string>(properties)
130+
: new Dictionary<string, string>();
106131
}
107132

108133
var telemetryEvent = new
109134
{
110135
Timestamp = DateTime.Now, // Use local time for easier follow-up
111-
EventName = eventName,
136+
EventName = prefixedEventName,
112137
Properties = finalProperties,
113138
Measurements = measurements
114139
};

0 commit comments

Comments
 (0)