mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-24 10:34:26 +08:00
0.6.3
优化了文本框焦点,优化了更新体验,优化了遥测,披露了收集的数据。
This commit is contained in:
@@ -15,6 +15,7 @@ public sealed class DeviceIdService
|
||||
{
|
||||
private static DeviceIdService? _instance;
|
||||
private string? _deviceId;
|
||||
private string? _persistentUserId; // 持久化的用户ID,用于关联设备
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private bool _isInitialized;
|
||||
|
||||
@@ -43,6 +44,19 @@ public sealed class DeviceIdService
|
||||
}
|
||||
}
|
||||
|
||||
// 持久化的用户ID,用于跨设备关联用户
|
||||
public string PersistentUserId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_persistentUserId is null)
|
||||
{
|
||||
throw new InvalidOperationException("PersistentUserId not initialized");
|
||||
}
|
||||
return _persistentUserId;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureDeviceId()
|
||||
{
|
||||
if (_isInitialized)
|
||||
@@ -56,13 +70,22 @@ public sealed class DeviceIdService
|
||||
{
|
||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
|
||||
// 初始化或生成持久化用户ID(只生成一次,永不改变)
|
||||
if (string.IsNullOrEmpty(snapshot.PersistentUserId))
|
||||
{
|
||||
snapshot.PersistentUserId = GeneratePersistentUserId();
|
||||
AppLogger.Info("DeviceId", $"Generated new persistent user ID: {snapshot.PersistentUserId}");
|
||||
}
|
||||
_persistentUserId = snapshot.PersistentUserId;
|
||||
|
||||
// 初始化或生成设备ID(可以刷新)
|
||||
if (string.IsNullOrEmpty(snapshot.DeviceId))
|
||||
{
|
||||
snapshot.DeviceId = GenerateDeviceId();
|
||||
_settingsFacade.Settings.SaveSnapshot(
|
||||
SettingsScope.App,
|
||||
snapshot,
|
||||
changedKeys: [nameof(AppSettingsSnapshot.DeviceId)]);
|
||||
changedKeys: [nameof(AppSettingsSnapshot.DeviceId), nameof(AppSettingsSnapshot.PersistentUserId)]);
|
||||
_deviceId = snapshot.DeviceId;
|
||||
AppLogger.Info("DeviceId", $"Generated new device ID: {_deviceId}");
|
||||
}
|
||||
@@ -75,6 +98,7 @@ public sealed class DeviceIdService
|
||||
catch (Exception ex)
|
||||
{
|
||||
_deviceId = GenerateDeviceId();
|
||||
_persistentUserId = GeneratePersistentUserId();
|
||||
AppLogger.Warn("DeviceId", $"Failed to persist device ID, using generated ID: {_deviceId}", ex);
|
||||
}
|
||||
}
|
||||
@@ -87,6 +111,15 @@ public sealed class DeviceIdService
|
||||
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(deviceInfo));
|
||||
return Convert.ToHexString(hash)[..32].ToLower();
|
||||
}
|
||||
|
||||
private static string GeneratePersistentUserId()
|
||||
{
|
||||
// 生成一个永久性的用户ID,基于机器名和用户名的哈希
|
||||
var userInfo = $"{Environment.MachineName}|{Environment.UserName}|LanMountainDesktop";
|
||||
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||
var hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(userInfo));
|
||||
return Convert.ToHexString(hash)[..32].ToLower();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
@@ -140,9 +173,18 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30));
|
||||
|
||||
// 发送PostHog标准的$pageview事件用于统计日活(始终发送,不受开关影响)
|
||||
CaptureEvent("$pageview", new Dictionary<string, object>
|
||||
{
|
||||
{ "$current_url", "app://main" },
|
||||
{ "$title", "LanMountainDesktop" }
|
||||
});
|
||||
|
||||
// 发送应用启动事件(始终发送,用于统计用户数量)
|
||||
CaptureEvent("app_online", new Dictionary<string, object>
|
||||
{
|
||||
{ "event_type", "app_start" }
|
||||
{ "event_type", "app_start" },
|
||||
{ "analytics_enabled", _isEnabled }
|
||||
});
|
||||
|
||||
AppLogger.Info("UserBehaviorAnalytics", $"Analytics initialized. DeviceId={_deviceIdService.DeviceId}, Enabled={_isEnabled}");
|
||||
@@ -382,10 +424,22 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
|
||||
try
|
||||
{
|
||||
// 基础事件($pageview, app_online, app_shutdown等)始终发送,用于统计用户数量
|
||||
bool isBasicEvent = eventName.StartsWith("$") ||
|
||||
eventName == "app_online" ||
|
||||
eventName == "app_shutdown" ||
|
||||
eventName == "$identify";
|
||||
|
||||
// 非基础事件只有在启用时才发送
|
||||
if (!isBasicEvent && !_isEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventData = new UserBehaviorEvent
|
||||
{
|
||||
Event = eventName,
|
||||
DistinctId = _deviceIdService.DeviceId,
|
||||
DistinctId = _deviceIdService.PersistentUserId, // 使用持久化用户ID
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Properties = properties ?? new Dictionary<string, object>(),
|
||||
IncludeDetailedData = _isEnabled
|
||||
@@ -542,21 +596,29 @@ public sealed class UserBehaviorAnalyticsService : IDisposable
|
||||
{
|
||||
var userProperties = new Dictionary<string, object>
|
||||
{
|
||||
{ "$device_id", distinctId },
|
||||
{ "$app_version", GetAppVersion() },
|
||||
{ "$os", GetOsName() },
|
||||
{ "$os_version", GetOsVersion() }
|
||||
{ "$os_version", GetOsVersion() },
|
||||
{ "$device_type", GetDeviceModel() },
|
||||
{ "$device_id", _deviceIdService.DeviceId } // 当前设备ID
|
||||
};
|
||||
|
||||
// PostHog正确的$identify格式
|
||||
// 使用PersistentUserId作为distinct_id,确保设备ID刷新后仍能关联到同一用户
|
||||
var requestBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "api_key", PostHogApiKey },
|
||||
{ "event", "$identify" },
|
||||
{ "distinct_id", _deviceIdService.PersistentUserId }, // 使用持久化用户ID
|
||||
{ "timestamp", DateTimeOffset.UtcNow.ToString("o") },
|
||||
{ "properties", new Dictionary<string, object>
|
||||
{
|
||||
{ "distinct_id", distinctId },
|
||||
{ "$set", userProperties }
|
||||
{ "$set", userProperties },
|
||||
{ "$set_once", new Dictionary<string, object>
|
||||
{
|
||||
{ "first_app_open", DateTimeOffset.UtcNow.ToString("o") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -796,6 +858,9 @@ public sealed class CrashReportService
|
||||
});
|
||||
|
||||
ConfigureCrashReportingScope();
|
||||
|
||||
// 显式开始会话跟踪
|
||||
SentrySdk.StartSession();
|
||||
|
||||
AppLogger.Info("CrashReport", $"Sentry crash reporting initialized. DeviceId={_deviceIdService.DeviceId}");
|
||||
|
||||
@@ -849,7 +914,10 @@ public sealed class CrashReportService
|
||||
{
|
||||
if (_isEnabled && _isInitialized)
|
||||
{
|
||||
AppLogger.Info("CrashReport", $"Shutdown event will be sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
|
||||
// 结束Sentry会话
|
||||
SentrySdk.EndSession();
|
||||
SentrySdk.Flush(TimeSpan.FromSeconds(3));
|
||||
AppLogger.Info("CrashReport", $"Shutdown event sent via Sentry. DeviceId={_deviceIdService.DeviceId}");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ public sealed record PrivacySettingsState(
|
||||
bool UploadAnonymousCrashData,
|
||||
bool UploadAnonymousUsageData);
|
||||
public sealed record UpdateSettingsState(
|
||||
bool AutoCheckUpdates,
|
||||
bool IncludePrereleaseUpdates,
|
||||
string UpdateChannel,
|
||||
string UpdateMode,
|
||||
|
||||
@@ -628,7 +628,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
snapshot.UpdateChannel,
|
||||
snapshot.IncludePrereleaseUpdates);
|
||||
return new UpdateSettingsState(
|
||||
snapshot.AutoCheckUpdates,
|
||||
string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
|
||||
normalizedChannel,
|
||||
UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode),
|
||||
@@ -646,7 +645,6 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
|
||||
state.UpdateChannel,
|
||||
state.IncludePrereleaseUpdates);
|
||||
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
|
||||
snapshot.IncludePrereleaseUpdates = string.Equals(
|
||||
normalizedChannel,
|
||||
UpdateSettingsValues.ChannelPreview,
|
||||
@@ -672,7 +670,7 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
|
||||
snapshot,
|
||||
changedKeys:
|
||||
[
|
||||
nameof(AppSettingsSnapshot.AutoCheckUpdates),
|
||||
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
||||
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
|
||||
nameof(AppSettingsSnapshot.UpdateChannel),
|
||||
nameof(AppSettingsSnapshot.UpdateMode),
|
||||
|
||||
@@ -131,13 +131,10 @@ public sealed class UpdateWorkflowService
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var state = _settingsFacade.Update.Get();
|
||||
if (!state.AutoCheckUpdates)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Always check for updates on startup (removed AutoCheckUpdates check)
|
||||
var result = await CheckForUpdatesAsync(currentVersion, cancellationToken);
|
||||
if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
|
||||
{
|
||||
@@ -145,12 +142,14 @@ public sealed class UpdateWorkflowService
|
||||
}
|
||||
|
||||
var normalizedMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode);
|
||||
if (string.Equals(normalizedMode, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
// For "Silent Download" and "Silent Install" modes, automatically download the update
|
||||
if (string.Equals(normalizedMode, UpdateSettingsValues.ModeDownloadThenConfirm, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(normalizedMode, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
|
||||
// For "Manual" mode, just check but don't download
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user