mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
0.6.0.1
应用遥测,插件市场
This commit is contained in:
943
LanMountainDesktop/Services/CrashReportService.cs
Normal file
943
LanMountainDesktop/Services/CrashReportService.cs
Normal file
@@ -0,0 +1,943 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using Sentry;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public sealed class DeviceIdService
|
||||
{
|
||||
private static DeviceIdService? _instance;
|
||||
private string? _deviceId;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private bool _isInitialized;
|
||||
|
||||
public static DeviceIdService Instance => _instance ?? throw new InvalidOperationException("DeviceIdService not initialized");
|
||||
|
||||
public DeviceIdService(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
}
|
||||
|
||||
public static void Initialize(ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_instance = new DeviceIdService(settingsFacade);
|
||||
_instance.EnsureDeviceId();
|
||||
}
|
||||
|
||||
public string DeviceId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_deviceId is null)
|
||||
{
|
||||
throw new InvalidOperationException("DeviceId not initialized");
|
||||
}
|
||||
return _deviceId;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureDeviceId()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
|
||||
if (string.IsNullOrEmpty(snapshot.DeviceId))
|
||||
{
|
||||
snapshot.DeviceId = GenerateDeviceId();
|
||||
_settingsFacade.Settings.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys: [nameof(AppSettingsSnapshot.DeviceId)]);
|
||||
_deviceId = snapshot.DeviceId;
|
||||
AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_deviceId = snapshot.DeviceId;
|
||||
AppLogger.Info("DeviceId", $"Loaded existing device ID: {_deviceId}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_deviceId = GenerateDeviceId();
|
||||
AppLogger.Warn("DeviceId", $"Failed to persist device ID, using generated ID: {_deviceId}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateDeviceId()
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var deviceInfo = $"{Environment.MachineName}|{Environment.ProcessorCount}|{Environment.OSVersion}|{Environment.UserName}|{timestamp}";
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(deviceInfo));
|
||||
return Convert.ToHexString(hash)[..32].ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
{
|
||||
private const string PostHogApiKey = "phc_bhQZvKDDfsEdLT6kkRFvrWMT8Pc5aCGGsnxoc5ijSf9";
|
||||
private const string PostHogHost = "https://us.i.posthog.com/capture/";
|
||||
|
||||
private bool _isEnabled;
|
||||
private bool _isInitialized;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly DeviceIdService _deviceIdService;
|
||||
private readonly Queue<UserBehaviorEvent> _eventQueue = new();
|
||||
private readonly object _queueLock = new();
|
||||
private System.Threading.Timer? _flushTimer;
|
||||
private readonly PluginSdk.ISettingsService _settingsService;
|
||||
|
||||
public UserBehaviorAnalyticsService(ISettingsFacadeService settingsFacade, DeviceIdService deviceIdService)
|
||||
{
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
_settingsService = settingsFacade.Settings;
|
||||
_deviceIdService = deviceIdService ?? throw new ArgumentNullException(nameof(deviceIdService));
|
||||
_settingsService.Changed += OnSettingsChanged;
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, PluginSdk.SettingsChangedEvent e)
|
||||
{
|
||||
if (e.Scope == PluginSdk.SettingsScope.App &&
|
||||
e.ChangedKeys is not null &&
|
||||
(e.ChangedKeys.Contains("UploadAnonymousCrashData") || e.ChangedKeys.Contains("UploadAnonymousUsageData")))
|
||||
{
|
||||
AppLogger.Info("UserBehaviorAnalytics", "Settings changed, refreshing enabled state.");
|
||||
RefreshEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
RefreshEnabledState();
|
||||
|
||||
try
|
||||
{
|
||||
_flushTimer = new System.Threading.Timer(
|
||||
_ => FlushEvents(),
|
||||
null,
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30));
|
||||
|
||||
CaptureEvent("app_online", new Dictionary<string, object>
|
||||
{
|
||||
{ "event_type", "app_start" }
|
||||
});
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to initialize analytics.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void TrackClick(string componentName, string? action = null)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("ui_click", new Dictionary<string, object>
|
||||
{
|
||||
{ "component", componentName },
|
||||
{ "action", action ?? "click" }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackComponentDrag(string componentId, string action)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("component_drag", new Dictionary<string, object>
|
||||
{
|
||||
{ "component_id", componentId },
|
||||
{ "action", action }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackComponentDrop(string componentId, string targetPosition)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("component_drop", new Dictionary<string, object>
|
||||
{
|
||||
{ "component_id", componentId },
|
||||
{ "target_position", targetPosition }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackSettingsOpen(string settingsPage)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("settings_open", new Dictionary<string, object>
|
||||
{
|
||||
{ "page", settingsPage }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackSettingsChange(string settingsPage, string settingKey, string? oldValue, string newValue)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("settings_change", new Dictionary<string, object>
|
||||
{
|
||||
{ "page", settingsPage },
|
||||
{ "key", settingKey },
|
||||
{ "old_value", oldValue ?? "" },
|
||||
{ "new_value", newValue }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackSettingsClose(string settingsPage)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("settings_close", new Dictionary<string, object>
|
||||
{
|
||||
{ "page", settingsPage }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackUpdateAction(string action, string? version = null)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var props = new Dictionary<string, object>
|
||||
{
|
||||
{ "action", action }
|
||||
};
|
||||
|
||||
if (version is not null)
|
||||
{
|
||||
props["version"] = version;
|
||||
}
|
||||
|
||||
CaptureEvent("update_action", props);
|
||||
}
|
||||
|
||||
public void TrackRestartAction(string action)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("restart_action", new Dictionary<string, object>
|
||||
{
|
||||
{ "action", action }
|
||||
});
|
||||
}
|
||||
|
||||
public void TrackNavigation(string fromPage, string toPage)
|
||||
{
|
||||
if (!_isEnabled || !_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CaptureEvent("navigation", new Dictionary<string, object>
|
||||
{
|
||||
{ "from", fromPage },
|
||||
{ "to", toPage }
|
||||
});
|
||||
}
|
||||
|
||||
public void SendCrashEvent()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "app_version", GetAppVersion() },
|
||||
{ "event_time", DateTimeOffset.UtcNow.ToString("o") },
|
||||
{ "event_type", "app_crash" }
|
||||
};
|
||||
|
||||
CaptureEvent("app_crash", properties);
|
||||
FlushEvents();
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"Crash event sent. DeviceId={_deviceIdService.DeviceId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to send crash event.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendShutdownEvent()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "app_version", GetAppVersion() },
|
||||
{ "event_time", DateTimeOffset.UtcNow.ToString("o") },
|
||||
{ "event_type", "app_shutdown" }
|
||||
};
|
||||
|
||||
if (_isEnabled)
|
||||
{
|
||||
properties["os_name"] = GetOsName();
|
||||
properties["os_version"] = GetOsVersion();
|
||||
properties["device_name"] = GetDeviceName();
|
||||
properties["device_model"] = GetDeviceModel();
|
||||
properties["device_arch"] = GetDeviceArchitecture();
|
||||
properties["language"] = GetSystemLanguage();
|
||||
}
|
||||
|
||||
CaptureEvent("app_shutdown", properties);
|
||||
FlushEvents();
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"Shutdown event sent. DeviceId={_deviceIdService.DeviceId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to send shutdown event.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var newEnabled = snapshot.UploadAnonymousUsageData;
|
||||
|
||||
if (_isEnabled != newEnabled)
|
||||
{
|
||||
_isEnabled = newEnabled;
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"User behavior analytics enabled state changed to '{_isEnabled}'.");
|
||||
|
||||
if (_isEnabled && _isInitialized)
|
||||
{
|
||||
CaptureEvent("analytics_enabled", new Dictionary<string, object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to refresh analytics enabled state.", ex);
|
||||
_isEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void CaptureEvent(string eventName, Dictionary<string, object>? properties = null)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var eventData = new UserBehaviorEvent
|
||||
{
|
||||
Event = eventName,
|
||||
DistinctId = _deviceIdService.DeviceId,
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Properties = properties ?? new Dictionary<string, object>(),
|
||||
IncludeDetailedData = _isEnabled
|
||||
};
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
_eventQueue.Enqueue(eventData);
|
||||
|
||||
if (_eventQueue.Count >= 20)
|
||||
{
|
||||
FlushEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", $"Failed to capture event '{eventName}'.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void CapturePageView(string pageName, string? sourcePage = null)
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "page_name", pageName }
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(sourcePage))
|
||||
{
|
||||
properties["source_page"] = sourcePage;
|
||||
}
|
||||
|
||||
CaptureEvent("page_view", properties);
|
||||
}
|
||||
|
||||
public void CaptureFeatureUsage(string featureName, string action)
|
||||
{
|
||||
CaptureEvent("feature_usage", new Dictionary<string, object>
|
||||
{
|
||||
{ "feature_name", featureName },
|
||||
{ "action", action }
|
||||
});
|
||||
}
|
||||
|
||||
private void FlushEvents()
|
||||
{
|
||||
List<UserBehaviorEvent> eventsToSend;
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
if (_eventQueue.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
eventsToSend = new List<UserBehaviorEvent>();
|
||||
while (_eventQueue.Count > 0 && eventsToSend.Count < 20)
|
||||
{
|
||||
eventsToSend.Add(_eventQueue.Dequeue());
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SendEventsToPostHog(eventsToSend);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to send events to PostHog.", ex);
|
||||
|
||||
lock (_queueLock)
|
||||
{
|
||||
foreach (var evt in eventsToSend)
|
||||
{
|
||||
if (_eventQueue.Count < 100)
|
||||
{
|
||||
_eventQueue.Enqueue(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SendEventsToPostHog(List<UserBehaviorEvent> events)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new System.Net.Http.HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
var firstEvent = events.FirstOrDefault();
|
||||
if (firstEvent is not null)
|
||||
{
|
||||
SendIdentifyToPostHog(client, firstEvent.DistinctId);
|
||||
}
|
||||
|
||||
foreach (var e in events)
|
||||
{
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
{ "distinct_id", e.DistinctId }
|
||||
};
|
||||
|
||||
if (e.IncludeDetailedData)
|
||||
{
|
||||
properties["$os"] = GetOsName();
|
||||
properties["$os_version"] = GetOsVersion();
|
||||
properties["$app_version"] = GetAppVersion();
|
||||
properties["$device_id"] = e.DistinctId;
|
||||
}
|
||||
|
||||
foreach (var kvp in e.Properties)
|
||||
{
|
||||
properties[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
var requestBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "api_key", PostHogApiKey },
|
||||
{ "event", e.Event },
|
||||
{ "timestamp", e.Timestamp.ToString("o") },
|
||||
{ "properties", properties }
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(requestBody);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var content = new System.Net.Http.ByteArrayContent(bytes);
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||
|
||||
var response = client.PostAsync(PostHogHost, content).GetAwaiter().GetResult();
|
||||
var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", $"PostHog API error for event '{e.Event}': {response.StatusCode} - {responseBody}");
|
||||
}
|
||||
}
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"Successfully sent {events.Count} events to PostHog.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to send events to PostHog API.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void SendIdentifyToPostHog(System.Net.Http.HttpClient client, string distinctId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userProperties = new Dictionary<string, object>
|
||||
{
|
||||
{ "$device_id", distinctId },
|
||||
{ "$app_version", GetAppVersion() },
|
||||
{ "$os", GetOsName() },
|
||||
{ "$os_version", GetOsVersion() }
|
||||
};
|
||||
|
||||
var requestBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "api_key", PostHogApiKey },
|
||||
{ "event", "$identify" },
|
||||
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
|
||||
{ "properties", new Dictionary<string, object>
|
||||
{
|
||||
{ "distinct_id", distinctId },
|
||||
{ "$set", userProperties }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(requestBody);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var content = new System.Net.Http.ByteArrayContent(bytes);
|
||||
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||
|
||||
var response = client.PostAsync(PostHogHost, content).GetAwaiter().GetResult();
|
||||
var responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"PostHog identify response: {response.StatusCode}");
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", $"PostHog identify failed: {response.StatusCode} - {responseBody}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Failed to send identify to PostHog.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, object> GetEventProperties(UserBehaviorEvent e)
|
||||
{
|
||||
var props = new Dictionary<string, object>
|
||||
{
|
||||
{ "$os", GetOsName() },
|
||||
{ "$os_version", GetOsVersion() },
|
||||
{ "$app_version", GetAppVersion() },
|
||||
{ "$device_id", e.DistinctId }
|
||||
};
|
||||
|
||||
foreach (var kvp in e.Properties)
|
||||
{
|
||||
props[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
public bool IsEnabled => _isEnabled;
|
||||
|
||||
public string DeviceId => _deviceIdService.DeviceId;
|
||||
|
||||
private static string GetAppVersion()
|
||||
{
|
||||
var assembly = typeof(UserBehaviorAnalyticsService).Assembly;
|
||||
var version = assembly.GetName().Version;
|
||||
return version is null ? "1.0.0" : $"{version.Major}.{version.Minor}.{version.Build}";
|
||||
}
|
||||
|
||||
private static string GetOsName()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "Windows";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "Linux";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "macOS";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private static string GetOsVersion()
|
||||
{
|
||||
try { return Environment.OSVersion.VersionString ?? "Unknown"; }
|
||||
catch { return "Unknown"; }
|
||||
}
|
||||
|
||||
private static string GetDeviceName()
|
||||
{
|
||||
try { return Environment.MachineName ?? "Unknown"; }
|
||||
catch { return "Unknown"; }
|
||||
}
|
||||
|
||||
private static string GetDeviceModel()
|
||||
{
|
||||
var osDesc = RuntimeInformation.OSDescription;
|
||||
if (osDesc.Contains("Windows")) return "Windows PC";
|
||||
if (osDesc.Contains("Linux")) return "Linux PC";
|
||||
if (osDesc.Contains("Darwin")) return "Mac";
|
||||
return osDesc;
|
||||
}
|
||||
|
||||
private static string GetDeviceArchitecture()
|
||||
{
|
||||
return RuntimeInformation.OSArchitecture.ToString();
|
||||
}
|
||||
|
||||
private static string GetSystemLanguage()
|
||||
{
|
||||
try { return System.Globalization.CultureInfo.CurrentUICulture.Name ?? "en-US"; }
|
||||
catch { return "en-US"; }
|
||||
}
|
||||
|
||||
private static string GetOsBuild()
|
||||
{
|
||||
try { return Environment.OSVersion.Version.Build.ToString() ?? "Unknown"; }
|
||||
catch { return "Unknown"; }
|
||||
}
|
||||
|
||||
private static int GetProcessorCount()
|
||||
{
|
||||
return Environment.ProcessorCount;
|
||||
}
|
||||
|
||||
private static long GetTotalMemoryMB()
|
||||
{
|
||||
try { return GC.GetGCMemoryInfo().TotalAvailableMemoryBytes / (1024 * 1024); }
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static string GetRuntimeVersion()
|
||||
{
|
||||
return Environment.Version.ToString();
|
||||
}
|
||||
|
||||
private static string GetClrVersion()
|
||||
{
|
||||
return Environment.Version.ToString();
|
||||
}
|
||||
|
||||
private static string GetDotNetVersion()
|
||||
{
|
||||
return Environment.Version.ToString();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
_flushTimer?.Dispose();
|
||||
FlushEvents();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("UserBehaviorAnalytics", "Error disposing analytics service.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private class UserBehaviorEvent
|
||||
{
|
||||
public string Event { get; set; } = string.Empty;
|
||||
public string DistinctId { get; set; } = string.Empty;
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public Dictionary<string, object> Properties { get; set; } = new();
|
||||
public bool IncludeDetailedData { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static Dictionary<string, object> Merge(this Dictionary<string, object> first, Dictionary<string, object> second)
|
||||
{
|
||||
var result = new Dictionary<string, object>(first);
|
||||
foreach (var kvp in second)
|
||||
{
|
||||
result[kvp.Key] = kvp.Value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CrashReportService
|
||||
{
|
||||
private const string SentryDsn = "https://f2aad3a1c63b5f2213ad82683ce93c06@o4511049423257600.ingest.us.sentry.io/4511049425813504";
|
||||
|
||||
private bool _isInitialized;
|
||||
private bool _isEnabled;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly DeviceIdService _deviceIdService;
|
||||
private readonly PluginSdk.ISettingsService _settingsService;
|
||||
|
||||
public CrashReportService(ISettingsFacadeService settingsFacade, DeviceIdService deviceIdService)
|
||||
{
|
||||
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
|
||||
_settingsService = settingsFacade.Settings;
|
||||
_deviceIdService = deviceIdService ?? throw new ArgumentNullException(nameof(deviceIdService));
|
||||
_settingsService.Changed += OnSettingsChanged;
|
||||
}
|
||||
|
||||
private void OnSettingsChanged(object? sender, PluginSdk.SettingsChangedEvent e)
|
||||
{
|
||||
if (e.Scope == PluginSdk.SettingsScope.App &&
|
||||
e.ChangedKeys is not null &&
|
||||
(e.ChangedKeys.Contains("UploadAnonymousCrashData") || e.ChangedKeys.Contains("UploadAnonymousUsageData")))
|
||||
{
|
||||
AppLogger.Info("CrashReport", "Settings changed, refreshing enabled state.");
|
||||
RefreshEnabledState();
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshEnabledState()
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var newEnabled = snapshot.UploadAnonymousCrashData;
|
||||
|
||||
if (_isEnabled != newEnabled)
|
||||
{
|
||||
_isEnabled = newEnabled;
|
||||
AppLogger.Info("CrashReport", $"Crash reporting enabled state changed to '{_isEnabled}'.");
|
||||
|
||||
if (_isEnabled && !_isInitialized)
|
||||
{
|
||||
InitializeSentry();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("CrashReport", "Failed to refresh crash reporting enabled state.", ex);
|
||||
_isEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSentry()
|
||||
{
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
try
|
||||
{
|
||||
SentrySdk.Init(options =>
|
||||
{
|
||||
options.Dsn = SentryDsn;
|
||||
options.AutoSessionTracking = true;
|
||||
options.AttachStacktrace = true;
|
||||
options.MaxBreadcrumbs = 100;
|
||||
options.Release = GetAppVersion();
|
||||
options.Environment = GetEnvironment();
|
||||
});
|
||||
|
||||
ConfigureCrashReportingScope();
|
||||
|
||||
AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}");
|
||||
|
||||
#if DEBUG
|
||||
SentrySdk.CaptureMessage($"Crash reporting enabled - Debug mode test. DeviceId={_deviceIdService.DeviceId}");
|
||||
#endif
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("CrashReport", "Failed to initialize Sentry crash reporting.", ex);
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureCrashReportingScope()
|
||||
{
|
||||
try
|
||||
{
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = _deviceIdService.DeviceId
|
||||
};
|
||||
|
||||
scope.SetTag("data_type", "crash_report");
|
||||
scope.SetTag("device_id", _deviceIdService.DeviceId);
|
||||
scope.SetTag("device_name", GetDeviceName());
|
||||
scope.SetTag("device_model", GetDeviceModel());
|
||||
scope.SetTag("device_arch", GetDeviceArchitecture());
|
||||
scope.SetTag("os_name", GetOsName());
|
||||
scope.SetTag("os_version", GetOsVersion());
|
||||
scope.SetTag("language", GetSystemLanguage());
|
||||
});
|
||||
|
||||
AppLogger.Info("CrashReport", $"Crash reporting scope configured. DeviceId={_deviceIdService.DeviceId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("CrashReport", "Failed to configure crash reporting scope.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled => _isEnabled;
|
||||
|
||||
public string DeviceId => _deviceIdService.DeviceId;
|
||||
|
||||
public void SendShutdownEvent()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_isEnabled && _isInitialized)
|
||||
{
|
||||
AppLogger.Info("CrashReport", $"Shutdown event will be sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isInitialized)
|
||||
{
|
||||
SentrySdk.Init(options =>
|
||||
{
|
||||
options.Dsn = SentryDsn;
|
||||
options.AutoSessionTracking = false;
|
||||
options.Release = GetAppVersion();
|
||||
options.Environment = GetEnvironment();
|
||||
});
|
||||
}
|
||||
|
||||
SentrySdk.ConfigureScope(scope =>
|
||||
{
|
||||
scope.User = new SentryUser
|
||||
{
|
||||
Id = _deviceIdService.DeviceId
|
||||
};
|
||||
scope.SetTag("data_type", "shutdown");
|
||||
scope.SetTag("device_id", _deviceIdService.DeviceId);
|
||||
scope.SetTag("app_version", GetAppVersion());
|
||||
});
|
||||
|
||||
SentrySdk.CaptureMessage($"app_shutdown - DeviceId={_deviceIdService.DeviceId}");
|
||||
SentrySdk.Flush(TimeSpan.FromSeconds(3));
|
||||
|
||||
AppLogger.Info("CrashReport", $"Shutdown event sent. DeviceId={_deviceIdService.DeviceId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("CrashReport", "Failed to send shutdown event.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetDeviceName()
|
||||
{
|
||||
try { return Environment.MachineName ?? "Unknown"; }
|
||||
catch { return "Unknown"; }
|
||||
}
|
||||
|
||||
private static string GetDeviceModel()
|
||||
{
|
||||
var osDesc = RuntimeInformation.OSDescription;
|
||||
if (osDesc.Contains("Windows")) return "Windows PC";
|
||||
if (osDesc.Contains("Linux")) return "Linux PC";
|
||||
if (osDesc.Contains("Darwin")) return "Mac";
|
||||
return osDesc;
|
||||
}
|
||||
|
||||
private static string GetDeviceArchitecture()
|
||||
{
|
||||
return RuntimeInformation.OSArchitecture.ToString();
|
||||
}
|
||||
|
||||
private static string GetOsName()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return "Windows";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "Linux";
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return "macOS";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private static string GetOsVersion()
|
||||
{
|
||||
try { return Environment.OSVersion.VersionString ?? "Unknown"; }
|
||||
catch { return "Unknown"; }
|
||||
}
|
||||
|
||||
private static string GetSystemLanguage()
|
||||
{
|
||||
try { return System.Globalization.CultureInfo.CurrentUICulture.Name ?? "en-US"; }
|
||||
catch { return "en-US"; }
|
||||
}
|
||||
|
||||
private static string GetAppVersion()
|
||||
{
|
||||
var version = typeof(CrashReportService).Assembly.GetName().Version;
|
||||
return version is null ? "1.0.0" : $"{version.Major}.{version.Minor}.{version.Build}";
|
||||
}
|
||||
|
||||
private static string GetEnvironment()
|
||||
{
|
||||
#if DEBUG
|
||||
return "development";
|
||||
#else
|
||||
return "production";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user