improve:遥测

This commit is contained in:
2026-03-14 17:35:55 +08:00
parent bea92f3483
commit 3806454b18
+114 -72
View File
@@ -1,21 +1,47 @@
using Sentry;
using Sentry;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
/// <summary>
/// 遥测上传:根据用户设置,通过 Sentry 上传 usage_stats 和 Crashes 目录的摘要信息。
/// </summary>
internal static class TelemetryUploader
{
/// <summary>
/// 根据当前设置决定是否上传遥测数据。
/// 在主窗口加载完成后调用一次即可。
/// </summary>
private static readonly Regex EmailRegex = new Regex(
@"(?i)\b[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}\b",
RegexOptions.Compiled);
private static readonly Regex PhoneRegex = new Regex(
@"\b1[3-9]\d{9}\b",
RegexOptions.Compiled);
private static readonly Regex IPv4Regex = new Regex(
@"\b(?:\d{1,3}\.){3}\d{1,3}\b",
RegexOptions.Compiled);
private static readonly Regex WindowsPathRegex = new Regex(
@"\b[A-Za-z]:\\[^\s<>|]+\b",
RegexOptions.Compiled);
private static readonly Regex UncPathRegex = new Regex(
@"\\\\[^\s]+",
RegexOptions.Compiled);
private static readonly Regex KeyValueSecretRegex = new Regex(
@"(?i)(\b(?:access[_-]?token|refresh[_-]?token|token|password|passwd|pwd|secret|authorization)\b\s*[:=]\s*)([^\s,;]+)",
RegexOptions.Compiled);
private static readonly Regex JsonSecretRegex = new Regex(
"(?i)(\"(?:access_token|refresh_token|token|password|passwd|pwd|secret|authorization)\"\\s*:\\s*\")([^\"]*)(\")",
RegexOptions.Compiled);
private static readonly Regex UrlSecretRegex = new Regex(
@"(?i)([?&](?:access_token|token|password|pwd|secret)=)[^&\s]+",
RegexOptions.Compiled);
public static Task UploadTelemetryIfNeededAsync()
{
return Task.Run(() =>
@@ -31,10 +57,9 @@ namespace Ink_Canvas.Helpers
var level = settings.Startup.TelemetryUploadLevel;
if (level == TelemetryUploadLevel.None)
{
return; // 用户未开启
return;
}
// 获取并校验设备ID
string deviceId = DeviceIdentifier.GetDeviceId();
if (string.IsNullOrWhiteSpace(deviceId) || deviceId.Length < 5)
{
@@ -42,65 +67,20 @@ namespace Ink_Canvas.Helpers
return;
}
// 可选:Crashes 目录下的崩溃日志
List<object> crashFiles = null;
// Basic 和 Extended 均上传崩溃日志(脱敏)
object crashFile = TryGetLatestSanitizedFile(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Crashes"),
"Crash_*.txt",
"崩溃日志");
// Extended 额外上传运行日志(脱敏)
object runtimeLogFile = null;
if (level == TelemetryUploadLevel.Extended)
{
try
{
string crashDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Crashes");
if (Directory.Exists(crashDir))
{
crashFiles = new List<object>();
var files = Directory.GetFiles(crashDir);
FileInfo latestInfo = null;
string latestContent = null;
foreach (var file in files)
{
try
{
var info = new FileInfo(file);
// 避免一次上传过大,单文件限制为 8192KB
if (info.Length > 8192 * 1024)
{
continue;
}
string content = File.ReadAllText(file);
if (latestInfo == null || info.LastWriteTime > latestInfo.LastWriteTime)
{
latestInfo = info;
latestContent = content;
}
}
catch (Exception exFile)
{
LogHelper.WriteLogToFile(
$"TelemetryUploader | 读取崩溃日志失败: {exFile.Message}",
LogHelper.LogType.Warning);
}
}
if (latestInfo != null && latestContent != null)
{
crashFiles.Add(new
{
file_name = latestInfo.Name,
content = latestContent
});
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile(
$"TelemetryUploader | 收集崩溃日志失败: {ex.Message}",
LogHelper.LogType.Warning);
}
runtimeLogFile = TryGetLatestSanitizedFile(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"),
"Log_*.txt",
"运行日志");
}
var telemetryData = new
@@ -110,10 +90,11 @@ namespace Ink_Canvas.Helpers
update_channel = settings.Startup.UpdateChannel.ToString(),
app_version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
os_version = Environment.OSVersion.VersionString,
crash_files = crashFiles
has_crash_log = crashFile != null,
has_runtime_log = runtimeLogFile != null
};
// 通过 Sentry 上报一个包含遥测信息的事件
// 按你的要求保留现有用户字段行为
string userName = Environment.UserName;
SentrySdk.ConfigureScope(scope =>
{
@@ -147,9 +128,14 @@ namespace Ink_Canvas.Helpers
evt.SetTag("os_version", Environment.OSVersion.VersionString);
evt.SetExtra("telemetry_data", telemetryData);
if (crashFiles != null)
if (crashFile != null)
{
evt.SetExtra("crash_files", crashFiles);
evt.SetExtra("crash_file", crashFile);
}
if (runtimeLogFile != null)
{
evt.SetExtra("runtime_log_file", runtimeLogFile);
}
SentrySdk.CaptureEvent(evt);
@@ -161,7 +147,63 @@ namespace Ink_Canvas.Helpers
}
});
}
private static object TryGetLatestSanitizedFile(string directory, string pattern, string fileType)
{
try
{
if (!Directory.Exists(directory))
{
return null;
}
var latest = new DirectoryInfo(directory)
.GetFiles(pattern)
.OrderByDescending(file => file.LastWriteTime)
.FirstOrDefault();
if (latest == null)
{
return null;
}
string content = File.ReadAllText(latest.FullName);
string sanitizedContent = SanitizeLogContent(content);
return new
{
file_type = fileType,
file_name = latest.Name,
last_write_time = latest.LastWriteTime.ToString("o"),
content = sanitizedContent
};
}
catch (Exception ex)
{
LogHelper.WriteLogToFile(
$"TelemetryUploader | 收集{fileType}失败: {ex.Message}",
LogHelper.LogType.Warning);
return null;
}
}
private static string SanitizeLogContent(string content)
{
if (string.IsNullOrEmpty(content))
{
return content;
}
string sanitized = content;
sanitized = EmailRegex.Replace(sanitized, "[REDACTED_EMAIL]");
sanitized = PhoneRegex.Replace(sanitized, "[REDACTED_PHONE]");
sanitized = IPv4Regex.Replace(sanitized, "[REDACTED_IP]");
sanitized = WindowsPathRegex.Replace(sanitized, "[REDACTED_PATH]");
sanitized = UncPathRegex.Replace(sanitized, "[REDACTED_PATH]");
sanitized = UrlSecretRegex.Replace(sanitized, "$1[REDACTED]");
sanitized = KeyValueSecretRegex.Replace(sanitized, "$1[REDACTED]");
sanitized = JsonSecretRegex.Replace(sanitized, "$1[REDACTED]$3");
return sanitized;
}
}
}