Class: TrackRelay::Subscribers::Ga4MeasurementProtocol

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

Overview

GA4 Measurement Protocol server-side subscriber (REQ-08, REQ-11).

POSTs each event to Google Analytics 4 via the Measurement Protocol endpoint:

POST https://www.google-analytics.com/mp/collect
  ?measurement_id=G-XXXXXXXXXX
  &api_secret=<secret>
Content-Type: application/json
Body: { client_id:, user_id:, timestamp_micros:, events: [{name:, params:}] }

Async by default — ‘#deliver` runs inside a DeliveryJob (loadbalanced via ActiveJob). Hosts that need inline delivery (e.g. unit-test determinism, low-traffic ingestion) can call Base.synchronous! per REQ-11.

## Configuration

Read from Configuration at *delivery time* (NOT at class-body load time) so credentials lambdas / late-bound configs work:

- `config.ga4_measurement_id` — `G-XXXXXXXXXX`
- `config.ga4_api_secret`     — per-stream MP secret
- `config.ga4_use_eu_endpoint` — when `true`, post to
  `https://region1.google-analytics.com/mp/collect`

When ‘ga4_measurement_id` or `ga4_api_secret` is `nil`, `#deliver` emits a single `Rails.logger.warn` and returns without raising —gem-loaded-but-not-configured apps must not crash.

## Error contract

‘#deliver` raises:

- {TrackRelay::DeliveryRetriableError} on transient failures
  (HTTP 5xx, `Net::OpenTimeout`, `Net::ReadTimeout`,
  `Errno::ECONNREFUSED`, `SocketError`). Mapped to
  `retry_on` in {DeliveryJob}.
- {TrackRelay::DeliveryDiscardableError} on permanent failures
  (HTTP 4xx — defensive: GA4 returns 2xx in practice even on
  malformed payloads). Mapped to `discard_on` in {DeliveryJob}.
- {TrackRelay::Ga4ConstraintError} when call-time payload
  validation fails AND `config.raise_on_validation_error` is
  `true` (dev/test). In production (`raise_on_validation_error
  = false`) the violation is logged via `Rails.logger.warn` and
  the POST is skipped.

## Why typed retriable/discardable exceptions?

ActiveJob’s ‘retry_on`/`discard_on` macros only fire on raised exceptions, not returned values. Base#safe_deliver normally rescues any `StandardError` and returns it (the REQ-23 blanket-rescue contract), so a 5xx retry would never reach the job’s retry policy. Base therefore carves these two exception classes out of the rescue: it re-raises them so DeliveryJob can map them to ‘retry_on`/`discard_on`. See `test/unit/subscribers/base_retry_passthrough_test.rb`.

Constant Summary collapse

ENDPOINT_URL =

GA4 production endpoint (US/global region).

"https://www.google-analytics.com/mp/collect"
ENDPOINT_URL_EU =

GA4 EU-region endpoint, selected via ‘config.ga4_use_eu_endpoint = true`.

"https://region1.google-analytics.com/mp/collect"
OPEN_TIMEOUT_SECONDS =

Net::HTTP open timeout (TCP connect).

5
READ_TIMEOUT_SECONDS =

Net::HTTP read timeout (response wait).

10
RESERVED_PARAM_PREFIXES =

GA4-reserved param-name prefixes. Per Scout §2 / REQ-27, params starting with these prefixes must not be sent — GA4 silently drops them.

%w[firebase_ ga_ google_].freeze

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.

POST ‘payload` to the GA4 Measurement Protocol endpoint.

See class docs for the full configuration / error contract.

Parameters:

Raises:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/track_relay/subscribers/ga4_measurement_protocol.rb', line 98

def deliver(payload)
  config = TrackRelay.config
  measurement_id = config.ga4_measurement_id
  api_secret = config.ga4_api_secret

  if measurement_id.nil? || api_secret.nil?
    warn_missing_credentials(measurement_id, api_secret)
    return
  end

  return unless validate_ga4_payload!(payload)

  post_to_ga4(payload, measurement_id, api_secret, config.ga4_use_eu_endpoint)
end