# Set up Lantern analytics

You are setting up Lantern (web analytics for founders) in the user's codebase, then instrumenting the
goals that matter to their business, so tracking starts on the next deploy with nothing for the user
to type. Work through the steps in order. Make small, focused edits that follow the project's existing
style, and don't change application behavior.

- Site ID and domain: take them from the user's message. If they're missing, ask the user to copy them from the Lantern dashboard (Install panel) and stop until you have them. Replace `<SITE_ID>` and `<DOMAIN>` below.
- Lantern API: `https://api.visitlantern.com`

Goals need no setup in Lantern: any event name the code sends appears in the dashboard automatically.

## 1. Find out what you're working with

Read the project before editing. Determine:

- **Framework and router**: package.json, next.config.*, astro.config.*, svelte.config.*, vite.config.*,
  nuxt.config.*, remix/react-router config, plain HTML, WordPress theme, etc.
- **Host**: vercel.json or a Vercel project, netlify.toml / `_redirects`, wrangler.toml (Cloudflare),
  nginx config, Dockerfile. If unclear, infer from the framework (Next.js → the Next.js rewrite works
  on any host) or ask.
- **Where the root layout / `<head>` is**: app/layout.tsx, pages/_document.tsx, index.html,
  src/app.html, a layout component, etc.
- **Monorepo?** Install only in the user-facing web app(s) for `<DOMAIN>` and its subdomains.

If Lantern (`/script.js` with `data-site`) is already installed, don't add it twice: fix it up and
continue with goals.

## 2. Install the script, served from the site's own domain

Ad blockers hide 20–40% of visitors when analytics loads from a third-party domain, so serve Lantern
from the site's own domain with one rewrite of `/_p/*` to the Lantern API. The script sends its
events back through the same path, so one rule covers both.

Use the block for the project's framework/host. Merge into existing config (e.g. add one entry to an
existing `rewrites()` array) rather than replacing it. If `/_p` is already taken, pick another
unused path and change it in both the rewrite and the script `src`.

### Next.js

Rewrite in `next.config.ts`:

```
const nextConfig = {
  async rewrites() {
    return [{ source: "/_p/:path*", destination: "https://api.visitlantern.com/:path*" }];
  },
};
```

Script (app/layout.tsx):

```
import Script from "next/script";

// inside <body>
<Script src="/_p/script.js" data-site="<SITE_ID>" data-domain="<DOMAIN>" strategy="afterInteractive" />
```

Already have rewrites? Add the one entry to your existing array.

### Vercel

Rewrite in `vercel.json`:

```
{
  "rewrites": [{ "source": "/_p/:path*", "destination": "https://api.visitlantern.com/:path*" }]
}
```

Script (index.html, inside <head>):

```
<script defer src="/_p/script.js" data-site="<SITE_ID>" data-domain="<DOMAIN>"></script>
```

For any framework deployed on Vercel (Astro, SvelteKit, Vite, Remix…). On Next.js, use the Next.js tab.

### Netlify

Rewrite in `public/_redirects`:

```
/_p/*  https://api.visitlantern.com/:splat  200
```

Script (index.html, inside <head>):

```
<script defer src="/_p/script.js" data-site="<SITE_ID>" data-domain="<DOMAIN>"></script>
```

The 200 makes it a rewrite (proxy), not a redirect. Put the file in the folder Netlify publishes.

### Cloudflare

Rewrite in `Worker, on the route <DOMAIN>/_p/*`:

```
export default {
  async fetch(request) {
    const url = new URL(request.url);
    const upstream = new Request("https://api.visitlantern.com" + url.pathname.slice(3) + url.search, request);
    upstream.headers.set("X-Forwarded-For", request.headers.get("CF-Connecting-IP") ?? "");
    return fetch(upstream);
  },
};
```

Script (index.html, inside <head>):

```
<script defer src="/_p/script.js" data-site="<SITE_ID>" data-domain="<DOMAIN>"></script>
```

For sites behind Cloudflare. Forwarding CF-Connecting-IP keeps visitor locations accurate.

### nginx

Rewrite in `server { … }`:

```
location /_p/ {
    proxy_pass https://api.visitlantern.com/;
    proxy_ssl_server_name on;
    proxy_set_header Host api.visitlantern.com;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
```

Script (index.html, inside <head>):

```
<script defer src="/_p/script.js" data-site="<SITE_ID>" data-domain="<DOMAIN>"></script>
```

### Other

Script (inside <head>):

```
<script defer src="https://api.visitlantern.com/script.js" data-site="<SITE_ID>" data-domain="<DOMAIN>"></script>
```

Works anywhere, with no server changes. Ad blockers will hide some visitors, so add a rewrite later if your host supports one.

Notes:
- Load the script on every page, once, from the root layout.
- `data-domain` shares the visitor across subdomains (marketing site → app). Keep it.
- If a Content-Security-Policy exists, `'self'` covers the proxied script; without the rewrite, add
  `https://api.visitlantern.com` to `script-src` and `connect-src`.

## 3. Understand the business, then choose goals

Read routes, pages, forms, API handlers, auth and billing code to answer:

- What does the product sell or want visitors to do? (SaaS sign-up, e-commerce purchase, lead form,
  newsletter, download, booking, waitlist…)
- What is the **primary conversion** (the KPI the founder cares about most)?
- What are the **steps leading to it** (the funnel), and what does **activation** look like after
  sign-up (the first meaningful action in the product)?

Then choose **5 to 12 goals**. Prioritise, in this order:

1. The primary conversion: `signup`, `purchase`, `subscription_started`, `lead_submitted`,
   `demo_booked`, `waitlist_joined`…
2. Funnel steps before it: `pricing_cta_click`, `checkout_started`, `signup_started`, `trial_started`…
3. Activation in the product: `onboarding_completed`, `project_created`, `teammate_invited`,
   `first_<core_action>`…
4. Secondary intent: `newsletter_subscribed`, `contact_submitted`, `docs_copy_snippet`, `download`…

Skip noise: generic navigation clicks, every button, scroll or hover events, and anything Lantern
already records (pageviews, time on page, scroll depth, sources, outbound referrers). In particular,
don't add goals for *viewing* a page (`pricing_viewed`): every page view is already recorded and can
be filtered by path. A click goal is worth it only for a CTA that leads toward the conversion.

Naming: `snake_case`, object_action or action_object, past tense for completed things
(`signup_completed` vs `signup_click`), at most 120 characters, consistent across the codebase.

## 4. Instrument the goals

First, add this queue stub to the root layout's `<head>`, before the Lantern script, so calls made
before the script loads (e.g. `identify` on app start) are queued instead of lost:

```html
<script>window.pulse = window.pulse || function () { (pulse.q = pulse.q || []).push(arguments) };</script>
```

In Next.js App Router, put it in app/layout.tsx as:

```tsx
<Script id="pulse-queue" strategy="beforeInteractive">
  {"window.pulse = window.pulse || function () { (pulse.q = pulse.q || []).push(arguments) };"}
</Script>
```

Lantern's browser API:

```js
pulse("track", "goal_name", { plan: "pro" })   // a goal, with optional properties
pulse("identify", user.id)                      // after sign-in/sign-up: links visits to the user
pulse("revenue", 49, "USD", { plan: "pro" })    // a payment confirmed in the browser
pulse.visitorId()                               // anonymous id, to pass to payment metadata
```

Rules:

- **Track completions where they succeed**, not on the click that starts them: after the sign-up API
  returns OK, on the checkout success page, after the form submission resolves. A click that fails
  validation isn't a conversion.
- **For pure click intent** (CTA buttons, pricing plan buttons, outbound links), prefer the
  declarative attribute, which needs no JavaScript:
  `<a href="/signup" data-pulse-goal="pricing_cta_click" data-pulse-plan="pro">`
  Every other `data-pulse-*` attribute becomes a property.
- **Guard every call** so analytics can never break the app and is safe during server rendering:
  `window.pulse?.("track", "signup_completed")`.
  In TypeScript projects, add a declaration once (e.g. `types/pulse.d.ts`):

  ```ts
  declare global {
    interface Window {
      pulse?: ((cmd: "track" | "identify" | "revenue", ...args: unknown[]) => void) & { visitorId?: () => string };
    }
  }
  export {};
  ```
- **Browser only.** `pulse` exists in the browser. In server code (API routes, server actions,
  webhooks), track from the client after the server responds instead, e.g. in the form's success
  handler. Server components can't call it; put the call in a small client component or effect.
- **Identify** with an internal user id (never an email or name) right after sign-in, sign-up and on
  app load when a session exists.
- **Revenue**: if checkout success is visible in the browser, call `pulse("revenue", amount,
  currency)` there with the real amount. If payment is confirmed server-side (Stripe Checkout,
  webhooks), add `pulse.visitorId()` to the checkout session's metadata (e.g. `metadata.pulse_visitor_id`)
  so it can be attributed later; don't fake a browser revenue call.
- **Properties**: at most a few per event (≤ 30), short strings/numbers/booleans, no personal data
  (emails, names, phone numbers, free-text input).
- **Fire once**: guard against React effects running twice (dev Strict Mode) and against re-renders,
  e.g. track inside the event handler or success callback rather than in render. A goal fired when a
  success page mounts (`/checkout/success`) must fire once per order, not on every reload: key it on
  the order/session id from the URL, e.g. skip when `sessionStorage` already holds that id.

## 5. Write the tracking plan

Create `lantern-tracking-plan.md` at the repository root (or next to the web app in a monorepo):

```md
# Analytics tracking plan (Lantern)

Primary KPI: <the conversion> — <one line on why>

## Goals
| Goal | Fires when | Where in code | Properties |
|---|---|---|---|
| signup_completed | sign-up API succeeded | app/(auth)/sign-up/form.tsx | plan |

## Funnels
1. Acquisition → <step> → <step> → <primary conversion>
2. Activation: signup_completed → onboarding_completed → <first core action>
```

Keep it accurate to what you instrumented; it's how the team will read the dashboard.

## 6. Verify

- Run the project's typecheck, lint and build (whatever exists) and fix anything you introduced.
- Confirm the script tag is in the root layout and the rewrite is in the right config file.
- Don't deploy, commit or push unless the user asks.

## 7. Report back

Tell the user, briefly:

1. What you installed and where (files changed).
2. The primary KPI you identified and why.
3. The goals you added, as a short list, and the funnels from the plan.
4. Anything you couldn't instrument automatically and why (e.g. revenue confirmed only in a webhook).
5. Next step: deploy, open the site, and check the Lantern dashboard's Install panel. It confirms the
   first pageview and whether the script is served from their domain. After deploying,
   `https://<DOMAIN>/_p/script.js` should return JavaScript.

Docs: https://visitlantern.com/docs
