File: http.rb

package info (click to toggle)
ruby-sentry-ruby-core 5.3.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 412 kB
  • sloc: ruby: 3,030; makefile: 8; sh: 4
file content (115 lines) | stat: -rw-r--r-- 3,388 bytes parent folder | download
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
# frozen_string_literal: true

require "net/http"

module Sentry
  # @api private
  module Net
    module HTTP
      OP_NAME = "http.client"
      BREADCRUMB_CATEGORY = "net.http"

      # To explain how the entire thing works, we need to know how the original Net::HTTP#request works
      # Here's part of its definition. As you can see, it usually calls itself inside a #start block
      #
      # ```
      # def request(req, body = nil, &block)
      #   unless started?
      #     start {
      #       req['connection'] ||= 'close'
      #       return request(req, body, &block) # <- request will be called for the second time from the first call
      #     }
      #   end
      #   # .....
      # end
      # ```
      #
      # So we're only instrumenting request when `Net::HTTP` is already started
      def request(req, body = nil, &block)
        return super unless started?

        sentry_span = start_sentry_span
        set_sentry_trace_header(req, sentry_span)

        super.tap do |res|
          record_sentry_breadcrumb(req, res)
          record_sentry_span(req, res, sentry_span)
        end
      end

      private

      def set_sentry_trace_header(req, sentry_span)
        return unless sentry_span

        trace = Sentry.get_current_client.generate_sentry_trace(sentry_span)
        req[SENTRY_TRACE_HEADER_NAME] = trace if trace
      end

      def record_sentry_breadcrumb(req, res)
        return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
        return if from_sentry_sdk?

        request_info = extract_request_info(req)

        crumb = Sentry::Breadcrumb.new(
          level: :info,
          category: BREADCRUMB_CATEGORY,
          type: :info,
          data: {
            status: res.code.to_i,
            **request_info
          }
        )
        Sentry.add_breadcrumb(crumb)
      end

      def record_sentry_span(req, res, sentry_span)
        return unless Sentry.initialized? && sentry_span

        request_info = extract_request_info(req)
        sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
        sentry_span.set_data(:status, res.code.to_i)
        finish_sentry_span(sentry_span)
      end

      def start_sentry_span
        return unless Sentry.initialized? && span = Sentry.get_current_scope.get_span
        return if from_sentry_sdk?
        return if span.sampled == false

        span.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
      end

      def finish_sentry_span(sentry_span)
        return unless Sentry.initialized? && sentry_span

        sentry_span.set_timestamp(Sentry.utc_now.to_f)
      end

      def from_sentry_sdk?
        dsn = Sentry.configuration.dsn
        dsn && dsn.host == self.address
      end

      def extract_request_info(req)
        uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}")
        url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s

        result = { method: req.method, url: url }

        if Sentry.configuration.send_default_pii
          result[:url] = result[:url] + "?#{uri.query}"
          result[:body] = req.body
        end

        result
      end
    end
  end
end

Sentry.register_patch do
  patch = Sentry::Net::HTTP
  Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
end