193 lines
7.8 KiB
TypeScript
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'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.',
|
|
],
|
|
};
|