From 9974c9f6ddf6f344cbff532a123825665dd694f6 Mon Sep 17 00:00:00 2001
From: Neel Shah <neelshah.sa@gmail.com>
Date: Mon, 13 May 2024 15:33:22 +0200
Subject: [PATCH] w3c traceparent

---
 .gitignore                                    |  1 +
 CHANGELOG.md                                  |  3 +
 sentry-ruby/lib/sentry-ruby.rb                | 13 +++-
 sentry-ruby/lib/sentry/hub.rb                 | 10 +++
 sentry-ruby/lib/sentry/propagation_context.rb | 63 +++++++++++++++----
 sentry-ruby/lib/sentry/span.rb                |  8 +++
 sentry-ruby/spec/sentry_spec.rb               |  3 +-
 7 files changed, 88 insertions(+), 13 deletions(-)

diff --git a/.gitignore b/.gitignore
index 0b513cc74..43d2239ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,5 +12,6 @@ Gemfile.lock
 .ruby-gemset
 .idea
 *.rdb
+.yardoc/
 
 examples/**/node_modules
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7eaff1a37..ad5bcd65c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,9 @@
       config.enabled_patches += [:graphql]
     end
     ```
+- Add [W3C traceparent header](https://www.w3.org/TR/trace-context/#traceparent-header) support ([#2310](https://github.com/getsentry/sentry-ruby/pull/2310))
+
+  The SDK now also propagates and accepts incoming W3C `traceparent` headers along with the currently implemented `sentry-trace` and `baggage` headers.
 
 ### Bug Fixes
 
diff --git a/sentry-ruby/lib/sentry-ruby.rb b/sentry-ruby/lib/sentry-ruby.rb
index d412dec2a..f3a0a5c80 100644
--- a/sentry-ruby/lib/sentry-ruby.rb
+++ b/sentry-ruby/lib/sentry-ruby.rb
@@ -44,7 +44,7 @@ module Sentry
   LOGGER_PROGNAME = "sentry".freeze
 
   SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze
-
+  W3C_TRACEPARENT_HEADER_NAME = "traceparent".freeze
   BAGGAGE_HEADER_NAME = "baggage".freeze
 
   THREAD_LOCAL = :sentry_hub
@@ -533,6 +533,17 @@ def get_traceparent
       get_current_hub.get_traceparent
     end
 
+    # Returns the W3C traceparent header for distributed tracing.
+    # Can be either from the currently active span or the propagation context.
+    #
+    # @see https://www.w3.org/TR/trace-context/#traceparent-header W3C Traceparent specification
+    #
+    # @return [String, nil]
+    def get_w3c_traceparent
+      return nil unless initialized?
+      get_current_hub.get_w3c_traceparent
+    end
+
     # Returns the baggage header for distributed tracing.
     # Can be either from the currently active span or the propagation context.
     #
diff --git a/sentry-ruby/lib/sentry/hub.rb b/sentry-ruby/lib/sentry/hub.rb
index 05d22152b..3e9067673 100644
--- a/sentry-ruby/lib/sentry/hub.rb
+++ b/sentry-ruby/lib/sentry/hub.rb
@@ -265,6 +265,13 @@ def get_traceparent
         current_scope.propagation_context.get_traceparent
     end
 
+    def get_w3c_traceparent
+      return nil unless current_scope
+
+      current_scope.get_span&.get_w3c_traceparent ||
+        current_scope.propagation_context.get_w3c_traceparent
+    end
+
     def get_baggage
       return nil unless current_scope
 
@@ -278,6 +285,9 @@ def get_trace_propagation_headers
       traceparent = get_traceparent
       headers[SENTRY_TRACE_HEADER_NAME] = traceparent if traceparent
 
+      w3c_traceparent = get_w3c_traceparent
+      headers[W3C_TRACEPARENT_HEADER_NAME] = w3c_traceparent if w3c_traceparent
+
       baggage = get_baggage
       headers[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty?
 
diff --git a/sentry-ruby/lib/sentry/propagation_context.rb b/sentry-ruby/lib/sentry/propagation_context.rb
index 12ce55540..86ab01d7f 100644
--- a/sentry-ruby/lib/sentry/propagation_context.rb
+++ b/sentry-ruby/lib/sentry/propagation_context.rb
@@ -13,6 +13,17 @@ class PropagationContext
       "[ \t]*$"  # whitespace
     )
 
+    W3C_TRACEPARENT_REGEX = Regexp.new(
+      "^[ \t]*" +  # whitespace
+      "([0-9a-f]{2})?" +  # version
+      "-?([0-9a-f]{32})?" +  # trace_id
+      "-?([0-9a-f]{16})?" +  # parent_span_id
+      "-?([0-9a-f]{2})?" +  # trace_flags
+      "[ \t]*$"  # whitespace
+    )
+
+    W3C_TRACEPARENT_VERSION = '00'
+
     # An uuid that can be used to identify a trace.
     # @return [String]
     attr_reader :trace_id
@@ -42,6 +53,7 @@ def initialize(scope, env = nil)
 
       if env
         sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME]
+        w3c_traceparent_header = env["HTTP_TRACEPARENT"] || env[W3C_TRACEPARENT_HEADER_NAME]
         baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME]
 
         if sentry_trace_header
@@ -49,21 +61,30 @@ def initialize(scope, env = nil)
 
           if sentry_trace_data
             @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
+            @incoming_trace = true
+          end
+        elsif w3c_traceparent_header
+          traceparent_data = self.class.extract_w3c_traceparent(w3c_traceparent_header)
 
-            @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
-
-            @baggage.freeze!
+          if traceparent_data
+            @trace_id, @parent_span_id, @parent_sampled = traceparent_data
             @incoming_trace = true
           end
         end
+
+        if @incoming_trace
+          @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
+
+          @baggage.freeze!
+        end
       end
 
       @trace_id ||= SecureRandom.uuid.delete("-")
@@ -84,6 +105,20 @@ def self.extract_sentry_trace(sentry_trace)
       [trace_id, parent_span_id, parent_sampled]
     end
 
+    # Extract the trace_id, parent_span_id and parent_sampled values from a W3C traceparent header.
+    #
+    # @param traceparent [String] the traceparent header value from the previous transaction.
+    # @return [Array, nil]
+    def self.extract_w3c_traceparent(traceparent)
+      match = W3C_TRACEPARENT_REGEX.match(traceparent)
+      return nil if match.nil?
+
+      trace_id, parent_span_id, trace_flags = match[2..4]
+      parent_sampled = (trace_flags.hex & 0x01) == 0x01
+
+      [version, trace_id, parent_span_id, parent_sampled]
+    end
+
     # Returns the trace context that can be used to embed in an Event.
     # @return [Hash]
     def get_trace_context
@@ -100,6 +135,12 @@ def get_traceparent
       "#{trace_id}-#{span_id}"
     end
 
+    # Returns the w3c traceparent header from the propagation context.
+    # @return [String]
+    def get_w3c_traceparent
+      "#{W3C_TRACEPARENT_VERSION}-#{trace_id}-#{span_id}-00"
+    end
+
     # Returns the Baggage from the propagation context or populates as head SDK if empty.
     # @return [Baggage, nil]
     def get_baggage
diff --git a/sentry-ruby/lib/sentry/span.rb b/sentry-ruby/lib/sentry/span.rb
index 69374b496..825982123 100644
--- a/sentry-ruby/lib/sentry/span.rb
+++ b/sentry-ruby/lib/sentry/span.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require "securerandom"
+require "sentry/propagation_context"
 require "sentry/metrics/local_aggregator"
 
 module Sentry
@@ -141,6 +142,13 @@ def to_sentry_trace
       "#{@trace_id}-#{@span_id}-#{sampled_flag}"
     end
 
+    # Generates a w3c traceparent header that can be used to connect other transactions.
+    # @return [String]
+    def get_w3c_traceparent
+      trace_flags = @sampled ? '01' : '00'
+      "#{PropagationContext::W3C_TRACEPARENT_VERSION}-#{@trace_id}-#{@span_id}-#{trace_flags}"
+    end
+
     # Generates a W3C Baggage header string for distributed tracing
     # from the incoming baggage stored on the transaction.
     # @return [String, nil]
diff --git a/sentry-ruby/spec/sentry_spec.rb b/sentry-ruby/spec/sentry_spec.rb
index c3f12777a..989e3e124 100644
--- a/sentry-ruby/spec/sentry_spec.rb
+++ b/sentry-ruby/spec/sentry_spec.rb
@@ -777,7 +777,8 @@
     it "returns a Hash of sentry-trace and baggage" do
       expect(described_class.get_trace_propagation_headers).to eq({
         "sentry-trace" => described_class.get_traceparent,
-        "baggage" => described_class.get_baggage
+        "baggage" => described_class.get_baggage,
+        "traceparent" => described_class.get_w3c_traceparent
       })
     end
   end