npm package: @klime/node
Description: Klime SDK for Node.js
Author: Klime
Latest version: 1.3.0

# @klime/node

Klime SDK for Node.js.

## Installation

```bash
npm install @klime/node
```

## Quick Start

```javascript
// ESM
import { KlimeClient } from "@klime/node";

// CommonJS
// const { KlimeClient } = require("@klime/node");

const client = new KlimeClient({
  writeKey: "your-write-key",
});

// Identify a user
client.identify("user_123", {
  email: "user@example.com",
  name: "Stefan",
});

// Track an event
client.track(
  "Button Clicked",
  {
    buttonName: "Sign up",
    plan: "pro",
  },
  { userId: "user_123" }
);

// Associate user with a group and set group traits
client.group(
  "org_456",
  { name: "Acme Inc", plan: "enterprise" },
  { userId: "user_123" }
);

// Or just link the user to a group (if traits are already set)
client.group("org_456", null, { userId: "user_123" });

// Shutdown gracefully
process.on("SIGTERM", async () => {
  await client.shutdown();
  process.exit(0);
});
```

## Good to know

- **No anonymous tracking.** Every `track()` call needs a `userId` or `groupId`. Wait until the user is identified before sending events.
- **Name your events in past tense** with an Object + Action pattern: `"Report Generated"`, `"User Invited"`, `"Export Completed"`.
- **Always set `email` and `name` traits** in `identify()` and `group()`. These are used for display and search in the dashboard.
- **Traits can be strings, numbers, booleans, or ISO 8601 date strings.** Use camelCase for trait keys.
- **In Group mode, your dashboard stays empty until you call `group()`.** Events from `identify()` and `track()` are recorded, but customers won't appear until users are linked to groups.
- **Order doesn't matter.** Events sent before `identify()` or `group()` are retroactively attributed once the relationships are established.

## Installation Prompt

Copy and paste this prompt into Cursor, Copilot, or your favorite AI editor to integrate Klime:

```
Integrate Klime for customer analytics. Klime tracks user activity to identify which customers are healthy vs at risk of churning.

ANALYTICS MODES (determine which applies):
- Companies & Teams: Your customers are companies with multiple team members (SaaS, enterprise tools)
  → Use identify() + group() + track()
- Individual Customers: Your customers are individuals with private accounts (consumer apps, creator tools)
  → Use identify() + track() only (no group() needed)

KEY CONCEPTS:
- Every track() call requires either userId OR groupId (no anonymous events)
- Use groupId alone for org-level events (webhooks, cron jobs, system metrics)
- group() links a user to a company AND sets company traits (only for Companies & Teams mode)
- Order doesn't matter - events before identify/group still get attributed correctly

BEST PRACTICES:
- Initialize client ONCE at app startup (singleton pattern)
- Store write key in KLIME_WRITE_KEY environment variable
- Call shutdown() on SIGTERM/SIGINT to flush remaining events

Install: npm install @klime/node

const { KlimeClient } = require("@klime/node");

// Initialize with your KLIME_WRITE_KEY environment variable
const client = new KlimeClient({ writeKey: "YOUR_WRITE_KEY" });

// Identify users at signup/login:
client.identify("usr_abc123", { email: "jane@acme.com", name: "Jane Smith" });

// Track key activities:
client.track("Report Generated", { report_type: "revenue" }, { userId: "usr_abc123" });
client.track("Feature Used", { feature: "export", format: "csv" }, { userId: "usr_abc123" });
client.track("Teammate Invited", { role: "member" }, { userId: "usr_abc123" });

// If Companies & Teams mode: link user to their company and set company traits
client.group("org_456", { name: "Acme Inc", plan: "enterprise" }, { userId: "usr_abc123" });

// Graceful shutdown:
process.on("SIGTERM", async () => { await client.shutdown(); process.exit(0); });

INTEGRATION WORKFLOW:

Phase 1: Discover
Explore the codebase to understand:
1. What framework is used? (Express, Hono, Fastify, Koa, Next.js, NestJS, etc.)
2. Where is user identity available? (e.g., req.user.id, ctx.state.user, session.userId, JWT payload)
3. Is this Companies & Teams or Individual Customers?
   - Look for: organization, workspace, tenant, team, account models → Companies & Teams (use group())
   - No company/org concept, just individual users → Individual Customers (skip group())
4. Where do core user actions happen? (route handlers, services, controllers)
5. Is there existing analytics? (search: segment, posthog, mixpanel, amplitude, .track)
Match your integration style to the framework's conventions.

Phase 2: Instrument
Add these calls using idiomatic patterns for the framework:
- Initialize client once at startup (Express: app.js, Next.js: instrumentation.ts, NestJS: module provider)
- identify() in auth/login success handler
- group() when user-org association is established (Companies & Teams mode only)
- track() for key user actions (see below)

WHAT TO TRACK:
Active engagement (primary): feature usage, resource creation, collaboration, completing flows
Session signals (secondary): login/session start, dashboard access - distinguishes "low usage" from "churned"
Do NOT track: every page view, every API request, health checks, background jobs

Phase 3: Verify
Confirm: client initialized, shutdown handled, identify/group/track calls added

Phase 4: Summarize
Report what you added:
- Files modified and what was added to each
- Events being tracked (list event names and what triggers them)
- How userId is obtained (and groupId if Companies & Teams mode)
- Any assumptions made or questions
```

## API Reference

### Constructor

```typescript
new KlimeClient(config: {
  writeKey: string;              // Required: Your Klime write key
  endpoint?: string;              // Optional: API endpoint (default: https://i.klime.com)
  flushInterval?: number;        // Optional: Milliseconds between flushes (default: 2000)
  maxBatchSize?: number;         // Optional: Max events per batch (default: 20, max: 100)
  maxQueueSize?: number;         // Optional: Max queued events (default: 1000)
  retryMaxAttempts?: number;     // Optional: Max retry attempts (default: 5)
  retryInitialDelay?: number;    // Optional: Initial retry delay in ms (default: 1000)
  flushOnShutdown?: boolean;     // Optional: Auto-flush on SIGTERM/SIGINT (default: true)
  logger?: Logger;               // Optional: Custom logger (default: console with [Klime] prefix)
  onError?: (error, events) => void;   // Optional: Callback for batch failures
  onSuccess?: (response) => void;      // Optional: Callback for successful sends
})
```

### Methods

#### `track(event: string, properties?: object, options?: { userId?, groupId? })`

Track an event. Events can be attributed in two ways:

- **User events**: Provide `userId` to track user activity (most common)
- **Group events**: Provide `groupId` without `userId` for organization-level events

```javascript
// User event (most common)
client.track(
  "Button Clicked",
  {
    buttonName: "Sign up",
    plan: "pro",
  },
  { userId: "user_123" }
);

// Group event (for webhooks, cron jobs, system events)
client.track(
  "Events Received",
  {
    count: 100,
    source: "webhook",
  },
  { groupId: "org_456" }
);
```

> **Note**: The `groupId` option can also be combined with `userId` for multi-tenant scenarios where you need to specify which organization context a user event occurred in.

#### `identify(userId: string, traits?: object)`

Identify a user with traits.

```javascript
client.identify("user_123", {
  email: "user@example.com",
  name: "Stefan",
});
```

#### `group(groupId: string, traits?: object, options?: { userId? })`

Associate a user with a group and/or set group traits.

```javascript
// Associate user with a group and set group traits (most common)
client.group(
  "org_456",
  { name: "Acme Inc", plan: "enterprise" },
  { userId: "user_123" }
);

// Just link a user to a group (traits already set or not needed)
client.group("org_456", null, { userId: "user_123" });

// Just update group traits (e.g., from a webhook or background job)
client.group("org_456", { plan: "enterprise", employeeCount: 50 });
```

#### `flush(): Promise<void>`

Manually flush queued events immediately.

```javascript
await client.flush();
```

#### `shutdown(): Promise<void>`

Gracefully shutdown the client, flushing remaining events.

```javascript
await client.shutdown();
```

#### `getQueueSize(): number`

Return the number of events currently in the queue.

```javascript
const pending = client.getQueueSize();
console.log(`${pending} events waiting to be sent`);
```

### Synchronous Methods

For cases where you need confirmation that events were sent (e.g., before process exit, in tests), use the synchronous variants:

#### `trackSync(event, properties, options): Promise<BatchResponse>`

Track an event synchronously. Returns `BatchResponse` or throws `SendError`.

```javascript
const { SendError } = require("@klime/node");

try {
  const response = await client.trackSync(
    "Critical Action",
    { key: "value" },
    { userId: "user_123" }
  );
  console.log(`Sent! Accepted: ${response.accepted}`);
} catch (error) {
  if (error instanceof SendError) {
    console.error(`Failed to send: ${error.message}`);
  }
}
```

#### `identifySync(userId, traits): Promise<BatchResponse>`

Identify a user synchronously. Returns `BatchResponse` or throws `SendError`.

#### `groupSync(groupId, traits, options): Promise<BatchResponse>`

Associate a user with a group synchronously. Returns `BatchResponse` or throws `SendError`.

## Features

- **Automatic Batching**: Events are automatically batched and sent every 2 seconds or when the batch size reaches 20 events
- **Automatic Retries**: Failed requests are automatically retried with exponential backoff
- **Process Exit Handling**: Automatically flushes events on SIGTERM/SIGINT
- **Zero Dependencies**: Uses only Node.js standard library (fetch for Node 18+, https/http for older versions)
- **Universal Module Support**: Works with ESM and CommonJS out of the box

## Module Compatibility

This package ships dual ESM/CommonJS builds:

```javascript
// ESM (recommended for modern projects)
import { KlimeClient } from "@klime/node";

// CommonJS
const { KlimeClient } = require("@klime/node");
```

Works with all major Node.js frameworks including Express, Fastify, Hono, Next.js, and NestJS.

## Performance

When you call `track()`, `identify()`, or `group()`, the SDK:

1. Adds the event to an in-memory queue (microseconds)
2. Returns immediately without waiting for network I/O

Events are sent to Klime's servers asynchronously via Node.js's event loop. This means:

- **No network blocking**: HTTP requests happen asynchronously without blocking the event loop
- **No latency impact**: Tracking calls add < 1ms to your request handling time
- **Automatic batching**: Events are queued and sent in batches (default: every 2 seconds or 20 events)

```javascript
// This returns immediately - no HTTP request is made here
client.track("Button Clicked", { button: "signup" }, { userId: "user_123" });

// Your code continues without waiting
res.json({ success: true });
```

The only blocking operation is `await flush()`, which waits for all queued events to be sent. This is typically only called during graceful shutdown.

## Configuration

### Default Values

- `flushInterval`: 2000ms
- `maxBatchSize`: 20 events
- `maxQueueSize`: 1000 events
- `retryMaxAttempts`: 5 attempts
- `retryInitialDelay`: 1000ms
- `flushOnShutdown`: true

### Logging

The SDK uses a console wrapper with `[Klime]` prefix by default. You can provide a custom logger:

```javascript
const client = new KlimeClient({
  writeKey: "your-write-key",
  logger: {
    debug: (msg, ...args) => myLogger.debug(msg, ...args),
    info: (msg, ...args) => myLogger.info(msg, ...args),
    warn: (msg, ...args) => myLogger.warn(msg, ...args),
    error: (msg, ...args) => myLogger.error(msg, ...args),
  },
});
```

### Callbacks

```javascript
const client = new KlimeClient({
  writeKey: "your-write-key",
  onError: (error, events) => {
    // Report to your error tracking service
    Sentry.captureException(error);
    console.error(`Failed to send ${events.length} events: ${error.message}`);
  },
  onSuccess: (response) => {
    console.log(`Sent ${response.accepted} events`);
  },
});
```

## Error Handling

The SDK automatically handles:

- **Transient errors** (429, 503, network failures): Retries with exponential backoff
- **Permanent errors** (400, 401): Logs error and drops event
- **Rate limiting**: Respects `Retry-After` header

For synchronous operations, use `*Sync()` methods which throw `SendError` on failure:

```javascript
const { KlimeClient, SendError } = require("@klime/node");

try {
  const response = await client.trackSync("Event", {}, { userId: "user_123" });
} catch (error) {
  if (error instanceof SendError) {
    console.error(`Failed: ${error.message}, events: ${error.events.length}`);
  }
}
```

## Size Limits

- Maximum event size: 200KB
- Maximum batch size: 10MB
- Maximum events per batch: 100

Events exceeding these limits are rejected and logged.

## Express.js Example

```javascript
const express = require("express");
const { KlimeClient } = require("@klime/node");

const app = express();
const client = new KlimeClient({
  writeKey: process.env.KLIME_WRITE_KEY,
});

app.post("/api/button-clicked", (req, res) => {
  client.track(
    "Button Clicked",
    {
      buttonName: req.body.buttonName,
    },
    {
      userId: req.user.id,
    }
  );

  res.json({ success: true });
});

// Graceful shutdown
process.on("SIGTERM", async () => {
  await client.shutdown();
  process.exit(0);
});
```

## Requirements

- Node.js 14.0.0 or higher
- For Node.js 18+, native `fetch` is used
- For Node.js 14-17, `https`/`http` modules are used

## License

MIT