Skip to content

Commit

Permalink
Expand on non-nullability of _Service.sdl field
Browse files Browse the repository at this point in the history
Fed v2 updated the subgraph spec to make _Service.sdl non-nullable.
We should explain this thoroughly in our docs, test for both cases,
and update a relevant recent changelog entry. Follow-up to #7274.
  • Loading branch information
trevor-scheer committed Jan 13, 2023
1 parent 48b38af commit d556b30
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 77 deletions.
5 changes: 4 additions & 1 deletion .changeset/quiet-pears-float.md
Expand Up @@ -2,4 +2,7 @@
'@apollo/server': patch
---

Update schemaIsSubgraph to also support non nullable \_Service.sdl
The subgraph spec has evolved in Federation v2 such that the type of
`_Service.sdl` (formerly nullable) is now non-nullable. Apollo Server now
detects both cases correctly in order to determine whether to install / enable
the `ApolloServerPluginInlineTrace` plugin.
4 changes: 2 additions & 2 deletions docs/source/api/plugin/inline-trace.mdx
Expand Up @@ -11,7 +11,7 @@ This article documents the options for the `ApolloServerPluginInlineTrace` plugi

This plugin enables your GraphQL server to include encoded performance and usage traces inside responses. This is primarily designed for use with [Apollo Federation](/federation/metrics/). Federated subgraphs use this plugin and include a trace in the `ftv1` GraphQL response extension if requested to do so by the Apollo gateway. The gateway requests this trace by passing the HTTP header `apollo-federation-include-trace: ftv1`.

Apollo Server installs this plugin by default in all federated subgraphs, with its default configuration. Apollo Server inspects whether or not the schema it is serving includes a field `_Service.sdl: String` to determine if it is a federated subgraph. You typically do not have to install this plugin yourself; you only need to do so if you want to provide non-default configuration.
Apollo Server installs this plugin by default in all federated subgraphs, with its default configuration. Apollo Server inspects whether or not the schema it is serving includes a field `_Service.sdl: String!` (as well as its former nullable version `_Service.sdl: String`) to determine if it is a federated subgraph. You typically do not have to install this plugin yourself; you only need to do so if you want to provide non-default configuration.

If you want to configure the `ApolloServerPluginInlineTrace` plugin, import it and pass it to your `ApolloServer` constructor's `plugins` array:

Expand All @@ -34,7 +34,7 @@ const server = new ApolloServer({

</MultiCodeBlock>

If you don't want to use the inline trace plugin even though your schema defines `_Service.sdl: String`, you can explicitly disable it with the `ApolloServerPluginInlineTraceDisabled` plugin:
If you don't want to use the inline trace plugin even though your schema defines `_Service.sdl: String!`, you can explicitly disable it with the `ApolloServerPluginInlineTraceDisabled` plugin:

<MultiCodeBlock>

Expand Down
163 changes: 89 additions & 74 deletions packages/integration-testsuite/src/apolloServerTests.ts
Expand Up @@ -2456,12 +2456,18 @@ export function defineIntegrationTestSuiteApolloServerTests(

describe('Federated tracing', () => {
// Enable federated tracing by pretending to be federated.
const federationTypeDefs = gql`
const federationV1TypeDefs = gql`
type _Service {
sdl: String
}
`;

const federationV2TypeDefs = gql`
type _Service {
sdl: String!
}
`;

const baseTypeDefs = gql`
type Book {
title: String
Expand All @@ -2479,7 +2485,8 @@ export function defineIntegrationTestSuiteApolloServerTests(
}
`;

const allTypeDefs = [federationTypeDefs, baseTypeDefs];
const v1TypeDefs = [federationV1TypeDefs, baseTypeDefs];
const v2TypeDefs = [federationV2TypeDefs, baseTypeDefs];

const resolvers = {
Query: {
Expand All @@ -2506,7 +2513,7 @@ export function defineIntegrationTestSuiteApolloServerTests(

it("doesn't include federated trace without the special header", async () => {
const uri = await createServerAndGetUrl({
typeDefs: allTypeDefs,
typeDefs: v2TypeDefs,
resolvers,
logger: quietLogger,
});
Expand Down Expand Up @@ -2535,94 +2542,102 @@ export function defineIntegrationTestSuiteApolloServerTests(
expect(result.extensions).toBeUndefined();
});

it('reports a total duration that is longer than the duration of its resolvers', async () => {
const uri = await createServerAndGetUrl({
typeDefs: allTypeDefs,
resolvers,
logger: quietLogger,
});
describe.each([
['nullable _Service.sdl field', v1TypeDefs],
['non-nullable _Service.sdl! field', v2TypeDefs],
])('with %s', (_, typeDefs) => {
it('reports a total duration that is longer than the duration of its resolvers', async () => {
const uri = await createServerAndGetUrl({
typeDefs,
resolvers,
logger: quietLogger,
});

const apolloFetch = createApolloFetchAsIfFromGateway(uri);
const apolloFetch = createApolloFetchAsIfFromGateway(uri);

const result = await apolloFetch({
query: `{ books { title author } }`,
});
const result = await apolloFetch({
query: `{ books { title author } }`,
});

const ftv1: string = result.extensions.ftv1;
const ftv1: string = result.extensions.ftv1;

expect(ftv1).toBeTruthy();
const encoded = Buffer.from(ftv1, 'base64');
const trace = Trace.decode(encoded);
expect(ftv1).toBeTruthy();
const encoded = Buffer.from(ftv1, 'base64');
const trace = Trace.decode(encoded);

let earliestStartOffset = Infinity;
let latestEndOffset = -Infinity;
let earliestStartOffset = Infinity;
let latestEndOffset = -Infinity;

function walk(node: Trace.Node) {
if (node.startTime !== 0 && node.endTime !== 0) {
earliestStartOffset = Math.min(earliestStartOffset, node.startTime);
latestEndOffset = Math.max(latestEndOffset, node.endTime);
function walk(node: Trace.Node) {
if (node.startTime !== 0 && node.endTime !== 0) {
earliestStartOffset = Math.min(
earliestStartOffset,
node.startTime,
);
latestEndOffset = Math.max(latestEndOffset, node.endTime);
}
node.child.forEach((n) => walk(n as Trace.Node));
}
node.child.forEach((n) => walk(n as Trace.Node));
}

walk(trace.root as Trace.Node);
expect(earliestStartOffset).toBeLessThan(Infinity);
expect(latestEndOffset).toBeGreaterThan(-Infinity);
const resolverDuration = latestEndOffset - earliestStartOffset;
expect(resolverDuration).toBeGreaterThan(0);
expect(trace.durationNs).toBeGreaterThanOrEqual(resolverDuration);
walk(trace.root as Trace.Node);
expect(earliestStartOffset).toBeLessThan(Infinity);
expect(latestEndOffset).toBeGreaterThan(-Infinity);
const resolverDuration = latestEndOffset - earliestStartOffset;
expect(resolverDuration).toBeGreaterThan(0);
expect(trace.durationNs).toBeGreaterThanOrEqual(resolverDuration);

expect(trace.startTime!.seconds).toBeLessThanOrEqual(
trace.endTime!.seconds!,
);
if (trace.startTime!.seconds === trace.endTime!.seconds) {
expect(trace.startTime!.nanos).toBeLessThanOrEqual(
trace.endTime!.nanos!,
expect(trace.startTime!.seconds).toBeLessThanOrEqual(
trace.endTime!.seconds!,
);
}
});
if (trace.startTime!.seconds === trace.endTime!.seconds) {
expect(trace.startTime!.nanos).toBeLessThanOrEqual(
trace.endTime!.nanos!,
);
}
});

it('includes errors in federated trace', async () => {
const uri = await createServerAndGetUrl({
typeDefs: allTypeDefs,
resolvers,
formatError(err) {
return {
...err,
message: `Formatted: ${err.message}`,
};
},
plugins: [
ApolloServerPluginInlineTrace({
includeErrors: {
transform(err) {
err.message = `Rewritten for Usage Reporting: ${err.message}`;
return err;
it('includes errors in federated trace', async () => {
const uri = await createServerAndGetUrl({
typeDefs,
resolvers,
formatError(err) {
return {
...err,
message: `Formatted: ${err.message}`,
};
},
plugins: [
ApolloServerPluginInlineTrace({
includeErrors: {
transform(err) {
err.message = `Rewritten for Usage Reporting: ${err.message}`;
return err;
},
},
},
}),
],
});
}),
],
});

const apolloFetch = createApolloFetchAsIfFromGateway(uri);
const apolloFetch = createApolloFetchAsIfFromGateway(uri);

const result = await apolloFetch({
query: `{ error }`,
});
const result = await apolloFetch({
query: `{ error }`,
});

expect(result.data).toStrictEqual({ error: null });
expect(result.errors).toBeTruthy();
expect(result.errors.length).toBe(1);
expect(result.errors[0].message).toBe('Formatted: It broke');
expect(result.data).toStrictEqual({ error: null });
expect(result.errors).toBeTruthy();
expect(result.errors.length).toBe(1);
expect(result.errors[0].message).toBe('Formatted: It broke');

const ftv1: string = result.extensions.ftv1;
const ftv1: string = result.extensions.ftv1;

expect(ftv1).toBeTruthy();
const encoded = Buffer.from(ftv1, 'base64');
const trace = Trace.decode(encoded);
expect(trace.root!.child![0].error![0].message).toBe(
'Rewritten for Usage Reporting: It broke',
);
expect(ftv1).toBeTruthy();
const encoded = Buffer.from(ftv1, 'base64');
const trace = Trace.decode(encoded);
expect(trace.root!.child![0].error![0].message).toBe(
'Rewritten for Usage Reporting: It broke',
);
});
});
});

Expand Down

0 comments on commit d556b30

Please sign in to comment.