Skip to content

Commit 96d9454

Browse files
authored
Merge pull request #19 from simsteward/feature/sentry-phase1
feat: Sentry integration Phase 1 — Replay, Transactions, Cron
2 parents 5d2f26a + c6f6648 commit 96d9454

7 files changed

Lines changed: 145 additions & 4 deletions

File tree

deploy.ps1

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,52 @@ function Push-SentryApi {
434434
}
435435
}
436436

437+
function Push-SentryCheckIn {
438+
param([string]$MonitorSlug, [string]$Status, [string]$CheckInId)
439+
if ([string]::IsNullOrWhiteSpace($sentryAuthToken)) { return $null }
440+
try {
441+
$headers = @{ Authorization = "Bearer $sentryAuthToken"; 'Content-Type' = 'application/json' }
442+
if ([string]::IsNullOrWhiteSpace($CheckInId)) {
443+
# Initial check-in: POST with monitor_config for auto-creation
444+
$url = "https://sentry.io/api/0/organizations/$sentryOrg/monitors/$MonitorSlug/checkins/"
445+
$body = @{
446+
status = $Status
447+
monitor_config = @{
448+
schedule = @{ type = 'interval'; value = 1; unit = 'day' }
449+
checkin_margin = 5
450+
max_runtime = 10
451+
timezone = 'UTC'
452+
}
453+
}
454+
$json = $body | ConvertTo-Json -Compress -Depth 5
455+
$resp = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $json -ErrorAction Stop
456+
$newId = $resp.id
457+
Push-LokiEvent 'deploy_sentry_checkin' 'INFO' "Sentry cron check-in started: $MonitorSlug" @{
458+
monitor_slug = $MonitorSlug
459+
status = $Status
460+
checkin_id = $newId
461+
}
462+
return $newId
463+
} else {
464+
# Completion check-in: PUT to update existing
465+
$url = "https://sentry.io/api/0/organizations/$sentryOrg/monitors/$MonitorSlug/checkins/$CheckInId/"
466+
$body = @{ status = $Status }
467+
$json = $body | ConvertTo-Json -Compress -Depth 5
468+
Invoke-RestMethod -Uri $url -Method Put -Headers $headers -Body $json -ErrorAction Stop | Out-Null
469+
Push-LokiEvent 'deploy_sentry_checkin' 'INFO' "Sentry cron check-in completed: $MonitorSlug ($Status)" @{
470+
monitor_slug = $MonitorSlug
471+
status = $Status
472+
checkin_id = $CheckInId
473+
}
474+
return $null
475+
}
476+
} catch {
477+
# Non-fatal: deploy must not fail because Sentry API is down
478+
Write-Warning "Sentry CheckIn ($MonitorSlug/$Status): $($_.Exception.Message)"
479+
return $null
480+
}
481+
}
482+
437483
if (-not [string]::IsNullOrWhiteSpace($sentryAuthToken) -and -not [string]::IsNullOrWhiteSpace($sentryRelease)) {
438484
Write-Host "Registering Sentry release: $sentryRelease (3 projects)"
439485

@@ -509,6 +555,7 @@ if (-not $skipTests) {
509555
script_count = $testScripts.Count
510556
scripts = ($testScripts | ForEach-Object { $_.Name }) -join ','
511557
}
558+
$checkInId = Push-SentryCheckIn 'post-deploy-tests' 'in_progress'
512559
foreach ($ts in $testScripts) {
513560
Write-Host " Running: $($ts.Name)"
514561
$tsStart = Get-Date
@@ -557,6 +604,8 @@ if (-not $skipTests) {
557604
}
558605
}
559606
}
607+
$checkInStatus = if ($postDeployFailed) { 'error' } else { 'ok' }
608+
Push-SentryCheckIn 'post-deploy-tests' $checkInStatus $checkInId | Out-Null
560609
if ($postDeployFailed) {
561610
Write-Warning "Post-deploy tests failed. Deploy files are in place but integration is not fully verified."
562611
} else {

src/SimSteward.Dashboard/data-capture-suite.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,15 @@
244244
.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; }
245245
.filter-clear:hover { color: var(--text); }
246246
</style>
247-
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.min.js" crossorigin="anonymous"></script>
247+
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.tracing.replay.min.js" crossorigin="anonymous"></script>
248248
<script>
249249
Sentry.init({
250250
dsn: 'https://46ad94b35a51979ba4223eeffe38f5c8@o4511097126780928.ingest.us.sentry.io/4511103122210816',
251251
environment: 'local',
252252
tracesSampleRate: 1.0,
253+
replaysSessionSampleRate: 0.1,
254+
replaysOnErrorSampleRate: 1.0,
255+
integrations: [Sentry.replayIntegration()],
253256
});
254257
Sentry.setTag('dashboard', 'data-capture-suite');
255258
</script>

src/SimSteward.Dashboard/index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,13 +475,16 @@
475475
::-webkit-scrollbar-track { background: transparent; }
476476
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
477477
</style>
478-
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.min.js" crossorigin="anonymous"></script>
478+
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.tracing.replay.min.js" crossorigin="anonymous"></script>
479479
<script>
480480
Sentry.init({
481481
dsn: 'https://46ad94b35a51979ba4223eeffe38f5c8@o4511097126780928.ingest.us.sentry.io/4511103122210816',
482482
environment: 'local',
483483
release: '',
484484
tracesSampleRate: 1.0,
485+
replaysSessionSampleRate: 0.1,
486+
replaysOnErrorSampleRate: 1.0,
487+
integrations: [Sentry.replayIntegration()],
485488
initialScope: { tags: { dashboard: 'index' } },
486489
});
487490
</script>

src/SimSteward.Dashboard/replay-incident-index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@
6565
}
6666
.toast.show { opacity: 1; }
6767
</style>
68+
<script src="https://browser.sentry-cdn.com/9.27.0/bundle.tracing.replay.min.js" crossorigin="anonymous"></script>
69+
<script>
70+
Sentry.init({
71+
dsn: 'https://46ad94b35a51979ba4223eeffe38f5c8@o4511097126780928.ingest.us.sentry.io/4511103122210816',
72+
environment: 'local',
73+
tracesSampleRate: 1.0,
74+
replaysSessionSampleRate: 0.1,
75+
replaysOnErrorSampleRate: 1.0,
76+
integrations: [Sentry.replayIntegration()],
77+
initialScope: { tags: { dashboard: 'replay-incident-index' } },
78+
});
79+
</script>
6880
</head>
6981
<body>
7082
<div class="wrap">

src/SimSteward.Plugin/DashboardBridge.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,22 @@ private void HandleMessage(IWebSocketConnection socket, string msg)
278278
}
279279

280280
var correlationId = Guid.NewGuid().ToString("N").Substring(0, 8);
281-
var (success, result, error) = _dispatchAction(action, arg ?? "", correlationId);
282-
SendActionResult(socket, action, success, result, error);
281+
var tx = SentrySdk.StartTransaction("ws.message", "handle");
282+
tx.SetExtra("action", action);
283+
tx.SetExtra("correlation_id", correlationId);
284+
SentrySdk.ConfigureScope(scope => scope.Transaction = tx);
285+
try
286+
{
287+
var (success, result, error) = _dispatchAction(action, arg ?? "", correlationId);
288+
SendActionResult(socket, action, success, result, error);
289+
tx.Finish(success ? SpanStatus.Ok : SpanStatus.InternalError);
290+
}
291+
catch (Exception ex)
292+
{
293+
SentrySdk.CaptureException(ex);
294+
tx.Finish(SpanStatus.InternalError);
295+
throw;
296+
}
283297
}
284298

285299
private void SendActionResult(IWebSocketConnection socket, string action, bool success, string result, string error)

src/SimSteward.Plugin/SimStewardPlugin.DataCaptureSuite.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ private enum PreflightStep
108108
private int _suiteT8PollTicks;
109109
private bool _suiteT8BuildWasRunning;
110110

111+
// Sentry performance tracing
112+
private ITransactionTracer _sentryTx;
113+
private ISpan _sentryCurrentSpan;
114+
111115
// ── Public entry points (called from DataUpdate / DispatchAction) ──────
112116

113117
private void TryStartDataCaptureSuite(string[] skipIds = null)
@@ -184,6 +188,13 @@ private void ProcessDataCaptureSuiteTick()
184188
_suite60HzRecorder = null;
185189
StopReplayIncidentIndexRecordModeLocked("suite_cancel");
186190
EmitSuiteLifecycleEvent("sdk_capture_suite_cancelled", "Suite cancelled.", "T_cancel");
191+
192+
// Sentry: finish spans/transaction as cancelled
193+
_sentryCurrentSpan?.Finish(SpanStatus.Cancelled);
194+
_sentryCurrentSpan = null;
195+
_sentryTx?.Finish(SpanStatus.Cancelled);
196+
_sentryTx = null;
197+
187198
_suitePhase = DataCaptureSuitePhase.Cancelled;
188199
}
189200
return;
@@ -683,6 +694,12 @@ private void BeginDataCaptureSuite()
683694
_suiteStep = SuiteInternalStep.T0_Rewind;
684695
_suitePhase = DataCaptureSuitePhase.Running;
685696

697+
// Sentry performance transaction for the entire suite run
698+
_sentryTx = SentrySdk.StartTransaction("data-capture-suite", "test.run");
699+
_sentryTx.SetExtra("test_run_id", _suiteTestRunId);
700+
SentrySdk.ConfigureScope(scope => scope.Transaction = _sentryTx);
701+
_sentryCurrentSpan = _sentryTx.StartChild("step", SuiteInternalStep.T0_Rewind.ToString());
702+
686703
EmitSuiteLifecycleEvent(DataCaptureSuiteConstants.EventSuiteStarted,
687704
$"Data capture suite started. test_run_id={_suiteTestRunId}", "T_start");
688705
SentrySdk.AddBreadcrumb("Data capture suite started", "lifecycle",
@@ -694,6 +711,8 @@ private void BeginDataCaptureSuite()
694711

695712
private void TickSuiteRunning()
696713
{
714+
var stepBefore = _suiteStep;
715+
697716
switch (_suiteStep)
698717
{
699718
case SuiteInternalStep.T0_Rewind: TickT0_Rewind(); break;
@@ -724,6 +743,16 @@ private void TickSuiteRunning()
724743
case SuiteInternalStep.Done: TransitionToLoki(); break;
725744
}
726745

746+
// Sentry: finish previous span and start new one when step changes
747+
if (_suiteStep != stepBefore && _sentryTx != null)
748+
{
749+
_sentryCurrentSpan?.Finish(SpanStatus.Ok);
750+
if (_suiteStep != SuiteInternalStep.Done)
751+
_sentryCurrentSpan = _sentryTx.StartChild("step", _suiteStep.ToString());
752+
else
753+
_sentryCurrentSpan = null;
754+
}
755+
727756
// 60Hz recording: every tick while running
728757
_suite60HzRecorder?.RecordTick(_irsdk);
729758
}
@@ -1638,6 +1667,12 @@ private void TransitionToLoki()
16381667
_suiteEmitCompleteUtc = DateTime.UtcNow;
16391668
_suitePhase = DataCaptureSuitePhase.AwaitingLoki;
16401669

1670+
// Sentry: finish any remaining span and the transaction
1671+
_sentryCurrentSpan?.Finish(SpanStatus.Ok);
1672+
_sentryCurrentSpan = null;
1673+
_sentryTx?.Finish(SpanStatus.Ok);
1674+
_sentryTx = null;
1675+
16411676
var fields = BuildTestFields("T_done");
16421677
fields["loki_wait_ms"] = DataCaptureSuiteConstants.LokiVerifyDelayMs;
16431678
MergeSessionAndRoutingFields(fields);

src/SimSteward.Plugin/SimStewardPlugin.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,16 @@ private System.Collections.Generic.Dictionary<string, object> BuildCaptureIncide
609609
{
610610
if (string.IsNullOrEmpty(action))
611611
return (false, null, "missing_action");
612+
613+
// Sentry: create a child span under the current transaction (if any)
614+
ISpan _actionSpan = null;
615+
SentrySdk.ConfigureScope(scope =>
616+
{
617+
var parentTx = scope.Transaction;
618+
if (parentTx != null)
619+
_actionSpan = parentTx.StartChild("action", action ?? "unknown");
620+
});
621+
612622
var dispatchFields = new System.Collections.Generic.Dictionary<string, object>
613623
{
614624
["action"] = action,
@@ -618,6 +628,9 @@ private System.Collections.Generic.Dictionary<string, object> BuildCaptureIncide
618628
MergeSessionAndRoutingFields(dispatchFields);
619629
_logger?.Structured("INFO", "simhub-plugin", "action_dispatched", action, dispatchFields, "action", null);
620630

631+
try
632+
{
633+
621634
if (string.Equals(action, "replay_session", StringComparison.OrdinalIgnoreCase))
622635
{
623636
var dir = (arg ?? "").Trim().ToLowerInvariant();
@@ -943,6 +956,18 @@ private System.Collections.Generic.Dictionary<string, object> BuildCaptureIncide
943956

944957
LogActionResult(action, arg, correlationId, false, "not_supported");
945958
return (false, null, "not_supported");
959+
960+
}
961+
catch
962+
{
963+
_actionSpan?.Finish(SpanStatus.InternalError);
964+
_actionSpan = null;
965+
throw;
966+
}
967+
finally
968+
{
969+
_actionSpan?.Finish(SpanStatus.Ok);
970+
}
946971
}
947972

948973
private void OnLog(string level, string message, string source)

0 commit comments

Comments
 (0)