---
url: https://simpledatalogs.com/docs/sdks/javascript.md
description: >-
  The simpledatalogs JavaScript SDK for browsers, extensions, React Native and
  Node; install, track events, log lines and exceptions, identity, flushing,
  offline behaviour, caps and every option.
---

# JavaScript

The `simpledatalogs` package sends events, log lines and exceptions from browsers, browser extensions, React Native and Node. It is one ES module of about 9 KB gzipped, with no dependencies and with TypeScript types, and it follows every [sender rule](/api/sender-rules) for you.

## Install

::: code-group

```sh [npm]
npm install simpledatalogs
```

```sh [pnpm]
pnpm add simpledatalogs
```

```sh [yarn]
yarn add simpledatalogs
```

:::

Node 18 or later, any modern browser, a Manifest V3 extension or React Native. The package is ESM only, and its types ship in the package.

::: info Early access
The package is published with the public launch. Until then, your invitation includes the package source.
:::

## Set up with one command

```sh
npx simpledatalogs init
```

It detects Next.js, Nuxt, SvelteKit, React or Vue on Vite, React Native/Expo, plain HTML, Node and Laravel, asks for the key (or takes `--key`), shows a diff of the few lines it adds, writes them only after you say yes, sends a `setup_test` event and prints the dashboard link.
Browser frameworks get the one-line autocapture; flags: `--framework`, `--endpoint`, `--app-url`, `--dashboard-url`, `--dir`, `--yes`, `--no-test`.

The onboarding page and the new-key dialog show this command with your key and `--dashboard-url` filled in, so the link it prints opens your environment's dashboard. The link is only printed; nothing is sent with it.

## Track your first event

```js
import { createClient } from 'simpledatalogs'

const sdl = createClient({ ingestKey: 'sdl_pub_…' })

sdl.track('signup', { plan: 'pro' })
```

`track()` only queues: it returns at once and never throws. Every event gets a UUID `event_id` and a `ts` the moment you call it, so retries never count twice.

Create the client **once** per key, for example in a module you import everywhere:

```js
// analytics.js
import { createClient } from 'simpledatalogs'

export const sdl = createClient({ ingestKey: import.meta.env.VITE_SIMPLEDATALOGS_KEY })
```

## Event options

```js
const sdl = createClient({ ingestKey: 'sdl_pub_…', defaultProperties: { app_version: '1.4.0' } })

sdl.track('purchase', { order_id: 7 }, { eventId: storedUuid, ts: purchasedAt })
```

* **Default properties** go into every event, log line and exception. `setDefaultProperties(props)` replaces them; the event's own props win on a clash.
* **`eventId`** is a UUID you keep, so re-sending the same event, after an app restart for example, is counted once. Anything that isn't a UUID gets a new id, with one warning.
* **`ts`** says when it happened: a `Date`, epoch milliseconds or ISO 8601 text from the last 10 years. Anything else becomes now, with one warning.
* `log(level, message, props, { eventId, ts })` takes the same options.

## Autocapture

One option sends page views, sessions and errors, with no `track()` calls. It is off until you turn it on.

```js
import { init } from 'simpledatalogs'

const sdl = init({ key: 'sdl_pub_…', autocapture: true })
```

`init({ key })` is the short form of `createClient({ ingestKey })`; both take every option below.

| Flag | With `autocapture: true` | What you get |
|---|---|---|
| `pageViews` | on | `page_view` on load and on every SPA route change that changes the path. Props: `path` (never the query), `title`; a session's first page view adds `referrer`, `referrer_host` and `utm_source` … `utm_content` |
| `sessions` | on | `session_start`, a `session_id` on every event, and `session_end` after 30 idle minutes (`duration_s`, `page_views`) |
| `errors` | on | Uncaught errors and unhandled promise rejections as `error` log lines with `stack`, `file`, `line` and `path` |
| `outboundLinks` | off | `outbound_click` (`url`, `host`) for links to other sites |
| `trackedClicks` | on | A click on an element with `data-track="signup"` sends that event, with its `data-track-*` attributes as props and `path` |
| `uncaughtExceptions` | on, Node only | A `critical` log line, then exit code 1 as Node would, after up to 2 s of flushing |
| `respectDoNotTrack` | on | Do Not Track or Global Privacy Control turns automatic capture off; your own calls still go (see [Do Not Track](#do-not-track)) |

Pick features with an object:

```js
init({ key: 'sdl_pub_…', autocapture: { errors: false, outboundLinks: true } })
```

* **No cookies.** The session id lives in the tab's `sessionStorage`, so each tab has its own session. A closed tab sends no `session_end`; its last event marks the end.
* **Bounded.** At most 5 copies of one error message and 50 errors per page load.
* SPA routes are caught by wrapping `history.pushState` and `history.replaceState`; `shutdown()` restores them.

## Active and background

In pages and React Native apps, every event tracked now says whether someone was using it, so the [Sessions tab](/guide/web-analytics) can tell active sessions from passive ones:

* `app_state` is `active` while the page is visible or the app is in front, else `background`.
* `engaged_ms` is the time in front since the previous event.
* Mark background work yourself with `track('sync_finished', { app_state: 'background' })`; your value wins.
* `activity: 'off'` sends neither. Back-dated events, log lines, Node and workers never carry them.

## Log lines

```js
sdl.log('error', 'Payment failed', { order_id: 7 })
sdl.log('info', 'Checkout opened')

try {
  await pay()
} catch (error) {
  sdl.log('error', error) // stored as "TypeError: card declined"
}
```

The level is one of `debug`, `info`, `notice`, `warning`, `error` or `critical`. An `Error` becomes its `Name: message`.

## Exceptions

```js
try {
  await pay()
} catch (error) {
  sdl.captureException(error, { props: { order_id: 7 } })
}

sdl.installGlobalHandlers() // uncaught errors and unhandled rejections
```

`captureException` sends an [exception object](/api/events) with `type`, `message`, `handled` and stack `frames`, so errors group into issues by type, message and the first frame of your own code.

| Option | Meaning |
|---|---|
| `props` | Props for this exception, on top of the default properties |
| `handled` | `true` by default; `false` marks a crash nobody caught, stored as `critical` |
| `fingerprint` | Your own grouping key, up to 128 characters |
| `eventId`, `ts` | As for `track()` |

* Stacks from Chrome, Node, Safari, Firefox and Hermes are read into at most 50 frames. Query strings are dropped from file names; `node_modules`, `node:` and native frames don't count as your code.
* The client's `release` option is sent with every exception.
* `installGlobalHandlers()` reports as `handled: false`, at most 5 copies of one message and 50 in all, and returns a function that removes the handlers.
  * **Page or worker:** listens for `error` and `unhandledrejection`; your own handlers keep working.
  * **React Native:** chains `ErrorUtils`. A fatal error waits up to 1 second for the flush, then goes on to the previous handler.
  * **Node:** listens for `uncaughtException`. When nothing else listens, it prints the error and exits with code 1 after up to 2 seconds, as Node would.
* Autocapture's `errors` flag sends browser errors as `error` log lines instead. Use one or the other.

## Identity

Set an anonymous device id to unlock active devices and per-device pages:

```js
sdl.setDeviceId(anonymousInstallId)
sdl.setUserId(accountId) // optional

sdl.setDeviceId(null) // clears it
```

Both apply to events tracked afterwards. You can also pass `deviceId` and `userId` to `createClient`. `sdl.setSessionId(id)` sets your own `session_id` the same way; a new one starts a new visit. Never use an email or a name; see [Privacy for senders](/reference/privacy).

## Props

Props must be strings, finite numbers or booleans:

```js
sdl.track('export_finished', { format: 'png', duration_ms: 412, is_pro: true })
```

Any other value (an object, `NaN`, `undefined`) is skipped, with one warning.

## In the browser

### Script tag

For a site without a bundler, one tag does everything. The tag is the opt-in, so page views, outbound clicks, `data-track` clicks and errors are captured:

```html
<script src="https://cdn.jsdelivr.net/npm/simpledatalogs@0.5.0/dist/simpledatalogs.min.js" data-key="sdl_pub_…" defer></script>
```

The tag runs in **web mode**: no cookies and no browser storage. Events wait in memory and go out when the page hides, and visitors and visits are counted on our side (see [Web analytics](/guide/web-analytics)).

| Attribute | Meaning |
|---|---|
| `data-key` | Your public key (required) |
| `data-endpoint` | Another ingest host, e.g. `http://localhost:4001` |
| `data-autocapture="false"` | Manual calls only |
| `data-outbound-links="false"` | Stop tracking clicks on links to other sites |
| `data-storage="true"` | Keep unsent events in `localStorage` and a tab session in `sessionStorage`, as the npm package does |

Your own calls use the global client: `simpledatalogs.track('signup')`, or no code at all:

```html
<button data-track="signup" data-track-plan="pro">Start free</button>
```

unpkg serves the same file at `https://unpkg.com/simpledatalogs@0.5.0/dist/simpledatalogs.min.js`.

::: info Early access
The CDN URLs work once the package is published to npm with the public launch.
:::

### With a bundler

```js
import { init } from 'simpledatalogs'

export const sdl = init({ key: import.meta.env.VITE_SIMPLEDATALOGS_KEY, autocapture: true })
```

* **Use a public key** (`sdl_pub_…`). The SDK warns if it sees a secret key in a web page.
* **Unsent events survive reloads.** The outbox lives in `localStorage`, so a closed tab sends its events on the next page load.
* **Page hide flushes.** On `pagehide` or a hidden tab, one `fetch` with `keepalive` sends up to 60 KB. Anything unconfirmed stays stored and is resent later.
* **Cross-origin works.** Ingest answers CORS preflights, and there are no cookies.

## In a browser extension

In a Manifest V3 service worker the SDK runs in worker mode, not as Node:

```js
// background.js
import { init } from 'simpledatalogs'

export const sdl = init({ key: 'sdl_pub_…' })
sdl.installGlobalHandlers()
```

* **The outbox survives the worker stopping.** With the `storage` permission in your manifest, it lives in `chrome.storage.local`; without it, in memory.
* **Sends every 10 seconds**, since Chrome stops an idle worker after 30 seconds. Events still waiting are sent when the worker next starts.
* **No `User-Agent` header**, and a warning if you ship a secret key.
* Popups, options pages and content scripts can share the storage with a prefix of their own:

```js
import { chromeStorageAdapter, init } from 'simpledatalogs'

const sdl = init({ key: 'sdl_pub_…', storage: chromeStorageAdapter(chrome.storage.local, 'popup:') })
```

## In React Native

The SDK detects React Native and skips browser autocapture. Pass `AsyncStorage` to keep unsent events across launches and `AppState` to flush on background and run sessions; see [React Native / Expo](/quickstarts/react-native).

## In Node

```js
import { createClient } from 'simpledatalogs'

const sdl = createClient({ ingestKey: process.env.SIMPLEDATALOGS_KEY }) // sdl_sec_…

sdl.track('job_finished', { queue: 'emails', duration_ms: 812 })

// Before the process exits:
process.on('SIGTERM', async () => {
  await sdl.shutdown()
  process.exit(0)
})
```

* **Use a secret key** (`sdl_sec_…`) on servers.
* **Call `await sdl.shutdown()`** before the process exits. In Node the outbox is kept in memory, so this is what sends the last batch.
* Batches go one at a time over one reused keep-alive connection.

## Flushing

Events go out every 10 seconds (every 5 while you develop: a local site, an unpacked extension, React Native's `__DEV__` or `NODE_ENV=development`), or at once when 50 are waiting. The first batch goes 1 second after the client starts. You rarely need to flush by hand, but you can:

```js
await sdl.flush()          // sends everything now; never rejects
console.log(sdl.pendingCount) // events not yet confirmed by a 202
```

`flush()` resolves when the attempt ends, whether or not it succeeded. Anything undelivered stays in the outbox.

## Opt-out

```js
sdl.setEnabled(false) // deletes every unsent event and ignores new ones
sdl.setEnabled(true)  // collects again
```

The SDK doesn't remember the choice. Your app's own setting is the source of truth, so apply it on every page load.

## Do Not Track

By default, Do Not Track and Global Privacy Control turn off **automatic capture only**. Your own `track()`, `log()` and `captureException()` calls still go.

To honour them for every send, pass `respectDoNotTrack: true`:

```js
const sdl = init({ key: 'sdl_pub_…', autocapture: true, respectDoNotTrack: true })
```

With the signal on, the client starts opted out, as if you called `setEnabled(false)`, so unsent events are deleted. Calling `setEnabled(true)` after your own consent prompt overrides it.

## Offline and errors

The SDK handles every [answer](/api/responses) for you:

| Answer | What the SDK does |
|---|---|
| `202` | Removes the batch from the outbox |
| `401` | Keeps everything, warns once, and re-checks the key after 1 minute, then every 10 minutes |
| `413` | Halves the batch and resends; each `202` doubles it back to 50. A single event over 256 KB is dropped |
| `415` | Drops that batch, with one warning |
| Anything else, a timeout, offline | Backs off with full jitter from 1 second to 5 minutes, never sooner than `Retry-After` |

Problems are logged once each with `console.warn`. No call ever throws into your app.

## Caps

Storage is small and shared with your page or app, so the outbox is bounded:

* at most **1,000 events or 2 MB** (`maxOutboxEvents`, `maxOutboxBytes`);
* only when that fills, for example after hours with a revoked key, are the oldest unsent events dropped, with a warning;
* a stored outbox older than 30 days is deleted, whatever key wrote it.

`track()` never writes storage itself. The outbox is saved once per turn of the event loop.

## Options

```js
const sdl = createClient({
  ingestKey: 'sdl_pub_…',                     // required
  endpoint: 'https://edge.simpledatalogs.com',  // the ingest host
  deviceId: null,
  userId: null,
  flushIntervalMs: 10_000,
  firstFlushDelayMs: 1_000,
  maxBatchEvents: 50,
  maxRequestBytes: 256_000,
  maxKeepaliveBytes: 60_000,
  maxOutboxEvents: 1_000,
  maxOutboxBytes: 2_000_000,
  requestTimeoutMs: 15_000,
  autocapture: false,
  defaultProperties: {},
  release: null,
  respectDoNotTrack: false,
})
```

| Option | Default | Meaning |
|---|---|---|
| `ingestKey` | — | Your `sdl_pub_…` or `sdl_sec_…` key. Without one, nothing is sent |
| `endpoint` | `https://edge.simpledatalogs.com` | The ingest host; `/v1/ingest` is added |
| `deviceId`, `userId` | `null` | Identity on every event |
| `defaultProperties` | `{}` | Props merged into every event, log line and exception |
| `release` | `null` | Your app's release, sent with exceptions |
| `flushIntervalMs` | `10000` (`5000` while you develop) | How long a partial batch waits |
| `firstFlushDelayMs` | `1000` | Wait before the first batch, so a new setup shows data at once |
| `maxBatchEvents` | `50` | Events per request |
| `maxRequestBytes` | `256000` | Bytes per request |
| `maxKeepaliveBytes` | `60000` | Bytes in the page-hide request |
| `maxOutboxEvents`, `maxOutboxBytes` | `1000`, `2000000` | The outbox caps |
| `requestTimeoutMs` | `15000` | One request's timeout |
| `autocapture` | `false` | `true`, or an object of flags; see [Autocapture](#autocapture) |
| `respectDoNotTrack` | `false` | Makes Do Not Track and Global Privacy Control an opt-out for every send |
| `storage` | `localStorage` in browsers, `chrome.storage.local` in an extension worker, none elsewhere | `localStorage`, or any object whose `getItem`, `setItem` and `removeItem` may return Promises (AsyncStorage); `null` keeps the outbox in memory |
| `appState` | — | React Native's `AppState`: flushes on background and drives sessions |
| `runtime` | detected | `browser`, `worker`, `react-native` or `node`; `sdl.runtime` says which is in use |
| `logger` | `console` | Where warnings go; `null` silences them |
| `sendUserAgent` | `true` in Node only | Whether to send `simpledatalogs-js/<version>` as the User-Agent |

## Next steps

* [Events and log objects](/api/events): what the SDK sends.
* [Privacy for senders](/reference/privacy): what to keep out of props.
* [Prompt recipes](/agents/recipes): let your coding agent add tracking for you.
