Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cloud/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,23 @@ HR_API_KEY=
#
# Generate with: openssl rand -base64 32 (must decode to exactly 32 bytes)
GHOST_MFA_KEY=""

# ---------------------------------------------------------------------------
# Sentry (optional — error tracking + tracing)
# ---------------------------------------------------------------------------
# Every value below absent means fully disabled: no network calls, nothing
# captured, nothing shipped anywhere — same opt-in contract as HR_API_KEY and
# S3_BUCKET. Create a project at https://sentry.io to get a DSN, or run
# `npx @sentry/wizard@latest -i nextjs` from apps/web to provision one and
# fill these in interactively.
#
# SENTRY_DSN also covers apps/worker (@ghost/core/sentry) — see docs/DEPLOY.md.
# SENTRY_DSN="https://examplePublicKey@o0.ingest.sentry.io/0"
# NEXT_PUBLIC_SENTRY_DSN="https://examplePublicKey@o0.ingest.sentry.io/0"

# Build-time only — enables source map upload during `next build` so
# production stack traces show real source instead of minified output.
# Leaving this unset skips upload rather than failing the build.
# SENTRY_AUTH_TOKEN=""
# SENTRY_ORG=""
# SENTRY_PROJECT=""
19 changes: 19 additions & 0 deletions cloud/apps/web/instrumentation-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as Sentry from "@sentry/nextjs";

/**
* Browser runtime. `NEXT_PUBLIC_SENTRY_DSN` absent means fully disabled — no
* network calls, nothing captured — mirroring the worker's `SENTRY_DSN`
* opt-in (`@ghost/core/sentry`). A deployment is not required to have a
* Sentry account to boot or run correctly.
*/
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
environment: process.env.NODE_ENV,

// 100% in dev, 10% in production.
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
});

// Hooks into App Router navigation transitions so route changes show up as
// tracing spans.
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;
16 changes: 16 additions & 0 deletions cloud/apps/web/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Sentry from "@sentry/nextjs";

export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}

if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}

// Captures unhandled server-side request errors (server components, route
// handlers, server actions) that would otherwise never reach
// `Sentry.captureException`.
export const onRequestError = Sentry.captureRequestError;
13 changes: 12 additions & 1 deletion cloud/apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// people to create was never picked up — see packages/core/src/env.ts.
import "@ghost/core/env";
import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig: NextConfig = {
// @ghost/core ships TS source via subpath exports; Next transpiles it.
Expand All @@ -28,4 +29,14 @@ const nextConfig: NextConfig = {
},
};

export default nextConfig;
// org/project fall back to the SENTRY_ORG/SENTRY_PROJECT env vars when unset
// here, and authToken to nothing — matching the rest of this file's
// opt-in-via-env-var pattern (see @ghost/core/sentry for the same contract on
// the worker side). Without SENTRY_AUTH_TOKEN the plugin skips source map
// upload rather than failing the build.
export default withSentryConfig(nextConfig, {
authToken: process.env.SENTRY_AUTH_TOKEN,
widenClientFileUpload: true,
tunnelRoute: "/monitoring",
silent: !process.env.CI,
});
1 change: 1 addition & 0 deletions cloud/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@auth/prisma-adapter": "^2.11.3",
"@ghost/core": "workspace:*",
"@sentry/nextjs": "^10.69.0",
"bullmq": "^5.34.4",
"clsx": "^2.1.1",
"ioredis": "^5.4.2",
Expand Down
13 changes: 13 additions & 0 deletions cloud/apps/web/sentry.edge.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Sentry from "@sentry/nextjs";

/**
* Edge runtime (`src/middleware.ts`). `SENTRY_DSN` absent means fully
* disabled — same opt-in contract as the other two runtime configs.
*/
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,

// 100% in dev, 10% in production.
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
});
14 changes: 14 additions & 0 deletions cloud/apps/web/sentry.server.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Sentry from "@sentry/nextjs";

/**
* Node.js server runtime. `SENTRY_DSN` absent means fully disabled — same
* opt-in contract as `instrumentation-client.ts` and the worker's
* `@ghost/core/sentry`.
*/
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,

// 100% in dev, 10% in production.
tracesSampleRate: process.env.NODE_ENV === "development" ? 1.0 : 0.1,
});
20 changes: 20 additions & 0 deletions cloud/apps/web/src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";

/** Catches errors thrown by the root layout and otherwise-unhandled React render errors. */
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);

return (
<html>
<body>
<NextError statusCode={0} />
</body>
</html>
);
}
27 changes: 17 additions & 10 deletions cloud/docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ B2, or MinIO.
| `APP_URL` | ● | ● | the demo fixture is unreachable from the worker |
| `NEXT_PUBLIC_SOURCE_URL` | ○ | | AGPL §13 link points at upstream, not your fork |
| `ARTIFACT_RETENTION_DAYS` | | ○ | defaults to 90; the `purge-artifacts` job (scheduled by the worker itself, see `apps/worker/src/index.ts`) deletes a run's screenshots once it ended this many days ago |
| `SENTRY_DSN` | | ○ | **worker only.** Error tracking stays off without it, matching `HR_API_KEY`/`S3_BUCKET` (see `@ghost/core/sentry`). `apps/web` is not wired to this — `@sentry/node`'s auto-instrumentation cannot be webpack-bundled (tried, broke `pnpm build`); wiring web needs `@sentry/nextjs` via `npx @sentry/wizard@latest -i nextjs` against a real Sentry project |
| `SENTRY_DSN` | ○ | ○ | Error tracking + tracing stay off without it, matching `HR_API_KEY`/`S3_BUCKET`. Worker: `@ghost/core/sentry` (`@sentry/node`). Web (server + edge runtimes): `sentry.server.config.ts` / `sentry.edge.config.ts` (`@sentry/nextjs`) |
| `NEXT_PUBLIC_SENTRY_DSN` | ○ | | **web only**, browser runtime (`instrumentation-client.ts`). Usually the same DSN as `SENTRY_DSN`; a separate var because it's inlined into the client bundle at build time |
| `SENTRY_AUTH_TOKEN` / `SENTRY_ORG` / `SENTRY_PROJECT` | ○ | | **build-time, web only.** Enables source map upload during `next build` (`next.config.ts`'s `withSentryConfig`) so production stack traces show real source instead of minified output; unset just skips the upload |

`GHOST_SESSION_KEY` is **worker-only** by design. It decrypts captured browser
sessions — live cookies for the customer's systems — and the web app has no
Expand Down Expand Up @@ -288,15 +290,20 @@ another):
requirement the host's UI assumes by default and configure it as a
background worker / long-running process instead.

**Sentry (optional)** — create a project, generate a DSN, and set
`SENTRY_DSN` on the container host only; this alone gives the worker error
tracking, since `apps/worker` is already wired against `@ghost/core/sentry`
(see that file for why the wrapper doesn't try to also cover `apps/web`).
Wiring `apps/web` needs `@sentry/nextjs`, not this env var — run
`npx @sentry/wizard@latest -i nextjs` against the same Sentry project once the
Vercel project above exists, since the wizard needs a real project to
configure against and writes files (`instrumentation-client.ts`,
`sentry.server.config.ts`, a `next.config.ts` wrapper) this repo does not ship.
**Sentry (optional)** — create a project, generate a DSN, and set `SENTRY_DSN`
on both hosts: the worker reads it via `@ghost/core/sentry`
(`apps/worker/src/index.ts`), and `apps/web`'s server + edge runtimes read it
via `sentry.server.config.ts` / `sentry.edge.config.ts`. Also set
`NEXT_PUBLIC_SENTRY_DSN` (usually the same value) on the web host for
`instrumentation-client.ts`, the browser runtime — it's inlined into the
client bundle at build time, so setting it only at runtime on the host has no
effect. For readable production stack traces, set `SENTRY_AUTH_TOKEN`,
`SENTRY_ORG`, and `SENTRY_PROJECT` at **build time** on the web host so
`next.config.ts`'s `withSentryConfig` uploads source maps on `next build`; any
of the three absent just skips the upload. Alternatively, run
`npx @sentry/wizard@latest -i nextjs` against the same Sentry project — it can
reconfigure these files interactively and wire up the Vercel integration for
you, but isn't required since the wiring already ships in this repo.

## Rolling back

Expand Down
13 changes: 8 additions & 5 deletions cloud/packages/core/src/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ import * as Sentry from "@sentry/node";
* a capability and nothing else changes when it is unset. A deployment is not
* required to have a Sentry account to boot or run correctly.
*
* `apps/web` is NOT wired to this module. `@sentry/node`'s auto-instrumentation
* `apps/web` does NOT use this module. `@sentry/node`'s auto-instrumentation
* (`import-in-the-middle`, `@sentry/node-core`) needs `node:child_process` and
* `require('module')` at module-load time, which Next.js's webpack build
* cannot bundle even with the package marked `serverExternalPackages` — this
* was tried and broke `pnpm build` outright (`UnhandledSchemeError` on
* `node:child_process` reached from `instrumentation.ts`). The worker has no
* such constraint: `apps/worker/tsup.config.ts` marks `@sentry/node` fully
* external, so Node resolves it normally from `node_modules` at runtime
* instead of webpack trying to statically bundle it. Wiring Sentry into
* `apps/web` needs `@sentry/nextjs` (its own webpack/turbopack plugin exists
* specifically to handle this), set up via `npx @sentry/wizard@latest -i
* nextjs` against a real Sentry project — see the deploy checklist.
* instead of webpack trying to statically bundle it. `apps/web` instead has
* its own, separate `@sentry/nextjs` setup (its webpack/turbopack plugin
* exists specifically to handle this): `instrumentation.ts`,
* `sentry.server.config.ts`, `sentry.edge.config.ts`,
* `instrumentation-client.ts`, and the `withSentryConfig` wrap in
* `next.config.ts`, all reading the same `SENTRY_DSN` / `NEXT_PUBLIC_SENTRY_DSN`
* env vars — see `docs/DEPLOY.md`.
*/

let initialized = false;
Expand Down
Loading
Loading