Class: TrackRelay::Subscribers::Base
- Inherits:
-
Object
- Object
- TrackRelay::Subscribers::Base
- 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
Class Method Summary collapse
-
.coerce_event_set(value) ⇒ Set<Symbol>?
Coerce a filter input (Array<Symbol|String>, Set, single Symbol, or nil) into a
Set<Symbol>ornil. -
.filter(only: nil, except: nil) ⇒ void
Class-level DSL for declaring an event-name filter.
-
.synchronous! ⇒ Boolean
Mark this subclass as synchronous.
Instance Method Summary collapse
-
#deliver(payload) ⇒ void
Implement in subclasses to receive an EventPayload.
-
#except_events ⇒ Set<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 Base.filter. -
#handle(payload) ⇒ nil, StandardError
Route
payloadto the sync or async path. -
#only_events ⇒ Set<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 Base.filter. -
#prepare(payload) ⇒ EventPayload?
Notification-time hook, called by #handle after the event-name filter and BEFORE the sync/async branch — i.e.
-
#safe_deliver(payload) ⇒ nil, StandardError
Wrap #deliver with the per-subscriber rescue.
-
#set_filter_overrides!(only: nil, except: nil) ⇒ self
Set per-instance
only:/except:filter overrides on this subscriber.
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).
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.
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.
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.
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_events ⇒ Set<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.
221 222 223 224 225 226 227 |
# File 'lib/track_relay/subscribers/base.rb', line 221 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.
109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/track_relay/subscribers/base.rb', line 109 def handle(payload) return nil if filtered?(payload.name.to_sym) payload = prepare(payload) return nil if payload.nil? 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_events ⇒ Set<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.
208 209 210 211 212 213 214 |
# File 'lib/track_relay/subscribers/base.rb', line 208 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 |
#prepare(payload) ⇒ EventPayload?
Notification-time hook, called by #handle after the event-name filter and BEFORE the sync/async branch — i.e. while the request (Current.request) is still in scope, which is gone by the time an async DeliveryJob runs.
Subclasses override this to gate delivery (return nil to drop
the event silently — no job, no deliver) or to swap in an
enriched copy of the payload. The same payload object fans
out to every subscriber, so implementations must never mutate
payload — build a new EventPayload instead.
136 137 138 |
# File 'lib/track_relay/subscribers/base.rb', line 136 def prepare(payload) payload 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 StandardErrors — 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 StandardErrors
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.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/track_relay/subscribers/base.rb', line 165 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).
193 194 195 196 197 198 199 200 201 |
# File 'lib/track_relay/subscribers/base.rb', line 193 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 |