#if UNITY_EDITOR using System; using UnityEditor; using UnityEngine.InputSystem.LowLevel; using UnityEngine.Serialization; namespace UnityEngine.InputSystem.Editor { /// /// Analytics record for tracking engagement with Input Action Asset editor(s). /// #if UNITY_2023_2_OR_NEWER [UnityEngine.Analytics.AnalyticInfo(eventName: kEventName, maxEventsPerHour: kMaxEventsPerHour, maxNumberOfElements: kMaxNumberOfElements, vendorKey: UnityEngine.InputSystem.InputAnalytics.kVendorKey)] #endif // UNITY_2023_2_OR_NEWER internal class InputActionsEditorSessionAnalytic : UnityEngine.InputSystem.InputAnalytics.IInputAnalytic { public const string kEventName = "input_actionasset_editor_closed"; public const int kMaxEventsPerHour = 100; // default: 1000 public const int kMaxNumberOfElements = 100; // default: 1000 /// /// Construct a new InputActionsEditorSession record of the given type. /// /// The editor type for which this record is valid. public InputActionsEditorSessionAnalytic(Data.Kind kind) { if (kind == Data.Kind.Invalid) throw new ArgumentException(nameof(kind)); Initialize(kind); } /// /// Register that an action map edit has occurred. /// public void RegisterActionMapEdit() { if (ImplicitFocus()) ++m_Data.action_map_modification_count; } /// /// Register that an action edit has occurred. /// public void RegisterActionEdit() { if (ImplicitFocus() && ComputeDuration() > 0.5) // Avoid logging actions triggered via UI initialization ++m_Data.action_modification_count; } /// /// Register than a binding edit has occurred. /// public void RegisterBindingEdit() { if (ImplicitFocus()) ++m_Data.binding_modification_count; } /// /// Register that a control scheme edit has occurred. /// public void RegisterControlSchemeEdit() { if (ImplicitFocus()) ++m_Data.control_scheme_modification_count; } /// /// Register that the editor has received focus which is expected to reflect that the user /// is currently exploring or editing it. /// public void RegisterEditorFocusIn() { if (!hasSession || hasFocus) return; m_FocusStart = currentTime; } /// /// Register that the editor has lost focus which is expected to reflect that the user currently /// has the attention elsewhere. /// /// /// Calling this method without having an ongoing session and having focus will not have any effect. /// public void RegisterEditorFocusOut() { if (!hasSession || !hasFocus) return; var duration = currentTime - m_FocusStart; m_FocusStart = float.NaN; m_Data.session_focus_duration_seconds += (float)duration; ++m_Data.session_focus_switch_count; } /// /// Register a user-event related to explicitly saving in the editor, e.g. /// using a button, menu or short-cut to trigger the save command. /// public void RegisterExplicitSave() { if (!hasSession) return; // No pending session ++m_Data.explicit_save_count; } /// /// Register a user-event related to implicitly saving in the editor, e.g. /// by having auto-save enabled and indirectly saving the associated asset. /// public void RegisterAutoSave() { if (!hasSession) return; // No pending session ++m_Data.auto_save_count; } /// /// Register a user-event related to resetting the editor action configuration to defaults. /// public void RegisterReset() { if (!hasSession) return; // No pending session ++m_Data.reset_count; } /// /// Begins a new session if the session has not already been started. /// /// /// If the session has already been started due to a previous call to without /// a call to this method has no effect. /// public void Begin() { if (hasSession) return; // Session already started. m_SessionStart = currentTime; } /// /// Ends the current session. /// /// /// If the session has not previously been started via a call to calling this /// method has no effect. /// public void End() { if (!hasSession) return; // No pending session // Make sure we register focus out if failed to capture or not invoked if (hasFocus) RegisterEditorFocusOut(); // Compute and record total session duration var duration = ComputeDuration(); m_Data.session_duration_seconds += duration; // Sanity check data, if less than a second its likely a glitch so avoid sending incorrect data // Send analytics event if (duration >= 1.0) runtime.SendAnalytic(this); // Reset to allow instance to be reused Initialize(m_Data.kind); } #region IInputAnalytic Interface #if UNITY_EDITOR && UNITY_2023_2_OR_NEWER public bool TryGatherData(out UnityEngine.Analytics.IAnalytic.IData data, out Exception error) #else public bool TryGatherData(out InputAnalytics.IInputAnalyticData data, out Exception error) #endif { if (!isValid) { data = null; error = new Exception("Unable to gather data without a valid session"); return false; } data = this.m_Data; error = null; return true; } public InputAnalytics.InputAnalyticInfo info => new InputAnalytics.InputAnalyticInfo(kEventName, kMaxEventsPerHour, kMaxNumberOfElements); #endregion private double ComputeDuration() => hasSession ? currentTime - m_SessionStart : 0.0; private void Initialize(Data.Kind kind) { m_FocusStart = float.NaN; m_SessionStart = float.NaN; m_Data = new Data(kind); } private bool ImplicitFocus() { if (!hasSession) return false; if (!hasFocus) RegisterEditorFocusIn(); return true; } private Data m_Data; private double m_FocusStart; private double m_SessionStart; private static IInputRuntime runtime => InputSystem.s_Manager.m_Runtime; private bool hasFocus => !double.IsNaN(m_FocusStart); private bool hasSession => !double.IsNaN(m_SessionStart); // Returns current time since startup. Note that IInputRuntime explicitly defines in interface that // IInputRuntime.currentTime corresponds to EditorApplication.timeSinceStartup in editor. private double currentTime => runtime.currentTime; private bool isValid => m_Data.session_duration_seconds >= 0; [Serializable] public struct Data : UnityEngine.InputSystem.InputAnalytics.IInputAnalyticData { /// /// Represents an editor type. /// /// /// This may be added to in the future but items may never be removed. /// [Serializable] public enum Kind { Invalid = 0, EditorWindow = 1, EmbeddedInProjectSettings = 2 } /// /// Constructs a InputActionsEditorSessionData. /// /// Specifies the kind of editor metrics is being collected for. public Data(Kind kind) { this.kind = kind; session_duration_seconds = 0; session_focus_duration_seconds = 0; session_focus_switch_count = 0; action_map_modification_count = 0; action_modification_count = 0; binding_modification_count = 0; explicit_save_count = 0; auto_save_count = 0; reset_count = 0; control_scheme_modification_count = 0; } /// /// Specifies what kind of Input Actions editor this event represents. /// public Kind kind; /// /// The total duration for the session, i.e. the duration during which the editor window was open. /// public double session_duration_seconds; /// /// The total duration for which the editor window was open and had focus. /// public double session_focus_duration_seconds; /// /// Specifies the number of times the window has transitioned from not having focus to having focus in a single session. /// public int session_focus_switch_count; /// /// The total number of action map modifications during the session. /// public int action_map_modification_count; /// /// The total number of action modifications during the session. /// public int action_modification_count; /// The total number of binding modifications during the session. /// public int binding_modification_count; /// /// The total number of controls scheme modifications during the session. /// public int control_scheme_modification_count; /// /// The total number of explicit saves during the session, i.e. as in user-initiated save. /// public int explicit_save_count; /// /// The total number of automatic saves during the session, i.e. as in auto-save on close or focus-lost. /// public int auto_save_count; /// /// The total number of user-initiated resets during the session, i.e. as in using Reset option in menu. /// public int reset_count; public bool isValid => kind != Kind.Invalid && session_duration_seconds >= 0; } } } #endif