# Shipabase docs > Privacy-first product analytics for JavaScript and TypeScript apps. SDK: @shipabase/js. No cookies, no localStorage, IP addresses never stored. --- # Introduction > Privacy-first product analytics for the apps you ship. Add it by hand in five minutes, or let your coding agent do it. Source: https://shipabase.dev/docs ## Let your agent do it Paste this into Claude Code, Cursor or another coding agent: ```text Add Shipabase analytics to this app using the @shipabase/js package. Read node_modules/@shipabase/js/llms.txt first and follow it exactly. 1. Install @shipabase/js with this project's package manager. 2. Call init() once on the client at app startup, with the app key from an env var (NEXT_PUBLIC_SHIPABASE_KEY for Next.js, VITE_SHIPABASE_KEY for Vite, SHIPABASE_KEY for Node). In Next.js App Router, do it in a "use client" providers component inside useEffect, and wrap app/layout.tsx with it. 3. Find the 5–10 most important user actions (signup, core feature used, upgrade, etc.) and add trackEvent() calls named in snake_case object_verb form (e.g. project_created). 4. Props: only non-personal values (plan, count, duration, variant). NEVER emails, names, user IDs, IPs or free text typed by users. 5. Show me the list of events you added, then tell me to open the Shipabase dashboard Setup page to confirm the first event arrived. ``` Shipabase counts visitors, visits, events and sessions without cookies, without storing IP addresses, and with a JavaScript SDK of about 2 KB. Page views are tracked automatically; events are one line each. These docs are written to be read by humans and followed by coding agents. - [Quickstart](https://shipabase.dev/docs/quickstart.md): Install the SDK, call init once, and see your first event. Five minutes, no cookie banner. - [For AI agents](https://shipabase.dev/docs/ai-agents.md): Shipabase is built to be installed by a coding agent. Give it one prompt, it reads the rules, you review the diff. - [Next.js](https://shipabase.dev/docs/nextjs.md): Add Shipabase to a Next.js App Router project. - [React (Vite)](https://shipabase.dev/docs/react.md): Add Shipabase to a React app built with Vite. - [Vue & Svelte](https://shipabase.dev/docs/vue-svelte.md): Add Shipabase to a Vue or Svelte app built with Vite, or to SvelteKit. - [React Native & Expo](https://shipabase.dev/docs/react-native.md): Track screens and events in a mobile app built with Expo or React Native. - [Node.js](https://shipabase.dev/docs/node.md): Track events from API servers, CLIs, scripts and cron jobs. - [Page views](https://shipabase.dev/docs/page-views.md): Pages are tracked automatically in the browser. Screens in mobile apps take one line. - [Naming events](https://shipabase.dev/docs/naming-events.md): Short, stable names make every chart readable. - [Props & privacy](https://shipabase.dev/docs/props.md): Add context to events without collecting personal data. - [Sessions](https://shipabase.dev/docs/sessions.md): How visits are grouped, and how conversions are counted. - [Verify your setup](https://shipabase.dev/docs/verify.md): See your first event, and fix it when nothing shows up. - [API reference](https://shipabase.dev/docs/api.md): Three functions. None of them throw, and calls before init are ignored. - [Privacy](https://shipabase.dev/docs/privacy.md): What the SDK sends, how visitors are counted, and what you should never send. --- # Quickstart > Install the SDK, call init once, and see your first event. Five minutes, no cookie banner. Source: https://shipabase.dev/docs/quickstart ## 1. Install the SDK `Terminal` ```bash npm i @shipabase/js ``` Zero dependencies, about 2 KB gzipped. Works with npm, pnpm, yarn and bun. ## 2. Add your app key `.env.local` ```bash NEXT_PUBLIC_SHIPABASE_KEY=SB-EU-xxxxxxxxxx ``` Find it in the dashboard, on your app’s **Setup** page. It looks like `SB-EU-` followed by 10 lowercase letters or digits. ## 3. Call init once, on the client **Next.js** `app/providers.tsx` ```tsx "use client"; import { useEffect } from "react"; import { init } from "@shipabase/js"; export function Providers({ children }: { children: React.ReactNode }) { useEffect(() => { init(process.env.NEXT_PUBLIC_SHIPABASE_KEY!, { appVersion: "1.0.0" }); }, []); return <>{children}; } ``` Then wrap `{children}` in `app/layout.tsx` with ``. Never call `init` in a Server Component. **React (Vite)** `src/main.tsx` ```tsx import { init } from "@shipabase/js"; init(import.meta.env.VITE_SHIPABASE_KEY, { appVersion: import.meta.env.VITE_APP_VERSION }); ``` **Vue / Svelte** `src/main.ts` ```ts import { init } from "@shipabase/js"; init(import.meta.env.VITE_SHIPABASE_KEY, { appVersion: import.meta.env.VITE_APP_VERSION }); ``` **Node.js** `server.ts` ```ts import { init, trackEvent, flush } from "@shipabase/js/node"; init(process.env.SHIPABASE_KEY!, { appVersion: "2.3.0" }); trackEvent("report_generated", { pages: 12, format: "pdf" }); // Short-lived process? Flush before exit. await flush(); ``` ## 4. Track what matters `any client component` ```tsx import { trackEvent } from "@shipabase/js"; trackEvent("project_created", { plan: "pro" }); ``` > Name events in `snake_case`, `object_verb`, past tense. Page views are already tracked automatically: no need to add them. See [Page views](https://shipabase.dev/docs/page-views). ## 5. Check your first event Trigger the action locally, then open your app’s **Setup** page in the dashboard: it shows the first event as soon as it arrives. Local events are flagged `isDebug`. > Nothing shows up? See [Verify your setup](https://shipabase.dev/docs/verify). --- # For AI agents > Shipabase is built to be installed by a coding agent. Give it one prompt, it reads the rules, you review the diff. Source: https://shipabase.dev/docs/ai-agents ## Let your agent do it Paste this into Claude Code, Cursor or another coding agent: ```text Add Shipabase analytics to this app using the @shipabase/js package. Read node_modules/@shipabase/js/llms.txt first and follow it exactly. 1. Install @shipabase/js with this project's package manager. 2. Call init() once on the client at app startup, with the app key from an env var (NEXT_PUBLIC_SHIPABASE_KEY for Next.js, VITE_SHIPABASE_KEY for Vite, SHIPABASE_KEY for Node). In Next.js App Router, do it in a "use client" providers component inside useEffect, and wrap app/layout.tsx with it. 3. Find the 5–10 most important user actions (signup, core feature used, upgrade, etc.) and add trackEvent() calls named in snake_case object_verb form (e.g. project_created). 4. Props: only non-personal values (plan, count, duration, variant). NEVER emails, names, user IDs, IPs or free text typed by users. 5. Show me the list of events you added, then tell me to open the Shipabase dashboard Setup page to confirm the first event arrived. ``` ## MCP server Connect the Shipabase MCP server and your agent can create apps, check that the first event arrived and read your stats. You sign in once in the browser; disconnect an agent anytime in **Settings → AI agents**. **Claude Code** `Terminal` ```bash claude mcp add --transport http shipabase https://api.shipabase.dev/mcp ``` Then type /mcp in Claude Code and sign in. **Cursor** `.cursor/mcp.json` ```json { "mcpServers": { "shipabase": { "url": "https://api.shipabase.dev/mcp" } } } ``` Then sign in from Cursor’s MCP settings. **Gemini CLI** `Terminal` ```bash gemini mcp add --transport http shipabase https://api.shipabase.dev/mcp ``` Then run /mcp auth shipabase in Gemini CLI. **Codex** `Terminal` ```bash codex mcp add shipabase --url https://api.shipabase.dev/mcp codex mcp login shipabase ``` The login opens your browser. ## The rules your agent follows The same rules ship inside the package at `node_modules/@shipabase/js/llms.txt`, so the agent works offline and with the exact version you installed. | Rule | | | --- | --- | | Install | Use the project’s package manager. | | init once | On the client, at startup. Key from `NEXT_PUBLIC_SHIPABASE_KEY`, `VITE_SHIPABASE_KEY` or `SHIPABASE_KEY`, never hardcoded. | | Name events | `snake_case`, `object_verb`, past tense. Constant strings. 5–15 events is typical. | | Props | Max 20 keys. Values are string, number or boolean. No nesting. | | Never | Personal data in props (emails, names, user IDs, IPs, tokens, free text). No cookies, localStorage or custom identifiers. The API masks what it detects, but don’t rely on it. | | Verify | Open the dashboard **Setup** page: it shows the first event. | ## Point your tools at the docs | URL | What it is | | --- | --- | | [/llms.txt](https://shipabase.dev/llms.txt) | The rules above, plus an index of every page | | [/llms-full.txt](https://shipabase.dev/llms-full.txt) | All docs in one Markdown file, for context windows | | `/docs/.md` | Any page as plain Markdown: append `.md` to its URL | | Copy as Markdown | Button at the top of every page | --- # Next.js > Add Shipabase to a Next.js App Router project. Source: https://shipabase.dev/docs/nextjs ## Install `Terminal` ```bash npm i @shipabase/js ``` `.env.local` ```bash NEXT_PUBLIC_SHIPABASE_KEY=SB-EU-xxxxxxxxxx ``` ## Create a providers component `app/providers.tsx` ```tsx "use client"; import { useEffect } from "react"; import { init } from "@shipabase/js"; export function Providers({ children }: { children: React.ReactNode }) { useEffect(() => { init(process.env.NEXT_PUBLIC_SHIPABASE_KEY!, { appVersion: "1.0.0" }); }, []); return <>{children}; } ``` ## Wrap your layout `app/layout.tsx` ```tsx import { Providers } from "./providers"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ## Track events `any client component` ```tsx "use client"; import { trackEvent } from "@shipabase/js"; ; ``` > Importing `@shipabase/js` in server code won’t crash, but call `init` and `trackEvent` from the browser (in `useEffect` or event handlers). For server-side events, use [`@shipabase/js/node`](https://shipabase.dev/docs/node). Pages Router: call `init` in a `useEffect` in `pages/_app.tsx`. --- # React (Vite) > Add Shipabase to a React app built with Vite. Source: https://shipabase.dev/docs/react ## Install `Terminal` ```bash npm i @shipabase/js ``` `.env` ```bash VITE_SHIPABASE_KEY=SB-EU-xxxxxxxxxx ``` ## Call init before rendering `src/main.tsx` ```tsx import { init } from "@shipabase/js"; init(import.meta.env.VITE_SHIPABASE_KEY, { appVersion: import.meta.env.VITE_APP_VERSION }); ``` ## Track events `anywhere` ```tsx import { trackEvent } from "@shipabase/js"; trackEvent("timer_started", { minutes: 25 }); ``` --- # Vue & Svelte > Add Shipabase to a Vue or Svelte app built with Vite, or to SvelteKit. Source: https://shipabase.dev/docs/vue-svelte ## Vue or Svelte with Vite `Terminal` ```bash npm i @shipabase/js ``` Call `init` at the top of `src/main.ts`, before the app is mounted. `src/main.ts` ```ts import { init } from "@shipabase/js"; init(import.meta.env.VITE_SHIPABASE_KEY, { appVersion: import.meta.env.VITE_APP_VERSION }); ``` ## SvelteKit Call `init` in `onMount` of the root layout, so it only runs in the browser. `src/routes/+layout.svelte` ```svelte ``` ## Track events `anywhere` ```ts import { trackEvent } from "@shipabase/js"; trackEvent("project_created", { plan: "pro" }); ``` --- # React Native & Expo > Track screens and events in a mobile app built with Expo or React Native. Source: https://shipabase.dev/docs/react-native ## Install `Terminal` ```bash npx expo install @shipabase/js ``` Works in Expo and bare React Native: the SDK uses `fetch` and stores nothing on the device. ## Expo Router Call `init` once, then record a screen view each time the route changes: `app/_layout.tsx` ```tsx import { useEffect } from "react"; import { Stack, usePathname } from "expo-router"; import { init, trackPageview } from "@shipabase/js"; init(process.env.EXPO_PUBLIC_SHIPABASE_KEY!); export default function RootLayout() { const pathname = usePathname(); useEffect(() => trackPageview(pathname), [pathname]); return ; } ``` ## React Navigation `App.tsx` ```tsx import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native"; import { init, trackPageview } from "@shipabase/js"; init(process.env.EXPO_PUBLIC_SHIPABASE_KEY!); export default function App() { const ref = useNavigationContainerRef(); return ( trackPageview(ref.getCurrentRoute()?.name)}> {/* … */} ); } ``` ## Track events `anywhere` ```ts import { trackEvent } from "@shipabase/js"; trackEvent("workout_completed", { minutes: 25 }); ``` > Screens show up in the **Pages** card, and every event is attached to the screen it happened on. --- # Node.js > Track events from API servers, CLIs, scripts and cron jobs. Source: https://shipabase.dev/docs/node ## Install `Terminal` ```bash npm i @shipabase/js ``` ## Use the Node entry `server.ts` ```ts import { init, trackEvent, flush } from "@shipabase/js/node"; init(process.env.SHIPABASE_KEY!, { appVersion: "2.3.0" }); trackEvent("report_generated", { pages: 12, format: "pdf" }); // Short-lived process? Flush before exit. await flush(); ``` - Uses the global `fetch` (Node 18+). - Reads the OS from `process.platform` and `os.release()`, and the locale from `Intl`. - Its timer never keeps your process alive. In short-lived processes, `await flush()` before exit. --- # Page views > Pages are tracked automatically in the browser. Screens in mobile apps take one line. Source: https://shipabase.dev/docs/page-views ## Automatic in the browser After `init`, the SDK sends a `page_viewed` event on load and on every client-side route change (Next.js, React Router, Vue Router, SvelteKit…). Nothing to add. Every other event is also attached to the page it happened on, so you can see where a `checkout_started` really happens. `turn it off` ```ts init("SB-EU-xxxxxxxxxx", { pageviews: false }); ``` ## What is sent - Only the path: `/pricing`. Never the query string or the hash, which often hold tokens or emails. - The server replaces identifiers with `:id`, so `/projects/8f3a…` and `/projects/42` both become `/projects/:id`, and your Pages list stays readable. - An email or a long token inside a path is replaced too. ## Hash routers and mobile screens If your routes live in the hash (`/#/settings`), or in a mobile app, record views yourself: `example` ```ts import { trackPageview } from "@shipabase/js"; trackPageview("/settings"); ``` See [React Native & Expo](https://shipabase.dev/docs/react-native) for a one-line setup with Expo Router or React Navigation. ## Already tracking page_viewed yourself? Remove your manual `trackEvent("page_viewed", …)` calls after upgrading to `@shipabase/js` 0.4, or set `pageviews: false`, so views aren’t counted twice. --- # Naming events > Short, stable names make every chart readable. Source: https://shipabase.dev/docs/naming-events Use `snake_case` in `object_verb` form, past tense. Names are constants: never build them from user data. | Good | Avoid | | --- | --- | | `project_created` | `CreateProject`, `click_button_3` | | `checkout_completed` | `checkout-completed-for-alice` | | `timer_started` | `start` | | `invoice_sent` | `invoice sent!` | - Allowed characters: `A-Z a-z 0-9 _ . : -`, up to 64. - Track meaningful actions: activation, core feature usage, conversion. Not every click or render. - 5–15 events is typical for a product. - Don’t name an event `page_viewed`: it’s reserved for page views, which the SDK tracks for you. See [Page views](https://shipabase.dev/docs/page-views). --- # Props & privacy > Add context to events without collecting personal data. Source: https://shipabase.dev/docs/props `example` ```ts trackEvent("invoice_sent", { plan: "pro", count: 3, success: true }); ``` | Limit | Value | | --- | --- | | Keys per event | 20 | | Key length | 40 characters | | Values | string (≤ 200 characters), finite number or boolean | | Nesting | Not allowed: no objects or arrays | Invalid props are dropped. In debug mode, the console shows a warning. ## Keep props anonymous You decide what you send, and you are responsible for it. Shipabase is built for anonymous analytics: the server already derives an anonymous daily visitor hash and a country, so props only need to describe the action (`plan`, `count`, `variant`), not the person. Personal data in props (emails, names, user or account IDs, phone numbers, IP addresses, tokens, free text typed by users) would link events back to a person, defeat the daily visitor hash, and bring GDPR obligations back to your app. ## Built-in guard As a safety net, the API masks values that look personal before anything is stored. The event and the key are kept, the value becomes `[redacted]`, so you can spot it in the dashboard and fix the call. | Masked | Examples | | --- | --- | | Keys that name personal data | `email`, `phone`, `user_id`, `username`, `first_name`, `ip`, `token`, `password`, `address`… | | Emails anywhere in a value | `alice@example.com` | | Phone numbers, IP addresses, JWTs | `+33 6 12 34 56 78`, `203.0.113.42`, `eyJ…` | > The guard catches common mistakes, it is not a guarantee. Numbers and booleans are never masked, and free text can still contain a name: keep props to values you choose. --- # Sessions > How visits are grouped, and how conversions are counted. Source: https://shipabase.dev/docs/sessions ## How a session is defined - The SDK keeps a session ID in memory only: `-<8 random hex>`. Nothing is written to the browser. - A new session starts after 1 hour of inactivity, and on every page reload. - Sessions shorter than 10 seconds are shown as **bounced**. ## Conversions Pick the event that means success (for example `checkout_completed`) in your app’s **Settings**. The **Sessions** tab then marks every session that reaches it as **converted**. --- # Verify your setup > See your first event, and fix it when nothing shows up. Source: https://shipabase.dev/docs/verify ## 1. Trigger one tracked action locally Local events are flagged `isDebug: true` automatically on `localhost` and when `NODE_ENV=development`. ## 2. Open the Setup page In the dashboard, open your app’s **Setup** page: it shows the first event as soon as it arrives. In other views, turn on the **debug** toggle to see local events. ## Nothing shows up? - Look for a `[shipabase]` warning in the browser console: an invalid key or event name. - In the Network tab, look for `POST /v1/events` returning `202`. - Check that `init` runs in the browser, once, before `trackEvent`. Calls before `init` are ignored. - Events are sent every 5 seconds, or when the tab is hidden: wait a few seconds. --- # API reference > Three functions. None of them throw, and calls before init are ignored. Source: https://shipabase.dev/docs/api ## init(appKey, options?) Starts the SDK. Call it once, on the client, at app startup. An invalid key logs one warning, and every later call does nothing. | Option | Type | Default | | | --- | --- | --- | --- | | `host` | `string` | `https://api.shipabase.dev` | API host (proxies) | | `appVersion` | `string` | `""` | Your app version, for version breakdowns | | `isDebug` | `boolean` | auto | `true` on `localhost` / `127.0.0.1` or when `NODE_ENV` is `development`. Debug events are hidden unless you turn on the debug toggle | | `flushInterval` | `number` | `5000` | Milliseconds between batch sends | | `pageviews` | `boolean` | `true` | Browser: send `page_viewed` on load and on every route change. See [Page views](https://shipabase.dev/docs/page-views) | ## trackEvent(eventName, props?) - `eventName`: 1–64 characters from `[A-Za-z0-9_.:-]`. - `props`: at most 20 keys of 40 characters. Values: string (≤ 200), finite number or boolean. - Never throws. Calls made before `init` are ignored. `example` ```ts trackEvent("invoice_sent", { plan: "pro", count: 3 }); ``` ## trackPageview(path?) Records a page or screen view (`page_viewed`). In the browser it happens automatically; call it yourself with hash routers or in React Native. Without a path, the current page is used. `example` ```ts trackPageview("/settings"); ``` ## flush(): Promise Sends queued events now. It resolves even when the network fails. Useful in short-lived Node processes, before exit. ## Delivery - Events are sent in batches of up to 25 (and about 20 KB): every `flushInterval`, and right away when 25 events are queued. - When the tab is hidden or closed, the queue is sent with `navigator.sendBeacon`. - On a network error or a 5xx, the batch is retried once. - The queue holds at most 100 events; the oldest are dropped first. --- # Privacy > What the SDK sends, how visitors are counted, and what you should never send. Source: https://shipabase.dev/docs/privacy ## What is stored in the browser Nothing. No cookies, localStorage, sessionStorage or IndexedDB. The session ID lives in memory only. ## What the SDK sends The event name, your props, and coarse system info: locale, time zone, OS name and version, browser engine and major version, app version, SDK version, a debug flag, the page path (never the query string; IDs become `:id` on the server), and the traffic source of the page load (the `utm_source` parameter, or the referring site’s host, never a full URL). Visits from AI assistants such as ChatGPT, Perplexity or Claude are recognized automatically. ## How visitors are counted IP addresses and User-Agents are never stored. The server uses them for a moment to derive a country and an anonymous visitor hash, `sha256(daily_salt + app + ip + ua)`, then discards them. - The salt changes every day, so visitors can’t be followed from one day to the next. - The day is the visitor’s local day: they count once from their midnight to the next. - Over 7, 30 or 90 days, a visitor who comes back on another day counts again. ## Cookie banner Nothing is stored on the visitor’s device, so this setup typically needs no cookie banner under the GDPR and ePrivacy rules. You stay responsible for what you send: keep props anonymous and Shipabase stays anonymous too. See [Props & privacy](https://shipabase.dev/docs/props).