/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEditor; using UnityEngine; namespace Meta.Voice.TelemetryUtilities { /// /// This class provides facilities to share telemetry from Unity Editor with Meta. /// [ExecuteInEditMode] internal class Telemetry : MonoBehaviour { /// /// The editor prefs key holding the logging level. /// private const string TELEMETRY_LOGGING_LEVEL = "VoiceSdk.Telemetry.LogLevel"; /// /// The editor prefs key holding a unique identifier that is set once when consent is obtained and never changes. /// This is an approximation of a device ID but is tied to the Unity editor prefs instead, so can change. /// private const string TELEMETRY_ENV_ID = "VoiceSdk.Telemetry.EnvironmentId"; /// /// The active telemetry channel. Used to switch between live and local. /// private static TelemetryChannel _instance = new LocalTelemetry(); /// /// Holds the current status of use consent to share telemetry. /// private static bool _consentProvided = false; /// /// Sequential counter to use as unique instance keys. /// private static int _nextEventSequenceId = 1; /// /// The ID of the entire session event. /// private static int _sessionEventInstanceId; /// /// An ID that is generated once and stored in the preferences. It's similar to a device ID, except it will change /// any time it's regenerated or Unity editor preferences are removed. /// private static Guid _envId; /// /// A random ID given to the session to use for correlation. /// private static Guid _sessionID = Guid.NewGuid(); static Telemetry() { #if UNITY_EDITOR_WIN ConsentProvided = TelemetryConsentManager.ConsentProvided; #endif } internal static TelemetryLogLevel LogLevel { get { if (_logLevel != TelemetryLogLevel.Unassigned) { return _logLevel; } if (!EditorPrefs.HasKey(TELEMETRY_LOGGING_LEVEL)) { _logLevel = TelemetryLogLevel.Off; } else { var telemetryLevelString = EditorPrefs.GetString(Telemetry.TELEMETRY_LOGGING_LEVEL); Enum.TryParse(telemetryLevelString, true, out TelemetryLogLevel telemetryLogLevel); _logLevel = telemetryLogLevel; } return _logLevel; } set { EditorPrefs.SetString(Telemetry.TELEMETRY_LOGGING_LEVEL, value.ToString()); _logLevel = value; } } private static TelemetryLogLevel _logLevel = TelemetryLogLevel.Unassigned; /// /// Sets consent as obtained or withdrawn. This controls whether Meta will collect telemetry or not /// internal static bool ConsentProvided { set { if (_consentProvided == value) { return; } _consentProvided = value; if (value) { ExtractEnvironmentId(); _instance = new TelemetryChannel(); _sessionEventInstanceId = StartEvent(TelemetryEventId.Session); } else { _instance = new LocalTelemetry(); } } } /// /// Logs an event as started. /// /// The ID of the event that just started. /// Instance key identifying this specific occurrence of the event. internal static int StartEvent(TelemetryEventId eventId) { return _instance.StartEvent(eventId); } /// /// Logs an instantaneous event that happens at one point in time (as opposed to one with a start and end). /// /// The ID of the event. /// Optional annotations to add to the event. /// Instance key identifying this specific occurrence of the event. internal static void LogInstantEvent(TelemetryEventId eventId, Dictionary annotations = null) { _instance.LogInstantEvent(eventId, annotations); } /// /// Annotates an event that has started but did not end yet. /// /// The instance key of the event. Must match the one used starting the event. /// The annotation key. /// The annotation value. internal static void AnnotateEvent(int instanceKey, AnnotationKey key, string value) { _instance.AnnotateEvent(instanceKey, key, value); } /// /// Logs an event as ended. /// /// The instance key of the event. Must match the one used starting the event. /// The result of the event. /// Instance key. internal static void EndEvent(int instanceKey, ResultType result) { _instance.EndEvent(instanceKey, result); } /// /// Logs an event as ended. /// /// The instance key of the event. Must match the one used starting the event. /// The error. /// Instance key. internal static void EndEventWithFailure(int instanceKey, string error = null) { if (!string.IsNullOrEmpty(error)) { _instance.AnnotateEvent(instanceKey, AnnotationKey.Error, error); } _instance.EndEvent(instanceKey, ResultType.Failure); } private static void ExtractEnvironmentId() { var keyExists = EditorPrefs.HasKey(TELEMETRY_ENV_ID); if (!keyExists) { _envId = Guid.NewGuid(); EditorPrefs.SetString(TELEMETRY_ENV_ID, _envId.ToString()); } else { var idString = EditorPrefs.GetString(TELEMETRY_ENV_ID); if (!Guid.TryParse(idString, out _envId)) { LogWarning($"Failed to parse telemetry environment ID from: {idString}"); } } } private static void LogWarning(object content) { if (_logLevel == TelemetryLogLevel.Off) { return; } Debug.LogWarning(content); } private static void LogVerbose(object content) { if (_logLevel != TelemetryLogLevel.Verbose) { return; } Debug.Log(content); } private static void LogError(object content) { if (_logLevel == TelemetryLogLevel.Off) { return; } Debug.LogError(content); } private void OnDestroy() { EndEvent(_sessionEventInstanceId, ResultType.Success); _instance.ShutdownTelemetry(); } /// /// The telemetry channel represents a target for telemetry. The default implementation sends live telemetry /// to Meta. /// private class TelemetryChannel { private const string PluginName = "SDKTelemetry"; /// /// Maps the instance keys with their corresponding events. /// private Dictionary _instanceKeyMap = new Dictionary(); /// /// Logs an event as started. /// /// The ID of the event that just started. /// Instance key identifying this specific occurrence of the event. internal virtual int StartEvent(TelemetryEventId eventId) { _instanceKeyMap[_nextEventSequenceId] = eventId; QplMarkerStart((int)eventId, _nextEventSequenceId, -1); AnnotateEvent(_nextEventSequenceId, AnnotationKey.SessionId, _sessionID.ToString()); AnnotateEvent(_nextEventSequenceId, AnnotationKey.StartTimeStamp, ElapsedMilliseconds.ToString()); LogVerbose($"Started telemetry event {eventId}:{_nextEventSequenceId}"); return _nextEventSequenceId++; } /// /// Logs an instantaneous event that happens at one point in time (as opposed to one with a start and end). /// /// The ID of the event. /// Optional annotations to add to the event. /// Instance key identifying this specific occurrence of the event. internal virtual void LogInstantEvent(TelemetryEventId eventId, Dictionary annotations = null) { var instanceKey = _nextEventSequenceId; _instanceKeyMap[instanceKey] = eventId; var timeStamp = ElapsedMilliseconds.ToString(); QplMarkerStart((int)eventId, instanceKey, -1); LogVerbose($"Started instant telemetry event {eventId}:{instanceKey}"); AnnotateEvent(instanceKey, AnnotationKey.SessionId, _sessionID.ToString()); AnnotateEvent(instanceKey, AnnotationKey.StartTimeStamp, timeStamp); AnnotateEvent(instanceKey, AnnotationKey.EndTimeStamp, timeStamp); if (annotations != null) { foreach (var annotation in annotations) { AnnotateEvent(instanceKey, annotation.Key, annotation.Value); } } QplMarkerEnd((int)eventId, ResultType.Success, instanceKey, -1); LogVerbose($"Ended instant telemetry event {eventId}:{instanceKey}"); _instanceKeyMap.Remove(instanceKey); ++_nextEventSequenceId; } /// /// Annotates an event that has started but did not end yet. /// /// The ID of the event. Should already have started but not ended already. /// The instance key of the event. Must match the one used starting the event. /// The annotation key. /// The annotation value. internal virtual void AnnotateEvent(int instanceKey, AnnotationKey key, string value) { if (!this._instanceKeyMap.ContainsKey(instanceKey)) { LogWarning($"Attempted to end an event that's invalid or not started. Instance ID: {instanceKey}"); return; } var eventId = _instanceKeyMap[instanceKey]; QplMarkerAnnotation((int)eventId, key.ToString(), value, instanceKey); LogVerbose($"Annotated telemetry event {eventId}:{instanceKey} with {key}:{value}"); } /// /// Logs an event as ended. /// /// The ID of the event. Should already have started but not ended already. /// The instance key of the event. Must match the one used starting the event. /// The result of the event. /// Instance key. internal virtual void EndEvent(int instanceKey, ResultType result) { if (!this._instanceKeyMap.ContainsKey(instanceKey)) { LogWarning($"Attempted to end an event that's not started or invalid. Instance ID: {instanceKey}"); return; } var eventId = _instanceKeyMap[instanceKey]; AnnotateEvent(instanceKey, AnnotationKey.EndTimeStamp, ElapsedMilliseconds.ToString()); QplMarkerEnd((int)eventId, result, instanceKey, -1); _instanceKeyMap.Remove(instanceKey); LogVerbose($"Ended telemetry event {eventId}:{instanceKey}({result})"); } public void ShutdownTelemetry() { try { foreach (var instanceKey in _instanceKeyMap.Keys) { AnnotateEvent(instanceKey, AnnotationKey.Error, "Telemetry event not ended gracefully"); EndEvent(instanceKey, ResultType.Cancel); } } finally { OnEditorShutdown(); } } private static long ElapsedMilliseconds { get => DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond; } #region Telemetry native methods #if UNITY_EDITOR_WIN [DllImport(PluginName)] private static extern bool QplMarkerStart(int markerId, int instanceKey, long timestampMs); [DllImport(PluginName)] private static extern bool QplMarkerEnd(int markerId, ResultType boolTypeId, int instanceKey, long timestampMs); [DllImport(PluginName)] private static extern bool QplMarkerPointCached(int markerId, int nameHandle, int instanceKey, long timestampMs); [DllImport(PluginName)] private static extern bool QplMarkerAnnotation(int markerId, [MarshalAs(UnmanagedType.LPStr)] string annotationKey, [MarshalAs(UnmanagedType.LPStr)] string annotationValue, int instanceKey); [DllImport(PluginName)] private static extern bool QplCreateMarkerHandle([MarshalAs(UnmanagedType.LPStr)] string name, out int nameHandle); [DllImport(PluginName)] private static extern bool QplDestroyMarkerHandle(int nameHandle); [DllImport(PluginName, CallingConvention = CallingConvention.Cdecl)] private static extern bool OnEditorShutdown(); #else private static bool QplMarkerStart(int markerId, int instanceKey, long timestampMs) { return false; } private static bool QplMarkerEnd(int markerId, ResultType boolTypeId, int instanceKey, long timestampMs) { return false; } private static bool QplMarkerPointCached(int markerId, int nameHandle, int instanceKey, long timestampMs) { return false; } private static bool QplMarkerAnnotation(int markerId, [MarshalAs(UnmanagedType.LPStr)] string annotationKey, [MarshalAs(UnmanagedType.LPStr)] string annotationValue, int instanceKey) { return false; } private static bool QplCreateMarkerHandle([MarshalAs(UnmanagedType.LPStr)] string name, out int nameHandle) { nameHandle = -1; return false; } private static bool QplDestroyMarkerHandle(int nameHandle) { return false; } private static bool OnEditorShutdown() { return false; } #endif #endregion } /// /// This instance will be used when we don't have consent to collect telemetry. /// private class LocalTelemetry : TelemetryChannel { private bool _warnOnConsent = true; /// internal override int StartEvent(TelemetryEventId eventId) { if (_warnOnConsent) { LogWarning("Not sending telemetry. User consent required."); _warnOnConsent = false; } return 0; } /// internal override void LogInstantEvent(TelemetryEventId eventId, Dictionary annotations = null) { } /// internal override void AnnotateEvent(int instanceKey, AnnotationKey key, string value) { } /// internal override void EndEvent(int instanceKey, ResultType result) { } } /// /// The result of an event. /// public enum ResultType : short { Success = 2, Failure = 3, Cancel = 4 } /// /// The event IDs. These should map to GQL IDs. /// public enum TelemetryEventId { Unknown = 92612351, Session = 92611421, SupplyToken = 92608491, CheckAutoTrain = 92612591, AutoTrain = 92617773, ToggleCheckbox = 1063854409, SyncEntities = 92609990, ClickButton = 92615194, AssignIntentMatcherInInspector = 92616101, SelectOption = 92604542, GenerateManifest = 92615944, LoadManifest = 92613324, NavigateToCodeFromInspector = 92614941, OpenUi = 92610372, } /// /// The annotation keys used for the key-value annotations. /// public enum AnnotationKey { Unknown, UnrecognizedEvent, UnrecognizedAnnotationKey, EnvironmentId, SessionId, StartTimeStamp, EndTimeStamp, Error, PageId, CompatibleSignatures, IncompatibleSignatures, IsAvailable, ControlId, Value, Type, } } }