Skip to content

Commit 2e6fa76

Browse files
committed
GraphQL: Capture user-provided error extension values
1 parent cf6efdc commit 2e6fa76

File tree

10 files changed

+182
-5
lines changed

10 files changed

+182
-5
lines changed

Diff for: docs/GettingStarted.md

+2
Original file line numberDiff line numberDiff line change
@@ -883,6 +883,8 @@ The `instrument :graphql` method accepts the following parameters. Additional op
883883
| `with_unified_tracer` | `DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER` | `Bool` | (Recommended) Enable to instrument with `UnifiedTrace` tracer for `graphql` >= v2.2, **enabling support for Endpoints list** in the Service Catalog. `with_deprecated_tracer` has priority over this. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
884884
| `with_deprecated_tracer` | | `Bool` | Enable to instrument with deprecated `GraphQL::Tracing::DataDogTracing`. This has priority over `with_unified_tracer`. Default is `false`, using `GraphQL::Tracing::DataDogTrace` instead | `false` |
885885
| `service_name` | | `String` | Service name used for graphql instrumentation | `'ruby-graphql'` |
886+
| `error_extensions` | `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS` | `Array` | List of extensions keys to include in the span event reported for GraphQL queries with errors. | `[]` |
887+
886888

887889
Once an instrumentation strategy is selected (`with_unified_tracer: true`, `with_deprecated_tracer: true`, or *no option set* which defaults to `GraphQL::Tracing::DataDogTrace`), it is not possible to change the instrumentation strategy in the same Ruby process.
888890
This is especially important for [auto instrumented applications](#rails-or-hanami-applications) because an automatic initial instrumentation is always applied at startup, thus such applications will always instrument GraphQL with the default strategy (`GraphQL::Tracing::DataDogTrace`).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module Datadog
4+
module Tracing
5+
module Contrib
6+
module GraphQL
7+
module Configuration
8+
# Parses the environment variable `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS` for error extension names declaration.
9+
class ErrorExtensionEnvParser
10+
# Parses the environment variable `DD_TRACE_GRAPHQL_ERROR_EXTENSIONS` into an array of error extension names.
11+
def self.call(values)
12+
return unless values
13+
14+
# Split by comma, remove leading and trailing whitespaces,
15+
# remove empty values, and remove repeated values.
16+
values.split(',').each(&:strip!).reject(&:empty?).uniq
17+
end
18+
end
19+
end
20+
end
21+
end
22+
end
23+
end

Diff for: lib/datadog/tracing/contrib/graphql/configuration/settings.rb

+10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require_relative '../../configuration/settings'
44
require_relative '../ext'
5+
require_relative 'error_extension_env_parser'
56

67
module Datadog
78
module Tracing
@@ -48,6 +49,15 @@ class Settings < Contrib::Configuration::Settings
4849
o.type :bool
4950
o.default false
5051
end
52+
53+
# Capture error extensions provided by the user in their GraphQL error responses.
54+
# The extensions can be anything, so the user is responsible for ensuring they are safe to capture.
55+
option :error_extensions do |o|
56+
o.env Ext::ENV_ERROR_EXTENSIONS
57+
o.type :array, nilable: false
58+
o.default []
59+
o.env_parser { |v| ErrorExtensionEnvParser(v) }
60+
end
5161
end
5262
end
5363
end

Diff for: lib/datadog/tracing/contrib/graphql/ext.rb

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Ext
1212
ENV_ANALYTICS_ENABLED = 'DD_TRACE_GRAPHQL_ANALYTICS_ENABLED'
1313
ENV_ANALYTICS_SAMPLE_RATE = 'DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE'
1414
ENV_WITH_UNIFIED_TRACER = 'DD_TRACE_GRAPHQL_WITH_UNIFIED_TRACER'
15+
ENV_ERROR_EXTENSIONS = 'DD_TRACE_GRAPHQL_ERROR_EXTENSIONS'
1516
SERVICE_NAME = 'graphql'
1617
TAG_COMPONENT = 'graphql'
1718

Diff for: lib/datadog/tracing/contrib/graphql/unified_trace.rb

+21
Original file line numberDiff line numberDiff line change
@@ -196,10 +196,30 @@ def multiplex_resource(multiplex)
196196
# These are represented in the Datadog App as special GraphQL errors,
197197
# given their event name `dd.graphql.query.error`.
198198
def add_query_error_events(span, errors)
199+
capture_extensions = Datadog.configuration.tracing[:graphql][:error_extensions]
199200
errors.each do |error|
200201
e = Core::Error.build_from(error)
201202
err = error.to_h
202203

204+
extensions = if !capture_extensions.empty? && (extensions = error.extensions)
205+
# Capture extensions, ensuring all values are primitives
206+
extensions.each_with_object({}) do |(key, value), hash|
207+
next unless capture_extensions.include?(key.to_s)
208+
209+
value = case value
210+
when TrueClass, FalseClass, Integer, Float
211+
value
212+
else
213+
# Stringify anything that is not a boolean or a number
214+
value.to_s
215+
end
216+
217+
hash["extensions.#{key}"] = value
218+
end
219+
else
220+
{}
221+
end
222+
203223
span.span_events << Datadog::Tracing::SpanEvent.new(
204224
Ext::EVENT_QUERY_ERROR,
205225
attributes: {
@@ -208,6 +228,7 @@ def add_query_error_events(span, errors)
208228
stacktrace: e.backtrace,
209229
locations: serialize_error_locations(err['locations']),
210230
path: err['path'],
231+
**extensions
211232
}
212233
)
213234
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Datadog
2+
module Tracing
3+
module Contrib
4+
module GraphQL
5+
module Configuration
6+
class ErrorExtensionEnvParser
7+
def self.call: (string values) -> Array[string]
8+
end
9+
end
10+
end
11+
end
12+
end
13+
end

Diff for: sig/datadog/tracing/contrib/graphql/ext.rbs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Datadog
1010
ENV_ANALYTICS_SAMPLE_RATE: "DD_TRACE_GRAPHQL_ANALYTICS_SAMPLE_RATE"
1111

1212
ENV_WITH_UNIFIED_TRACER: string
13+
ENV_ERROR_EXTENSIONS: string
1314
EVENT_QUERY_ERROR: String
1415
SERVICE_NAME: "graphql"
1516

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
require 'datadog/tracing/contrib/graphql/configuration/error_extension_env_parser'
2+
3+
RSpec.describe Datadog::Tracing::Contrib::GraphQL::Configuration::ErrorExtensionEnvParser do
4+
describe '.call' do
5+
subject(:call) { described_class.call(value) }
6+
7+
context 'when value is nil' do
8+
let(:value) { nil }
9+
it 'returns nil' do
10+
is_expected.to be_nil
11+
end
12+
end
13+
14+
context 'when value is an empty string' do
15+
let(:value) { '' }
16+
it 'returns an empty array' do
17+
is_expected.to eq([])
18+
end
19+
end
20+
21+
context 'when value contains multiple commas' do
22+
let(:value) { 'foo,bar,baz' }
23+
it 'returns an array with split values' do
24+
is_expected.to eq(['foo', 'bar', 'baz'])
25+
end
26+
end
27+
28+
context 'when value contains leading and trailing whitespace' do
29+
let(:value) { ' foo , bar , baz ' }
30+
it 'removes whitespace around values' do
31+
is_expected.to eq(['foo', 'bar', 'baz'])
32+
end
33+
end
34+
35+
context 'when value contains empty elements' do
36+
let(:value) { ',foo,,bar,,baz,' }
37+
it 'removes the empty elements' do
38+
is_expected.to eq(['foo', 'bar', 'baz'])
39+
end
40+
end
41+
42+
context 'when value contains repeated elements' do
43+
let(:value) { 'foo,foo' }
44+
it 'remove repeated elements' do
45+
is_expected.to eq(['foo'])
46+
end
47+
end
48+
end
49+
end

Diff for: spec/datadog/tracing/contrib/graphql/configuration/settings_spec.rb

+20
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,24 @@
8080
end
8181
end
8282
end
83+
84+
describe 'error_extensions' do
85+
context 'when default' do
86+
it do
87+
settings = described_class.new
88+
89+
expect(settings.error_extensions).to eq([])
90+
end
91+
end
92+
93+
context 'when given an array' do
94+
it do
95+
error_extension = double
96+
97+
settings = described_class.new(error_extensions: [error_extension])
98+
99+
expect(settings.error_extensions).to eq([error_extension])
100+
end
101+
end
102+
end
83103
end

Diff for: spec/datadog/tracing/contrib/graphql/test_schema_examples.rb

+42-5
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ class #{prefix}TestGraphQLSchema < ::GraphQL::Schema
3535
3636
rescue_from(RuntimeError) do |err, obj, args, ctx, field|
3737
raise GraphQL::ExecutionError.new(err.message, extensions: {
38-
'int-1': 1,
39-
'str-1': '1',
40-
'array-1-2': [1,'2'],
41-
'': 'empty string',
42-
',': 'comma',
38+
'int': 1,
39+
'bool': true,
40+
'str': '1',
41+
'array-1-2': [1, '2'],
42+
'hash-a-b': {a: 'b'},
43+
'object': ::Object.new,
44+
'extra-int': 2, # This should not be included
4345
})
4446
end
4547
end
@@ -183,5 +185,40 @@ def unload_test_schema(prefix: '')
183185
)
184186
)
185187
end
188+
189+
context 'with error extension capture enabled' do
190+
before do
191+
Datadog.configure do |c|
192+
c.tracing.instrument :graphql, error_extensions: ['int', 'str', 'bool', 'array-1-2', 'hash-a-b', 'object']
193+
end
194+
end
195+
196+
after { Datadog.configuration.tracing[:graphql].reset! }
197+
198+
it 'creates query span for error with extensions' do
199+
expect(result.to_h['errors'][0]['message']).to eq('GraphQL error')
200+
201+
expect(graphql_execute.events).to contain_exactly(
202+
a_span_event_with(
203+
name: 'dd.graphql.query.error',
204+
attributes: {
205+
'message' => 'GraphQL error',
206+
'type' => 'GraphQL::ExecutionError',
207+
'stacktrace' => include(__FILE__),
208+
'locations' => ['1:14'],
209+
'path' => ['graphqlError'],
210+
'extensions.int' => 1,
211+
'extensions.bool' => true,
212+
'extensions.str' => '1',
213+
'extensions.array-1-2' => '[1, "2"]',
214+
'extensions.hash-a-b' => '{:a=>"b"}',
215+
'extensions.object' => start_with('#<Object:'),
216+
}
217+
)
218+
)
219+
220+
expect(graphql_execute.events[0].attributes).to_not include('extensions.extra-int')
221+
end
222+
end
186223
end
187224
end

0 commit comments

Comments
 (0)