diff --git a/deploy.ps1 b/deploy.ps1 index 72ef38d..a00606c 100644 --- a/deploy.ps1 +++ b/deploy.ps1 @@ -434,6 +434,52 @@ function Push-SentryApi { } } +function Push-SentryCheckIn { + param([string]$MonitorSlug, [string]$Status, [string]$CheckInId) + if ([string]::IsNullOrWhiteSpace($sentryAuthToken)) { return $null } + try { + $headers = @{ Authorization = "Bearer $sentryAuthToken"; 'Content-Type' = 'application/json' } + if ([string]::IsNullOrWhiteSpace($CheckInId)) { + # Initial check-in: POST with monitor_config for auto-creation + $url = "https://sentry.io/api/0/organizations/$sentryOrg/monitors/$MonitorSlug/checkins/" + $body = @{ + status = $Status + monitor_config = @{ + schedule = @{ type = 'interval'; value = 1; unit = 'day' } + checkin_margin = 5 + max_runtime = 10 + timezone = 'UTC' + } + } + $json = $body | ConvertTo-Json -Compress -Depth 5 + $resp = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $json -ErrorAction Stop + $newId = $resp.id + Push-LokiEvent 'deploy_sentry_checkin' 'INFO' "Sentry cron check-in started: $MonitorSlug" @{ + monitor_slug = $MonitorSlug + status = $Status + checkin_id = $newId + } + return $newId + } else { + # Completion check-in: PUT to update existing + $url = "https://sentry.io/api/0/organizations/$sentryOrg/monitors/$MonitorSlug/checkins/$CheckInId/" + $body = @{ status = $Status } + $json = $body | ConvertTo-Json -Compress -Depth 5 + Invoke-RestMethod -Uri $url -Method Put -Headers $headers -Body $json -ErrorAction Stop | Out-Null + Push-LokiEvent 'deploy_sentry_checkin' 'INFO' "Sentry cron check-in completed: $MonitorSlug ($Status)" @{ + monitor_slug = $MonitorSlug + status = $Status + checkin_id = $CheckInId + } + return $null + } + } catch { + # Non-fatal: deploy must not fail because Sentry API is down + Write-Warning "Sentry CheckIn ($MonitorSlug/$Status): $($_.Exception.Message)" + return $null + } +} + if (-not [string]::IsNullOrWhiteSpace($sentryAuthToken) -and -not [string]::IsNullOrWhiteSpace($sentryRelease)) { Write-Host "Registering Sentry release: $sentryRelease (3 projects)" @@ -509,6 +555,7 @@ if (-not $skipTests) { script_count = $testScripts.Count scripts = ($testScripts | ForEach-Object { $_.Name }) -join ',' } + $checkInId = Push-SentryCheckIn 'post-deploy-tests' 'in_progress' foreach ($ts in $testScripts) { Write-Host " Running: $($ts.Name)" $tsStart = Get-Date @@ -557,6 +604,8 @@ if (-not $skipTests) { } } } + $checkInStatus = if ($postDeployFailed) { 'error' } else { 'ok' } + Push-SentryCheckIn 'post-deploy-tests' $checkInStatus $checkInId | Out-Null if ($postDeployFailed) { Write-Warning "Post-deploy tests failed. Deploy files are in place but integration is not fully verified." } else { diff --git a/src/SimSteward.Dashboard/data-capture-suite.html b/src/SimSteward.Dashboard/data-capture-suite.html index 2f70c16..e239650 100644 --- a/src/SimSteward.Dashboard/data-capture-suite.html +++ b/src/SimSteward.Dashboard/data-capture-suite.html @@ -244,12 +244,15 @@ .filter-clear { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 0.82rem; padding: 0 4px; margin-left: -28px; margin-right: 4px; transition: color 0.15s; z-index: 1; } .filter-clear:hover { color: var(--text); } - + diff --git a/src/SimSteward.Dashboard/index.html b/src/SimSteward.Dashboard/index.html index 13a8e2b..9b2bc08 100644 --- a/src/SimSteward.Dashboard/index.html +++ b/src/SimSteward.Dashboard/index.html @@ -475,13 +475,16 @@ ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } - + diff --git a/src/SimSteward.Dashboard/replay-incident-index.html b/src/SimSteward.Dashboard/replay-incident-index.html index e7de108..13a3915 100644 --- a/src/SimSteward.Dashboard/replay-incident-index.html +++ b/src/SimSteward.Dashboard/replay-incident-index.html @@ -65,6 +65,18 @@ } .toast.show { opacity: 1; } + +
diff --git a/src/SimSteward.Plugin/DashboardBridge.cs b/src/SimSteward.Plugin/DashboardBridge.cs index 6e63795..17694e3 100644 --- a/src/SimSteward.Plugin/DashboardBridge.cs +++ b/src/SimSteward.Plugin/DashboardBridge.cs @@ -278,8 +278,22 @@ private void HandleMessage(IWebSocketConnection socket, string msg) } var correlationId = Guid.NewGuid().ToString("N").Substring(0, 8); - var (success, result, error) = _dispatchAction(action, arg ?? "", correlationId); - SendActionResult(socket, action, success, result, error); + var tx = SentrySdk.StartTransaction("ws.message", "handle"); + tx.SetExtra("action", action); + tx.SetExtra("correlation_id", correlationId); + SentrySdk.ConfigureScope(scope => scope.Transaction = tx); + try + { + var (success, result, error) = _dispatchAction(action, arg ?? "", correlationId); + SendActionResult(socket, action, success, result, error); + tx.Finish(success ? SpanStatus.Ok : SpanStatus.InternalError); + } + catch (Exception ex) + { + SentrySdk.CaptureException(ex); + tx.Finish(SpanStatus.InternalError); + throw; + } } private void SendActionResult(IWebSocketConnection socket, string action, bool success, string result, string error) diff --git a/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs b/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs index ccbc586..ee9076d 100644 --- a/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs +++ b/src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs @@ -108,6 +108,10 @@ private enum PreflightStep private int _suiteT8PollTicks; private bool _suiteT8BuildWasRunning; + // Sentry performance tracing + private ITransactionTracer _sentryTx; + private ISpan _sentryCurrentSpan; + // ── Public entry points (called from DataUpdate / DispatchAction) ────── private void TryStartDataCaptureSuite(string[] skipIds = null) @@ -184,6 +188,13 @@ private void ProcessDataCaptureSuiteTick() _suite60HzRecorder = null; StopReplayIncidentIndexRecordModeLocked("suite_cancel"); EmitSuiteLifecycleEvent("sdk_capture_suite_cancelled", "Suite cancelled.", "T_cancel"); + + // Sentry: finish spans/transaction as cancelled + _sentryCurrentSpan?.Finish(SpanStatus.Cancelled); + _sentryCurrentSpan = null; + _sentryTx?.Finish(SpanStatus.Cancelled); + _sentryTx = null; + _suitePhase = DataCaptureSuitePhase.Cancelled; } return; @@ -683,6 +694,12 @@ private void BeginDataCaptureSuite() _suiteStep = SuiteInternalStep.T0_Rewind; _suitePhase = DataCaptureSuitePhase.Running; + // Sentry performance transaction for the entire suite run + _sentryTx = SentrySdk.StartTransaction("data-capture-suite", "test.run"); + _sentryTx.SetExtra("test_run_id", _suiteTestRunId); + SentrySdk.ConfigureScope(scope => scope.Transaction = _sentryTx); + _sentryCurrentSpan = _sentryTx.StartChild("step", SuiteInternalStep.T0_Rewind.ToString()); + EmitSuiteLifecycleEvent(DataCaptureSuiteConstants.EventSuiteStarted, $"Data capture suite started. test_run_id={_suiteTestRunId}", "T_start"); SentrySdk.AddBreadcrumb("Data capture suite started", "lifecycle", @@ -694,6 +711,8 @@ private void BeginDataCaptureSuite() private void TickSuiteRunning() { + var stepBefore = _suiteStep; + switch (_suiteStep) { case SuiteInternalStep.T0_Rewind: TickT0_Rewind(); break; @@ -724,6 +743,16 @@ private void TickSuiteRunning() case SuiteInternalStep.Done: TransitionToLoki(); break; } + // Sentry: finish previous span and start new one when step changes + if (_suiteStep != stepBefore && _sentryTx != null) + { + _sentryCurrentSpan?.Finish(SpanStatus.Ok); + if (_suiteStep != SuiteInternalStep.Done) + _sentryCurrentSpan = _sentryTx.StartChild("step", _suiteStep.ToString()); + else + _sentryCurrentSpan = null; + } + // 60Hz recording: every tick while running _suite60HzRecorder?.RecordTick(_irsdk); } @@ -1638,6 +1667,12 @@ private void TransitionToLoki() _suiteEmitCompleteUtc = DateTime.UtcNow; _suitePhase = DataCaptureSuitePhase.AwaitingLoki; + // Sentry: finish any remaining span and the transaction + _sentryCurrentSpan?.Finish(SpanStatus.Ok); + _sentryCurrentSpan = null; + _sentryTx?.Finish(SpanStatus.Ok); + _sentryTx = null; + var fields = BuildTestFields("T_done"); fields["loki_wait_ms"] = DataCaptureSuiteConstants.LokiVerifyDelayMs; MergeSessionAndRoutingFields(fields); diff --git a/src/SimSteward.Plugin/SimStewardPlugin.cs b/src/SimSteward.Plugin/SimStewardPlugin.cs index 220be6a..019ca81 100644 --- a/src/SimSteward.Plugin/SimStewardPlugin.cs +++ b/src/SimSteward.Plugin/SimStewardPlugin.cs @@ -609,6 +609,16 @@ private System.Collections.Generic.Dictionary BuildCaptureIncide { if (string.IsNullOrEmpty(action)) return (false, null, "missing_action"); + + // Sentry: create a child span under the current transaction (if any) + ISpan _actionSpan = null; + SentrySdk.ConfigureScope(scope => + { + var parentTx = scope.Transaction; + if (parentTx != null) + _actionSpan = parentTx.StartChild("action", action ?? "unknown"); + }); + var dispatchFields = new System.Collections.Generic.Dictionary { ["action"] = action, @@ -618,6 +628,9 @@ private System.Collections.Generic.Dictionary BuildCaptureIncide MergeSessionAndRoutingFields(dispatchFields); _logger?.Structured("INFO", "simhub-plugin", "action_dispatched", action, dispatchFields, "action", null); + try + { + if (string.Equals(action, "replay_session", StringComparison.OrdinalIgnoreCase)) { var dir = (arg ?? "").Trim().ToLowerInvariant(); @@ -943,6 +956,18 @@ private System.Collections.Generic.Dictionary BuildCaptureIncide LogActionResult(action, arg, correlationId, false, "not_supported"); return (false, null, "not_supported"); + + } + catch + { + _actionSpan?.Finish(SpanStatus.InternalError); + _actionSpan = null; + throw; + } + finally + { + _actionSpan?.Finish(SpanStatus.Ok); + } } private void OnLog(string level, string message, string source)