Skip to content

Commit 1fd58cc

Browse files
committed
apollo-engine-reporting: fix reporting errors from backends
The extension stack executionDidEnd method gets called before didEncounterErrors for GraphQL errors returned from execution (although confusingly the plugin executionDidEnd method gets called after), which caused the assertion that nothing gets added to the EngineReportingTreeBuilder after stopTiming to fail. Fix by moving stopTiming to the last possible moment: format(). Actually test error reporting, including both kinds of rewriting. Add a comment noting that backend parse and validation errors don't get reported. Fixes #3052.
1 parent 31028e7 commit 1fd58cc

5 files changed

Lines changed: 93 additions & 70 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The version headers in this history reflect the versions of Apollo Server itself
66

77
> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section.
88
9+
- `apollo-engine-reporting`: fix reporting errors from backend. (The support for federated metrics introduced in v2.7.0 did not properly handle GraphQL errors from the backend; all users of federated metrics should upgrade to this version.) [PR #3056](https://github.com/apollographql/apollo-server/pull/3056) [Issue #3052](https://github.com/apollographql/apollo-server/issues/3052)
910
- `apollo-engine-reporting`: clean up `SIGINT` and `SIGTERM` handlers when `EngineReportingAgent` is stopped; fixes 'Possible EventEmitter memory leak detected' log. [PR #3090](https://github.com/apollographql/apollo-server/pull/3090)
1011

1112
### v2.7.1

package-lock.json

Lines changed: 20 additions & 40 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/apollo-engine-reporting/src/federatedExtension.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GraphQLResolveInfo, GraphQLError } from 'graphql';
2-
import { GraphQLExtension, EndHandler } from 'graphql-extensions';
2+
import { GraphQLExtension } from 'graphql-extensions';
33
import { Trace } from 'apollo-engine-reporting-protobuf';
44
import { GraphQLRequestContext } from 'apollo-server-types';
55

@@ -53,30 +53,27 @@ export class EngineFederatedTracingExtension<TContext = any>
5353
}
5454
}
5555

56-
public executionDidStart(): EndHandler | void {
57-
if (this.enabled) {
58-
// It's a little odd that we record the end time after execution rather than
59-
// at the end of the whole request, but because we need to include our
60-
// formatted trace in the request itself, we have to record it before the
61-
// request is over! It's also odd that we don't do traces for parse or
62-
// validation errors, but runQuery doesn't currently support that, as
63-
// format() is only invoked after execution.
64-
return () => {
65-
this.treeBuilder.stopTiming();
66-
this.done = true;
67-
};
68-
}
69-
}
70-
7156
// The ftv1 extension is a base64'd Trace protobuf containing only the
7257
// durationNs, startTime, endTime, and root fields.
58+
//
59+
// Note: format() is only called after executing an operation, and
60+
// specifically isn't called for parse or validation errors. Parse and validation
61+
// errors in a federated backend will get reported to the end user as a downstream
62+
// error but will not get reported to Engine (because Engine filters out downstream
63+
// errors)! See #3091.
7364
public format(): [string, string] | undefined {
7465
if (!this.enabled) {
7566
return;
7667
}
77-
if (!this.done) {
78-
throw Error('format called before end of execution?');
68+
if (this.done) {
69+
throw Error('format called twice?');
7970
}
71+
72+
// We record the end time at the latest possible time: right before serializing the trace.
73+
// If we wait any longer, the time we record won't actually be sent anywhere!
74+
this.treeBuilder.stopTiming();
75+
this.done = true;
76+
8077
const encodedUint8Array = Trace.encode(this.treeBuilder.trace).finish();
8178
const encodedBuffer = Buffer.from(
8279
encodedUint8Array,

packages/apollo-engine-reporting/src/treeBuilder.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {
66
} from 'graphql';
77
import { Trace, google } from 'apollo-engine-reporting-protobuf';
88

9+
function internalError(message: string) {
10+
return new Error(`[internal apollo-server error] ${message}`);
11+
}
12+
913
export class EngineReportingTreeBuilder {
1014
private rootNode = new Trace.Node();
1115
public trace = new Trace({ root: this.rootNode });
@@ -24,21 +28,21 @@ export class EngineReportingTreeBuilder {
2428

2529
public startTiming() {
2630
if (this.startHrTime) {
27-
throw Error('startTiming called twice!');
31+
throw internalError('startTiming called twice!');
2832
}
2933
if (this.stopped) {
30-
throw Error('startTiming called after stopTiming!');
34+
throw internalError('startTiming called after stopTiming!');
3135
}
3236
this.trace.startTime = dateToProtoTimestamp(new Date());
3337
this.startHrTime = process.hrtime();
3438
}
3539

3640
public stopTiming() {
3741
if (!this.startHrTime) {
38-
throw Error('stopTiming called before startTiming!');
42+
throw internalError('stopTiming called before startTiming!');
3943
}
4044
if (this.stopped) {
41-
throw Error('stopTiming called twice!');
45+
throw internalError('stopTiming called twice!');
4246
}
4347

4448
this.trace.durationNs = durationHrTimeToNanos(
@@ -50,10 +54,10 @@ export class EngineReportingTreeBuilder {
5054

5155
public willResolveField(info: GraphQLResolveInfo): () => void {
5256
if (!this.startHrTime) {
53-
throw Error('willResolveField called before startTiming!');
57+
throw internalError('willResolveField called before startTiming!');
5458
}
5559
if (this.stopped) {
56-
throw Error('willResolveField called after stopTiming!');
60+
throw internalError('willResolveField called after stopTiming!');
5761
}
5862

5963
const path = info.path;
@@ -75,6 +79,10 @@ export class EngineReportingTreeBuilder {
7579
errors.forEach(err => {
7680
// This is an error from a federated service. We will already be reporting
7781
// it in the nested Trace in the query plan.
82+
//
83+
// XXX This probably shouldn't skip query or validation errors, which are
84+
// not in nested Traces because format() isn't called in this case! Or
85+
// maybe format() should be called in that case?
7886
if (err.extensions && err.extensions.serviceName) {
7987
return;
8088
}
@@ -101,10 +109,10 @@ export class EngineReportingTreeBuilder {
101109
error: Trace.Error,
102110
) {
103111
if (!this.startHrTime) {
104-
throw Error('addProtobufError called before startTiming!');
112+
throw internalError('addProtobufError called before startTiming!');
105113
}
106114
if (this.stopped) {
107-
throw Error('addProtobufError called after stopTiming!');
115+
throw internalError('addProtobufError called after stopTiming!');
108116
}
109117

110118
// By default, put errors on the root node.

0 commit comments

Comments
 (0)