AnalyticsPeriodicReporter.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. using PixiEditor.Helpers;
  2. namespace PixiEditor.Models.AnalyticsAPI;
  3. public class AnalyticsPeriodicReporter
  4. {
  5. private int _sendExceptions = 0;
  6. private bool _resumeSession;
  7. private readonly SemaphoreSlim _semaphore = new(1, 1);
  8. private readonly AnalyticsClient _client;
  9. private readonly PeriodicPerformanceReporter _performanceReporter;
  10. private readonly List<AnalyticEvent> _backlog = new();
  11. private readonly CancellationTokenSource _cancellationToken = new();
  12. private DateTime lastActivity;
  13. public static AnalyticsPeriodicReporter? Instance { get; private set; }
  14. public Guid SessionId { get; private set; }
  15. public AnalyticsPeriodicReporter(AnalyticsClient client)
  16. {
  17. if (Instance != null)
  18. throw new InvalidOperationException("There's already a AnalyticsReporter present");
  19. Instance = this;
  20. _client = client;
  21. _performanceReporter = new PeriodicPerformanceReporter(this);
  22. }
  23. public void Start(Guid? sessionId)
  24. {
  25. if (sessionId != null)
  26. {
  27. SessionId = sessionId.Value;
  28. _resumeSession = true;
  29. _backlog.Add(new AnalyticEvent { Time = DateTime.UtcNow, EventType = AnalyticEventTypes.ResumeSession });
  30. }
  31. Task.Run(RunAsync);
  32. _performanceReporter.StartPeriodicReporting();
  33. }
  34. public async Task StopAsync()
  35. {
  36. _cancellationToken.Cancel();
  37. await _client.EndSessionAsync(SessionId).WaitAsync(TimeSpan.FromSeconds(1));
  38. }
  39. public void AddEvent(AnalyticEvent value)
  40. {
  41. // Don't send startup as it gives invalid results for crash resumed sessions
  42. if (value.EventType == AnalyticEventTypes.Startup && _resumeSession)
  43. {
  44. return;
  45. }
  46. Task.Run(() =>
  47. {
  48. _semaphore.Wait();
  49. try
  50. {
  51. _backlog.Add(value);
  52. }
  53. finally
  54. {
  55. _semaphore.Release();
  56. }
  57. });
  58. }
  59. private async Task RunAsync()
  60. {
  61. if (!_resumeSession)
  62. {
  63. var createSession = await _client.CreateSessionAsync(_cancellationToken.Token);
  64. if (!createSession.HasValue)
  65. {
  66. return;
  67. }
  68. SessionId = createSession.Value;
  69. }
  70. Task.Run(RunHeartbeatAsync);
  71. while (!_cancellationToken.IsCancellationRequested)
  72. {
  73. try
  74. {
  75. if (_backlog.Any(x => x.ExpectingEndTimeReport))
  76. WaitForEndTimes();
  77. await SendBacklogAsync();
  78. await Task.Delay(TimeSpan.FromSeconds(10));
  79. }
  80. catch (TaskCanceledException) { }
  81. catch (Exception e)
  82. {
  83. await SendExceptionAsync(e);
  84. }
  85. }
  86. }
  87. private void WaitForEndTimes()
  88. {
  89. var totalTimeout = DateTime.Now + TimeSpan.FromSeconds(10);
  90. foreach (var backlog in _backlog)
  91. {
  92. var timeout = totalTimeout - DateTime.Now;
  93. if (timeout < TimeSpan.Zero)
  94. {
  95. break;
  96. }
  97. backlog.WaitForEndTime(timeout);
  98. }
  99. }
  100. private async Task SendBacklogAsync()
  101. {
  102. await _semaphore.WaitAsync();
  103. try
  104. {
  105. if (_backlog.Count == 0)
  106. {
  107. return;
  108. }
  109. var result = await _client.SendEventsAsync(SessionId, _backlog, _cancellationToken.Token);
  110. _backlog.Clear();
  111. if (result == null) _cancellationToken.Cancel();
  112. lastActivity = DateTime.UtcNow;
  113. }
  114. finally
  115. {
  116. _semaphore.Release();
  117. }
  118. }
  119. private async Task RunHeartbeatAsync()
  120. {
  121. lastActivity = DateTime.UtcNow;
  122. while (!_cancellationToken.IsCancellationRequested)
  123. {
  124. try
  125. {
  126. await SendHeartbeatIfNeededAsync();
  127. await Task.Delay(TimeSpan.FromSeconds(10), _cancellationToken.Token);
  128. }
  129. catch (TaskCanceledException) { }
  130. catch (Exception e)
  131. {
  132. await SendExceptionAsync(e);
  133. }
  134. }
  135. }
  136. private async ValueTask SendHeartbeatIfNeededAsync()
  137. {
  138. var timeSinceLastActivity = DateTime.UtcNow - lastActivity;
  139. if (timeSinceLastActivity.TotalSeconds < 60)
  140. {
  141. return;
  142. }
  143. var result = await _client.SendHeartbeatAsync(SessionId, _cancellationToken.Token);
  144. lastActivity = DateTime.UtcNow;
  145. if (!result)
  146. {
  147. _cancellationToken.Cancel();
  148. }
  149. }
  150. private async Task SendExceptionAsync(Exception e)
  151. {
  152. if (_sendExceptions > 6)
  153. {
  154. await CrashHelper.SendExceptionInfoToWebhookAsync(e);
  155. _sendExceptions++;
  156. }
  157. }
  158. }