← Public packages
@kentcdodds/sentry-triage
Sentry triage wakes Cole (Grok Bot) per issue: one active wake per repo (lease + queue), loop-safe Discord. Cole may spawn Cursor for isolated repo work.
src/shared.ts
317 lines · 14.0 KB · TypeScriptexport const discordChannelId = '1530233790516039881'
export const sentryOrgSlug = 'kent-c-dodds-tech-llc'
export const agentModelId = 'grok-4.6'
/**
* Model for ship agents (review + ship a Seer-drafted PR). The investigation
* is already done (Seer RCA + Seer code change), so the job is review, test,
* and CI iteration — Composer's speed fits it.
*/
export const shipAgentModelId = 'composer-2.5'
/** Max cloud agents spawned per rolling clock hour (across all projects). */
export const maxAgentsPerHour = 10
/** Issues seen per hour above this suggests an incident/loop; triage pauses. */
export const issueAnomalyThresholdPerHour = 30
/**
* Seer-first triage: on a new issue, request a Seer root-cause analysis and
* let the `seer.root_cause_completed` webhook wake Cole with the RCA,
* instead of immediately investigating from scratch. Sentry's Seer does the
* investigation once (server-side), and our agent starts from a verified
* hypothesis — the token/latency sink we measured was investigation, not
* fetching. Falls back to from-scratch triage when Seer cannot run (see the
* handler) so nothing is ever silently dropped.
*/
export const seerFirstEnabled = true
/**
* If an issue is still awaiting a Seer RCA this long after we requested it
* (Seer stalled or produced nothing), a later delivery triages it from
* scratch instead of waiting forever.
*/
export const seerGraceMs = 15 * 60 * 1000
/**
* Seer-PR-first: when a project's Seer stopping point is "open_pr", the RCA
* webhook holds the issue (awaiting-seer-pr) while Seer continues through
* solution/coding to a draft PR; `seer.pr_created` then wakes Cole for ship review
* on that PR. If the PR never arrives within this window (Seer coding can
* fail), a later delivery or the reconciler falls back to an investigation
* agent seeded with the stored RCA.
*/
export const seerPrGraceMs = 45 * 60 * 1000
/**
* Sentry projects under triage. One Sentry internal-integration webhook
* covers the whole org; this registry decides which projects get triage
* agents and which repository each agent works in. Issues from projects not
* listed here are recorded as skipped.
*
* Per-project prompt content (facts, filter idiom, risk guide) keeps the
* agent prompt accurate for each codebase — a wrong "repo fact" is worse
* than none.
*/
export const triageProjects = [
{
slug: 'kody',
slugAliases: ['kody-cloudflare'],
projectId: '4511072505036800',
repository: 'https://github.com/kentcdodds/kody',
repoSlug: 'kentcdodds/kody',
contextNote: 'production worker "kody-production"',
repoFacts: [
'- `npm run validate` is the single authoritative gate; CI runs exactly it.',
'- Sentry wiring: `packages/worker/src/sentry-options.ts` (options +',
' `beforeSend` filters, e.g. `filterRetryableD1LockSentryEvent`);',
' MCP failure capture: `packages/worker/src/mcp/observability.ts`.',
'- Issues titled "Unknown: ..." with tag `mcp.sandbox_error=true` are user',
' code failing inside the execute sandbox, not platform bugs — but per',
' the no-user-error principle, a RECURRING sandbox mistake pattern may',
' signal a missing validation, unclear error message, or capability-doc',
' gap worth eliminating.',
'- Kody MCP execute calls can transiently time out (MCP error -32001): retry',
' idempotent reads once before treating them as failures.',
].join('\n'),
filterGuidance: [
'The repo idiom is a targeted `beforeSend` filter in',
'`packages/worker/src/sentry-options.ts` — see',
'`filterRetryableD1LockSentryEvent` for the precedent (keep filters narrow:',
'match the specific error signature, never blanket-drop).',
].join('\n'),
riskGuide: 'classify per `.agents/skills/visual-recap/SKILL.md`',
},
{
slug: 'kody-home-connector',
projectId: '4511105963655168',
repository: 'https://github.com/kentcdodds/kody-home-connector',
repoSlug: 'kentcdodds/kody-home-connector',
contextNote:
"the local-network 'home' remote connector for Kody (Sonos, Bond, Venstar, Roku, Samsung TV, Lutron, JellyFish, Kasa)",
repoFacts: [
'- `npm run validate` is the gate (format:check + oxlint + vitest). Use',
' Node 24 and npm. Follow AGENTS.md.',
"- Long-running service on Kent's home network; shared connector protocol",
' helpers come from `@kody-bot/connector-kit`. Device handlers live in',
' `app/*-handlers.ts` with MSW-backed device mocks under `mocks/`.',
'- Sentry wiring: `src/sentry-init.ts` (startup init) and `src/sentry.ts`',
' (capture helpers with `shouldCapture`, fingerprints, and TTL dedupe;',
' tests in `src/sentry.node.test.ts`).',
'- Many errors reflect flaky local devices or network (timeouts,',
' unreachable hosts, device quirks) rather than code bugs — check whether',
' the failure is an expected device condition the code should tolerate.',
].join('\n'),
filterGuidance: [
'The repo idiom is the capture-helper layer in `src/sentry.ts`: prefer a',
'`shouldCapture` condition, a TTL `dedupe`, or a fingerprint for the',
'specific error signature over blanket drops, and extend',
'`src/sentry.node.test.ts` alongside.',
].join('\n'),
riskGuide: 'classify the risk yourself from the blast radius of the change',
},
{
slug: 'epicshop',
projectId: '4509630082252800',
repository: 'https://github.com/epicweb-dev/epicshop',
repoSlug: 'epicweb-dev/epicshop',
contextNote: 'the EpicWeb.dev workshop app',
repoFacts: [
'- npm workspaces monorepo (`packages/*`): `workshop-app` (the local web',
' app), `workshop-cli`, `workshop-mcp`, `workshop-utils`, and more.',
'- `npm run validate` is the authoritative gate (build, typecheck, lint,',
' test via nx). Follow AGENTS.md.',
'- This app runs locally on learners\' machines (macOS/Windows/Linux, varied',
' Node versions, sometimes offline). Many errors originate in environments',
' you cannot reproduce; weigh the "filtered" outcome accordingly, but keep',
' every filter narrow.',
'- Sentry wiring: server init `packages/workshop-app/instrument.js` with',
' filters in `packages/workshop-app/sentry-server-filters.js` (tests:',
' `packages/workshop-app/tests/sentry-filters.test.ts`); client',
' `packages/workshop-app/app/utils/monitoring.client.ts` with',
' `packages/workshop-app/app/utils/sentry-filters.ts`; CLI',
' `packages/workshop-cli/src/utils/sentry-cli-filters.ts`; MCP',
' `packages/workshop-mcp/src/sentry-filters.ts`.',
].join('\n'),
filterGuidance: [
'Add the filter to the module for the surface that emitted the event —',
'server: `packages/workshop-app/sentry-server-filters.js`, client:',
'`packages/workshop-app/app/utils/sentry-filters.ts`, CLI:',
'`packages/workshop-cli/src/utils/sentry-cli-filters.ts`, MCP:',
'`packages/workshop-mcp/src/sentry-filters.ts` — and extend the existing',
'tests beside each one. Match the specific error signature, never',
'blanket-drop.',
].join('\n'),
riskGuide: 'classify the risk yourself from the blast radius of the change',
},
{
slug: 'kody-video',
projectId: '4511810800713728',
repository: 'https://github.com/kentcdodds/kody-video',
repoSlug: 'kentcdodds/kody-video',
contextNote:
'Kody Video — a privacy-first, on-device PWA clips camera on Cloudflare Pages (kody.video)',
repoFacts: [
'- Gates: `npm run build` (typecheck + production build), `npx vitest run`',
' (unit tests), and `node scripts/manual-smoke.mjs` (Playwright smoke;',
' needs `npx playwright install chromium`). All three must pass.',
'- Sentry wiring: `src/lib/error-reporting.ts` — init is gated to the',
' production hostnames, errors-only (no tracing, replay, or PII).',
'- Entirely client-side (IndexedDB storage, WebCodecs export, MediaRecorder',
' fallback, no server) except `functions/api/verify-purchase.ts` (Stripe).',
' README.md documents the architecture; there is no AGENTS.md.',
'- Errors are often device/browser-specific (camera, codecs, permissions,',
' storage quota): check whether the stack points at a hardware-dependent',
' path only some devices hit before assuming a logic bug. The browser/OS',
' tags on the event are load-bearing evidence here.',
].join('\n'),
filterGuidance: [
'Add a narrow `beforeSend` filter in `src/lib/error-reporting.ts` matching',
'the specific error signature — never blanket-drop, and never add data',
'collection: this is a privacy-focused app whose policy promises',
'errors-only reporting.',
].join('\n'),
riskGuide: 'classify the risk yourself from the blast radius of the change',
},
{
slug: 'kcd',
projectId: '5878963',
repository: 'https://github.com/kentcdodds/kentcdodds.com',
repoSlug: 'kentcdodds/kentcdodds.com',
contextNote: 'the kentcdodds.com website',
repoFacts: [
'- READ `docs/agents/bugfix-workflow.md` FIRST — it has a dedicated "Sentry',
' triage" section (reproduce before fixing; only filter with confirmed',
' external evidence; client `RouteErrorResponse: 502 Route Error` usually',
' wraps Cloudflare edge Bad Gateway noise, confirm via',
' `extra.route_error_response.data`).',
'- npm workspaces monorepo; the site lives in `services/site`; root',
' `npm run validate` runs the site workspace validate. Follow AGENTS.md.',
'- Runs on Cloudflare Workers (workerd) — see',
' `docs/agents/cloudflare-worker-architecture.md`. Stack traces that look',
' like stale Fly/Express noise may still reproduce on workerd.',
'- Client Sentry noise filters: `services/site/app/utils/sentry-noise.ts`',
' (wired from `monitoring.client.tsx`, tests in',
' `services/site/app/utils/__tests__/sentry-noise.test.ts`).',
].join('\n'),
filterGuidance: [
'Follow the "Sentry triage" section of `docs/agents/bugfix-workflow.md`:',
'client noise filters live in `services/site/app/utils/sentry-noise.ts`',
'(narrow `ignoreErrors` / `denyUrls` / `beforeSend` signatures, tests in',
'`__tests__/sentry-noise.test.ts`). Every drop rule must establish an',
'external source; never broadly filter `Failed to fetch` / network errors —',
'those can hide real outages.',
].join('\n'),
riskGuide: 'classify the risk yourself from the blast radius of the change',
},
{
slug: 'kody-exchange',
projectId: '4511934498734080',
repository: 'https://github.com/kentcdodds/kody-exchange',
repoSlug: 'kentcdodds/kody-exchange',
contextNote: 'production worker "kody-exchange" at kody.exchange',
repoFacts: [
'- `npm run validate` is the single authoritative gate; CI runs exactly it.',
'- Cloudflare Worker (wrangler.jsonc) with D1, KV, R2, and Durable Object',
' `ThreadRoom`. No PR preview deploys; production deploys from `main` after',
' Validate succeeds.',
'- Production Worker secrets are synced from GitHub Actions by',
' `tools/ci/sync-worker-secrets.ts`. Do not `wrangler secret put` by hand.',
'- Guest is one live thread per IP via REST `POST /v1/threads`. `/mcp` and',
' `/api/` require an OAuth access token from a free GitHub account.',
'- Sentry wiring: `src/sentry-options.ts` (`withSentry` + ThreadRoom',
' `instrumentDurableObjectWithSentry`, `beforeSend` filters for retryable',
' D1 platform noise and bare Durable Object isolate resets).',
].join('\n'),
filterGuidance: [
'The repo idiom is a targeted `beforeSend` filter in',
'`src/sentry-options.ts` — see `filterRetryableD1PlatformSentryEvent` and',
'`filterDurableObjectIsolateResetSentryEvent` for the precedent (keep',
'filters narrow: match the specific error signature, never blanket-drop).',
'Extend `src/sentry-options.node.test.ts` alongside.',
].join('\n'),
riskGuide: 'classify the risk yourself from the blast radius of the change',
},
]
export function findTriageProjectForSlug(projects, slug) {
if (!slug) return null
return (
projects.find(
(project) =>
project.slug === slug || project.slugAliases?.includes(slug),
) ?? null
)
}
export function findTriageProjectForProjectId(projects, projectId) {
if (projectId == null) return null
return (
projects.find(
(project) => String(project.projectId) === String(projectId),
) ?? null
)
}
export function triageProjectForSlug(slug) {
return findTriageProjectForSlug(triageProjects, slug)
}
export function triageProjectForProjectId(projectId) {
return findTriageProjectForProjectId(triageProjects, projectId)
}
export function hourBucket(date = new Date()) {
return date.toISOString().slice(0, 13)
}
export function issueStorageKey(issueId) {
return `issue:${issueId}`
}
/**
* Lower-kebab-case slug for storage keys derived from idempotency strings
* (colons, slashes, etc. become hyphens).
*/
export function toLowerKebabCase(value) {
return String(value ?? '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
/**
* Stable packageStorage key for a staged Sentry webhook body. Derived from
* the workflow idempotency key so retries overwrite the same slot and the
* durable processor can load a bounded `{ payloadKey }` reference instead of
* embedding the full Sentry JSON in `workflows.create` params (16KB cap).
*/
export function webhookPayloadStorageKey(idempotencyKey) {
return `webhook-payload:${toLowerKebabCase(idempotencyKey)}`
}
export function spawnedCounterKey(bucket = hourBucket()) {
return `spawned:${bucket}`
}
export function issuesSeenCounterKey(bucket = hourBucket()) {
return `issues-seen:${bucket}`
}
export function alertSentKey(kind, bucket = hourBucket()) {
return `alerted:${kind}:${bucket}`
}
export function truncate(text, maxLength) {
const value = String(text ?? '')
return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`
}
/**
* Sanitize attacker-controllable text before embedding it in an agent prompt.
* Backticks are replaced so error content can never close the prompt's code
* fences and impersonate prompt-level instructions, and « » are stripped so
* it cannot forge the prompt's secret-placeholder encoding.
*/
export function sanitizeForPrompt(text) {
return String(text ?? '')
.replaceAll('`', 'ˋ')
.replaceAll('«', '<')
.replaceAll('»', '>')
}