#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