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