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 @@
+