Skip to main content

README.md

# ditliti

Official Elixir SDK for [Ditliti](https://github.com/kangartha/inspectora) - error tracking, tracing, session replay, and profiling ingestion.

This SDK talks to a **self-hosted** Ditliti instance (`ingestion-api`). It does not connect to any managed/hosted service - point it at your own deployment's ingestion URL. Dependency-free (no Jason/Poison/HTTPoison required).

## Install

```elixir
def deps do
  [
    {:ditliti, git: "https://github.com/kangartha/inspectora.git", ref: "sdk-v0.1.2", sparse: "sdk/elixir"}
  ]
end
```

## Quick start

```elixir
client = Ditliti.new("http://localhost:8305", "mp_xxx", "PROJECT_UUID", release: "1.2.3", environment: "production")

try do
  risky_operation()
rescue
  exception ->
    Ditliti.capture_exception(client, exception, context: %{extra: "context"})
end
```

The client struct is immutable: functions that add context
(`add_breadcrumb/3`, `put_user/2`, `put_session/2`) return an updated struct
rather than mutating anything in place.

## API

- `Ditliti.new(endpoint, api_key, project_id, opts \\\\ [])`
- `Ditliti.add_breadcrumb(client, message, opts \\\\ [])`
- `Ditliti.put_user(client, user)` / `Ditliti.put_session(client, session_id)`
- `Ditliti.capture_exception(client, exception, opts \\\\ [])`
- `Ditliti.capture_message(client, message, opts \\\\ [])`
- `Ditliti.capture_replay_segment(client, duration_ms, events, opts \\\\ [])`
- `Ditliti.capture_profile(client, duration_ms, samples, opts \\\\ [])`
- `Ditliti.Plug.capture_exception(client, conn, exception, stacktrace)` - Phoenix/Plug helper for `handle_errors/2`.
- `Ditliti.send_envelope(client, envelope)` - low-level batch submit.

## Distributed tracing

Generates real W3C `traceparent` (https://www.w3.org/TR/trace-context/) trace context, so a trace can be propagated across service/microservice boundaries instead of staying siloed per project. Since the client struct is immutable, `start_trace/2` returns an updated client rather than mutating anything:

```elixir
# Service A: start a trace and call Service B, forwarding the header.
client = Ditliti.start_trace(client)
HTTPoison.get(url, [{"traceparent", Ditliti.get_traceparent(client)}])

# Service B: continue the same trace from the inbound header (Ditliti.Plug does this automatically).
client = Ditliti.start_trace(client, incoming_traceparent_header)

{client, span} = Ditliti.start_span(client, "db.query", tags: %{"db" => "postgres"})
# ... do the work ...
Ditliti.finish_span(client, span)

# capture_exception/2 automatically tags trace_id when a trace is active,
# so errors show up correlated to the trace at GET /api/v1/traces/:trace_id (org-wide).
```

- `Ditliti.start_trace(client, incoming_traceparent \\\\ nil)` - starts a new trace, or continues one from an inbound `traceparent` header.
- `Ditliti.get_traceparent(client)` - the header value to forward on outgoing requests.
- `Ditliti.start_span(client, operation_name, opts \\\\ [])` → `{client, span}`, then `Ditliti.finish_span(client, span, extra_tags \\\\ %{})`.

## Database query instrumentation (Database Insights)

`Ditliti.Db` wraps a query with a `db.query` span carrying the OpenTelemetry-shaped attributes the backend uses for Database Insights (slow query / N+1 / error-rate / regression detection). The server parameterizes and hashes `db.query.text` itself - send the real SQL text, never a redacted one:

```elixir
# Generic wrapper - works for any driver/Ecto adapter:
{client, result} =
  Ditliti.Db.trace_query(client, [system: "postgresql", text: "SELECT * FROM orders WHERE id = $1"], fn ->
    Postgrex.query(conn, "SELECT * FROM orders WHERE id = $1", [order_id])
  end)

# Or auto-instrument every query via Ecto's own :telemetry event - no Repo
# wrapping required, works for the query builder/changesets/raw queries alike:
Ditliti.Db.attach_ecto_telemetry(client, [:my_app, :repo, :query], system: "postgresql")
```

`attach_ecto_telemetry/3` correlates to the current trace via `Ditliti.current_trace/0`, which `start_trace/2` populates automatically for the calling process (Ecto/Postgrex telemetry handlers run in the same process as the query call) - call `start_trace/2` early in the request lifecycle (e.g. a Phoenix Plug) for DB spans to show up linked to that request's trace.

Neither ever sets `db.query.summary` or `ditliti.query_hash` (server-computed). On failure they record `error.type` and preserve the original error unchanged (`trace_query/3` re-raises; the Ecto handler reads `error.type` from the telemetry event's `:result` metadata); on success they record `db.response.returned_rows`, inferred from common Ecto/Postgrex result shapes or via an explicit `:row_count` option.

## License

MIT - see [LICENSE](https://github.com/kangartha/inspectora/blob/main/LICENSE).