skills/sentry/SKILL.md
Interact with Sentry (EU region) — install SDK, triage issues, manage releases, upload source maps, CRUD alerts and projects. Use when user wants to track errors, add error monitoring, see recent issues, create a release, upload source maps, wire Sentry into an app, or manage alerts.
npx skillsauth add RonanCodes/ronan-skills sentryInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
CLI-first Sentry ops via the user API (EU region — de.sentry.io for most endpoints, ronan-connolly.sentry.io for org URL). Covers SDK install, issue triage, releases, source maps, and project/alert management.
/ro:sentry install [--tanstack|--node|--both] # wire SDK into current app
/ro:sentry issue list [--project <slug>] [--limit 20]
/ro:sentry issue get <issue-id>
/ro:sentry issue resolve <issue-id>
/ro:sentry release create <version> [--project <slug>]
/ro:sentry release finalize <version>
/ro:sentry sourcemaps upload <version> --dist <path>
/ro:sentry project list
/ro:sentry project create <slug> --platform javascript-react
/ro:sentry alert list [--project <slug>]
~/.claude/.env:
SENTRY_AUTH_TOKEN — all-access user auth token (scopes: alerts:, event:, member:, org:, project:, team:)SENTRY_ORG=ronan-connollySENTRY_URL=https://ronan-connolly.sentry.io/ (UI only)SENTRY_REGION_URL=https://de.sentry.io (API — EU region routing)sentry-cli for source-map uploads: pnpm add -D @sentry/cli (per-project) or brew install getsentry/tools/sentry-cliMost API calls go to ${SENTRY_REGION_URL}/api/0/... (EU region). The non-region URL (sentry.io) works for some endpoints but returns 404 for others after the EU migration. Always use the region URL.
pnpm add @sentry/react
pnpm add -D @sentry/vite-plugin
Client — src/lib/sentry.ts:
declare const __APP_RELEASE__: string
if (typeof window !== "undefined" && import.meta.env.PROD) {
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
environment: import.meta.env.MODE,
release: typeof __APP_RELEASE__ === "string" ? __APP_RELEASE__ : undefined,
sendDefaultPii: true, // safe for no-auth utility apps; flip off if PII inputs exist
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({
maskAllInputs: true, // safe default; flip off only if you've audited all inputs
blockAllMedia: false,
}),
Sentry.feedbackIntegration({
colorScheme: "system",
showBranding: false,
// Ronan default: don't auto-inject the floating bottom-right widget.
// Attach to an in-footer button (see "Footer-attached feedback button"
// section below). Floating UI is noisy on marketing sites and easy
// to miss — footer trigger lives where users already look.
autoInject: false,
}),
],
tracesSampleRate: 0.1,
replaysSessionSampleRate: 0.1, // ambient coverage for low-traffic; drop to 0.01 at scale
replaysOnErrorSampleRate: 1.0,
});
}
Defaults rationale: integrations array is the load-bearing line — replaysOnErrorSampleRate is a no-op without replayIntegration(), same for tracesSampleRate without browserTracingIntegration(). Skipping it is the most common reason "Sentry's wired but I see nothing."
feedbackIntegration is on by default for utility apps. It opens a one-shot form (name, email, description, optional screenshot) and creates a Sentry issue tagged as user feedback. For a no-auth side project this replaces the missing contact form. Drop it for apps that already have a richer in-app feedback path.
The Sentry SDK's default feedbackIntegration() auto-injects a floating "Report a Bug" button bottom-right. Looks tacked-on, easy to miss on a long page, and clashes with the visual language of marketing/portfolio sites.
Default for every Ronan project: set autoInject: false in the integration config (above) and attach the widget to a button you place in the site footer next to the copyright line. The button reads as part of the chrome; the floating widget reads as a vendor stamp.
Footer button (Astro example):
<!-- src/components/Footer.astro, in the bottom flex row alongside copyright -->
<button
type="button"
id="sentry-feedback-trigger"
class="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<Icon name="lucide:megaphone" class="w-4 h-4" />
{t.footer.reportBug}
</button>
Wire-up in the Sentry init:
// after Sentry.init({...}) has resolved
const bindFeedback = () => {
const feedback = Sentry.getFeedback();
const trigger = document.getElementById("sentry-feedback-trigger");
if (feedback && trigger) feedback.attachTo(trigger);
};
bindFeedback();
// Re-bind on Astro view transitions / SPA routes — the previous trigger
// node gets swapped out and the prior `attachTo` reference goes stale.
document.addEventListener("astro:page-load", bindFeedback);
Translation strings: footer.reportBug → "Report a bug" (en) / "Bug melden" (nl) / equivalent for any other locale you ship.
Why bind on page-load: Astro's <ViewTransitions> (and Tanstack Start's client router) swap the body, which destroys the previous trigger element. Without re-binding, the button stops opening the widget after the first SPA navigation. Same pattern applies to any framework with client-side routing.
When PostHog is also in the app, link the two so a Sentry issue points at the matching PostHog session replay. Drop this snippet at the end of initSentry():
// after Sentry.init(...)
void linkPostHog(Sentry)
async function linkPostHog(Sentry: typeof import("@sentry/react")) {
try {
const { initPostHog, posthog } = await import("./posthog")
await initPostHog() // idempotent
const distinctId = posthog.get_distinct_id?.()
if (distinctId) Sentry.setUser({ id: distinctId })
const sessionUrl = posthog.get_session_replay_url?.()
if (sessionUrl) Sentry.setTag("posthog.session_url", sessionUrl)
} catch (err) {
console.warn("[sentry] posthog cross-link skipped", err)
}
}
get_session_replay_url exists on posthog-js ≥ 1.115. The tag becomes a clickable URL in the Sentry issue UI — one click jumps from "what broke" to "what was the user doing right before it broke." The linker is fire-and-forget so it doesn't block Sentry's own init.
Server (Cloudflare Workers) — src/lib/sentry-server.ts:
import * as Sentry from "@sentry/cloudflare";
export default Sentry.withSentry(
(env: CloudflareEnv) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 0.1,
}),
handler, // your worker fetch handler
);
⚠️ TanStack Start caveat. TanStack Start sets main: "@tanstack/react-start/server-entry" in wrangler — a virtual module owned by the framework's vite plugin. You can't drop in withSentry without authoring a bespoke worker entry that re-exports the framework handler, which is fragile across framework upgrades. Practical options:
observability.enabled: true for raw worker errors, and add per-route try/catch + Sentry.captureException only in the handlers that matter (e.g. /api/og Satori rendering).Vite plugin for source maps — vite.config.ts:
import { execSync } from "node:child_process";
import { sentryVitePlugin } from "@sentry/vite-plugin";
const release = process.env.VITE_RELEASE
|| (() => { try { return execSync("git rev-parse --short HEAD", { encoding: "utf8" }).trim() } catch { return "dev" } })();
const sentryPlugin = process.env.SENTRY_AUTH_TOKEN
? sentryVitePlugin({
org: process.env.SENTRY_ORG ?? "ronan-connolly",
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
url: process.env.SENTRY_REGION_URL ?? "https://de.sentry.io",
release: { name: release },
sourcemaps: { filesToDeleteAfterUpload: ["**/*.map"] }, // strip maps from shipped bundle
})
: null;
export default defineConfig({
define: { __APP_RELEASE__: JSON.stringify(release) }, // shared with src/lib/sentry.ts
build: { sourcemap: true },
plugins: [/* ... */, ...(sentryPlugin ? [sentryPlugin] : [])],
});
Why gate on SENTRY_AUTH_TOKEN: local builds and any CI job without secrets (PRs from forks, etc.) would otherwise fail in the plugin's auth check. Skipping it cleanly is the right default.
__APP_RELEASE__ define: lets the client SDK pick up the same release tag the plugin uploads against, with no separate env wiring. Just declare const __APP_RELEASE__: string in any file that reads it.
CI env (GitHub Actions) for the deploy job's build step:
- name: Build (with Sentry source map upload)
run: pnpm build
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ronan-connolly
SENTRY_PROJECT: <project-slug>
SENTRY_REGION_URL: https://de.sentry.io
Common mistake: putting the env vars on the wrangler deploy step instead of the pnpm build step. The plugin runs at build time; if it doesn't see the token then, sourcemaps never upload no matter what's set during deploy.
curl -s "${SENTRY_REGION_URL}/api/0/projects/${SENTRY_ORG}/${PROJECT_SLUG}/issues/?statsPeriod=24h&limit=20" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
| jq '.[] | {id, title, level, count: .count, userCount, lastSeen, status}'
curl -s "${SENTRY_REGION_URL}/api/0/issues/${ISSUE_ID}/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
| jq '{title, culprit, platform, permalink, count, userCount, firstSeen, lastSeen}'
curl -s -X PUT "${SENTRY_REGION_URL}/api/0/issues/${ISSUE_ID}/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status": "resolved"}'
Valid statuses: resolved, unresolved, ignored.
Releases pair errors to deploys. Create one per deploy — the skill's release create runs on deploy (pairs well with /ro:cf-ship).
VERSION=$(git rev-parse --short HEAD)
# 1. Create release
curl -s -X POST "${SENTRY_REGION_URL}/api/0/organizations/${SENTRY_ORG}/releases/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d "{
\"version\": \"${VERSION}\",
\"projects\": [\"${PROJECT_SLUG}\"],
\"refs\": [{\"repository\": \"${GH_OWNER:-$(gh repo view --json owner --jq .owner.login)}/${REPO}\", \"commit\": \"${VERSION}\"}]
}"
# 2. Upload source maps (via sentry-cli)
sentry-cli releases files "${VERSION}" upload-sourcemaps ./dist --url-prefix '~/'
# 3. Finalize
curl -s -X PUT "${SENTRY_REGION_URL}/api/0/organizations/${SENTRY_ORG}/releases/${VERSION}/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"dateReleased": "'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'"}'
sentry-cli honours SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_URL env vars — point SENTRY_URL to the region URL for uploads:
export SENTRY_URL=${SENTRY_REGION_URL}
curl -s "${SENTRY_REGION_URL}/api/0/organizations/${SENTRY_ORG}/projects/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
| jq '.[] | {slug, name, platform, id}'
curl -s -X POST "${SENTRY_REGION_URL}/api/0/teams/${SENTRY_ORG}/${TEAM_SLUG}/projects/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app",
"slug": "my-app",
"platform": "javascript-react"
}'
Grab the DSN from the response's keys[0].dsn.public — this is what goes in the app's SENTRY_DSN (per-app, .dev.vars + wrangler secret).
curl -s "${SENTRY_REGION_URL}/api/0/projects/${SENTRY_ORG}/${PROJECT_SLUG}/rules/" \
-H "Authorization: Bearer ${SENTRY_AUTH_TOKEN}" \
| jq '.[] | {id, name, status, conditions: [.conditions[] | .name]}'
Creating alert rules via API is verbose (complex condition/action schemas) — the skill prefers the dashboard for creation and API for listing/auditing.
Global (~/.claude/.env):
SENTRY_AUTH_TOKEN — this skill's management APISENTRY_ORG=ronan-connollySENTRY_URL=https://ronan-connolly.sentry.io/ — for UI permalinks in outputSENTRY_REGION_URL=https://de.sentry.io — for all API callsPer-app (.dev.vars + wrangler secret):
SENTRY_DSN — client + server init. Generate via sentry project create or dashboardSENTRY_PROJECT — project slug (used by Vite plugin)VITE_SENTRY_DSNCI deploy job (GitHub Actions environment secrets):
SENTRY_AUTH_TOKEN — the plugin needs this on the build step, not the deploy stepSENTRY_ORG, SENTRY_PROJECT, SENTRY_REGION_URL — can be inlined in the workflow yaml since they're not secretsFor public open-source apps where forks shouldn't accidentally ship your DSN, expose the DSN via a /api/config endpoint and fetch it at first run instead of inlining at build time. The client SDK initialiser becomes async (await getRuntimeConfig() before Sentry.init), and the worker vars block in wrangler.jsonc reads from CI-provided --var SENTRY_DSN:"...". Tradeoff: one extra network call before Sentry is armed, so the very first error in a session may not be captured. Acceptable for utility apps; not for high-stakes flows.
Ronan's org is on the EU region (de.sentry.io). The UI URL (ronan-connolly.sentry.io) works in browser, but API calls must hit de.sentry.io or you get 404/403. SENTRY_REGION_URL captures this distinction.
SENTRY_AUTH_TOKEN has org-admin scope — NEVER ship it to the client or commit it. Server-only./ro:posthog — the other half of observability/ro:cf-ship — chain release creation + finalize into deploy pipelinedevelopment
--- name: worktree description: Coordinate multiple agents on one repo via a worktree-lock pool, so two agents never clobber each other's working tree. Acquire the first free slot (main, then beta/gamma… worktrees, created on demand), work there on your own branch, release when you've pushed. Use before modifying any repo that might be in use by another agent (factory, dataforce, etc.), or whenever you're told a repo is being worked on. Backed by `ro worktree`. category: development argument-hin
testing
--- name: ship description: Ship a feature branch the local-CI-first way — run the full local gate, push, open a PR, squash-merge, then deploy, without waiting on GitHub Actions. Use when a branch is ready for main and you want it merged and deployed now. Reads CI policy from `ro ci` (default skips remote CI because GitHub Actions billing keeps hitting limits). Sibling to /ro:gh-ship (waits on GitHub checks) and /ro:cf-ship (the deploy half). Triggers on "ship it", "ship this", "merge and deploy
testing
--- name: setup-logging description: Set up (or audit) the observability stack in a TanStack Start + Cloudflare Workers app so it is "diagnosable by default" — structured logging (logtape) with a request context carrying trace_id + userId + tenant/orgId, a trace_id propagated FE→BE→logs→Sentry→PostHog, Cloudflare Workers observability enabled, and Sentry + PostHog wired. Two modes: `setup` (wire it into an app) and `audit` (check an existing app + report gaps). Use when scaffolding a new app, wh
development
Manage credentials INSIDE the active ~/.claude/.env file — read which token/account to use for a given app (Simplicity vs Dataforce vs Ronan-personal), add or update a secret WITHOUT it passing through the chat (an interactive Terminal window prompts for it), and track secrets that were exposed in a transcript so they get rotated. Sibling to /ro:context (which switches WHICH env file is active). Use when the user wants to add an API key/token/secret, asks "which credential do I use for X", needs the env organized/labelled, or a secret was pasted into the chat and should be rotated.