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

# Telemetry Events

> Push product telemetry events from your app into Trajectory.

`TelemetryEvent` is the SDK's primitive for product telemetry — user actions, tool calls, agent
decisions, anything you'd send to a product analytics tool. Events are pushed independently of
trajectories, and can be correlated to them via `trace_id` (see
[Linking Events to Trajectories](/sdk/linking-events-to-trajectories)).

## Quickstart

For telemetry-only usage, only `project_id` and `trajectory_api_key` are required in `init()` —
no provider credentials needed.

```python theme={null}
import trajectory_sdk as tj

tj.init(project_id="acme", trajectory_api_key="tj_key_...")

result = tj.push_events([
    tj.TelemetryEvent(
        event_type="user.accept",
        session_id="session-123",
        properties={"surface": "chat", "model": "gpt-4o"},
    ),
])

print(result.pushed, result.skipped, result.errors)
```

## Constructing Events

`TelemetryEvent` is a frozen dataclass. Only `event_type` and `session_id` are required; every
other field has a sensible default.

```python theme={null}
event = tj.TelemetryEvent(
    event_type="tool.call",
    session_id="session-123",
    properties={"tool": "search", "query": "refunds policy"},
    user_id="user-abc",
    trace_id="req_01HXYZ",
    trajectory_id="traj_xyz",
    source="my_app",
    metadata={"app_version": "2.4.0"},
)
```

| Field           | Type           | Default       | Description                                                                           |
| --------------- | -------------- | ------------- | ------------------------------------------------------------------------------------- |
| `event_type`    | `str`          | required      | Dotted event name (e.g. `user.accept`, `tool.call`).                                  |
| `session_id`    | `str`          | required      | Session grouping. Defaults to `trace_id` when produced via `TraceContext.event(...)`. |
| `properties`    | `dict`         | `{}`          | Free-form event payload.                                                              |
| `event_id`      | `str`          | new UUID4 hex | **Idempotency key** (see below).                                                      |
| `timestamp`     | `str`          | now (UTC ISO) | Event time. Pass an explicit value to backdate.                                       |
| `user_id`       | `str \| None`  | `None`        | End-user identifier.                                                                  |
| `trajectory_id` | `str \| None`  | `None`        | Backend trajectory ID once known.                                                     |
| `trace_id`      | `str \| None`  | `None`        | Caller-owned correlation key. Always set when producing events via `TraceContext`.    |
| `source`        | `str`          | `"sdk"`       | Where the event originated.                                                           |
| `metadata`      | `dict \| None` | `None`        | Additional context not part of `properties`.                                          |

## Idempotency

`event_id` is the idempotency key. The default UUID4 means re-runs of the same code produce
different IDs (and the backend will accept both as separate events).

For at-least-once delivery pipelines — webhooks, retries, replay from a queue — set `event_id`
**deterministically** from your own primary key so duplicate pushes are coalesced server-side:

```python theme={null}
import hashlib

def event_id_for(record_id: str) -> str:
    return hashlib.sha256(record_id.encode()).hexdigest()

event = tj.TelemetryEvent(
    event_type="user.accept",
    session_id="session-123",
    event_id=event_id_for(record["id"]),
)
```

Re-pushing the same `event_id` is a no-op on the backend.

## Pushing Events

`tj.push_events(events)` validates, chunks, retries, and pushes a batch of events.

```python theme={null}
result = tj.push_events(
    events,
    organization_id="org_42",
    chunk_size=1000,
    max_retries=3,
)
```

| Parameter         | Type                   | Default  | Description                                                                                     |
| ----------------- | ---------------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `events`          | `list[TelemetryEvent]` | required | Events to push.                                                                                 |
| `organization_id` | `str \| None`          | `None`   | Logged client-side for attribution. The backend derives the authoritative org from the API key. |
| `chunk_size`      | `int`                  | `1000`   | Max events per HTTP request. The SDK splits longer lists automatically.                         |
| `max_retries`     | `int`                  | `3`      | Per-chunk retry budget on transient failures.                                                   |

The call returns a `PushResult`:

| Field     | Type        | Description                            |
| --------- | ----------- | -------------------------------------- |
| `pushed`  | `int`       | Events accepted by the backend.        |
| `skipped` | `int`       | Events dropped client-side as invalid. |
| `errors`  | `list[str]` | One message per invalid event.         |

<Note>
  Invalid events are dropped with a logged warning rather than raising. This matches the
  pattern used by `upload()` for empty trajectories: bad inputs don't poison a whole batch.
  Inspect `result.errors` to see what was rejected.
</Note>

## Producing Events via TraceContext

When you're already inside a `TraceContext` (the recommended path for SDK-instrumented agent
runs), use `trace.event(...)` so events automatically inherit the `trace_id`:

```python theme={null}
trace = tj.start_trace()

trace.event("tool.call", {"tool": "search"})
trace.event("tool.result", {"ok": True}, user_id="user-abc")

# trace.events is the accumulated buffer
tj.push_events(trace.events)
```

`TraceContext.event(...)` sets `session_id = trace_id` automatically. Override either via the
keyword arguments if you need a different grouping.

See [Linking Events to Trajectories](/sdk/linking-events-to-trajectories) for the full
workflow, including `tj.upload_trace()` which uploads a trajectory and pushes correlated events
in one atomic call.

## Related

<CardGroup cols={2}>
  <Card title="Linking Events to Trajectories" icon="link" href="/sdk/linking-events-to-trajectories">
    The `trace_id` correlation story end-to-end.
  </Card>

  <Card title="API Reference" icon="square-terminal" href="/sdk/api-reference">
    Full signatures for `push_events`, `TelemetryEvent`, `PushResult`.
  </Card>
</CardGroup>
