diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Program.cs index bad2a37..4518917 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,17 @@ namespace WelsonJS.Launcher logger: _logger, requireSigned: requireSigned ); + + // telemetry + var telemetryProvider = GetAppConfig("TelemetryProvider"); + var telemetryOptions = new TelemetryOptions + { + ApiKey = GetAppConfig("TelemetryApiKey"), + BaseUrl = GetAppConfig("TelemetryBaseUrl"), + DistinctId = Environment.MachineName, + Disabled = string.Equals(GetAppConfig("TelemetryDisabled"), "true", StringComparison.OrdinalIgnoreCase) + }; + _telemetryClient = new TelemetryClient(telemetryProvider, telemetryOptions, _logger); } [STAThread] @@ -70,6 +83,9 @@ namespace WelsonJS.Launcher return; } + // send event to the telemetry server + _telemetryClient.TrackAppStartedAsync("WelsonJS.Launcher", "0.2.7.57"); + // draw the main form Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs b/WelsonJS.Toolkit/WelsonJS.Launcher/Properties/Resources.Designer.cs index d329414..1036a54 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); + } + } + + /// + /// false과(와) 유사한 지역화된 문자열을 찾습니다. + /// + internal static string TelemetryDisabled { + get { + return ResourceManager.GetString("TelemetryDisabled", 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..e418c1c 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 + + + false + \ 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..1924ae7 --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/PosthogTelemetryProvider.cs @@ -0,0 +1,85 @@ +// 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 bool _disposed; + + public PosthogTelemetryProvider(TelemetryOptions options) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + + 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) }; + } + + 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 + { + await _httpClient.PostAsync( + url, + new StringContent(json, Encoding.UTF8, "application/json"), + cancellationToken + ); + } + catch + { + // swallow for fire-and-forget telemetry + } + } + + 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..9c6dfdc --- /dev/null +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/Telemetry/TelemetryClient.cs @@ -0,0 +1,51 @@ +// 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; + private readonly ICompatibleLogger _logger; + + public TelemetryClient(string providerName, TelemetryOptions options, ICompatibleLogger logger = null) + { + _provider = TelemetryProviderFactory.Create(providerName, options); + _logger = 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/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..b7a4978 --- /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) + { + 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); + + 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..d2deefc 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/WelsonJS.Launcher.csproj @@ -128,6 +128,11 @@ GlobalSettingsForm.cs + + + + + diff --git a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config index b74b7fe..4f86bd8 100644 --- a/WelsonJS.Toolkit/WelsonJS.Launcher/app.config +++ b/WelsonJS.Toolkit/WelsonJS.Launcher/app.config @@ -26,6 +26,10 @@ + + + +