Files
vibn-agent-runner/vibn-frontend/lib/scaffold/sentry-snippets.ts

193 lines
7.8 KiB
TypeScript

/**
* Canonical Sentry wiring snippets the AI should drop into any
* new app it scaffolds. Keeping these in one place (and naming
* the file paths explicitly) means the AI's outputs are
* deterministic across chats — every Next.js app it scaffolds
* gets the same instrumentation files, the same global error
* boundary, and the same `withSentryConfig` wrapper.
*
* The runtime env vars `NEXT_PUBLIC_SENTRY_DSN` and
* `SENTRY_AUTH_TOKEN` are guaranteed to exist on every Coolify
* app created via apps.create with a `projectId` — see
* `lib/integrations/sentry.ts` and `applyEnvsAndDeploy` in
* `app/api/mcp/route.ts`.
*
* The AI references these via `getSentrySnippets("nextjs")` etc.
* Authoring rule: when changing any snippet, also bump the
* matching file in vibn-frontend itself so they stay in sync —
* vibn-frontend is the reference implementation.
*/
export type SentryFramework = 'nextjs' | 'vite-react';
export interface SentrySnippet {
/** Repo-relative path the file should land at. */
path: string;
/** File contents, copied verbatim by the AI. */
contents: string;
/** Short description for AI to mention to the user, if relevant. */
purpose: string;
}
export interface SentryWiringPackage {
framework: SentryFramework;
/** npm dependencies the AI must add to package.json. */
dependencies: string[];
/** Files to write into the project. */
files: SentrySnippet[];
/** Free-form modifications the AI must apply to existing files. */
modifications: string[];
}
/**
* Returns the full set of files + dependency edits + free-form
* modifications the AI must apply when scaffolding a new app of
* this framework. Currently covers Next.js (App Router) and
* Vite + React.
*/
export function getSentrySnippets(framework: SentryFramework): SentryWiringPackage {
switch (framework) {
case 'nextjs':
return NEXTJS_PACKAGE;
case 'vite-react':
return VITE_REACT_PACKAGE;
default: {
const exhaustive: never = framework;
throw new Error(`Unsupported Sentry framework: ${exhaustive}`);
}
}
}
// ──────────────────────────────────────────────────────────────────
// Next.js (App Router, Next 14+)
// ──────────────────────────────────────────────────────────────────
const NEXTJS_PACKAGE: SentryWiringPackage = {
framework: 'nextjs',
dependencies: ['@sentry/nextjs'],
files: [
{
path: 'instrumentation.ts',
purpose: 'Server + edge runtime Sentry init',
contents: `import * as Sentry from '@sentry/nextjs';
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
enabled: Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN),
environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV,
});
}
if (process.env.NEXT_RUNTIME === 'edge') {
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
enabled: Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN),
environment: process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV,
});
}
}
export const onRequestError = Sentry.captureRequestError;
`,
},
{
path: 'instrumentation-client.ts',
purpose: 'Browser Sentry init with Session Replay',
contents: `import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
enabled: Boolean(process.env.NEXT_PUBLIC_SENTRY_DSN),
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || process.env.NODE_ENV,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
});
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
`,
},
{
path: 'app/global-error.tsx',
purpose: 'Catches root-layout crashes that escape every other error boundary',
contents: `"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", padding: "2rem", fontFamily: "system-ui, sans-serif" }}>
<h1 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>Something went wrong</h1>
<p style={{ color: "#666" }}>We&apos;ve been notified. Try refreshing.</p>
{error.digest ? <code style={{ fontSize: "0.75rem", color: "#999", marginTop: "1rem" }}>ref: {error.digest}</code> : null}
</div>
</body>
</html>
);
}
`,
},
],
modifications: [
'In next.config.ts (or next.config.js), import withSentryConfig from "@sentry/nextjs" and wrap the exported config with withSentryConfig(nextConfig, { org: "vibnai", project: "<sentry-project-slug>", silent: !process.env.CI, widenClientFileUpload: true, tunnelRoute: "/monitoring", telemetry: false, errorHandler: (err) => console.warn("Sentry source map upload skipped:", err.message) }).',
'In Dockerfile (if the project uses Docker), add ARG NEXT_PUBLIC_SENTRY_DSN and ARG SENTRY_AUTH_TOKEN in the builder stage, then ENV NEXT_PUBLIC_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN and ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN — without these the build args from Coolify never reach `next build`.',
'The Sentry project slug for this Vibn project is fs_projects.data.sentry.slug — fetch it from the projects/get MCP tool and substitute into the next.config.ts above.',
],
};
// ──────────────────────────────────────────────────────────────────
// Vite + React
// ──────────────────────────────────────────────────────────────────
const VITE_REACT_PACKAGE: SentryWiringPackage = {
framework: 'vite-react',
dependencies: ['@sentry/react', '@sentry/vite-plugin'],
files: [
{
path: 'src/sentry.ts',
purpose: 'Sentry init, imported once at the top of main.tsx',
contents: `import * as Sentry from "@sentry/react";
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
enabled: Boolean(import.meta.env.VITE_SENTRY_DSN),
environment: import.meta.env.MODE,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.browserTracingIntegration(),
Sentry.replayIntegration({ maskAllText: true, blockAllMedia: true }),
],
});
`,
},
],
modifications: [
'In src/main.tsx, add `import "./sentry"` as the FIRST import line (before React, before App).',
'In vite.config.ts, add the Sentry vite plugin: import { sentryVitePlugin } from "@sentry/vite-plugin"; then in plugins: [sentryVitePlugin({ org: "vibnai", project: "<sentry-project-slug>", authToken: process.env.SENTRY_AUTH_TOKEN, telemetry: false, errorHandler: (err) => console.warn(err.message) })]. Set build.sourcemap = true so source maps generate.',
'Vite uses VITE_SENTRY_DSN (not NEXT_PUBLIC_SENTRY_DSN). Add `VITE_SENTRY_DSN=$NEXT_PUBLIC_SENTRY_DSN` as a Coolify env var alias on the app, OR have the AI rename the env when injecting via apps_create.',
'Wrap the root <App /> in <Sentry.ErrorBoundary fallback={<p>Something broke</p>}><App /></Sentry.ErrorBoundary> so React render errors propagate to Sentry.',
],
};