Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion packages/plugins/on-resolve/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export type UseOnResolveOptions = {
* @default true
*/
skipIntrospection?: boolean;

/**
* Skip executing the `onResolve` hook on fields with default resolvers.
*
* @default false
*/
skipDefaultResolvers?: boolean;
};

/**
Expand All @@ -49,7 +56,7 @@ export type UseOnResolveOptions = {
*/
export function useOnResolve<PluginContext extends Record<string, any> = {}>(
onResolve: OnResolve<PluginContext>,
opts: UseOnResolveOptions = { skipIntrospection: true },
opts: UseOnResolveOptions = { skipIntrospection: true, skipDefaultResolvers: false },
): Plugin<PluginContext> {
const hasWrappedResolveSymbol = Symbol('hasWrappedResolve');
return {
Expand All @@ -61,6 +68,12 @@ export function useOnResolve<PluginContext extends Record<string, any> = {}>(
if ((!opts.skipIntrospection || !isIntrospectionType(type)) && isObjectType(type)) {
for (const field of Object.values(type.getFields())) {
if ((field as { [hasWrappedResolveSymbol]?: true })[hasWrappedResolveSymbol]) continue;
if (
opts.skipDefaultResolvers &&
(!field.resolve || field.resolve === defaultFieldResolver)
) {
continue;
}

let resolver = (field.resolve || defaultFieldResolver) as Resolver<PluginContext>;

Expand Down
49 changes: 49 additions & 0 deletions packages/plugins/on-resolve/test/use-on-resolve.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,20 @@ describe('useOnResolve', () => {
type Query {
value1: String!
value2: String!
obj: Obj!
}

type Obj {
field1: String!
}
`,
resolvers: {
Query: {
value1: () => 'value1',
value2: () => 'value2',
obj: () => ({
field1: 'field1',
}),
},
},
});
Expand Down Expand Up @@ -85,6 +93,47 @@ describe('useOnResolve', () => {
expect(onResolveDoneFn).toHaveBeenCalledTimes(0);
});

it('should invoke the callback for default resolvers when not skipping', async () => {
const onResolveDoneFn = jest.fn();
const onResolveFn = jest.fn((_opts: OnResolveOptions) => onResolveDoneFn);
const testkit = createTestkit(
[useOnResolve(onResolveFn, { skipDefaultResolvers: false })],
schema,
);

await testkit.execute('{ obj { field1 } }');

expect(onResolveFn).toHaveBeenCalledTimes(2);
expect(onResolveDoneFn).toHaveBeenCalledTimes(2);

let i = 0;
for (const field of ['obj', 'field1']) {
expect(onResolveFn.mock.calls[i][0].context).toBeDefined();
expect(onResolveFn.mock.calls[i][0].root).toBeDefined();
expect(onResolveFn.mock.calls[i][0].args).toBeDefined();
expect(onResolveFn.mock.calls[i][0].info).toBeDefined();
expect(onResolveFn.mock.calls[i][0].info.fieldName).toBe(field);
expect(onResolveFn.mock.calls[i][0].resolver).toBeInstanceOf(Function);
expect(onResolveFn.mock.calls[i][0].replaceResolver).toBeInstanceOf(Function);

i++;
}
});

it('should not invoke the callback for default resolvers when skipping', async () => {
const onResolveDoneFn = jest.fn();
const onResolveFn = jest.fn((_opts: OnResolveOptions) => onResolveDoneFn);
const testkit = createTestkit(
[useOnResolve(onResolveFn, { skipDefaultResolvers: true })],
schema,
);

await testkit.execute('{ obj { field1 } }');

expect(onResolveFn).toHaveBeenCalledTimes(1);
expect(onResolveDoneFn).toHaveBeenCalledTimes(1);
});

it('should replace the result using the after hook', async () => {
const testkit = createTestkit(
[
Expand Down
66 changes: 35 additions & 31 deletions packages/plugins/opentelemetry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type ExcludeOperationNamesFn = (operationName: string | undefined) => boo
export type TracingOptions = {
document?: boolean;
resolvers?: boolean;
defaultResolvers?: boolean;
variables?: boolean | ResolveVariablesAttributesFn;
result?: boolean;
traceIdInResult?: string;
Expand Down Expand Up @@ -89,39 +90,42 @@ export const useOpenTelemetry = (
onPluginInit({ addPlugin }) {
if (options.resolvers) {
addPlugin(
useOnResolve(({ info, context, args }) => {
const parentSpan = spanByContext.get(context);
if (parentSpan) {
const ctx = opentelemetry.trace.setSpan(getCurrentOtelContext(context), parentSpan);
const { fieldName, returnType, parentType } = info;

const resolverSpan = tracer.startSpan(
`${spanPrefix}${parentType.name}.${fieldName}`,
{
attributes: {
[AttributeName.RESOLVER_FIELD_NAME]: fieldName,
[AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(),
[AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(),
[AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}),
useOnResolve(
({ info, context, args }) => {
const parentSpan = spanByContext.get(context);
if (parentSpan) {
const ctx = opentelemetry.trace.setSpan(getCurrentOtelContext(context), parentSpan);
const { fieldName, returnType, parentType } = info;

const resolverSpan = tracer.startSpan(
`${spanPrefix}${parentType.name}.${fieldName}`,
{
attributes: {
[AttributeName.RESOLVER_FIELD_NAME]: fieldName,
[AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(),
[AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(),
[AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}),
},
},
},
ctx,
);

return ({ result }) => {
if (result instanceof Error) {
resolverSpan.recordException({
name: AttributeName.RESOLVER_EXCEPTION,
message: JSON.stringify(result),
});
} else {
resolverSpan.end();
}
};
}
ctx,
);

return ({ result }) => {
if (result instanceof Error) {
resolverSpan.recordException({
name: AttributeName.RESOLVER_EXCEPTION,
message: JSON.stringify(result),
});
} else {
resolverSpan.end();
}
};
}

return () => {};
}),
return () => {};
},
{ skipDefaultResolvers: options.defaultResolvers === false },
),
);
}
},
Expand Down
53 changes: 51 additions & 2 deletions packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ describe('useOpenTelemetry', () => {
echo(message: String): String
error: String
context: String
obj: Obj
}

type Obj {
field1: String!
}

type Subscription {
Expand All @@ -48,6 +53,9 @@ describe('useOpenTelemetry', () => {
error: () => {
throw new GraphQLError('boom');
},
obj: () => ({
field1: 'field1',
}),
},
Subscription: {
counter: {
Expand Down Expand Up @@ -130,6 +138,49 @@ describe('useOpenTelemetry', () => {
expect(actual[1].name).toBe('query.anonymous');
});

it('Should add default resolver spans if enabled / unspecified', async () => {
const exporter = new InMemorySpanExporter();
const testInstance = createTestkit(
[useTestOpenTelemetry(exporter, { resolvers: true })],
schema,
);

await testInstance.execute(/* GraphQL */ `
query {
obj {
field1
}
}
`);

const actual = exporter.getFinishedSpans();
expect(actual.length).toBe(3);
expect(actual[0].name).toBe('Query.obj');
expect(actual[1].name).toBe('Obj.field1');
expect(actual[2].name).toBe('query.anonymous');
});

it('Should not add default resolver spans if disabled', async () => {
const exporter = new InMemorySpanExporter();
const testInstance = createTestkit(
[useTestOpenTelemetry(exporter, { resolvers: true, defaultResolvers: false })],
schema,
);

await testInstance.execute(/* GraphQL */ `
query {
obj {
field1
}
}
`);

const actual = exporter.getFinishedSpans();
expect(actual.length).toBe(2);
expect(actual[0].name).toBe('Query.obj');
expect(actual[1].name).toBe('query.anonymous');
});

it('query should add trace_id to extensions', async () => {
const exporter = new InMemorySpanExporter();
const testInstance = createTestkit(
Expand Down Expand Up @@ -299,8 +350,6 @@ describe('useOpenTelemetry', () => {
selector: '1',
});

console.log(resp);

assertSingleValue(resp);

const actual = exporter.getFinishedSpans();
Expand Down
Loading