npm package: @pulsealarm/pulse-event-sdk
Description: Non-blocking event/error SDK for browser and Node.js
Latest version: 0.1.10

# Pulse Event SDK (JavaScript)

Framework-agnostic, non-blocking event and error tracking SDK for:
- Browser apps
- Node.js services
- React / Vue
- Express / Fastify
- Next.js / Nuxt

## Features

- Non-blocking event delivery (queue + background flush)
- Token-based authentication (`/event/:token`)
- Server-to-server secret key support (`X-Pulse-Secret-Key` header)
- Manual custom event tracking
- Built-in page view tracking (`pageView`)
- Manual exception capture (`captureException`, `captureError`)
- Global runtime error handlers (browser + Node.js)
- Automatic `session_id` generation
- Batched sending with configurable batch size
- Queue overflow protection with callback
- Retry with configurable max retries
- Lightweight adapters for modern frameworks
- TypeScript support
- Fully tested

## Installation

```bash
npm install @pulsealarm/pulse-event-sdk
```

## Browser (No Bundler / CDN)

If you are not using a bundler (Vite, webpack, etc.), load the SDK directly from a CDN with a plain `<script>` tag. No build step or `import` required.

```html
<!-- jsDelivr (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/@pulsealarm/pulse-event-sdk/dist/pulse.min.js"></script>

<!-- or unpkg -->
<script src="https://unpkg.com/@pulsealarm/pulse-event-sdk/dist/pulse.min.js"></script>

<script>
  const pulse = new Pulse.PulseClient({
    token: "your_webhook_token_here",
    autoPageView: true,
    defaults: {
      environment: "production",
      platform: "web"
    }
  });

  pulse.installGlobalHandlers();

  // Manual page view
  pulse.pageView();

  // Custom event
  pulse.track({
    title: "Payment failed",
    level: "error",
    type: "exception",
    customer_id: "cust_456"
  });
</script>
```

The SDK is exposed as `window.Pulse`. All exports are accessible under the `Pulse` namespace:

| Export | Usage |
|--------|-------|
| `Pulse.PulseClient` | `new Pulse.PulseClient({ token })` |
| `Pulse.createPulseClient` | `Pulse.createPulseClient({ token })` |

> **Tip:** For bundler-based projects (Vite, webpack, Next.js, etc.), use `npm install` above — tree-shaking and TypeScript types are included automatically.

## Quick Start

### Browser

```js
import { createPulseClient } from "@pulsealarm/pulse-event-sdk";

const pulse = createPulseClient({
  token: "your_webhook_token_here",
  autoPageView: true,
  defaults: {
    environment: "production",
    app_version: "2.3.1",
    platform: "web"
  }
});

pulse.installGlobalHandlers();

pulse.track({
  title: "Payment failed for order #1234",
  description: "Stripe returned card_declined error during checkout",
  level: "error",
  type: "exception",
  data: {
    stack_trace: "Error: card_declined\n  at processPayment (checkout.js:42)",
    any_key: "any_value"
  },
  session_id: "sess_abc123",
  customer_id: "cust_456",
  ip_address: "203.0.113.10",
  tags: ["payment", "critical-path"],
  revenue: { currency: "USD", price: 49.99 }
});
```

### Node.js

```js
import { createPulseClient } from "@pulsealarm/pulse-event-sdk";

const pulse = createPulseClient({
  token: "your_webhook_token_here",
  secretKey: "sk_your_secret_key",   // required for server-to-server
  defaults: {
    platform: "server",
    environment: "production"
  }
});

pulse.installGlobalHandlers();
pulse.event("worker.started");

process.on("SIGTERM", () => {
  pulse.close();
  process.exit(0);
});
```

## Authentication

### Browser (Client-side)
Only `token` is needed. Domain validation is handled on the server.

```js
const pulse = createPulseClient({
  token: "your_webhook_token_here"
});
```

### Server-to-Server
Requires `secretKey` in addition to `token`. The SDK sends it as `X-Pulse-Secret-Key` header.

```js
const pulse = createPulseClient({
  token: "your_webhook_token_here",
  secretKey: "sk_your_secret_key"
});
```

## Endpoints

The SDK automatically builds endpoint URLs from the token:

| Type | URL Pattern |
|------|------------|
| Event | `https://pulsealarm.dev/event/:token` |
| Page View | `https://pulsealarm.dev/page-view/:token` |

Custom base URL:

```js
const pulse = createPulseClient({
  token: "your_token",
  baseUrl: "https://your-self-hosted-domain.com"
});
// → https://your-self-hosted-domain.com/event/your_token
// → https://your-self-hosted-domain.com/page-view/your_token
```

You can also pass a fully custom endpoint:

```js
const pulse = createPulseClient({
  endpoint: "https://custom-domain.com/my-ingest"
});
```

## Runtime Token Updates

```js
// Update token (and optionally secretKey) at runtime
pulse.setToken("new_token", "new_secret_key");

// Update just the endpoint URL
pulse.setEndpoint("https://custom-domain.com/event/new_token");
```

## Session Tracking

- If `session_id` is not provided, SDK auto-generates `sess_*`.
- In browsers, session ID is persisted in both cookie (`pulse_sdk_session_id`) and `sessionStorage`.
- Priority: explicit `session_id` > cookie > `sessionStorage` > generated value.

```js
console.log(pulse.getSessionId()); // "sess_a1b2c3d4..."
```

## Custom Event Tracking

```js
// Simple event
pulse.event("checkout.step.completed", {
  level: "info",
  tags: ["checkout"]
});

// Full track with all fields
pulse.track({
  title: "Order placed",
  level: "info",
  type: "event",
  customer_id: "cust_456",
  revenue: { currency: "USD", price: 99.99 },
  tags: ["orders"]
});
```

In browser runtime, `track()` auto-fills these fields when missing:
- `request_url`
- `duration`
- `data.referrer`
- `data.user_agent`

If you provide them explicitly, your values override SDK defaults.

## Exception Capture

```js
try {
  doPayment();
} catch (error) {
  pulse.captureException(error, {
    tags: ["payment"],
    customer_id: "cust_456",
    data: { order_id: "ord_123" }
  });
}
```

`captureException` automatically extracts:
- `title` from error message
- `stack_trace` into `data.stack_trace`
- `platform` as `"web"` (browser) or `"server"` (Node.js)
- `type` as `"exception"`, `level` as `"error"`

## Page View Tracking

`pageView()` sends data to the dedicated `/page-view/:token` endpoint.

Auto-filled fields (when available in browser):
- `url` — current page URL
- `duration` — page load time (Navigation Timing API)
- `session_id` — from SDK session
- `referrer` — document referrer
- `user_agent` — navigator user agent
- `timestamp` — ISO-8601

```js
// Automatic (uses browser APIs)
pulse.pageView();

// With overrides
pulse.pageView({
  url: "https://myapp.com/checkout",
  duration: 1200,
  customer_id: "cust_456",
  ip_address: "203.0.113.10"
});
```

Auto page view on load:

```js
const pulse = createPulseClient({
  token: "your_token",
  autoPageView: true
});
```

## Global Error Handlers

```js
pulse.installGlobalHandlers();
```

- **Browser:** `error` and `unhandledrejection` events
- **Node.js:** `uncaughtException` and `unhandledRejection` events

Selective:

```js
pulse.installGlobalHandlers({
  browser: true,
  node: false,
  uncaughtException: true,
  unhandledRejection: true
});
```

## Queue & Stats

```js
const stats = pulse.getStats();
// { sent: 42, failed: 1, dropped: 0, queued: 3 }
```

Queue overflow callback:

```js
const pulse = createPulseClient({
  token: "your_token",
  maxQueueSize: 500,
  onQueueOverflow: (totalDropped) => {
    console.warn(`${totalDropped} events dropped due to queue overflow`);
  }
});
```

## Configuration Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `token` | `string` | — | Webhook token (used in URL: `/event/:token`) |
| `secretKey` | `string` | `null` | Server secret key (sent as `X-Pulse-Secret-Key` header) |
| `endpoint` | `string` | — | Custom endpoint URL (overrides token) |
| `baseUrl` | `string` | `https://pulsealarm.dev` | Base URL for endpoint building |
| `defaults` | `object` | `{}` | Default fields merged into every event |
| `flushIntervalMs` | `number` | `1000` | Background flush interval |
| `maxQueueSize` | `number` | `500` | Max queue size before dropping oldest |
| `maxRetries` | `number` | `2` | Retry count for failed sends |
| `batchSize` | `number` | `10` | Events sent per flush cycle |
| `timeoutMs` | `number` | `10000` | HTTP request timeout |
| `autoPageView` | `boolean` | `false` | Auto-send page view on load |
| `enableBrowserContext` | `boolean` | `false` | Add browser context to events |
| `onError` | `function` | — | Error callback `(error, context) => {}` |
| `onQueueOverflow` | `function` | — | Queue overflow callback `(totalDropped) => {}` |
| `debug` | `boolean` | `false` | Enable debug logging |

## React Integration

```jsx
import { usePulse, PulseErrorBoundary, withPulseErrorBoundary } from "@pulsealarm/pulse-event-sdk/react";

// Hook
const pulse = usePulse({ token: "your_token" });

// Error Boundary
<PulseErrorBoundary pulse={pulse} fallback={<div>Something went wrong</div>}>
  <App />
</PulseErrorBoundary>

// HOC
const SafeCheckout = withPulseErrorBoundary(Checkout, { pulse });
```

## Vue Integration

```js
import { createPulseVuePlugin } from "@pulsealarm/pulse-event-sdk/vue";

app.use(
  createPulseVuePlugin({
    token: "your_token",
    defaults: { platform: "web", environment: "production" }
  })
);
```

The plugin also configures `app.config.errorHandler` automatically.

## Express Integration

```js
import { createPulseClient } from "@pulsealarm/pulse-event-sdk";
import {
  createExpressPulseRequestMiddleware,
  createExpressPulseErrorMiddleware
} from "@pulsealarm/pulse-event-sdk/express";

const pulse = createPulseClient({
  token: "your_token",
  secretKey: "sk_your_secret_key"
});

app.use(createExpressPulseRequestMiddleware(pulse));
// ... your routes
app.use(createExpressPulseErrorMiddleware(pulse));
```

## Fastify Integration

```js
import { createFastifyPulsePlugin } from "@pulsealarm/pulse-event-sdk/fastify";

await fastify.register(createFastifyPulsePlugin(pulse));
```

## Next.js Integration

Pages Router API handler:

```js
import { withPulseApiHandler } from "@pulsealarm/pulse-event-sdk/next";

export default withPulseApiHandler(async function handler(req, res) {
  // your handler
}, pulse);
```

App Router route handler:

```js
import { withPulseRouteHandler } from "@pulsealarm/pulse-event-sdk/next";

export const POST = withPulseRouteHandler(async function POST(request) {
  // your handler
}, pulse);
```

## Nuxt Integration

```js
import { createNuxtPulsePlugin } from "@pulsealarm/pulse-event-sdk/nuxt";

export default defineNuxtPlugin((nuxtApp) => {
  createNuxtPulsePlugin(pulse)(nuxtApp);
});
```

## Event Payload Schema

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | `string` | Yes | Event title |
| `description` | `string` | No | Detailed description |
| `level` | `string` | No | Dynamic value from API (must be non-empty) |
| `type` | `string` | No | Dynamic value from API (must be non-empty) |
| `timestamp` | `string` | No | ISO-8601 timestamp |
| `data` | `object` | No | Custom key-value data |
| `session_id` | `string` | No | Session identifier |
| `customer_id` | `string` | No | Customer identifier |
| `ip_address` | `string` | No | Client IP address |
| `request_url` | `string` | No | Request URL |
| `duration` | `number` | No | Duration in milliseconds |
| `country_code` | `string` | No | ISO-2 country code (e.g. `TR`) |
| `tags` | `string[]` | No | Tags for categorization |
| `revenue` | `object` | No | `{ currency: "USD", price: 49.99 }` |
| `group` | `boolean` | No | Enable deduplication grouping |
| `resolved` | `boolean` | No | Mark as resolved |
| `environment` | `string` | No | `production`, `staging`, `development`, `test` |
| `app_version` | `string` | No | Application version |
| `platform` | `string` | No | `web`, `ios`, `android`, `server`, `desktop` |

## Page View Payload Schema

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `url` | `string` | Yes | Page URL |
| `duration` | `number` | No | Load time in milliseconds |
| `session_id` | `string` | No | Session identifier |
| `customer_id` | `string` | No | Customer identifier |
| `timestamp` | `string` | No | ISO-8601 timestamp |
| `ip_address` | `string` | No | Client IP address |
| `referrer` | `string` | No | Referring URL |
| `user_agent` | `string` | No | Browser user agent |

## Testing

```bash
npm test
```

## CLI Quick Test

```bash
TOKEN="your_webhook_token_here"
BASE_URL="https://pulsealarm.dev"

curl -X POST "${BASE_URL}/event/${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"title":"CLI test event","level":"info"}'
```

With secret key (server-to-server):

```bash
curl -X POST "${BASE_URL}/event/${TOKEN}" \
  -H "Content-Type: application/json" \
  -H "X-Pulse-Secret-Key: sk_your_secret_key" \
  -d '{"title":"Server test event","level":"info"}'
```

## Notes

- `track()` is non-blocking: events go to queue, then flush in background.
- Events are sent in batches (default: 10 per flush cycle).
- Browser attempts best-effort flush via `sendBeacon` on page exit/hidden state.
- In Node.js, call `pulse.close()` before shutdown for clean flush.
- Failed sends are retried up to `maxRetries` times before being dropped.