npm package: @risenexa/tracking
Description: Node.js SDK for tracking user events with Risenexa
Author: Patrick Espake
Latest version: 0.1.0

# @risenexa/tracking

Node.js SDK for tracking user registration and conversion events with [Risenexa](https://app.risenexa.com).

## Requirements

- Node.js 18 or later (uses built-in `fetch` and `crypto.randomUUID()`)
- Zero runtime dependencies

## Installation

```bash
npm install @risenexa/tracking
```

## Quick Start

### Global Configuration (recommended for single-startup apps)

```typescript
import { configure, trackRegistration, trackConversion } from "@risenexa/tracking";

// Call once at app boot
configure({
  apiKey: "rxt_live_your_api_key",
  startupSlug: "my-startup",
});

// Track a new user registration
await trackRegistration({ userId: "usr_123" });

// Track a user becoming a paying customer
await trackConversion({ userId: "usr_123" });
```

### Per-Instance Configuration (for multi-startup apps)

```typescript
import { RisenexaTracking } from "@risenexa/tracking";

const client = new RisenexaTracking({
  apiKey: "rxt_live_your_api_key",
  startupSlug: "my-startup",
});

await client.trackRegistration({ userId: "usr_456" });
await client.trackConversion({ userId: "usr_456" });
```

Multiple instances are fully independent — no shared state:

```typescript
const startupA = new RisenexaTracking({ apiKey: "key-a", startupSlug: "startup-a" });
const startupB = new RisenexaTracking({ apiKey: "key-b", startupSlug: "startup-b" });

await startupA.trackRegistration({ userId: "user-1" });
await startupB.trackConversion({ userId: "user-2" });
```

## API Reference

### `configure(options)`

Configure the global SDK instance. Call once at app startup before using module-level functions.

```typescript
configure({
  apiKey: "rxt_live_abc123",       // required
  startupSlug: "my-startup",        // required
  baseUrl: "https://app.risenexa.com", // optional, default shown
  timeout: 2000,                    // optional, default: 2000ms
  maxRetries: 3,                    // optional, default: 3
});
```

### `trackRegistration({ userId, ...options })`

Track a user registration event (`event_type: "user_registered"`).

```typescript
await trackRegistration({
  userId: "usr_123",                          // required
  eventId: "550e8400-e29b-41d4-a716-...",    // optional — auto-generated UUID v4
  occurredAt: "2026-04-01T12:00:00Z",         // optional — ISO 8601 UTC
  metadata: { plan: "free", source: "web" },  // optional — arbitrary JSONB
  action: "add",                              // optional — "add" | "remove", default: "add"
});
```

### `trackConversion({ userId, ...options })`

Track a user conversion event (`event_type: "user_converted"`).

```typescript
await trackConversion({
  userId: "usr_123",
  metadata: { plan: "pro", mrr: 29 },
});
```

### `track({ eventType, userId, ...options })`

Low-level method that accepts all event fields directly.

```typescript
await track({
  eventType: "user_registered",               // required
  userId: "usr_123",                          // required
  eventId: "550e8400-e29b-41d4-a716-...",    // optional
  occurredAt: "2026-04-01T12:00:00Z",         // optional
  metadata: { plan: "pro" },                  // optional
  action: "add",                              // optional
});
```

### `new RisenexaTracking(options)`

Create a per-instance client with independent configuration.

```typescript
const client = new RisenexaTracking({
  apiKey: string;        // required
  startupSlug: string;   // required
  baseUrl?: string;      // default: "https://app.risenexa.com"
  timeout?: number;      // default: 2000 (ms)
  maxRetries?: number;   // default: 3
});

// Same methods as module-level functions:
await client.trackRegistration({ userId });
await client.trackConversion({ userId });
await client.track({ eventType, userId, ... });
```

## Configuration Options

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `apiKey` | Yes | — | Bearer token with `tracking:write` scope from Risenexa dashboard |
| `startupSlug` | Yes | — | The slug of your startup in Risenexa |
| `baseUrl` | No | `"https://app.risenexa.com"` | Override for staging or self-hosted instances |
| `timeout` | No | `2000` | Per-request timeout in milliseconds; resets per retry attempt |
| `maxRetries` | No | `3` | Maximum retry attempts; set to `0` to disable retries |

## Error Handling

All errors extend `RisenexaTrackingError`, which extends `Error`.

```typescript
import {
  RisenexaTracking,
  AuthenticationError,
  AuthorizationError,
  ValidationError,
  StartupNotFoundError,
  RateLimitError,
  MaxRetriesExceededError,
  ConnectionError,
  ConfigurationError,
} from "@risenexa/tracking";

try {
  await client.trackRegistration({ userId: "usr_123" });
} catch (err) {
  if (err instanceof ConfigurationError) {
    // Missing apiKey or startupSlug — fix your configure() call
    console.error("SDK not configured:", err.message);
  } else if (err instanceof AuthenticationError) {
    // HTTP 401 — invalid or missing API token
    console.error("Invalid API key:", err.message);
  } else if (err instanceof AuthorizationError) {
    // HTTP 403 — token lacks tracking:write scope
    console.error("Token lacks required scope:", err.message);
  } else if (err instanceof StartupNotFoundError) {
    // HTTP 404 — wrong startupSlug or startup not in your account
    console.error("Startup not found:", err.message);
  } else if (err instanceof ValidationError) {
    // HTTP 422 — invalid payload
    console.error("Validation errors:", err.errors); // string[]
  } else if (err instanceof RateLimitError) {
    // All retries exhausted on 429 responses
    console.error(`Rate limited. Retry after ${err.retryAfter}s`);
  } else if (err instanceof MaxRetriesExceededError) {
    // All retries exhausted on 5xx responses
    console.error("Max retries exceeded:", err.message);
  } else if (err instanceof ConnectionError) {
    // All retries exhausted on transport errors (timeouts, connection refused)
    console.error("Connection failed:", err.message);
  }
}
```

## Return Value

On success, all tracking methods return a `TrackingResult`:

```typescript
interface TrackingResult {
  success: boolean;               // true when HTTP 202 received
  statusCode: number;             // 202
  eventId: string;                // UUID v4 sent with the request
  body: Record<string, unknown>;  // { status: "accepted" }
}
```

## Retry Behavior

The SDK automatically retries on transient failures with exponential backoff and ±20% jitter:

| Condition | Action |
|-----------|--------|
| HTTP 429 (rate limited) | Retry; honor `Retry-After` header if present |
| HTTP 500, 502, 503 | Retry with exponential backoff |
| Request timeout (2000ms) | Retry; timeout resets per attempt |
| Connection error | Retry with exponential backoff |
| HTTP 401, 403, 404, 422 | **Never retry** — permanent failure, throws immediately |

**Backoff formula:**
```
delay = min(1.0 * 2^attempt, 30.0) seconds  (±20% random jitter)
```

| Before Retry | Base Delay | Jitter Range |
|--------------|------------|--------------|
| Retry 1 | 1.0s | [0.8s, 1.2s] |
| Retry 2 | 2.0s | [1.6s, 2.4s] |
| Retry 3 | 4.0s | [3.2s, 4.8s] |

## Idempotency

Each tracking call generates a UUID v4 `event_id` before the first HTTP attempt. This `event_id` is reused on all retry attempts, ensuring exactly-once counter increments even if a request is retried after a transient failure.

## TypeScript

All types are included in the package. No `@types/*` packages required.

```typescript
import type {
  ConfigOptions,
  ConvenienceOptions,
  TrackOptions,
  TrackingResult,
} from "@risenexa/tracking";
```

## CJS and ESM

The package ships both CommonJS (`.cjs`) and ES module (`.js`) builds with TypeScript declarations.

```javascript
// CommonJS
const { configure, trackRegistration } = require("@risenexa/tracking");

// ES modules
import { configure, trackRegistration } from "@risenexa/tracking";
```

## Next.js Example

```typescript
// lib/risenexa.ts
import { configure } from "@risenexa/tracking";

configure({
  apiKey: process.env.RISENEXA_API_KEY!,
  startupSlug: process.env.RISENEXA_STARTUP_SLUG!,
});

export { trackRegistration, trackConversion } from "@risenexa/tracking";
```

```typescript
// app/api/auth/signup/route.ts
import { trackRegistration } from "@/lib/risenexa";

export async function POST(request: Request) {
  const { userId } = await createUser(request);

  // Non-blocking fire-and-forget
  trackRegistration({ userId }).catch(console.error);

  return Response.json({ success: true });
}
```

## License

MIT — Copyright (c) 2026 Patrick Espake