# OpenPanel - Self-Hosted Analytics

Instance: `https://analytics.elevaseo.app`
Dashboard: `https://analytics.elevaseo.app`

**Intégrations actives :**
- ElevaSEO (Next.js) — via `@openpanel/nextjs` v1.4.0
- astro6-seo-template (Astro 6) — via script-tag pattern (zéro dep, voir section "Astro adaptation" ci-dessous)

---

## Env Variables

| Variable | Scope | Valeur |
|----------|-------|--------|
| `NEXT_PUBLIC_OP_CLIENT_ID` | Client + Server | Client ID depuis le dashboard OpenPanel |
| `OP_CLIENT_SECRET` | Server only | Secret pour le server-side tracking |
| `NEXT_PUBLIC_OP_API_URL` | Client + Server | `https://analytics.elevaseo.app/api` |

> Pas de cookie consent necessaire (self-hosted, pas de tiers).

---

## Client-Side Setup

**Fichier:** `app/[locale]/layout.tsx`

```tsx
import { OpenPanelComponent } from "@openpanel/nextjs";

<OpenPanelComponent
  clientId={process.env.NEXT_PUBLIC_OP_CLIENT_ID!}
  apiUrl={process.env.NEXT_PUBLIC_OP_API_URL!}
  scriptUrl="https://analytics.elevaseo.app/op1.js"
  trackScreenViews={true}
  trackOutgoingLinks={true}
  trackAttributes={true}
/>
```

### Props importantes

| Prop | Role |
|------|------|
| `apiUrl` | Pointe vers l'API self-hosted (pas `/api/op` proxy) |
| `scriptUrl` | Script depuis l'instance self-hosted (referrals + UTM) |
| `trackScreenViews` | Page views automatiques sur chaque navigation |
| `trackOutgoingLinks` | Track les clics sur liens externes |
| `trackAttributes` | Active `data-track` sur les elements HTML |
| `profileId` | Optionnel - identifier un user connecte |
| `globalProperties` | Props incluses dans chaque event |

### Track un event client

```tsx
import { useOpenPanel } from "@openpanel/nextjs";

function MyComponent() {
  const op = useOpenPanel();
  return (
    <button onClick={() => op.track("cta_clicked", { location: "hero" })}>
      CTA
    </button>
  );
}
```

### Track via data-attributes (HTML)

```html
<button data-track="cta_clicked" data-property-location="hero">
  CTA
</button>
```

---

## Server-Side Setup

**Fichier:** `lib/openpanel.ts`

```typescript
import { OpenPanel } from "@openpanel/nextjs";

export const opServer = new OpenPanel({
  clientId: process.env.NEXT_PUBLIC_OP_CLIENT_ID!,
  clientSecret: process.env.OP_CLIENT_SECRET!,
  apiUrl: process.env.NEXT_PUBLIC_OP_API_URL!,
});
```

### Track un event serveur

```typescript
import { opServer } from "@/lib/openpanel";

// Non-blocking (fire and forget)
opServer
  .track("contact_form_submitted", { projectType: "seo-audit" })
  .catch((err) => console.error("OpenPanel error:", err));
```

### Vercel/Serverless — waitUntil

```typescript
import { waitUntil } from "@vercel/functions";

waitUntil(opServer.track("my_event", { foo: "bar" }));
```

### Identifier un user

```typescript
opServer.identify({
  profileId: "user_123",
  firstName: "Bastien",
  email: "bastien@elevaseo.com",
  properties: { plan: "pro" },
});
```

### Increment/Decrement

```typescript
opServer.increment({ profileId: "user_123", property: "visits", value: 1 });
opServer.decrement({ profileId: "user_123", property: "credits", value: 5 });
```

---

## Events actifs sur ElevaSEO

| Event | Declencheur | Properties |
|-------|-------------|------------|
| `screen_view` | Auto (client) | page, referrer, UTM params |
| `outgoing_link` | Auto (client) | url, text |
| `contact_form_submitted` | Server (API route) | projectType, website, newsletterConsent |

## Events actifs sur astro6-seo-template

| Event | Declencheur | Properties |
|-------|-------------|------------|
| `screen_view` | Auto (client, `op1.js`) | page, referrer, UTM |
| `outgoing_link` | Auto (client, `op1.js`) | url, text |
| `contact_form_submitted` | Server (`src/pages/api/contact.ts`) | messageLength |
| `lang_switched` | Client (`LanguageSwitcher.astro` click) | from, to |
| `search_opened` | Client (`search-client.ts`, modal open) | locale |

---

## Config Instance Self-Hosted

Variables a configurer sur l'instance OpenPanel (`docker-compose.yml` ou `.env`) :

```bash
SELF_HOSTED=true
API_URL=https://analytics.elevaseo.app/api
DASHBOARD_URL=https://analytics.elevaseo.app
API_CORS_ORIGINS=https://www.elevaseo.com,https://elevaseo.com,https://test.elevaseo.app
```

> **CORS** : `API_CORS_ORIGINS` doit inclure tous les domaines qui envoient des events.
> À ajouter au fur et à mesure que des projets vont live (template preview, prod domain, etc.).

---

## Comment les Referrals fonctionnent

Le SDK web (`@openpanel/web`) capture automatiquement :

1. **`__referrer`** : `document.referrer` au chargement initial → `setGlobalProperties({ __referrer })`
2. **`__path`** : `window.location.href` a chaque `screen_view` et `track()`
3. **`__title`** : `document.title` a chaque `screen_view`

Ces proprietes prefixees `__` sont traitees cote serveur par OpenPanel pour construire les rapports referrals, sources, pages.

Le script `op1.js` est charge via `next/script` (client-side uniquement). `document.referrer` est disponible car l'execution se fait dans le navigateur, pas en SSR.

**UTM** : les parametres `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content` sont extraits automatiquement de l'URL par le SDK.

### Test API direct (debug referrals)

```bash
curl -s -X POST "https://analytics.elevaseo.app/api/track" \
  -H "Content-Type: application/json" \
  -H "openpanel-client-id: $NEXT_PUBLIC_OP_CLIENT_ID" \
  -H "openpanel-client-secret: $OP_CLIENT_SECRET" \
  -d '{
    "type": "track",
    "payload": {
      "name": "test_referrer",
      "properties": {
        "__referrer": "https://www.google.com/search?q=elevaseo",
        "__path": "https://www.elevaseo.com/",
        "__title": "ElevaSEO Test"
      }
    }
  }'
# Reponse attendue : {"deviceId":"...","sessionId":"..."}
```

---

## Astro adaptation (pas de package officiel)

Pas de `@openpanel/astro`. Le package `@openpanel/nextjs` ne fonctionne pas (hooks React, conventions `NEXT_PUBLIC_*`, scripts SSR-only). On **réimplémente le pattern script-tag** que `@openpanel/nextjs` utilise sous le capot — zéro dependency, même protocole.

### Convention env vars

Astro utilise `PUBLIC_*` au lieu de `NEXT_PUBLIC_*` (Vite standard). Les noms côté serveur restent inchangés.

| Next.js | Astro |
|---------|-------|
| `NEXT_PUBLIC_OP_CLIENT_ID` | `PUBLIC_OPENPANEL_CLIENT_ID` |
| `NEXT_PUBLIC_OP_API_URL` | `PUBLIC_OPENPANEL_API_URL` |
| — | `PUBLIC_OPENPANEL_SCRIPT_URL` (explicite, pas de fallback implicite) |
| `OP_CLIENT_SECRET` | `OPENPANEL_CLIENT_SECRET` |

### Composant loader — `src/components/analytics/OpenPanel.astro`

```astro
---
const clientId = import.meta.env.PUBLIC_OPENPANEL_CLIENT_ID;
const apiUrl = import.meta.env.PUBLIC_OPENPANEL_API_URL;
const scriptUrl = import.meta.env.PUBLIC_OPENPANEL_SCRIPT_URL;
const enabled = Boolean(clientId && apiUrl && scriptUrl);
---
{enabled && (
  <>
    <script is:inline define:vars={{ clientId, apiUrl }}>{`
      window.op = window.op || function (...args) {
        (window.op.q = window.op.q || []).push(args);
      };
      window.op('ctor', {
        clientId,
        apiUrl,
        trackScreenViews: true,
        trackOutgoingLinks: true,
        trackAttributes: true,
      });
    `}</script>
    <script is:inline async defer src={scriptUrl} />
  </>
)}
```

**Drop-in** : monté dans `BaseLayout.astro`. Si `PUBLIC_OPENPANEL_CLIENT_ID` vide → zéro byte émis. Permet de ship le template sans analytics par défaut.

### Client tracking — `src/lib/analytics.ts`

```ts
export function track(event: string, props?: TrackProps): void {
  if (typeof window === "undefined" || !window.op) return;
  window.op("track", event, props ?? {});
}
```

Usage dans n'importe quel `<script>` Astro ou `.ts` client :

```ts
import { track } from "@/lib/analytics";
track("cta_clicked", { location: "hero" });
```

Le queue primer dans `OpenPanel.astro` bufferise les calls avant que `op1.js` soit chargé — pas de race condition.

### Server tracking — fetch direct

Pas de SDK ; on appelle le REST endpoint directement pour rester dep-free.

```ts
export async function trackServer(event: string, props?: TrackProps): Promise<Response | null> {
  const clientId = import.meta.env.PUBLIC_OPENPANEL_CLIENT_ID;
  const clientSecret = import.meta.env.OPENPANEL_CLIENT_SECRET;
  const apiUrl = import.meta.env.PUBLIC_OPENPANEL_API_URL;
  if (!clientId || !clientSecret || !apiUrl) return null;
  return await fetch(`${apiUrl}/track`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "openpanel-client-id": clientId,
      "openpanel-client-secret": clientSecret,
    },
    body: JSON.stringify({ type: "track", payload: { name: event, properties: props ?? {} } }),
  });
}
```

Usage côté API route :

```ts
void trackServer("contact_form_submitted", { messageLength });
```

Le `void` fire-and-forget. En Vercel Fluid Compute, pas besoin de `waitUntil` — la function graceful shutdown attend les fetches en cours.

### Éléments non portés

- `useOpenPanel()` hook → pas de hook React côté layout ; utiliser `track()` directement depuis les `<script>` Astro.
- `OpenPanelComponent` JSX → composant Astro équivalent (ci-dessus).
- Proxy route handler → pas nécessaire en self-hosted (pas d'ad-blocker à bypasser).
- Session replay → désactivé (même défaut qu'ElevaSEO Next.js).

## Proxy (optionnel, pas utilise)

Si besoin de bypass ad blockers (pas notre cas en self-hosted) :

```tsx
// app/api/[...op]/route.ts
import { createRouteHandler } from "@openpanel/nextjs/server";
export const { GET, POST } = createRouteHandler({ apiUrl: "..." });

// Puis dans le layout :
<OpenPanelComponent apiUrl="/api/op" scriptUrl="/api/op/op1.js" ... />
```

---

## Session Replay (desactive)

Disponible si besoin :

```tsx
<OpenPanelComponent
  sessionReplay={{
    enabled: true,
    maskAllInputs: true,
    maskTextSelector: ".sensitive",
    blockSelector: ".private",
  }}
/>
```

---

## Liens

- Dashboard: https://analytics.elevaseo.app
- Docs SDK: https://openpanel.dev/docs/sdks/nextjs
- Docs API: https://openpanel.dev/docs/api/track
- Self-hosting: https://openpanel.dev/docs/self-hosting/self-hosting
- Env vars: https://openpanel.dev/docs/self-hosting/environment-variables
- GitHub: https://github.com/openpanel-dev/openpanel
