npm package: @riuve/next
Description: Behavioral event tracking SDK for Next.js
Latest version: 1.0.8

# @riuve/next

> **Behavioral event tracking SDK for Next.js — automatic page tracking, offline queuing, and intelligent batching.**

[![npm version](https://img.shields.io/npm/v/@riuve/next.svg)](https://www.npmjs.com/package/@riuve/next)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

---

## Table of Contents

1. [What is Riuve?](#what-is-riuve)
2. [Installation](#installation)
3. [Quick Start](#quick-start)
4. [Usage with React Hooks](#usage-with-react-hooks)
5. [Auto Page Tracking](#auto-page-tracking)
6. [User Identification](#user-identification)
7. [Super Properties](#super-properties)
8. [Advanced Configuration](#advanced-configuration)
9. [Best Practices](#best-practices)
10. [API Reference](#api-reference)

---

## What is Riuve?

**Riuve** is a behavioral push notification platform. You track user events via the SDK, and from the [Riuve Dashboard](https://dashboard.riuve.com) you create campaigns that trigger push notifications based on those behaviors.

**Example:** A user adds an item to their cart but doesn't check out → 2 hours later, send them a reminder notification.

### Features

- ✅ **Auto Page Tracking** — `$page_view` events on every route change (App Router & Pages Router)
- ✅ **Offline Queue** — Events are persisted in `localStorage` and sent when back online
- ✅ **Intelligent Batching** — Events are grouped and sent in batches to minimize API calls
- ✅ **Auto Retry** — Failed requests retry with exponential backoff (3 attempts)
- ✅ **Background Flush** — Events flush automatically when the tab becomes hidden or the page unloads
- ✅ **Session Management** — Automatic `$session_start` / `$session_end` tracking
- ✅ **Super Properties** — Attach global properties to every event automatically
- ✅ **Pre-init Queue** — Events tracked before `initialize()` completes are not lost
- ✅ **TypeScript** — Full type definitions included
- ✅ **SSR Safe** — All browser APIs are guarded with `typeof window !== 'undefined'`

---

## Installation

```bash
npm install @riuve/next
# or
yarn add @riuve/next
# or
pnpm add @riuve/next
```

**Requirements:** Next.js 14+, React 18+

---

## Quick Start

### Step 1: Add the Provider

Wrap your app with `RiuveProvider` in your root layout. Get your API key from the [Riuve Dashboard](https://dashboard.riuve.com).

**App Router (`app/layout.tsx`):**

```tsx
import { RiuveProvider } from '@riuve/next';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <RiuveProvider apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!}>
          {children}
        </RiuveProvider>
      </body>
    </html>
  );
}
```

**Pages Router (`pages/_app.tsx`):**

```tsx
import type { AppProps } from 'next/app';
import { RiuveProvider } from '@riuve/next';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <RiuveProvider apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!}>
      <Component {...pageProps} />
    </RiuveProvider>
  );
}
```

### Step 2: Track Events

```tsx
'use client';

import { useRiuve } from '@riuve/next';

export default function ProductPage({ product }) {
  const { track } = useRiuve();

  const handleAddToCart = async () => {
    await track('add_to_cart', {
      product_id: product.id,
      product_name: product.name,
      price: product.price,
    });
  };

  return <button onClick={handleAddToCart}>Add to Cart</button>;
}
```

### Step 3: Enable Auto Page Tracking

Add `usePageTracking()` once anywhere inside `RiuveProvider` — it automatically fires a `$page_view` event on every route change.

```tsx
'use client';

import { usePageTracking } from '@riuve/next';

export default function Analytics() {
  usePageTracking();
  return null;
}
```

```tsx
// app/layout.tsx
import { RiuveProvider } from '@riuve/next';
import Analytics from './Analytics';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <RiuveProvider apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!}>
          <Analytics />
          {children}
        </RiuveProvider>
      </body>
    </html>
  );
}
```

---

## Usage with React Hooks

### `useRiuve()`

The main hook for tracking events and identifying users.

```tsx
'use client';

import { useRiuve } from '@riuve/next';

export default function CheckoutButton() {
  const { track, identify, isReady } = useRiuve();

  const handleCheckout = async () => {
    await track('checkout_started', {
      cart_value: 149.99,
      item_count: 3,
    });
  };

  return (
    <button onClick={handleCheckout} disabled={!isReady()}>
      Checkout
    </button>
  );
}
```

### `useRiuveContext()`

Access the provider context directly — includes the `isReady` boolean as reactive state.

```tsx
'use client';

import { useRiuveContext } from '@riuve/next';

export default function TrackingStatus() {
  const { isReady, track } = useRiuveContext();

  return (
    <div>
      <span>Tracking: {isReady ? 'Active' : 'Initializing...'}</span>
      <button onClick={() => track('test_event')}>Send Test</button>
    </div>
  );
}
```

> **`useRiuve` vs `useRiuveContext`:** Use `useRiuve` in most cases — it's lighter. Use `useRiuveContext` when you need the reactive `isReady` boolean to update your UI.

### Direct import (without hooks)

You can also import the `Riuve` singleton directly. Useful in utility functions, server actions, or outside React components.

```ts
import Riuve from '@riuve/next';

// In a utility function
export async function trackPurchase(orderId: string, amount: number) {
  await Riuve.track('purchase_completed', { order_id: orderId, amount });
  await Riuve.flush(); // Send immediately
}
```

> **Note:** Always call `Riuve.track()` on the client side only. Do not call it in Server Components or API routes.

---

## Auto Page Tracking

`usePageTracking()` listens to Next.js pathname changes and fires a `$page_view` event automatically.

```tsx
usePageTracking();
// Tracks: { page_url, page_path, page_title } on every route change
```

**With extra properties:**

```tsx
usePageTracking({
  extraProperties: {
    app_section: 'marketing',
    experiment: 'hero_v2',
  },
});
```

**Event payload:**

```json
{
  "name": "$page_view",
  "properties": {
    "page_url": "https://example.com/products/123",
    "page_path": "/products/123",
    "page_title": "Product Name — My Store"
  }
}
```

---

## User Identification

Call `identify()` when a user logs in or when their information is available. This links all subsequent events to their user profile in the Riuve Dashboard.

```tsx
'use client';

import { useRiuve } from '@riuve/next';

export default function LoginForm() {
  const { identify } = useRiuve();

  const handleLogin = async (user: User) => {
    // Authenticate user...

    await identify(user.id, {
      email: user.email,
      name: user.name,
      plan: user.subscriptionPlan,
      created_at: user.createdAt,
    });
  };

  return <form onSubmit={handleLogin}>{/* ... */}</form>;
}
```

**When to call `identify`:**
- After a user logs in
- After a user signs up
- On page load if the user is already authenticated
- When user profile data changes

**When to call `reset`:**

```ts
// On logout — clears identity and generates a new anonymous ID
await Riuve.reset();
```

---

## Super Properties

Super properties are attached to **every event** automatically. Set them once for properties that always apply, like plan tier, experiment group, or app version.

```ts
import Riuve from '@riuve/next';

// Set after initialization
await Riuve.registerSuperProperties({
  app_version: '2.4.0',
  plan: 'pro',
  experiment_group: 'variant_b',
});

// Now every track() call includes these properties automatically
await Riuve.track('button_clicked', { button_id: 'upgrade' });
// → { button_id: "upgrade", app_version: "2.4.0", plan: "pro", experiment_group: "variant_b" }
```

```ts
// Register only if not already set (useful for first-time values)
await Riuve.registerSuperPropertiesOnce({ first_referrer: document.referrer });

// Remove a single super property
await Riuve.unregisterSuperProperty('experiment_group');
```

---

## Advanced Configuration

```tsx
<RiuveProvider
  apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!}
  disableTracking={process.env.NODE_ENV !== 'production'}  // Disable in development
  config={{
    // Batching
    batchSize: 10,              // Events per batch (default: 10)
    batchTimeout: 30000,        // Auto-send after N ms (default: 30000)

    // Network
    requestTimeout: 10000,      // API request timeout in ms (default: 10000)
    maxRetries: 3,              // Retry attempts on failure (default: 3)
    retryDelay: 1000,           // Base retry delay in ms (default: 1000)

    // Storage
    persistEvents: true,        // Persist queue to localStorage (default: true)
    maxQueueSize: 1000,         // Max queued events (default: 1000)
    eventRetentionDays: 7,      // Days to keep failed events (default: 7)

    // Behaviour
    autoFlushOnBackground: true, // Flush on tab hide / page unload (default: true)

    // Debug
    debugMode: false,           // Verbose console logging (default: false)
  }}
>
  {children}
</RiuveProvider>
```

### Recommended configs

**Disable tracking entirely:**
```tsx
// Always disabled
<RiuveProvider apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!} disableTracking>

// Disabled only in development
<RiuveProvider
  apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!}
  disableTracking={process.env.NODE_ENV !== 'production'}
>
```

**Development (with tracking enabled for debugging):**
```ts
{
  debugMode: true,
  batchSize: 1,       // Send every event immediately for easier testing
  batchTimeout: 0,
}
```

**Production:**
```ts
{
  debugMode: false,
  batchSize: 10,
  batchTimeout: 30000,
  autoFlushOnBackground: true,
}
```

**High-traffic pages:**
```ts
{
  batchSize: 25,
  batchTimeout: 60000,
  maxQueueSize: 5000,
}
```

---

## Best Practices

### 1. Use environment variables for the API key

```ts
// .env.local
NEXT_PUBLIC_RIUVE_API_KEY=riuve_base_xxxxxxxxxxxxxxxxxxxxx
```

```tsx
<RiuveProvider apiKey={process.env.NEXT_PUBLIC_RIUVE_API_KEY!}>
```

> Never hardcode the API key directly in source files.

### 2. Flush after critical events

```ts
// Payment completed — send immediately, don't wait for the next batch
await Riuve.track('purchase_completed', { order_id, amount });
await Riuve.flush();
```

### 3. Use consistent event naming

```ts
// ✅ Good — snake_case, verb + noun
track('product_viewed', { product_id })
track('cart_item_added', { product_id, quantity })
track('checkout_completed', { order_id, total })

// ❌ Bad — inconsistent
track('viewProduct')
track('AddToCart')
track('checkout')
```

### 4. Don't block UI for tracking

```ts
// ✅ Good — fire and forget
const handleClick = () => {
  track('button_clicked', { id: 'subscribe' }).catch(console.error);
  router.push('/subscribe');
};

// ❌ Bad — awaiting track blocks navigation
const handleClick = async () => {
  await track('button_clicked', { id: 'subscribe' }); // Don't await unless critical
  router.push('/subscribe');
};
```

### 5. Identify users on the client side

```tsx
// ✅ Good — in a Client Component after authentication
'use client';
const { identify } = useRiuve();
useEffect(() => {
  if (session?.user) identify(session.user.id, { email: session.user.email });
}, [session]);

// ❌ Bad — in a Server Component or API route
// identify() only works on the client
```

---

## API Reference

### `RiuveProvider`

```tsx
<RiuveProvider apiKey={string} enabled?: boolean config?: RiuveConfigInput>
```

Initializes the SDK on mount. All hooks must be used inside this provider.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `apiKey` | `string` | — | Your Riuve API key (required) |
| `disableTracking` | `boolean` | `false` | When `true`, the SDK skips initialization and all calls become no-ops |
| `config` | `object` | — | Advanced configuration (see above) |

---

### `useRiuve()`

```ts
const { track, identify, flush, isReady, getAnonymousId, getExternalUserId, registerSuperProperties } = useRiuve()
```

| Method | Signature | Description |
|--------|-----------|-------------|
| `track` | `(name: string, props?: object) => Promise<void>` | Track an event |
| `identify` | `(userId: string, props?: object) => Promise<void>` | Identify a user |
| `flush` | `() => Promise<void>` | Send all queued events immediately |
| `isReady` | `() => boolean` | Returns `true` after initialization |
| `getAnonymousId` | `() => string \| null` | Returns the anonymous ID |
| `getExternalUserId` | `() => string \| null` | Returns the identified user ID |
| `registerSuperProperties` | `(props: object) => Promise<void>` | Set global event properties |

---

### `usePageTracking(options?)`

```ts
usePageTracking({ extraProperties?: Record<string, any> })
```

Tracks a `$page_view` event on every pathname change. Add it once in your root layout.

---

### `useRiuveContext()`

```ts
const { isReady, track, identify, flush } = useRiuveContext()
```

Same as `useRiuve()` but `isReady` is a reactive boolean state that triggers re-renders.

---

### `Riuve` (singleton)

Importable anywhere on the client side.

```ts
import Riuve from '@riuve/next';
```

| Method | Description |
|--------|-------------|
| `Riuve.initialize(apiKey, config?)` | Initialize the SDK manually (not needed if using `RiuveProvider`) |
| `Riuve.track(name, props?)` | Track an event |
| `Riuve.identify(userId, props?)` | Identify a user |
| `Riuve.flush()` | Send all queued events immediately |
| `Riuve.reset()` | Clear identity and generate a new anonymous ID |
| `Riuve.setLocale(locale)` | Override session locale (e.g. `'tr'`, `'en'`) |
| `Riuve.registerSuperProperties(props)` | Set persistent global event properties |
| `Riuve.registerSuperPropertiesOnce(props)` | Set only if not already set |
| `Riuve.unregisterSuperProperty(key)` | Remove a single super property |
| `Riuve.isReady()` | Returns `true` after initialization |
| `Riuve.getAnonymousId()` | Returns the anonymous ID |
| `Riuve.getExternalUserId()` | Returns the identified user ID |
| `Riuve.getSessionId()` | Returns the current session ID |
| `Riuve.getQueueSize()` | Returns number of queued events |
| `Riuve.getConfig()` | Returns a copy of the current config |
| `Riuve.cleanup()` | Flush and tear down (for testing / unmounting) |

---

## Automatic Events

The SDK automatically tracks the following events — no setup required:

| Event | When | Properties |
|-------|------|------------|
| `$session_start` | New session begins | `session_id` |
| `$session_end` | Session ends (tab closes / 30 min idle) | `session_id`, `session_duration_ms` |
| `$page_view` | Route changes (requires `usePageTracking`) | `page_url`, `page_path`, `page_title` |

---

## Support

- 📚 Documentation: [docs.riuve.com](https://docs.riuve.com)
- 📧 Email: support@riuve.com
- 🐛 Issues: [github.com/riuve/sdk](https://github.com/riuve)

---

## License

MIT © [Riuve](https://riuve.com)