Skip to content

Commit 692cf0a

Browse files
authored
fix: Enable union type support in @opentelemetry/instrumentation-graphql (#1506) (#2923)
1 parent 68989f3 commit 692cf0a

File tree

3 files changed

+258
-35
lines changed

3 files changed

+258
-35
lines changed

packages/instrumentation-graphql/src/utils.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,7 @@ export function wrapFields(
305305
tracer: api.Tracer,
306306
getConfig: () => GraphQLInstrumentationParsedConfig
307307
): void {
308-
if (
309-
!type ||
310-
typeof type.getFields !== 'function' ||
311-
type[OTEL_PATCHED_SYMBOL]
312-
) {
308+
if (!type || type[OTEL_PATCHED_SYMBOL]) {
313309
return;
314310
}
315311
const fields = type.getFields();
@@ -328,16 +324,47 @@ export function wrapFields(
328324
}
329325

330326
if (field.type) {
331-
let unwrappedType: any = field.type;
332-
333-
while (unwrappedType.ofType) {
334-
unwrappedType = unwrappedType.ofType;
327+
const unwrappedTypes = unwrapType(field.type);
328+
for (const unwrappedType of unwrappedTypes) {
329+
wrapFields(unwrappedType, tracer, getConfig);
335330
}
336-
wrapFields(unwrappedType, tracer, getConfig);
337331
}
338332
});
339333
}
340334

335+
function unwrapType(
336+
type: graphqlTypes.GraphQLOutputType
337+
): readonly graphqlTypes.GraphQLObjectType[] {
338+
// unwrap wrapping types (non-nullable and list types)
339+
if ('ofType' in type) {
340+
return unwrapType(type.ofType);
341+
}
342+
343+
// unwrap union types
344+
if (isGraphQLUnionType(type)) {
345+
return type.getTypes();
346+
}
347+
348+
// return object types
349+
if (isGraphQLObjectType(type)) {
350+
return [type];
351+
}
352+
353+
return [];
354+
}
355+
356+
function isGraphQLUnionType(
357+
type: graphqlTypes.GraphQLType
358+
): type is graphqlTypes.GraphQLUnionType {
359+
return 'getTypes' in type && typeof type.getTypes === 'function';
360+
}
361+
362+
function isGraphQLObjectType(
363+
type: graphqlTypes.GraphQLType
364+
): type is graphqlTypes.GraphQLObjectType {
365+
return 'getFields' in type && typeof type.getFields === 'function';
366+
}
367+
341368
const handleResolveSpanError = (
342369
resolveSpan: api.Span,
343370
err: any,

packages/instrumentation-graphql/test/graphql.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,21 @@ const sourceFindUsingVariable = `
8686
}
8787
`;
8888

89+
const sourceSearch = `
90+
query Search ($name: String!) {
91+
search(name: $name) {
92+
... on Book {
93+
__typename
94+
name
95+
}
96+
... on EBook {
97+
__typename
98+
name
99+
}
100+
}
101+
}
102+
`;
103+
89104
const badQuery = `
90105
query foo bar
91106
`;
@@ -244,6 +259,7 @@ describe('graphql', () => {
244259
assert.ok(times[RESOLVE].end <= times[EXECUTE].end);
245260
});
246261
});
262+
247263
describe('AND source is query with param', () => {
248264
let spans: ReadableSpan[];
249265

@@ -338,6 +354,7 @@ describe('graphql', () => {
338354
);
339355
});
340356
});
357+
341358
describe('AND source is query with param and variables', () => {
342359
let spans: ReadableSpan[];
343360

@@ -442,6 +459,110 @@ describe('graphql', () => {
442459
);
443460
});
444461
});
462+
463+
describe('AND source is query to get a list of union type', () => {
464+
let spans: ReadableSpan[];
465+
beforeEach(async () => {
466+
create({});
467+
await graphql({
468+
schema,
469+
source: sourceSearch,
470+
variableValues: { name: 'first' },
471+
});
472+
spans = exporter.getFinishedSpans();
473+
});
474+
475+
afterEach(() => {
476+
exporter.reset();
477+
graphQLInstrumentation.disable();
478+
spans = [];
479+
});
480+
481+
it('should have 6 spans', () => {
482+
assert.deepStrictEqual(spans.length, 6);
483+
});
484+
485+
it('should instrument parse', () => {
486+
const parseSpan = spans[0];
487+
assert.deepStrictEqual(
488+
parseSpan.attributes[AttributeNames.SOURCE],
489+
sourceSearch
490+
);
491+
assert.deepStrictEqual(parseSpan.name, SpanNames.PARSE);
492+
});
493+
494+
it('should instrument validate', () => {
495+
const validateSpan = spans[1];
496+
497+
assert.deepStrictEqual(validateSpan.name, SpanNames.VALIDATE);
498+
assert.deepStrictEqual(
499+
validateSpan.parentSpanContext?.spanId,
500+
undefined
501+
);
502+
});
503+
504+
it('should instrument execute', () => {
505+
const executeSpan = spans[5];
506+
507+
assert.deepStrictEqual(
508+
executeSpan.attributes[AttributeNames.SOURCE],
509+
sourceSearch
510+
);
511+
assert.deepStrictEqual(
512+
executeSpan.attributes[AttributeNames.OPERATION_TYPE],
513+
'query'
514+
);
515+
assert.deepStrictEqual(
516+
executeSpan.attributes[AttributeNames.OPERATION_NAME],
517+
'Search'
518+
);
519+
assert.deepStrictEqual(executeSpan.name, 'query Search');
520+
assert.deepStrictEqual(
521+
executeSpan.parentSpanContext?.spanId,
522+
undefined
523+
);
524+
});
525+
526+
it('should instrument resolvers', () => {
527+
const [, , resolveParentSpan, span1, span2, executeSpan] = spans;
528+
529+
assertResolveSpan(
530+
resolveParentSpan,
531+
'search',
532+
'search',
533+
'[SearchResult]',
534+
'search(name: $name) {\n' +
535+
' ... on Book {\n' +
536+
' __typename\n' +
537+
' name\n' +
538+
' }\n' +
539+
' ... on EBook {\n' +
540+
' __typename\n' +
541+
' name\n' +
542+
' }\n' +
543+
' }',
544+
executeSpan.spanContext().spanId
545+
);
546+
547+
const parentId = resolveParentSpan.spanContext().spanId;
548+
assertResolveSpan(
549+
span1,
550+
'name',
551+
'search.0.name',
552+
'String',
553+
'name',
554+
parentId
555+
);
556+
assertResolveSpan(
557+
span2,
558+
'name',
559+
'search.1.name',
560+
'String',
561+
'name',
562+
parentId
563+
);
564+
});
565+
});
445566
});
446567

447568
describe('when depth is set to 0', () => {

0 commit comments

Comments
 (0)