diff --git a/WinUIGallery/Telemetry/Events/EmptyEvent.cs b/WinUIGallery/Telemetry/Events/EmptyEvent.cs new file mode 100644 index 000000000..a8947b127 --- /dev/null +++ b/WinUIGallery/Telemetry/Events/EmptyEvent.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Diagnostics.Telemetry.Internal; +using System; +using System.Diagnostics.Tracing; + +namespace AIDevGallery.Telemetry; + +[EventData] +internal class EmptyEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags { get; } + + public EmptyEvent(PartA_PrivTags tags) + { + PartA_PrivTags = tags; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive string + } +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/Events/EventBase.cs b/WinUIGallery/Telemetry/Events/EventBase.cs new file mode 100644 index 000000000..557624ed0 --- /dev/null +++ b/WinUIGallery/Telemetry/Events/EventBase.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Diagnostics.Telemetry.Internal; +using System; +using System.Diagnostics.Tracing; + +namespace AIDevGallery.Telemetry; + +/// +/// Base class for all telemetry events to ensure they are properly tagged. +/// +/// +/// The public properties of each event are logged in the telemetry. +/// We should not change an event's properties, as that could break the processing of that event's data. +/// +[EventData] +public abstract class EventBase +{ + /// + /// Gets the privacy datatype tag for the telemetry event. + /// +#pragma warning disable CA1707 // Identifiers should not contain underscores + public abstract PartA_PrivTags PartA_PrivTags +#pragma warning restore CA1707 // Identifiers should not contain underscores + { + get; + } + + /// + /// Replaces all the strings in this event that may contain PII using the provided function. + /// + /// + /// This is called by before logging the event. + /// It is the responsibility of each event to ensure we replace all strings with possible PII; + /// we ensure we at least consider this by forcing to implement this. + /// + /// + /// A function that replaces all the sensitive strings in a given string with tokens + /// + public abstract void ReplaceSensitiveStrings(Func replaceSensitiveStrings); +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/Events/NavigatedToSampleEvent.cs b/WinUIGallery/Telemetry/Events/NavigatedToSampleEvent.cs new file mode 100644 index 000000000..ef513d5cf --- /dev/null +++ b/WinUIGallery/Telemetry/Events/NavigatedToSampleEvent.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; +using System; +using System.Diagnostics.Tracing; + +namespace AIDevGallery.Telemetry.Events; + +[EventData] +internal class NavigatedToSampleEvent : EventBase +{ + public override PartA_PrivTags PartA_PrivTags => PrivTags.ProductAndServiceUsage; + + public string Name { get; } + + public DateTime StartTime { get; } + + private NavigatedToSampleEvent(string name, DateTime startTime) + { + Name = name; + StartTime = startTime; + } + + public override void ReplaceSensitiveStrings(Func replaceSensitiveStrings) + { + // No sensitive strings to replace. + } + + public static void Log(string name) + { + TelemetryFactory.Get().Log("NavigatedToSample_Event", LogLevel.Critical, new NavigatedToSampleEvent(name, DateTime.Now)); + } +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/ITelemetry.cs b/WinUIGallery/Telemetry/ITelemetry.cs new file mode 100644 index 000000000..ff073ab7f --- /dev/null +++ b/WinUIGallery/Telemetry/ITelemetry.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace AIDevGallery.Telemetry; + +internal interface ITelemetry +{ + /// + /// Add a string that we should try stripping out of some of our telemetry for sensitivity reasons (ex. VM name, etc.). + /// We can never be 100% sure we can remove every string, but this should greatly reduce us collecting PII. + /// Note that the order in which AddSensitive is called matters, as later when we call ReplaceSensitiveStrings, it will try + /// finding and replacing the earlier strings first. This can be helpful, since we can target specific + /// strings (like username) first, which should help preserve more information helpful for diagnosis. + /// + /// Sensitive string to add (ex. "c:\xyz") + /// string to replace it with (ex. "-path-") + public void AddSensitiveString(string name, string replaceWith); + + /// + /// Gets a value indicating whether telemetry is on + /// For future use if we add a registry key or some other setting to check if telemetry is turned on. + /// + public bool IsTelemetryOn { get; } + + /// + /// Gets or sets a value indicating whether diagnostic telemetry is on. + /// + public bool IsDiagnosticTelemetryOn { get; set; } + + /// + /// Logs an exception at Measure level. To log at Critical level, the event name needs approval. + /// + /// What we trying to do when the exception occurred. + /// Exception object + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + public void LogException(string action, Exception e, Guid? relatedActivityId = null); + + /// + /// Log the time an action took (ex. time spent on a tool). + /// + /// The measurement we're performing (ex. "DeployTime"). + /// How long the action took in milliseconds. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + public void LogTimeTaken(string eventName, uint timeTakenMilliseconds, Guid? relatedActivityId = null); + + /// + /// Log an event with no additional data. + /// + /// The name of the event to log + /// Set to true if an error condition raised this event. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + public void LogCritical(string eventName, bool isError = false, Guid? relatedActivityId = null); + + /// + /// Log an informational event. Typically used for just a single event that's only called one place in the code. + /// If you are logging the same event multiple times, it's best to add a helper method in Telemetry + /// + /// Name of the error event + /// Determines whether to upload the data to our servers, and on how many machines. + /// Values to send to the telemetry system. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + /// Anonymous type. + public void Log<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(string eventName, LogLevel level, T data, Guid? relatedActivityId = null) + where T : EventBase; + + /// + /// Log an error event. Typically used for just a single event that's only called one place in the code. + /// If you are logging the same event multiple times, it's best to add a helper method in Telemetry + /// + /// Name of the error event + /// Determines whether to upload the data to our servers, and on how many machines. + /// Values to send to the telemetry system. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + /// Anonymous type. + public void LogError<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(string eventName, LogLevel level, T data, Guid? relatedActivityId = null) + where T : EventBase; +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/LogLevel.cs b/WinUIGallery/Telemetry/LogLevel.cs new file mode 100644 index 000000000..26bed7354 --- /dev/null +++ b/WinUIGallery/Telemetry/LogLevel.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace AIDevGallery.Telemetry; + +/// +/// Telemetry Levels. +/// These levels are defined by our telemetry system, so it's possible the sampling +/// could change in the future. +/// There aren't any convenient enums we can consume, so create our own. +/// +public enum LogLevel +{ + /// + /// Local. + /// Only log telemetry locally on the machine (similar to an ETW event). + /// + Local, + + /// + /// Info. + /// Send telemetry from internal and flighted machines, but no external retail machines. + /// + Info, + + /// + /// Measure. + /// Send telemetry from internal and flighted machines, plus a small, sample % of retail machines. + /// Should only be used for telemetry we use to derive measures from. + /// + Measure, + + /// + /// Critical. + /// Send telemetry from all devices sampled at 100%. + /// Should only be used for approved events. + /// + Critical, +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/PrivacyConsentHelpers.cs b/WinUIGallery/Telemetry/PrivacyConsentHelpers.cs new file mode 100644 index 000000000..07a50a4d0 --- /dev/null +++ b/WinUIGallery/Telemetry/PrivacyConsentHelpers.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Linq; +using Windows.Globalization; + +namespace AIDevGallery.Telemetry; + +internal static class PrivacyConsentHelpers +{ + private static readonly string[] PrivacySensitiveRegions = + [ + "AUT", + "BEL", + "BGR", + "BRA", + "CAN", + "HRV", + "CYP", + "CZE", + "DNK", + "EST", + "FIN", + "FRA", + "DEU", + "GRC", + "HUN", + "ISL", + "IRL", + "ITA", + "KOR", // Double Check + "LVA", + "LIE", + "LTU", + "LUX", + "MLT", + "NLD", + "NOR", + "POL", + "PRT", + "ROU", + "SVK", + "SVN", + "ESP", + "SWE", + "CHE", + "GBR", + ]; + + public static bool IsPrivacySensitiveRegion() + { + var geographicRegion = new GeographicRegion(); + + return PrivacySensitiveRegions.Contains(geographicRegion.CodeThreeLetter, StringComparer.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/Telemetry.cs b/WinUIGallery/Telemetry/Telemetry.cs new file mode 100644 index 000000000..c9ccd6165 --- /dev/null +++ b/WinUIGallery/Telemetry/Telemetry.cs @@ -0,0 +1,349 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Diagnostics.Telemetry; +using Microsoft.Diagnostics.Telemetry.Internal; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; + +namespace AIDevGallery.Telemetry; + +/// +/// To create an instance call TelemetryFactory.Get<ITelemetry>() +/// +internal sealed class Telemetry : ITelemetry +{ + private const string ProviderName = "Microsoft.Windows.AIDevGallery"; + + /// + /// Time Taken Event Name + /// + private const string TimeTakenEventName = "TimeTaken"; + + /// + /// Exception Thrown Event Name + /// + private const string ExceptionThrownEventName = "ExceptionThrown"; + + private static readonly Guid DefaultRelatedActivityId = Guid.Empty; + + /// + /// Can only have one EventSource alive per process, so just create one statically. + /// + private static readonly EventSource TelemetryEventSourceInstance = new TelemetryEventSource(ProviderName); + + /// + /// Logs telemetry locally, but shouldn't upload it. Similar to an ETW event. + /// Should be the same as EventSourceOptions(), as Verbose is the default level. + /// + private static readonly EventSourceOptions LocalOption = new() { Level = EventLevel.Verbose }; + + /// + /// Logs error telemetry locally, but shouldn't upload it. Similar to an ETW event. + /// + private static readonly EventSourceOptions LocalErrorOption = new() { Level = EventLevel.Error }; + + /// + /// Logs telemetry. + /// Currently this is at 0% sampling for both internal and external retail devices. + /// + private static readonly EventSourceOptions InfoOption = new() { Keywords = TelemetryEventSource.TelemetryKeyword }; + + /// + /// Logs error telemetry. + /// Currently this is at 0% sampling for both internal and external retail devices. + /// + private static readonly EventSourceOptions InfoErrorOption = new() { Level = EventLevel.Error, Keywords = TelemetryEventSource.TelemetryKeyword }; + + /// + /// Logs measure telemetry. + /// This should be sent back on internal devices, and a small, sampled % of external retail devices. + /// + private static readonly EventSourceOptions MeasureOption = new() { Keywords = TelemetryEventSource.MeasuresKeyword }; + + /// + /// Logs measure error telemetry. + /// This should be sent back on internal devices, and a small, sampled % of external retail devices. + /// + private static readonly EventSourceOptions MeasureErrorOption = new() { Level = EventLevel.Error, Keywords = TelemetryEventSource.MeasuresKeyword }; + + /// + /// Logs critical telemetry. + /// This should be sent back on all devices sampled at 100%. + /// + private static readonly EventSourceOptions CriticalDataOption = new() { Keywords = TelemetryEventSource.CriticalDataKeyword }; + + /// + /// Logs critical error telemetry. + /// This should be sent back on all devices sampled at 100%. + /// + private static readonly EventSourceOptions CriticalDataErrorOption = new() { Level = EventLevel.Error, Keywords = TelemetryEventSource.CriticalDataKeyword }; + + /// + /// ActivityId so we can correlate all events in the same run + /// + private static Guid activityId = Guid.NewGuid(); + + /// + /// List of strings we should try removing for sensitivity reasons. + /// + private readonly List> sensitiveStrings = []; + + /// + /// Initializes a new instance of the class. + /// Prevents a default instance of the Telemetry class from being created. + /// + internal Telemetry() + { + } + + /// + /// Gets a value indicating whether telemetry is on + /// For future use if we add a registry key or some other setting to check if telemetry is turned on. + /// + public bool IsTelemetryOn { get; } = true; + + /// + /// Gets or sets a value indicating whether diagnostic telemetry is on. + /// + public bool IsDiagnosticTelemetryOn { get; set; } + + /// + /// Add a string that we should try stripping out of some of our telemetry for sensitivity reasons (ex. VM name, etc.). + /// We can never be 100% sure we can remove every string, but this should greatly reduce us collecting PII. + /// Note that the order in which AddSensitive is called matters, as later when we call ReplaceSensitiveStrings, it will try + /// finding and replacing the earlier strings first. This can be helpful, since we can target specific + /// strings (like username) first, which should help preserve more information helpful for diagnosis. + /// + /// Sensitive string to add (ex. "c:\xyz") + /// string to replace it with (ex. "-path-") + public void AddSensitiveString(string name, string replaceWith) + { + // Make sure the name isn't blank, hasn't already been added, and is greater than three characters. + // Otherwise they could name their VM "a", and then we would end up replacing every "a" with another string. + if (!string.IsNullOrWhiteSpace(name) && name.Length > 3 && !this.sensitiveStrings.Exists(item => name.Equals(item.Key, StringComparison.Ordinal))) + { + this.sensitiveStrings.Add(new KeyValuePair(name, replaceWith ?? string.Empty)); + } + } + + /// + /// Logs an exception at Measure level. To log at Critical level, the event name needs approval. + /// + /// What we trying to do when the exception occurred. + /// Exception object + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and corelate them + public void LogException(string action, Exception e, Guid? relatedActivityId = null) + { + var innerMessage = this.ReplaceSensitiveStrings(e.InnerException?.Message); + StringBuilder innerStackTrace = new(); + Exception? innerException = e.InnerException; + while (innerException != null) + { + innerStackTrace.Append(innerException.StackTrace); + + // Separating by 2 new lines to distinguish between different exceptions. + innerStackTrace.AppendLine(); + innerStackTrace.AppendLine(); + innerException = innerException.InnerException; + } + + this.LogInternal( + ExceptionThrownEventName, + LogLevel.Critical, + new + { + action, + name = e.GetType().Name, + stackTrace = e.StackTrace, + innerName = e.InnerException?.GetType().Name, + innerMessage, + innerStackTrace = innerStackTrace.ToString(), + message = this.ReplaceSensitiveStrings(e.Message), + PartA_PrivTags = PartA_PrivTags.ProductAndServicePerformance, + }, + relatedActivityId, + isError: true); + } + + /// + /// Log the time an action took (ex. deploy time). + /// + /// The measurement we're performing (ex. "DeployTime"). + /// How long the action took in milliseconds. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and corelate them + public void LogTimeTaken(string eventName, uint timeTakenMilliseconds, Guid? relatedActivityId = null) + { + this.LogInternal( + TimeTakenEventName, + LogLevel.Critical, + new + { + eventName, + timeTakenMilliseconds, + PartA_PrivTags = PartA_PrivTags.ProductAndServicePerformance, + }, + relatedActivityId, + isError: false); + } + + /// + /// Log an informal event with no additional data at log level measure. + /// + /// The name of the event to log + /// Set to true if an error condition raised this event. + /// GUID to correlate activities. + public void LogCritical(string eventName, bool isError = false, Guid? relatedActivityId = null) + { + this.LogInternal(eventName, LogLevel.Critical, new EmptyEvent(PartA_PrivTags.ProductAndServiceUsage), relatedActivityId, isError); + } + + /// + /// Log an informational event. Typically used for just a single event that's only called one place in the code. + /// + /// Name of the error event + /// Determines whether to upload the data to our servers, and on how many machines. + /// Values to send to the telemetry system. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + /// Anonymous type. + public void Log<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(string eventName, LogLevel level, T data, Guid? relatedActivityId = null) + where T : EventBase + { + data.ReplaceSensitiveStrings(this.ReplaceSensitiveStrings); + this.LogInternal(eventName, level, data, relatedActivityId, isError: false); + } + + /// + /// Log an error event. Typically used for just a single event that's only called one place in the code. + /// + /// Name of the error event + /// Determines whether to upload the data to our servers, and on how many machines. + /// Values to send to the telemetry system. + /// Optional relatedActivityId which will allow to correlate this telemetry with other telemetry in the same action/activity or thread and correlate them + /// Anonymous type. + public void LogError<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(string eventName, LogLevel level, T data, Guid? relatedActivityId = null) + where T : EventBase + { + data.ReplaceSensitiveStrings(this.ReplaceSensitiveStrings); + this.LogInternal(eventName, level, data, relatedActivityId, isError: true); + } + + private void LogInternal<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(string eventName, LogLevel level, T data, Guid? relatedActivityId, bool isError) + { + this.WriteTelemetryEvent(eventName, level, relatedActivityId ?? DefaultRelatedActivityId, isError, data); + } + + /// + /// Replaces sensitive strings in a string with non sensitive strings. + /// + /// Before, unstripped string. + /// After, stripped string + private string? ReplaceSensitiveStrings(string? message) + { + if (message != null) + { + foreach (KeyValuePair pair in this.sensitiveStrings) + { + // There's no String.Replace() with case insensitivity. + // We could use Regular Expressions here for searching for case-insensitive string matches, + // but it's not easy to specify the RegEx timeout value for .net 4.0. And we were worried + // about rare cases where the user could accidentally lock us up with RegEx, since we're using strings + // provided by the user, so just use a simple non-RegEx replacement algorithm instead. + var sb = new StringBuilder(); + var i = 0; + while (true) + { + // Find the string to strip out. + var foundPosition = message.IndexOf(pair.Key, i, StringComparison.OrdinalIgnoreCase); + if (foundPosition < 0) + { + sb.Append(message, i, message.Length - i); + message = sb.ToString(); + break; + } + + // Replace the string. + sb.Append(message, i, foundPosition - i); + sb.Append(pair.Value); + i = foundPosition + pair.Key.Length; + } + } + } + + return message; + } + + /// + /// Writes the telemetry event info using the TraceLogging API. + /// + /// Anonymous type. + /// Name of the event. + /// Determines whether to upload the data to our servers, and the sample set of host machines. + /// GUID to correlate activities. + /// Set to true if an error condition raised this event. + /// Values to send to the telemetry system. + private void WriteTelemetryEvent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(string eventName, LogLevel level, Guid relatedActivityId, bool isError, T data) + { + EventSourceOptions telemetryOptions; + if (this.IsTelemetryOn) + { + if (!IsDiagnosticTelemetryOn) + { + if (!isError && (level == LogLevel.Measure || level == LogLevel.Info)) + { + level = LogLevel.Local; + } + } + + switch (level) + { + case LogLevel.Critical: + telemetryOptions = isError ? Telemetry.CriticalDataErrorOption : Telemetry.CriticalDataOption; + break; + case LogLevel.Measure: + telemetryOptions = isError ? Telemetry.MeasureErrorOption : Telemetry.MeasureOption; + break; + case LogLevel.Info: + telemetryOptions = isError ? Telemetry.InfoErrorOption : Telemetry.InfoOption; + break; + case LogLevel.Local: + default: + telemetryOptions = isError ? Telemetry.LocalErrorOption : Telemetry.LocalOption; + break; + } + } + else + { + // The telemetry is not turned on, downgrade to local telemetry + telemetryOptions = isError ? Telemetry.LocalErrorOption : Telemetry.LocalOption; + } +#pragma warning disable IL2026 + TelemetryEventSourceInstance.Write(eventName, ref telemetryOptions, ref activityId, ref relatedActivityId, ref data); +#pragma warning restore IL2026 + } + + internal void AddWellKnownSensitiveStrings() + { + try + { + // This should convert "c:\users\johndoe" to "". + var userDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + this.AddSensitiveString(userDirectory.ToString(), ""); + + // Include both these names, since they should cover the logged on user, and the user who is running the tools built on top of these API's + // These names should almost always be the same, but technically could be different. + this.AddSensitiveString(Environment.UserName, ""); + var currentUserName = System.Security.Principal.WindowsIdentity.GetCurrent().Name.Split('\\').Last(); + this.AddSensitiveString(currentUserName, ""); + } + catch (Exception e) + { + // Catch and log exception + this.LogException("AddSensitiveStrings", e); + } + } +} \ No newline at end of file diff --git a/WinUIGallery/Telemetry/TelemetryEventSource.cs b/WinUIGallery/Telemetry/TelemetryEventSource.cs new file mode 100644 index 000000000..4f2ebb076 --- /dev/null +++ b/WinUIGallery/Telemetry/TelemetryEventSource.cs @@ -0,0 +1,353 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma warning disable CA1707 // Identifiers should not contain underscores +#pragma warning disable SA1604 // Element documentation should have summary +#pragma warning disable SA1642 // Constructor summary documentation should begin with standard text +#pragma warning disable SA1625 // Element documentation should not be copied and pasted + +#if TELEMETRYEVENTSOURCE_USE_NUGET +using Microsoft.Diagnostics.Tracing; +#else +using System.Diagnostics.Tracing; +#endif +using System; +using SuppressMessageAttribute = System.Diagnostics.CodeAnalysis.SuppressMessageAttribute; + +#pragma warning disable 3021 // 'type' does not need a CLSCompliant attribute + +namespace Microsoft.Diagnostics.Telemetry +{ + /// + /// + /// An EventSource with extra methods and constants commonly used in Microsoft's + /// TraceLogging-based ETW. This class inherits from EventSource, and is exactly + /// the same as EventSource except that it always enables + /// EtwSelfDescribingEventFormat and never uses traits. It also provides several + /// constants and helpers commonly used by Microsoft code. + /// + /// + /// Different versions of this class use different provider traits. The provider + /// traits in this class are empty. As a result, providers using this class will + /// not join any ETW Provider Groups and will not be given any special treatment + /// by group-sensitive ETW listeners. + /// + /// + /// When including this class in your project, you may define the following + /// conditional-compilation symbols to adjust the default behaviors: + /// + /// + /// TELEMETRYEVENTSOURCE_USE_NUGET - use Microsoft.Diagnostics.Tracing instead + /// of System.Diagnostics.Tracing. + /// + /// + /// TELEMETRYEVENTSOURCE_PUBLIC - define TelemetryEventSource as public instead + /// of internal. + /// + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + public +#else + internal +#endif + class TelemetryEventSource + : EventSource + { + /// + /// Keyword 0x0000100000000000 is reserved for future definition. Do + /// not use keyword 0x0000100000000000 in Microsoft-style ETW. + /// + public const EventKeywords Reserved44Keyword = (EventKeywords)0x0000100000000000; + + /// + /// Add TelemetryKeyword to eventSourceOptions.Keywords to indicate that + /// an event is for general-purpose telemetry. + /// This keyword should not be combined with MeasuresKeyword or + /// CriticalDataKeyword. + /// + public const EventKeywords TelemetryKeyword = (EventKeywords)0x0000200000000000; + + /// + /// Add MeasuresKeyword to eventSourceOptions.Keywords to indicate that + /// an event is for understanding measures and reporting scenarios. + /// This keyword should not be combined with TelemetryKeyword or + /// CriticalDataKeyword. + /// + public const EventKeywords MeasuresKeyword = (EventKeywords)0x0000400000000000; + + /// + /// Add CriticalDataKeyword to eventSourceOptions.Keywords to indicate that + /// an event powers user experiences or is critical to business intelligence. + /// This keyword should not be combined with TelemetryKeyword or + /// MeasuresKeyword. + /// + public const EventKeywords CriticalDataKeyword = (EventKeywords)0x0000800000000000; + + /// + /// Add CostDeferredLatency to eventSourceOptions.Tags to indicate that an event + /// should try to upload over free networks for a period of time before resorting + /// to upload over costed networks. + /// + public const EventTags CostDeferredLatency = (EventTags)0x040000; + + /// + /// Add CoreData to eventSourceOptions.Tags to indicate that an event + /// contains high priority "core data". + /// + public const EventTags CoreData = (EventTags)0x00080000; + + /// + /// Add InjectXToken to eventSourceOptions.Tags to indicate that an XBOX + /// identity token should be injected into the event before the event is + /// uploaded. + /// + public const EventTags InjectXToken = (EventTags)0x00100000; + + /// + /// Add RealtimeLatency to eventSourceOptions.Tags to indicate that an event + /// should be transmitted in real time (via any available connection). + /// + public const EventTags RealtimeLatency = (EventTags)0x0200000; + + /// + /// Add NormalLatency to eventSourceOptions.Tags to indicate that an event + /// should be transmitted via the preferred connection based on device policy. + /// + public const EventTags NormalLatency = (EventTags)0x0400000; + + /// + /// Add CriticalPersistence to eventSourceOptions.Tags to indicate that an + /// event should be deleted last when low on spool space. + /// + public const EventTags CriticalPersistence = (EventTags)0x0800000; + + /// + /// Add NormalPersistence to eventSourceOptions.Tags to indicate that an event + /// should be deleted first when low on spool space. + /// + public const EventTags NormalPersistence = (EventTags)0x1000000; + + /// + /// Add DropPii to eventSourceOptions.Tags to indicate that an event contains + /// PII and should be anonymized by the telemetry client. If this tag is + /// present, PartA fields that might allow identification or cross-event + /// correlation will be removed from the event. + /// + public const EventTags DropPii = (EventTags)0x02000000; + + /// + /// Add HashPii to eventSourceOptions.Tags to indicate that an event contains + /// PII and should be anonymized by the telemetry client. If this tag is + /// present, PartA fields that might allow identification or cross-event + /// correlation will be hashed (obfuscated). + /// + public const EventTags HashPii = (EventTags)0x04000000; + + /// + /// Add MarkPii to eventSourceOptions.Tags to indicate that an event contains + /// PII but may be uploaded as-is. If this tag is present, the event will be + /// marked so that it will only appear on the private stream. + /// + public const EventTags MarkPii = (EventTags)0x08000000; + + /// + /// Add DropPiiField to eventFieldAttribute.Tags to indicate that a field + /// contains PII and should be dropped by the telemetry client. + /// + public const EventFieldTags DropPiiField = (EventFieldTags)0x04000000; + + /// + /// Add HashPiiField to eventFieldAttribute.Tags to indicate that a field + /// contains PII and should be hashed (obfuscated) prior to uploading. + /// + public const EventFieldTags HashPiiField = (EventFieldTags)0x08000000; + + /// + /// Constructs a new instance of the TelemetryEventSource class with the + /// specified name. Sets the EtwSelfDescribingEventFormat option. + /// + /// The name of the event source. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public TelemetryEventSource( + string eventSourceName) + : base( + eventSourceName, + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// For use by derived classes that set the eventSourceName via EventSourceAttribute. + /// Sets the EtwSelfDescribingEventFormat option. + /// + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + protected TelemetryEventSource() + : base( + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// Constructs a new instance of the TelemetryEventSource class with the + /// specified name. Sets the EtwSelfDescribingEventFormat option. + /// + /// The name of the event source. + /// The parameter is not used. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "API compatibility")] + public TelemetryEventSource( + string eventSourceName, + TelemetryGroup telemetryGroup) + : base( + eventSourceName, + EventSourceSettings.EtwSelfDescribingEventFormat) + { + return; + } + + /// + /// Returns an instance of EventSourceOptions with the TelemetryKeyword set. + /// + /// Returns an instance of EventSourceOptions with the TelemetryKeyword set. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public static EventSourceOptions TelemetryOptions() + { + return new EventSourceOptions { Keywords = TelemetryKeyword }; + } + + /// + /// Returns an instance of EventSourceOptions with the MeasuresKeyword set. + /// + /// Returns an instance of EventSourceOptions with the MeasuresKeyword set. + [SuppressMessage("Microsoft.Performance", "CA1811", Justification = "Shared class with tiny helper methods - not all constructors/methods are used by all consumers")] + public static EventSourceOptions MeasuresOptions() + { + return new EventSourceOptions { Keywords = MeasuresKeyword }; + } + } + + /// + /// + /// The PrivTags class defines privacy tags that can be used to specify the privacy + /// category of an event. Add a privacy tag as a field with name "PartA_PrivTags". + /// As a shortcut, you can use _1 as the field name, which will automatically be + /// expanded to "PartA_PrivTags" at runtime. + /// + /// + /// Multiple tags can be OR'ed together if necessary (rarely needed). + /// + /// + /// + /// Typical usage: + /// + /// es.Write("UsageEvent", new + /// { + /// _1 = PrivTags.ProductAndServiceUsage, + /// field1 = fieldValue1, + /// field2 = fieldValue2 + /// }); + /// + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + [CLSCompliant(false)] + public +#else + internal +#endif + static class PrivTags + { + /// + public const Internal.PartA_PrivTags BrowsingHistory = Internal.PartA_PrivTags.BrowsingHistory; + + /// + public const Internal.PartA_PrivTags DeviceConnectivityAndConfiguration = Internal.PartA_PrivTags.DeviceConnectivityAndConfiguration; + + /// + public const Internal.PartA_PrivTags InkingTypingAndSpeechUtterance = Internal.PartA_PrivTags.InkingTypingAndSpeechUtterance; + + /// + public const Internal.PartA_PrivTags ProductAndServicePerformance = Internal.PartA_PrivTags.ProductAndServicePerformance; + + /// + public const Internal.PartA_PrivTags ProductAndServiceUsage = Internal.PartA_PrivTags.ProductAndServiceUsage; + + /// + public const Internal.PartA_PrivTags SoftwareSetupAndInventory = Internal.PartA_PrivTags.SoftwareSetupAndInventory; + } + + /// + /// Pass a TelemetryGroup value to the constructor of TelemetryEventSource + /// to control which telemetry group should be joined. + /// Note: has no effect in this version of TelemetryEventSource. + /// +#if TELEMETRYEVENTSOURCE_PUBLIC + public +#else + internal +#endif + enum TelemetryGroup + { + /// + /// The default group. Join this group to log normal, non-critical, non-coredata + /// events. + /// + MicrosoftTelemetry, + + /// + /// Join this group to log CriticalData, CoreData, or other specially approved + /// events. + /// + WindowsCoreTelemetry + } + +#pragma warning disable SA1403 // File may only contain a single namespace + namespace Internal +#pragma warning restore SA1403 // File may only contain a single namespace + { + /// + /// The complete list of privacy tags supported for events. + /// Multiple tags can be OR'ed together if an event belongs in multiple + /// categories. + /// Note that the PartA_PrivTags enum should not be used directly. + /// Instead, use values from the PrivTags class. + /// + [Flags] +#if TELEMETRYEVENTSOURCE_PUBLIC + [CLSCompliant(false)] + public +#else + internal +#endif + enum PartA_PrivTags + : ulong + { + /// + None = 0, + + /// + BrowsingHistory = 0x0000000000000002u, + + /// + DeviceConnectivityAndConfiguration = 0x0000000000000800u, + + /// + InkingTypingAndSpeechUtterance = 0x0000000000020000u, + + /// + ProductAndServicePerformance = 0x0000000001000000u, + + /// + ProductAndServiceUsage = 0x0000000002000000u, + + /// + SoftwareSetupAndInventory = 0x0000000080000000u, + } + } +} + +#pragma warning restore SA1625 // Element documentation should not be copied and pasted +#pragma warning restore SA1642 // Constructor summary documentation should begin with standard text +#pragma warning restore SA1604 // Element documentation should have summary +#pragma warning restore CA1707 // Identifiers should not contain underscores \ No newline at end of file diff --git a/WinUIGallery/Telemetry/TelemetryFactory.cs b/WinUIGallery/Telemetry/TelemetryFactory.cs new file mode 100644 index 000000000..20f6d0e25 --- /dev/null +++ b/WinUIGallery/Telemetry/TelemetryFactory.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading; + +namespace AIDevGallery.Telemetry; + +/// +/// Creates instance of Telemetry +/// This would be useful for the future when interfaces have been updated for logger like ITelemetry2, ITelemetry3 and so on +/// +internal class TelemetryFactory +{ + private static readonly Lock LockObj = new(); + + private static Telemetry? telemetryInstance; + + private static Telemetry GetTelemetryInstance() + { + if (telemetryInstance == null) + { + lock (LockObj) + { + telemetryInstance ??= new Telemetry(); + telemetryInstance.AddWellKnownSensitiveStrings(); + } + } + + return telemetryInstance; + } + + /// + /// Gets a singleton instance of Telemetry + /// This would be useful for the future when interfaces have been updated for logger like ITelemetry2, ITelemetry3 and so on + /// + /// The type of telemetry interface. + /// A singleton instance of the specified telemetry interface. + public static T Get() + where T : ITelemetry + { + return (T)(object)GetTelemetryInstance(); + } +} \ No newline at end of file diff --git a/WinUIGallery/WinUIGallery.csproj b/WinUIGallery/WinUIGallery.csproj index b785c4f48..6d643cc3b 100644 --- a/WinUIGallery/WinUIGallery.csproj +++ b/WinUIGallery/WinUIGallery.csproj @@ -112,6 +112,7 @@ + diff --git a/standalone.props b/standalone.props index 8faf4db7e..88399064c 100644 --- a/standalone.props +++ b/standalone.props @@ -8,6 +8,7 @@ 2.0.15 8.2.250402 2.2.0 + 10.0.25148.1001-220626-1600.rs-fun-deploy-dev5 obj\$(MSBuildProjectName)\ bin\$(MSBuildProjectName)\