Module: DebugBundle::BeforeSend

Defined in:
lib/debugbundle/before_send.rb

Constant Summary collapse

REQUIRED_PAYLOAD_FIELDS =
{
  'backend_exception' => %w[name message stack handled request response runtime],
  'request_event' => %w[method path query headers response_status duration_ms],
  'log_event' => %w[level message attributes],
  'frontend_breadcrumb' => %w[breadcrumb_type data],
  'frontend_exception' => %w[name message stack],
  'deploy_metadata' => %w[commit_sha version branch environment deployed_at],
  'error_suppressed' => %w[fingerprint suppressed_count window_seconds first_seen last_seen],
  'probe_event' => %w[label data activation_id probe_label_pattern]
}.freeze
ALLOWED_PAYLOAD_FIELDS =
{
  'backend_exception' => %w[name message stack handled request response runtime probe_data],
  'request_event' => %w[
    method path query headers body response_status duration_ms route_template
    response_headers response_body device
  ],
  'log_event' => %w[level message attributes device],
  'frontend_breadcrumb' => %w[breadcrumb_type route data device],
  'frontend_exception' => %w[
    name message stack route browser breadcrumbs device browser_event
    rejection_reason dom_context probe_data
  ],
  'deploy_metadata' => %w[commit_sha version branch environment deployed_at],
  'error_suppressed' => %w[fingerprint suppressed_count window_seconds first_seen last_seen device],
  'probe_event' => %w[label data activation_id probe_label_pattern device]
}.freeze
ROOT_FIELDS =
%w[
  schema_version event_id event_type project_token project_id sdk_name sdk_version
  service occurred_at correlation context payload
].freeze
UUID_PATTERN =
/\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i

Class Method Summary collapse

Class Method Details

.apply(event, hook) ⇒ Object



41
42
43
44
45
46
47
48
49
50
# File 'lib/debugbundle/before_send.rb', line 41

def apply(event, hook)
  return event unless hook

  result = hook.call(Marshal.load(Marshal.dump(event)))
  return nil if result.nil?

  valid?(result) ? result : event
rescue StandardError
  event
end

.non_empty_strings?(payload, *fields) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
# File 'lib/debugbundle/before_send.rb', line 142

def non_empty_strings?(payload, *fields)
  fields.all? { |field| payload[field].is_a?(String) && !payload[field].strip.empty? }
end

.non_negative_integer?(value) ⇒ Boolean

Returns:

  • (Boolean)


154
155
156
# File 'lib/debugbundle/before_send.rb', line 154

def non_negative_integer?(value)
  value.is_a?(Integer) && value >= 0
end

.non_negative_number?(value) ⇒ Boolean

Returns:

  • (Boolean)


150
151
152
# File 'lib/debugbundle/before_send.rb', line 150

def non_negative_number?(value)
  value.is_a?(Numeric) && value.finite? && value >= 0
end

.nullable_uuid?(value) ⇒ Boolean

Returns:

  • (Boolean)


168
169
170
# File 'lib/debugbundle/before_send.rb', line 168

def nullable_uuid?(value)
  value.nil? || (value.is_a?(String) && UUID_PATTERN.match?(value))
end

.optional_hash?(payload, field) ⇒ Boolean

Returns:

  • (Boolean)


146
147
148
# File 'lib/debugbundle/before_send.rb', line 146

def optional_hash?(payload, field)
  !payload.key?(field) || payload[field].is_a?(Hash)
end

.positive_integer?(value) ⇒ Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/debugbundle/before_send.rb', line 158

def positive_integer?(value)
  value.is_a?(Integer) && value.positive?
end

.timestamp?(value) ⇒ Boolean

Returns:

  • (Boolean)


162
163
164
165
166
# File 'lib/debugbundle/before_send.rb', line 162

def timestamp?(value)
  value.is_a?(String) && Time.iso8601(value)
rescue ArgumentError
  false
end

.valid?(event) ⇒ Boolean

Returns:

  • (Boolean)


52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/debugbundle/before_send.rb', line 52

def valid?(event)
  return false unless event.is_a?(Hash)
  return false unless (event.keys - ROOT_FIELDS).empty?
  return false unless %w[schema_version event_id event_type occurred_at sdk_name sdk_version].all? do |field|
    event[field].is_a?(String) && !event[field].empty?
  end
  return false unless UUID_PATTERN.match?(event['event_id'])

  Time.iso8601(event['occurred_at'])
  service = event['service']
  payload = event['payload']
  fields = REQUIRED_PAYLOAD_FIELDS[event['event_type']]
  allowed_fields = ALLOWED_PAYLOAD_FIELDS[event['event_type']]
  service.is_a?(Hash) &&
    service['name'].is_a?(String) && !service['name'].empty? &&
    service['environment'].is_a?(String) && !service['environment'].empty? &&
    payload.is_a?(Hash) &&
    fields &&
    allowed_fields &&
    (payload.keys - allowed_fields).empty? &&
    fields.all? { |field| payload.key?(field) } &&
    valid_payload_shape?(event['event_type'], payload)
rescue ArgumentError
  false
end

.valid_backend_exception?(payload) ⇒ Boolean

Returns:

  • (Boolean)


101
102
103
104
105
106
# File 'lib/debugbundle/before_send.rb', line 101

def valid_backend_exception?(payload)
  non_empty_strings?(payload, 'name', 'message', 'stack') &&
    [true, false].include?(payload['handled']) &&
    %w[request response runtime].all? { |field| payload[field].is_a?(Hash) } &&
    optional_hash?(payload, 'probe_data')
end

.valid_deploy_metadata?(payload) ⇒ Boolean

Returns:

  • (Boolean)


123
124
125
126
# File 'lib/debugbundle/before_send.rb', line 123

def valid_deploy_metadata?(payload)
  non_empty_strings?(payload, 'commit_sha', 'version', 'branch', 'environment') &&
    timestamp?(payload['deployed_at'])
end

.valid_error_suppressed?(payload) ⇒ Boolean

Returns:

  • (Boolean)


128
129
130
131
132
133
134
# File 'lib/debugbundle/before_send.rb', line 128

def valid_error_suppressed?(payload)
  non_empty_strings?(payload, 'fingerprint') &&
    non_negative_integer?(payload['suppressed_count']) &&
    positive_integer?(payload['window_seconds']) &&
    timestamp?(payload['first_seen']) &&
    timestamp?(payload['last_seen'])
end

.valid_frontend_exception?(payload) ⇒ Boolean

Returns:

  • (Boolean)


117
118
119
120
121
# File 'lib/debugbundle/before_send.rb', line 117

def valid_frontend_exception?(payload)
  non_empty_strings?(payload, 'name', 'message', 'stack') &&
    (!payload.key?('breadcrumbs') || payload['breadcrumbs'].is_a?(Array)) &&
    optional_hash?(payload, 'probe_data')
end

.valid_payload_shape?(event_type, payload) ⇒ Boolean

Returns:

  • (Boolean)


78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/debugbundle/before_send.rb', line 78

def valid_payload_shape?(event_type, payload)
  case event_type
  when 'backend_exception'
    valid_backend_exception?(payload)
  when 'request_event'
    valid_request_event?(payload)
  when 'log_event'
    non_empty_strings?(payload, 'level', 'message') && payload['attributes'].is_a?(Hash)
  when 'frontend_breadcrumb'
    non_empty_strings?(payload, 'breadcrumb_type') && payload['data'].is_a?(Hash)
  when 'frontend_exception'
    valid_frontend_exception?(payload)
  when 'deploy_metadata'
    valid_deploy_metadata?(payload)
  when 'error_suppressed'
    valid_error_suppressed?(payload)
  when 'probe_event'
    valid_probe_event?(payload)
  else
    false
  end
end

.valid_probe_event?(payload) ⇒ Boolean

Returns:

  • (Boolean)


136
137
138
139
140
# File 'lib/debugbundle/before_send.rb', line 136

def valid_probe_event?(payload)
  non_empty_strings?(payload, 'label', 'probe_label_pattern') &&
    payload['data'].is_a?(Hash) &&
    nullable_uuid?(payload['activation_id'])
end

.valid_request_event?(payload) ⇒ Boolean

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
# File 'lib/debugbundle/before_send.rb', line 108

def valid_request_event?(payload)
  non_empty_strings?(payload, 'method', 'path') &&
    payload['query'].is_a?(Hash) &&
    payload['headers'].is_a?(Hash) &&
    non_negative_number?(payload['response_status']) &&
    non_negative_number?(payload['duration_ms']) &&
    optional_hash?(payload, 'response_headers')
end