Class: TrackRelay::EventPayload

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

Overview

Runtime data for a single event in flight.

An EventPayload pairs a EventDefinition (the schema) with the raw values supplied at the call site (‘TrackRelay.track(:foo, …)`), plus the request-derived context (user, visitor_token, client_id, etc.) and a timestamp. It is the value passed through the `track_relay.event` ActiveSupport::Notifications instrumentation.

Two constructors:

  • ‘EventPayload.new(definition:, params:, …)` — typed payload; #validate! coerces and enforces the definition’s schema.

  • ‘EventPayload.untyped(name:, params:, …)` — untyped payload (no matching definition); #validate! is a no-op so consumers can still ship the raw event for the untyped-events linter (see Plan 04).

‘validate!` is destructive: it replaces `@params` with the coerced hash so subscribers see post-coercion values. Errors raise ValidationError naming the offending key.

Constant Summary collapse

BOOLEAN_TRUE_VALUES =

Sentinel values that the strict boolean coercion accepts. Anything else raises ValidationError.

[true, "true", 1].freeze
BOOLEAN_FALSE_VALUES =
[false, "false", 0].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(definition:, params:, context: {}, timestamp: Time.now) ⇒ EventPayload

Returns a new instance of EventPayload.

Parameters:

  • definition (EventDefinition)

    schema for typed events

  • params (Hash{Symbol => Object})

    raw param values

  • context (Hash) (defaults to: {})

    request-derived metadata (user, visitor, request, etc.)

  • timestamp (Time) (defaults to: Time.now)

    event timestamp; defaults to now



39
40
41
42
43
44
45
# File 'lib/track_relay/event_payload.rb', line 39

def initialize(definition:, params:, context: {}, timestamp: Time.now)
  @definition = definition
  @params = params
  @context = context
  @timestamp = timestamp
  @untyped_name = nil
end

Instance Attribute Details

#contextObject (readonly)

Returns the value of attribute context.



32
33
34
# File 'lib/track_relay/event_payload.rb', line 32

def context
  @context
end

#definitionObject (readonly)

Returns the value of attribute definition.



32
33
34
# File 'lib/track_relay/event_payload.rb', line 32

def definition
  @definition
end

#paramsObject (readonly)

Returns the value of attribute params.



32
33
34
# File 'lib/track_relay/event_payload.rb', line 32

def params
  @params
end

#timestampObject (readonly)

Returns the value of attribute timestamp.



32
33
34
# File 'lib/track_relay/event_payload.rb', line 32

def timestamp
  @timestamp
end

Class Method Details

.from_h(hash) ⇒ EventPayload

Reconstruct an EventPayload from a serialized #to_h form. Used by Plan 05’s DeliveryJob to rehydrate a payload after ActiveJob has serialized arguments through the queue adapter.

The reconstructed payload is always untyped (‘definition: nil`): validation happened at track time on the calling thread, so the async delivery path doesn’t need the schema. ActiveJob’s argument serialization round-trips Symbols as Strings under the standard adapter, so ‘String#to_sym` is applied defensively to the name.

Parameters:

  • hash (Hash)

    result of #to_h (Symbol- or String-keyed)

Returns:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/track_relay/event_payload.rb', line 75

def self.from_h(hash)
  payload = allocate
  payload.instance_variable_set(:@definition, nil)
  payload.instance_variable_set(:@params, hash[:params] || hash["params"] || {})
  payload.instance_variable_set(:@context, hash[:context] || hash["context"] || {})
  payload.instance_variable_set(
    :@timestamp,
    Time.iso8601(hash[:timestamp] || hash["timestamp"])
  )
  payload.instance_variable_set(
    :@untyped_name,
    (hash[:name] || hash["name"]).to_sym
  )
  payload
end

.untyped(name:, params:, context: {}, timestamp: Time.now) ⇒ EventPayload

Build an untyped payload — no definition, no schema enforcement. Used when a host application calls ‘TrackRelay.track(:unknown, …)` while `untyped_events_allowed = true`.

Parameters:

  • name (Symbol)

    event name (stored separately because there is no definition to read it from)

  • params (Hash)
  • context (Hash) (defaults to: {})
  • timestamp (Time) (defaults to: Time.now)

Returns:



57
58
59
60
61
# File 'lib/track_relay/event_payload.rb', line 57

def self.untyped(name:, params:, context: {}, timestamp: Time.now)
  payload = new(definition: nil, params: params, context: context, timestamp: timestamp)
  payload.instance_variable_set(:@untyped_name, name)
  payload
end

Instance Method Details

#nameSymbol

Returns event name (from definition or untyped store).

Returns:

  • (Symbol)

    event name (from definition or untyped store)



92
93
94
# File 'lib/track_relay/event_payload.rb', line 92

def name
  @definition ? @definition.name : @untyped_name
end

#to_hHash

Serialize to a Hash suitable for ActiveJob arguments / JSON encoding. Used by the DeliveryJob in Plan 05.

Returns:

  • (Hash)


157
158
159
160
161
162
163
164
# File 'lib/track_relay/event_payload.rb', line 157

def to_h
  {
    name: name,
    params: @params,
    context: @context,
    timestamp: @timestamp.respond_to?(:iso8601) ? @timestamp.iso8601 : @timestamp.to_s
  }
end

#untyped?Boolean

Returns whether this payload was built without a matching catalog definition.

Returns:

  • (Boolean)

    whether this payload was built without a matching catalog definition



98
99
100
# File 'lib/track_relay/event_payload.rb', line 98

def untyped?
  @definition.nil?
end

#validate!Hash{Symbol => Object}

Coerce and validate ‘@params` against `@definition.params`. Mutates `@params` to the coerced hash.

For each schema entry the order of operations is:

  1. Apply ‘sanitize` callable if present (raw -> sanitized).

  2. Check ‘required` against post-sanitize value.

  3. Coerce to ‘type`.

  4. Apply ‘max` (length for strings, value for numbers).

  5. Apply ‘in` inclusion list.

  6. Apply ‘format` regex (strings only).

After per-key processing, any incoming param not declared in the schema raises ValidationError.

No-op for untyped payloads.

Returns:

  • (Hash{Symbol => Object})

    coerced params

Raises:



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/track_relay/event_payload.rb', line 120

def validate!
  return @params if untyped?

  coerced = {}

  @definition.params.each do |key, schema|
    raw_value = @params[key]
    raw_value = schema.sanitize.call(raw_value) if schema.sanitize&.respond_to?(:call) && @params.key?(key)

    if raw_value.nil?
      if schema.required
        raise ValidationError, "Param #{key.inspect} is required but was not provided"
      end
      next
    end

    coerced_value = coerce(key, schema.type, raw_value)
    check_max!(key, schema.max, coerced_value) if schema.max
    check_in!(key, schema.in, coerced_value) if schema.in
    check_format!(key, schema.format, coerced_value) if schema.format

    coerced[key] = coerced_value
  end

  extras = @params.keys - @definition.params.keys
  unless extras.empty?
    raise ValidationError,
      "Unexpected param(s) #{extras.map(&:inspect).join(", ")} not declared on event #{@definition.name.inspect}"
  end

  @params = coerced
end