OmniTrack-rb 🎯

Gem Version Ruby Rails License: MIT

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:

  1. Captures gclid, fbclid, ttclid, UTMs, IP, and User-Agent from every request
  2. Makes them available at Omnitrack::Context.current throughout the request
  3. 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

  1. Fork the repository
  2. Create your branch (git checkout -b feature/my-adapter)
  3. Write tests
  4. Run bundle exec rspec — all green
  5. Open a Pull Request

License

MIT. See LICENSE.txt.