risenexa_tracking
Elixir/Hex SDK for tracking user registrations and conversions with Risenexa.
Track when users sign up or convert to paying customers in your Phoenix application with a single function call.
Installation
Add risenexa_tracking to your mix.exs dependencies:
def deps do
[
{:risenexa_tracking, "~> 0.1.0"}
]
endThen run:
mix deps.getConfiguration
Global Configuration (recommended)
Configure once in config/config.exs (or config/runtime.exs for runtime secrets):
config :risenexa_tracking,
api_key: "rxt_live_abc123",
startup_slug: "my-phoenix-startup"Then call module-level functions anywhere in your app:
RisenexaTracking.track_registration(user_id: user.id)
RisenexaTracking.track_conversion(user_id: user.id)Per-Instance Configuration
For multi-startup use cases or dynamic credentials:
client = RisenexaTracking.client(
api_key: "rxt_live_abc123",
startup_slug: "my-startup"
)
RisenexaTracking.track_registration(client, user_id: user.id)
RisenexaTracking.track_conversion(client, user_id: user.id)Per-instance clients are fully independent — no shared state with global config or other instances.
Configuration Options
| Option | Required | Default | Description |
|---|---|---|---|
api_key | Yes | — |
Bearer token with tracking:write scope |
startup_slug | Yes | — | Slug identifying the startup for all events |
base_url | No | "https://app.risenexa.com" | API base URL (override for staging) |
timeout | No | 2000 | Per-request timeout in milliseconds |
max_retries | No | 3 | Max retry attempts (0 disables retries) |
Usage
Track User Registration
Call when a user creates an account:
case RisenexaTracking.track_registration(user_id: to_string(user.id)) do
{:ok, result} ->
Logger.info("Tracked registration: #{result.event_id}")
{:error, %{type: :configuration_error, message: msg}} ->
Logger.error("SDK not configured: #{msg}")
{:error, %{type: :authentication_error}} ->
Logger.error("Invalid API key")
{:error, error} ->
Logger.warning("Tracking failed: #{inspect(error)}")
endTrack User Conversion
Call when a user becomes a paying customer:
{:ok, result} = RisenexaTracking.track_conversion(user_id: to_string(user.id))
Low-Level track/2
For full control over all event fields:
{:ok, result} = RisenexaTracking.track(client,
event_type: "user_registered",
user_id: "usr_123",
event_id: "550e8400-e29b-41d4-a716-446655440000", # optional — auto-generated if absent
occurred_at: "2026-04-01T12:00:00Z", # optional — server uses current time
metadata: %{plan: "pro", source: "web"}, # optional — stored as JSONB
action: "add" # optional — "add" or "remove"
)Removing Events
Use action: "remove" to decrement counters (e.g., when a user cancels):
# Decrement paying customer count on subscription cancellation
RisenexaTracking.track(client,
event_type: "user_converted",
user_id: to_string(user.id),
action: "remove"
)Return Values
All functions return tagged tuples — no exceptions are raised.
Success
{:ok, %{
status_code: 202,
event_id: "550e8400-e29b-41d4-a716-446655440000", # UUID sent in request
body: %{"status" => "accepted"}
}}Errors
# Missing configuration
{:error, %{type: :configuration_error, message: "api_key is required..."}}
# Invalid token
{:error, %{type: :authentication_error, status_code: 401, message: "Invalid token"}}
# Token lacks tracking:write scope
{:error, %{type: :authorization_error, status_code: 403, message: "Unauthorized scope"}}
# Wrong startup slug
{:error, %{type: :startup_not_found, status_code: 404, message: "startup not found"}}
# Invalid event data
{:error, %{
type: :validation_error,
status_code: 422,
message: "Validation failed",
errors: ["event_type must be user_registered or user_converted"]
}}
# Rate limited and retries exhausted
{:error, %{
type: :rate_limited,
status_code: 429,
message: "Request failed after 3 retries",
retry_after: 45 # seconds from Retry-After header
}}
# Server errors and retries exhausted
{:error, %{
type: :max_retries_exceeded,
last_status_code: 503,
attempts: 4,
message: "Request failed after 3 retries"
}}
# Transport failure (timeout, connection refused)
{:error, %{type: :connection_error, status_code: nil, message: "connection refused"}}Retry Behavior
The SDK automatically retries on transient errors with exponential backoff:
| Condition | Retryable | Behavior |
|---|---|---|
429 Too Many Requests | Yes |
Honors Retry-After header; falls back to backoff |
500 Internal Server Error | Yes | Exponential backoff |
502 Bad Gateway | Yes | Exponential backoff |
503 Service Unavailable | Yes | Exponential backoff |
| Timeout / Connection error | Yes | Exponential backoff |
401 Unauthorized | No |
Returns {:error, ...} immediately |
403 Forbidden | No |
Returns {:error, ...} immediately |
404 Not Found | No |
Returns {:error, ...} immediately |
422 Unprocessable Entity | No |
Returns {:error, ...} immediately |
Backoff Formula
delay = min(1.0 * 2^attempt, 30.0) * (1 + jitter) # jitter: ±20%| Retry | Base Delay | Range |
|---|---|---|
| 1st | 1.0s | [0.8s, 1.2s] |
| 2nd | 2.0s | [1.6s, 2.4s] |
| 3rd | 4.0s | [3.2s, 4.8s] |
Idempotency
The SDK generates a UUID v4 event_id before the first HTTP attempt and reuses it on all retries. This ensures the Risenexa server counts each logical event exactly once, even if network conditions cause multiple delivery attempts.
Phoenix Integration Example
# lib/my_app/accounts.ex
defmodule MyApp.Accounts do
def create_user(attrs) do
with {:ok, user} <- %User{} |> User.changeset(attrs) |> Repo.insert() do
# Track registration asynchronously to avoid blocking the request
Task.start(fn ->
case RisenexaTracking.track_registration(user_id: to_string(user.id)) do
{:ok, _result} -> :ok
{:error, error} -> Logger.warning("Risenexa tracking failed: #{inspect(error)}")
end
end)
{:ok, user}
end
end
endLicense
MIT License. Copyright (c) 2026 Patrick Espake.
See LICENSE for full text.