using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Analytics;
namespace Unity.VisualScripting.Analytics
{
internal static class HotkeyUsageAnalytics
{
private const int MaxEventsPerHour = 120;
private const int MaxNumberOfElements = 1000;
private const string VendorKey = "unity.visualscripting";
private const string EventName = "VScriptHotkeyUsage";
#if !UNITY_2023_2_OR_NEWER
private static bool _isRegistered = false;
#endif
private const int HotkeyUseLimitBeforeSend = 10000;
private static bool _interruptEventsRegistered = false;
private static bool _onControlSchemeEventRegistered = false;
private static Data _collectedData = null;
// Limiting to stop spam from some continuous events like pan or zoom
private const int HotkeyFrameLimit = 2;
private static Dictionary<Hotkey, FrameLimiterUtility> _frameLimiters;
internal static void HotkeyUsed(Hotkey key)
{
EnsureCollectedDataInitialized();
EnsureInterruptEventsRegistered();
EnsureOnControlSchemeChangedRegistered();
_collectedData.canvasControlSchemeName = Enum.GetName(typeof(CanvasControlScheme), BoltCore.Configuration.controlScheme);
if (!_frameLimiters.ContainsKey(key))
_frameLimiters.Add(key, new FrameLimiterUtility(HotkeyFrameLimit));
if (!_frameLimiters[key].IsWithinFPSLimit()) return;
var enumStringVal = Enum.GetName(typeof(Hotkey), key);
if (enumStringVal == null)
return;
if (!_collectedData.HotkeyUsageCountDict.ContainsKey(enumStringVal))
_collectedData.HotkeyUsageCountDict.Add(enumStringVal, 0);
_collectedData.HotkeyUsageCountDict[enumStringVal]++;
if (_collectedData.HotkeyUsageCountDict[enumStringVal] >= HotkeyUseLimitBeforeSend)
Send();
}
#if UNITY_2023_2_OR_NEWER
private static void Send()
{
if (!EditorAnalytics.enabled)
return;
// Because dicts aren't serializable, convert it to a list right before we send
foreach (var count in _collectedData.HotkeyUsageCountDict)
{
var c = new HotkeyStringUsageCount();
c.hotkey = count.Key;
c.count = count.Value;
_collectedData.hotkeyUsageCount.Add(c);
}
EditorAnalytics.SendAnalytic(new HotkeyUsageAnalytic(_collectedData));
ResetCollectedData();
}
#else
private static void Send()
{
if (!EditorAnalytics.enabled)
return;
if (!RegisterEvent())
return;
// Because dicts aren't serializable, convert it to a list right before we send
foreach (var count in _collectedData.HotkeyUsageCountDict)
{
var c = new HotkeyStringUsageCount();
c.hotkey = count.Key;
c.count = count.Value;
_collectedData.hotkeyUsageCount.Add(c);
}
EditorAnalytics.SendEventWithLimit(EventName, _collectedData);
ResetCollectedData();
}
private static bool RegisterEvent()
{
if (!_isRegistered)
{
var result = EditorAnalytics.RegisterEventWithLimit(EventName, MaxEventsPerHour, MaxNumberOfElements, VendorKey);
if (result == UnityEngine.Analytics.AnalyticsResult.Ok)
{
_isRegistered = true;
}
}
return _isRegistered;
}
#endif
private static void EnsureInterruptEventsRegistered()
{
if (_interruptEventsRegistered) return;
EditorApplication.quitting += Send;
AssemblyReloadEvents.beforeAssemblyReload += Send;
_interruptEventsRegistered = true;
}
private static void EnsureOnControlSchemeChangedRegistered()
{
if (_onControlSchemeEventRegistered) return;
BoltCore.Configuration.ControlSchemeChanged += Send;
_onControlSchemeEventRegistered = true;
}
private static void EnsureCollectedDataInitialized()
{
if (_collectedData != null) return;
ResetCollectedData();
_frameLimiters = new Dictionary<Hotkey, FrameLimiterUtility>();
}
private static void ResetCollectedData()
{
var controlScheme = Enum.GetName(typeof(CanvasControlScheme), BoltCore.Configuration.controlScheme);
_collectedData = new Data()
{
canvasControlSchemeName = controlScheme,
hotkeyUsageCount = new List<HotkeyStringUsageCount>(),
HotkeyUsageCountDict = new Dictionary<string, int>(),
};
}
#if UNITY_2023_2_OR_NEWER
[AnalyticInfo(eventName: EventName, vendorKey: VendorKey, maxEventsPerHour:MaxEventsPerHour, maxNumberOfElements:MaxNumberOfElements)]
class HotkeyUsageAnalytic : IAnalytic
{
private Data data;
public HotkeyUsageAnalytic(Data data)
{
this.data = data;
}
public bool TryGatherData(out IAnalytic.IData data, out Exception error)
{
error = null;
data = this.data;
return data != null;
}
}
[Serializable]
private class Data : IAnalytic.IData
#else
[Serializable]
private class Data
#endif
{
// Note: using strings instead of enums to make it resilient to enum order changing
[SerializeField]
internal string canvasControlSchemeName;
[SerializeField]
internal List<HotkeyStringUsageCount> hotkeyUsageCount;
// Not actually sent to analytics because analytics uses UnitySerializer under the hood
// which cannot serialize dictionaries
internal Dictionary<string, int> HotkeyUsageCountDict;
}
[Serializable]
private struct HotkeyStringUsageCount
{
[SerializeField] internal string hotkey;
[SerializeField] internal int count;
}
internal enum Hotkey
{
Zoom,
Scroll,
PanMmb,
PanAltLmb,
Home,
RmbRemoveConnections,
Cut,
Copy,
Paste,
Duplicate,
Delete,
SelectAll,
Tab,
}
}
}