README.md

# Rbtz.PostgrexDisconnectTracker

[![CI](https://github.com/tinyrbtz/postgrex_disconnect_tracker/actions/workflows/ci.yml/badge.svg)](https://github.com/tinyrbtz/postgrex_disconnect_tracker/actions/workflows/ci.yml)
[![Hex version](https://img.shields.io/hexpm/v/rbtz_postgrex_disconnect_tracker.svg "Hex version")](https://hex.pm/packages/rbtz_postgrex_disconnect_tracker)
[![Hex downloads](https://img.shields.io/hexpm/dt/rbtz_postgrex_disconnect_tracker.svg "Hex downloads")](https://hex.pm/packages/rbtz_postgrex_disconnect_tracker)
[![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org)

Pins Postgrex disconnection errors to the ExUnit test that caused them.

When a test leaks a Postgrex connection — e.g. a spawned task outlives the test, a sandbox owner exits mid-query, or a driver crash hits the pool — the error logged by Postgrex looks like this:

```
[error] Postgrex.Protocol (#PID<0.512.0>) disconnected: ** (DBConnection.ConnectionError) client #PID<0.1234.0> exited
```

The message names the **client PID**, but that PID no longer exists and has no connection back to the test suite. You're left grepping for which test touched the affected code. In a CI run of a thousand tests that can be a bad afternoon.

This library attaches a telemetry handler to your repo and an OTP `:logger` handler, and maintains an ETS map from query-executing PIDs (and their ancestors) to the test that started them. When Postgrex logs a disconnect, the handler parses the client PID, looks up the owning test, and prints an annotated error like:

```
=== POSTGREX DISCONNECTION ===
Test: MyApp.AccountsTest - test registers a user
Client PID: #PID<0.1234.0>
Stacktrace: ...
Error: ...
==============================
```

## Why / when to use this

**Use it when:**
- You've seen intermittent `Postgrex.Protocol ... disconnected` errors in CI and don't know which test is responsible.
- You're hunting a leaked connection, a background task that outlives its test, or a misconfigured `Ecto.Adapters.SQL.Sandbox` owner.
- A CI run is flaky and you suspect one bad test is knocking out the connection pool and poisoning later tests.

**Don't use it when:**
- You aren't seeing Postgrex disconnect errors. The tracker adds a telemetry handler on every query — the overhead is small but non-zero, and the output is noise unless disconnects are actually happening.
- You're running production code. This is a **test-only** utility — only attach it from `test_helper.exs`.

It's a diagnostic aid, not a fix. Once it points you at the offending test, you still need to patch that test (close the connection, wait for the task, re-scope the sandbox, etc.).

## Installation

Add `rbtz_postgrex_disconnect_tracker` to your `mix.exs` test deps:

```elixir
def deps do
  [
    {:rbtz_postgrex_disconnect_tracker, "~> 0.1", only: :test}
  ]
end
```

Run `mix deps.get`.

## Setup (recommended)

One call in `test/test_helper.exs`, before `ExUnit.start/1`:

```elixir
Rbtz.PostgrexDisconnectTracker.setup(telemetry_event: [:my_app, :repo, :query])

ExUnit.start()
```

`:telemetry_event` is the Ecto query telemetry event for your repo — usually `[:<your_otp_app>, :repo, :query]`. Ecto fires it on every query; the tracker uses it to learn which process is running queries for which test.

Then in your `DataCase` (or whichever `ExUnit.CaseTemplate` sets up the sandbox), register the current test at the start of each `setup`:

```elixir
defmodule MyApp.DataCase do
  use ExUnit.CaseTemplate

  setup tags do
    Rbtz.PostgrexDisconnectTracker.register_test("#{tags.module} - #{tags.test}")

    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)

    :ok
  end
end
```

That's it. When a disconnect is logged, you'll see the test name in the output.

## Setup (individual functions)

If you need to wire the pieces yourself — for example, to attach the logger on a different subset of tests, or to hold off on the telemetry handler until a suite-level setup — call them separately instead of `setup/1`:

```elixir
Rbtz.PostgrexDisconnectTracker.Tracker.init(telemetry_event: [:my_app, :repo, :query])
Rbtz.PostgrexDisconnectTracker.Logger.attach()
```

`setup/1` is exactly equivalent to these two calls in this order.

## License

MIT. See [LICENSE](LICENSE).