Packages

Elixir SDK for SignalBoard error tracking, logs, and activity events

Current section

Files

Jump to
Raw

README.md

# SignalBoard Elixir SDK
Minimal Elixir SDK for sending events and structured logs to SignalBoard.
## Installation
For local development inside this workspace:
```elixir
def deps do
[
{:signalboard_sdk, path: "../sdk-elixir"}
]
end
```
From Hex:
```elixir
def deps do
[
{:signalboard_sdk, "~> 0.1.0"}
]
end
```
## Configuration
```bash
export SIGNALBOARD_DSN="https://sbp_live_xxx@signalboard.deployado.com"
export SIGNALBOARD_ENV="production"
export SIGNALBOARD_RELEASE="2026.05.13-1"
```
For Phoenix applications, prefer runtime configuration:
```elixir
config :signalboard_sdk,
dsn: System.fetch_env!("SIGNALBOARD_DSN"),
environment: System.get_env("SIGNALBOARD_ENV", "production"),
release: System.get_env("SIGNALBOARD_RELEASE")
```
## Phoenix / InsuranceBoard setup
In your Phoenix Endpoint:
```elixir
defmodule InsuranceBoardWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :insurance_board
use SignalBoard.PlugCapture,
user: &InsuranceBoardWeb.SignalBoardContext.user/1,
attributes: &InsuranceBoardWeb.SignalBoardContext.attributes/1,
account_id: &InsuranceBoardWeb.SignalBoardContext.account_id/1,
organization_id: &InsuranceBoardWeb.SignalBoardContext.organization_id/1
plug Plug.RequestId
plug SignalBoard.PlugContext,
user: &InsuranceBoardWeb.SignalBoardContext.user/1,
attributes: &InsuranceBoardWeb.SignalBoardContext.attributes/1,
account_id: &InsuranceBoardWeb.SignalBoardContext.account_id/1,
organization_id: &InsuranceBoardWeb.SignalBoardContext.organization_id/1
plug SignalBoard.PlugRequestLogger,
user: &InsuranceBoardWeb.SignalBoardContext.user/1,
attributes: &InsuranceBoardWeb.SignalBoardContext.attributes/1,
account_id: &InsuranceBoardWeb.SignalBoardContext.account_id/1,
organization_id: &InsuranceBoardWeb.SignalBoardContext.organization_id/1
plug InsuranceBoardWeb.Router
end
```
Example extractor module:
```elixir
defmodule InsuranceBoardWeb.SignalBoardContext do
def user(conn) do
case conn.assigns[:current_user] do
nil -> nil
user -> %{id: user.id, email: user.email}
end
end
def account_id(conn), do: conn.assigns[:current_account] && conn.assigns.current_account.id
def organization_id(conn), do: conn.assigns[:current_organization] && conn.assigns.current_organization.id
def attributes(conn) do
case conn.assigns[:current_user] do
%{agency_id: agency_id, role: role} when not is_nil(agency_id) ->
%{agency_id: agency_id, tenant_id: agency_id, user_role: role}
_user ->
%{}
end
end
end
```
The Phoenix integration automatically attaches:
- `request_id` from `Plug.RequestId` / `x-request-id`
- `trace_id` from `traceparent` / `x-trace-id`
- request method, path, remote IP, and user agent
- `user.id`, `user.email`
- searchable attributes such as `agency_id`, `tenant_id`, plan, or role
- account and organization ids in event context and log metadata
- release, environment, runtime, and server name
- one structured request log per non-static request when
`SignalBoard.PlugRequestLogger` is enabled
Query strings are excluded by default to avoid leaking sensitive values. Pass
`include_query_string: true` to `SignalBoard.PlugContext` only when query params
are safe for your app.
Use `attributes` for low-cardinality fields you want to search and facet on,
for example `agency_id=123`, `tenant_id=123`, `plan=pro`, or `role=admin`.
Use `context` and log `metadata` for diagnostic payloads that are useful in
details but are not primary search dimensions.
## Logger handler
`SignalBoard.PlugCapture` only sees exceptions raised inside a Plug request.
Crashes in LiveView processes, GenServers, Tasks, or Oban workers, and explicit
`Logger.error/1` calls, are forwarded by the `:logger` handler. Attach it once
in `Application.start/2`, before starting the supervision tree:
```elixir
SignalBoard.LoggerHandler.attach(
level: :error,
metadata: [:request_id, :institution_id],
excluded_exceptions: [Postgrex.Error, DBConnection.ConnectionError]
)
```
Crash reports (`crash_reason` metadata) are sent as exceptions with their
stacktrace; other messages at or above `level` are sent as message events.
Logs from the `:cowboy` and `:bandit` domains are skipped by default so request
crashes already captured by `SignalBoard.PlugCapture` are not reported twice.
Delivery runs in a separate process (`async: true`) and never raises, so a
SignalBoard outage cannot affect logging.
Options: `level`, `capture_log_messages`, `metadata` (list or `:all`),
`excluded_domains`, `excluded_exceptions`, `tags`, `async`, plus `dsn`,
`environment`, `release`, and `transport` overrides.
## Usage
```elixir
SignalBoard.SDK.capture_message("Payment failed", level: "error")
try do
risky_operation()
rescue
exception ->
SignalBoard.SDK.capture_exception(exception, __STACKTRACE__,
tags: %{"job" => "billing"},
context: %{"invoice_id" => "inv_123"}
)
reraise exception, __STACKTRACE__
end
SignalBoard.SDK.log("Payment intent created",
level: "info",
logger: "MyApp.Payments",
request_id: "req_123",
attributes: %{agency_id: "agency_123", plan: "pro"},
metadata: %{"amount" => 1999, "currency" => "usd"}
)
SignalBoard.SDK.add_breadcrumb("policy quoted",
category: "policy",
metadata: %{policy_id: "pol_123"}
)
SignalBoard.SDK.set_context(%{context: %{carrier: "acme"}})
SignalBoard.SDK.set_attributes(%{agency_id: "agency_123", tenant_id: "agency_123"})
```
## Activity conventions
Use these helpers for the common business and operational events every SaaS
should report. They all call `SignalBoard.SDK.activity/2` under the hood, so
DSN, environment, release, request context, tenant, user, and fail-silent
behavior work the same way.
```elixir
SignalBoard.SDK.track_feature_used("policy.quote",
tenant: %{id: agency.id, name: agency.name},
attributes: %{policy_type: "auto"}
)
SignalBoard.SDK.track_email_sent(
tenant: %{id: agency.id},
template: "policy_renewal",
provider: "resend",
message_id: message_id,
recipient_email: customer.email,
duration_ms: duration_ms
)
SignalBoard.SDK.track_email_failed(reason,
tenant: %{id: agency.id},
template: "policy_renewal",
provider: "resend",
recipient_email: customer.email
)
SignalBoard.SDK.track_job_started("renewal_reminders", queue: "default")
SignalBoard.SDK.track_job_finished("renewal_reminders", queue: "default", duration_ms: 842)
SignalBoard.SDK.track_job_failed("renewal_reminders", reason, queue: "default", attempt: 2)
SignalBoard.SDK.track_tenant_created(%{id: agency.id, name: agency.name}, plan: agency.plan)
SignalBoard.SDK.track_subscription_changed(
tenant: %{id: agency.id},
from_plan: "basic",
to_plan: "pro",
provider: "stripe"
)
```
Recommended event names:
- `feature.used`
- `email.sent`
- `email.failed`
- `job.started`
- `job.finished`
- `job.failed`
- `tenant.created`
- `subscription.changed`
For app-specific activity, keep names in `noun.verb` form:
```elixir
SignalBoard.SDK.activity("policy.issued",
tenant: %{id: agency.id, name: agency.name},
user: %{id: user.id, email: user.email},
attributes: %{policy_id: policy.id, carrier: policy.carrier},
properties: %{premium_cents: policy.premium_cents}
)
```
## Español
SDK mínimo de Elixir para enviar eventos y logs estructurados a SignalBoard.
### Instalación local
```elixir
def deps do
[
{:signalboard_sdk, path: "../sdk-elixir"}
]
end
```
### Configuración
```bash
export SIGNALBOARD_DSN="https://sbp_live_xxx@signalboard.deployado.com"
export SIGNALBOARD_ENV="production"
export SIGNALBOARD_RELEASE="2026.05.13-1"
```
Para Phoenix, configura el SDK en runtime:
```elixir
config :signalboard_sdk,
dsn: System.fetch_env!("SIGNALBOARD_DSN"),
environment: System.get_env("SIGNALBOARD_ENV", "production"),
release: System.get_env("SIGNALBOARD_RELEASE")
```
### Setup Phoenix / InsuranceBoard
En el Endpoint:
```elixir
defmodule InsuranceBoardWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :insurance_board
use SignalBoard.PlugCapture,
user: &InsuranceBoardWeb.SignalBoardContext.user/1,
attributes: &InsuranceBoardWeb.SignalBoardContext.attributes/1,
account_id: &InsuranceBoardWeb.SignalBoardContext.account_id/1,
organization_id: &InsuranceBoardWeb.SignalBoardContext.organization_id/1
plug Plug.RequestId
plug SignalBoard.PlugContext,
user: &InsuranceBoardWeb.SignalBoardContext.user/1,
attributes: &InsuranceBoardWeb.SignalBoardContext.attributes/1,
account_id: &InsuranceBoardWeb.SignalBoardContext.account_id/1,
organization_id: &InsuranceBoardWeb.SignalBoardContext.organization_id/1
plug SignalBoard.PlugRequestLogger,
user: &InsuranceBoardWeb.SignalBoardContext.user/1,
attributes: &InsuranceBoardWeb.SignalBoardContext.attributes/1,
account_id: &InsuranceBoardWeb.SignalBoardContext.account_id/1,
organization_id: &InsuranceBoardWeb.SignalBoardContext.organization_id/1
plug InsuranceBoardWeb.Router
end
```
Extractor recomendado:
```elixir
defmodule InsuranceBoardWeb.SignalBoardContext do
def user(conn) do
case conn.assigns[:current_user] do
nil -> nil
user -> %{id: user.id, email: user.email}
end
end
def account_id(conn), do: conn.assigns[:current_account] && conn.assigns.current_account.id
def organization_id(conn), do: conn.assigns[:current_organization] && conn.assigns.current_organization.id
def attributes(conn) do
case conn.assigns[:current_user] do
%{agency_id: agency_id, role: role} when not is_nil(agency_id) ->
%{agency_id: agency_id, tenant_id: agency_id, user_role: role}
_user ->
%{}
end
end
end
```
Esto adjunta automáticamente `request_id`, `trace_id`, usuario, atributos
buscables, cuenta, organización, release, environment, runtime y metadata básica
del request.
El query string se excluye por defecto para evitar filtrar datos sensibles.
`SignalBoard.PlugRequestLogger` envía un log estructurado por request no estático.
Usa `attributes` para dimensiones que quieras buscar o convertir en facets,
por ejemplo `agency_id=123`, `tenant_id=123`, `plan=pro` o `role=admin`.
Usa `context` y `metadata` para payloads de diagnóstico que deben verse en el
detalle, pero no son la dimensión principal de búsqueda.
### Logger handler
`SignalBoard.PlugCapture` solo ve excepciones dentro de un request de Plug. Los
crashes en procesos LiveView, GenServers, Tasks u Oban workers, y las llamadas
explícitas a `Logger.error/1`, se reenvían con el handler de `:logger`. Actívalo
una vez en `Application.start/2`, antes de arrancar el árbol de supervisión:
```elixir
SignalBoard.LoggerHandler.attach(
level: :error,
metadata: [:request_id, :institution_id],
excluded_exceptions: [Postgrex.Error, DBConnection.ConnectionError]
)
```
Los crash reports se envían como excepciones con stacktrace; el resto de
mensajes con nivel ≥ `level` se envían como eventos de mensaje. Los dominios
`:cowboy` y `:bandit` se omiten por defecto para no duplicar lo que ya captura
`SignalBoard.PlugCapture`. El envío corre en otro proceso (`async: true`) y
nunca lanza excepciones.
### Uso
```elixir
SignalBoard.SDK.capture_message("Falló el pago", level: "error")
try do
operacion_riesgosa()
rescue
exception ->
SignalBoard.SDK.capture_exception(exception, __STACKTRACE__,
tags: %{"job" => "billing"},
context: %{"invoice_id" => "inv_123"}
)
reraise exception, __STACKTRACE__
end
SignalBoard.SDK.log("Payment intent creado",
level: "info",
logger: "MyApp.Payments",
request_id: "req_123",
attributes: %{agency_id: "agency_123", plan: "pro"},
metadata: %{"amount" => 1999, "currency" => "usd"}
)
SignalBoard.SDK.add_breadcrumb("cotización generada",
category: "policy",
metadata: %{policy_id: "pol_123"}
)
SignalBoard.SDK.set_attributes(%{agency_id: "agency_123", tenant_id: "agency_123"})
```
### Convenciones de actividad
Usa estos helpers para los eventos de negocio y operación que conviene reportar
en todos los SaaS. Todos usan `SignalBoard.SDK.activity/2` internamente, así que
mantienen DSN, environment, release, contexto del request, tenant, usuario y el
comportamiento fail-silent.
```elixir
SignalBoard.SDK.track_feature_used("policy.quote",
tenant: %{id: agency.id, name: agency.name},
attributes: %{policy_type: "auto"}
)
SignalBoard.SDK.track_email_sent(
tenant: %{id: agency.id},
template: "policy_renewal",
provider: "resend",
message_id: message_id,
recipient_email: customer.email,
duration_ms: duration_ms
)
SignalBoard.SDK.track_email_failed(reason,
tenant: %{id: agency.id},
template: "policy_renewal",
provider: "resend",
recipient_email: customer.email
)
SignalBoard.SDK.track_job_started("renewal_reminders", queue: "default")
SignalBoard.SDK.track_job_finished("renewal_reminders", queue: "default", duration_ms: 842)
SignalBoard.SDK.track_job_failed("renewal_reminders", reason, queue: "default", attempt: 2)
SignalBoard.SDK.track_tenant_created(%{id: agency.id, name: agency.name}, plan: agency.plan)
SignalBoard.SDK.track_subscription_changed(
tenant: %{id: agency.id},
from_plan: "basic",
to_plan: "pro",
provider: "stripe"
)
```
Nombres recomendados:
- `feature.used`
- `email.sent`
- `email.failed`
- `job.started`
- `job.finished`
- `job.failed`
- `tenant.created`
- `subscription.changed`
Para eventos propios de cada app, usa nombres tipo `noun.verb`:
```elixir
SignalBoard.SDK.activity("policy.issued",
tenant: %{id: agency.id, name: agency.name},
user: %{id: user.id, email: user.email},
attributes: %{policy_id: policy.id, carrier: policy.carrier},
properties: %{premium_cents: policy.premium_cents}
)
```