summaryrefslogtreecommitdiff
path: root/lib/gitlab/error_tracking.rb
blob: 876a1cbb183d73b33e140bfa18d604aa6e4cd337 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# frozen_string_literal: true

module Gitlab
  module ErrorTracking
    # Exceptions in this group will receive custom Sentry fingerprinting
    CUSTOM_FINGERPRINTING = %w[
      Acme::Client::Error::BadNonce
      Acme::Client::Error::NotFound
      Acme::Client::Error::RateLimited
      Acme::Client::Error::Timeout
      Acme::Client::Error::UnsupportedOperation
      ActiveRecord::ConnectionTimeoutError
      Gitlab::RequestContext::RequestDeadlineExceeded
      GRPC::DeadlineExceeded
      JIRA::HTTPError
      Rack::Timeout::RequestTimeoutException
    ].freeze

    PROCESSORS = [
      ::Gitlab::ErrorTracking::Processor::SidekiqProcessor,
      ::Gitlab::ErrorTracking::Processor::GrpcErrorProcessor,
      ::Gitlab::ErrorTracking::Processor::ContextPayloadProcessor,
      ::Gitlab::ErrorTracking::Processor::SanitizeErrorMessageProcessor,
      # IMPORTANT: this processor must stay at the bottom, right before
      # sending the event to Sentry.
      ::Gitlab::ErrorTracking::Processor::SanitizerProcessor
    ].freeze

    class << self
      def configure(&block)
        configure_raven(&block)
        configure_sentry(&block)
      end

      def configure_raven
        Raven.configure do |config|
          config.dsn = sentry_dsn
          config.release = Gitlab.revision
          config.current_environment = Gitlab.config.sentry.environment

          # Sanitize fields based on those sanitized from Rails.
          config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)

          # Sanitize authentication headers
          config.sanitize_http_headers = %w[Authorization Private-Token]
          config.before_send = method(:before_send_raven)

          yield config if block_given?
        end
      end

      def configure_sentry
        Sentry.init do |config|
          config.dsn = new_sentry_dsn
          config.release = Gitlab.revision
          config.environment = new_sentry_environment
          config.before_send = method(:before_send_sentry)
          config.background_worker_threads = 0
          config.send_default_pii = true

          yield config if block_given?
        end
      end

      # This should be used when you want to passthrough exception handling:
      # rescue and raise to be catched in upper layers of the application.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def track_and_raise_exception(exception, extra = {})
        process_exception(exception, extra: extra)

        raise exception
      end

      # This can be used for investigating exceptions that can be recovered from in
      # code. The exception will still be raised in development and test
      # environments.
      #
      # That way we can track down these exceptions with as much information as we
      # need to resolve them.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      #
      # Provide an issue URL for follow up.
      # as `issue_url: 'http://gitlab.com/gitlab-org/gitlab/issues/111'`
      def track_and_raise_for_dev_exception(exception, extra = {})
        process_exception(exception, extra: extra)

        raise exception if should_raise_for_dev?
      end

      # This should be used when you only want to track the exception.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def track_exception(exception, extra = {})
        process_exception(exception, extra: extra)
      end

      # This should be used when you only want to log the exception,
      # but not send it to Sentry.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def log_exception(exception, extra = {})
        process_exception(exception, extra: extra, trackers: [Logger])
      end

      # This should be used when you want to log the exception and passthrough
      # exception handling: rescue and raise to be catched in upper layers of
      # the application.
      #
      # If the exception implements the method `sentry_extra_data` and that method
      # returns a Hash, then the return value of that method will be merged into
      # `extra`. Exceptions can use this mechanism to provide structured data
      # to sentry in addition to their message and back-trace.
      def log_and_raise_exception(exception, extra = {})
        process_exception(exception, extra: extra, trackers: [Logger])

        raise exception
      end

      private

      def before_send_raven(event, hint)
        return unless Feature.enabled?(:enable_old_sentry_integration)

        before_send(event, hint)
      end

      def before_send_sentry(event, hint)
        return unless Feature.enabled?(:enable_new_sentry_integration)

        before_send(event, hint)
      end

      def before_send(event, hint)
        # Don't report Sidekiq retry errors to Sentry
        return if hint[:exception].is_a?(Gitlab::SidekiqMiddleware::RetryError)

        inject_context_for_exception(event, hint[:exception])
        custom_fingerprinting(event, hint[:exception])

        PROCESSORS.reduce(event) do |processed_event, processor|
          processor.call(processed_event)
        end
      end

      def process_exception(exception, extra:, trackers: default_trackers)
        context_payload = Gitlab::ErrorTracking::ContextPayloadGenerator.generate(exception, extra)

        trackers.each do |tracker|
          tracker.capture_exception(exception, **context_payload)
        end
      end

      def default_trackers
        [].tap do |destinations|
          destinations << Raven if Raven.configuration.server
          # There is a possibility that this method is called before Sentry is
          # configured. Since Sentry 4.0, some methods of Sentry are forwarded to
          # to `nil`, hence we have to check the client as well.
          destinations << ::Sentry if ::Sentry.get_current_client && ::Sentry.configuration.dsn
          destinations << Logger
        end
      end

      def sentry_dsn
        return unless sentry_configurable?
        return unless Gitlab.config.sentry.enabled

        Gitlab.config.sentry.dsn
      end

      def new_sentry_dsn
        return unless sentry_configurable?
        return unless Gitlab::CurrentSettings.respond_to?(:sentry_enabled?)
        return unless Gitlab::CurrentSettings.sentry_enabled?

        Gitlab::CurrentSettings.sentry_dsn
      end

      def new_sentry_environment
        return unless Gitlab::CurrentSettings.respond_to?(:sentry_environment)

        Gitlab::CurrentSettings.sentry_environment
      end

      def sentry_configurable?
        Rails.env.production? || Rails.env.development?
      end

      def should_raise_for_dev?
        Rails.env.development? || Rails.env.test?
      end

      # Group common, mostly non-actionable exceptions by type and message,
      # rather than cause
      def custom_fingerprinting(event, ex)
        return event unless CUSTOM_FINGERPRINTING.include?(ex.class.name)

        event.fingerprint = [ex.class.name, ex.message]
      end

      def inject_context_for_exception(event, ex)
        sql = Gitlab::ExceptionLogFormatter.find_sql(ex)

        event.extra[:sql] = sql if sql
      end
    end
  end
end