@risenexa/tracking
v0.1.0
Published
Node.js SDK for tracking user events with Risenexa
Readme
@risenexa/tracking
Node.js SDK for tracking user registration and conversion events with Risenexa.
Requirements
- Node.js 18 or later (uses built-in
fetchandcrypto.randomUUID()) - Zero runtime dependencies
Installation
npm install @risenexa/trackingQuick Start
Global Configuration (recommended for single-startup apps)
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)
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:
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.
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").
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").
await trackConversion({
userId: "usr_123",
metadata: { plan: "pro", mrr: 29 },
});track({ eventType, userId, ...options })
Low-level method that accepts all event fields directly.
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.
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.
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:
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.
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.
// CommonJS
const { configure, trackRegistration } = require("@risenexa/tracking");
// ES modules
import { configure, trackRegistration } from "@risenexa/tracking";Next.js Example
// 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";// 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
