npm package: @soda-exp/core
Description: Lightweight A/B testing SDK for Next.js applications
Latest version: 0.1.0

<div align="center">
  <img src="./assets/soda.png" alt="Soda SDK Logo" width="200"/>

  # Soda SDK

  ### Lightweight A/B testing SDK for Next.js applications

  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
  [![Next.js](https://img.shields.io/badge/Next.js-13+-black.svg)](https://nextjs.org/)
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)

</div>

---

## Overview

Soda SDK is a Next.js-native A/B testing solution that integrates seamlessly with your existing middleware. It provides developers with tools to run experiments, assign variants, and manage experiment state through cookies.

**Key Features:**

- **Automatic pathname-based experiment matching** - No manual experiment selection needed
- **Deterministic sticky assignments** - Users see consistent variants across sessions
- **Next.js Data Cache integration** - Built-in caching with configurable TTL
- **Edge runtime compatible** - Works in both Edge and Node.js runtimes
- **Route-level experiments** - Rewrite paths based on variant assignments
- **Non-invasive integration** - Works alongside existing middleware
- **Comprehensive error handling** - Never crashes your application

## Installation

```bash
npm install @soda-sdk/core
```

## Quick Start

### 1. Initialize the SDK

```typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { initSoda } from '@soda-sdk/core';

const soda = await initSoda({
  token: process.env.SODA_TOKEN!, // Required: Your Soda API token
  apiEndpoint: 'https://soda.com/experiments', // Optional: defaults to this
  cacheTTL: 300, // Optional: cache duration in seconds (default: 5 minutes)
});

export async function middleware(request: NextRequest) {
  // Automatically processes experiments based on request pathname
  const result = await soda.processExperiments(request);

  // If experiment matched and variant assigned, rewrite the path
  if (result.success && result.rewritePath) {
    return NextResponse.rewrite(new URL(result.rewritePath, request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};
```

### 2. Experiment Configuration (from Backend)

Your experiments are fetched automatically from the Soda API. Each experiment should follow this structure:

```typescript
{
  "experimentId": "landing-redesign",
  "name": "Landing Page Redesign",
  "active": true,
  "type": "route",
  "variants": [
    {
      "id": "control",
      "name": "Original",
      "percentage": 90,
      "metadata": {
        "route": "/mattress"  // The pathname to match
      }
    },
    {
      "id": "variant-a",
      "name": "Variant A",
      "percentage": 5,
      "metadata": {
        "route": "/mattress-a"  // The rewrite path
      }
    },
    {
      "id": "variant-b",
      "name": "Variant B",
      "percentage": 5,
      "metadata": {
        "route": "/mattress-b"  // The rewrite path
      }
    }
  ]
}
```

### 3. How It Works

1. **User visits** `/mattress`
2. **SDK fetches** all active experiments (cached with Next.js Data Cache)
3. **SDK matches** experiment by comparing control variant route to pathname
4. **SDK checks** if user has existing assignment (sticky behavior via cookies)
5. **SDK assigns** variant based on deterministic hash if new user
6. **SDK returns** result with `rewritePath` for middleware to apply
7. **Cookies set**: `__ab_uid` (user ID) and `__ab_exp` (assignments)

## API Reference

### `initSoda(config: SodaConfig)`

Initializes the Soda SDK and authenticates with the backend.

**Parameters:**
- `config.token` (required): Your Soda API authentication token
- `config.apiEndpoint` (optional): API endpoint URL (default: `https://soda.com/experiments`)
- `config.cacheTTL` (optional): Cache duration in seconds (default: `300`)

**Returns:** Promise resolving to SDK instance with `processExperiments` method

**Example:**
```typescript
const soda = await initSoda({
  token: process.env.SODA_TOKEN!,
  cacheTTL: 600, // 10 minutes
});
```

### `soda.processExperiments(request: NextRequest)`

Processes experiments for the current request. Automatically matches experiments based on pathname, assigns variants, and manages cookies.

**Parameters:**
- `request`: Next.js request object

**Returns:** Promise resolving to `ExperimentResult`

**Example:**
```typescript
const result = await soda.processExperiments(request);

if (result.success && result.rewritePath) {
  return NextResponse.rewrite(new URL(result.rewritePath, request.url));
}
```

### `ExperimentResult`

```typescript
interface ExperimentResult {
  success: boolean;              // Whether processing succeeded
  error?: string;                // Error message if failed
  experimentId: string | null;   // Matched experiment ID
  assignedVariant: string | null; // Assigned variant ID
  variantName?: string;          // Human-readable variant name
  isNewAssignment: boolean;      // Whether this is a new assignment
  rewritePath: string | null;    // Path to rewrite to (if applicable)
  metadata: object | null;       // Additional variant metadata
  userId: string;                // User ID (from cookie or generated)
}
```

## Advanced Usage

### Multiple Experiments

The SDK automatically handles multiple experiments. Simply define them in your backend:

```typescript
// No changes needed in middleware - automatic matching!
export async function middleware(request: NextRequest) {
  const result = await soda.processExperiments(request);

  if (result.success && result.rewritePath) {
    return NextResponse.rewrite(new URL(result.rewritePath, request.url));
  }

  return NextResponse.next();
}
```

### Error Handling

The SDK never crashes your application. All errors are caught and logged:

```typescript
const result = await soda.processExperiments(request);

if (!result.success) {
  console.error('Experiment processing failed:', result.error);
  // App continues normally
}
```

### Component-Level Experiments

While the current implementation focuses on route-level experiments, the infrastructure supports component-level experiments through the `type` field in experiment configuration.

## How Assignment Works

1. **Deterministic Hashing**: Uses a hash of `userId` + `experimentId` to generate a seed (0-100)
2. **Percentage Distribution**: Variants are assigned based on cumulative percentage ranges
3. **Sticky Behavior**: Assignments are stored in `__ab_exp` cookie for consistency
4. **Reassignment**: If a variant is removed from config, users are automatically reassigned

**Example:**
- Control: 0-90 (90%)
- Variant A: 90-95 (5%)
- Variant B: 95-100 (5%)

A user with seed `92.5` gets assigned to Variant A.

## Cookies

The SDK uses two cookies:

- `__ab_uid`: User identifier (UUID v4, HttpOnly, 1 year)
- `__ab_exp`: Experiment assignments (JSON, readable by client, 1 year)

Both cookies use `sameSite: 'lax'` and `secure` in production.

## Caching

Experiment configurations are cached using Next.js Data Cache:

```typescript
fetch(apiEndpoint, {
  next: { revalidate: cacheTTL }, // Automatic caching
});
```

Default cache duration is 5 minutes (300 seconds), configurable via `cacheTTL`.

## Documentation

See [CLAUDE.md](./CLAUDE.md) for detailed project architecture and implementation notes.

## Development

```bash
# Install dependencies
npm install

# Build the SDK
npm run build

# Run in development mode
npm run dev

# Type check
npm run type-check

# Lint code
npm run lint

# Format code
npm run format
```

## Requirements

- Next.js 13+ (App Router)
- Node.js 18+
- TypeScript 5+

## License

MIT