diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index bad2a37..2ed4b5d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading; using System.Windows.Forms; +using WelsonJS.Launcher.Telemetry; namespace WelsonJS.Launcher { @@ -21,6 +22,7 @@ namespace WelsonJS.Launcher public static Mutex _mutex; public static ResourceServer _resourceServer; public static string _dateTimeFormat; + public static TelemetryClient _telemetryClient; static Program() { @@ -32,10 +34,10 @@ namespace WelsonJS.Launcher // load native libraries string appDataSubDirectory = "WelsonJS"; - bool requireSigned = string.Equals( - GetAppConfig("NativeRequireSigned"), - "true", - StringComparison.OrdinalIgnoreCase); + bool requireSigned = string.Equals( + GetAppConfig("NativeRequireSigned"), + "true", + StringComparison.OrdinalIgnoreCase); NativeBootstrap.Init( dllNames: new[] { "ChakraCore.dll" }, @@ -43,6 +45,35 @@ namespace WelsonJS.Launcher logger: _logger, requireSigned: requireSigned ); + + // telemetry + try + { + var telemetryProvider = GetAppConfig("TelemetryProvider"); + var telemetryOptions = new TelemetryOptions + { + ApiKey = GetAppConfig("TelemetryApiKey"), + BaseUrl = GetAppConfig("TelemetryBaseUrl"), + DistinctId = TelemetryIdentity.GetDistinctId(), + Disabled = !string.Equals( + GetAppConfig("TelemetryEnabled"), + "true", + StringComparison.OrdinalIgnoreCase) + }; + + if (!telemetryOptions.Disabled && + !string.IsNullOrWhiteSpace(telemetryProvider) && + !string.IsNullOrWhiteSpace(telemetryOptions.ApiKey) && + !string.IsNullOrWhiteSpace(telemetryOptions.BaseUrl)) + { + _telemetryClient = new TelemetryClient(telemetryProvider, telemetryOptions, _logger); + } + } + catch (Exception ex) + { + _logger.Error($"Telemetry initialization failed: {ex}"); + _telemetryClient = null; + } } [STAThread] @@ -70,6 +101,13 @@ namespace WelsonJS.Launcher return; } + // send event to the telemetry server + if (_telemetryClient != null) + { + var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; + _ = _telemetryClient.TrackAppStartedAsync("WelsonJS.Launcher", version); + } + // draw the main form Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/AssemblyInfo.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/AssemblyInfo.cs index 8b48616..520bc4d 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/AssemblyInfo.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // 모든 값을 지정하거나 아래와 같이 '*'를 사용하여 빌드 번호 및 수정 번호를 // 기본값으로 할 수 있습니다. // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.2.7.55")] -[assembly: AssemblyFileVersion("0.2.7.55")] +[assembly: AssemblyVersion("0.2.7.58")] +[assembly: AssemblyFileVersion("0.2.7.58")] diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index d329414..6ef92bc 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs @@ -378,6 +378,42 @@ namespace WelsonJS.Launcher.Properties { } } + /// + /// phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string TelemetryApiKey { + get { + return ResourceManager.GetString("TelemetryApiKey", resourceCulture); + } + } + + /// + /// https://us.i.posthog.com과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string TelemetryBaseUrl { + get { + return ResourceManager.GetString("TelemetryBaseUrl", resourceCulture); + } + } + + /// + /// true과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string TelemetryEnabled { + get { + return ResourceManager.GetString("TelemetryEnabled", resourceCulture); + } + } + + /// + /// posthog과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string TelemetryProvider { + get { + return ResourceManager.GetString("TelemetryProvider", resourceCulture); + } + } + /// /// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다. /// diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx index 7397892..0f32638 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.resx @@ -229,4 +229,16 @@ ..\Resources\icon_editor_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8 + + + https://us.i.posthog.com + + + posthog + + + true + \ No newline at end of file diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/ITelemetryProvider.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/ITelemetryProvider.cs new file mode 100644 index 0000000..40abe54 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/ITelemetryProvider.cs @@ -0,0 +1,20 @@ +// ITelemetryProvider.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace WelsonJS.Launcher.Telemetry +{ + public interface ITelemetryProvider + { + Task TrackEventAsync( + string eventName, + IDictionary properties = null, + CancellationToken cancellationToken = default + ); + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs new file mode 100644 index 0000000..5240b55 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs @@ -0,0 +1,94 @@ +// PosthogTelemetryProvider.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.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace WelsonJS.Launcher.Telemetry +{ + public sealed class PosthogTelemetryProvider : ITelemetryProvider, IDisposable + { + private readonly TelemetryOptions _options; + private readonly HttpClient _httpClient; + private readonly ICompatibleLogger _logger; + private bool _disposed; + + public PosthogTelemetryProvider(TelemetryOptions options, ICompatibleLogger logger = null) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + + 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.DistinctId)) + _options.DistinctId = $"anon-{Guid.NewGuid():N}"; + + _httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; + _logger = logger; + } + + public async Task TrackEventAsync( + string eventName, + IDictionary properties = null, + CancellationToken cancellationToken = default) + { + if (_disposed) throw new ObjectDisposedException(nameof(PosthogTelemetryProvider)); + if (_options.Disabled) return; + + if (string.IsNullOrWhiteSpace(eventName)) + return; + + string json; + using (var ser = new JsSerializer()) + { + var payload = new Dictionary + { + ["api_key"] = _options.ApiKey, + ["distinct_id"] = _options.DistinctId, + ["event"] = eventName, + ["properties"] = properties ?? new Dictionary() + }; + + json = ser.Serialize(payload, 0); + } + + var url = $"{_options.BaseUrl.TrimEnd('/')}/i/v0/e"; + + try + { + using (var response = await _httpClient.PostAsync( + url, + new StringContent(json, Encoding.UTF8, "application/json"), + cancellationToken + )) + { + response.EnsureSuccessStatusCode(); + } + } + catch (Exception ex) + { + // Log and swallow for fire-and-forget telemetry + _logger?.Error($"Failed to send telemetry event '{eventName}': {ex.Message}"); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + _httpClient.Dispose(); + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryClient.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryClient.cs new file mode 100644 index 0000000..9fb1de7 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryClient.cs @@ -0,0 +1,49 @@ +// TelemetryClient.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.Threading; +using System.Threading.Tasks; + +namespace WelsonJS.Launcher.Telemetry +{ + public sealed class TelemetryClient : IDisposable + { + private readonly ITelemetryProvider _provider; + + public TelemetryClient(string providerName, TelemetryOptions options, ICompatibleLogger logger = null) + { + _provider = TelemetryProviderFactory.Create(providerName, options, logger); + } + + public Task TrackEventAsync( + string eventName, + IDictionary properties = null, + CancellationToken cancellationToken = default) + { + return _provider.TrackEventAsync(eventName, properties, cancellationToken); + } + + public Task TrackAppStartedAsync(string appName, string appVersion) + { + var props = new Dictionary + { + { "app_name", appName }, + { "app_version", appVersion }, + { "os_platform", Environment.OSVersion.Platform.ToString() }, + { "os_version", Environment.OSVersion.Version.ToString() }, + { "timestamp_utc", DateTime.UtcNow.ToString("o") } + }; + + return TrackEventAsync("app_started", props); + } + + public void Dispose() + { + (_provider as IDisposable)?.Dispose(); + } + } +} 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/Telemetry/TelemetryOptions.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryOptions.cs new file mode 100644 index 0000000..4b057a9 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryOptions.cs @@ -0,0 +1,15 @@ +// TelemetryOptions.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +namespace WelsonJS.Launcher.Telemetry +{ + public class TelemetryOptions + { + public string ApiKey { get; set; } + public string BaseUrl { get; set; } + public string DistinctId { get; set; } + public bool Disabled { get; set; } = false; + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryProviderFactory.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryProviderFactory.cs new file mode 100644 index 0000000..3454e15 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryProviderFactory.cs @@ -0,0 +1,34 @@ +// TelemetryProviderFactory.cs +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2025 Catswords OSS and WelsonJS Contributors +// https://github.com/gnh1201/welsonjs +// +using System; + +namespace WelsonJS.Launcher.Telemetry +{ + public static class TelemetryProviderFactory + { + public static ITelemetryProvider Create(string provider, TelemetryOptions options, ICompatibleLogger logger = null) + { + if (options == null) + throw new ArgumentNullException(nameof(options)); + + if (provider == null) + throw new ArgumentNullException(nameof(provider)); + + provider = provider.ToLowerInvariant(); + + switch (provider) + { + case "posthog": + return new PosthogTelemetryProvider(options, logger); + + default: + throw new NotSupportedException( + "Unknown telemetry provider: " + provider + ); + } + } + } +} diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj index 5bc8b61..a656599 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -80,6 +80,7 @@ + @@ -128,6 +129,12 @@ GlobalSettingsForm.cs + + + + + + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index b74b7fe..617e7c7 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -26,6 +26,10 @@ + + + +