summaryrefslogtreecommitdiff
path: root/app/services/error_tracking/collect_error_service.rb
blob: 50508c9810a9d2ca0ff022e54d66a4440a106011 (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
# frozen_string_literal: true

module ErrorTracking
  class CollectErrorService < ::BaseService
    include Gitlab::Utils::StrongMemoize

    def execute
      # Error is a way to group events based on common data like name or cause
      # of exception. We need to keep a sane balance here between taking too little
      # and too much data into group logic.
      error = project.error_tracking_errors.report_error(
        name: exception['type'], # Example: ActionView::MissingTemplate
        description: exception['value'], # Example: Missing template posts/show in...
        actor: actor, # Example: PostsController#show
        platform: event['platform'], # Example: ruby
        timestamp: timestamp
      )

      # The payload field contains all the data on error including stacktrace in jsonb.
      # Together with occurred_at these are 2 main attributes that we need to save here.
      error.events.create!(
        environment: event['environment'],
        description: exception['value'],
        level: event['level'],
        occurred_at: timestamp,
        payload: event
      )
    end

    private

    def event
      @event ||= format_event(params[:event])
    end

    def format_event(event)
      # Some SDK send exception payload as Array. For exmple Go lang SDK.
      # We need to convert it to hash format we expect.
      if event['exception'].is_a?(Array)
        exception = event['exception']
        event['exception'] = { 'values' => exception }
      end

      event
    end

    def exception
      strong_memoize(:exception) do
        # Find the first exception that has a stacktrace since the first
        # exception may not provide adequate context (e.g. in the Go SDK).
        entries = event['exception']['values']
        entries.find { |x| x.key?('stacktrace') } || entries.first
      end
    end

    def stacktrace_frames
      strong_memoize(:stacktrace_frames) do
        exception.dig('stacktrace', 'frames')
      end
    end

    def actor
      return event['transaction'] if event['transaction']

      # Some SDKs do not have a transaction attribute.
      # So we build it by combining function name and module name from
      # the last item in stacktrace.
      return unless stacktrace_frames.present?

      last_line = stacktrace_frames.last

      "#{last_line['function']}(#{last_line['module']})"
    end

    def timestamp
      return @timestamp if @timestamp

      @timestamp = (event['timestamp'] || Time.zone.now)

      # Some SDK send timestamp in numeric format like '1630945472.13'.
      if @timestamp.to_s =~ /\A\d+(\.\d+)?\z/
        @timestamp = Time.zone.at(@timestamp.to_f)
      end

      @timestamp
    end
  end
end