> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# Observe

You can verify a flow at three levels, each one step closer to production:

1. **Simulate**: run an event through the built flow offline, with destinations mocked, see [bundled mode](/preview/pr-749/docs/getting-started/modes/bundled.md#see-your-event).
2. **Run locally**: run the flow where your app runs and watch each event in your console, see [integrated mode](/preview/pr-749/docs/getting-started/modes/integrated.md#see-your-event).
3. **Observe**: attach a running flow to an Observe session and watch its live records arrive in a feed, from any browser or server deployment.

This page covers the third rung: connecting a flow to an observer.

## What you commit vs what arrives later[​](#what-you-commit-vs-what-arrives-later "Direct link to What you commit vs what arrives later")

Your code and config carry only public connect values: the observer `url` and the project `binding`. Neither is a secret, both are safe to commit.

The per-session credential arrives out-of-band: as a URL parameter in the browser, or as environment variables on the server. **No credentials ever live in committed artifacts**, not in `flow.json`, not in a built bundle, not in your application code.

## Web, integrated mode[​](#web-integrated-mode "Direct link to Web, integrated mode")

Add the public connect pair to `startFlow`:

```typescript
import { startFlow } from '@walkeros/collector';
import { sourceBrowser } from '@walkeros/web-source-browser';

const { elb } = await startFlow({
  sources: {
    browser: { code: sourceBrowser, config: { settings: { pageview: true } } },
  },
  observe: {
    url: 'https://observer.example.com', // public, safe to commit
    binding: 'pb_x', // public project binding, safe to commit
  },
});
```

Then open your dev URL with a session credential appended:

```text
http://localhost:3000/?elbObserve=obsw_pb_x.ses_9.sec_1
```

On load, the runtime checks that the credential's binding matches the one in your config, persists the credential in `localStorage`, removes the parameter from the address bar, and starts posting flow records to the observer. Later page loads reuse the stored credential, so one activation lasts until the Observe session ends.

Without a credential, the same page does zero observation work: nothing is parsed, stored, or sent. When the Observe session ends, the observer rejects the next post and the runtime detaches and clears the stored credential on its own.

## Web, bundled mode[​](#web-bundled-mode "Direct link to Web, bundled mode")

The same connect pair goes into `flow.json` under `config.observe`:

```json
{
  "version": 4,
  "flows": {
    "default": {
      "config": {
        "platform": "web",
        "observe": {
          "url": "https://observer.example.com",
          "binding": "pb_x"
        },
        "bundle": {
          "packages": {
            "@walkeros/collector": { "version": "^4" },
            "@walkeros/web-source-browser": { "version": "^4" }
          }
        }
      },
      "sources": {
        "browser": {
          "package": "@walkeros/web-source-browser",
          "config": { "settings": { "pageview": true } }
        }
      },
      "collector": { "run": true }
    }
  }
}
```

Build once:

```bash
walkeros bundle flow.json -o ./dist/walker.js
```

The bundle carries only the public pair. `url` and `binding` must both be set: a partial pair warns at build time and wires nothing. Activation works exactly as in integrated mode, via the `?elbObserve=` parameter on any page that loads the bundle.

## Server[​](#server "Direct link to Server")

Server deployments read their connect config from the environment:

```typescript
import { startFlow } from '@walkeros/collector';
import { observeFromEnv } from '@walkeros/core';

const { elb } = await startFlow({
  // undefined unless all three variables are set; undefined means zero observation work
  observe: observeFromEnv(process.env),
  // ...your sources and destinations
});
```

Set the environment on the deployment, never in code or config files:

```bash
WALKEROS_OBSERVER_URL=https://observer.example.com
WALKEROS_DEPLOYMENT_ID=ses_1a2b3c
WALKEROS_INGEST_TOKEN=... # secret, inject via deployment env only
WALKEROS_OBSERVE_LEVEL=standard # optional: off | standard | trace
```

`observeFromEnv` returns a config only when the full trio (`URL`, `DEPLOYMENT_ID`, `INGEST_TOKEN`) is present; otherwise it returns `undefined` and the deployment does no observation work. A set but unrecognized `WALKEROS_OBSERVE_LEVEL` (a typo like `offf`) also disables observation entirely rather than silently observing at `standard`. The ingest token is the one secret in this setup, which is why it lives in the deployment environment and nowhere else.

## Connect values vs telemetry knobs[​](#connect-values-vs-telemetry-knobs "Direct link to Connect values vs telemetry knobs")

The `observe` block carries two kinds of fields, and they answer different questions:

| Fields            | Question                                                                | Who sets them                         |
| ----------------- | ----------------------------------------------------------------------- | ------------------------------------- |
| `url`, `binding`  | Where to attach, and which project may attach                           | You, committed once                   |
| `level`, `sample` | How verbose managed telemetry is, and what fraction of records it emits | The managing platform, per deployment |

The web connect pair is `url` and `binding`, nothing else. `level` (`off` | `standard` | `trace`) and `sample` (a fraction from 0 to 1) are managed-telemetry knobs: leave them unset in your own config and let the managing platform decide what gets captured. It sets them per runtime, not per session: an Observe session's `--level` steers the container arm it provisions, while the web arm captures at the level baked into the bundle it loads. On the server, `WALKEROS_OBSERVE_LEVEL` is the same knob in environment form.

## Bring your own observer[​](#bring-your-own-observer "Direct link to Bring your own observer")

Observation is an open channel, not a hosted-only feature. Any function can receive the same flow records via `observers`:

```typescript
const { elb } = await startFlow({
  observers: [
    (state) => console.log('flow state', state.stepId, state.phase),
  ],
});
```

Observers are advisory by contract: they run inside the collector's emit loop, and a throwing observer never breaks event processing. Use this to stream records to your own endpoint, a log pipeline, or a test assertion.

## Observability and runtime tracing[​](#observability-and-runtime-tracing "Direct link to Observability and runtime tracing")

A flow's baseline observability is set by the `flow.<name>.config.observe` block:

| Level                | What the runtime emits                                                                                                                                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `off`                | No baseline records are emitted. In a managed deployment the observer and trace poll stay wired, so runtime trace can still elevate to `trace`; a self-hosted bundle built with no telemetry option makes no fetches at all. |
| `standard` (default) | Structural FlowState records, without inbound or outbound event payloads.                                                                                                                                                    |
| `trace`              | Full payloads on every hop (inbound and outbound), sample rate `1`.                                                                                                                                                          |

When `observe` is omitted, managed deployments default to `standard`.

### Activating trace at runtime (no redeploy)[​](#activating-trace-at-runtime-no-redeploy "Direct link to Activating trace at runtime (no redeploy)")

Trace can be turned on and off for a running deployment without rebuilding or redeploying. The mechanism is a poll, not the heartbeat:

1. The deployment carries a trace window timestamp (`traceUntil`).
2. The observer exposes `GET /trace/:deploymentId` (authenticated with the ingest token), returning `{ "traceUntil": "<ISO timestamp>" | null }`.
3. Both the server runtime (`runneros start`) and the deployed browser bundle poll that endpoint about every 15 seconds and feed the returned value into `resolveTelemetryOptions({ traceUntil })` in `@walkeros/core`.

The resolver applies the value per emit:

* A `traceUntil` that parses to a **future** ISO timestamp forces `level: trace` with full inbound and outbound payloads and `sample: 1`, overriding the flow's `observe` block.
* A `null` or **past** value reverts to the flow's `observe` baseline.
* The window self-expires once the timestamp passes, so trace stops on its own with no further action.

Only a `200` with a parseable body changes the active trace state. A network error, a non-200, or an unparseable response leaves the current window untouched, so a transient observer outage never silently disables (or enables) tracing.

### Journey correlation across flows[​](#journey-correlation-across-flows "Direct link to Journey correlation across flows")

Every FlowState record carries correlation fields so a single observer can stitch one request across web and server flows. These fields survive every level, including `standard`; they are never gated behind `trace`.

| Field           | Meaning                                                                                                                                                                                  |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `traceId`       | W3C 32-hex trace id of the originating run. Resolved as `event.source.trace`, falling back to an inbound header's trace, then the run-scoped collector trace. Never synthesized per hop. |
| `eventId`       | W3C span id of the walker event at this hop (`event.id`).                                                                                                                                |
| `parentEventId` | The upstream runtime's `event.id`, present when this flow was entered across a `$flow` crossing (from an inbound `traceparent`).                                                         |
| `sourceId`      | The originating source id.                                                                                                                                                               |
| `seq`           | Per-poster monotonic counter, stamped on transport (see [Loss-visible transport](#loss-visible-transport)).                                                                              |

Correlation crosses the wire via the W3C `traceparent` header:

* **Emit.** The web `api` destination adds `traceparent: 00-<traceId>-<eventId>-01` to each non-batched send when the event carries both ids. A `traceparent` you configure yourself always wins (matched case-insensitively). Batched sends carry no `traceparent`, since one batch may aggregate distinct upstream traces.
* **Adopt.** Any server source with a request header bag (for example an Express or fetch handler) reads a valid inbound `traceparent` and threads its trace and parent span into the ingest context, so the receiving flow's records share the caller's `traceId` and link back via `parentEventId`. An absent or malformed header is ignored.

### Journey status[​](#journey-status "Direct link to Journey status")

The records of one event assemble into a single journey, identified by that event's id. A `traceId` is run-scoped rather than per event, so it is carried as a shared handle that links every journey of one page load or container run. Each journey carries a status derived from the hops observed so far:

| Status     | Meaning                                                                                                  |
| ---------- | -------------------------------------------------------------------------------------------------------- |
| `pending`  | The journey is still settling: a record arrived recently, or an expected hop has no terminal record yet. |
| `complete` | Every expected hop reached a terminal state (`out`, `error`, or `skip`).                                 |
| `partial`  | The journey has settled, but an expected hop never produced a terminal record.                           |

"Expected" comes from the flow's step topology when one is known (only steps downstream of a processed hop are expected); without a topology, a journey is `complete` once every observed hop is terminal. A separate `lossy` flag is orthogonal to status: a journey is `lossy` when a `seq` gap (see [Loss-visible transport](#loss-visible-transport)) overlaps its window, so a journey can be `complete` and `lossy`, or `partial` and `lossy`, at once.

### Vendor-call capture[​](#vendor-call-capture "Direct link to Vendor-call capture")

At `trace` level a destination can record the outgoing vendor calls it makes, so an observer sees not just the mapped event but the exact call that left for the vendor. A destination opts in by declaring its observable callables as dot-paths (the `gtag` destination declares `call:window.gtag`); the runtime then wraps those callables and records each `{ fn, args, ts }` onto the record's outbound payload, with arguments sanitized to JSON-safe markers.

Capture works whenever the destination reaches its callable through the env. Server and simulate runs carry the callable in the env directly. On live web a destination resolves a real global (for example `window.gtag`) through `@walkeros/web-core`'s `getEnv`; the runtime wraps the declared callables at that resolution point, so calls are recorded without the destination or the page changing. The batched delivery path is not captured: batched sends flush against the unwrapped env, so a batching destination shows its per-event input and output but not its vendor calls. Recorded calls are payload-class: they appear only when the outbound payload does (`trace`, or an explicit `includeOut`).

### Loss-visible transport[​](#loss-visible-transport "Direct link to Loss-visible transport")

The batched poster stamps each record with a monotonic `seq` before sending. A dropped or rejected POST is never re-sequenced, so a gap in `seq` at the observer makes lost telemetry visible instead of silent. The poster also splits a batch by real UTF-8 byte size (`maxBodyBytes`, default 60000) rather than letting an oversized body fail with a 413.

## Starting an Observe session[​](#starting-an-observe-session "Direct link to Starting an Observe session")

## Next steps[​](#next-steps "Direct link to Next steps")

* [Deploy](/preview/pr-749/docs/getting-started/deploy.md): take the flow to production first
* [Bundled mode](/preview/pr-749/docs/getting-started/modes/bundled.md): the `flow.json` workflow
* [Collector reference](/preview/pr-749/docs/collector.md): full `startFlow()` API documentation
