npm package: @frissly/analytics
Description: Production-grade cross-platform analytics SDK for Expo and Next.js applications
Author: Frissly
Latest version: 1.0.2

# @frissly/analytics

[![npm version](https://badge.fury.io/js/@frissly%2Fanalytics.svg)](https://www.npmjs.com/package/@frissly/analytics) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Production-grade cross-platform analytics SDK for Expo (iOS/Android/Web) and Next.js (App Router / Pages Router). **v1.0.2**

This file lists:
- All public functions
- Platform‑specific usage
- Config options
- Router options

---

## Install

### Expo (iOS/Android/Web)
```
npm install @frissly/analytics
npx expo install @react-native-async-storage/async-storage expo-device expo-application expo-localization expo-constants
```

### Next.js (App Router / Pages Router)
```
npm install @frissly/analytics
```

---

## Quick Start (all platforms)
```ts
import { analytics } from "@frissly/analytics";

await analytics.init({
  apiEndpoint: "https://your-backend.com/events",
  apiKey: "YOUR_API_KEY",
  appName: "expo-mobile", // or "expo-web" or "next-web"
  environment: "prod",
});

analytics.track("app_opened");
```

---

## How events are sent (flow)

1. **You call** `analytics.track(...)` or `analytics.page(...)`.
2. **Each event is added to an in-memory queue** (and optionally saved to storage for offline).
3. **Events are sent in batches** to your `apiEndpoint` when a flush happens (see below).
4. **You do not send one HTTP request per event.** The SDK batches events and sends them together.

So: **track** → **queue** → **flush** (sends batch) → **your backend**.

---

## When does flush happen? (automatic vs manual)

### Automatic flush (you do nothing)

A flush runs automatically when **any** of these happen:

| Trigger | Meaning |
|--------|---------|
| **Queue size** | The queue has `batchSize` events (default 10). |
| **Timer** | `batchInterval` ms have passed since the last flush (default 10 seconds). |
| **Page hidden (web)** | User switches tab or minimizes the browser. |
| **Page unload (web)** | User closes the tab or navigates away (`beforeunload` / `pagehide`). |
| **App background (Expo/RN)** | App goes to background or becomes inactive. |
| **Route change** | You have `flushOnRouteChange: true` and a page/screen change is tracked. |

You **do not** need to call any function for these; the SDK handles them.

### Manual flush (you call the function)

Call **`analytics.flush()`** when you want to force-send whatever is in the queue **right now**. For example:

- Right before the user logs out.
- After a critical action (e.g. purchase) so events are sent immediately.
- In tests to ensure events are delivered before asserting.

```ts
// Force send queued events now
await analytics.flush();
```

---

## What is Beacon? (web only)

**Beacon** is the browser’s **`navigator.sendBeacon()`** API. It is used to send a small payload (your event batch) to your server **when the page is closing or hidden**. Unlike a normal `fetch()`, the browser guarantees it will try to send the request even as the tab is closing, so you don’t lose events when the user leaves the page.

- **When the SDK uses it:** On **web**, when a flush is triggered by **page hide** or **page unload**, the SDK can use `sendBeacon` instead of `fetch` (if `useBeaconOnFlush` is `true`, which is the default).
- **Where it does not apply:** Expo/React Native (no `sendBeacon`); there we use normal HTTP (e.g. `fetch`) for every flush.
- **Optional manual use:** You can call `analytics.flush(true)` to request a beacon-style send on web; the SDK will use it when possible.

Config that affects this:

- `useBeaconOnFlush?: boolean` (default `true`) — use beacon when flushing on page hide/unload (web).
- `beaconRetainOnSuccess?: boolean` (default `false`) — if beacon is used and returns success, whether to keep events in the queue (e.g. for a later normal flush).

---

## Platform Usage (very detailed)

### Expo (React Native) — Mobile (iOS/Android)
```ts
import { analytics } from "@frissly/analytics";

await analytics.init({
  apiEndpoint: "https://your-backend.com/events",
  apiKey: "YOUR_API_KEY",
  appName: "expo-mobile",
  environment: "prod",
  router: "expo-router",       // or "react-navigation"
  trackingMode: "manual",      // "auto" or "manual"
  autoTrackRoutes: false,      // use hooks below to track routes
});
```

#### Expo Router (auto page tracking)
```ts
import { useExpoRouterTracking } from "@frissly/analytics";

export function RootLayout() {
  useExpoRouterTracking();
  return <Slot />;
}
```

#### React Navigation (auto page tracking)
```ts
import { useReactNavigationTracking } from "@frissly/analytics";

export function App() {
  useReactNavigationTracking();
  return <NavigationContainer>{/* ... */}</NavigationContainer>;
}
```

---

### Expo Web (browser)
```ts
await analytics.init({
  apiEndpoint: "https://your-backend.com/events",
  apiKey: "YOUR_API_KEY",
  appName: "expo-web",
  environment: "prod",
  router: "history",
  trackingMode: "auto",
  includeSearchInRoute: true,
});
```

---

### Next.js App Router
```ts
// app/layout.tsx
"use client";
import { analytics, useNextAppRouterTracking } from "@frissly/analytics";
import { useEffect } from "react";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    analytics.init({
      apiEndpoint: "https://your-backend.com/events",
      apiKey: "YOUR_API_KEY",
      appName: "next-web",
      environment: "prod",
      router: "next-app",
      trackingMode: "manual",
    });
  }, []);

  useNextAppRouterTracking({ includeSearch: true });
  return <html><body>{children}</body></html>;
}
```

---

### Next.js Pages Router
```ts
// pages/_app.tsx
import { analytics, useNextPagesRouterTracking } from "@frissly/analytics";
import { useEffect } from "react";

export default function App({ Component, pageProps }: any) {
  useEffect(() => {
    analytics.init({
      apiEndpoint: "https://your-backend.com/events",
      apiKey: "YOUR_API_KEY",
      appName: "next-web",
      environment: "prod",
      router: "next-pages",
      trackingMode: "auto",
    });
  }, []);

  useNextPagesRouterTracking();
  return <Component {...pageProps} />;
}
```

---

## Public API (Functions + Usage — very detailed)

### Core SDK (singleton) — `analytics`

#### `analytics.init(config)`
Initialize the SDK once on app start.
**Input:** `AnalyticsConfig`  
**Returns:** `Promise<void>`
```ts
await analytics.init({
  apiEndpoint: "https://your-backend.com/events",
  apiKey: "YOUR_API_KEY",
  appName: "expo-mobile",
  environment: "prod",
  trackingMode: "manual",
});
```

#### `analytics.track(eventName, options?)`
Track any custom event.
**Input:** `eventName: string`, `options?: TrackOptions`  
**TrackOptions:** `element?`, `elementType?` (`"button"` | `"card"` | `"input"` | `"link"`), `data?`, `route?`, `previousRoute?`  
**Returns:** `Promise<void>`
```ts
analytics.track("button_clicked", {
  element: "buy_button",
  elementType: "button",
  data: { sku: "ABC-123" },
  route: "/checkout",       // optional
  previousRoute: "/cart",   // optional
});
```

#### `analytics.page({ route, previousRoute?, data? })`
Track a page/screen view.
**Input:** `PageViewOptions`  
**Returns:** `Promise<void>`
```ts
analytics.page({
  route: "/products/123",
  previousRoute: "/home",
  data: { category: "shoes" },
});
```

#### `analytics.identify(userId, traits?)`
Attach user identity after login.
**Input:** `userId: string`, `traits?: IdentifyOptions`  
**Returns:** `Promise<void>`
```ts
analytics.identify("user-123", {
  email: "user@example.com",
  plan: "pro",
});
```

#### `analytics.setAnonymousId(anonymousId)`
Set the anonymous device/browser ID (e.g. from your own ID solution). If you don’t call this, the SDK generates one.
**Input:** `anonymousId: string`  
**Returns:** `void`
```ts
analytics.setAnonymousId("my-device-uuid");
```

#### `analytics.flush(useBeacon?)`
Force send queued events **now** (manual flush). Automatic flush is described in “When does flush happen?” above.
**Input:** `useBeacon?: boolean` — on web, `true` asks the SDK to use `sendBeacon` if available.  
**Returns:** `Promise<void>`
```ts
analytics.flush();        // normal HTTP send
analytics.flush(true);    // prefer beacon on web (e.g. before tab close)
```

#### `analytics.startSession()` / `analytics.endSession()`
Manually control sessions if needed.
**Returns:** `void` / `Promise<void>`
```ts
analytics.startSession();
analytics.endSession();
```

#### `analytics.reset()`
Reset identity + session (e.g., logout).
**Returns:** `Promise<void>`
```ts
await analytics.reset();
```

#### `analytics.setConfig(partialConfig)`
Update config at runtime.
**Input:** `Partial<AnalyticsConfig>`  
**Returns:** `void`
```ts
analytics.setConfig({ debug: true, enabled: false });
```

#### `analytics.disable()` / `analytics.enable()`
Globally pause or resume tracking.
**Returns:** `void`
```ts
analytics.disable();
analytics.enable();
```

#### `analytics.addPlugin(plugin)` / `analytics.removePlugin(name)`
Extend behavior via plugins.
**Input:** `AnalyticsPlugin` / `string`  
**AnalyticsPlugin:** `name: string` plus optional: `beforeTrack?(event) => event | null`, `afterTrack?(event) => void`, `onFlush?(events) => void`, `onError?(error, event?) => void`. Return `null` from `beforeTrack` to drop the event.  
**Returns:** `void`
```ts
analytics.addPlugin({
  name: "log-all",
  beforeTrack(event) { return event; },
  afterTrack(event) { console.log(event); },
  onFlush(events) { console.log("Flushing", events.length); },
  onError(err, event) { console.error(err, event); },
});
analytics.removePlugin("log-all");
```

#### `analytics.getUserId()` / `analytics.getAnonymousId()` / `analytics.getSessionId()`
Read current identity/session values.
**Returns:** `string | null` / `string` / `string`
```ts
const userId = analytics.getUserId();
const anonId = analytics.getAnonymousId();
const sessionId = analytics.getSessionId();
```

---

### Class API — `new Analytics()`
Use your own instance instead of the singleton.
**Use case:** multiple analytics instances with different configs
```ts
import { Analytics } from "@frissly/analytics";
const myAnalytics = new Analytics();
```

---

### Hooks (React / React Native)

#### `useAnalytics()`
Get core methods in components.
**Returns:** `{ track, page, identify, reset, userId, anonymousId, sessionId }`
```ts
const { track, page, identify } = useAnalytics();
track("cta_clicked");
```

#### `usePageTracking(route, options?)`
Auto page tracking when `route` changes.
**Input:** `route: string`, `options?: { data?: Record<string, any> }`
```ts
usePageTracking("/profile", { data: { tab: "settings" } });
```

#### `useTrackClick(eventName, element, options?)`
Quick click tracking.
**Returns:** click handler function
```ts
const onClick = useTrackClick("buy_clicked", "buy_button");
```

#### `useTrackMount(eventName, options?)`
Track when component mounts.
**Runs once on mount**
```ts
useTrackMount("screen_loaded", { data: { screen: "Home" } });
```

#### `useTrackUnmount(eventName, options?)`
Track when component unmounts.
**Runs on unmount**
```ts
useTrackUnmount("screen_closed", { data: { screen: "Home" } });
```

#### `useSessionTracking()`
Auto start/stop session on mount/unmount.
```ts
useSessionTracking();
```

#### Router hooks
All accept optional `{ data?: Record<string, any>; includeSearch?: boolean }`. `includeSearch` (App Router) includes query string in the route.
```ts
useNextAppRouterTracking({ includeSearch: true, data: { source: "app" } })
useNextPagesRouterTracking({ data: { source: "app" } })
useExpoRouterTracking({ data: { source: "app" } })
useReactNavigationTracking({ data: { source: "app" } })
```

---

### Utilities
```ts
generateUUID()              // create UUID
validateEventName(name)     // returns true/false
toSnakeCase(name)           // convert to snake_case
sanitizeData(data)          // redacts sensitive keys
```

---

### Platform Helpers
```ts
detectPlatform()            // "ios" | "android" | "web"
detectAppType(appName?)     // "expo-mobile" | "expo-web" | "next-web"
getDeviceInfo()             // device info object
```

---

### Storage Adapters
```ts
WebStorageAdapter           // localStorage
AsyncStorageAdapter         // RN AsyncStorage
MemoryStorageAdapter        // in-memory fallback
getStorageAdapter()         // auto-detect
```

---

## Config Options (AnalyticsConfig) — detailed

Required:
- `apiEndpoint: string`
- `apiKey: string`
- `appName: "expo-mobile" | "expo-web" | "next-web"`
- `environment: "dev" | "staging" | "prod"`

Optional (app):
- `appVersion?: string` — override app version (otherwise auto-detected where possible).

#### Batching & queue options (in config)
These control when and how many events are sent:

| Option | Type | Default | Meaning |
|--------|------|---------|---------|
| `batchSize` | `number` | `10` | Send a batch when the queue has this many events. |
| `batchInterval` | `number` | `10000` | Send a batch after this many milliseconds (10 sec) even if queue isn’t full. |
| `maxQueueSize` | `number` | `1000` | Maximum events kept in memory; oldest are dropped if exceeded. |
| `persistQueue` | `boolean` | `true` | Save queued events to storage so they survive app close/refresh. |
| `maxPersistedEvents` | `number` | `200` | Maximum number of events to save when persisting the queue. |

Example — smaller batches, more frequent sends:
```ts
await analytics.init({
  apiEndpoint: "https://your-backend.com/events",
  apiKey: "YOUR_API_KEY",
  appName: "next-web",
  environment: "prod",
  batchSize: 5,           // send after 5 events
  batchInterval: 5000,    // or every 5 seconds
  maxQueueSize: 500,      // max 500 events in queue
  persistQueue: true,
  maxPersistedEvents: 100,
});
```

Optional (other config, with defaults and meaning):
- `debug?: boolean` (default: `false`) — log SDK actions
- `enabled?: boolean` (default: `true`) — master on/off switch
- `logEventsOnly?: boolean` (default: `false`) — **no network calls**; event batches are **logged to console** only (useful for local/dev)
- `trackingMode?: "auto" | "manual"` (default: `"manual"`)
  - `auto`: auto route tracking if supported
  - `manual`: you call `analytics.page(...)` yourself or use hooks
- `router?: "auto" | "history" | "next-app" | "next-pages" | "expo-router" | "react-navigation" | "manual"`
  - `auto`: best effort based on platform
  - `history`: browser history (web)
  - `next-app`: Next.js App Router (use hook)
  - `next-pages`: Next.js Pages Router (auto works + hook)
  - `expo-router`: Expo Router (use hook)
  - `react-navigation`: React Navigation (use hook)
  - `manual`: no auto route tracking
- `autoTrackRoutes?: boolean` (default: `false`, forced `true` when `trackingMode:"auto"`)
- `includeSearchInRoute?: boolean` (default: `true`) — include `?query` in route
- `batchSize?: number` (default: `10`) — flush after N events
- `batchInterval?: number` (default: `10000`) — flush every N ms
- `maxQueueSize?: number` (default: `1000`) — max in‑memory queue
- `persistQueue?: boolean` (default: `true`) — persist queue offline
- `maxPersistedEvents?: number` (default: `200`) — limit persisted events
- `flushOnRouteChange?: boolean` (default: `true`)
- `flushOnBackground?: boolean` (default: `true`)
- `useBeaconOnFlush?: boolean` (default: `true`) — web unload flush
- `beaconRetainOnSuccess?: boolean` (default: `false`) — keep events after beacon
- `timeout?: number` (default: `5000`) — request timeout ms
- `retryAttempts?: number` (default: `3`) — retries on failure
- `retryDelay?: number` (default: `1000`) — base delay in ms
- `trackAnonymousUsers?: boolean` (default: `true`)
- `persistUserData?: boolean` (default: `true`)
- `customHeaders?: Record<string, string>` — add headers

---

## Common Usage (clear examples)

### Disable network and log events to console
Useful for local development or testing — no requests are sent; each batch is logged to the console.
```ts
await analytics.init({
  apiEndpoint: "https://your-backend.com/events",
  apiKey: "YOUR_API_KEY",
  appName: "next-web",
  environment: "dev",
  logEventsOnly: true,   // no network; log batches to console
});
analytics.track("test_event");
// When the queue flushes you'll see: [Analytics] Events (network off, log only): [...]
```

### Track event
```ts
analytics.track("button_clicked", {
  element: "buy_button",
  elementType: "button",
  data: { sku: "ABC-123" },
});
```

### Track page
```ts
analytics.page({ route: "/products/123", data: { category: "shoes" } });
```

### Identify user
```ts
analytics.identify("user-123", { email: "user@example.com" });
```

---

## Event payload (what your backend receives)

Each **batch** is one POST with a body like:

```ts
{
  events: AnalyticsEvent[],
  batch: true,
  sentAt: number,
  sessionId?: string,
  anonymousId?: string,
  app?: string,
  platform?: string,
  environment?: string,
  device?: { ... },
  userAgent?: string
}
```

Each **event** in `events` includes: `eventId`, `event`, `userId?`, `anonymousId`, `sessionId`, `app`, `platform`, `environment`, `route?`, `previousRoute?`, `referrer?`, `element?`, `elementType?`, `data?`, `device?`, `timestamp`, `userAgent?`, `eventSequence?`, `sessionSequence?`.

---

## Exported types (TypeScript)

You can import for typing: `AnalyticsEvent`, `AnalyticsConfig`, `AnalyticsSDK`, `TrackOptions`, `PageViewOptions`, `IdentifyOptions`, `SessionData`, `StorageAdapter`, `NetworkAdapter`, `DeviceInfo`, `PlatformType`, `AppType`, `RouterType`, `RouterConfig`, `AnalyticsPlugin`, `QueueItem`.

---

## Router Choice (quick)

Use one of these:
- `router: "next-app"` + `useNextAppRouterTracking()`
- `router: "next-pages"` + `useNextPagesRouterTracking()` (auto works too)
- `router: "expo-router"` + `useExpoRouterTracking()`
- `router: "react-navigation"` + `useReactNavigationTracking()`
- `router: "history"` + `trackingMode: "auto"`
- `router: "manual"` + call `analytics.page(...)` yourself

---

## Notes (behavior summary)
- Events are **batched** and sent in one request when a flush runs (see “How events are sent” and “When does flush happen?”).
- Flush runs **automatically** on interval, batch size, page hide, app background, and (optionally) route change; you can also call `analytics.flush()` for a manual send.
- On **web**, the SDK can use **beacon** for flush on page hide/unload so events are not lost when the user leaves (see “What is Beacon?”).
- Works for Expo (mobile + web) and Next.js (App/Pages Router).