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