diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index 6f6a705a839..bcd1b2b4e66 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -927,10 +927,8 @@ tests/: nextjs: bug (APMAPI-939) # the nextjs weblog application changes the sampling priority from 1.0 to 2.0 test_graphql.py: Test_GraphQLQueryErrorReporting: - '*': *ref_5_34_0 - express4-typescript: incomplete_test_app (endpoint not implemented) - express5: missing_feature - nextjs: missing_feature + '*': irrelevant + express4: missing_feature test_identify.py: Test_Basic: v2.4.0 Test_Propagate: *ref_3_2_0 diff --git a/manifests/ruby.yml b/manifests/ruby.yml index 747a39ec287..4710dbaed83 100644 --- a/manifests/ruby.yml +++ b/manifests/ruby.yml @@ -523,7 +523,10 @@ tests/: Test_Span_Links_Flags_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) Test_Span_Links_From_Conflicting_Contexts: missing_feature Test_Span_Links_Omit_Tracestate_From_Conflicting_Contexts: missing_feature (implementation specs have not been determined) - test_graphql.py: missing_feature + test_graphql.py: + Test_GraphQLQueryErrorReporting: + "*": irrelevant + graphql23: missing_feature test_identify.py: Test_Basic: v1.0.0 Test_Propagate: missing_feature diff --git a/tests/test_graphql.py b/tests/test_graphql.py index fb9bddcf2c8..2587aadc9cb 100644 --- a/tests/test_graphql.py +++ b/tests/test_graphql.py @@ -31,7 +31,9 @@ def setup_execute_error_span_event(self): ) def test_execute_error_span_event(self): - """Test if the main GraphQL span contains a span event with the appropriate error information""" + """Test if the main GraphQL span contains a span event with the appropriate error information. + The error extensions allowed are DD_TRACE_GRAPHQL_ERROR_EXTENSIONS=int,float,str,bool,other. + """ assert self.request.status_code == 200 @@ -66,6 +68,23 @@ def test_execute_error_span_event(self): assert location.split(":")[0].isdigit() assert location.split(":")[1].isdigit() + assert attributes["extensions.int"] == 1 + assert attributes["extensions.float"] == 1.1 + assert attributes["extensions.str"] == "1" + assert attributes["extensions.bool"] == True + + # A list with two heterogeneous elements: [1, "foo"]. + # This test simulates an object that is not a supported scalar above (int,float,string,boolean). + # This object should be serialized as a string, either using the language's default serialization or + # JSON serialization of the object. + # The goal here is to display the original as well as possible in the UI, without supporting arbitrary + # nested levels inside `span_event.attributes`. + # use regex to match the list format + assert "1" in attributes["extensions.other"] + assert "foo" in attributes["extensions.other"] + + assert "extensions.not_captured" not in attributes + @staticmethod def _get_events(span): if "events" in span["meta"]: diff --git a/utils/_context/_scenarios/__init__.py b/utils/_context/_scenarios/__init__.py index e1a86243b8a..a12cbb52557 100644 --- a/utils/_context/_scenarios/__init__.py +++ b/utils/_context/_scenarios/__init__.py @@ -153,7 +153,10 @@ class _Scenarios: # This GraphQL scenario can be used for any GraphQL testing, not just AppSec graphql_appsec = EndToEndScenario( "GRAPHQL_APPSEC", - weblog_env={"DD_APPSEC_RULES": "/appsec_blocking_rule.json"}, + weblog_env={ + "DD_APPSEC_RULES": "/appsec_blocking_rule.json", + "DD_TRACE_GRAPHQL_ERROR_EXTENSIONS": "int,float,str,bool,other", + }, weblog_volumes={"./tests/appsec/blocking_rule.json": {"bind": "/appsec_blocking_rule.json", "mode": "ro"}}, doc="AppSec tests for GraphQL integrations", github_workflow="graphql", diff --git a/utils/build/docker/nodejs/express/graphql.js b/utils/build/docker/nodejs/express/graphql.js index 6c39dc0d073..958871f9a22 100644 --- a/utils/build/docker/nodejs/express/graphql.js +++ b/utils/build/docker/nodejs/express/graphql.js @@ -1,6 +1,6 @@ 'use strict' -const { ApolloServer, gql } = require('apollo-server-express') +const { ApolloServer, gql, ApolloError } = require('apollo-server-express') const { readFileSync } = require('fs') const users = [ @@ -32,16 +32,21 @@ const typeDefs = gql` id: Int name: String } +` - type Error { - message: String - extensions: [Extension] - } - - type Extension { - key: String - value: String - }` +// Custom GraphQL error class +class CustomGraphQLError extends ApolloError { + constructor (message, code, properties) { + super(message, properties) + this.extensions.code = code + this.extensions.int = 1 + this.extensions.float = 1.1 + this.extensions.str = '1' + this.extensions.bool = true + this.extensions.other = [1, 'foo'] + this.extensions.not_captured = 'nope' + } +} function getUser (parent, args) { return users.find((item) => args.id === item.id) @@ -61,7 +66,7 @@ function testInjection (parent, args) { } function withError (parent, args) { - throw new Error('test error') + throw new CustomGraphQLError('test error', 'CUSTOM_USER_DEFINED_ERROR', 'Some extra context about the error.') } const resolvers = { @@ -73,22 +78,8 @@ const resolvers = { } } -// Custom error formatting -const formatError = (error) => { - return { - message: error.message, - extensions: [ - { key: 'int-1', value: '1' }, - { key: 'str-1', value: '1' }, - { key: 'array-1-2', value: [1, '2'] }, - { key: 'empty', value: 'empty string' }, - { key: 'comma', value: 'comma' } - ] - } -} - module.exports = async function (app) { - const server = new ApolloServer({ typeDefs, resolvers, formatError }) + const server = new ApolloServer({ typeDefs, resolvers }) await server.start() server.applyMiddleware({ app }) } diff --git a/utils/build/docker/ruby/graphql23/app/graphql/system_test_schema.rb b/utils/build/docker/ruby/graphql23/app/graphql/system_test_schema.rb index 91b2bf52655..1c7a0a36992 100644 --- a/utils/build/docker/ruby/graphql23/app/graphql/system_test_schema.rb +++ b/utils/build/docker/ruby/graphql23/app/graphql/system_test_schema.rb @@ -14,11 +14,12 @@ class SystemTestSchema < GraphQL::Schema rescue_from(RuntimeError) do |err, obj, args, ctx, field| # Custom extension values used for testing. raise GraphQL::ExecutionError.new(err.message, extensions: { - 'int-1': 1, - 'str-1': '1', - 'array-1-2': [1,'2'], - '': 'empty string', - ',': 'comma', + int: 1, + float: 1.1, + str: '1', + bool: true, + other: [1, 'foo'], + not_captured: 'nope', }) end