Files
LanMountainDesktop/docs/archive/superpowers/plans/2026-05-26-telemetry-normalization.md
2026-06-08 03:54:33 +08:00

26 KiB
Raw Permalink Blame History

遥测系统规范化改进实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 修复 Sentry/PostHog 遥测系统的数据一致性问题,添加中文可读标签,规范化上报数据格式,补充缺失业务事件。

Architecture: 保持现有三个服务SentryCrashTelemetryService、PostHogUsageTelemetryService、TelemetryIdentityService的架构不变在各服务内部进行数据修复和增强。新增 TelemetryEventNames 静态类统一管理事件名和中文显示名,新增 TelemetryEnvironmentInfo 增强方法。

Tech Stack: C# / .NET 8 / Sentry 6.4.1 / PostHog 2.6.0 / Avalonia UI


文件变更地图

文件 操作 职责
LanMountainDesktop/Services/TelemetryEventNames.cs 新建 统一管理所有事件名和中文显示名
LanMountainDesktop/Services/TelemetryEnvironmentInfo.cs 修改 增强环境信息采集、修复重复方法
LanMountainDesktop/Services/SentryCrashTelemetryService.cs 修改 修复 Tags/Extras 冗余、添加中文标签、修复 PII、增加业务上下文
LanMountainDesktop/Services/PostHogUsageTelemetryService.cs 修改 修复 distinct_id 不一致、修复 Session 生命周期、添加中文标签、优化 Flush、增强 DescribePlacement
LanMountainDesktop/Views/MainWindow.axaml.cs 修改 添加 Session 生命周期调用
LanMountainDesktop/App.axaml.cs 修改 添加 Session 结束调用

Task 1: 新建 TelemetryEventNames 统一事件名管理

Files:

  • Create: LanMountainDesktop/Services/TelemetryEventNames.cs

  • Step 1: 创建 TelemetryEventNames.cs

using System.Collections.Generic;

namespace LanMountainDesktop.Services;

internal static class TelemetryEventNames
{
    internal static string DisplayName(string eventName) =>
        EventDisplayNames.TryGetValue(eventName, out var displayName)
            ? displayName
            : eventName;

    internal const string AppFirstLaunch = "app_first_launch";
    internal const string AppSessionStart = "app_session_start";
    internal const string AppSessionEnd = "app_session_end";
    internal const string MainWindowOpened = "main_window_opened";
    internal const string MainWindowClosed = "main_window_closed";
    internal const string SettingsWindowOpened = "settings_window_opened";
    internal const string SettingsWindowClosed = "settings_window_closed";
    internal const string SettingsNavigation = "settings_navigation";
    internal const string SettingsDrawerOpened = "settings_drawer_opened";
    internal const string SettingsDrawerClosed = "settings_drawer_closed";
    internal const string DesktopComponentPlaced = "desktop_component_placed";
    internal const string DesktopComponentMoved = "desktop_component_moved";
    internal const string DesktopComponentResized = "desktop_component_resized";
    internal const string DesktopComponentDeleted = "desktop_component_deleted";
    internal const string DesktopComponentEditorOpened = "desktop_component_editor_opened";
    internal const string ThemeChanged = "theme_changed";
    internal const string PluginInstalled = "plugin_installed";
    internal const string PluginUninstalled = "plugin_uninstalled";
    internal const string PluginEnabled = "plugin_enabled";
    internal const string PluginDisabled = "plugin_disabled";
    internal const string UpdateChecked = "update_checked";
    internal const string UpdateInstalled = "update_installed";
    internal const string AppCrash = "app_crash";

    internal const string SentryUnhandledException = "unhandled_exception";
    internal const string SentryTaskException = "task_exception";
    internal const string SentryShutdown = "shutdown";

    private static readonly Dictionary<string, string> EventDisplayNames = new()
    {
        [AppFirstLaunch] = "应用首次启动",
        [AppSessionStart] = "会话开始",
        [AppSessionEnd] = "会话结束",
        [MainWindowOpened] = "主窗口打开",
        [MainWindowClosed] = "主窗口关闭",
        [SettingsWindowOpened] = "设置窗口打开",
        [SettingsWindowClosed] = "设置窗口关闭",
        [SettingsNavigation] = "设置页导航",
        [SettingsDrawerOpened] = "设置抽屉打开",
        [SettingsDrawerClosed] = "设置抽屉关闭",
        [DesktopComponentPlaced] = "桌面组件放置",
        [DesktopComponentMoved] = "桌面组件移动",
        [DesktopComponentResized] = "桌面组件缩放",
        [DesktopComponentDeleted] = "桌面组件删除",
        [DesktopComponentEditorOpened] = "组件编辑器打开",
        [ThemeChanged] = "主题变更",
        [PluginInstalled] = "插件安装",
        [PluginUninstalled] = "插件卸载",
        [PluginEnabled] = "插件启用",
        [PluginDisabled] = "插件禁用",
        [UpdateChecked] = "更新检查",
        [UpdateInstalled] = "更新安装",
        [AppCrash] = "应用崩溃",
        [SentryUnhandledException] = "未处理异常",
        [SentryTaskException] = "任务异常",
        [SentryShutdown] = "应用关闭"
    };
}

Task 2: 增强 TelemetryEnvironmentInfo

Files:

  • Modify: LanMountainDesktop/Services/TelemetryEnvironmentInfo.cs

  • Step 1: 修复 GetClrVersion 重复问题,增加 GetScreenInfo、GetRenderMode、GetSystemLanguageDisplayName

TelemetryEnvironmentInfo.cs 中:

  1. 修改 GetClrVersion() 使其返回实际的 CLR 信息而非与 GetRuntimeVersion() 重复:
public static string GetClrVersion()
{
    try
    {
        return System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion() ?? "Unknown";
    }
    catch
    {
        return "Unknown";
    }
}
  1. 新增 GetScreenInfo() 方法:
public static string GetScreenInfo()
{
    try
    {
        var screenList = new List<string>();
        foreach (var screen in Avalonia.Controls.Screens.All)
        {
            screenList.Add($"{screen.Bounds.Width}x{screen.Bounds.Height}@{screen.Scaling:F1}x");
        }
        return screenList.Count > 0 ? string.Join("; ", screenList) : "Unknown";
    }
    catch
    {
        return "Unknown";
    }
}

注意:由于 TelemetryEnvironmentInfointernal static 类且可能在 UI 线程之外调用,Screens API 需要 UI 线程。因此改用更安全的方式:

public static string GetScreenInfo()
{
    return "requires_ui_thread";
}

并提供一个可从 UI 线程调用的重载:

public static string GetScreenInfoFromUiThread(Avalonia.Controls.TopLevel? topLevel)
{
    try
    {
        var screens = topLevel?.Screens;
        if (screens is null)
        {
            return "Unknown";
        }

        var screenList = new List<string>();
        foreach (var screen in screens.All)
        {
            screenList.Add($"{screen.Bounds.Width}x{screen.Bounds.Height}@{screen.Scaling:F1}x");
        }
        return screenList.Count > 0 ? string.Join("; ", screenList) : "Unknown";
    }
    catch
    {
        return "Unknown";
    }
}
  1. 新增 GetSystemLanguageDisplayName() 方法:
public static string GetSystemLanguageDisplayName()
{
    try
    {
        var culture = CultureInfo.CurrentUICulture;
        return culture.NativeName ?? culture.Name ?? "Unknown";
    }
    catch
    {
        return "Unknown";
    }
}
  1. 新增 GetRenderMode() 方法:
public static string GetRenderMode()
{
    return Program.StartupRenderMode ?? "Unknown";
}

注意:Program.StartupRenderMode 已是 internal static,同项目内可直接访问。


Task 3: 修复 SentryCrashTelemetryService — Tags/Extras 冗余、中文标签、PII、业务上下文

Files:

  • Modify: LanMountainDesktop/Services/SentryCrashTelemetryService.cs

  • Step 1: 修改 EnableSentry 方法 — 关闭 SendDefaultPii

将第 212 行:

options.SendDefaultPii = true;

改为:

options.SendDefaultPii = false;
  • Step 2: 重写 ApplyCommonScope 方法 — 消除 Tags/Extras 冗余,添加中文标签和业务上下文

将整个 ApplyCommonScope 方法(第 289-346 行)替换为:

private void ApplyCommonScope(Scope scope, string source, string eventType, bool includeLogTail)
{
    var installId = TelemetryIdentityService.Instance.InstallId;
    var telemetryId = TelemetryIdentityService.Instance.TelemetryId;

    scope.User = new SentryUser
    {
        Id = telemetryId
    };

    scope.SetTag("telemetry_channel", "sentry");
    scope.SetTag("event_type", eventType);
    scope.SetTag("event_display_name", TelemetryEventNames.DisplayName(eventType));
    scope.SetTag("source", source);
    scope.SetTag("app_version", TelemetryEnvironmentInfo.GetAppVersion());
    scope.SetTag("environment", TelemetryEnvironmentInfo.GetEnvironment());
    scope.SetTag("os_name", TelemetryEnvironmentInfo.GetOsName());
    scope.SetTag("os_version", TelemetryEnvironmentInfo.GetOsVersion());
    scope.SetTag("language", TelemetryEnvironmentInfo.GetSystemLanguage());

    scope.SetExtra("install_id", installId);
    scope.SetExtra("telemetry_id", telemetryId);
    scope.SetExtra("app_version", TelemetryEnvironmentInfo.GetAppVersion());
    scope.SetExtra("environment", TelemetryEnvironmentInfo.GetEnvironment());
    scope.SetExtra("os_name", TelemetryEnvironmentInfo.GetOsName());
    scope.SetExtra("os_version", TelemetryEnvironmentInfo.GetOsVersion());
    scope.SetExtra("os_build", TelemetryEnvironmentInfo.GetOsBuild());
    scope.SetExtra("device_model", TelemetryEnvironmentInfo.GetDeviceModel());
    scope.SetExtra("device_arch", TelemetryEnvironmentInfo.GetDeviceArchitecture());
    scope.SetExtra("processor_count", TelemetryEnvironmentInfo.GetProcessorCount());
    scope.SetExtra("total_memory_mb", TelemetryEnvironmentInfo.GetTotalMemoryMB());
    scope.SetExtra("runtime_version", TelemetryEnvironmentInfo.GetRuntimeVersion());
    scope.SetExtra("clr_version", TelemetryEnvironmentInfo.GetClrVersion());
    scope.SetExtra("language", TelemetryEnvironmentInfo.GetSystemLanguage());
    scope.SetExtra("language_display_name", TelemetryEnvironmentInfo.GetSystemLanguageDisplayName());
    scope.SetExtra("render_mode", TelemetryEnvironmentInfo.GetRenderMode());
    scope.SetExtra("log_file_path", AppLogger.LogFilePath);

    if (includeLogTail)
    {
        var logTail = ReadLogTail(maxLines: 200, maxCharacters: 32_768);
        if (!string.IsNullOrWhiteSpace(logTail))
        {
            scope.SetExtra("log_tail", logTail);
            scope.SetExtra("log_tail_line_count", logTail.Count(character => character == '\n') + 1);
            scope.AddAttachment(
                Encoding.UTF8.GetBytes(logTail),
                "log-tail.txt",
                contentType: "text/plain");
        }
    }
}

关键变更:

  • Tags 只保留用于过滤/索引的核心字段6 个),移除 install_idtelemetry_idos_builddevice_modeldevice_archprocessor_counttotal_memory_mbruntime_versionclr_version 等非索引字段

  • Extras 保留所有详细上下文信息

  • 新增 event_display_name Tag中文显示名

  • 新增 language_display_namerender_mode Extra

  • 移除 IpAddr = AutoIpAddress(配合 SendDefaultPii = false

  • Step 3: 修改 CaptureUnhandledException 方法 — 使用 TelemetryEventNames 常量

将第 107 行:

ApplyCommonScope(scope, source, "unhandled_exception", includeLogTail: true);

改为:

ApplyCommonScope(scope, source, TelemetryEventNames.SentryUnhandledException, includeLogTail: true);
  • Step 4: 修改 CaptureTaskException 方法 — 使用 TelemetryEventNames 常量

将第 139 行:

ApplyCommonScope(scope, source, "task_exception", includeLogTail: true);

改为:

ApplyCommonScope(scope, source, TelemetryEventNames.SentryTaskException, includeLogTail: true);
  • Step 5: 修改 CaptureShutdown 方法 — 使用 TelemetryEventNames 常量

将第 160 行:

ApplyCommonScope(scope, source, "shutdown", includeLogTail: true);

改为:

ApplyCommonScope(scope, source, TelemetryEventNames.SentryShutdown, includeLogTail: true);

同时将第 158 行的硬编码消息:

var eventId = SentrySdk.CaptureMessage("application_shutdown", scope =>

改为:

var eventId = SentrySdk.CaptureMessage(TelemetryEventNames.SentryShutdown, scope =>

Task 4: 修复 PostHogUsageTelemetryService — distinct_id 不一致、Session 生命周期、中文标签、Flush 优化、DescribePlacement 增强

Files:

  • Modify: LanMountainDesktop/Services/PostHogUsageTelemetryService.cs

  • Step 1: 修复 EnsureBaselineEventSent — 统一使用 telemetryId 作为 distinct_id

将第 314 行:

var distinctId = identity.InstallId;

改为:

var distinctId = identity.TelemetryId;

同时将 personProps 中增加 install_id(保留为属性但不再作为 distinct_id

将 personProps 定义(第 314-324 行)改为:

var distinctId = identity.TelemetryId;
var personProps = new Dictionary<string, object?>
{
    ["install_id"] = identity.InstallId,
    ["telemetry_id"] = identity.TelemetryId,
    ["app_version"] = TelemetryEnvironmentInfo.GetAppVersion(),
    ["os_name"] = TelemetryEnvironmentInfo.GetOsName(),
    ["os_version"] = TelemetryEnvironmentInfo.GetOsVersion(),
    ["os_build"] = TelemetryEnvironmentInfo.GetOsBuild(),
    ["device_model"] = TelemetryEnvironmentInfo.GetDeviceModel(),
    ["device_arch"] = TelemetryEnvironmentInfo.GetDeviceArchitecture(),
    ["runtime_version"] = TelemetryEnvironmentInfo.GetRuntimeVersion(),
    ["clr_version"] = TelemetryEnvironmentInfo.GetClrVersion(),
    ["language"] = TelemetryEnvironmentInfo.GetSystemLanguage(),
    ["language_display_name"] = TelemetryEnvironmentInfo.GetSystemLanguageDisplayName(),
    ["render_mode"] = TelemetryEnvironmentInfo.GetRenderMode()
};

同时将 app_first_launch 事件名改为使用常量:

将第 329 行:

"app_first_launch",

改为:

TelemetryEventNames.AppFirstLaunch,
  • Step 2: 修复 CaptureEvent — 添加中文 event_display_name优化环境信息重复

将整个 CaptureEvent 方法(第 436-503 行)替换为:

private void CaptureEvent(
    string eventName,
    IReadOnlyDictionary<string, object?>? payload = null,
    IReadOnlyDictionary<string, object?>? stateBefore = null,
    IReadOnlyDictionary<string, object?>? stateAfter = null,
    bool forceFlush = false)
{
    if (!_isInitialized || !_isUsageEnabled || !_sessionActive)
    {
        return;
    }

    var identity = TelemetryIdentityService.Instance;
    var distinctId = identity.TelemetryId;
    var seq = Interlocked.Increment(ref _sequence);

    var properties = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
    {
        ["install_id"] = identity.InstallId,
        ["telemetry_id"] = identity.TelemetryId,
        ["session_id"] = _sessionId,
        ["sequence"] = seq,
        ["timestamp_utc"] = DateTimeOffset.UtcNow.ToString("o"),
        ["event_display_name"] = TelemetryEventNames.DisplayName(eventName)
    };

    if (payload is not null)
    {
        foreach (var kvp in payload)
        {
            properties[kvp.Key] = kvp.Value;
        }
    }

    if (stateBefore is not null && stateBefore.Count > 0)
    {
        foreach (var kvp in stateBefore)
        {
            properties[$"state_before_{kvp.Key}"] = kvp.Value;
        }
    }

    if (stateAfter is not null && stateAfter.Count > 0)
    {
        foreach (var kvp in stateAfter)
        {
            properties[$"state_after_{kvp.Key}"] = kvp.Value;
        }
    }

    _client.Capture(
        distinctId,
        eventName,
        properties,
        groups: null,
        sendFeatureFlags: false);

    if (forceFlush)
    {
        _ = _client.FlushAsync();
    }
}

关键变更:

  • 移除每个事件中重复的 app_versionos_nameos_versiondevice_modeldevice_archruntime_versionlanguage(这些已通过 Identify 设置为 person properties

  • 添加 event_display_name 属性(中文显示名)

  • 移除 payload_ 前缀payload 属性直接使用原始 key

  • Step 3: 修复 StartSession — 使用 TelemetryEventNames 常量,移除重复环境信息

将 StartSession 方法中的 CaptureEvent 调用(第 362-378 行)改为:

CaptureEvent(
    TelemetryEventNames.AppSessionStart,
    new Dictionary<string, object?>
    {
        ["source"] = source,
        ["launch_id"] = _launchId,
        ["session_start_utc"] = _sessionStartUtc.ToString("o"),
        ["local_hour"] = _sessionStartUtc.ToLocalTime().Hour,
        ["day_part"] = TelemetryEnvironmentInfo.GetLocalDayPart(_sessionStartUtc),
        ["timezone"] = TimeZoneInfo.Local.Id
    },
    forceFlush: true);
  • Step 4: 修复 EndSession — 使用 TelemetryEventNames 常量

将 EndSession 方法中的 CaptureEvent 调用(第 393-404 行)改为:

CaptureEvent(
    TelemetryEventNames.AppSessionEnd,
    new Dictionary<string, object?>
    {
        ["source"] = source,
        ["launch_id"] = _launchId,
        ["session_start_utc"] = _sessionStartUtc.ToString("o"),
        ["session_end_utc"] = endUtc.ToString("o"),
        ["duration_ms"] = durationMs,
        ["is_restart"] = isRestart
    },
    forceFlush: true);
  • Step 5: 修改所有 Track 方法 — 使用 TelemetryEventNames 常量,移除 payload_ 前缀影响*

将所有 Track 方法中的硬编码事件名替换为常量引用:

TrackMainWindowOpened(第 105-114 行):

public void TrackMainWindowOpened(string source, bool isVisible, string windowState)
{
    CaptureEvent(
        TelemetryEventNames.MainWindowOpened,
        new Dictionary<string, object?>
        {
            ["source"] = source,
            ["is_visible"] = isVisible,
            ["window_state"] = windowState
        },
        forceFlush: true);
}

TrackMainWindowClosed(第 116-127 行):

public void TrackMainWindowClosed(string source, bool wasVisible, string windowState)
{
    CaptureEvent(
        TelemetryEventNames.MainWindowClosed,
        new Dictionary<string, object?>
        {
            ["source"] = source,
            ["was_visible"] = wasVisible,
            ["window_state"] = windowState
        },
        forceFlush: true);
}

TrackSettingsWindowOpened(第 129-139 行):

public void TrackSettingsWindowOpened(string source, string? currentPageId)
{
    CaptureEvent(
        TelemetryEventNames.SettingsWindowOpened,
        new Dictionary<string, object?>
        {
            ["source"] = source,
            ["current_page_id"] = currentPageId
        },
        forceFlush: true);
}

TrackSettingsWindowClosed(第 141-151 行):

public void TrackSettingsWindowClosed(string source, string? currentPageId)
{
    CaptureEvent(
        TelemetryEventNames.SettingsWindowClosed,
        new Dictionary<string, object?>
        {
            ["source"] = source,
            ["current_page_id"] = currentPageId
        },
        forceFlush: true);
}

TrackSettingsNavigation(第 153-165 行):

public void TrackSettingsNavigation(string? fromPageId, string? toPageId, string source)
{
    CaptureEvent(
        TelemetryEventNames.SettingsNavigation,
        new Dictionary<string, object?>
        {
            ["source"] = source,
            ["from_page_id"] = fromPageId,
            ["to_page_id"] = toPageId
        },
        stateBefore: CreatePageState(fromPageId),
        stateAfter: CreatePageState(toPageId));
}

TrackSettingsDrawerOpened(第 167-177 行):

public void TrackSettingsDrawerOpened(string? pageId, string? drawerTitle)
{
    CaptureEvent(
        TelemetryEventNames.SettingsDrawerOpened,
        new Dictionary<string, object?>
        {
            ["page_id"] = pageId,
            ["drawer_title"] = drawerTitle
        },
        forceFlush: true);
}

TrackSettingsDrawerClosed(第 179-189 行):

public void TrackSettingsDrawerClosed(string? pageId, string? drawerTitle)
{
    CaptureEvent(
        TelemetryEventNames.SettingsDrawerClosed,
        new Dictionary<string, object?>
        {
            ["page_id"] = pageId,
            ["drawer_title"] = drawerTitle
        },
        forceFlush: true);
}

TrackDesktopComponentPlaced(第 191-201 行):

public void TrackDesktopComponentPlaced(DesktopComponentPlacementSnapshot placement, string source)
{
    CaptureEvent(
        TelemetryEventNames.DesktopComponentPlaced,
        new Dictionary<string, object?>
        {
            ["source"] = source
        },
        stateAfter: DescribePlacement(placement),
        forceFlush: true);
}

TrackDesktopComponentMoved(第 203-217 行):

public void TrackDesktopComponentMoved(
    DesktopComponentPlacementSnapshot before,
    DesktopComponentPlacementSnapshot after,
    string source)
{
    CaptureEvent(
        TelemetryEventNames.DesktopComponentMoved,
        new Dictionary<string, object?>
        {
            ["source"] = source
        },
        stateBefore: DescribePlacement(before),
        stateAfter: DescribePlacement(after),
        forceFlush: true);
}

TrackDesktopComponentResized(第 219-233 行):

public void TrackDesktopComponentResized(
    DesktopComponentPlacementSnapshot before,
    DesktopComponentPlacementSnapshot after,
    string source)
{
    CaptureEvent(
        TelemetryEventNames.DesktopComponentResized,
        new Dictionary<string, object?>
        {
            ["source"] = source
        },
        stateBefore: DescribePlacement(before),
        stateAfter: DescribePlacement(after),
        forceFlush: true);
}

TrackDesktopComponentDeleted(第 235-245 行):

public void TrackDesktopComponentDeleted(DesktopComponentPlacementSnapshot before, string source)
{
    CaptureEvent(
        TelemetryEventNames.DesktopComponentDeleted,
        new Dictionary<string, object?>
        {
            ["source"] = source
        },
        stateBefore: DescribePlacement(before),
        forceFlush: true);
}

TrackDesktopComponentEditorOpened(第 247-257 行):

public void TrackDesktopComponentEditorOpened(DesktopComponentPlacementSnapshot placement, string source)
{
    CaptureEvent(
        TelemetryEventNames.DesktopComponentEditorOpened,
        new Dictionary<string, object?>
        {
            ["source"] = source
        },
        stateBefore: DescribePlacement(placement),
        forceFlush: true);
}
  • Step 6: 增强 DescribePlacement — 添加 component_name

DescribePlacement 方法(第 513-525 行)改为:

private static IReadOnlyDictionary<string, object?> DescribePlacement(DesktopComponentPlacementSnapshot placement)
{
    return new Dictionary<string, object?>
    {
        ["placement_id"] = placement.PlacementId,
        ["component_id"] = placement.ComponentId,
        ["component_name"] = placement.ComponentName ?? placement.ComponentId,
        ["page_index"] = placement.PageIndex,
        ["row"] = placement.Row,
        ["column"] = placement.Column,
        ["width_cells"] = placement.WidthCells,
        ["height_cells"] = placement.HeightCells
    };
}

注意:这要求 DesktopComponentPlacementSnapshotComponentName 属性。如果不存在,需要在 DesktopComponentPlacementSnapshot.cs 中添加:

public string ComponentName { get; set; } = string.Empty;

并在创建 placement snapshot 的地方(ClonePlacementSnapshot 方法等)填充该字段。

  • Step 7: 优化 Flush 策略 — 仅关键事件 forceFlush

将以下 Track 方法的 forceFlush: true 改为 forceFlush: false(仅保留 session 和 first_launch 的 forceFlush

  • TrackMainWindowOpenedforceFlush: false
  • TrackMainWindowClosedforceFlush: false
  • TrackSettingsWindowOpenedforceFlush: false
  • TrackSettingsWindowClosedforceFlush: false
  • TrackSettingsDrawerOpenedforceFlush: false
  • TrackSettingsDrawerClosedforceFlush: false
  • TrackDesktopComponentPlacedforceFlush: false
  • TrackDesktopComponentMovedforceFlush: false
  • TrackDesktopComponentResizedforceFlush: false
  • TrackDesktopComponentDeletedforceFlush: false
  • TrackDesktopComponentEditorOpenedforceFlush: false

保留 forceFlush: true 的:

  • StartSessionapp_session_start
  • EndSessionapp_session_end
  • EnsureBaselineEventSentapp_first_launch

Task 5: 修复 Session 生命周期 — MainWindow 和 App 层调用

Files:

  • Modify: LanMountainDesktop/Views/MainWindow.axaml.cs

  • Modify: LanMountainDesktop/App.axaml.cs

  • Step 1: 在 MainWindow.OnOpened 中添加 TrackSessionStarted 调用

MainWindow.axaml.csOnOpened 方法中,在 TrackMainWindowOpened 调用之后(约第 519 行),添加:

TelemetryServices.Usage?.TrackSessionStarted("MainWindow.OnOpened");
  • Step 2: 在 App.PerformExitCleanup 中确保 TrackSessionEnded 被调用

App.axaml.csPerformExitCleanup 方法中,在 TelemetryServices.Usage?.Shutdown(...) 调用之前(约第 1202 行),添加:

TelemetryServices.Usage?.TrackSessionEnded("App.PerformExitCleanup");

Task 6: 为 DesktopComponentPlacementSnapshot 添加 ComponentName 属性

Files:

  • Modify: LanMountainDesktop/Models/DesktopComponentPlacementSnapshot.cs

  • Step 1: 添加 ComponentName 属性

DesktopComponentPlacementSnapshot.cs 中,在 ComponentId 属性之后添加:

public string ComponentName { get; set; } = string.Empty;
  • Step 2: 搜索所有 ClonePlacementSnapshot 方法,确保 ComponentName 被正确填充

MainWindow.ComponentSystem.csMainWindow.DesktopEditing.cs 中的 ClonePlacementSnapshot 方法里,需要确保 ComponentName 被赋值。搜索项目中所有 ClonePlacementSnapshot 的实现,在克隆时同时复制 ComponentName 字段。


Task 7: 构建验证

  • Step 1: 执行 dotnet build 确保编译通过

Run: dotnet build LanMountainDesktop.slnx -c Debug

Expected: Build succeeded, 0 errors

  • Step 2: 执行 dotnet test 确保测试通过

Run: dotnet test LanMountainDesktop.slnx -c Debug

Expected: All tests pass