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)