Helios Tracker

A Rails engine that exposes your app's user, visit, and email-suppression data as JSON API endpoints for Helios Crusader to consume.

Helios pulls from these endpoints daily to track signups, logins, click-throughs, and blocked emails — automatically moving targets from cold to warm to hot based on engagement.

Requirements

Installation

Add to your Gemfile:

gem "helios-tracker"

Then run:

bundle install
rails generate helios_tracker:install
rails db:migrate

The install generator will:

  1. Install and configure Universal Track Manager (visit tracking)
  2. Add include UniversalTrackManagerConcern to your ApplicationController
  3. Create a blocked_emails migration
  4. Generate config/initializers/helios_tracker.rb
  5. Mount the engine in your routes

If Universal Track Manager is already installed, the generator will detect it and skip that step.

Generator Options

# Specify your User model class (default: "User")
rails generate helios_tracker:install --user-class=Account

# Skip UTM installation if already set up
rails generate helios_tracker:install --skip-utm

Configuration

Set your API key in your environment (.env, credentials, or server config):

HELIOS_TRACKER_API_KEY=your_secret_key_here

Then edit config/initializers/helios_tracker.rb:

HeliosTracker.configure do |config|
  # ---- Users ----

  config.user_class_name = "User"

  # Return users updated since query_start.
  # Receives (query_start, params). Must return an ActiveRecord relation.
  config.user_scope = ->(query_start, params) {
    User.where.not(email: "")
        .where("updated_at > ?", query_start)
  }

  # Map API field names to model attributes or lambdas.
  # :email is required. All others are optional.
  config.user_fields = {
    email:                      :email,
    created_at:                 :created_at,
    source_ip:                  :source_ip,
    login_count:                :login_count,
    accounts_owned_count:       ->(user) { user.accounts_owned_count },
    free_accounts_count:        ->(user) { user.free_accounts_count },
    unsubscribe_nonce:          :unsubscribe_nonce,
    first_unconfirmed_visit_id: :first_unconfirmed_visit_id,
    login_attempt_count:        :login_attempt_count,
    app_open_days_count:        :app_open_days_count,
  }

  # ---- Visits ----

  config.visit_scope = ->(query_start, params) {
    UniversalTrackManager::Visit.where.not(hmid: nil)
        .where("updated_at > ?", query_start)
  }

  # :hmid is required.
  config.visit_fields = {
    hmid:                  :hmid,
    visited_download_page: ->(visit) { visit.visited_download_page? },
  }

  # ---- Blocked Emails ----

  config.blocked_email_class_name = "BlockedEmail"

  config.blocked_email_scope = ->(query_start, params) {
    BlockedEmail.where("created_at > ?", query_start)
  }

  config.blocked_email_fields = {
    email:  :email,
    source: :source,
  }

end

Field Mapping

Each field in user_fields, visit_fields, and blocked_email_fields can be either:

  • A symbol — calls that method on the record (e.g., :email calls user.email)
  • A lambda — receives the record and returns a value (e.g., ->(user) { user.accounts.count })

Fields you don't include in the hash are silently omitted from the API response.

Scoping with Lambdas

The user_scope, visit_scope, and blocked_email_scope lambdas receive two arguments:

Argument Description
query_start A YYYY-MM-DD date string sent by Helios
params The full request params (includes domain_name, etc.)

Use these to filter your records however your app requires:

# Multi-domain example
config.user_scope = ->(query_start, params) {
  domain = Domain.find_by(name: params[:domain_name])
  User.where.not(email: "")
      .where(domain_id: domain&.id)
      .where("updated_at > ?", query_start)
}

API Endpoints

All endpoints require authentication via HELIOS_TRACKER_API_KEY, passed as either:

  • Header: Authorization: Bearer <API_KEY>
  • Query parameter: ?api_key=<API_KEY>

GET /api/all_users.json

Returns users updated since query_start.

Parameter Required Description
query_start yes YYYY-MM-DD — records from this date onward
domain_name no Domain filter (if your app is multi-domain)
api_key yes Shared API key

GET /api/all_visits.json

Returns visits with a non-null hmid since query_start.

Parameter Required Description
query_start yes YYYY-MM-DD — visits from this date onward
api_key yes Shared API key

GET /api/blocked_emails.json

Returns suppressed emails (bounces, unsubscribes) since query_start.

Parameter Required Description
query_start yes YYYY-MM-DD — blocks from this date onward
api_key yes Shared API key

How It Works

Helios calls your endpoints once per day (9:00 AM UTC). Each call includes a query_start date — your scopes should return records created or updated on or after that date.

Important for window-based fields: login_count, accounts_owned_count, and app_open_days_count should return only counts within the query_start..now window. Helios accumulates these across successive pulls. Returning lifetime totals will result in double-counting.

Helios handles duplicates gracefully (find-or-create logic), so returning the same records on a re-request is safe.

License

MIT License. See LICENSE for details.