Skip to content

Commit 2c83422

Browse files
umaannamalaiTimPansino
authored andcommitted
Add field resolver returnType span attribute (#300)
* Add field.returnType span attr and update error logic. * Change solr docker image to pass health checks.
1 parent f1c6af2 commit 2c83422

File tree

5 files changed

+42
-20
lines changed

5 files changed

+42
-20
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,7 +509,7 @@ jobs:
509509

510510
services:
511511
solr:
512-
image: bitnami/solr:8
512+
image: bitnami/solr:8.8.2
513513
env:
514514
SOLR_CORE: collection
515515
ports:

newrelic/core/attribute.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
'graphql.field.name',
7676
'graphql.field.parentType',
7777
'graphql.field.path',
78+
'graphql.field.returnType',
7879
'graphql.operation.name',
7980
'graphql.operation.type',
8081
'graphql.operation.query',

newrelic/hooks/framework_graphql.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,14 @@ def ignore_graphql_duplicate_exception(exc, val, tb):
4747
# after the original exception has already been recorded.
4848

4949
if transaction and hasattr(val, "original_error"):
50-
_, _, fullnames, message = parse_exc_info((None, val.original_error, None))
50+
while hasattr(val, "original_error"):
51+
# Unpack lowest level original error
52+
val = val.original_error
53+
54+
_, _, fullnames, message = parse_exc_info((None, val, None))
5155
fullname = fullnames[0]
5256
for error in transaction._errors:
53-
if error.message == message:
57+
if error.type == fullname and error.message == message:
5458
return True
5559

5660
return None # Follow original exception matching rules
@@ -93,6 +97,11 @@ def wrap_execute_operation(wrapped, instance, args, kwargs):
9397
except TypeError:
9498
operation = bind_operation_v2(*args, **kwargs)
9599

100+
if graphql_version() < (3, 0, 0):
101+
execution_context = args[0]
102+
else:
103+
execution_context = instance
104+
96105
operation_name = get_node_value(operation, "name")
97106
trace.operation_name = operation_name = operation_name or "<anonymous>"
98107

@@ -106,12 +115,7 @@ def wrap_execute_operation(wrapped, instance, args, kwargs):
106115
if get_node_value(field, "name") in GRAPHQL_INTROSPECTION_FIELDS:
107116
ignore_transaction()
108117

109-
if graphql_version() <= (3, 0, 0):
110-
fragments = args[
111-
0
112-
].fragments # In v2, args[0] is the ExecutionContext object
113-
else:
114-
fragments = instance.fragments # instance is the ExecutionContext object
118+
fragments = execution_context.fragments
115119
deepest_path = traverse_deepest_unique_path(fields, fragments)
116120
trace.deepest_path = deepest_path = ".".join(deepest_path) or ""
117121

@@ -122,7 +126,9 @@ def wrap_execute_operation(wrapped, instance, args, kwargs):
122126
if deepest_path
123127
else "%s/%s" % (operation_type, operation_name)
124128
)
125-
transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
129+
130+
if not execution_context.errors:
131+
transaction.set_transaction_name(transaction_name, "GraphQL", priority=14)
126132

127133
return result
128134

@@ -359,10 +365,14 @@ def wrap_resolve_field(wrapped, instance, args, kwargs):
359365
parent_type, field_asts, field_path = bind_resolve_field(*args, **kwargs)
360366

361367
field_name = field_asts[0].name.value
368+
field_def = parent_type.fields.get(field_name)
369+
field_return_type = str(field_def.type) if field_def else "<unknown>"
362370

363371
with GraphQLResolverTrace(field_name) as trace:
364372
with ErrorTrace(ignore=ignore_graphql_duplicate_exception):
365373
trace._add_agent_attribute("graphql.field.parentType", parent_type.name)
374+
trace._add_agent_attribute("graphql.field.returnType", field_return_type)
375+
366376
if isinstance(field_path, list):
367377
trace._add_agent_attribute("graphql.field.path", field_path[0])
368378
else:

tests/framework_graphql/test_application.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def error_middleware(next, root, info, **args):
8686

8787

8888
@dt_enabled
89-
def test_basic(app, graphql_run):
89+
def test_basic(app, graphql_run, is_graphql_2):
9090
from graphql import __version__ as version
9191

9292
FRAMEWORK_METRICS = [
@@ -114,6 +114,7 @@ def test_basic(app, graphql_run):
114114
"graphql.field.name": "storage_add",
115115
"graphql.field.parentType": "Mutation",
116116
"graphql.field.path": "storage_add",
117+
"graphql.field.returnType": "[String]" if is_graphql_2 else "String",
117118
}
118119
_expected_query_operation_attributes = {
119120
"graphql.operation.type": "query",
@@ -123,6 +124,7 @@ def test_basic(app, graphql_run):
123124
"graphql.field.name": "storage",
124125
"graphql.field.parentType": "Query",
125126
"graphql.field.path": "storage",
127+
"graphql.field.returnType": "[String]",
126128
}
127129

128130
@validate_transaction_metrics(
@@ -189,14 +191,15 @@ def test_exception_in_middleware(app, graphql_run):
189191
_test_exception_rollup_metrics = [
190192
("Errors/all", 1),
191193
("Errors/allOther", 1),
192-
("Errors/OtherTransaction/GraphQL/query/MyQuery/%s" % field, 1),
194+
("Errors/OtherTransaction/GraphQL/test_application:error_middleware", 1),
193195
] + _test_exception_scoped_metrics
194196

195197
# Attributes
196198
_expected_exception_resolver_attributes = {
197199
"graphql.field.name": field,
198200
"graphql.field.parentType": "Query",
199201
"graphql.field.path": field,
202+
"graphql.field.returnType": "String",
200203
}
201204
_expected_exception_operation_attributes = {
202205
"graphql.operation.type": "query",
@@ -205,7 +208,7 @@ def test_exception_in_middleware(app, graphql_run):
205208
}
206209

207210
@validate_transaction_metrics(
208-
"query/MyQuery/hello",
211+
"test_application:error_middleware",
209212
"GraphQL",
210213
scoped_metrics=_test_exception_scoped_metrics,
211214
rollup_metrics=_test_exception_rollup_metrics + _graphql_base_rollup_metrics,
@@ -224,13 +227,10 @@ def _test():
224227

225228
@pytest.mark.parametrize("field", ("error", "error_non_null"))
226229
@dt_enabled
227-
def test_exception_in_resolver(app, graphql_run, is_graphql_2, field):
230+
def test_exception_in_resolver(app, graphql_run, field):
228231
query = "query MyQuery { %s }" % field
229232

230-
if is_graphql_2 and field == "error_non_null":
231-
txn_name = "_target_application:resolve_error"
232-
else:
233-
txn_name = "query/MyQuery/%s" % field
233+
txn_name = "_target_application:resolve_error"
234234

235235
# Metrics
236236
_test_exception_scoped_metrics = [
@@ -248,6 +248,7 @@ def test_exception_in_resolver(app, graphql_run, is_graphql_2, field):
248248
"graphql.field.name": field,
249249
"graphql.field.parentType": "Query",
250250
"graphql.field.path": field,
251+
"graphql.field.returnType": "String!" if "non_null" in field else "String",
251252
}
252253
_expected_exception_operation_attributes = {
253254
"graphql.operation.type": "query",
@@ -366,6 +367,7 @@ def test_field_resolver_metrics_and_attrs(app, graphql_run):
366367
"graphql.field.name": "hello",
367368
"graphql.field.parentType": "Query",
368369
"graphql.field.path": "hello",
370+
"graphql.field.returnType": "String",
369371
}
370372

371373
@validate_transaction_metrics(
@@ -462,8 +464,13 @@ def _test():
462464
@dt_enabled
463465
@pytest.mark.parametrize("query,expected_path", _test_queries)
464466
def test_deepest_unique_path(app, graphql_run, query, expected_path):
467+
if expected_path == "/error":
468+
txn_name = "_target_application:resolve_error"
469+
else:
470+
txn_name = "query/<anonymous>%s" % expected_path
471+
465472
@validate_transaction_metrics(
466-
"query/<anonymous>%s" % expected_path,
473+
txn_name,
467474
"GraphQL",
468475
background_task=True,
469476
)

tests/framework_graphql/test_application_async.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from testing_support.validators.validate_span_events import validate_span_events
99
from newrelic.api.background_task import background_task
1010

11+
from test_application import is_graphql_2
12+
1113

1214
@pytest.fixture(scope="session")
1315
def graphql_run_async():
@@ -25,7 +27,7 @@ def graphql_run(*args, **kwargs):
2527

2628

2729
@dt_enabled
28-
def test_basic_async(app, graphql_run_async):
30+
def test_basic_async(app, graphql_run_async, is_graphql_2):
2931
from graphql import __version__ as version
3032

3133
FRAMEWORK_METRICS = [
@@ -53,6 +55,7 @@ def test_basic_async(app, graphql_run_async):
5355
"graphql.field.name": "storage_add",
5456
"graphql.field.parentType": "Mutation",
5557
"graphql.field.path": "storage_add",
58+
"graphql.field.returnType": "[String]" if is_graphql_2 else "String",
5659
}
5760
_expected_query_operation_attributes = {
5861
"graphql.operation.type": "query",
@@ -62,6 +65,7 @@ def test_basic_async(app, graphql_run_async):
6265
"graphql.field.name": "storage",
6366
"graphql.field.parentType": "Query",
6467
"graphql.field.path": "storage",
68+
"graphql.field.returnType": "[String]",
6569
}
6670

6771
@validate_transaction_metrics(

0 commit comments

Comments
 (0)