优化了文本框焦点,优化了更新体验,优化了遥测,披露了收集的数据。
This commit is contained in:
lincube
2026-03-17 01:01:48 +08:00
parent 298defb829
commit dadd132b4f
21 changed files with 704 additions and 70 deletions

View File

@@ -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;
}

View File

@@ -47,7 +47,6 @@ public sealed record PrivacySettingsState(
bool UploadAnonymousCrashData,
bool UploadAnonymousUsageData);
public sealed record UpdateSettingsState(
bool AutoCheckUpdates,
bool IncludePrereleaseUpdates,
string UpdateChannel,
string UpdateMode,

View File

@@ -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),

View File

@@ -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)
{