Elixir / Phoenix
HTTP comes from OTP (:httpc), so the SDK adds no HTTP client to your dependency tree.
Before you start
- Where your InsightRecorder runs
- The base URL every call goes to —
http://localhost:8080when you are running it locally, your own hostname otherwise. - An API key
- Settings → API keys, scoped to
logs:writeandbug:write. The token (crk_…) is shown exactly once — it is hashed at rest — and is revocable. Do not use a login JWT: it expires within the hour and is not meant for machines.
- 01
Install
Only jason as a runtime dependency.
{:insight_recorder, "~> 0.1"} - 02
Put the shipper in your supervision tree
It traps exits, so shutdown flushes the last batch.
client = InsightRecorder.Client.new( System.fetch_env!("INSIGHT_BASE_URL"), System.fetch_env!("INSIGHT_API_KEY") ) children = [{InsightRecorder.Shipper, client: client, name: MyApp.Shipper}] - 03
Ship a log line
Recognized keys map to the canonical model.
InsightRecorder.Shipper.send(MyApp.Shipper, %{ level: "error", message: "checkout failed", trace_id: trace_id }) - 04
Report unhandled exceptions
In your Phoenix endpoint. It hooks Plug.ErrorHandler, which is what actually sees exceptions raised downstream — a plain plug wrapping the pipeline never does.
use InsightRecorder.ErrorHandler, client: {MyApp, :insight_client, []}, shipper: MyApp.Shipper, service: "checkout-api" - 05
Verify it is working
Send one line through the supervised shipper. Because it lives in the supervision tree, shutdown flushes the last batch for you.
InsightRecorder.Shipper.send(MyApp.Shipper, %{ level: "error", message: "insightrecorder smoke test", trace_id: "smoke-1" })Then look for the line at /app/logs and the report at /app/bugs.
If nothing shows upNothing arrived? Check InsightRecorder.Shipper.dropped/1 — a rising count means the queue is undersized. Delivery errors are silent unless you pass :on_error. And confirm the shipper is in the supervision tree rather than started ad hoc, or nothing flushes on shutdown.
Before you ship
- — A body-less POST needs :httpc's 4-tuple form; the SDK handles that so summarize/analyze calls work.
Paste this into a coding agent running inside your project, or point the agent at /docs/prompts/elixir directly. It is self-contained: the agent needs no prior knowledge of InsightRecorder.
# Install and wire the InsightRecorder Elixir SDK
Paste this into a coding agent (Claude Code, Cursor, …) running inside your
Elixir or Phoenix application's repository. It is self-contained — the agent
needs no prior knowledge of InsightRecorder.
---
You are working in an Elixir application. Install and wire the InsightRecorder SDK so
it ships its logs to InsightRecorder and reports unhandled exceptions.
**Context you need:**
- InsightRecorder collects application logs and bug reports. `:insight_recorder` is
the supported way for an Elixir service to talk to it.
- Requires **Elixir 1.15+**. It has one runtime dependency (`:jason`); HTTP uses
OTP's `:httpc`, so it adds no HTTP client to the application tree.
- Authentication uses a long-lived **API key** shaped like `crk_…`, created in
the InsightRecorder UI under *Settings → API keys* with the `logs:write` scope (add
`bug:write` to report errors). **Do not** use a login JWT: those expire after
an hour and are not meant for machines.
Add the matching **read** scopes for anything the SDK reads back — `bug:read` for `list_bugs/get_bug`, `logs:read` for `list_logs`. A write-only key is the common mistake: shipping works, the first read returns 403.
- **Wire trace correlation if the application uses OpenTelemetry.** A log line
and the request that wrote it are one investigation, and that join is the
reason to ship logs here rather than anywhere else — see the "Trace
correlation" section of the README for what this language needs (several do it
automatically; the rest take one hook). Without it, every log call has to carry
`trace_id` by hand, which nobody sustains past the first week.
**Steps:**
1. Add `{:insight_recorder, "~> 0.1"}` to `deps/0` in `mix.exs` and run
`mix deps.get`.
2. Read configuration from the environment at **runtime**, not compile time —
put it in `config/runtime.exs`, which is where releases read config:
```elixir
config :my_app, :insight,
base_url: System.get_env("INSIGHT_BASE_URL"),
api_key: System.get_env("INSIGHT_API_KEY")
```
If either is missing, the application must boot normally with shipping
disabled — telemetry is never a startup dependency.
3. Build the client and start the shipper **in the application's supervision
tree** (`lib/my_app/application.ex`), so shutdown flushes the last batch:
```elixir
children = [
# … existing children …
{InsightRecorder.Shipper, client: insight_client(), name: MyApp.Shipper}
]
```
Starting it outside a supervisor would skip the flush on shutdown.
4. Ship logs. Prefer an explicit call at the points that matter:
```elixir
InsightRecorder.Shipper.send(MyApp.Shipper, %{
level: "error",
message: "checkout failed",
trace_id: trace_id,
attrs: %{"order_id" => order_id}
})
```
**To ship every `Logger` call rather than scattering `log/2` by hand**, add a
`:logger` handler — OTP has supported custom handlers natively since OTP 21,
so no extra dependency is needed. A handler is a module with `log/2`:
```elixir
defmodule MyApp.InsightLogHandler do
@moduledoc "Ships every Logger event to InsightRecorder, best-effort."
def log(%{level: level, msg: msg, meta: meta}, _config) do
InsightRecorder.log(MyApp.insight_client(), %{
level: Atom.to_string(level),
message: message(msg),
trace_id: meta[:trace_id],
attrs: %{}
})
:ok
end
defp message({:string, text}), do: IO.iodata_to_binary(text)
defp message({:report, report}), do: inspect(report)
defp message({format, args}), do: format |> :io_lib.format(args) |> IO.iodata_to_binary()
end
```
Register it **alongside** the default handler, never instead of it — shipping
is best-effort, so the local log stays the source of truth for the running
process:
```elixir
:logger.add_handler(:insight, MyApp.InsightLogHandler, %{level: :info})
```
The one trap: **never log from inside the handler.** A delivery failure that
logs fires the handler, which logs. `InsightRecorder.log/2` queues and returns
without blocking, and errors surface through the shipper's own callback.
If the application already has a telemetry handler pipeline, wire it there
instead and follow the existing pattern.
5. If this is a Phoenix application, report unhandled exceptions through
`Plug.ErrorHandler` — the mechanism that actually sees exceptions raised
downstream (a plug cannot: its `call/2` returns before they happen). In the
endpoint module:
```elixir
use InsightRecorder.ErrorHandler,
client: {MyApp, :insight_client, []},
shipper: MyApp.Shipper,
service: "<app name>"
```
The client is an MFA tuple because the endpoint compiles before runtime
config is loaded. If the application already defines `handle_errors/2`, call
`InsightRecorder.ErrorReporter.report(conn, error, opts)` from inside it
instead of adding a second handler.
**Rules — do not violate these:**
- Do **not** build a custom `:httpc`/Finch caller that posts one request per log
line. The SDK batches, chunks each request at `:batch_size`, retries with
backoff, and respects the server's quota.
- Do **not** wrap `Shipper.send/2` in a `GenServer.call` or otherwise make it
synchronous — it is a cast on purpose and drops when its queue is full.
- Do **not** log the API key, and do not commit it.
- Do **not** capture request bodies, headers, cookies, or query strings in
reports. The plug records only the method and path.
**Verify before you finish:**
1. `mix compile --warnings-as-errors` and the project's test command pass.
2. Boot the app with both env vars set, exercise a request, then confirm the log
lines appear in InsightRecorder at `/app/logs`.
3. Raise a deliberate exception in a controller and confirm a bug appears at
`/app/bugs`.
4. Confirm the app still boots with both variables **unset**.
5. Report what you changed, and state explicitly whether the shipper is under
the supervision tree.
If anything is ambiguous — where the application module is, how config is
loaded, whether Phoenix is present — inspect the repository and follow what is
already there. Do not restructure the application to fit the SDK.