risenexa-tracking

Ruby SDK for tracking user events with Risenexa. Report user registrations and conversions to your Risenexa dashboard with a single method call.

  • Zero runtime dependencies — uses Ruby's built-in Net::HTTP
  • Idempotent retries — UUID v4 anchor prevents duplicate counts on retry
  • Typed errors — distinct error classes for auth, validation, rate limiting, and network failures
  • Two configuration modes — global (Rails initializer) or per-instance (multi-startup)

For the full behavioral specification, see SDK-SPEC.md.


Installation

Add this line to your application's Gemfile:

gem "risenexa-tracking"

Then run:

bundle install

Or install directly:

gem install risenexa-tracking

Requirements: Ruby >= 3.1.0


Quick Start

Create an initializer at config/initializers/risenexa_tracking.rb:

Risenexa::Tracking.configure do |config|
  config.api_key      = ENV["RISENEXA_API_KEY"]        # Bearer token with tracking:write scope
  config.startup_slug = ENV["RISENEXA_STARTUP_SLUG"]  # Your startup's slug
end

Then call the module-level convenience methods anywhere in your app:

# After a user signs up
Risenexa::Tracking.track_registration(user_id: current_user.id.to_s)

# After a user starts paying
Risenexa::Tracking.track_conversion(user_id: current_user.id.to_s)

Per-Instance Configuration

Useful when managing multiple startups from a single Rails app:

startup_a = Risenexa::Tracking::Client.new(
  api_key:      "rxt_live_abc123",
  startup_slug: "startup-alpha"
)

startup_b = Risenexa::Tracking::Client.new(
  api_key:      "rxt_live_xyz789",
  startup_slug: "startup-beta"
)

startup_a.track_registration(user_id: "usr_1")
startup_b.track_conversion(user_id: "usr_2")

Per-instance clients are fully independent — no shared state with global configuration or other instances.


API Reference

Convenience Methods

track_registration(user_id:, **opts)

Sends event_type: "user_registered", action: "add".

result = client.track_registration(user_id: "usr_123")
result.success?    # => true
result.status_code # => 202
result.event_id    # => "550e8400-e29b-41d4-a716-446655440000"
result.body        # => {"status" => "accepted"}

track_conversion(user_id:, **opts)

Sends event_type: "user_converted", action: "add".

result = client.track_conversion(user_id: "usr_123")

Both methods accept optional keyword arguments:

Option Type Default Description
event_id: String auto UUID v4 Idempotency anchor
occurred_at: String server time ISO 8601 UTC timestamp
metadata: Hash {} Arbitrary JSONB payload
action: String "add" "add" or "remove"

Low-Level track Method

Use when you need full control over all HTTP contract fields:

client.track(
  event_type:  "user_registered",
  user_id:     "usr_123",
  event_id:    "550e8400-e29b-41d4-a716-446655440000",  # optional
  occurred_at: "2026-04-01T12:00:00Z",                  # optional
  metadata:    { plan: "pro", source: "google" },        # optional
  action:      "add"                                     # optional
)

Configuration Options

Option Required Default Type Description
api_key Yes — String Bearer token with tracking:write scope
startup_slug Yes — String Slug identifying the startup
base_url No "https://app.risenexa.com" String API base URL (override for staging)
timeout No 2000 Integer Per-request timeout in milliseconds
max_retries No 3 Integer Maximum retry attempts (0 disables retries)

Error Handling

All error classes inherit from Risenexa::Tracking::Error < StandardError.

begin
  client.track_registration(user_id: "usr_123")
rescue Risenexa::Tracking::AuthenticationError => e
  # HTTP 401 — missing or invalid API key; check your api_key
  puts e.message
rescue Risenexa::Tracking::AuthorizationError => e
  # HTTP 403 — token lacks tracking:write scope
  puts e.message
rescue Risenexa::Tracking::StartupNotFoundError => e
  # HTTP 404 — wrong startup_slug; check your configuration
  puts e.message
rescue Risenexa::Tracking::ValidationError => e
  # HTTP 422 — invalid payload
  puts e.errors.inspect  # Array of error strings from response body
rescue Risenexa::Tracking::RateLimitError => e
  # All retries exhausted on 429
  puts "Retry after #{e.retry_after} seconds"
rescue Risenexa::Tracking::MaxRetriesExceededError => e
  # All retries exhausted on 500/502/503
  puts "Last response: #{e.last_response.code}"
rescue Risenexa::Tracking::ConnectionError => e
  # All retries exhausted due to network/timeout failures
  puts "Transport error: #{e.cause.class}"
rescue Risenexa::Tracking::ConfigurationError => e
  # Missing api_key or startup_slug — caught before any HTTP request
  puts e.message
end

Error Classes

Class HTTP Status Notes
AuthenticationError 401 Never retried
AuthorizationError 403 Never retried
StartupNotFoundError 404 Never retried
ValidationError 422 Never retried; .errors contains array from response
RateLimitError 429 (exhausted) .retry_after has seconds from Retry-After header
MaxRetriesExceededError 5xx (exhausted) .last_response has the last Net::HTTPResponse
ConnectionError timeout/refused .cause has the underlying transport exception
ConfigurationError — Raised before HTTP; missing api_key or startup_slug

Retry Behavior

The SDK retries on transient errors (429, 500, 502, 503, timeouts, connection failures) with exponential backoff and ±20% jitter:

Before Retry Base Delay Jitter Range Actual Range
Retry 1 1.0s ±0.2s [0.8s, 1.2s]
Retry 2 2.0s ±0.4s [1.6s, 2.4s]
Retry 3 4.0s ±0.8s [3.2s, 4.8s]

Idempotency: The SDK generates a UUID v4 event_id before the first attempt and reuses it across all retries. This ensures the server counts the event exactly once even if the SDK retries.

Disabling retries:

client = Risenexa::Tracking::Client.new(
  api_key:      "rxt_live_abc123",
  startup_slug: "my-startup",
  max_retries:  0  # raise immediately on any retryable error
)

Development

git clone https://github.com/envixo/risenexa-tracking-rb.git
cd risenexa-tracking-rb
bundle install
bundle exec rspec

The test suite includes all 35 compliance tests from the SDK specification.


License

MIT License. See LICENSE.txt.