> ## 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.

# Link AI Traces to Journeys

> Put the journey id onto your AI traces with dataframer-journey, so they join your users' journeys

DataFramer puts an AI trace into a user's journey when the trace carries the same journey id as the user's events. The [browser SDK](/docs/user-signals/browser-events) creates that id and sends it to your backend with each request. This page is the second step: your backend writes the id onto every Langfuse trace or LangSmith run it creates. `dataframer-journey`, a small Python library, does that for you, so you never pass the id around by hand. Skip this page if your [linking rule](/docs/user-signals/fields-and-linking#linking-rule) links by a value your traces already carry, or by user and time gap.

## Install

```bash theme={null}
pip install -U dataframer-journey
```

It has no dependencies of its own. It hooks into `requests`, `httpx`, `langfuse` and `langsmith` when they are installed.

## Set up

**FastAPI / Starlette**

```python theme={null}
from dataframer_journey import instrument

app = FastAPI()
instrument(app)
```

**Django**

Add the middleware, above `django-cors-headers`' `CorsMiddleware` if you use it, then call `instrument()` once at process start (an `AppConfig.ready()` is a good place):

```python theme={null}
MIDDLEWARE = [
    'dataframer_journey.django.JourneyIdMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    ...,
]

import dataframer_journey
dataframer_journey.instrument()
```

From then on, three things happen on every request:

1. **Read in**: the journey id is taken from the request (the `X-Journey-Id` header or the SDK's cookie) and kept in a Python context variable for the rest of that request.
2. **Passed along**: outgoing `requests` or `httpx` calls to your own services carry the id forward, so a chain of services stays connected. By default only private addresses get it (localhost, private IPs, and internal names such as `orders-service`, `*.internal` or Kubernetes `*.svc` names); add your public services with `instrument(propagate_to=['https://agents.acme.com'])`. Third parties such as LLM vendors never receive it.
3. **Stamped on traces**: every Langfuse trace and every LangSmith run created during the request gets `metadata.journey_id`, plus a `journey:<id>` tag.

## Non-HTTP entry points

**Celery** needs nothing more: a task queued during a request carries the id, and its traces are stamped. Call `instrument()` in the worker process too; on Django, `AppConfig.ready()` already runs there.

Other queue consumers, scheduled jobs, raw `ThreadPoolExecutor` or `run_in_executor` work and WebSocket handlers do not pass through the middleware, so hand the id over from your payload:

```python theme={null}
from dataframer_journey import with_journey

with with_journey(payload["journey_id"]):
    ...  # traces created here get stamped; outbound calls carry the id
```

> [!WARNING]
>
> **Langfuse SDK 3.0 to 3.8 is not stamped.** Those releases have no way to attach attributes to every trace, so the library logs a warning and skips them; upgrade to 3.9.0 or newer. Langfuse v2, v4 and v5 and every LangSmith release are stamped. If you cannot upgrade, call `get_journey_id()` and stamp the trace yourself, as in [Without this library](#without-this-library).

## Use PostHog's session id

If PostHog runs in your frontend, its session id can serve as the journey id, with no DataFramer code in the browser. Set `tracing_headers: ['api.acme.com']` (your API's hostname) in `posthog.init`, so PostHog sends `X-POSTHOG-SESSION-ID` on requests to your API, then read that header:

```python theme={null}
instrument(app, journey_headers=['X-POSTHOG-SESSION-ID'])
```

Your traces then carry the PostHog session id. [Import your PostHog events](/docs/user-signals/warehouse-events), then add `properties.$session_id` as a path on events to the built-in **Journey id** [field](/docs/user-signals/fields-and-linking#fields). Your traces already carry it at `metadata.journey_id`, so the default linking rule links them. If your API is on another origin, its CORS settings must allow PostHog's three headers, `X-POSTHOG-SESSION-ID`, `X-POSTHOG-DISTINCT-ID` and `X-POSTHOG-WINDOW-ID`. PostHog adds them only once that part of its library has loaded, so the first requests on a page may carry none.

## Without this library

For a backend in another language, or a tracing setup this library does not stamp, write the id onto the trace yourself. DataFramer needs one thing: the journey id, stored under the key `journey_id` in:

* **Langfuse**: the trace's metadata, not only a span's
* **LangSmith**: the root run's metadata

The id is either your own, the one you pass to `startJourney()` in the browser, or the value of the request's `X-Journey-Id` header. The browser SDK sends that header only to the URLs in `propagateHeaderTo`, so set it even when your backend shares the page's origin, for example `propagateHeaderTo: ['/api']`. Work that leaves the request, such as a queue job or a call to another service, needs the id passed along with it.

## CORS

If your frontend and backend are on different origins, the browser SDK must send the id as a header; see [Getting the journey id to your own backend](/docs/user-signals/browser-events#getting-the-journey-id-to-your-own-backend). This library then allows that header in your CORS preflights by itself, for the origins your CORS settings already allow; it never allows a new origin.

## Next steps

> **[Send Events from Backend](/docs/user-signals/backend-events)**
>
> Send events that happen only on your server, such as a completed order.


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