Browse Source

Implement Startup, Document Created and File Opened analytic events

CPKreuz 1 year ago
parent
commit
1f45633ad2

+ 4 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -11,6 +11,7 @@ using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.FlyUI;
 using PixiEditor.Extensions.IO;
 using PixiEditor.Extensions.Runtime;
+using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.ExtensionServices;
@@ -74,6 +75,9 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<LayoutManager>()
             .AddSingleton<LayoutViewModel>()
             .AddSingleton(x => new ExtensionsViewModel(x.GetService<ViewModels_ViewModelMain>(), extensionLoader))
+            // Analytics
+            .AddSingleton<AnalyticsClient>(_ => new AnalyticsClient(Environment.GetEnvironmentVariable("PixiEditorAnalytics")))
+            .AddSingleton<AnalyticsPeriodicReporter>()
             // Controllers
             .AddSingleton<ShortcutController>()
             .AddSingleton<CommandController>()

+ 1 - 1
src/PixiEditor/Helpers/SupportedFilesHelper.cs

@@ -55,7 +55,7 @@ internal class SupportedFilesHelper
     {
         return AllSupportedExtensions.Contains(fileExtension);
     }
-    public static IoFileType ParseImageFormat(string extension)
+    public static IoFileType? ParseImageFormat(string extension)
     {
         var allExts = FileTypes;
         var fileData = allExts.SingleOrDefault(i => i.Extensions.Contains(extension));

+ 14 - 14
src/PixiEditor/Models/AnalyticsAPI/AnalyticEventTypes.cs

@@ -2,20 +2,20 @@
 
 public class AnalyticEventTypes
 {
-    public string Startup { get; } = GetEventType("Startup");
-    public string CreateDocument { get; } = GetEventType("CreateDocument");
-    public string SwitchTab { get; } = GetEventType("SwitchTab");
-    public string OpenWindow { get; } = GetEventType("OpenWindow");
-    public string CreateNode { get; } = GetEventType("CreateNode");
-    public string CreateKeyframe { get; } = GetEventType("CreateKeyframe");
-    public string CloseDocument { get; } = GetEventType("CloseDocument");
-    public string ResizeDocument { get; } = GetEventType("ResizeDocument");
-    public string OpenExample { get; } = GetEventType("OpenExample");
-    public string OpenFile { get; } = GetEventType("OpenFile");
-    public string GeneralCommand { get; } = GetEventType("GeneralCommand");
-    public string SwitchTool { get; } = GetEventType("SwitchTool");
-    public string UseTool { get; } = GetEventType("UseTool");
-    public string SetColor { get; } = GetEventType("SetColor");
+    public static string Startup { get; } = GetEventType("Startup");
+    public static string CreateDocument { get; } = GetEventType("CreateDocument");
+    public static string SwitchTab { get; } = GetEventType("SwitchTab");
+    public static string OpenWindow { get; } = GetEventType("OpenWindow");
+    public static string CreateNode { get; } = GetEventType("CreateNode");
+    public static string CreateKeyframe { get; } = GetEventType("CreateKeyframe");
+    public static string CloseDocument { get; } = GetEventType("CloseDocument");
+    public static string ResizeDocument { get; } = GetEventType("ResizeDocument");
+    public static string OpenExample { get; } = GetEventType("OpenExample");
+    public static string OpenFile { get; } = GetEventType("OpenFile");
+    public static string GeneralCommand { get; } = GetEventType("GeneralCommand");
+    public static string SwitchTool { get; } = GetEventType("SwitchTool");
+    public static string UseTool { get; } = GetEventType("UseTool");
+    public static string SetColor { get; } = GetEventType("SetColor");
 
     private static string GetEventType(string value) => $"PixiEditor.{value}";
 }

+ 35 - 0
src/PixiEditor/Models/AnalyticsAPI/Analytics.cs

@@ -0,0 +1,35 @@
+using PixiEditor.Models.Files;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.Models.AnalyticsAPI;
+
+public static class Analytics
+{
+    public static AnalyticEvent SendStartup(StartupPerformance startup) =>
+        SendEvent(AnalyticEventTypes.Startup, startup.GetData());
+
+    public static AnalyticEvent SendCreateDocument(int width, int height) =>
+        SendEvent(AnalyticEventTypes.CreateDocument, ("Width", width), ("Height", height));
+
+    internal static AnalyticEvent SendOpenFile(IoFileType fileType, long fileSize, VecI size) =>
+        SendEvent(AnalyticEventTypes.OpenFile, ("FileType", fileType.PrimaryExtension), ("FileSize", fileSize), ("Width", size.X), ("Height", size.Y));
+    
+    private static AnalyticEvent SendEvent(string name, params (string, object)[] data) =>
+        SendEvent(name, data.ToDictionary());
+
+    private static AnalyticEvent SendEvent(string name, Dictionary<string, object> data)
+    {
+        var e = new AnalyticEvent
+        {
+            EventType = name,
+            Time = DateTimeOffset.Now,
+            Data = data
+        };
+        
+        var reporter = AnalyticsPeriodicReporter.Instance;
+
+        reporter.AddEvent(e);
+
+        return e;
+    }
+}

+ 2 - 2
src/PixiEditor/Models/AnalyticsAPI/AnalyticsClient.cs

@@ -15,11 +15,11 @@ public class AnalyticsClient
 
     public async Task<Guid?> CreateSessionAsync(CancellationToken cancellationToken = default)
     {
-        var response = await _client.GetAsync("init-session", cancellationToken);
+        var response = await _client.GetAsync($"init-session?version={VersionHelpers.GetCurrentAssemblyVersion()}", cancellationToken);
 
         if (response.IsSuccessStatusCode)
         {
-            return Guid.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
+            return await response.Content.ReadFromJsonAsync<Guid?>(cancellationToken);
         }
 
         if (response.StatusCode is not HttpStatusCode.ServiceUnavailable)

+ 9 - 2
src/PixiEditor/Models/AnalyticsAPI/AnalyticsPeriodicReporter.cs

@@ -61,7 +61,9 @@ public class AnalyticsPeriodicReporter
             return;
         }
 
-        await Task.Run(RunHeartbeatAsync);
+        SessionId = createSession.Value;
+
+        Task.Run(RunHeartbeatAsync);
 
         while (!_cancellationToken.IsCancellationRequested)
         {
@@ -69,7 +71,7 @@ public class AnalyticsPeriodicReporter
             {
                 await SendBacklogAsync();
 
-                await Task.Delay(10);
+                await Task.Delay(TimeSpan.FromSeconds(10));
             }
             catch (TaskCanceledException) { }
             catch (Exception e)
@@ -85,6 +87,11 @@ public class AnalyticsPeriodicReporter
 
         try
         {
+            if (_backlog.Count == 0)
+            {
+                return;
+            }
+            
             var result = await _client.SendEventsAsync(SessionId, _backlog, _cancellationToken.Token);
             _backlog.Clear();
 

+ 49 - 0
src/PixiEditor/Models/AnalyticsAPI/StartupPerformance.cs

@@ -0,0 +1,49 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace PixiEditor.Models.AnalyticsAPI;
+
+public class StartupPerformance
+{
+    private readonly DateTimeOffset _processStart;
+    
+    private TimeSpan _timeToMainWindow;
+    private TimeSpan _timeToMainViewModel;
+    private TimeSpan _timeToInteractivity;
+
+    // Do not rename - Used in GetKeyValue
+    public DateTimeOffset ProcessStart => _processStart;
+
+    // Do not rename - Used in GetKeyValue
+    public TimeSpan TimeToMainWindow => _timeToMainWindow;
+
+    // Do not rename - Used in GetKeyValue
+    public TimeSpan TimeToMainViewModel => _timeToMainViewModel;
+
+    // Do not rename - Used in GetKeyValue
+    public TimeSpan TimeToInteractivity => _timeToInteractivity;
+
+    public StartupPerformance()
+    {
+        _processStart = Process.GetCurrentProcess().StartTime;
+    }
+
+    public void ReportToMainWindow() => ReportFor(out _timeToMainWindow);
+    
+    public void ReportToMainViewModel() => ReportFor(out _timeToMainViewModel);
+
+    public void ReportToInteractivity() => ReportFor(out _timeToInteractivity);
+
+    public Dictionary<string, object> GetData() => new[]
+    {
+        GetKeyValue(ProcessStart),
+        GetKeyValue(TimeToMainWindow),
+        GetKeyValue(TimeToMainViewModel),
+        GetKeyValue(TimeToInteractivity)
+    }.ToDictionary();
+    
+    private void ReportFor(out TimeSpan t) => t = DateTimeOffset.Now - _processStart;
+
+    private static KeyValuePair<string, object> GetKeyValue(object value, [CallerArgumentExpression(nameof(value))] string name = "") =>
+        new(name, value);
+}

+ 21 - 0
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -14,13 +14,16 @@ using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Exceptions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Helpers;
+using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.Models.Controllers;
 using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.Files;
 using PixiEditor.Models.IO;
 using PixiEditor.Models.UserData;
 using PixiEditor.Numerics;
@@ -223,8 +226,12 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     private void OpenDotPixi(string path, bool associatePath = true)
     {
         DocumentViewModel document = Importer.ImportDocument(path, associatePath);
+
         AddDocumentViewModelToTheSystem(document);
         AddRecentlyOpened(document.FullFilePath);
+        
+        var fileSize = new FileInfo(path).Length;
+        Analytics.SendOpenFile(PixiFileType.PixiFile, fileSize, document.SizeBindable);
     }
 
     /// <summary>
@@ -261,6 +268,18 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         }
 
         AddRecentlyOpened(path);
+
+        var fileType = SupportedFilesHelper.ParseImageFormat(path);
+
+        if (fileType != null)
+        {
+            var fileSize = new FileInfo(path).Length;
+            Analytics.SendOpenFile(fileType, fileSize, doc.SizeBindable);
+        }
+        else
+        {
+            CrashHelper.SendExceptionInfoToWebhook(new InvalidFileTypeException(default, $"Invalid file type '{fileType}'"));
+        }
     }
 
 
@@ -305,6 +324,8 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                         new VecI(newFile.Width, newFile.Height), out int id)
                     .WithOutputNode(id, "Output")
                 ));
+
+            Analytics.SendCreateDocument(newFile.Width, newFile.Height);
         }
     }
 

+ 11 - 0
src/PixiEditor/Views/MainWindow.axaml.cs

@@ -13,6 +13,7 @@ using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.Runtime;
 using PixiEditor.Helpers;
 using PixiEditor.Initialization;
+using PixiEditor.Models.AnalyticsAPI;
 using PixiEditor.Models.ExceptionHandling;
 using PixiEditor.Platform;
 using PixiEditor.ViewModels.SubViewModels;
@@ -28,6 +29,8 @@ internal partial class MainWindow : Window
     private readonly IServiceProvider services;
     private static ExtensionLoader extLoader;
 
+    private StartupPerformance _startupPerformance = new();
+    
     public new ViewModels_ViewModelMain DataContext { get => (ViewModels_ViewModelMain)base.DataContext; set => base.DataContext = value; }
     
     public static MainWindow? Current {
@@ -43,6 +46,8 @@ internal partial class MainWindow : Window
 
     public MainWindow(ExtensionLoader extensionLoader)
     {
+        _startupPerformance.ReportToMainWindow();
+        
         (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).MainWindow = this;
         extLoader = extensionLoader;
 
@@ -61,7 +66,11 @@ internal partial class MainWindow : Window
         platform = services.GetRequiredService<IPlatform>();
         DataContext = services.GetRequiredService<ViewModels_ViewModelMain>();
         DataContext.Setup(services);
+        _startupPerformance.ReportToMainViewModel();
 
+        var analytics = services.GetService<AnalyticsPeriodicReporter>();
+        analytics?.Start();
+        
         InitializeComponent();
     }
 
@@ -116,6 +125,8 @@ internal partial class MainWindow : Window
         base.OnLoaded(e);
         LoadingWindow.Instance?.SafeClose();
         Activate();
+        _startupPerformance.ReportToInteractivity();
+        Analytics.SendStartup(_startupPerformance);
     }
 
     protected override void OnClosing(WindowClosingEventArgs e)