diff --git a/.changeset/khaki-coins-tie.md b/.changeset/khaki-coins-tie.md new file mode 100644 index 0000000000..3205d1abd2 --- /dev/null +++ b/.changeset/khaki-coins-tie.md @@ -0,0 +1,5 @@ +--- +'@envelop/apollo-inline-trace': major +--- + +Implementation of Apollo's federated inline tracing diff --git a/packages/plugins/apollo-inline-trace/README.md b/packages/plugins/apollo-inline-trace/README.md new file mode 100644 index 0000000000..53608b201b --- /dev/null +++ b/packages/plugins/apollo-inline-trace/README.md @@ -0,0 +1,54 @@ +## `@envelop/apollo-inline-trace` + +This plugin integrates Apollo's FTV1 tracing. Read more about it on [Apollo's website about federated trace data](https://www.apollographql.com/docs/federation/metrics/). + +## Getting Started + +``` +yarn add @envelop/apollo-inline-trace +``` + +## Usage Example + +```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' +import { useApolloInlineTrace } from '@envelop/apollo-inline-trace' + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + useApolloInlineTrace({ + // request must specify the ftv1 tracing protocol + shouldTrace: ctx => ctx.req.headers['apollo-federation-include-trace'] === 'ftv1' + }) + ] +}) + +const server = createServer((req, res) => { + let payload = '' + + req.on('data', chunk => { + payload += chunk.toString() + }) + + req.on('end', async () => { + const { perform } = getEnveloped({ req }) + + const { query, variables } = JSON.parse(payload) + + const result = await perform({ query, variables }) + + res.end(JSON.stringify(result)) + }) +}) + +server.listen(3000) +``` + +## Note + +For accurate tracing behaviour, you MUST use the `perform` function from `getEnveloped`. diff --git a/packages/plugins/apollo-inline-trace/package.json b/packages/plugins/apollo-inline-trace/package.json new file mode 100644 index 0000000000..2b945ef8ab --- /dev/null +++ b/packages/plugins/apollo-inline-trace/package.json @@ -0,0 +1,65 @@ +{ + "name": "@envelop/apollo-inline-trace", + "version": "0.0.0", + "description": "Apollo's federated tracing plugin.", + "license": "MIT", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/n1ru4l/envelop.git", + "directory": "packages/plugins/apollo-inline-trace" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./*": { + "require": { + "types": "./dist/typings/*.d.cts", + "default": "./dist/cjs/*.js" + }, + "import": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + }, + "default": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "peerDependencies": { + "@envelop/core": "^2.5.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "dependencies": { + "@envelop/on-resolve": "^1.0.0", + "apollo-reporting-protobuf": "^3.3.2" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module" +} diff --git a/packages/plugins/apollo-inline-trace/src/index.ts b/packages/plugins/apollo-inline-trace/src/index.ts new file mode 100644 index 0000000000..127ba6e37e --- /dev/null +++ b/packages/plugins/apollo-inline-trace/src/index.ts @@ -0,0 +1,276 @@ +import { isAsyncIterable, Plugin } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; +import { GraphQLError, ResponsePath } from 'graphql'; +import { google, Trace } from 'apollo-reporting-protobuf'; + +const ctxKey = Symbol('ApolloInlineTracePluginContextKey'); + +interface ApolloInlineTracePluginContext { + startHrTime: [number, number]; + rootNode: Trace.Node; + trace: Trace; + nodes: Map; + /** + * graphql-js can continue to execute more fields indefinitely after + * `execute()` resolves. That's because parallelism on a selection set + * is implemented using `Promise.all`, and as soon as one field + * throws an error, the combined Promise resolves, but there's no + * "cancellation" of the rest of Promises/fields in `Promise.all`. + */ + stopped: boolean; +} + +export interface ApolloInlineTracePluginOptions = {}> { + /** + * Decide whether the operation should be traced. + * + * When dealing with HTTP requests, `apollo-federation-include-trace` header must be set to `ftv1`. + */ + shouldTrace(ctx: PluginContext): boolean; + /** + * Format errors before being sent for tracing. Beware that only the error + * `message` and `extensions` can be changed. + * + * Return `null` to skip reporting error. + */ + rewriteError?: (err: GraphQLError) => GraphQLError | null; +} + +/** + * Produces Apollo's base64 trace protocol containing timing, resolution and + * errors information. + * + * The output is placed in `extensions.ftv1` of the GraphQL result. + * + * The Apollo Gateway utilizes this data to construct the full trace and submit + * it to Apollo's usage reporting ingress. + */ +export function useApolloInlineTrace = {}>({ + shouldTrace, + rewriteError, +}: ApolloInlineTracePluginOptions): Plugin< + PluginContext & { [ctxKey]: ApolloInlineTracePluginContext } +> { + return { + onEnveloped({ context, extendContext }) { + if (!context) { + throw new Error("Context must be set for Apollo's inline tracing plugin"); + } + + if (shouldTrace(context)) { + const startHrTime = process.hrtime(); + const rootNode = new Trace.Node(); + extendContext({ + ...context, + [ctxKey]: { + startHrTime, + rootNode, + trace: new Trace({ + root: rootNode, + fieldExecutionWeight: 1, // Why 1? See: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L16-L23 + startTime: nowTimestamp(), + }), + nodes: new Map([[responsePathToString(), rootNode]]), + stopped: false, + }, + }); + } + }, + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(({ context, info }) => { + const ctx = context[ctxKey]; + if (!ctx) return; + + // result was already shipped (see ApolloInlineTracePluginContext.stopped) + if (ctx.stopped) { + return () => { + // noop + }; + } + + const node = newTraceNode(ctx, info.path); + node.type = info.returnType.toString(); + node.parentType = info.parentType.toString(); + node.startTime = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime)); + if (typeof info.path.key === 'string' && info.path.key !== info.fieldName) { + // field was aliased, send the original field name too + node.originalFieldName = info.fieldName; + } + + return () => { + node.endTime = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime)); + }; + }) + ); + }, + onPerform({ context }) { + return { + onPerformDone({ result }) { + const ctx = context[ctxKey]; + if (!ctx) return; + + // TODO: should handle streaming results? how? + if (isAsyncIterable(result)) return; + + if (result.extensions?.ftv1 !== undefined) { + throw new Error('The `ftv1` extension is already present'); + } + + // onResultProcess will be called only once since we disallow async iterables + if (ctx.stopped) throw new Error('Trace stopped multiple times'); + + if (result.errors) { + handleErrors(ctx, result.errors, rewriteError); + } + + ctx.stopped = true; + ctx.trace.durationNs = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime)); + ctx.trace.endTime = nowTimestamp(); + + const encodedUint8Array = Trace.encode(ctx.trace).finish(); + const base64 = btoa(String.fromCharCode(...encodedUint8Array)); + + result.extensions = { + ...result.extensions, + ftv1: base64, + }; + }, + }; + }, + }; +} + +/** + * Converts an hrtime array (as returned from process.hrtime) to nanoseconds. + * + * The entire point of the hrtime data structure is that the JavaScript Number + * type can't represent all int64 values without loss of precision. + * + * Reference: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L269-L285 + */ +function hrTimeToDurationInNanos(hrtime: [number, number]) { + return hrtime[0] * 1e9 + hrtime[1]; +} + +/** + * Current time from Date.now() as a google.protobuf.Timestamp. + * + * Reference: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L315-L323 + */ +function nowTimestamp(): google.protobuf.Timestamp { + const totalMillis = Date.now(); + const millis = totalMillis % 1000; + return new google.protobuf.Timestamp({ + seconds: (totalMillis - millis) / 1000, + nanos: millis * 1e6, + }); +} + +/** + * Convert from the linked-list ResponsePath format to a dot-joined + * string. Includes the full path (field names and array indices). + * + * Reference: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L287-L303 + */ +function responsePathToString(path?: ResponsePath): string { + if (path === undefined) { + return ''; + } + + // `responsePathAsArray` from `graphql-js/execution` created new arrays unnecessarily + let res = String(path.key); + + while ((path = path.prev) !== undefined) { + res = `${path.key}.${res}`; + } + + return res; +} + +function ensureParentTraceNode(ctx: ApolloInlineTracePluginContext, path: ResponsePath): Trace.Node { + const parentNode = ctx.nodes.get(responsePathToString(path.prev)); + if (parentNode) return parentNode; + // path.prev isn't undefined because we set up the root path in ctx.nodes + return newTraceNode(ctx, path.prev!); +} + +function newTraceNode(ctx: ApolloInlineTracePluginContext, path: ResponsePath) { + const node = new Trace.Node(); + const id = path.key; + if (typeof id === 'number') { + node.index = id; + } else { + node.responseName = id; + } + ctx.nodes.set(responsePathToString(path), node); + const parentNode = ensureParentTraceNode(ctx, path); + parentNode.child.push(node); + return node; +} + +function handleErrors( + ctx: ApolloInlineTracePluginContext, + errors: readonly GraphQLError[], + rewriteError: ApolloInlineTracePluginOptions['rewriteError'] +) { + if (ctx.stopped) { + throw new Error('Handling errors after tracing was stopped'); + } + + for (const err of errors) { + /** + * This is an error from a federated service. We will already be reporting + * it in the nested Trace in the query plan. + * + * Reference: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L133-L141 + */ + if (err.extensions?.serviceName) { + continue; + } + + let errToReport = err; + + // errors can be rewritten through `rewriteError` + if (rewriteError) { + // clone error to avoid users mutating the original one + const clonedErr = Object.assign(Object.create(Object.getPrototypeOf(err)), err); + const rewrittenError = rewriteError(clonedErr); + if (!rewrittenError) { + // return nullish to skip reporting + continue; + } + errToReport = rewrittenError; + } + + // only message and extensions can be rewritten + errToReport = new GraphQLError(errToReport.message, { + extensions: errToReport.extensions || err.extensions, + nodes: err.nodes, + source: err.source, + positions: err.positions, + path: err.path, + originalError: err.originalError, + }); + + // put errors on the root node by default + let node = ctx.rootNode; + + if (Array.isArray(errToReport.path)) { + const specificNode = ctx.nodes.get(errToReport.path.join('.')); + if (specificNode) { + node = specificNode; + } else { + throw new Error(`Could not find node with path ${errToReport.path.join('.')}`); + } + } + + node.error.push( + new Trace.Error({ + message: errToReport.message, + location: (errToReport.locations || []).map(({ line, column }) => new Trace.Location({ line, column })), + json: JSON.stringify(errToReport), + }) + ); + } +} diff --git a/packages/plugins/apollo-inline-trace/test/use-apollo-inline-trace.spec.ts b/packages/plugins/apollo-inline-trace/test/use-apollo-inline-trace.spec.ts new file mode 100644 index 0000000000..74aa30b060 --- /dev/null +++ b/packages/plugins/apollo-inline-trace/test/use-apollo-inline-trace.spec.ts @@ -0,0 +1,378 @@ +import { useApolloInlineTrace } from '../src'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { Trace } from 'apollo-reporting-protobuf'; +import { parse, validate, execute, subscribe, GraphQLError, versionInfo } from 'graphql'; +import { envelop, useSchema } from '@envelop/core'; +import { assertSingleExecutionValue, assertStreamExecutionValue } from '@envelop/testing'; + +const graphqlFuncs = { parse, validate, execute, subscribe }; + +describe('Apollo Inline Trace Plugin', () => { + if (versionInfo.major < 16) { + it('dummy', () => {}); + return; + } + + // must create a new schema because on-resolve mutates the existing one + const getSchema = () => + makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + boom: String! + person: Person! + people: [Person!]! + } + type Subscription { + hello: String! + } + type Person { + name: String! + } + `, + resolvers: { + Query: { + hello() { + return 'world'; + }, + boom() { + throw new Error('bam'); + }, + person() { + return { name: 'John' }; + }, + people() { + return [{ name: 'John' }, { name: 'Jane' }]; + }, + }, + Subscription: { + hello: { + async *subscribe() { + yield { hello: 'world' }; + }, + }, + }, + }, + }); + + it('should add ftv1 tracing to result extensions', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeUndefined(); + expect(typeof result.extensions?.ftv1).toBe('string'); + }); + + function expectTrace(trace: Trace) { + expect(trace.startTime).toBeDefined(); + expect(typeof trace.startTime?.seconds).toBe('number'); + expect(typeof trace.startTime?.nanos).toBe('number'); + + expect(typeof trace.durationNs).toBe('number'); + + expect(trace.endTime).toBeDefined(); + expect(typeof trace.endTime?.seconds).toBe('number'); + expect(typeof trace.endTime?.nanos).toBe('number'); + + expect(addSecondsAndNanos(trace.startTime!.seconds!, trace.startTime!.nanos!)).toBeLessThanOrEqual( + addSecondsAndNanos(trace.endTime!.seconds!, trace.endTime!.nanos!) + ); + + expect(typeof trace.fieldExecutionWeight).toBe('number'); + + expect(trace.root).toBeDefined(); + expect(trace.root?.child).toBeInstanceOf(Array); + } + + function expectTraceNode(node: Trace.INode | null | undefined, field: string, type: string, parent: string) { + expect(node).toBeDefined(); + + expect(node!.responseName).toBe(field); + expect(node!.type).toBe(type); + expect(node!.parentType).toBe(parent); + + expect(typeof node!.startTime).toBe('number'); + expect(typeof node!.endTime).toBe('number'); + + expect(node!.startTime!).toBeLessThanOrEqual(node!.endTime!); + } + + it('should have proto tracing on flat query', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + + const ftv1 = result.extensions?.ftv1 as string as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + + const hello = trace.root?.child?.[0]; + expect(hello?.error?.length).toBe(0); + expectTraceNode(hello, 'hello', 'String!', 'Query'); + }); + + it('should have proto tracing on aliased flat query', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ hi: hello }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeUndefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + + const hi = trace.root?.child?.[0]; + expect(hi?.error?.length).toBe(0); + expectTraceNode(hi, 'hi', 'String!', 'Query'); + expect(hi?.originalFieldName).toBe('hello'); + }); + + it('should have proto tracing on nested query', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ person { name } }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeUndefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + + const person = trace.root?.child?.[0]; + expect(person?.error?.length).toBe(0); + expectTraceNode(person, 'person', 'Person!', 'Query'); + + const personName = person?.child?.[0]; + expect(personName?.error?.length).toBe(0); + expectTraceNode(personName, 'name', 'String!', 'Person'); + }); + + it('should have proto tracing on flat query with array field', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ people { name } }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeUndefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + + const people = trace.root?.child?.[0]; + expect(people?.error?.length).toBe(0); + expectTraceNode(people, 'people', '[Person!]!', 'Query'); + + const arr = people!.child!; + for (let i = 0; i < arr.length; i++) { + const person = arr[i]; + expect(person?.error?.length).toBe(0); + expect(person.index).toBe(i); + expectTraceNode(person.child?.[0], 'name', 'String!', 'Person'); + } + }); + + function expectTraceNodeError(node: Trace.INode | null | undefined) { + expect(node).toBeDefined(); + expect(node!.error).toBeDefined(); + const error = node!.error!; + expect(error).toBeInstanceOf(Array); + expect(error.length).toBeGreaterThan(0); + expect(typeof error[0].message).toBe('string'); + expect(typeof error[0].location).toBeDefined(); + expect(typeof error[0].location?.[0].line).toBe('number'); + expect(typeof error[0].location?.[0].column).toBe('number'); + expect(() => { + JSON.parse(error[0].json!); + }).not.toThrow(); + } + + it('should have proto tracing on parse fail', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ he' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeDefined(); + + // + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expectTraceNodeError(trace.root); + }); + + it('should have proto tracing on validation fail', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ henlo }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeDefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expectTraceNodeError(trace.root); + }); + + it('should have proto tracing on execution fail', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ boom }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeDefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + + const boom = trace.root?.child?.[0]; + expectTraceNode(boom, 'boom', 'String!', 'Query'); + expectTraceNodeError(boom); + }); + + it('should skip tracing errors through rewriteError', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [useSchema(getSchema()), useApolloInlineTrace({ shouldTrace: () => true })], + })(); + + const result = await perform({ query: '{ boom }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeDefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + }); + + it('should rewrite only error messages and extensions through rewriteError', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(getSchema()), + useApolloInlineTrace({ + shouldTrace: () => true, + rewriteError: () => new GraphQLError('bim', { extensions: { str: 'ing' } }), + }), + ], + })(); + + const result = await perform({ query: '{ boom }' }); + assertSingleExecutionValue(result); + + expect(result.errors).toBeDefined(); + + // + + const ftv1 = result.extensions?.ftv1 as string; + expect(typeof ftv1).toBe('string'); + const trace = Trace.decode(Buffer.from(ftv1, 'base64')); + + expectTrace(trace); + expect(trace.root?.error?.length).toBe(0); + + const boom = trace.root?.child?.[0]; + expectTraceNode(boom, 'boom', 'String!', 'Query'); + expectTraceNodeError(boom); // will check for location + + const error = boom!.error!; + expect(error[0].message).toBe('bim'); // not 'bam' + + const errObj = JSON.parse(error[0].json!); + expect(errObj.extensions).toEqual({ str: 'ing' }); + }); + + it('should not trace subscriptions', async () => { + const { perform } = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(getSchema()), + useApolloInlineTrace({ + shouldTrace: () => true, + }), + ], + })(); + + const result = await perform({ query: 'subscription { hello }' }); + assertStreamExecutionValue(result); + + for await (const part of result) { + expect(part.data).toEqual({ hello: 'world' }); + expect(part.errors).toBeUndefined(); + expect(part.extensions).toBeUndefined(); + } + }); + + function addSecondsAndNanos(seconds: number, nanos: number): number { + return seconds + nanos / 1e9; + } +}); diff --git a/yarn.lock b/yarn.lock index 0e595f92c1..d43ca4f129 100644 --- a/yarn.lock +++ b/yarn.lock @@ -221,6 +221,25 @@ "@types/node" "^10.1.0" long "^4.0.0" +"@apollo/protobufjs@1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.4.tgz#d913e7627210ec5efd758ceeb751c776c68ba133" + integrity sha512-npVJ9NVU/pynj+SCU+fambvTneJDyCnif738DnZ7pCxdDtzeEz7WkpSIq5wNUmWm5Td55N+S2xfqZ+WP4hDLng== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.0" + "@types/node" "^10.1.0" + long "^4.0.0" + "@apollo/query-planner@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@apollo/query-planner/-/query-planner-0.6.0.tgz#6aef343cfc094706a86c996ed0af94ca50369587" @@ -4529,6 +4548,13 @@ apollo-reporting-protobuf@^0.8.0: dependencies: "@apollo/protobufjs" "1.2.2" +apollo-reporting-protobuf@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-3.3.2.tgz#2078c53d3140bc6221c6040c5326623e0c21c8d4" + integrity sha512-j1tx9tmkVdsLt1UPzBrvz90PdjAeKW157WxGn+aXlnnGfVjZLIRXX3x5t1NWtXvB7rVaAsLLILLtDHW382TSoQ== + dependencies: + "@apollo/protobufjs" "1.2.4" + apollo-server-caching@3.3.0, "apollo-server-caching@^0.7.0 || ^3.0.0", apollo-server-caching@^3.1.0, apollo-server-caching@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-3.3.0.tgz#f501cbeb820a4201d98c2b768c085f22848d9dc5"