Class: TrackRelay::Subscribers::Base

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

Overview

Base class for all track_relay subscribers.

Each subscriber receives an EventPayload via #handle, which routes to one of two paths:

- **sync** — `safe_deliver(payload)` is invoked inline on the
  calling thread. Used when the subclass calls {.synchronous!}
  or when {Configuration#force_synchronous} is `true`.
- **async** — {DeliveryJob} is enqueued with the subscriber's
  class name and the payload's serialized form. The job calls
  `safe_deliver` on a fresh instance when it eventually runs.

**Error contract (locked in 01-CONTEXT.md, 01-05-PLAN.md):** ‘safe_deliver` returns `nil` on success or the StandardError on failure — it NEVER re-raises inline. The Dispatcher collects those returns during fan-out and re-raises the first one afterwards, but only when Configuration#swallow_subscriber_errors is `false`. This guarantees that one bad subscriber never blocks peers, while still letting dev/test surface failures loudly once everyone has had their chance.

Direct Known Subclasses

Ahoy, Ga4MeasurementProtocol, Logger, Test

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.coerce_event_set(value) ⇒ Set<Symbol>?

Coerce a filter input (Array<Symbol|String>, Set, single Symbol, or nil) into a ‘Set<Symbol>` or `nil`. Internal helper shared by the class-level filter DSL and the per-instance override path (#set_filter_overrides!, used by TrackRelay.subscribe).

Parameters:

  • value (Array, Set, Symbol, String, nil)

Returns:

  • (Set<Symbol>, nil)


81
82
83
84
# File 'lib/track_relay/subscribers/base.rb', line 81

def self.coerce_event_set(value)
  return nil if value.nil?
  Set.new(Array(value).map(&:to_sym))
end

.filter(only: nil, except: nil) ⇒ void

This method returns an undefined value.

Class-level DSL for declaring an event-name filter.

class MySubscriber < TrackRelay::Subscribers::Base
  filter only: %i[purchase sign_up]
end

‘only:` and `except:` are mutually exclusive in spirit but not enforced as such — if both are set, `only:` wins (an event must be in the allow-list AND not in the deny-list to pass). Pass `nil` to clear a previously set filter.

Parameters:

  • only (Array<Symbol, String>, nil) (defaults to: nil)

    allow-list; if non-nil, only events whose name is in this set are delivered.

  • except (Array<Symbol, String>, nil) (defaults to: nil)

    deny-list; events in this set are dropped.



69
70
71
72
# File 'lib/track_relay/subscribers/base.rb', line 69

def self.filter(only: nil, except: nil)
  self.only_events = coerce_event_set(only)
  self.except_events = coerce_event_set(except)
end

.synchronous!Boolean

Mark this subclass as synchronous. Calls to #handle will run ‘safe_deliver` inline rather than enqueueing a DeliveryJob.

Returns:

  • (Boolean)

    ‘true`



49
50
51
# File 'lib/track_relay/subscribers/base.rb', line 49

def self.synchronous!
  self.synchronous = true
end

Instance Method Details

#deliver(payload) ⇒ void

This method returns an undefined value.

Implement in subclasses to receive an EventPayload.

Parameters:

Raises:

  • (NotImplementedError)

    when not overridden



91
92
93
# File 'lib/track_relay/subscribers/base.rb', line 91

def deliver(payload)
  raise NotImplementedError, "#{self.class.name} must implement #deliver(payload)"
end

#except_eventsSet<Symbol>?

Read the effective ‘except:` filter for this instance — the singleton override (set by TrackRelay.subscribe) when present, otherwise the class-level default declared via filter.

Returns:

  • (Set<Symbol>, nil)


201
202
203
204
205
206
207
# File 'lib/track_relay/subscribers/base.rb', line 201

def except_events
  if singleton_class.instance_variable_defined?(:@except_events_override)
    singleton_class.instance_variable_get(:@except_events_override)
  else
    self.class.except_events
  end
end

#handle(payload) ⇒ nil, StandardError

Route ‘payload` to the sync or async path.

Returns: ‘nil` on success, the StandardError on a sync failure, or `nil` on the async path (the job runs later — its eventual failure mode is handled inside DeliveryJob#perform).

**Filter gate (Plan 02-01):** if ‘only_events` / `except_events` exclude `payload.name`, return `nil` immediately — BEFORE the sync/async branch and BEFORE `safe_deliver`’s rescue boundary. A filtered event with a buggy ‘#deliver` therefore neither runs nor logs.

Parameters:

Returns:

  • (nil, StandardError)


109
110
111
112
113
114
115
116
117
118
# File 'lib/track_relay/subscribers/base.rb', line 109

def handle(payload)
  return nil if filtered?(payload.name.to_sym)

  if self.class.synchronous || TrackRelay.config.force_synchronous
    safe_deliver(payload)
  else
    DeliveryJob.perform_later(self.class.name, payload.to_h)
    nil
  end
end

#only_eventsSet<Symbol>?

Read the effective ‘only:` filter for this instance — the singleton override (set by TrackRelay.subscribe) when present, otherwise the class-level default declared via filter.

Returns:

  • (Set<Symbol>, nil)


188
189
190
191
192
193
194
# File 'lib/track_relay/subscribers/base.rb', line 188

def only_events
  if singleton_class.instance_variable_defined?(:@only_events_override)
    singleton_class.instance_variable_get(:@only_events_override)
  else
    self.class.only_events
  end
end

#safe_deliver(payload) ⇒ nil, StandardError

Wrap #deliver with the per-subscriber rescue.

Returns ‘nil` on success or the StandardError on failure. ALWAYS logs the failure (via `Rails.logger.error`) when running under Rails. NEVER re-raises arbitrary `StandardError`s — the Dispatcher (or DeliveryJob) makes the loudness decision based on Configuration#swallow_subscriber_errors.

**REQ-23 carve-out (Plan 02-04):** DeliveryRetriableError and DeliveryDiscardableError are RE-RAISED unconditionally — even when ‘swallow_subscriber_errors = true` (the production default). ActiveJob’s ‘retry_on` / `discard_on` macros only fire on raised exceptions; without this carve-out the GA4 retry/discard policy in DeliveryJob would be silently broken in production because `safe_deliver` would catch the exception, return it as a value, and the job would think delivery succeeded.

The carve-out is INTENTIONALLY NARROW: arbitrary ‘StandardError`s still flow through the existing log-and-return path — REQ-23’s blanket-rescue contract is preserved for everything outside of the typed retry/discard exception classes.

Parameters:

Returns:

  • (nil, StandardError)


145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/track_relay/subscribers/base.rb', line 145

def safe_deliver(payload)
  deliver(payload)
  nil
rescue TrackRelay::DeliveryRetriableError, TrackRelay::DeliveryDiscardableError
  # Carve-out: ActiveJob retry_on/discard_on must see these.
  # Do NOT log here — the DeliveryJob's retry path will log on
  # eventual exhaustion, and the discard path is an intentional
  # drop. Logging on every retry attempt would spam the log with
  # transient blips that resolve on retry.
  raise
rescue => e
  log_failure(e)
  e
end

#set_filter_overrides!(only: nil, except: nil) ⇒ self

Set per-instance ‘only:` / `except:` filter overrides on this subscriber. Used by TrackRelay.subscribe so a single subscriber class can be registered multiple times with different filters.

Each non-nil argument is coerced via coerce_event_set and stored on the instance’s singleton class so it does not bleed across instances or mutate the class-level defaults declared via filter. Passing ‘nil` for either argument leaves that override untouched (the instance falls through to the class default).

Parameters:

  • only (Array<Symbol, String>, Set, nil) (defaults to: nil)
  • except (Array<Symbol, String>, Set, nil) (defaults to: nil)

Returns:

  • (self)


173
174
175
176
177
178
179
180
181
# File 'lib/track_relay/subscribers/base.rb', line 173

def set_filter_overrides!(only: nil, except: nil)
  unless only.nil?
    singleton_class.instance_variable_set(:@only_events_override, self.class.coerce_event_set(only))
  end
  unless except.nil?
    singleton_class.instance_variable_set(:@except_events_override, self.class.coerce_event_set(except))
  end
  self
end