> ## Documentation Index
> Fetch the complete documentation index at: https://www.dataframer.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Send Events from Browser

> Send user events from your frontend with the @dataframer/signals browser SDK, which also creates the journey id

`@dataframer/signals` is a small browser SDK. It sends user and product events to DataFramer, and it creates the journey id that ties those events to your AI traces.

## Install

```bash theme={null}
npm install @dataframer/signals
```

## Get a write key

In DataFramer, go to [**Settings → Events**](https://app.dataframer.ai/settings?tab=events) and copy the write key under **Events from your app**. This key can only send events in, never read data out, so it is safe to ship in frontend code.

![The Events from your app panel on the Events tab of Settings, with the write key and its Copy and Rotate buttons](https://mintcdn.com/aimonlabsinc/qPJ_ItzqzBilymj7/images/user-signals/write-key-card.png?fit=max\&auto=format\&n=qPJ_ItzqzBilymj7\&q=85\&s=0a455cef9e0ea67556bf5ed94f392dc4)

## Initialize once, at app start

```ts theme={null}
import { DataframerSignals } from '@dataframer/signals';

const signals = new DataframerSignals({
  writeKey: 'YOUR_WRITE_KEY',
});
```

## Send events

```ts theme={null}
signals.identify('user@acme.com', { plan: 'pro' });   // sets the journey's user
signals.track('RecommendationCancelled', { itemId: 'abc', reason: 'irrelevant' });
signals.group('acme');                                // the user's account or org
signals.reset();                                      // on logout: forget the user and account
```

Sending is best-effort and never throws an error into your app. If DataFramer rejects an event (wrong write key, invalid payload, rate limit), the SDK logs a `console.warn` with the reason, and the rejected event shows up with that reason under **User Journeys → Live Events**. During setup, that page shows whether events were rejected or never sent.

## Journey ids

Every event carries a `journey_id`. There are two ways to get one:

* **Automatic (default):** the SDK creates an id for you. It resets after 30 minutes without input from the user, such as a click, a key press or scrolling (`journeyIdleMinutes`). Requests your app sends by itself, such as polling, do not count.
* **Your own id:** if you already have a natural id, such as an order id, conversation id or ticket id, you can use that:

```ts theme={null}
signals.startJourney('order-1234');   // pin your own id
signals.resetJourney();               // end it, start a fresh one
signals.journeyId;                    // read the current id
```

If your backend already receives that id, as a chat backend receives the conversation id with each message, this is the simplest setup: stamp the same id onto your traces yourself, as in [Without this library](/docs/user-signals/link-traces#without-this-library), and skip the header, CORS and `dataframer-journey` setup below. The SDK keeps one id per browser, shared by all tabs, so:

* Call `startJourney()` again each time the user switches to another conversation, order or ticket.
* Call `resetJourney()` when the user leaves it. Your own id never resets by itself, so later events would land on it.
* If users keep two open at once in two tabs, both tabs get the id set last. Use the automatic id instead.

## Getting the journey id to your own backend

Your backend needs the journey id so it can stamp it onto your AI traces (see [Link AI Traces to Journeys](/docs/user-signals/link-traces)). How it gets there depends on whether your frontend and your backend share an origin.

**Same origin: nothing to do**

If your frontend calls your backend on the origin it was served from (same scheme, host and port, for example a page on `https://app.acme.com` calling `https://app.acme.com/api`), the browser sends the SDK's journey cookie with every request, and `dataframer-journey` reads it. **Nothing to configure.** If you stamp traces without that library, see [Without this library](/docs/user-signals/link-traces#without-this-library).

**Different origins: two steps**

If your backend is on another host, even a sibling subdomain such as `api.acme.com`, the browser does not send the cookie. Do two things:

1. **Tell the SDK to forward the id as a header** on requests to your backend:

```ts theme={null}
const signals = new DataframerSignals({
  writeKey: 'YOUR_WRITE_KEY',
  propagateHeaderTo: ['https://api.acme.com'],   // your own backend, not DataFramer
});
```

The SDK patches `fetch` and `XMLHttpRequest`, so every request to that URL gets an `X-Journey-Id` header.

2. **Let your backend's CORS allow that header.** On a Python backend with `dataframer-journey`, this is done for you, for the origins your CORS settings already allow. On any other backend, or when CORS is answered before your app (a proxy, an API gateway or a CDN), add `X-Journey-Id` to the allowed headers, next to headers such as `Authorization`.

Without this, the browser blocks the request in its CORS preflight, before it reaches your server, and the SDK logs a console warning that names the header.

## Content Security Policy (CSP)

If your site sets a `Content-Security-Policy`, allow your DataFramer API host in `connect-src`, or the browser blocks the SDK's requests:

```
connect-src 'self' https://df-api.dataframer.ai;
```

A blocked request produces no SDK error. It only shows as a CSP violation in the browser console (with `debug: true` the SDK also warns). If events do not arrive, check this first. In a customer-hosted deployment, allow your data plane's host instead.

## Config reference

| Option | Default | What it does |
| - | - | - |
| `apiBase` | `https://df-api.dataframer.ai` | Your DataFramer API base URL; change it only if you do not use hosted DataFramer. In a [customer-hosted deployment](/docs/deployment#user-signals-in-a-customer-hosted-deployment), it is your data plane's address, as in the setup snippet under **User Journeys → Live Events** |
| `writeKey` | none | Your Signals SDK write key |
| `journeyIdleMinutes` | `30` | Minutes without user input before an automatic journey id resets |
| `disableAutoRotation` | `false` | Never reset the automatic journey id |
| `propagateHeaderTo` | `[]` | Your backend URLs, or paths on your own origin such as `/api`, that get the `X-Journey-Id` header |
| `debug` | `false` | Log every delivery to the console, including network failures |

## Next steps

> **[Link AI Traces to Journeys](/docs/user-signals/link-traces)**
>
> Stamp this same journey id onto your AI traces automatically.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.