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-reportoing): sendVariableValues and sendHeaders. #2931

Merged
merged 12 commits into from
Jun 27, 2019
Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ The version headers in this history reflect the versions of Apollo Server itself

- `apollo-engine-reporting`: **BEHAVIOR CHANGE**: If the error returned from the `engine.rewriteError` hook has an `extensions` property, that property will be used instead of the original error's extensions. Document that changes to most other `GraphQLError` fields by `engine.rewriteError` are ignored. [PR #2932](https://github.com/apollographql/apollo-server/pull/2932)
- `apollo-engine-reporting`: **BEHAVIOR CHANGE**: The `engine.maskErrorDetails` option, deprecated by `engine.rewriteError` in v2.5.0, now behaves a bit more like the new option: while all error messages will be redacted, they will still show up on the appropriate nodes in a trace. [PR #2932](https://github.com/apollographql/apollo-server/pull/2932)
- `apollo-engine-reporting`: **BEHAVIOR CHANGE**: By default, send no GraphQL variable values to Apollo's servers instead of sending all variable values. Adding the new EngineReportingOption `sendVariableValues` to send some or all variable values, possibly after transforming them. This replaces the `privateVariables` option, which is now deprecated. [PR #2931](https://github.com/apollographql/apollo-server/pull/2931)

> Note: In order to keep shipping all GraphQL variable values to Apollo Engine, pass in the option:
>
> `new ApolloServer({engine: {sendVariableValues: {all: true}}})`.


### v2.6.7

Expand Down
69 changes: 54 additions & 15 deletions docs/source/api/apollo-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ new ApolloServer({

* `engine`: <`EngineReportingOptions`> | boolean

Provided the `ENGINE_API_KEY` environment variable is set, the engine reporting agent will be started automatically. The API key can also be provided as the `apiKey` field in an object passed as the `engine` field. See the [EngineReportingOptions](#enginereportingoptions) section for a full description of how to configure the reporting agent, including how to blacklist variables. When using the Engine proxy, this option should be set to false.
Provided the `ENGINE_API_KEY` environment variable is set, the engine reporting agent will be started automatically. The API key can also be provided as the `apiKey` field in an object passed as the `engine` field. See the [EngineReportingOptions](#enginereportingoptions) section for a full description of how to configure the reporting agent, including how to include variable values and HTTP headers. When using the Engine proxy, this option should be set to false.

* `persistedQueries`: <`Object`> | false

Expand Down Expand Up @@ -342,20 +342,59 @@ addMockFunctionsToSchema({
By default, errors sending reports to Engine servers will be logged
to standard error. Specify this function to process errors in a different
way.

* `privateVariables`: Array<String> | boolean

A case-sensitive list of names of variables whose values should not be sent
to Apollo servers, or 'true' to leave out all variables. In the former
case, the report will indicate that each private variable was redacted in
the latter case, no variables are sent at all.

* `privateHeaders`: Array<String> | boolean

A case-insensitive list of names of HTTP headers whose values should not be
sent to Apollo servers, or 'true' to leave out all HTTP headers. Unlike
with privateVariables, names of dropped headers are not reported.


* `sendVariableValues`: { transform: (options: { variables: Record<string, any>, operationString?: string } ) => Record<string, any> }
| { exceptNames: Array&lt;String&gt; }
| { onlyNames: Array&lt;String&gt; }
| { none: true }
| { all: true }

By default, Apollo Server does not send the values of any GraphQL variables to Apollo's servers, because variable values often contain the private data of your app's users. If you'd like variable values to be included in traces, set this option. This option can take several forms:

- `{ none: true }`: Don't send any variable values. **(DEFAULT)**
- `{ all: true }`: Send all variable values.
- `{ transform: ({ variables, operationString}) => { ... } }`: A custom function for modifying variable values. Keys added by the custom function will be removed, and keys removed will be added back with an empty value.
- `{ exceptNames: [...] }`: A case-sensitive list of names of variables whose values should not be sent to Apollo servers.
- `{ onlyNames: [...] }`: A case-sensitive list of names of variables whose values will be sent to Apollo servers.

Defaults to not sending any variable values if both this parameter and the deprecated `privateVariables` are not set.
The report will indicate each private variable key whose value was redacted by `{ none: true }` or `{ exceptNames: [...]` }.

* `privateVariables`: Array&lt;String&gt; | boolean

> Will be deprecated in 3.0. Use the option `sendVariableValues` instead.
Passing an array into `privateVariables` is equivalent to
passing in `{ exceptNames: array } ` to `sendVariableValues`, and passing in `true` or `false` is equivalent
to passing ` { none: true } ` or ` { all: true }`, respectively.

> Note: An error will be thrown if both this deprecated option and its replacement, `sendVariableValues` are defined.
In order to preserve the old default of `privateVariables`, which sends all variables and their values, pass in the `sendVariableValues` option:
`new ApolloServer({engine: {sendVariableValues: {all: true}}})`.

* `sendHeaders`: { exceptNames: Array&lt;String&gt; } | { onlyNames: Array&lt;String&gt; } | { all: boolean } | { none: boolean }
By default, Apollo Server does not send the list of HTTP request headers and values to
Apollo's servers, to protect private data of your app's users. If you'd like this information included in traces,
set this option. This option can take several forms:

- `{ none: true }`: Drop all HTTP request headers. **(DEFAULT)**
- `{ all: true }`: Send the values of all HTTP request headers.
- `{ exceptNames: [...] }`: A case-insensitive list of names of HTTP headers whose values should not be sent to Apollo servers.
- `{ onlyNames: [...] }`: A case-insensitive list of names of HTTP headers whose values will be sent to Apollo servers.

Defaults to not sending any request header names and values if both this parameter and the deprecated `privateHeaders` are not set.
Unlike with `sendVariableValues`, names of dropped headers are not reported.
The headers 'authorization', 'cookie', and 'set-cookie' are never reported.

* `privateHeaders`: Array&lt;String&gt; | boolean

> Will be deprecated in 3.0. Use the `sendHeaders` option instead.
Passing an array into `privateHeaders` is equivalent to passing ` { exceptNames: array } ` into `sendHeaders`, and
passing `true` or `false` is equivalent to passing in ` { none: true } ` and ` { all: true }`, respectively.

> Note: An error will be thrown if both this deprecated option and its replacement, `sendHeaders`, are defined.
In order to preserve the old default of `privateHeaders`, which sends all request headers and their values, pass in the `sendHeaders` option:
`new ApolloServer({engine: {sendHeaders: {all: true}}})`.

* `handleSignals`: boolean

By default, EngineReportingAgent listens for the 'SIGINT' and 'SIGTERM'
Expand Down
95 changes: 94 additions & 1 deletion packages/apollo-engine-reporting/src/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { signatureCacheKey } from '../agent';
import {
signatureCacheKey,
handleLegacyOptions,
EngineReportingOptions,
} from '../agent';

describe('signature cache key', () => {
it('generates without the operationName', () => {
Expand All @@ -11,3 +15,92 @@ describe('signature cache key', () => {
);
});
});

describe("test handleLegacyOptions(), which converts the deprecated privateVariable and privateHeaders options to the new options' formats", () => {
it('Case 1: privateVariables/privateHeaders == False; same as all', () => {
const optionsPrivateFalse: EngineReportingOptions<any> = {
privateVariables: false,
privateHeaders: false,
};
handleLegacyOptions(optionsPrivateFalse);
expect(optionsPrivateFalse.privateVariables).toBe(undefined);
expect(optionsPrivateFalse.sendVariableValues).toEqual({ all: true });
expect(optionsPrivateFalse.privateHeaders).toBe(undefined);
expect(optionsPrivateFalse.sendHeaders).toEqual({ all: true });
});

it('Case 2: privateVariables/privateHeaders == True; same as none', () => {
const optionsPrivateTrue: EngineReportingOptions<any> = {
privateVariables: true,
privateHeaders: true,
};
handleLegacyOptions(optionsPrivateTrue);
expect(optionsPrivateTrue.privateVariables).toBe(undefined);
expect(optionsPrivateTrue.sendVariableValues).toEqual({ none: true });
expect(optionsPrivateTrue.privateHeaders).toBe(undefined);
expect(optionsPrivateTrue.sendHeaders).toEqual({ none: true });
});

it('Case 3: privateVariables/privateHeaders set to an array', () => {
const privateArray: Array<String> = ['t1', 't2'];
const optionsPrivateArray: EngineReportingOptions<any> = {
privateVariables: privateArray,
privateHeaders: privateArray,
};
handleLegacyOptions(optionsPrivateArray);
expect(optionsPrivateArray.privateVariables).toBe(undefined);
expect(optionsPrivateArray.sendVariableValues).toEqual({
exceptNames: privateArray,
});
expect(optionsPrivateArray.privateHeaders).toBe(undefined);
expect(optionsPrivateArray.sendHeaders).toEqual({
exceptNames: privateArray,
});
});

it('Case 4: privateVariables/privateHeaders are null or undefined; no change', () => {
const optionsPrivateFalse: EngineReportingOptions<any> = {
privateVariables: undefined,
privateHeaders: null, // null is not a valid TS input, but check the output anyways
};
handleLegacyOptions(optionsPrivateFalse);
expect(optionsPrivateFalse.privateVariables).toBe(undefined);
expect(optionsPrivateFalse.sendVariableValues).toBe(undefined);
expect(optionsPrivateFalse.privateHeaders).toBe(undefined);
expect(optionsPrivateFalse.sendHeaders).toBe(undefined);
});

it('Case 5: throws error when both the new and old options are set', () => {
const optionsBothVariables: EngineReportingOptions<any> = {
privateVariables: true,
sendVariableValues: { none: true },
};
expect(() => {
handleLegacyOptions(optionsBothVariables);
}).toThrow();
const optionsBothHeaders: EngineReportingOptions<any> = {
privateHeaders: true,
sendHeaders: { none: true },
};
expect(() => {
handleLegacyOptions(optionsBothHeaders);
}).toThrow();
});

it('Case 6: the passed in options are not modified if deprecated fields were not set', () => {
const optionsNotDeprecated: EngineReportingOptions<any> = {
sendVariableValues: { exceptNames: ['test'] },
sendHeaders: { all: true },
};
const output: EngineReportingOptions<any> = {
sendVariableValues: { exceptNames: ['test'] },
sendHeaders: { all: true },
};
handleLegacyOptions(optionsNotDeprecated);
expect(optionsNotDeprecated).toEqual(output);

const emptyInput: EngineReportingOptions<any> = {};
handleLegacyOptions(emptyInput);
expect(emptyInput).toEqual({});
});
});