Skip to content

feat(spx-gui): report SPX runtime panics and lifecycle spans to Sentry - #3383

Open
1034674309 wants to merge 3 commits into
goplus:devfrom
1034674309:feat/issue-2419-sentry-spx-runtime
Open

feat(spx-gui): report SPX runtime panics and lifecycle spans to Sentry#3383
1034674309 wants to merge 3 commits into
goplus:devfrom
1034674309:feat/issue-2419-sentry-spx-runtime

Conversation

@1034674309

@1034674309 1034674309 commented Jul 31, 2026

Copy link
Copy Markdown

Summary

Closes #2419 by connecting the structured panic logs already emitted by tools/ispx to the existing browser-side Sentry client.

This PR intentionally does not add a separate Go/WASM Sentry SDK or configuration. tools/ispx emits a location-rich JSON panic record to its console; ProjectRunner recognizes that record and reports it through the app's existing @sentry/vue setup.

Runtime panic flow

  1. SPX user code triggers a runtime panic, for example an out-of-range array access.
  2. tools/ispx emits a JSON console record containing error, function, file, line, and column.
  3. ProjectRunner validates and parses that record from the runner iframe.
  4. The frontend sends a SpxRuntimePanic event to Sentry with:
    • tag: spx.runtime=panic
    • context: spx.function, spx.file, spx.line, and spx.column

This lets Sentry point to the SPX source location instead of only showing a generic game error.

Changes

  • Parse structured msg: "panic" JSON logs emitted by the WASM runner.
  • Capture a dedicated SpxRuntimePanic exception with structured SPX source context.
  • Defer Sentry capture until the console call has returned, so monitoring does not interfere with Go WASM panic unwinding.
  • Add lifecycle spans for:
    • spx.engine.init
    • spx.build
    • spx.start
    • spx.stop
  • Preserve the existing editor error state, console forwarding, and iframe reload behavior.

Duplicate-event handling

A single runtime panic can also produce the runner's existing generic onGameError or onEngineCrash callback.

Those generic captures are now deferred for one second. If the structured panic log arrives during that grace period, the pending generic capture is cancelled, leaving the source-aware SpxRuntimePanic as the single Sentry error event. If no panic log arrives, the normal error is still reported.

Verification

  • pnpm type-check
  • pnpm lint
  • pnpm format-check
  • ./build-wasm.sh
  • pnpm build
  • git diff --check
  • Production-mode local preview: triggered an array-out-of-range SPX panic and confirmed one SpxRuntimePanic Sentry event with NiuXiaoQi.spx, line/column, function name, environment=production, and spx.runtime=panic.

Scope

No backend APIs, user-code semantics, project save/publish flow, or editor error UI are changed.

Closes #2419

Add Sentry integration to ProjectRunner to capture and report runtime
errors from tools/ispx with structured context including file, line,
function, and column information.

Key features:
- Parse panic JSON logs from tools/ispx console output
- Report runtime panics to Sentry with SpxRuntimePanic exception type
- Add structured context (file, line, column, function) to events
- Instrument lifecycle spans for engine init, build, start, and stop
- Deduplicate errors within 5-second window to prevent duplicate reports
- Graceful degradation when Sentry is unavailable
- Async reporting to avoid interfering with WASM panic unwinding

Closes goplus#2419

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@1034674309 1034674309 changed the title feat(spx-gui): integrate Sentry monitoring for SPX runtime panics feat(spx-gui): report SPX runtime panics and lifecycle spans to Sentry Aug 2, 2026
@1034674309
1034674309 marked this pull request as ready for review August 3, 2026 01:53

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

This change adds Sentry tracing spans around SPX runner lifecycle operations and structured SPX runtime-panic reporting. The overall structure is clean, parseRuntimePanic is strict and defensive at the iframe trust boundary, and the WASM-unwinding deferral (setTimeout(0)) is a subtle correctness detail handled well. The four review passes surfaced a few concrete concerns worth addressing, noted inline. No blocking issues; submitting as a non-approving comment review.

Highlights:

  • Performance: JSON.parse runs on every single-string console.log from the game loop — worth a cheap prefilter.
  • Correctness: grace-period (1000ms) vs recent-panic window (5000ms) mismatch, and panic state is not reset on stop(), so a stale panic can suppress a legitimate error in a subsequent run.
  • Security (low): attacker-controlled panic strings reach Sentry unbounded.


let value: unknown
try {
value = JSON.parse(args[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: parseRuntimePanic runs on every single-string console.log forwarded from the game iframe, and the only guard before JSON.parse is the arity/typeof check on line 244. A game doing console.log("score: 42") once per frame will trigger a JSON.parse + throw/catch at up to 60x/sec. Consider a cheap prefilter before parsing, e.g. skip unless the string starts with { and contains "panic" — the payload requires record.msg === 'panic' anyway, so this is behavior-preserving.


function hasRecentRuntimePanic() {
const panic = lastRuntimePanic
return panic != null && Date.now() - lastRuntimePanicAt <= 5000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic-number mismatch / correctness: this bare 5000 recent-panic window is inconsistent with runtimePanicGracePeriod = 1000 (line 185). The deferral only waits 1s for a future panic, but a panic that arrived up to 5s earlier suppresses the generic capture. Recommend extracting both into named constants and documenting why they differ (or unifying them). A raw literal here is easy to drift out of sync with the grace period.

}

function deferRuntimeErrorCapture(err: unknown, ctx: string) {
if (hasRecentRuntimePanic()) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-run stale-panic suppression: lastRuntimePanic/lastRuntimePanicAt are reset only at the start of runInternal (lines 429-430), not in stop(). During rerun() (which calls stop() then run()), and for up to 5s after any run ends, hasRecentRuntimePanic() can still return true from a previous run's panic. That would cause this early return to silently drop a legitimately different error from the new run. Consider resetting panic state on stop() as well, or keying "recent" to the current run/AbortController.

line,
column
})
Sentry.captureException(exception)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security (low) — unbounded attacker-controlled data to Sentry: error, functionName, and file originate from the iframe's console.log (user/game WASM) and are forwarded verbatim into Sentry as the exception message, name, and context with no length cap or sanitization. A malicious project can emit arbitrary/large panic strings (log-poisoning) and could embed runtime-harvested data into your Sentry stream. Consider truncating these strings to a bounded length and confirming Sentry PII scrubbing covers this path.

// eslint-disable-next-line no-console
console.log(...args)
const panic = parseRuntimePanic(args)
if (panic != null) handleRuntimePanic(panic.error, panic.functionName, panic.file, panic.line, panic.column)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: parseRuntimePanic already returns a RuntimePanic object, but it's destructured into 5 positional args here only for handleRuntimePanic to immediately reassemble the identical object (line 215). Passing panic directly (handleRuntimePanic(panic)) is simpler and removes a class of positional-arg ordering bugs.

})
try {
const result = await operation()
span.setStatus({ code: 1 })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (readability): span.setStatus({ code: 1 }) / { code: 2 } use bare numeric codes for OK/ERROR. A short comment or named constants (// 1 = OK, 2 = ERROR) would match how logLevels is spelled out elsewhere in this file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Sentry integration to tools/ispx

1 participant