Class: TrackRelay::Subscribers::Ahoy

Inherits:
Base
  • Object
show all
Defined in:
lib/track_relay/subscribers/ahoy.rb

Overview

Server-side Ahoy subscriber (REQ-09).

Routes catalog events through Ahoy’s only public tracking surface — ‘controller.ahoy.track(name, properties)` — by reading Current.controller on the synchronous request thread. When no controller is in scope (background job, rake task, console), or the host application does not include `Ahoy::Controller` in its `ApplicationController`, the subscriber logs a warning and skips delivery — it does NOT raise, does NOT enqueue a DeliveryJob, and does NOT touch any internal Ahoy API.

## Why synchronous (not async like GA4)

‘Ahoy::Tracker` is bound to the live request — it wraps the controller’s cookie jar and visit lifecycle. By the time a DeliveryJob runs, ‘Rails.application.executor.wrap` has already cleared ActiveSupport::CurrentAttributes, so `Current.controller` is nil and the live tracker instance is unreachable. Base.synchronous! therefore opts the subscriber into inline delivery on the request thread — `#handle` calls `safe_deliver(payload)` directly instead of enqueueing a job.

‘tracker.track` is an in-process database write (it calls `@store.track_event(data)` which does `event_model.create!` or equivalent), not a network call, so synchronous delivery adds negligible request overhead and matches how Ahoy itself works (Ahoy::Trackable wires `track` as an inline before_action helper).

## Why no ‘require “ahoy”`

The subscriber must load cleanly in non-Ahoy host applications (the gem ships with the file in ‘lib/track_relay.rb`’s require manifest unconditionally). Duck-typing via ‘controller.respond_to?(:ahoy, true)` handles the absent-Ahoy case without a top-level require — the same pattern used by ClientId::AhoyVisitor.

## Why no ‘Ahoy::Event.create!` / `Ahoy::Tracker.new`

‘Ahoy::Tracker` is the sole public tracking surface. Internal APIs (`Ahoy::Event.create!`, `Ahoy::Visit#track` — which does NOT exist on the visit model) are off-limits because they bypass Ahoy’s bot-exclusion store, user-method config, and visit association logic. The subscriber dispatches via ‘controller.ahoy.track(name, properties)` only.

## Skip conditions

All three skip paths log a ‘Rails.logger.warn` line of the form `[track_relay] Ahoy subscriber skipping delivery — <reason>` and `return` from #deliver. They MUST NOT raise — host applications that boot without Ahoy or call `TrackRelay.track` from a job must not crash.

  1. ‘Current.controller` is nil — job, rake, or console context.

  2. The controller does not ‘respond_to?(:ahoy, true)` —`Ahoy::Controller` was never `include`d.

  3. ‘controller.ahoy` returns nil — defensive coverage for a controller that has the helper but no live tracker yet.

Instance Method Summary collapse

Methods inherited from Base

coerce_event_set, #except_events, filter, #handle, #only_events, #safe_deliver, #set_filter_overrides!, synchronous!

Instance Method Details

#deliver(payload) ⇒ void

This method returns an undefined value.

Dispatch ‘payload` to `controller.ahoy.track` when a live controller with an Ahoy tracker is in scope. Skip-and-warn otherwise.

Parameters:



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/track_relay/subscribers/ahoy.rb', line 77

def deliver(payload)
  controller = TrackRelay::Current.controller

  unless controller&.respond_to?(:ahoy, true)
    log_skip("no controller or ahoy tracker in context")
    return
  end

  tracker = controller.ahoy

  unless tracker
    log_skip("controller.ahoy returned nil")
    return
  end

  tracker.track(payload.name.to_s, payload.params)
end