# Next.js SDK

@cohorly/nextjs re-exports <CohorlyProvider>, useCohorly() and <CohorlyErrorBoundary> from the React SDK, adds createCohorlyProxy(), a first-party ingestion and feature-flag route handler, and onRequestError for server-side error tracking on the Node.js and Edge runtimes.

## Install

```bash
pnpm add @cohorly/nextjs
# or: npm install @cohorly/nextjs
```

Requires Next.js 13+ and React 18+ (peer dependencies). Server-side error
tracking (`onRequestError`) needs Next.js 15+.

## Two token modes

There are two ways to attach your project token to ingested data. Pick one.

### 1. Client-side (token in the browser bundle)

Pass `token` directly to `<CohorlyProvider>`. Simplest, but the token is
visible in your client JS.

```tsx
// app/layout.tsx
import { CohorlyProvider } from "@cohorly/nextjs";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <CohorlyProvider token="YOUR_PROJECT_TOKEN" trackPageviews>
          {children}
        </CohorlyProvider>
      </body>
    </html>
  );
}
```

### 2. Server-side proxy (token never leaves your server)

Point the client SDK at a first-party API route and let the route handler
inject the token server-side. Create the route with `createCohorlyProxy()`:

```ts
// app/api/cohorly/[...path]/route.ts
import { createCohorlyProxy } from "@cohorly/nextjs/server";

export const { POST } = createCohorlyProxy({
  token: "YOUR_PROJECT_TOKEN", // injected server-side into every forwarded body
  // apiHost defaults to the hosted Cohorly API; set it to target a different deployment.
});
```

Then point `apiHost` at your own route and omit `token` on the client:

```tsx
// app/layout.tsx
<CohorlyProvider apiHost="/api/cohorly" trackPageviews>
  {children}
</CohorlyProvider>
```

The proxy forwards `POST /api/cohorly/track`, `/engage`, `/alias`, and
`/flags/evaluate` upstream, stamping `token` into each forwarded body
server-side. This keeps
the token out of the browser bundle, hides `apiHost`, and dodges
ad-blockers that block direct requests to third-party analytics hosts.

## createCohorlyProxy(options)

| Option         | Type      | Notes                                                                                       |
| -------------- | --------- | ------------------------------------------------------------------------------------------- |
| `apiHost`      | string    | Optional upstream Cohorly server URL. Defaults to `https://cohorly-service.velloalabs.com`. |
| `token`        | string    | Injected into every forwarded body.                                                         |
| `allowedPaths` | string\[] | Defaults to `["track", "engage", "alias", "flags/evaluate"]`.                               |

Returns a `{ POST }` route handler. Use the client provider and
`useCohorly()` exactly as in the [React SDK](/sdks/react).

## Error tracking

Server-side errors (server components, route handlers, server actions,
middleware) are captured by Next.js's own hook, `onRequestError`, exported
from `instrumentation.ts` (Next.js 15+). Re-export ours:

```ts
// instrumentation.ts (project root, or src/ if you use it)
export { onRequestError } from "@cohorly/nextjs/instrumentation";
```

and set `COHORLY_TOKEN` on the server. That is all: every error Next.js
reports becomes a `$exception` event, the same format as the
[Node SDK](/sdks/node#error-tracking), so it shows up in the error issues and
can be broken down like any other event.

To configure it in code instead of env vars, use `createOnRequestError`:

```ts
// instrumentation.ts
import { createOnRequestError } from "@cohorly/nextjs/instrumentation";

export const onRequestError = createOnRequestError({
  token: process.env.COHORLY_SERVER_TOKEN,
  release: process.env.APP_RELEASE,
  serverName: "web",
});
```

`onRequestError` is exactly `createOnRequestError()` with no options.

### Options

| Option                | Default                                                           | Notes                                                                                   |
| --------------------- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `token`               | `COHORLY_TOKEN`                                                   | Project token. Without one, nothing is captured and nothing is sent.                    |
| `apiHost`             | `COHORLY_API_HOST`, else `https://cohorly-service.velloalabs.com` | Cohorly API base URL.                                                                   |
| `release`             | `VERCEL_GIT_COMMIT_SHA`                                           | Sent as `$exception_release` and `$app_version_string`. An empty value counts as unset. |
| `dist`                | -                                                                 | Build inside the release, sent as `$exception_dist`.                                    |
| `serverName`          | -                                                                 | Sent as `server_name`, and used as the exception's `distinct_id` (default `"server"`).  |
| `flushTimeoutMs`      | `2000`                                                            | How long the hook waits for the exception to be sent.                                   |
| `beforeSendException` | -                                                                 | Change or drop (`null`) an exception before it is sent. A hook that throws drops it.    |
| `debug`               | `false`                                                           | Log failures with `console.warn`.                                                       |

Options win over env vars.

### What is sent

Each event carries `$exception_list` (the error and its `cause` chain, with
the parsed stack), `$exception_level: "error"`, mechanism
`nextjs.onRequestError` (unhandled), the release, and these properties:

| Property                                            | From                                                                            |
| --------------------------------------------------- | ------------------------------------------------------------------------------- |
| `route`                                             | The route pattern, e.g. `/blog/[slug]`                                          |
| `method`                                            | The HTTP method                                                                 |
| `router_kind`                                       | `App Router` or `Pages Router`                                                  |
| `route_type`                                        | `render`, `route`, `action` or `middleware`                                     |
| `render_source`, `revalidate_reason`, `render_type` | Only when Next.js sets them                                                     |
| `request_path`                                      | The request path **without** its query string and fragment                      |
| `digest`                                            | The error's `digest`, the id Next.js shows in production instead of the message |
| `runtime`                                           | `node` or `edge`                                                                |

**Never sent:** request headers, cookies and the query string. Tokens and
e-mails often travel in them, so the hook does not read them at all.

### Node.js and Edge runtimes

`@cohorly/nextjs/instrumentation` resolves to a different build per runtime,
through package export conditions: the Edge runtime (and other worker
runtimes, `edge-light`, `workerd`, `worker`) gets a build that only uses
`fetch`, `AbortController` and `crypto`, while Node.js gets one built on
`@cohorly/node`. Next.js picks the right one when it compiles
`instrumentation.ts` for each runtime, so the same one-line re-export works
for both and the Edge bundle never pulls in Node APIs.

- **Node.js:** one `@cohorly/node` client per process, created on the first
  error; `$lib` is `node`.
- **Edge:** the event is POSTed straight to `/track`; `$lib` is
  `nextjs-edge`.

Which build reports an error depends on where Next.js runs the code. On
Vercel an Edge route runs in the Edge runtime, so the Edge build reports it. With a
self-hosted `next start`, the Node.js server calls the hook for Edge routes
too, so those events carry `$lib: "node"` and `runtime: "node"`.

### Serverless: the hook waits for the send

On Vercel and other serverless hosts the function can freeze as soon as the
response is sent. The hook therefore resolves only after the exception was
sent, or after `flushTimeoutMs` (the request is then aborted). It never
rejects and never changes or rethrows the error: Next.js keeps handling it
as it would without Cohorly.

### Client errors: CohorlyErrorBoundary and global-error.tsx

Errors thrown in the browser are captured by the browser SDK: pass
`captureExceptions` to `<CohorlyProvider>`, and wrap a client subtree you want
a fallback for in `<CohorlyErrorBoundary>`, which `@cohorly/nextjs` re-exports
from the [React SDK](/sdks/react). Do not wrap the root layout's `children`
with it: an error from a Server Component also reaches the browser with a
`digest`, and a boundary would capture that sanitized copy on top of the full
server-side capture.

```tsx
// app/layout.tsx
import { CohorlyProvider } from "@cohorly/nextjs";

<CohorlyProvider token={process.env.NEXT_PUBLIC_COHORLY_TOKEN} captureExceptions>
  {children}
</CohorlyProvider>
```

`@cohorly/nextjs` resolves to a module marked `"use client"`, so
`<CohorlyErrorBoundary fallback={<p>Something went wrong</p>}>` works in a
Server Component too. Passing a function (`fallback` as a function, `onError`)
needs a Client Component, because functions cannot cross the server/client
boundary. A `fallback` function receives the error and a `reset` that renders
the children again; `onError` runs after the error was captured.

```tsx
// app/checkout/checkout-panel.tsx
"use client";

import { CohorlyErrorBoundary } from "@cohorly/nextjs";

export function CheckoutPanel({ children }: { children: React.ReactNode }) {
  return (
    <CohorlyErrorBoundary
      fallback={({ reset }) => (
        <div role="alert">
          <p>The checkout failed to load.</p>
          <button onClick={reset}>Try again</button>
        </div>
      )}
      onError={(error, componentStack) => console.warn("checkout crashed", error)}
    >
      {children}
    </CohorlyErrorBoundary>
  );
}
```

In development React 18 also reports an error a boundary caught to `window`;
with `captureExceptions` on, an error with the same type, message and crashing
frame seen by the global handler and by the boundary within one second is sent
once. The event that stays is the one from the global handler (level `fatal`).

An error that reaches the root layout renders `app/global-error.tsx`, which
replaces the layout. Capture it there only when it has no `digest`: an error
with a `digest` came from the server, where `onRequestError` already
captured it, and capturing it again would count it twice.

```tsx
// app/global-error.tsx
"use client";

import { cohorly } from "@cohorly/web";
import { useEffect } from "react";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    if (error.digest !== undefined) return;
    try {
      cohorly.captureException(error, {
        mechanism: { type: "nextjs.global-error", handled: true },
      });
    } catch {
      // The browser SDK was never initialized.
    }
  }, [error]);

  return (
    <html lang="en">
      <body>
        <h2>Something went wrong.</h2>
        <button type="button" onClick={() => reset()}>Try again</button>
      </body>
    </html>
  );
}
```

`cohorly` is the browser SDK singleton the provider initialized, so add
`@cohorly/web` to your app's dependencies for this file.

### Releases and readable stacks

Use the same release string everywhere: for the hook (`release`, or
`VERCEL_GIT_COMMIT_SHA` on Vercel) and for
`cohorly sourcemaps upload --release` ([CLI](/cli)), so an error's frames
can be matched to the maps uploaded for that build. Until source map upload
for Next.js server bundles ships, production frames point at the compiled
files under `.next/server`, not at your sources.

A full example app (server component, Node.js and Edge route handlers,
`global-error.tsx`) lives in `packages/sdk-nextjs/examples/app-router` in
the SDK repository.
