Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apollo-engine-reporting) Introduce rewriteError to munge errors for reporting. #2618

Merged
merged 21 commits into from
Apr 30, 2019
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fb51125
Add errorFilter option
lpgera Sep 7, 2018
c285a74
update changelog
lpgera Sep 10, 2018
6024745
Rename errorFilter to filterErrors, return GraphQLError or null
lpgera Sep 13, 2018
dea9656
Add back `maskErrorDetails` type to maintain support.
abernix Apr 8, 2019
9897cba
Introduce additional tests for `filterErrors` and `maskErrorDetails`.
abernix Apr 8, 2019
25d05ae
Finish up new `filterErrors` functionality, sans renaming to a new name.
abernix Apr 8, 2019
f9b13dd
Add a test that ensures that the `stack` is not transmitted.
abernix Apr 8, 2019
4e705af
Rename new `filterErrors` function to a more accurate `rewriteError`.
abernix Apr 8, 2019
bcfc2c5
Adjust the line lengths of some comments.
abernix Apr 8, 2019
9fb8015
Add CHANGELOG.md for `rewriteError`.
abernix Apr 8, 2019
b18302c
Update deprecated to use rewriteError
michaelwatson93 Apr 16, 2019
21b8b86
Update rewriteError in docs
michaelwatson93 Apr 16, 2019
e2279ee
Merge remote-tracking branch 'origin/release-2.5.0' into abernix/fini…
abernix Apr 26, 2019
7ffd5e2
Remove now-unnecessary guard which checks for `errors`.
abernix Apr 26, 2019
3587aa1
Remove long-skipped test that was never close to working.
abernix Apr 26, 2019
f67fe77
Fix test structure after merge.
abernix Apr 26, 2019
acc1198
Partially Revert "Update rewriteError in docs"
abernix Apr 28, 2019
e99a99a
Update the API section with more clarity.
abernix Apr 28, 2019
8321d3b
Update docs for rewriteError. (#2585)
michael-watson Apr 30, 2019
c422d27
Merge branch 'release-2.5.0' into abernix/finish-pr-1639
abernix Apr 30, 2019
dc8b133
Update CHANGELOG.md
abernix Apr 30, 2019
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- `apollo-server-express`: Fix Playground URL when Apollo Server is mounted inside of another Express app by utilizing `req.originalUrl`. [PR #2451](https://github.com/apollographql/apollo-server/pull/2451)
- New plugin package `apollo-server-plugin-response-cache` implementing a full query response cache based on `apollo-cache-control` hints. The implementation added a few hooks and context fields; see the PR for details. There is a slight change to `cacheControl` object: previously, `cacheControl.stripFormattedExtensions` defaulted to false if you did not provide a `cacheControl` option object, but defaulted to true if you provided (eg) `cacheControl: {defaultMaxAge: 10}`. Now `stripFormattedExtensions` defaults to false unless explicitly provided as `true`, or if you use the legacy boolean `cacheControl: true`. [PR #2437](https://github.com/apollographql/apollo-server/pull/2437)
- Allow `GraphQLRequestListener` callbacks in plugins to depend on `this`. [PR #2470](https://github.com/apollographql/apollo-server/pull/2470)
- Add `rewriteError` option to `EngineReportingOptions` (i.e. the `engine` property of the `ApolloServer` constructor). When defined as a `function`, it will receive an `err` property as its first argument which can be used to manipulate (e.g. redaction) an error prior to sending it to Apollo Engine by modifying, e.g., its `message` property. The error can also be suppressed from reporting entirely by returning an explicit `null` value. [PR #1639](https://github.com/apollographql/apollo-server/pull/1639)

### v2.4.8

Expand Down
9 changes: 6 additions & 3 deletions docs/source/api/apollo-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,13 @@ addMockFunctionsToSchema({
'sendReport()' on other signals if you'd like. Note that 'sendReport()'
does not run synchronously so it cannot work usefully in an 'exit' handler.

* `maskErrorDetails`: boolean
* `rewriteError`: (err: GraphQLError) => GraphQLError | null

Set to true to remove error details from the traces sent to Apollo's servers. Defaults to false.
By default, all errors are reported to Apollo Engine. This function
can be used to exclude specific errors from being reported. This function
receives a copy of the `GraphQLError` and can manipulate it for the
purposes of Apollo Engine reporting. The modified error should be returned
or the function should return `null` to avoid reporting the error entirely.

* `generateClientInfo`: (GraphQLRequestContext) => ClientInfo **AS 2.2**

Expand All @@ -388,4 +392,3 @@ addMockFunctionsToSchema({
> [WARNING] If you specify a `clientReferenceId`, Engine will treat the
> `clientName` as a secondary lookup, so changing a `clientName` may result
> in an unwanted experience.

40 changes: 21 additions & 19 deletions docs/source/features/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ When an error occurs in Apollo server both inside and outside of resolvers, each
The first step to improving the usability of a server is providing the error stack trace by default. The following example demonstrates the response returned from Apollo server with a resolver that throws a node [`SystemError`](https://nodejs.org/api/errors.html#errors_system_errors).

```js line=14-16
const {
ApolloServer,
gql,
const {
ApolloServer,
gql,
} = require('apollo-server');

const typeDefs = gql`
Expand Down Expand Up @@ -45,10 +45,10 @@ The response will return:
In addition to stacktraces, Apollo Server's exported errors specify a human-readable string in the `code` field of `extensions` that enables the client to perform corrective actions. In addition to improving the client experience, the `code` field allows the server to categorize errors. For example, an `AuthenticationError` sets the code to `UNAUTHENTICATED`, which enables the client to reauthenticate and would generally be ignored as a server anomaly.

```js line=4,15-17
const {
ApolloServer,
gql,
AuthenticationError,
const {
ApolloServer,
gql,
AuthenticationError,
} = require('apollo-server');

const typeDefs = gql`
Expand All @@ -72,13 +72,13 @@ The response will return:

## Augmenting error details

When clients provide bad input, you may want to return additional information
like a localized message for each field or argument that was invalid. The
When clients provide bad input, you may want to return additional information
like a localized message for each field or argument that was invalid. The
following example demonstrates how you can use `UserInputError` to augment
your error messages with additional details.

```js line=15-21
const {
const {
ApolloServer,
UserInputError,
gql,
Expand Down Expand Up @@ -114,23 +114,25 @@ application, you can use the base `ApolloError` class.

```js
new ApolloError(message, code, additionalProperties);
```
```

## Masking and logging errors

The Apollo server constructor accepts a `formatError` function that is run on each error passed back to the client. This can be used to mask errors as well as for logging.
This example demonstrates masking (or suppressing the stacktrace):
The Apollo server constructor accepts a `rewriteError` function that is run on each error passed back to the client or trace reporting. This can be used to mask errors as well as for logging. This example demonstrates masking (or suppressing the error):

```js line=4-10
```js line=4-12
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: error => {
rewriteError: error => {
abernix marked this conversation as resolved.
Show resolved Hide resolved
console.log(error);
return new Error('Internal server error');
// Or, you can delete the exception information
// delete error.extensions.exception;
// return error;
return new GraphQLError('rewritten as a new error');

//Or, you can delete the exception information
return null

//Or return the original error
return error;
},
});

Expand Down
10 changes: 8 additions & 2 deletions packages/apollo-engine-reporting/src/agent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os from 'os';
import { gzip } from 'zlib';
import { DocumentNode } from 'graphql';
import { DocumentNode, GraphQLError } from 'graphql';
import {
FullTracesReport,
ReportHeader,
Expand Down Expand Up @@ -76,8 +76,14 @@ export interface EngineReportingOptions<TContext> {
handleSignals?: boolean;
// Sends the trace report immediately. This options is useful for stateless environments
sendReportsImmediately?: boolean;
// To remove the error message from traces, set this to true. Defaults to false
// (DEPRECATED; Use `rewriteError` instead) To remove the error message
// from traces, set this to true. Defaults to false.
maskErrorDetails?: boolean;
// By default, all errors get reported to Engine servers. You can specify a
// a filter function to exclude specific errors from being reported by
// returning an explicit `null`, or you can mask certain details of the error
// by modifying it and returning the modified error.
rewriteError?: (err: GraphQLError) => GraphQLError | null;
// A human readable name to tag this variant of a schema (i.e. staging, EU)
schemaTag?: string;
//Creates the client information for operation traces.
Expand Down
139 changes: 115 additions & 24 deletions packages/apollo-engine-reporting/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ const clientNameHeaderKey = 'apollographql-client-name';
const clientReferenceIdHeaderKey = 'apollographql-client-reference-id';
const clientVersionHeaderKey = 'apollographql-client-version';

// (DEPRECATE)
// This special type is used internally to this module to implement the
// `maskErrorDetails` (https://github.com/apollographql/apollo-server/pull/1615)
// functionality in the exact form it was originally implemented — which didn't
// have the result matching the interface provided by `GraphQLError` but instead
// just had a `message` property set to `<masked>`. Since `maskErrorDetails`
// is now slated for deprecation (with its behavior superceded by the more
// robust `rewriteError` functionality, this GraphQLErrorOrMaskedErrorObject`
// should be removed when that deprecation is completed in a major release.
type GraphQLErrorOrMaskedErrorObject =
| GraphQLError
| (Partial<GraphQLError> & Pick<GraphQLError, 'message'>);

// EngineReportingExtension is the per-request GraphQLExtension which creates a
// trace (in protobuf Trace format) for a single request. When the request is
// done, it passes the Trace back to its associated EngineReportingAgent via the
Expand Down Expand Up @@ -46,7 +59,6 @@ export class EngineReportingExtension<TContext = any>
addTrace: (signature: string, operationName: string, trace: Trace) => void,
) {
this.options = {
maskErrorDetails: false,
...options,
};
this.addTrace = addTrace;
Expand Down Expand Up @@ -276,31 +288,110 @@ export class EngineReportingExtension<TContext = any>
}

public didEncounterErrors(errors: GraphQLError[]) {
if (errors) {
errors.forEach((error: GraphQLError) => {
// By default, put errors on the root node.
let node = this.nodes.get('');
if (error.path) {
const specificNode = this.nodes.get(error.path.join('.'));
if (specificNode) {
node = specificNode;
}
}
// This life-cycle method is only invoked to capture errors.
if (!errors) {
return;
}

// Always send the trace errors, so that the UI acknowledges that there is an error.
const errorInfo = this.options.maskErrorDetails
? { message: '<masked>' }
: {
message: error.message,
location: (error.locations || []).map(
({ line, column }) => new Trace.Location({ line, column }),
),
json: JSON.stringify(error),
};

node!.error!.push(new Trace.Error(errorInfo));
});
errors.forEach(err => {
// In terms of reporting, errors can be re-written by the user by
// utilizing the `rewriteError` parameter. This allows changing
// the message or stack to remove potentially sensitive information.
// Returning `null` will result in the error not being reported at all.
const errorForReporting = this.rewriteError(err);

if (errorForReporting === null) {
return;
}

this.addError(errorForReporting);
});
}

private rewriteError(
err: GraphQLError,
): GraphQLErrorOrMaskedErrorObject | null {
// (DEPRECATE)
// This relatively basic representation of an error is an artifact
// introduced by https://github.com/apollographql/apollo-server/pull/1615.
// Interesting, the implementation of that feature didn't actually
// accomplish what the requestor had desired. This functionality is now
// being superceded by the `rewriteError` function, which is a more dynamic
// implementation which multiple Engine users have been interested in.
// When this `maskErrorDetails` is officially deprecated, this
// `rewriteError` method can be changed to return `GraphQLError | null`,
// and as noted in its definition, `GraphQLErrorOrMaskedErrorObject` can be
// removed.
if (this.options.maskErrorDetails) {
return {
message: '<masked>',
};
}

if (typeof this.options.rewriteError === 'function') {
// Before passing the error to the user-provided `rewriteError` function,
// we'll make a shadow copy of the error so the user is free to change
// the object as they see fit.

// At this stage, this error is only for the purposes of reporting, but
// this is even more important since this is still a reference to the
// original error object and changing it would also change the error which
// is returned in the response to the client.

// For the clone, we'll create a new object which utilizes the exact same
// prototype of the error being reported.
const clonedError = Object.assign(
Object.create(Object.getPrototypeOf(err)),
err,
);

const rewrittenError = this.options.rewriteError(clonedError);

// Returning an explicit `null` means the user is requesting that, in
// terms of Engine reporting, the error be buried.
if (rewrittenError === null) {
return null;
}

// We don't want users to be inadvertently not reporting errors, so if
// they haven't returned an explicit `GraphQLError` (or `null`, handled
// above), then we'll report the error as usual.
if (!(rewrittenError instanceof GraphQLError)) {
return err;
}

return new GraphQLError(
rewrittenError.message,
err.nodes,
err.source,
err.positions,
err.path,
err.originalError,
err.extensions,
);
}
return err;
}

private addError(error: GraphQLErrorOrMaskedErrorObject): void {
// By default, put errors on the root node.
let node = this.nodes.get('');
if (error.path) {
const specificNode = this.nodes.get(error.path.join('.'));
if (specificNode) {
node = specificNode;
}
}

node!.error!.push(
new Trace.Error({
message: error.message,
location: (error.locations || []).map(
({ line, column }) => new Trace.Location({ line, column }),
),
json: JSON.stringify(error),
}),
);
}

private newNode(path: ResponsePath): Trace.Node {
Expand Down
Loading