Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions GenHub/GenHub.Core/Constants/TelemetryConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
namespace GenHub.Core.Constants;

/// <summary>
/// Centralized constants for telemetry event names, properties, and configuration values.
/// </summary>
public static class TelemetryConstants
{
/// <summary>
/// Application name identifier for telemetry.
/// </summary>
public const string AppName = "GenHub";

/// <summary>
/// Default flush interval in seconds for background batching.
/// </summary>
public const int DefaultFlushIntervalSeconds = 30;

/// <summary>
/// Maximum capacity of the in-memory bounded channel before dropping oldest events.
/// </summary>
public const int MaxQueueCapacity = 500;

/// <summary>
/// Heartbeat interval in minutes for active game sessions.
/// </summary>
public const int SessionHeartbeatIntervalMinutes = 5;

/// <summary>
/// Maximum number of breadcrumbs preserved in the circular buffer for crash forensics.
/// </summary>
public const int MaxBreadcrumbsCount = 50;

/// <summary>
/// Mask string for sanitized sensitive data or user directories.
/// </summary>
public const string UserDirectoryMask = "<USER_DIR>";

/// <summary>
/// Mask string for sanitized workspace directories.
/// </summary>
public const string WorkspaceDirectoryMask = "<WORKSPACE_DIR>";

/// <summary>
/// Mask string for sanitized Wine prefix directories.
/// </summary>
public const string WinePrefixMask = "<WINE_PREFIX>";

/// <summary>
/// Mask string for sanitized IP addresses.
/// </summary>
public const string IpAddressMask = "<IP_MASKED>";

/// <summary>
/// Mask string for sanitized tokens and secrets.
/// </summary>
public const string SecretTokenMask = "<TOKEN_MASKED>";

/// <summary>
/// Default Sentry DSN endpoint for crash reporting.
/// </summary>
public const string DefaultSentryDsn = "https://06a9269c6418a6917f0fec49e1589e44@o4511370888347648.ingest.de.sentry.io/4511943606927440";
Comment thread
undead2146 marked this conversation as resolved.

/// <summary>
/// Default PostHog API project token for anonymous analytics.
/// </summary>
public const string DefaultPostHogApiKey = "phc_yJwFRxbvQ9HUge9kC3Lmt5DG3CpHt4DWnaJYK5YiK98g";
Comment thread
undead2146 marked this conversation as resolved.

/// <summary>
/// Default PostHog host URL.
/// </summary>
public const string DefaultPostHogHost = "https://us.i.posthog.com";

/// <summary>
/// Default PostHog event capture endpoint.
/// </summary>
public const string DefaultPostHogCaptureEndpoint = "https://us.i.posthog.com/capture/";

/// <summary>
/// Default PostHog project identifier.
/// </summary>
public const string DefaultPostHogProjectId = "567732";

/// <summary>
/// Telemetry event names.
/// </summary>
public static class Events
{
/// <summary>Emitted when a game process starts.</summary>
public const string GameSessionStarted = "game_session_started";

/// <summary>Emitted periodically while a game process is running.</summary>
public const string GameSessionHeartbeat = "game_session_heartbeat";

/// <summary>Emitted when a game process exits.</summary>
public const string GameSessionEnded = "game_session_ended";

/// <summary>Emitted when a content or mod download completes.</summary>
public const string ContentDownloadCompleted = "content_download_completed";

/// <summary>Emitted when an application update check finishes.</summary>
public const string AppUpdateChecked = "app_update_checked";

/// <summary>Emitted when an application update is applied.</summary>
public const string AppUpdateApplied = "app_update_applied";

/// <summary>Emitted when CAS workspace reconciliation completes.</summary>
public const string CasReconcileCompleted = "cas_reconcile_completed";

/// <summary>Emitted when an unhandled application exception or crash occurs.</summary>
public const string AppCrash = "app_unhandled_crash";
}

/// <summary>
/// Telemetry event property keys.
/// </summary>
public static class Properties
{
/// <summary>Session identifier.</summary>
public const string SessionId = "session_id";

/// <summary>Game type (e.g. Generals, ZeroHour).</summary>
public const string GameType = "game_type";

/// <summary>Profile identifier.</summary>
public const string ProfileId = "profile_id";

/// <summary>Profile name.</summary>
public const string ProfileName = "profile_name";

/// <summary>Duration in seconds.</summary>
public const string DurationSeconds = "duration_seconds";

/// <summary>Process exit code.</summary>
public const string ExitCode = "exit_code";

/// <summary>Operating system platform.</summary>
public const string Platform = "platform";

/// <summary>Game runner or execution environment (Native, Wine, Proton, etc.).</summary>
public const string Runner = "runner";

/// <summary>Screen resolution.</summary>
public const string Resolution = "resolution";

/// <summary>Manifest identifier.</summary>
public const string ManifestId = "manifest_id";

/// <summary>Content type (e.g. Mod, Patch, Map).</summary>
public const string ContentType = "content_type";

/// <summary>Content identifier.</summary>
public const string ContentId = "content_id";

/// <summary>Content name or display title.</summary>
public const string ContentName = "content_name";

/// <summary>Publisher identifier.</summary>
public const string PublisherId = "publisher_id";

/// <summary>Reconciliation strategy name.</summary>
public const string Strategy = "strategy";

/// <summary>Size in megabytes.</summary>
public const string SizeMb = "size_mb";

/// <summary>Average network speed in Mbps.</summary>
public const string SpeedMbps = "speed_mbps";

/// <summary>Source provider name.</summary>
public const string SourceProvider = "source_provider";

/// <summary>Retry attempt count.</summary>
public const string RetryCount = "retry_count";

/// <summary>Starting version for update.</summary>
public const string FromVersion = "from_version";

/// <summary>Target version for update.</summary>
public const string ToVersion = "to_version";

/// <summary>Update channel or branch.</summary>
public const string Channel = "channel";

/// <summary>Restart duration in milliseconds.</summary>
public const string RestartDurationMs = "restart_duration_ms";

/// <summary>Cache hit rate percentage.</summary>
public const string CacheHitRate = "cache_hit_rate";

/// <summary>Number of files reconciled.</summary>
public const string FileCount = "file_count";

/// <summary>Bytes reconciled.</summary>
public const string BytesReconciled = "bytes_reconciled";

/// <summary>Exception type name.</summary>
public const string ExceptionType = "exception_type";

/// <summary>Exception error message.</summary>
public const string ExceptionMessage = "exception_message";

/// <summary>Exception stack trace.</summary>
public const string StackTrace = "stack_trace";

/// <summary>Indicates whether the exception was fatal.</summary>
public const string IsFatal = "is_fatal";

/// <summary>Context or subsystem where exception occurred.</summary>
public const string Context = "context";

/// <summary>Installation identifier.</summary>
public const string InstallationId = "installation_id";

/// <summary>Application version.</summary>
public const string AppVersion = "app_version";

/// <summary>Executable path or name.</summary>
public const string ExecutablePath = "executable_path";
}
}
30 changes: 30 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Telemetry/ITelemetrySanitizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Collections.Generic;

namespace GenHub.Core.Interfaces.Telemetry;

/// <summary>
/// Sanitizes sensitive user data, personal paths, usernames, IP addresses, and tokens from telemetry payloads.
/// </summary>
public interface ITelemetrySanitizer
{
/// <summary>
/// Sanitizes an input string by removing sensitive usernames, home folders, and personal paths.
/// </summary>
/// <param name="input">The input string to sanitize.</param>
/// <returns>The sanitized string with sensitive data masked.</returns>
string SanitizeString(string? input);

/// <summary>
/// Sanitizes an exception stack trace.
/// </summary>
/// <param name="stackTrace">The raw stack trace string.</param>
/// <returns>The sanitized stack trace.</returns>
string SanitizeStackTrace(string? stackTrace);

/// <summary>
/// Recursively sanitizes a dictionary of properties.
/// </summary>
/// <param name="properties">The raw properties dictionary.</param>
/// <returns>A sanitized dictionary.</returns>
IReadOnlyDictionary<string, object?> SanitizeProperties(IReadOnlyDictionary<string, object?>? properties);
}
65 changes: 65 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Telemetry/ITelemetryService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GenHub.Core.Models.Enums;
using GenHub.Core.Models.Results;
using GenHub.Core.Models.Telemetry;

namespace GenHub.Core.Interfaces.Telemetry;

/// <summary>
/// Core contract for recording and dispatching structured telemetry events, crashes, and diagnostics.
/// </summary>
public interface ITelemetryService
{
/// <summary>
/// Gets the current active telemetry consent level.
/// </summary>
TelemetryLevel CurrentLevel { get; }

/// <summary>
/// Checks if the specified telemetry level is permitted under current user settings.
/// </summary>
/// <param name="level">The telemetry level to check.</param>
/// <returns><c>true</c> if permitted; otherwise, <c>false</c>.</returns>
bool IsEnabled(TelemetryLevel level);

/// <summary>
/// Tracks an anonymous structured telemetry event.
/// </summary>
/// <param name="eventName">The unique event name.</param>
/// <param name="properties">Optional structured properties.</param>
/// <param name="level">Minimum required telemetry level (defaults to AnonymousMetrics).</param>
void TrackEvent(string eventName, IReadOnlyDictionary<string, object?>? properties = null, TelemetryLevel level = TelemetryLevel.AnonymousMetrics);

/// <summary>
/// Tracks an exception or crash diagnostics with sanitized stack trace and breadcrumbs.
/// </summary>
/// <param name="exception">The exception to track.</param>
/// <param name="context">Optional context or subsystem name.</param>
/// <param name="properties">Optional metadata properties.</param>
/// <param name="isFatal">Whether the exception caused a fatal crash.</param>
void TrackException(Exception exception, string? context = null, IReadOnlyDictionary<string, object?>? properties = null, bool isFatal = false);

/// <summary>
/// Adds a breadcrumb record to the in-memory circular buffer for crash investigation.
/// </summary>
/// <param name="message">The breadcrumb message.</param>
/// <param name="category">The category (e.g. "ui", "game", "download").</param>
/// <param name="data">Optional structured data.</param>
void AddBreadcrumb(string message, string? category = null, IReadOnlyDictionary<string, object?>? data = null);

/// <summary>
/// Gets the recent breadcrumb history from the circular buffer.
/// </summary>
/// <returns>A snapshot of recent breadcrumbs.</returns>
IReadOnlyList<Breadcrumb> GetRecentBreadcrumbs();

/// <summary>
/// Asynchronously flushes all queued telemetry events to registered sinks.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An operation result indicating whether flush succeeded.</returns>
Task<OperationResult<bool>> FlushAsync(CancellationToken cancellationToken = default);
}
39 changes: 39 additions & 0 deletions GenHub/GenHub.Core/Interfaces/Telemetry/ITelemetrySink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Threading;
using System.Threading.Tasks;
using GenHub.Core.Models.Results;
using GenHub.Core.Models.Telemetry;

namespace GenHub.Core.Interfaces.Telemetry;

/// <summary>
/// Defines a pluggable destination sink for telemetry events.
/// </summary>
public interface ITelemetrySink
{
/// <summary>
/// Gets the unique name identifier of the sink.
/// </summary>
string Name { get; }

/// <summary>
/// Determines if this sink handles the given telemetry event.
/// </summary>
/// <param name="telemetryEvent">The telemetry event.</param>
/// <returns><c>true</c> if handled; otherwise, <c>false</c>.</returns>
bool CanHandle(TelemetryEvent telemetryEvent);

/// <summary>
/// Emits a single telemetry event to the sink.
/// </summary>
/// <param name="telemetryEvent">The telemetry event to emit.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An operation result indicating success or failure.</returns>
Task<OperationResult<bool>> EmitAsync(TelemetryEvent telemetryEvent, CancellationToken cancellationToken = default);

/// <summary>
/// Flushes any pending buffered events to the remote endpoint.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An operation result indicating success or failure.</returns>
Task<OperationResult<bool>> FlushAsync(CancellationToken cancellationToken = default);
}
12 changes: 12 additions & 0 deletions GenHub/GenHub.Core/Models/Common/DownloadConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,16 @@ public DownloadConfiguration()

/// <summary>Gets or sets the delay between retry attempts.</summary>
public TimeSpan RetryDelay { get; set; }

/// <summary>Gets or sets the display name or title of the content being downloaded.</summary>
public string? ContentName { get; set; }

/// <summary>Gets or sets the unique identifier of the content being downloaded.</summary>
public string? ContentId { get; set; }

/// <summary>Gets or sets the publisher identifier.</summary>
public string? PublisherId { get; set; }

/// <summary>Gets or sets the content type (e.g. Mod, Map, Patch, Addon).</summary>
public string? ContentType { get; set; }
}
Loading
Loading