SignalBoard Elixir SDK
Minimal Elixir SDK for sending events and structured logs to SignalBoard.
Installation
For local development inside this workspace:
def deps do
[
{:signalboard_sdk, path: "../sdk-elixir"}
]
end
From Hex:
def deps do
[
{:signalboard_sdk, "~> 0.1.0"}
]
end
Configuration
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:
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:
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:
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_idfromPlug.RequestId/x-request-idtrace_idfromtraceparent/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.PlugRequestLoggeris 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:
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
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.
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.usedemail.sentemail.failedjob.startedjob.finishedjob.failedtenant.createdsubscription.changed
For app-specific activity, keep names in noun.verb form:
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
def deps do
[
{:signalboard_sdk, path: "../sdk-elixir"}
]
end
Configuración
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:
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:
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:
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:
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
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.
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.usedemail.sentemail.failedjob.startedjob.finishedjob.failedtenant.createdsubscription.changed
Para eventos propios de cada app, usa nombres tipo noun.verb:
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}
)