File: propagation_context.rb

package info (click to toggle)
ruby-sentry-ruby 5.28.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 672 kB
  • sloc: ruby: 6,118; makefile: 8; sh: 4
file content (172 lines) | stat: -rw-r--r-- 5,327 bytes parent folder | download | duplicates (2)
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
# frozen_string_literal: true

require "securerandom"
require "sentry/baggage"
require "sentry/utils/uuid"
require "sentry/utils/sample_rand"

module Sentry
  class PropagationContext
    SENTRY_TRACE_REGEXP = Regexp.new(
      "\\A([0-9a-f]{32})?" + # trace_id
      "-?([0-9a-f]{16})?" +  # span_id
      "-?([01])?\\z"         # sampled
    )

    # An uuid that can be used to identify a trace.
    # @return [String]
    attr_reader :trace_id
    # An uuid that can be used to identify the span.
    # @return [String]
    attr_reader :span_id
    # Span parent's span_id.
    # @return [String, nil]
    attr_reader :parent_span_id
    # The sampling decision of the parent transaction.
    # @return [Boolean, nil]
    attr_reader :parent_sampled
    # Is there an incoming trace or not?
    # @return [Boolean]
    attr_reader :incoming_trace
    # This is only for accessing the current baggage variable.
    # Please use the #get_baggage method for interfacing outside this class.
    # @return [Baggage, nil]
    attr_reader :baggage
    # The propagated random value used for sampling decisions.
    # @return [Float, nil]
    attr_reader :sample_rand

    # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header.
    #
    # @param sentry_trace [String] the sentry-trace header value from the previous transaction.
    # @return [Array, nil]
    def self.extract_sentry_trace(sentry_trace)
      value = sentry_trace.to_s.strip
      return if value.empty?

      match = SENTRY_TRACE_REGEXP.match(value)
      return if match.nil?

      trace_id, parent_span_id, sampled_flag = match[1..3]
      parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0"

      [trace_id, parent_span_id, parent_sampled]
    end

    def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
      return unless baggage&.items

      sample_rand_str = baggage.items["sample_rand"]
      return unless sample_rand_str

      generator = Utils::SampleRand.new(trace_id: trace_id)
      generator.generate_from_value(sample_rand_str)
    end

    def self.generate_sample_rand(baggage, trace_id, parent_sampled)
      generator = Utils::SampleRand.new(trace_id: trace_id)

      if baggage&.items && !parent_sampled.nil?
        sample_rate_str = baggage.items["sample_rate"]
        sample_rate = sample_rate_str&.to_f

        if sample_rate && !parent_sampled.nil?
          generator.generate_from_sampling_decision(parent_sampled, sample_rate)
        else
          generator.generate_from_trace_id
        end
      else
        generator.generate_from_trace_id
      end
    end

    def initialize(scope, env = nil)
      @scope = scope
      @parent_span_id = nil
      @parent_sampled = nil
      @baggage = nil
      @incoming_trace = false
      @sample_rand = nil

      if env
        sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
        baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME]

        if sentry_trace_header
          sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)

          if sentry_trace_data
            @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data

            @baggage =
              if baggage_header && !baggage_header.empty?
                Baggage.from_incoming_header(baggage_header)
              else
                # If there's an incoming sentry-trace but no incoming baggage header,
                # for instance in traces coming from older SDKs,
                # baggage will be empty and frozen and won't be populated as head SDK.
                Baggage.new({})
              end

            @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)

            @baggage.freeze!
            @incoming_trace = true
          end
        end
      end

      @trace_id ||= Utils.uuid
      @span_id = Utils.uuid.slice(0, 16)
      @sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled)
    end

    # Returns the trace context that can be used to embed in an Event.
    # @return [Hash]
    def get_trace_context
      {
        trace_id: trace_id,
        span_id: span_id,
        parent_span_id: parent_span_id
      }
    end

    # Returns the sentry-trace header from the propagation context.
    # @return [String]
    def get_traceparent
      "#{trace_id}-#{span_id}"
    end

    # Returns the Baggage from the propagation context or populates as head SDK if empty.
    # @return [Baggage, nil]
    def get_baggage
      populate_head_baggage if @baggage.nil? || @baggage.mutable
      @baggage
    end

    # Returns the Dynamic Sampling Context from the baggage.
    # @return [Hash, nil]
    def get_dynamic_sampling_context
      get_baggage&.dynamic_sampling_context
    end

    private

    def populate_head_baggage
      return unless Sentry.initialized?

      configuration = Sentry.configuration

      items = {
        "trace_id" => trace_id,
        "sample_rand" => Utils::SampleRand.format(@sample_rand),
        "environment" => configuration.environment,
        "release" => configuration.release,
        "public_key" => configuration.dsn&.public_key
      }

      items.compact!
      @baggage = Baggage.new(items, mutable: false)
    end
  end
end