Skip to content

Commit 62e7d94

Browse files
Fix trace placement for errors occurring in lists (#7699)
Previously, when errors occurred while resolving a list item, the trace builder would fail to place the error at the correct path and just default to the root node with a warning message: > `Could not find node with path x.y.1, defaulting to put errors on root node.` This change places these errors at their correct paths and removes the log.
1 parent 6df54b8 commit 62e7d94

File tree

9 files changed

+136
-15
lines changed

9 files changed

+136
-15
lines changed

.changeset/gentle-cycles-look.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@apollo/server': patch
3+
---
4+
5+
Fix error path attachment for list items
6+
7+
Previously, when errors occurred while resolving a list item, the trace builder would fail to place the error at the correct path and just default to the root node with a warning message:
8+
9+
> `Could not find node with path x.y.1, defaulting to put errors on root node.`
10+
11+
This change places these errors at their correct paths and removes the log.

cspell-dict.txt

+1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ tsconfig
189189
tsconfigs
190190
typecheck
191191
typeis
192+
typenames
192193
typeof
193194
typesafe
194195
unawaited

jest.config.base.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { defaults } from 'jest-config';
2+
import { createRequire } from 'node:module';
23

34
export default {
45
testEnvironment: 'node',
@@ -22,6 +23,5 @@ export default {
2223
// Ignore '.js' at the end of imports; part of ESM support.
2324
'^(\\.{1,2}/.*)\\.js$': '$1',
2425
},
25-
// this can be removed with jest v29
26-
snapshotFormat: { escapeString: false, printBasicPrototype: false },
26+
prettierPath: createRequire(import.meta.url).resolve('prettier-2'),
2727
};

package-lock.json

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"nock": "13.3.2",
9696
"node-fetch": "2.6.12",
9797
"prettier": "3.0.1",
98+
"prettier-2": "npm:prettier@^2",
9899
"qs-middleware": "1.0.3",
99100
"requisition": "1.7.0",
100101
"rollup": "3.28.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { ApolloServer, HeaderMap } from '@apollo/server';
2+
import { gql } from 'graphql-tag';
3+
import { buildSubgraphSchema } from '@apollo/subgraph';
4+
import { describe, it, expect } from '@jest/globals';
5+
import assert from 'assert';
6+
import { Trace } from '@apollo/usage-reporting-protobuf';
7+
import { ApolloServerPluginInlineTrace } from '@apollo/server/plugin/inlineTrace';
8+
9+
describe('ApolloServerPluginInlineTrace', () => {
10+
it('Adds errors within lists to the correct path rather than the root', async () => {
11+
const server = new ApolloServer({
12+
schema: buildSubgraphSchema({
13+
typeDefs: gql`
14+
type Query {
15+
a: A!
16+
}
17+
type A {
18+
brokenList: [String!]!
19+
}
20+
`,
21+
resolvers: {
22+
Query: {
23+
a: () => ({}),
24+
},
25+
A: {
26+
brokenList() {
27+
return ['hello world!', null];
28+
},
29+
},
30+
},
31+
}),
32+
plugins: [
33+
// silence logs about the plugin being enabled
34+
ApolloServerPluginInlineTrace(),
35+
],
36+
});
37+
await server.start();
38+
const result = await server.executeHTTPGraphQLRequest({
39+
httpGraphQLRequest: {
40+
body: { query: '{a{brokenList}}' },
41+
headers: new HeaderMap([
42+
['content-type', 'application/json'],
43+
['apollo-federation-include-trace', 'ftv1'],
44+
]),
45+
method: 'POST',
46+
search: '',
47+
},
48+
context: async () => ({}),
49+
});
50+
assert(result.body.kind === 'complete');
51+
const trace = Trace.decode(
52+
Buffer.from(JSON.parse(result.body.string).extensions?.ftv1, 'base64'),
53+
);
54+
expect(trace.root?.error).toHaveLength(0);
55+
// error is found at path a.brokenList.1
56+
expect(trace.root?.child?.[0].child?.[0].child?.[0].error)
57+
.toMatchInlineSnapshot(`
58+
[
59+
{
60+
"json": "{"message":"<masked>","locations":[{"line":1,"column":4}],"path":["a","brokenList",1],"extensions":{"maskedBy":"ApolloServerPluginInlineTrace"}}",
61+
"location": [
62+
{
63+
"column": 4,
64+
"line": 1,
65+
},
66+
],
67+
"message": "<masked>",
68+
},
69+
]
70+
`);
71+
72+
await server.stop();
73+
});
74+
});

packages/server/src/plugin/inlineTrace/index.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,14 @@ export function ApolloServerPluginInlineTrace(
6666
}
6767
}
6868
},
69-
async requestDidStart({ request: { http }, metrics, logger }) {
69+
async requestDidStart({ request: { http }, metrics }) {
7070
if (!enabled) {
7171
return;
7272
}
7373

7474
const treeBuilder = new TraceTreeBuilder({
7575
maskedBy: 'ApolloServerPluginInlineTrace',
7676
sendErrors: options.includeErrors,
77-
logger,
7877
});
7978

8079
// XXX Provide a mechanism to customize this logic.

packages/server/src/plugin/traceTreeBuilder.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
type ResponsePath,
77
} from 'graphql';
88
import { Trace, google } from '@apollo/usage-reporting-protobuf';
9-
import type { Logger } from '@apollo/utils.logger';
109
import type { SendErrorsOptions } from './usageReporting';
1110
import { UnreachableCaseError } from '../utils/UnreachableCaseError.js';
1211

@@ -16,7 +15,6 @@ function internalError(message: string) {
1615

1716
export class TraceTreeBuilder {
1817
private rootNode = new Trace.Node();
19-
private logger: Logger;
2018
public trace = new Trace({
2119
root: this.rootNode,
2220
// By default, each trace counts as one operation for the sake of field
@@ -39,10 +37,9 @@ export class TraceTreeBuilder {
3937

4038
public constructor(options: {
4139
maskedBy: string;
42-
logger: Logger;
4340
sendErrors?: SendErrorsOptions;
4441
}) {
45-
const { logger, sendErrors, maskedBy } = options;
42+
const { sendErrors, maskedBy } = options;
4643
if (!sendErrors || 'masked' in sendErrors) {
4744
this.transformError = () =>
4845
new GraphQLError('<masked>', {
@@ -55,7 +52,6 @@ export class TraceTreeBuilder {
5552
} else {
5653
throw new UnreachableCaseError(sendErrors);
5754
}
58-
this.logger = logger;
5955
}
6056

6157
public startTiming() {
@@ -156,11 +152,11 @@ export class TraceTreeBuilder {
156152
if (specificNode) {
157153
node = specificNode;
158154
} else {
159-
this.logger.warn(
160-
`Could not find node with path ${path.join(
161-
'.',
162-
)}; defaulting to put errors on root node.`,
163-
);
155+
const responsePath = responsePathFromArray(path, this.rootNode);
156+
if (!responsePath) {
157+
throw internalError('addProtobufError called with invalid path!');
158+
}
159+
node = this.newNode(responsePath);
164160
}
165161
}
166162

@@ -280,6 +276,23 @@ function responsePathAsString(p?: ResponsePath): string {
280276
return res;
281277
}
282278

279+
function responsePathFromArray(
280+
path: ReadonlyArray<string | number>,
281+
node: Trace.Node,
282+
): ResponsePath | undefined {
283+
let responsePath: ResponsePath | undefined;
284+
let nodePtr: Trace.INode | undefined = node;
285+
for (const key of path) {
286+
nodePtr = nodePtr?.child?.find((child) => child.responseName === key);
287+
responsePath = {
288+
key,
289+
prev: responsePath,
290+
typename: nodePtr?.type ?? undefined,
291+
};
292+
}
293+
return responsePath;
294+
}
295+
283296
function errorToProtobufError(error: GraphQLError): Trace.Error {
284297
return new Trace.Error({
285298
message: error.message,

packages/server/src/plugin/usageReporting/plugin.ts

-1
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,6 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
411411
const treeBuilder: TraceTreeBuilder = new TraceTreeBuilder({
412412
maskedBy: 'ApolloServerPluginUsageReporting',
413413
sendErrors: options.sendErrors,
414-
logger,
415414
});
416415
treeBuilder.startTiming();
417416
metrics.startHrTime = treeBuilder.startHrTime;

0 commit comments

Comments
 (0)