Class: Webhookdb::Organization::ErrorHandler

Inherits:
Object
  • Object
show all
Includes:
Dbutil
Defined in:
lib/webhookdb/organization/error_handler.rb

Constant Summary collapse

DOCS_URL =
"https://docs.webhookdb.com/docs/integrating/error-handlers.html"
MAX_SENTRY_TAG_CHARS =
200

Constants included from Dbutil

Dbutil::MOCK_CONN

Instance Method Summary collapse

Methods included from Dbutil

borrow_conn, configured_connection_options, conn_opts, displaysafe_url, reduce_expr, take_conn

Instance Method Details

#_handle_sentry(payload) ⇒ Object

See develop.sentry.dev/sdk/data-model/envelopes/ for directly posting to Sentry. We do NOT want to use the SDK here, since we do not want to leak anything, and anyway, the runtime information is not important.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/webhookdb/organization/error_handler.rb', line 63

def _handle_sentry(payload)
  payload = payload.deep_symbolize_keys
  now = Time.now.utc
  # We can assume the url is the Sentry DSN
  u = URI(self.url)
  key = u.user
  project_id = u.path.delete("/")
  # Give some valid value for this, though it's not accurate.
  client = "sentry-ruby/5.22.1"
  ts = now.to_i
  # Auth headers are done by capturing an actual request. The docs aren't clear about their format.
  # It's possible using the DSN auth would also work but let's use this.
  headers = {
    "Content-Type" => "application/x-sentry-envelope",
    "X-Sentry-Auth" => "Sentry sentry_version=7, sentry_key=#{key}, sentry_client=#{client}, sentry_timestamp=#{ts}",
  }
  event_id = Uuidx.v4
  # The first line will be used as the title.
  message = "WebhookDB Error in #{payload.fetch(:service_integration_name)}\n\n#{payload.fetch(:message)}"
  # Let the caller set the level through query params
  level = URI.decode_www_form(u.query || "").to_h.fetch("level", "warning")

  # Split structured data into 'extra' (cannot be searched on, just shows in the UI)
  # and 'tags' (can be searched/faceted on, shows in the right bar).
  ignore_tags = Webhookdb::Message::Template.new.liquid_drops.keys.to_set
  tags, extra = payload.fetch(:details).partition do |k, v|
    # Non-strings are always tags
    next true unless v.is_a?(String)
    # Never tag on basic stuff that doesn't change ever
    next false if ignore_tags.include?(k)
    # Unstructured strings may include spaces or braces, and are not tags
    next false if v.include?(" ") || v.include?("{")
    # If it's a small string, treat it as a tag.
    v.size < MAX_SENTRY_TAG_CHARS
  end

  # Envelope structure is a multiline JSON file, I guess jsonl format
  envelopes = [
    {event_id:, sent_at: now.iso8601},
    {type: "event", content_type: "application/json"},
    {
      event_id:,
      timestamp: now.iso8601,
      platform: "ruby",
      level:,
      transaction: payload.fetch(:service_integration_table),
      release: "webhookdb@#{Webhookdb::RELEASE}",
      environment: Webhookdb::RACK_ENV,
      tags: tags.to_h,
      extra: extra.to_h,
      # We should use the same grouping for these messages as we would for emails
      fingerprint: [payload.fetch(:signature)],
      message: message,
    },
  ]
  body = envelopes.map(&:to_json).join("\n")
  store_url = URI(self.url)
  store_url.scheme = "https" if store_url.scheme == "sentry"
  store_url.user = nil
  store_url.password = nil
  store_url.path = "/api/#{project_id}/envelope/"
  store_url.query = ""
  Webhookdb::Http.post(
    store_url.to_s,
    body,
    headers:,
    timeout: Webhookdb::Organization::Alerting.error_handler_timeout,
    logger: self.logger,
  )
end

#before_createObject

:Sequel Hooks:



138
139
140
# File 'lib/webhookdb/organization/error_handler.rb', line 138

def before_create
  self[:opaque_id] ||= Webhookdb::Id.new_opaque_id("oeh")
end

#dispatch(payload) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/webhookdb/organization/error_handler.rb', line 39

def dispatch(payload)
  if self.sentry?
    self._handle_sentry(payload)
    return
  end

  Webhookdb::Http.post(
    self.url,
    payload,
    timeout: Webhookdb::Organization::Alerting.error_handler_timeout,
    logger: self.logger,
  )
end

#payload_for_template(tmpl) ⇒ Object

Parameters:



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/webhookdb/organization/error_handler.rb', line 16

def payload_for_template(tmpl)
  params = {
    error_type: tmpl.class.name.split("::").last.underscore,
    details: tmpl.liquid_drops.to_h,
    signature: tmpl.signature,
    organization_key: self.organization.key,
    service_integration_id: tmpl.service_integration.opaque_id,
    service_integration_name: tmpl.service_integration.service_name,
    service_integration_table: tmpl.service_integration.table_name,
  }
  recipient = Webhookdb::Message::Transport.for(:email).recipient(Webhookdb.support_email)
  message = Webhookdb::Message.render(tmpl, :email, recipient)
  message = message.to_s.strip
  message = Premailer.new(
    message,
    with_html_string: true,
    warn_level: Premailer::Warnings::SAFE,
  )
  message = message.to_plain_text
  params[:message] = message
  return params
end

#sentry?Boolean

Returns:

  • (Boolean)


53
54
55
56
# File 'lib/webhookdb/organization/error_handler.rb', line 53

def sentry?
  u = URI(self.url)
  return u.scheme == "sentry" || u.host&.end_with?("sentry.io")
end