services/playgroundPush
PURPOSE
Shared client helper for Ship Playground / dev-tool PUSH buttons. POSTs a balance change to /__dev/push-stats and routes every failure mode to Sentry with full pivot tags so Claude can query issues without asking Nate to read errors. Recognizes the three common prod failures distinctly: missing endpoint (Vercel SPA fallback returning index.html), HTTP error, and network error.
OWNS
PushResultinterface — discriminated payload covering both the HTTP outcome (ok,status,reason) and the full dev-plugin pipeline trace (fileWriteOk,fileError,file,changed,gitAttempted,commitOk,commitSha,commitError,pushOk,pushError,timestampMs).pushStats(body)— the single exported async helper. No internal state, no module-level mutables.
READS FROM
body.target(string) — used as the primary Sentry pivot tag (push_target).body.{hull,id,archetype,weaponId,upgradeId,artifactId,planetId,modId}— promoted topush_<key>Sentry tags when present as strings.Response.headers['content-type']— gates the SPA-fallback detection (text/html→ treat as missing endpoint).Response.status,Response.url,Response.text(),Response.json()— diagnostic surfaces fed to Sentry breadcrumbs/extras and into the returnedPushResult.
PUSHES TO
POST /__dev/push-stats— single fetch target. JSON body,Content-Type: application/json. Only exists in the dev Vite plugin; absent in production (SPA fallback returnsindex.html200).@sentry/browser— every failure path runsSentry.withScopeand emits eithercaptureException(network, JSON parse) orcaptureMessage(missing endpoint, HTTP error,ok:false). All four error categories carryscope.level = 'error'.console.error— local mirror of network / SPA-fallback / HTTP-error cases (truncated to 400 chars for bodies). Used for in-browser dev visibility only; user-facing diagnostics go through Sentry.
DOES NOT
- Does not retry. One fetch, one outcome.
- Does not validate the request
bodyshape — caller (Ship Playground UI / dev tools) is trusted to send the right schema. - Does not surface toasts or UI — returns
PushResult.reasonand lets the caller decide. - Does not persist state. Sentry scope is per-call (
withScope), tags do not leak across pushes. - Does not trigger git ops itself — those happen server-side in the dev plugin; this helper only reports their outcome via the
gitAttempted/commitOk/pushOkmirrored fields. - Does not swallow the JSON-parse error silently —
bad_response_shapeis captured to Sentry.
Signals
Sentry tags emitted
| Tag | Source | When |
|---|---|---|
push_target | body.target | every call |
push_hull / push_id / push_archetype / push_weaponId / push_upgradeId / push_artifactId / push_planetId / push_modId | corresponding body[k] if string | conditional |
push_failure_reason | constant per branch | network_error | production_no_endpoint | http_error | bad_response_shape | server_ok_false |
push_status | res.status | HTTP error branch only |
Sentry extras
push_body— the full request body (every call).push_content_type+push_url— SPA-fallback branch.push_response_body— HTTP-error branch, truncated to 2000 chars.push_server_error—ok:falsebranch, sourced fromparsed.error ?? parsed.fileError.
Capture kind
captureException(err)— network branch, bad-response-shape branch (attaches originalerr).captureMessage(string, 'error')— SPA fallback, HTTP error, serverok:false.
Entry points
Ship PlaygroundPUSH buttons (per-system Save panels) — the primary caller.- Any dev-only tool that wants to write a stats change back to the source
.tsfile: importpushStatsand POST{ target, ...fields }.
Pattern notes
- Vercel SPA fallback detection is load-bearing. Without the
text/htmlcontent-type guard, pushes in production return{ res.ok: true, status: 200 }and silently vanish. This branch translates that intopush_failure_reason=production_no_endpointwithreason: 'endpoint unavailable (deploy to dev)'. - Per-call Sentry scope. All tag/extra writes happen inside
Sentry.withScopevia the localwithScopeclosure, so tags do not contaminate the global scope and one PUSH’s diagnostics cannot leak into an unrelated capture. - Pipeline trace passthrough. On success, every field the server reports (
fileWriteOk,file,changed,gitAttempted,commitOk,commitSha,commitError,pushOk,pushError,timestampMs) is forwarded verbatim intoPushResultso the SAVE status panel can render the full file-write → commit → push trace without a second request. Defaults:fileWriteOkdefaults totrue(server already vouched for the write by responding 200), boolean git fields coerce via!!,timestampMsfalls back toDate.now(). ok:falsesemantic-failure branch. Even on HTTP 200, a server payload of{ ok: false, error?, fileError? }is reported to Sentry asserver_ok_falseand translated intoPushResult.ok=falsewithfileWriteOk: falseso the caller treats it as a hard failure.- No internal abstraction. The Sentry context is built up at call time as a flat
tagsmap plus extras; no helper module wraps Sentry beyond the local closure. Matches the global “no premature abstractions” code-style rule.
EXTRACT-CANDIDATE
- Sentry-scoped capture helper. The
withScope+setTag+setExtra+setLevel('error')pattern with a per-call tag map is identical in shape to anything else in the codebase that wants a “report this with pivots” surface. If another client-side service grows the sameSentry.withScopeboilerplate, lift this into a sharedlib/sentryReport.tswith areport(err|message, { tags, extras, level })signature. - Vercel SPA-fallback HTML detector. Any other
fetch('/__dev/...')or/__internal/...call in client code will hit the same production silent-failure mode. A sharedassertDevEndpointResponse(res)(or a wrapperdevFetch(url, init)) that throws / tagsproduction_no_endpointoncontent-type: text/htmlwould consolidate it.