# @thedaviddias/analytics
Analytics package powered by **OpenPanel**. Drop the provider into your Next.js layout to get pageviews, outbound link tracking, tagged events, and user identification out of the box.
## Exports
| Import path | Description |
| ------------------------------------------------------ | ------------------------------------------------------------------------ |
| `@thedaviddias/analytics` | `AnalyticsProvider` — renders OpenPanel script tags |
| `@thedaviddias/analytics/head` | `AnalyticsHead` — thin wrapper for use inside `
` |
| `@thedaviddias/analytics/server` | `opServer` — server-side OpenPanel SDK singleton |
| `@thedaviddias/analytics/providers/openpanel-identify` | `OpenPanelIdentify` — client component that syncs auth user to OpenPanel |
## Environment variables
| Variable | Required | Scope | Description |
| --------------------------------- | ----------------------------- | --------------- | --------------------------------------------------------------- |
| `NEXT_PUBLIC_OPENPANEL_CLIENT_ID` | Yes (for OpenPanel) | Client + Server | OpenPanel client ID from [openpanel.dev](https://openpanel.dev) |
| `OPENPANEL_CLIENT_SECRET` | Only for server-side tracking | Server only | OpenPanel client secret (never expose to the browser) |
Both variables must be added to `turbo.json` → `tasks.build.env` so Turborepo invalidates the build cache when they change.
## Setup
### 1. Install the package
```bash
pnpm add @thedaviddias/analytics
```
### 2. Add the provider to your root layout
Place `AnalyticsHead` inside `` and `OpenPanelIdentify` inside `` (within your auth provider tree):
```tsx
// app/layout.tsx
import { AnalyticsHead } from '@thedaviddias/analytics/head'
import { OpenPanelIdentify } from '@thedaviddias/analytics/providers/openpanel-identify'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{/* Must be inside your auth provider (e.g. ClerkProvider) */}
{children}
)
}
```
### 3. Create the OpenPanel proxy route
Create `app/api/op/[...path]/route.ts` to proxy analytics requests through your own domain (avoids ad blockers):
```ts
import { createRouteHandler } from '@openpanel/nextjs/server'
export const { GET, POST } = createRouteHandler()
```
### 4. Update proxy
Add these to your `proxy.ts`:
- **Public routes**: Add `'/api/op/(.*)'` so the proxy is accessible without auth.
- **CSP script-src**: Add `https://openpanel.dev` as a fallback origin.
- **Rate-limit skip**: Exclude `/api/op/` paths from rate limiting.
### 5. Add environment variables to turbo.json
```jsonc
// turbo.json → tasks.build.env
"NEXT_PUBLIC_OPENPANEL_CLIENT_ID",
"OPENPANEL_CLIENT_SECRET",
```
## Client-side event tracking
```ts
import { trackEvent, ANALYTICS_EVENTS } from '@/lib/analytics'
trackEvent(ANALYTICS_EVENTS.WEBSITE_CLICK, {
website_name: 'Example',
content_slug: 'example-llms-txt'
})
```
OpenPanel events are only sent in production (`NODE_ENV === 'production'`).
## Server-side event tracking
For API routes and server actions, use the `trackServerEvent` helper with Vercel's `waitUntil`:
```ts
// lib/openpanel-server.ts
import { waitUntil } from '@vercel/functions'
import { opServer } from '@thedaviddias/analytics/server'
export function trackServerEvent(event: string, properties?: Record) {
if (process.env.NODE_ENV !== 'production') return
waitUntil(opServer.track(event, properties ?? {}))
}
```
This requires `@vercel/functions` installed in your app and `OPENPANEL_CLIENT_SECRET` set.
## User identification
`OpenPanelIdentify` automatically syncs the authenticated user to OpenPanel using your auth provider's `useAuth()` hook. When a user signs in, it calls `window.op.identify()` with their profile ID, name, and email. On sign-out it calls `window.op.clear()`.
This component must be rendered inside your auth provider tree (e.g. inside `ClerkProvider`).
## How it works
```
AnalyticsHead
└─ AnalyticsProvider
└─ OpenPanelAnalyticsComponent → loads op1.js via /api/op proxy
└─ globalProperties: { environment }
OpenPanelIdentify (body)
└─ useAuth() → window.op.identify() / window.op.clear()
trackEvent() (client)
└─ window.op.track(event, props) [production only]
opServer.track() (server)
└─ OpenPanel SDK → direct API call with clientSecret
```
## Production guards
OpenPanel tracking is gated behind `process.env.NODE_ENV === 'production'` in three places:
1. **Component rendering** — `AnalyticsProvider` only renders `OpenPanelAnalyticsComponent` in production.
2. **Client-side tracking** — `trackEvent()` and `AnalyticsLink` skip `window.op.track()` in non-production.
3. **Server-side tracking** — `trackServerEvent()` returns early in non-production.