OmniTrack-rb 🎯
Production-grade, modular tracking and conversion system for Ruby on Rails.
OmniTrack dispatches events to multiple ad/analytics platforms through a clean adapter pattern — works identically in full-stack and API-only Rails apps, never crashes your main application, and emits structured JSON logs to a dedicated file.
Runtime: Ruby ≥ 2.7 (including 2.7.8) and Rails ≥ 6. No Ruby 3-only syntax in the gem runtime.
Practical usage (file placement + method examples): USAGE.md
Add the gem to any Rails app without boot errors (ENV, jobs, checklists): AI_GEM_SETUP.md
ENV variable names (copy into host .env.example): .env.example
Supported Platforms
| Platform | Adapter | Server-side API | JS Pixel |
|---|---|---|---|
| Google Ads | :google_ads |
✅ Click Conversion Upload | via omnitrack_tags |
| Google Analytics 4 | :google_analytics |
✅ Measurement Protocol | via omnitrack_tags |
| Meta (Facebook/Instagram) | :meta |
✅ Conversions API | via omnitrack_tags |
| TikTok Ads | :tiktok |
✅ Events API | via omnitrack_tags |
| Snapchat Ads | :snapchat |
✅ Conversions API | via omnitrack_tags |
| Custom | Your class | ✅ Extend Base |
— |
Installation
Add to your Gemfile:
gem "omnitrack-rb"
Run the installer:
bundle install
rails generate omnitrack:install
This creates config/initializers/omnitrack.rb with a fully commented template.
Configuration
# config/initializers/omnitrack.rb
Omnitrack.configure do |config|
# :auto — detects api_only? at runtime (recommended)
# :frontend — inject JS tags + use cookies
# :backend — server-side only (Conversions APIs)
# :hybrid — both simultaneously
config.mode = :auto
config.adapters = {
google_ads: {
enabled: ENV["GOOGLE_ADS_ENABLED"] == "true",
customer_id: ENV["GOOGLE_ADS_CUSTOMER_ID"],
developer_token: ENV["GOOGLE_ADS_DEVELOPER_TOKEN"],
access_token: ENV["GOOGLE_ADS_ACCESS_TOKEN"],
conversion_action_id: ENV["GOOGLE_ADS_CONVERSION_ACTION_ID"]
},
google_analytics: {
enabled: ENV["GA4_ENABLED"] == "true",
measurement_id: ENV["GA4_MEASUREMENT_ID"],
api_secret: ENV["GA4_API_SECRET"]
},
meta: {
enabled: ENV["META_ENABLED"] == "true",
pixel_id: ENV["META_PIXEL_ID"],
access_token: ENV["META_ACCESS_TOKEN"]
},
tiktok: {
enabled: ENV["TIKTOK_ENABLED"] == "true",
pixel_id: ENV["TIKTOK_PIXEL_ID"],
access_token: ENV["TIKTOK_ACCESS_TOKEN"]
},
snapchat: {
enabled: ENV["SNAPCHAT_ENABLED"] == "true",
pixel_id: ENV["SNAPCHAT_PIXEL_ID"],
access_token: ENV["SNAPCHAT_ACCESS_TOKEN"]
}
}
config.auto_capture = true # capture gclid, fbclid, ttclid, UTMs automatically
config.log_level = :info
config.async = Rails.env.production? # use ActiveJob in production
config.timeout = 5
config.max_retries = 3
# Optional: error hook for Sentry / Honeybadger
config.on_error = ->(error, adapter) {
Sentry.capture_exception(error, tags: { adapter: adapter.name })
}
end
Usage
Full-stack Rails (views + controllers)
Layout — inject all platform pixels into <head>:
<!DOCTYPE html>
<html>
<head>
<%= omnitrack_tags %>
</head>
Controller — track events server-side (Omnitrack::Controller is auto-included via the Railtie; you can also include Omnitrack::Controller explicitly in ApplicationController if needed):
class OrdersController < ApplicationController
def create
@order = Order.create!(order_params)
# Fires through all enabled server-side adapters
omnitrack_event("purchase",
value: @order.total,
currency: "USD",
order_id: @order.id)
redirect_to @order
end
end
View — emit a JS event tag on the confirmation page:
<%= omnitrack_event_tag("purchase", value: @order.total, currency: "USD") %>
API-only Rails
No JS involved — everything is server-side:
class Api::V1::OrdersController < ApplicationController
def create
@order = Order.create!(order_params)
Omnitrack.track("purchase",
value: @order.total,
currency: "USD",
order_id: @order.id,
# Pass click IDs from your mobile client via request body or headers:
fbclid: params[:fbclid],
gclid: params[:gclid])
render json: @order
end
end
Service objects / background jobs
class PurchaseTracker
def call(order)
Omnitrack.track("purchase",
value: order.total,
currency: order.currency,
order_id: order.id,
email: order.user.email)
end
end
Identify a user
Omnitrack.identify(
email: current_user.email,
phone: current_user.phone,
first_name: current_user.first_name,
last_name: current_user.last_name,
external_id: current_user.id.to_s
)
PII (email, phone, name) is SHA-256 hashed before being sent to any platform.
Working with Results
Every call returns an Omnitrack::MultiResult:
result = Omnitrack.track("purchase", value: 99.0)
result.success? # => true if ALL adapters succeeded
result.any_failure? # => true if at least one adapter failed
result.failures # => [Omnitrack::Result, ...]
result.successes # => [Omnitrack::Result, ...]
result.each do |r|
puts "#{r.adapter}: #{r.status}" # => google_ads: success
end
Building a Custom Adapter
# app/tracking/my_platform_adapter.rb
class MyPlatformAdapter < Omnitrack::Adapters::Base
self.adapter_name = :my_platform
def track_event(event_name, payload = {})
safe_execute(event_name) do
response = http_post(
"https://api.myplatform.com/events",
body: {
event: event_name,
data: payload,
api_key: config[:api_key]
}
)
Omnitrack::Result.success(adapter: name, data: JSON.parse(response.body))
end
end
def track_conversion(data = {})
track_event("conversion", data)
end
def identify_user(user_data = {})
# store or send user data
Omnitrack::Result.success(adapter: name)
end
private
def validate_config
require_config!(:api_key, hint: "MyPlatform API key")
end
end
Register in config:
Omnitrack.configure do |config|
config.adapters[:my_platform] = {
enabled: true,
api_key: ENV["MY_PLATFORM_API_KEY"]
}
end
# Register the class
Omnitrack::Registry.register(MyPlatformAdapter)
Logging
OmniTrack writes structured JSON to log/omnitrack.log (separate from Rails' main log):
{"timestamp":"2024-01-15T10:23:45.123Z","message":"adapter.request","adapter":"meta","event":"purchase","payload":{"value":99.0}}
{"timestamp":"2024-01-15T10:23:45.891Z","message":"adapter.response","adapter":"meta","event":"purchase","status":"success","duration_ms":768.4}
{"timestamp":"2024-01-15T10:23:46.002Z","message":"adapter.error","adapter":"google_ads","event":"purchase","error":"Omnitrack::AdapterError","message":"HTTP 401 from googleads.googleapis.com"}
Configure log level:
config.log_level = :debug # :debug | :info | :warn | :error | :none
Rotate logs:
rails omnitrack:rotate_log
Rake Tasks
rails omnitrack:status # Show configured adapters and status
rails omnitrack:test_event # Send a test event through all enabled adapters
rails omnitrack:test_event[purchase] # Send a named test event
rails omnitrack:rotate_log # Rotate the log file
Async / Background Jobs
When config.async = true, tracking calls are dispatched via ActiveJob:
config.async = true
config.queue_name = :omnitrack
Ensure your queue adapter is configured in production:
# config/environments/production.rb
config.active_job.queue_adapter = :sidekiq
Middleware
Omnitrack::Middleware::RequestTracker is automatically inserted into your Rack stack. It:
- Captures
gclid,fbclid,ttclid, UTMs, IP, and User-Agent from every request - Makes them available at
Omnitrack::Context.currentthroughout the request - Clears thread-local state after each response (no cross-request leakage)
Thread Safety
- All shared state uses
Mutex-guarded access - Context is stored in
Thread.current— safe under Puma/Sidekiq - Adapters are instantiated per-dispatch (stateless between requests)
Development
bundle install
bundle exec rspec
bundle exec rubocop
Contributing
- Fork the repository
- Create your branch (
git checkout -b feature/my-adapter) - Write tests
- Run
bundle exec rspec— all green - Open a Pull Request
License
MIT. See LICENSE.txt.