Files
community/Ink Canvas/Helpers/TelemetryUploader.cs
T

210 lines
8.1 KiB
C#
Raw Normal View History

2026-03-14 17:35:55 +08:00
using Sentry;
2026-02-06 23:12:54 +08:00
using System;
using System.IO;
2026-03-14 17:35:55 +08:00
using System.Linq;
2026-02-06 23:12:54 +08:00
using System.Reflection;
2026-03-14 17:35:55 +08:00
using System.Text.RegularExpressions;
2026-02-06 23:12:54 +08:00
using System.Threading.Tasks;
namespace Ink_Canvas.Helpers
{
internal static class TelemetryUploader
{
2026-03-14 17:35:55 +08:00
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);
2026-02-06 23:12:54 +08:00
public static Task UploadTelemetryIfNeededAsync()
{
return Task.Run(() =>
{
try
{
var settings = MainWindow.Settings;
if (settings == null || settings.Startup == null)
{
return;
}
var level = settings.Startup.TelemetryUploadLevel;
if (level == TelemetryUploadLevel.None)
{
2026-03-14 17:35:55 +08:00
return;
2026-02-06 23:12:54 +08:00
}
string deviceId = DeviceIdentifier.GetDeviceId();
if (string.IsNullOrWhiteSpace(deviceId) || deviceId.Length < 5)
{
LogHelper.WriteLogToFile("TelemetryUploader | 设备ID无效,取消遥测上传", LogHelper.LogType.Warning);
return;
}
2026-03-14 17:35:55 +08:00
// Basic 和 Extended 均上传崩溃日志(脱敏)
object crashFile = TryGetLatestSanitizedFile(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Crashes"),
"Crash_*.txt",
"崩溃日志");
// Extended 额外上传运行日志(脱敏)
object runtimeLogFile = null;
2026-02-06 23:12:54 +08:00
if (level == TelemetryUploadLevel.Extended)
{
2026-03-14 17:35:55 +08:00
runtimeLogFile = TryGetLatestSanitizedFile(
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"),
"Log_*.txt",
"运行日志");
2026-02-06 23:12:54 +08:00
}
2026-02-07 12:43:03 +08:00
var telemetryData = new
{
telemetry_level = level.ToString(),
device_id = deviceId,
update_channel = settings.Startup.UpdateChannel.ToString(),
app_version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
os_version = Environment.OSVersion.VersionString,
2026-03-14 17:35:55 +08:00
has_crash_log = crashFile != null,
has_runtime_log = runtimeLogFile != null
2026-02-07 12:43:03 +08:00
};
2026-03-14 17:58:01 +08:00
// 通过 Sentry 上报一个包含遥测信息的事件
2026-02-07 12:43:03 +08:00
string userName = Environment.UserName;
SentrySdk.ConfigureScope(scope =>
{
scope.User = new SentryUser
{
Id = deviceId,
Username = userName,
2026-03-03 16:04:20 +08:00
Email = $"{userName}",
2026-02-07 12:43:03 +08:00
IpAddress = "{{auto}}"
};
});
2026-02-06 23:12:54 +08:00
var evt = new SentryEvent
{
Message = "ICC CE Telemetry",
Level = SentryLevel.Info
};
2026-02-07 12:43:03 +08:00
evt.User = new SentryUser
{
Id = deviceId,
Username = userName,
2026-03-03 16:04:20 +08:00
Email = $"{userName}",
2026-02-07 12:43:03 +08:00
IpAddress = "{{auto}}"
};
2026-02-06 23:12:54 +08:00
evt.SetTag("telemetry_level", level.ToString());
evt.SetTag("device_id", deviceId);
2026-02-06 23:36:57 +08:00
evt.SetTag("update_channel", settings.Startup.UpdateChannel.ToString());
2026-02-06 23:12:54 +08:00
evt.SetTag("app_version", Assembly.GetExecutingAssembly().GetName().Version.ToString());
evt.SetTag("os_version", Environment.OSVersion.VersionString);
2026-03-03 16:04:20 +08:00
evt.SetExtra("telemetry_data", telemetryData);
2026-02-06 23:12:54 +08:00
2026-03-14 17:35:55 +08:00
if (crashFile != null)
2026-02-06 23:12:54 +08:00
{
2026-03-14 17:35:55 +08:00
evt.SetExtra("crash_file", crashFile);
}
if (runtimeLogFile != null)
{
evt.SetExtra("runtime_log_file", runtimeLogFile);
2026-02-06 23:12:54 +08:00
}
SentrySdk.CaptureEvent(evt);
LogHelper.WriteLogToFile("TelemetryUploader | 遥测数据已通过 Sentry 上报", LogHelper.LogType.Event);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"TelemetryUploader | 遥测上传失败: {ex.Message}", LogHelper.LogType.Warning);
}
});
}
2026-03-14 17:35:55 +08:00
private static object TryGetLatestSanitizedFile(string directory, string pattern, string fileType)
{
try
{
if (!Directory.Exists(directory))
{
return null;
}
2026-02-06 23:12:54 +08:00
2026-03-14 17:35:55 +08:00
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;
}
}
}