diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index b023a02..57d7a80 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -47,19 +47,15 @@ namespace WelsonJS.Launcher ); // telemetry - bool telemetryEnabled = string.Equals(GetAppConfig("TelemetryEnabled"), "true", StringComparison.OrdinalIgnoreCase); - if (!telemetryEnabled) + string telemetryProvider = GetAppConfig("TelemetryProvider"); + var telemetryOptions = new TelemetryOptions { - string telemetryProvider = GetAppConfig("TelemetryProvider"); - var telemetryOptions = new TelemetryOptions - { - ApiKey = GetAppConfig("TelemetryApiKey"), - BaseUrl = GetAppConfig("TelemetryBaseUrl"), - DistinctId = Environment.MachineName, - Disabled = !telemetryEnabled - }; - _telemetryClient = new TelemetryClient(telemetryProvider, telemetryOptions, _logger); - } + ApiKey = GetAppConfig("TelemetryApiKey"), + BaseUrl = GetAppConfig("TelemetryBaseUrl"), + DistinctId = TelemetryIdentity.GetDistinctId(), + Disabled = !string.Equals(GetAppConfig("TelemetryEnabled"), "true", StringComparison.OrdinalIgnoreCase) + }; + _telemetryClient = new TelemetryClient(telemetryProvider, telemetryOptions, _logger); } [STAThread] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs index 1924ae7..ea445eb 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs @@ -22,11 +22,14 @@ namespace WelsonJS.Launcher.Telemetry { _options = options ?? throw new ArgumentNullException(nameof(options)); - if (string.IsNullOrWhiteSpace(_options.ApiKey)) - throw new ArgumentException("PostHog API key is missing."); + if (!_options.Disabled) + { + if (string.IsNullOrWhiteSpace(_options.ApiKey)) + throw new ArgumentException("PostHog API key is missing."); - if (string.IsNullOrWhiteSpace(_options.BaseUrl)) - throw new ArgumentException("PostHog BaseUrl is missing."); + if (string.IsNullOrWhiteSpace(_options.BaseUrl)) + throw new ArgumentException("PostHog BaseUrl is missing."); + } if (string.IsNullOrWhiteSpace(_options.DistinctId)) _options.DistinctId = $"anon-{Guid.NewGuid():N}"; diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryIdentity.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryIdentity.cs new file mode 100644 index 0000000..68852bb --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryIdentity.cs @@ -0,0 +1,145 @@ +// TelemetryIdentity.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; +using System.Collections.Generic; +using System.Management; +using System.Security.Cryptography; +using System.Text; + +namespace WelsonJS.Launcher.Telemetry +{ + public static class TelemetryIdentity + { + /// + /// Collects multiple hardware/OS identity sources (BIOS UUID, OS SerialNumber, MachineName), + /// joins them with ',' and computes SHA-256 hash, then returns the first 16 hex characters + /// as a compact Distinct ID. + /// + public static string GetDistinctId() + { + var sources = new List(); + + string biosUuid = GetBiosUuid(); + if (!string.IsNullOrEmpty(biosUuid)) + sources.Add(biosUuid); + + string osUuid = GetOsUuid(); + if (!string.IsNullOrEmpty(osUuid)) + sources.Add(osUuid); + + string machineName = GetMachineName(); + if (!string.IsNullOrEmpty(machineName)) + sources.Add(machineName); + + string raw = string.Join(",", sources); + string hash = ComputeSha256(raw); + + if (!string.IsNullOrEmpty(hash) && hash.Length >= 16) + return hash.Substring(0, 16); + + return hash; + } + + private static string GetMachineName() + { + try { return Environment.MachineName ?? ""; } + catch { return ""; } + } + + /// + /// Retrieves BIOS UUID from Win32_ComputerSystemProduct. + /// Filters out invalid values (all zeros or all 'F'). + /// + private static string GetBiosUuid() + { + try + { + using (var searcher = + new ManagementObjectSearcher("SELECT UUID FROM Win32_ComputerSystemProduct")) + { + foreach (ManagementObject obj in searcher.Get()) + { + string uuid = obj["UUID"] as string; + if (string.IsNullOrEmpty(uuid)) + return null; + + uuid = uuid.Trim(); + string compact = uuid.Replace("-", "").ToUpperInvariant(); + + // Exclude invalid dummy UUIDs such as all 0s or all Fs + if (IsAllChar(compact, '0') || IsAllChar(compact, 'F')) + return null; + + return uuid; + } + } + } + catch { } + + return null; + } + + /// + /// Retrieves OS-level UUID equivalent using Win32_OperatingSystem.SerialNumber. + /// This value is unique per Windows installation. + /// + private static string GetOsUuid() + { + try + { + using (var searcher = + new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_OperatingSystem")) + { + foreach (ManagementObject obj in searcher.Get()) + { + string serial = obj["SerialNumber"] as string; + if (!string.IsNullOrEmpty(serial)) + return serial.Trim(); + } + } + } + catch { } + + return null; + } + + private static bool IsAllChar(string s, char c) + { + if (string.IsNullOrEmpty(s)) return false; + for (int i = 0; i < s.Length; i++) + if (s[i] != c) return false; + return true; + } + + /// + /// Computes SHA-256 hex string for the given input text. + /// Returns null if hashing fails. + /// + private static string ComputeSha256(string input) + { + if (input == null) input = ""; + + try + { + using (var sha = SHA256.Create()) + { + var data = Encoding.UTF8.GetBytes(input); + var digest = sha.ComputeHash(data); + + var sb = new StringBuilder(digest.Length * 2); + for (int i = 0; i < digest.Length; i++) + sb.Append(digest[i].ToString("x2")); + + return sb.ToString(); + } + } + catch + { + return null; + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index d2deefc..a656599 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -80,6 +80,7 @@ + @@ -131,6 +132,7 @@ +