Elixir library that reports errors, OpenTelemetry traces, and system resource breaches to the GoodIssues API. Integrates with error_tracker for error reporting, auto-instruments Phoenix, LiveView, and Ecto via the OTel SDK, and monitors BEAM/OS metrics for threshold breaches.
- Error reporting — Listens for
error_trackertelemetry events and reports errors to GoodIssues with fingerprinting, context, breadcrumbs, and stacktraces - OpenTelemetry integration — Auto-instruments Phoenix, LiveView, and Ecto via the OTel SDK, exporting spans to GoodIssues via OTLP
- Trace correlation — Injects
trace_idfrom active OTel spans into error and incident payloads - System monitor — Polls BEAM-native and OS-level metrics, reporting threshold breaches as incidents to GoodIssues with server-side deduplication and auto-resolve
- Network scanner — Periodically scans the local host for unexpected open TCP ports using deny/allow list strategies, reporting incidents with auto-resolve when ports close
- Async by default — API calls are non-blocking via
Task.Supervisor, with a synchronous mode for testing
Add good_issues_reporter to your dependencies in mix.exs:
def deps do
[
{:good_issues_reporter, github: "agoodway/goodissues_reporter"}
]
end# config/runtime.exs
config :good_issues_reporter,
api_url: System.fetch_env!("GOODISSUES_API_URL"),
api_key: System.fetch_env!("GOODISSUES_API_KEY"),
project_id: System.fetch_env!("GOODISSUES_PROJECT_ID")# lib/my_app/application.ex
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{GoodIssuesReporter, []}
]
Supervisor.start_link(children, strategy: :one_for_one)
end# config/dev.exs
config :good_issues_reporter, enabled: false
# config/test.exs
config :good_issues_reporter, enabled: falseThat's it. Errors tracked by error_tracker will now be reported to GoodIssues automatically.
Note: This library is compatible with
error_tracker>= 0.5. For the most reliable telemetry integration (guaranteed:occurrencedata on new errors, proper stacktraces, etc.), we recommenderror_tracker~> 0.9.0 or later.
Add a health-check route for the reporter in your Phoenix router:
# lib/my_app_web/router.ex
forward "/gi", GoodIssuesReporter.HealthRouterGET /gi/health returns JSON indicating whether the reporter is locally configured and enabled:
- 200
{"status": "ok", "details": {...}}— reporter is enabled and all required config is present - 503
{"status": "error", "details": {...}}— reporter is disabled or required config is missing
The details object includes boolean flags (enabled, configured, api_url, api_key, project_id) without exposing raw config values.
This endpoint validates local configuration only — it does not contact the GoodIssues API or perform any network calls. It reflects reporter readiness, not API availability or whole-app health.
Caution
Security: The health route exposes whether the reporter is configured and enabled. To avoid leaking deployment details:
- Mount it behind internal routing or existing auth controls — do not expose it on public paths.
- Apply proxy-level rate limiting to prevent enumeration or abuse.
- Review your network and authentication posture before deploying.
Mount the browser error endpoint in your Phoenix router. The endpoint is designed for same-origin browser beacons and exposes POST /report below the forwarded path:
# lib/my_app_web/router.ex
forward "/gi/errors", GoodIssuesReporter.BrowserErrorRouterServe the package-owned reporter script from priv/static assets:
# lib/my_app_web/endpoint.ex
plug Plug.Static,
at: "/good_issues_reporter",
from: {:good_issues_reporter, "priv/static"},
gzip: false,
only: ~w(js)Then load and initialize the script in your frontend. For Phoenix layouts, put this near the end of your root layout body so the reporter is available before most application JavaScript runs:
<script src="/good_issues_reporter/js/good_issues_reporter/reporter.js"></script>
<script>
window.GoodIssuesReporter.init({
endpoint: "/gi/errors",
context: {
release: "2026.05.25",
user_id: "<%= @current_user && @current_user.id %>"
}
});
</script>For Phoenix applications that expose JavaScript globals from assigns, encode context explicitly instead of interpolating raw values:
<script src="/good_issues_reporter/js/good_issues_reporter/reporter.js"></script>
<script>
window.GoodIssuesReporter.init({
endpoint: "/gi/errors",
context: <%= Jason.encode!(%{
release: Application.spec(:my_app, :vsn) |> to_string(),
user_id: @current_user && @current_user.id,
account_id: assigns[:current_account] && @current_account.id
}) |> Phoenix.HTML.raw() %>
});
</script>For bundler-based frontends, load the reporter script from the static path before initializing it from your app entry point:
<script src="/good_issues_reporter/js/good_issues_reporter/reporter.js"></script>
<script type="module" src="/assets/app.js"></script>// assets/js/app.js
window.GoodIssuesReporter.init({
endpoint: "/gi/errors",
context: {
release: window.APP_RELEASE,
user_id: window.CURRENT_USER_ID
}
});Initialize the reporter once per page load. Repeated init calls update endpoint/context options but do not install duplicate global handlers.
The reporter installs guarded window.error and window.unhandledrejection handlers and also supports manual capture:
window.GoodIssuesReporter.capture(new Error("Checkout failed"));
window.GoodIssuesReporter.capture({ message: "Custom browser failure", type: "manual" });Manual captures may include safe context fields when the global context is not enough:
try {
await submitCheckout();
} catch (error) {
window.GoodIssuesReporter.capture({
type: "checkout_failure",
message: error.message,
stack: error.stack,
context: {
step: "payment",
cart_size: cart.items.length
}
});
throw error;
}Frontend payload fields supported by the endpoint:
| Field | Description |
|---|---|
type |
Browser event type, such as error, unhandledrejection, or a custom manual type |
message |
Error message; required for forwarding |
stack |
Stack trace string or list of stack lines |
source, line, col |
Browser source URL and location fields |
url, referrer |
Page URL and referrer; the reporter fills these automatically |
event_id, timestamp |
Client-generated event metadata; the reporter fills these automatically |
user, session, release, context |
Optional host-provided context; keep these values small and non-sensitive |
Operational notes:
- Browser reporting is intended for same-origin browser clients. The endpoint requires browser Fetch Metadata and accepts only
Sec-Fetch-Site: same-origin,same-site, ornone. - Requests missing
Sec-Fetch-Site, explicit cross-site requests, and non-JSON content types are ignored with a beacon-safe200response and are not forwarded. - Browser error reports always dispatch asynchronously through
GoodIssuesReporter.TaskSupervisorwith bounded in-flight admission control. If the supervisor is unavailable or the browser dispatch queue is saturated, reports are dropped and the endpoint still returns200. - This package does not provide CORS handling or origin allowlist configuration for cross-origin ingestion.
- The endpoint is unauthenticated so browser beacons can reach it; use infrastructure rate limiting for noisy or abusive clients.
- Browser messages, stacks, URLs, and context may contain PII. The server caps fields, strips URL query strings/fragments and URL credentials, and redacts configured sensitive context keys, but host apps should avoid sending sensitive browser context.
- Source maps are not uploaded or processed by this package. Configure your own release/source map workflow if you need de-obfuscated browser stack traces.
- Cross-origin script failures may report only
Script error.unless the script and response headers are configured for browser stack access.
| Option | Description |
|---|---|
api_url |
Base URL of your GoodIssues instance (e.g. "https://goodissues.dev") |
api_key |
API key for Bearer token authentication |
project_id |
UUID of the GoodIssues project to report to |
| Option | Default | Description |
|---|---|---|
enabled |
true |
Set false to disable all reporting |
async |
true |
Set false for synchronous server-side reporting API calls (useful in tests) |
The async option controls server-side reporting paths. Browser error reports always dispatch asynchronously through GoodIssuesReporter.TaskSupervisor, even when async: false is configured.
Configure under the :otel key. OTel is disabled by default and requires a service_name matching the project's otel_service_name on GoodIssues.
config :good_issues_reporter,
otel: [
enabled: true,
service_name: "my-app",
phoenix: true,
liveview: true,
ecto_repo_prefix: [:my_app, :repo]
]| Option | Default | Description |
|---|---|---|
enabled |
false |
Enable OTel auto-instrumentation and trace_id injection |
service_name |
nil |
Must match project.otel_service_name on GoodIssues |
phoenix |
true |
Attach Phoenix instrumentation (set false for non-Phoenix apps) |
liveview |
true |
Attach LiveView instrumentation (set false if not using LiveView) |
ecto_repo_prefix |
nil |
Telemetry prefix for Ecto repo (e.g. [:my_app, :repo]). Ecto instrumentation is skipped when nil. |
Important: This library attaches OTel auto-instrumentation and injects
trace_idinto error/incident payloads, but does not configure the OTel SDK. The host app must configure the:opentelemetryapplication directly:
# config/runtime.exs
config :opentelemetry,
resource: [{"service.name", "my-app"}],
traces_exporter: {:otlp, [
protocol: :http_protobuf,
endpoints: [System.fetch_env!("GOODISSUES_API_URL") <> "/api/v1/otlp/traces"],
headers: [{"authorization", "Bearer " <> System.fetch_env!("GOODISSUES_API_KEY")}]
]}Setting enabled: false globally (via config :good_issues_reporter, enabled: false) disables all reporting including OTel auto-instrumentation.
Error payloads (including context, breadcrumbs, and stacktrace from error_tracker occurrences) are sent to the GoodIssues API. This library performs built-in redaction of common sensitive keys (password, secret, token, api_key, authorization, cookie, etc.) and supports additional filtering via the :filter_parameters configuration. However, the host application is still responsible for sanitizing sensitive data before it reaches error_tracker — use error_tracker context filters or a custom ErrorTracker.Integrations.Plug for strong guarantees.
The system monitor polls BEAM and OS-level metrics on a configurable interval and reports threshold breaches as incidents to GoodIssues. Deduplication is handled server-side via fingerprint matching. When a metric recovers below its threshold, the incident is automatically resolved.
Configure under the :system_monitor key:
config :good_issues_reporter,
system_monitor: [
enabled: true,
poll_interval_ms: 30_000,
severity: "warning",
reopen_window_hours: 24,
# BEAM-native metrics (set to nil to disable)
memory_total_mb: 512,
memory_processes_mb: 256,
process_count: 50_000,
atom_count: 500_000,
port_count: 10_000,
run_queue_length: 20,
# OS-level metrics via :os_mon (set to nil to disable)
cpu_percent: 90,
disk_percent: 85,
system_memory_percent: 90,
swap_percent: 80
]| Option | Default | Description |
|---|---|---|
enabled |
false |
Enable system resource monitoring |
poll_interval_ms |
30_000 |
How often to check metrics (min: 10,000) |
severity |
"warning" |
Incident severity: "info", "warning", or "critical" |
reopen_window_hours |
24 |
How long after resolution a new occurrence reopens the incident |
| Metric | Config Key | Source | Unit |
|---|---|---|---|
| Total BEAM memory | memory_total_mb |
:erlang.memory(:total) |
MB |
| Process memory | memory_processes_mb |
:erlang.memory(:processes) |
MB |
| Process count | process_count |
:erlang.system_info(:process_count) |
count |
| Atom count | atom_count |
:erlang.system_info(:atom_count) |
count |
| Port count | port_count |
:erlang.system_info(:port_count) |
count |
| Run queue length | run_queue_length |
:erlang.statistics(:run_queue) |
count |
| Metric | Config Key | Source | Unit |
|---|---|---|---|
| CPU utilization | cpu_percent |
:cpu_sup.util/0 |
percent (0-100) |
| Disk usage | disk_percent |
:disksup.get_disk_data/0 |
percent per partition |
| System memory | system_memory_percent |
:memsup.get_system_memory_data/0 |
percent |
| Swap usage | swap_percent |
:memsup.get_system_memory_data/0 |
percent |
OS-level metrics require the :os_mon application. The system monitor will attempt to start it automatically when needed. On platforms where :os_mon is unavailable, those metrics are skipped and retried each poll cycle.
Set any metric to nil (or omit it) to disable monitoring for that metric.
The network scanner periodically scans the local host for unexpected open TCP ports using :gen_tcp.connect/4. It supports two strategies:
:deny— configured ports should NOT be open. Any deny-listed port found open triggers an incident.:allow— configured ports are the only ones permitted. Any other open port triggers an incident.
When a previously-violating port closes, the incident is automatically resolved.
config :good_issues_reporter,
network_scanner: [
enabled: true,
scan_interval_ms: 3_600_000,
host: "127.0.0.1",
strategy: :deny,
ports: [3306, 5432, 6379, 11211, 9200, 27017],
connect_timeout_ms: 1_000,
severity: "warning",
source: "network_scanner",
reopen_window_hours: 24
]| Option | Default | Description |
|---|---|---|
enabled |
false |
Enable network port scanning |
scan_interval_ms |
3_600_000 |
Scan interval (min: 60,000) |
host |
"127.0.0.1" |
Host to scan (self-scan only) |
strategy |
:deny |
:deny or :allow |
ports |
[3306, 5432, ...] |
Port list for the chosen strategy |
connect_timeout_ms |
1_000 |
TCP connect timeout per port |
severity |
"warning" |
Incident severity |
source |
"network_scanner" |
Incident source label |
reopen_window_hours |
24 |
Incident reopen window |
Note: The network scanner performs TCP connections to localhost only. In
:allowmode, it scans a fixed set of ~70 well-known service ports to discover unexpected listeners.
Heartbeat monitors periodically check the health of supervised processes and report status to GoodIssues heartbeats. Each monitor maps to a heartbeat you've provisioned in GoodIssues, using the returned ping_token as the heartbeat_token.
Configure under the :heartbeat_monitors key as a list of keyword lists:
config :good_issues_reporter,
heartbeat_monitors: [
# Monitor the built-in SystemMonitor via GenServer.call(:health)
[
enabled: true,
name: :system_monitor,
target: GoodIssuesReporter.SystemMonitor,
heartbeat_token: System.fetch_env!("GOODISSUES_SYSTEM_MONITOR_PING_TOKEN"),
check: {:call, :health},
interval_ms: 60_000,
timeout_ms: 2_000
],
# Monitor a host application GenServer
[
enabled: true,
name: :my_worker,
target: MyApp.Worker,
heartbeat_token: System.fetch_env!("GOODISSUES_WORKER_PING_TOKEN"),
check: :process_alive,
metadata: %{env: "production"}
]
]| Option | Default | Description |
|---|---|---|
enabled |
false |
Enable this monitor (monitors are opt-in) |
name |
required | Monitor name for logging and ping payloads |
target |
required | Process to check — atom name, PID, {:global, term}, or {:via, module, term} |
heartbeat_token |
required | Ping token from a provisioned GoodIssues heartbeat |
check |
required | Health check strategy (see below) |
interval_ms |
30_000 |
Check interval (min: 30,000) |
timeout_ms |
1_000 |
Timeout for call/MFA checks |
metadata |
%{} |
Extra metadata merged into ping payloads |
| Strategy | Description | Success Condition |
|---|---|---|
:process_alive |
Resolves the target and checks Process.alive?/1 |
Process exists and is alive |
{:call, message} |
Calls the target with GenServer.call/3 |
Reply is :ok or {:ok, _} |
{:mfa, module, function, args} |
Calls apply(module, function, args) in a supervised task |
Returns :ok, true, or {:ok, _} |
For {:call, :health}, the target GenServer should implement:
def handle_call(:health, _from, state) do
{:reply, :ok, state}
endThe built-in SystemMonitor already implements this contract when enabled with at least one active or pending metric.
Note: Heartbeat monitors only start when global
enabledistrue. The reporter does not create heartbeat resources — you must provision heartbeats in GoodIssues and use the returnedping_tokenas theheartbeat_tokenvalue.
# config/runtime.exs
config :good_issues_reporter,
api_url: System.fetch_env!("GOODISSUES_API_URL"),
api_key: System.fetch_env!("GOODISSUES_API_KEY"),
project_id: System.fetch_env!("GOODISSUES_PROJECT_ID"),
enabled: true,
async: true
config :good_issues_reporter,
otel: [
enabled: true,
service_name: "my-app",
ecto_repo_prefix: [:my_app, :repo]
]
# OTel SDK config — host app responsibility
config :opentelemetry,
resource: [{"service.name", "my-app"}],
traces_exporter: {:otlp, [
protocol: :http_protobuf,
endpoints: [System.fetch_env!("GOODISSUES_API_URL") <> "/api/v1/otlp/traces"],
headers: [{"authorization", "Bearer " <> System.fetch_env!("GOODISSUES_API_KEY")}]
]}
config :good_issues_reporter,
system_monitor: [
enabled: true,
poll_interval_ms: 30_000,
severity: "warning",
reopen_window_hours: 24,
memory_total_mb: 512,
process_count: 100_000,
cpu_percent: 90,
disk_percent: 85
]
config :good_issues_reporter,
network_scanner: [
enabled: true,
strategy: :deny,
ports: [3306, 5432, 6379]
]
config :good_issues_reporter,
heartbeat_monitors: [
[
enabled: true,
name: :system_monitor,
target: GoodIssuesReporter.SystemMonitor,
heartbeat_token: System.fetch_env!("GOODISSUES_SYSTEM_MONITOR_PING_TOKEN"),
check: {:call, :health}
]
]GoodIssuesReporter (Supervisor)
├── Task.Supervisor # Async API call execution
├── TelemetryHandler # error_tracker event listener + trace_id capture
├── OTel auto-instrumentation # Phoenix, LiveView, Ecto (when enabled)
├── SystemMonitor # BEAM/OS metric polling → incidents
├── HeartbeatMonitor.Watcher(s) # Per-monitor health check + ping
└── NetworkScanner # Local TCP port scanning → incidents
error_trackeremits[:error_tracker, :error, :new]telemetry eventTelemetryHandlerreceives the event and passes metadata toClient.create_error/2Mapper.to_error/2transforms error-tracker metadata into the GoodIssues API payloadClientdelegates toGoodissuesEx.create_error/2(generated API client)
- The host app configures the
:opentelemetrySDK inconfig/runtime.exs - When OTel is enabled, auto-instrumentation attaches to Phoenix, LiveView, and Ecto telemetry
- The OTel SDK collects spans and exports them via OTLP HTTP to the configured endpoint
trace_idfrom active spans is injected into error and incident payloads for correlation
SystemMonitorGenServer polls metrics on a configurable interval- When a metric exceeds its configured threshold, an incident is created via
IncidentReporter.create_incident/2 - Server-side fingerprint deduplication prevents duplicate incidents — repeated breaches add occurrences
- When a metric recovers below threshold, the incident is automatically resolved via
IncidentReporter.resolve_incident/2
NetworkScannerGenServer scans configured ports on a configurable interval- For deny strategy: any deny-listed port found open triggers an incident
- For allow strategy: any open port not in the allow list triggers an incident
- When a previously-violating port closes, the incident is automatically resolved
| Error-Tracker Field | API Field | Notes |
|---|---|---|
error.kind |
kind |
Direct pass-through |
error.reason |
reason |
Direct pass-through |
error.source_line |
source_line |
Defaults to "-" if nil |
error.source_function |
source_function |
Defaults to "-" if nil |
error.fingerprint |
fingerprint |
Used for occurrence aggregation |
| config | project_id |
From configuration |
occurrence.context |
context |
Map (max 50 keys) |
occurrence.breadcrumbs |
breadcrumbs |
Array of strings (max 100) |
occurrence.stacktrace |
stacktrace |
%{lines: [...]} structure (max 100 lines) |
| OTel span context | trace_id |
32-char hex, omitted when no span active |
Context values are filtered before sending (common sensitive keys are redacted recursively; additional keys can be configured via :filter_parameters). See the Context Sanitization section for details and host-app responsibilities.
The cooldown_ms option has been removed from the :system_monitor configuration. Deduplication of threshold breach reports is now handled server-side by the GoodIssues Incident API via fingerprint matching.
Migration: Remove any cooldown_ms entries from your system_monitor config. Use severity and reopen_window_hours to control incident behavior instead.
This library depends on goodissues_ex for the generated GoodIssues API client. It requires support for the following operations:
- Error reporting (
create_error) - Incident creation and resolution (
create_incident/resolve_incident) - Heartbeat pings (
ping_heartbeat_pingsuccess + failure variants)
The version currently pinned in mix.exs is known to be compatible:
{:goodissues_ex,
github: "agoodway/goodissues_ex", ref: "d88dec847ad4caebb2b4406ea112cf60c14d34ae"}When depending on good_issues_reporter, ensure any goodissues_ex override matches or exceeds this revision.
MIT