Merge pull request #357 from gnh1201/dev
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
Deploy Jekyll with GitHub Pages dependencies preinstalled / build (push) Waiting to run
Deploy Jekyll with GitHub Pages dependencies preinstalled / deploy (push) Blocked by required conditions

Add a telemetry to WelsonJS Launcher
This commit is contained in:
Namhyeon Go 2025-12-03 17:05:28 +09:00 committed by GitHub
commit 5c41a4bf72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 460 additions and 6 deletions

View File

@ -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);

View File

@ -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")]

View File

@ -378,6 +378,42 @@ namespace WelsonJS.Launcher.Properties {
}
}
/// <summary>
/// phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string TelemetryApiKey {
get {
return ResourceManager.GetString("TelemetryApiKey", resourceCulture);
}
}
/// <summary>
/// https://us.i.posthog.com과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string TelemetryBaseUrl {
get {
return ResourceManager.GetString("TelemetryBaseUrl", resourceCulture);
}
}
/// <summary>
/// true과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string TelemetryEnabled {
get {
return ResourceManager.GetString("TelemetryEnabled", resourceCulture);
}
}
/// <summary>
/// posthog과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>
internal static string TelemetryProvider {
get {
return ResourceManager.GetString("TelemetryProvider", resourceCulture);
}
}
/// <summary>
/// 141.101.82.1과(와) 유사한 지역화된 문자열을 찾습니다.
/// </summary>

View File

@ -229,4 +229,16 @@
<data name="icon_editor_32" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\icon_editor_32.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="TelemetryApiKey" xml:space="preserve">
<value>phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8</value>
</data>
<data name="TelemetryBaseUrl" xml:space="preserve">
<value>https://us.i.posthog.com</value>
</data>
<data name="TelemetryProvider" xml:space="preserve">
<value>posthog</value>
</data>
<data name="TelemetryEnabled" xml:space="preserve">
<value>true</value>
</data>
</root>

View File

@ -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<string, object> properties = null,
CancellationToken cancellationToken = default
);
}
}

View File

@ -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<string, object> 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<string, object>
{
["api_key"] = _options.ApiKey,
["distinct_id"] = _options.DistinctId,
["event"] = eventName,
["properties"] = properties ?? new Dictionary<string, object>()
};
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();
}
}
}

View File

@ -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<string, object> properties = null,
CancellationToken cancellationToken = default)
{
return _provider.TrackEventAsync(eventName, properties, cancellationToken);
}
public Task TrackAppStartedAsync(string appName, string appVersion)
{
var props = new Dictionary<string, object>
{
{ "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();
}
}
}

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public static string GetDistinctId()
{
var sources = new List<string>();
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 ""; }
}
/// <summary>
/// Retrieves BIOS UUID from Win32_ComputerSystemProduct.
/// Filters out invalid values (all zeros or all 'F').
/// </summary>
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;
}
/// <summary>
/// Retrieves OS-level UUID equivalent using Win32_OperatingSystem.SerialNumber.
/// This value is unique per Windows installation.
/// </summary>
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;
}
/// <summary>
/// Computes SHA-256 hex string for the given input text.
/// Returns null if hashing fails.
/// </summary>
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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}
}
}

View File

@ -80,6 +80,7 @@
<Reference Include="System.Deployment" />
<Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Management" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Web" />
<Reference Include="System.Windows.Forms" />
@ -128,6 +129,12 @@
<DependentUpon>GlobalSettingsForm.cs</DependentUpon>
</Compile>
<Compile Include="ResourceServer.cs" />
<Compile Include="Telemetry\ITelemetryProvider.cs" />
<Compile Include="Telemetry\PosthogTelemetryProvider.cs" />
<Compile Include="Telemetry\TelemetryClient.cs" />
<Compile Include="Telemetry\TelemetryIdentity.cs" />
<Compile Include="Telemetry\TelemetryOptions.cs" />
<Compile Include="Telemetry\TelemetryProviderFactory.cs" />
<Compile Include="TraceLogger.cs" />
<Compile Include="WebSocketManager.cs" />
<EmbeddedResource Include="EnvForm.resx">

View File

@ -26,6 +26,10 @@
<add key="IpQueryApiPrefix2" value="https://api.abuseipdb.com/api/v2/"/>
<add key="DateTimeFormat" value="yyyy-MM-dd HH:mm:ss"/>
<add key="NativeRequireSigned" value="false"/>
<add key="TelemetryProvider" value="posthog"/>
<add key="TelemetryApiKey" value="phc_pmRHJ0aVEhtULRT4ilexwCjYpGtE9VYRhlA05fwiYt8"/>
<add key="TelemetryBaseUrl" value="https://us.i.posthog.com"/>
<add key="TelemetryEnabled" value="true"/>
</appSettings>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>