sentry-ruby 0.1.3 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -0
- data/Gemfile +6 -1
- data/README.md +88 -8
- data/Rakefile +3 -1
- data/lib/sentry-ruby.rb +38 -3
- data/lib/sentry/background_worker.rb +37 -0
- data/lib/sentry/benchmarks/benchmark_transport.rb +14 -0
- data/lib/sentry/breadcrumb.rb +7 -7
- data/lib/sentry/breadcrumb/sentry_logger.rb +10 -26
- data/lib/sentry/breadcrumb_buffer.rb +2 -5
- data/lib/sentry/client.rb +25 -7
- data/lib/sentry/configuration.rb +88 -87
- data/lib/sentry/dsn.rb +6 -3
- data/lib/sentry/event.rb +32 -26
- data/lib/sentry/hub.rb +13 -2
- data/lib/sentry/interfaces/request.rb +1 -31
- data/lib/sentry/rack.rb +2 -2
- data/lib/sentry/rack/{capture_exception.rb → capture_exceptions.rb} +20 -11
- data/lib/sentry/rack/interface.rb +22 -0
- data/lib/sentry/rake.rb +17 -0
- data/lib/sentry/scope.rb +27 -5
- data/lib/sentry/span.rb +132 -0
- data/lib/sentry/transaction.rb +157 -0
- data/lib/sentry/transaction_event.rb +29 -0
- data/lib/sentry/transport.rb +16 -24
- data/lib/sentry/transport/http_transport.rb +8 -8
- data/lib/sentry/utils/request_id.rb +16 -0
- data/lib/sentry/version.rb +1 -1
- data/sentry-ruby.gemspec +1 -0
- metadata +25 -4
- data/lib/sentry/transport/state.rb +0 -40
    
        data/lib/sentry/hub.rb
    CHANGED
    
    | @@ -67,6 +67,12 @@ module Sentry | |
| 67 67 | 
             
                  @stack.pop
         | 
| 68 68 | 
             
                end
         | 
| 69 69 |  | 
| 70 | 
            +
                def start_transaction(transaction: nil, **options)
         | 
| 71 | 
            +
                  transaction ||= Transaction.new(**options)
         | 
| 72 | 
            +
                  transaction.set_initial_sample_desicion
         | 
| 73 | 
            +
                  transaction
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 70 76 | 
             
                def capture_exception(exception, **options, &block)
         | 
| 71 77 | 
             
                  return unless current_client
         | 
| 72 78 |  | 
| @@ -74,12 +80,16 @@ module Sentry | |
| 74 80 |  | 
| 75 81 | 
             
                  return unless event
         | 
| 76 82 |  | 
| 83 | 
            +
                  options[:hint] ||= {}
         | 
| 84 | 
            +
                  options[:hint] = options[:hint].merge(exception: exception)
         | 
| 77 85 | 
             
                  capture_event(event, **options, &block)
         | 
| 78 86 | 
             
                end
         | 
| 79 87 |  | 
| 80 88 | 
             
                def capture_message(message, **options, &block)
         | 
| 81 89 | 
             
                  return unless current_client
         | 
| 82 90 |  | 
| 91 | 
            +
                  options[:hint] ||= {}
         | 
| 92 | 
            +
                  options[:hint] = options[:hint].merge(message: message)
         | 
| 83 93 | 
             
                  event = current_client.event_from_message(message)
         | 
| 84 94 | 
             
                  capture_event(event, **options, &block)
         | 
| 85 95 | 
             
                end
         | 
| @@ -87,6 +97,7 @@ module Sentry | |
| 87 97 | 
             
                def capture_event(event, **options, &block)
         | 
| 88 98 | 
             
                  return unless current_client
         | 
| 89 99 |  | 
| 100 | 
            +
                  hint = options.delete(:hint) || {}
         | 
| 90 101 | 
             
                  scope = current_scope.dup
         | 
| 91 102 |  | 
| 92 103 | 
             
                  if block
         | 
| @@ -97,9 +108,9 @@ module Sentry | |
| 97 108 | 
             
                    scope.update_from_options(**options)
         | 
| 98 109 | 
             
                  end
         | 
| 99 110 |  | 
| 100 | 
            -
                  event = current_client.capture_event(event, scope)
         | 
| 111 | 
            +
                  event = current_client.capture_event(event, scope, hint)
         | 
| 101 112 |  | 
| 102 | 
            -
                  @last_event_id = event. | 
| 113 | 
            +
                  @last_event_id = event.event_id
         | 
| 103 114 | 
             
                  event
         | 
| 104 115 | 
             
                end
         | 
| 105 116 |  | 
| @@ -1,5 +1,3 @@ | |
| 1 | 
            -
            require 'rack'
         | 
| 2 | 
            -
             | 
| 3 1 | 
             
            module Sentry
         | 
| 4 2 | 
             
              class RequestInterface < Interface
         | 
| 5 3 | 
             
                REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
         | 
| @@ -18,36 +16,8 @@ module Sentry | |
| 18 16 | 
             
                  self.cookies = nil
         | 
| 19 17 | 
             
                end
         | 
| 20 18 |  | 
| 21 | 
            -
                def from_rack(env_hash)
         | 
| 22 | 
            -
                  req = ::Rack::Request.new(env_hash)
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                  if Sentry.configuration.send_default_pii
         | 
| 25 | 
            -
                    self.data = read_data_from(req)
         | 
| 26 | 
            -
                    self.cookies = req.cookies
         | 
| 27 | 
            -
                  else
         | 
| 28 | 
            -
                    # need to completely wipe out ip addresses
         | 
| 29 | 
            -
                    IP_HEADERS.each { |h| env_hash.delete(h) }
         | 
| 30 | 
            -
                  end
         | 
| 31 | 
            -
             | 
| 32 | 
            -
                  self.url = req.scheme && req.url.split('?').first
         | 
| 33 | 
            -
                  self.method = req.request_method
         | 
| 34 | 
            -
                  self.query_string = req.query_string
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  self.headers = format_headers_for_sentry(env_hash)
         | 
| 37 | 
            -
                  self.env     = format_env_for_sentry(env_hash)
         | 
| 38 | 
            -
                end
         | 
| 39 | 
            -
             | 
| 40 19 | 
             
                private
         | 
| 41 20 |  | 
| 42 | 
            -
                # Request ID based on ActionDispatch::RequestId
         | 
| 43 | 
            -
                def read_request_id_from(env_hash)
         | 
| 44 | 
            -
                  REQUEST_ID_HEADERS.each do |key|
         | 
| 45 | 
            -
                    request_id = env_hash[key]
         | 
| 46 | 
            -
                    return request_id if request_id
         | 
| 47 | 
            -
                  end
         | 
| 48 | 
            -
                  nil
         | 
| 49 | 
            -
                end
         | 
| 50 | 
            -
             | 
| 51 21 | 
             
                # See Sentry server default limits at
         | 
| 52 22 | 
             
                # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
         | 
| 53 23 | 
             
                def read_data_from(request)
         | 
| @@ -67,7 +37,7 @@ module Sentry | |
| 67 37 | 
             
                    begin
         | 
| 68 38 | 
             
                      key = key.to_s # rack env can contain symbols
         | 
| 69 39 | 
             
                      value = value.to_s
         | 
| 70 | 
            -
                      next memo['X-Request-Id'] ||=  | 
| 40 | 
            +
                      next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env_hash) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
         | 
| 71 41 | 
             
                      next unless key.upcase == key # Non-upper case stuff isn't either
         | 
| 72 42 |  | 
| 73 43 | 
             
                      # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
         | 
    
        data/lib/sentry/rack.rb
    CHANGED
    
    
| @@ -1,45 +1,54 @@ | |
| 1 1 | 
             
            module Sentry
         | 
| 2 2 | 
             
              module Rack
         | 
| 3 | 
            -
                class  | 
| 3 | 
            +
                class CaptureExceptions
         | 
| 4 4 | 
             
                  def initialize(app)
         | 
| 5 5 | 
             
                    @app = app
         | 
| 6 6 | 
             
                  end
         | 
| 7 7 |  | 
| 8 8 | 
             
                  def call(env)
         | 
| 9 | 
            -
                     | 
| 10 | 
            -
             | 
| 11 | 
            -
                    #  | 
| 12 | 
            -
                    Sentry.clone_hub_to_current_thread | 
| 13 | 
            -
             | 
| 14 | 
            -
                    # it's essential for multi-process servers (e.g. unicorn)
         | 
| 9 | 
            +
                    return @app.call(env) unless Sentry.initialized?
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    # make sure the current thread has a clean hub
         | 
| 12 | 
            +
                    Sentry.clone_hub_to_current_thread
         | 
| 13 | 
            +
             | 
| 15 14 | 
             
                    Sentry.with_scope do |scope|
         | 
| 16 | 
            -
                      # there could be some breadcrumbs already stored in the top-level scope
         | 
| 17 | 
            -
                      # and for request information, we don't need those breadcrumbs
         | 
| 18 15 | 
             
                      scope.clear_breadcrumbs
         | 
| 19 | 
            -
                      env['sentry.client'] = Sentry.get_current_client
         | 
| 20 | 
            -
             | 
| 21 16 | 
             
                      scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
         | 
| 22 17 | 
             
                      scope.set_rack_env(env)
         | 
| 23 18 |  | 
| 19 | 
            +
                      span = Sentry.start_transaction(name: scope.transaction_name, op: "rack.request")
         | 
| 20 | 
            +
                      scope.set_span(span)
         | 
| 21 | 
            +
             | 
| 24 22 | 
             
                      begin
         | 
| 25 23 | 
             
                        response = @app.call(env)
         | 
| 26 24 | 
             
                      rescue Sentry::Error
         | 
| 25 | 
            +
                        finish_span(span, 500)
         | 
| 27 26 | 
             
                        raise # Don't capture Sentry errors
         | 
| 28 27 | 
             
                      rescue Exception => e
         | 
| 29 28 | 
             
                        Sentry.capture_exception(e)
         | 
| 29 | 
            +
                        finish_span(span, 500)
         | 
| 30 30 | 
             
                        raise
         | 
| 31 31 | 
             
                      end
         | 
| 32 32 |  | 
| 33 33 | 
             
                      exception = collect_exception(env)
         | 
| 34 34 | 
             
                      Sentry.capture_exception(exception) if exception
         | 
| 35 35 |  | 
| 36 | 
            +
                      finish_span(span, response[0])
         | 
| 37 | 
            +
             | 
| 36 38 | 
             
                      response
         | 
| 37 39 | 
             
                    end
         | 
| 38 40 | 
             
                  end
         | 
| 39 41 |  | 
| 42 | 
            +
                  private
         | 
| 43 | 
            +
             | 
| 40 44 | 
             
                  def collect_exception(env)
         | 
| 41 45 | 
             
                    env['rack.exception'] || env['sinatra.error']
         | 
| 42 46 | 
             
                  end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                  def finish_span(span, status_code)
         | 
| 49 | 
            +
                    span.set_http_status(status_code)
         | 
| 50 | 
            +
                    span.finish
         | 
| 51 | 
            +
                  end
         | 
| 43 52 | 
             
                end
         | 
| 44 53 | 
             
              end
         | 
| 45 54 | 
             
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module Sentry
         | 
| 2 | 
            +
              class RequestInterface
         | 
| 3 | 
            +
                def from_rack(env_hash)
         | 
| 4 | 
            +
                  req = ::Rack::Request.new(env_hash)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  if Sentry.configuration.send_default_pii
         | 
| 7 | 
            +
                    self.data = read_data_from(req)
         | 
| 8 | 
            +
                    self.cookies = req.cookies
         | 
| 9 | 
            +
                  else
         | 
| 10 | 
            +
                    # need to completely wipe out ip addresses
         | 
| 11 | 
            +
                    IP_HEADERS.each { |h| env_hash.delete(h) }
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  self.url = req.scheme && req.url.split('?').first
         | 
| 15 | 
            +
                  self.method = req.request_method
         | 
| 16 | 
            +
                  self.query_string = req.query_string
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  self.headers = format_headers_for_sentry(env_hash)
         | 
| 19 | 
            +
                  self.env     = format_env_for_sentry(env_hash)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
    
        data/lib/sentry/rake.rb
    ADDED
    
    | @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require "rake"
         | 
| 2 | 
            +
            require "rake/task"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Rake
         | 
| 5 | 
            +
              class Application
         | 
| 6 | 
            +
                alias orig_display_error_messsage display_error_message
         | 
| 7 | 
            +
                def display_error_message(ex)
         | 
| 8 | 
            +
                  Sentry.capture_exception(ex, hint: { background: false }) do |scope|
         | 
| 9 | 
            +
                    task_name = top_level_tasks.join(' ')
         | 
| 10 | 
            +
                    scope.set_transaction_name(task_name)
         | 
| 11 | 
            +
                    scope.set_tag("rake_task", task_name)
         | 
| 12 | 
            +
                  end if Sentry.initialized?
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  orig_display_error_messsage(ex)
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        data/lib/sentry/scope.rb
    CHANGED
    
    | @@ -3,7 +3,7 @@ require "etc" | |
| 3 3 |  | 
| 4 4 | 
             
            module Sentry
         | 
| 5 5 | 
             
              class Scope
         | 
| 6 | 
            -
                ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env]
         | 
| 6 | 
            +
                ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span]
         | 
| 7 7 |  | 
| 8 8 | 
             
                attr_reader(*ATTRIBUTES)
         | 
| 9 9 |  | 
| @@ -15,20 +15,25 @@ module Sentry | |
| 15 15 | 
             
                  set_default_value
         | 
| 16 16 | 
             
                end
         | 
| 17 17 |  | 
| 18 | 
            -
                def apply_to_event(event)
         | 
| 18 | 
            +
                def apply_to_event(event, hint = nil)
         | 
| 19 19 | 
             
                  event.tags = tags.merge(event.tags)
         | 
| 20 20 | 
             
                  event.user = user.merge(event.user)
         | 
| 21 21 | 
             
                  event.extra = extra.merge(event.extra)
         | 
| 22 22 | 
             
                  event.contexts = contexts.merge(event.contexts)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  if span
         | 
| 25 | 
            +
                    event.contexts[:trace] = span.get_trace_context
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 23 28 | 
             
                  event.fingerprint = fingerprint
         | 
| 24 | 
            -
                  event.level  | 
| 29 | 
            +
                  event.level = level
         | 
| 25 30 | 
             
                  event.transaction = transaction_names.last
         | 
| 26 31 | 
             
                  event.breadcrumbs = breadcrumbs
         | 
| 27 | 
            -
                  event.rack_env = rack_env
         | 
| 32 | 
            +
                  event.rack_env = rack_env if rack_env
         | 
| 28 33 |  | 
| 29 34 | 
             
                  unless @event_processors.empty?
         | 
| 30 35 | 
             
                    @event_processors.each do |processor_block|
         | 
| 31 | 
            -
                      event = processor_block.call(event)
         | 
| 36 | 
            +
                      event = processor_block.call(event, hint)
         | 
| 32 37 | 
             
                    end
         | 
| 33 38 | 
             
                  end
         | 
| 34 39 |  | 
| @@ -52,6 +57,7 @@ module Sentry | |
| 52 57 | 
             
                  copy.user = user.deep_dup
         | 
| 53 58 | 
             
                  copy.transaction_names = transaction_names.deep_dup
         | 
| 54 59 | 
             
                  copy.fingerprint = fingerprint.deep_dup
         | 
| 60 | 
            +
                  copy.span = span.deep_dup
         | 
| 55 61 | 
             
                  copy
         | 
| 56 62 | 
             
                end
         | 
| 57 63 |  | 
| @@ -63,6 +69,7 @@ module Sentry | |
| 63 69 | 
             
                  self.user = scope.user
         | 
| 64 70 | 
             
                  self.transaction_names = scope.transaction_names
         | 
| 65 71 | 
             
                  self.fingerprint = scope.fingerprint
         | 
| 72 | 
            +
                  self.span = scope.span
         | 
| 66 73 | 
             
                end
         | 
| 67 74 |  | 
| 68 75 | 
             
                def update_from_options(
         | 
| @@ -86,6 +93,11 @@ module Sentry | |
| 86 93 | 
             
                  @rack_env = env
         | 
| 87 94 | 
             
                end
         | 
| 88 95 |  | 
| 96 | 
            +
                def set_span(span)
         | 
| 97 | 
            +
                  check_argument_type!(span, Span)
         | 
| 98 | 
            +
                  @span = span
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 89 101 | 
             
                def set_user(user_hash)
         | 
| 90 102 | 
             
                  check_argument_type!(user_hash, Hash)
         | 
| 91 103 | 
             
                  @user = user_hash
         | 
| @@ -130,6 +142,15 @@ module Sentry | |
| 130 142 | 
             
                  @transaction_names.last
         | 
| 131 143 | 
             
                end
         | 
| 132 144 |  | 
| 145 | 
            +
                def get_transaction
         | 
| 146 | 
            +
                  # transaction will always be the first in the span_recorder
         | 
| 147 | 
            +
                  span.span_recorder.spans.first if span
         | 
| 148 | 
            +
                end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                def get_span
         | 
| 151 | 
            +
                  span
         | 
| 152 | 
            +
                end
         | 
| 153 | 
            +
             | 
| 133 154 | 
             
                def set_fingerprint(fingerprint)
         | 
| 134 155 | 
             
                  check_argument_type!(fingerprint, Array)
         | 
| 135 156 |  | 
| @@ -164,6 +185,7 @@ module Sentry | |
| 164 185 | 
             
                  @transaction_names = []
         | 
| 165 186 | 
             
                  @event_processors = []
         | 
| 166 187 | 
             
                  @rack_env = {}
         | 
| 188 | 
            +
                  @span = nil
         | 
| 167 189 | 
             
                end
         | 
| 168 190 |  | 
| 169 191 | 
             
                class << self
         | 
    
        data/lib/sentry/span.rb
    ADDED
    
    | @@ -0,0 +1,132 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "securerandom"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Sentry
         | 
| 5 | 
            +
              class Span
         | 
| 6 | 
            +
                STATUS_MAP = {
         | 
| 7 | 
            +
                  400 => "invalid_argument",
         | 
| 8 | 
            +
                  401 => "unauthenticated",
         | 
| 9 | 
            +
                  403 => "permission_denied",
         | 
| 10 | 
            +
                  404 => "not_found",
         | 
| 11 | 
            +
                  409 => "already_exists",
         | 
| 12 | 
            +
                  429 => "resource_exhausted",
         | 
| 13 | 
            +
                  499 => "cancelled",
         | 
| 14 | 
            +
                  500 => "internal_error",
         | 
| 15 | 
            +
                  501 => "unimplemented",
         | 
| 16 | 
            +
                  503 => "unavailable",
         | 
| 17 | 
            +
                  504 => "deadline_exceeded"
         | 
| 18 | 
            +
                }
         | 
| 19 | 
            +
             | 
| 20 | 
            +
             | 
| 21 | 
            +
                attr_reader :trace_id, :span_id, :parent_span_id, :sampled, :start_timestamp, :timestamp, :description, :op, :status, :tags, :data
         | 
| 22 | 
            +
                attr_accessor :span_recorder
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def initialize(description: nil, op: nil, status: nil, trace_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil)
         | 
| 25 | 
            +
                  @trace_id = trace_id || SecureRandom.uuid.delete("-")
         | 
| 26 | 
            +
                  @span_id = SecureRandom.hex(8)
         | 
| 27 | 
            +
                  @parent_span_id = parent_span_id
         | 
| 28 | 
            +
                  @sampled = sampled
         | 
| 29 | 
            +
                  @start_timestamp = start_timestamp || Sentry.utc_now.to_f
         | 
| 30 | 
            +
                  @timestamp = timestamp
         | 
| 31 | 
            +
                  @description = description
         | 
| 32 | 
            +
                  @op = op
         | 
| 33 | 
            +
                  @status = status
         | 
| 34 | 
            +
                  @data = {}
         | 
| 35 | 
            +
                  @tags = {}
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                def finish
         | 
| 39 | 
            +
                  # already finished
         | 
| 40 | 
            +
                  return if @timestamp
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  @timestamp = Sentry.utc_now.to_f
         | 
| 43 | 
            +
                  self
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def to_sentry_trace
         | 
| 47 | 
            +
                  sampled_flag = ""
         | 
| 48 | 
            +
                  sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  "#{@trace_id}-#{@span_id}-#{sampled_flag}"
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def to_hash
         | 
| 54 | 
            +
                  {
         | 
| 55 | 
            +
                    trace_id: @trace_id,
         | 
| 56 | 
            +
                    span_id: @span_id,
         | 
| 57 | 
            +
                    parent_span_id: @parent_span_id,
         | 
| 58 | 
            +
                    start_timestamp: @start_timestamp,
         | 
| 59 | 
            +
                    timestamp: @timestamp,
         | 
| 60 | 
            +
                    description: @description,
         | 
| 61 | 
            +
                    op: @op,
         | 
| 62 | 
            +
                    status: @status,
         | 
| 63 | 
            +
                    tags: @tags,
         | 
| 64 | 
            +
                    data: @data
         | 
| 65 | 
            +
                  }
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def get_trace_context
         | 
| 69 | 
            +
                  {
         | 
| 70 | 
            +
                    trace_id: @trace_id,
         | 
| 71 | 
            +
                    span_id: @span_id,
         | 
| 72 | 
            +
                    description: @description,
         | 
| 73 | 
            +
                    op: @op,
         | 
| 74 | 
            +
                    status: @status
         | 
| 75 | 
            +
                  }
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def start_child(**options)
         | 
| 79 | 
            +
                  options = options.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
         | 
| 80 | 
            +
                  Span.new(options)
         | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                def with_child_span(**options, &block)
         | 
| 84 | 
            +
                  child_span = start_child(**options)
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                  yield(child_span)
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  child_span.finish
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def deep_dup
         | 
| 92 | 
            +
                  dup
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                def set_op(op)
         | 
| 96 | 
            +
                  @op = op
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                def set_description(description)
         | 
| 100 | 
            +
                  @description = description
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def set_status(status)
         | 
| 104 | 
            +
                  @status = status
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                def set_timestamp(timestamp)
         | 
| 108 | 
            +
                  @timestamp = timestamp
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def set_http_status(status_code)
         | 
| 112 | 
            +
                  status_code = status_code.to_i
         | 
| 113 | 
            +
                  set_data("status_code", status_code)
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  status =
         | 
| 116 | 
            +
                    if status_code >= 200 && status_code < 299
         | 
| 117 | 
            +
                      "ok"
         | 
| 118 | 
            +
                    else
         | 
| 119 | 
            +
                      STATUS_MAP[status_code]
         | 
| 120 | 
            +
                    end
         | 
| 121 | 
            +
                  set_status(status)
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def set_data(key, value)
         | 
| 125 | 
            +
                  @data[key] = value
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def set_tag(key, value)
         | 
| 129 | 
            +
                  @tags[key] = value
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
              end
         | 
| 132 | 
            +
            end
         | 
| @@ -0,0 +1,157 @@ | |
| 1 | 
            +
            module Sentry
         | 
| 2 | 
            +
              class Transaction < Span
         | 
| 3 | 
            +
                SENTRY_TRACE_REGEXP = Regexp.new(
         | 
| 4 | 
            +
                  "^[ \t]*" +  # whitespace
         | 
| 5 | 
            +
                  "([0-9a-f]{32})?" +  # trace_id
         | 
| 6 | 
            +
                  "-?([0-9a-f]{16})?" +  # span_id
         | 
| 7 | 
            +
                  "-?([01])?" +  # sampled
         | 
| 8 | 
            +
                  "[ \t]*$"  # whitespace
         | 
| 9 | 
            +
                )
         | 
| 10 | 
            +
                UNLABELD_NAME = "<unlabeled transaction>".freeze
         | 
| 11 | 
            +
                MESSAGE_PREFIX = "[Tracing]"
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                attr_reader :name, :parent_sampled
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def initialize(name: nil, parent_sampled: nil, **options)
         | 
| 16 | 
            +
                  super(**options)
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  @name = name
         | 
| 19 | 
            +
                  @parent_sampled = parent_sampled
         | 
| 20 | 
            +
                  set_span_recorder
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def set_span_recorder
         | 
| 24 | 
            +
                  @span_recorder = SpanRecorder.new(1000)
         | 
| 25 | 
            +
                  @span_recorder.add(self)
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def self.from_sentry_trace(sentry_trace, **options)
         | 
| 29 | 
            +
                  return unless sentry_trace
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  match = SENTRY_TRACE_REGEXP.match(sentry_trace)
         | 
| 32 | 
            +
                  trace_id, parent_span_id, sampled_flag = match[1..3]
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  sampled = sampled_flag != "0"
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: sampled, **options)
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def to_hash
         | 
| 40 | 
            +
                  hash = super
         | 
| 41 | 
            +
                  hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
         | 
| 42 | 
            +
                  hash
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def start_child(**options)
         | 
| 46 | 
            +
                  child_span = super
         | 
| 47 | 
            +
                  child_span.span_recorder = @span_recorder
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  if @sampled
         | 
| 50 | 
            +
                    @span_recorder.add(child_span)
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  child_span
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def deep_dup
         | 
| 57 | 
            +
                  copy = super
         | 
| 58 | 
            +
                  copy.set_span_recorder
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  @span_recorder.spans.each do |span|
         | 
| 61 | 
            +
                    # span_recorder's first span is the current span, which should not be added to the copy's spans
         | 
| 62 | 
            +
                    next if span == self
         | 
| 63 | 
            +
                    copy.span_recorder.add(span.dup)
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  copy
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def set_initial_sample_desicion(sampling_context = {})
         | 
| 70 | 
            +
                  unless Sentry.configuration.tracing_enabled?
         | 
| 71 | 
            +
                    @sampled = false
         | 
| 72 | 
            +
                    return
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  return unless @sampled.nil?
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  transaction_description = generate_transaction_description
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  logger = Sentry.configuration.logger
         | 
| 80 | 
            +
                  sample_rate = Sentry.configuration.traces_sample_rate
         | 
| 81 | 
            +
                  traces_sampler = Sentry.configuration.traces_sampler
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  if traces_sampler.is_a?(Proc)
         | 
| 84 | 
            +
                    sampling_context = sampling_context.merge(
         | 
| 85 | 
            +
                      parent_sampled: @parent_sampled,
         | 
| 86 | 
            +
                      transaction_context: self.to_hash
         | 
| 87 | 
            +
                    )
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                    sample_rate = traces_sampler.call(sampling_context)
         | 
| 90 | 
            +
                  end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Float) && sample_rate >= 0.0 && sample_rate <= 1.0)
         | 
| 93 | 
            +
                    @sampled = false
         | 
| 94 | 
            +
                    logger.warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
         | 
| 95 | 
            +
                    return
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  if sample_rate == 0.0 || sample_rate == false
         | 
| 99 | 
            +
                    @sampled = false
         | 
| 100 | 
            +
                    logger.debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
         | 
| 101 | 
            +
                    return
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  if sample_rate == true
         | 
| 105 | 
            +
                    @sampled = true
         | 
| 106 | 
            +
                  else
         | 
| 107 | 
            +
                    @sampled = Random.rand < sample_rate
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                  if @sampled
         | 
| 111 | 
            +
                    logger.debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
         | 
| 112 | 
            +
                  else
         | 
| 113 | 
            +
                    logger.debug(
         | 
| 114 | 
            +
                      "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
         | 
| 115 | 
            +
                    )
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                def finish(hub: nil)
         | 
| 120 | 
            +
                  super() # Span#finish doesn't take arguments
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  if @name.nil?
         | 
| 123 | 
            +
                    @name = UNLABELD_NAME
         | 
| 124 | 
            +
                  end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  return unless @sampled
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  hub ||= Sentry.get_current_hub
         | 
| 129 | 
            +
                  event = hub.current_client.event_from_transaction(self)
         | 
| 130 | 
            +
                  hub.capture_event(event)
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                private
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def generate_transaction_description
         | 
| 136 | 
            +
                  result = op.nil? ? "" : "<#{@op}> "
         | 
| 137 | 
            +
                  result += "transaction"
         | 
| 138 | 
            +
                  result += " <#{@name}>" if @name
         | 
| 139 | 
            +
                  result
         | 
| 140 | 
            +
                end
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                class SpanRecorder
         | 
| 143 | 
            +
                  attr_reader :max_length, :spans
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                  def initialize(max_length)
         | 
| 146 | 
            +
                    @max_length = max_length
         | 
| 147 | 
            +
                    @spans = []
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  def add(span)
         | 
| 151 | 
            +
                    if @spans.count < @max_length
         | 
| 152 | 
            +
                      @spans << span
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
              end
         | 
| 157 | 
            +
            end
         |