-
Notifications
You must be signed in to change notification settings - Fork 20
feat(telemetry): integrate Sentry crash forensics and PostHog analytics pipeline #408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
undead2146
wants to merge
12
commits into
development
Choose a base branch
from
feat/telemetry-sentry-posthog
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
e9db3aa
feat(telemetry): integrate Sentry crash forensics and PostHog analyti…
undead2146 e5339ab
fix(review): address build errors and static analysis findings
undead2146 b027398
fix(telemetry): add test documentation and track runner environment f…
undead2146 bd520cb
fix(style): resolve StyleCop SA1116/SA1117 and CS8601 nullability war…
undead2146 1521bbc
fix(telemetry): correct Wine prefix regex and prioritization in Telem…
undead2146 17c5fa3
fix(telemetry): lazily initialize heartbeat timer and deduplicate upd…
undead2146 a1cfb95
fix(telemetry): aggregate sink flush failures and persist anonymous i…
undead2146 4dfff73
fix(telemetry): use FirstError property on OperationResult
undead2146 e0ecf9f
fix(telemetry): address review feedback on buffer bounding, async dra…
undead2146 32a934d
fix(style): add threading using and reorder members to satisfy StyleC…
undead2146 0950a12
fix(telemetry): synchronize heartbeat timer initialization and observ…
undead2146 49ba3ce
feat(telemetry): enrich download and process metadata and add setting…
undead2146 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
|
|
||
| /// <summary> | ||
| /// Default PostHog API project token for anonymous analytics. | ||
| /// </summary> | ||
| public const string DefaultPostHogApiKey = "phc_yJwFRxbvQ9HUge9kC3Lmt5DG3CpHt4DWnaJYK5YiK98g"; | ||
|
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
30
GenHub/GenHub.Core/Interfaces/Telemetry/ITelemetrySanitizer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
65
GenHub/GenHub.Core/Interfaces/Telemetry/ITelemetryService.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.