diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f785ae9881..90caeabc224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,23 @@ The version headers in this history reflect the versions of Apollo Server itself ### vNEXT -- _Nothing yet! Stay tuned._ +> 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. With few exceptions, the format of the entry should follow convention (i.e., prefix with package name, use markdown `backtick formatting` for package names and code, suffix with a link to the change-set à la `[PR #YYY](https://link/pull/YYY)`, etc.). When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. + +- _Nothing yet! Stay tuned!_ + +### v2.15.0 + +- `apollo-engine-reporting`: Added a `reportTiming` API to allow trace reporting to be enabled or disabled on a per request basis. The option takes either a boolean or a predicate function that takes a [`GraphQLRequestContextDidResolveOperation`](https://github.com/apollographql/apollo-server/blob/a926b7eedbb87abab2ec70fb03d71743985cb18d/packages/apollo-server-types/src/index.ts#L185-L190) or [`GraphQLRequestContextDidEncounterErrors`](https://github.com/apollographql/apollo-server/blob/a926b7eedbb87abab2ec70fb03d71743985cb18d/packages/apollo-server-types/src/index.ts#L191-L195) and returns a boolean. If the boolean is false the request will not be instrumented for tracing and no trace will be sent to Apollo Graph Manager. The default is `true` so all traces will get instrumented and sent, which is the same as the previous default behavior. [PR #3918](https://github.com/apollographql/apollo-server/pull/3918) +- `apollo-engine-reporting`: Removed `GraphQLServerOptions.reporting`. It isn't known whether a trace will be reported at the beginning of the request because of the above change. We believe this field was only used internally within Apollo Server; let us know if this is a problem and we can suggest alternatives. Additionally, the field `requestContext.metrics.captureTraces` is now initialized later in the request pipeline. [PR #3918](https://github.com/apollographql/apollo-server/pull/3918) +- `apollo-engine-reporting`: Make Apollo Server throw if schema reporting is enabled for a gateway or federated service. [PR #4246](https://github.com/apollographql/apollo-server/pull/4246) +- `apollo-engine-reporting`: Remove the `experimental_` prefix from schema reporting options, and specifically rename `experimental_schemaReporting` option name to `reportSchema`. (The old option names remain functional, but are deprecated.) [PR #4236](https://github.com/apollographql/apollo-server/pull/4236) + +### v2.14.5 + +- `apollo-engine-reporting`: Make Apollo Server throw if schema reporting is enabled for a gateway or federated service. [PR #4246](https://github.com/apollographql/apollo-server/pull/4246) ### v2.14.4 -> 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. With few exceptions, the format of the entry should follow convention (i.e., prefix with package name, use markdown `backtick formatting` for package names and code, suffix with a link to the change-set à la `[PR #YYY](https://link/pull/YYY)`, etc.). - When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section. - `apollo-engine-reporting`: Add environment variable `APOLLO_SCHEMA_REPORTING` that can enable schema reporting. If `experimental__schemaReporting` is set it will override the environment variable. [PR #4206](https://github.com/apollographql/apollo-server/pull/4206) - `apollo-engine-reporting`: The schema reporting URL has been changed to use the new dedicated sub-domain `https://edge-server-reporting.api.apollographql.com`. [PR #4232](https://github.com/apollographql/apollo-server/pull/4232) - `apollo-server-core`: Though Apollo Server **is not affected** due to the way it is integrated, in response to [an upstream security advisory for GraphQL Playground](https://github.com/prisma-labs/graphql-playground/security/advisories/GHSA-4852-vrh7-28rf) we have published [the same patch](https://github.com/prisma-labs/graphql-playground/commit/bf1883db538c97b076801a60677733816cb3cfb7) on our `@apollographql/graphql-playground-html` fork and bumped Apollo Server to use it. Again, this was done out of an **abundance of caution** since the way that Apollo Server utilizes `renderPlaygroundPage` is _not_ vulnerable as it does not allow per-request Playground configuration that could allow interpolation of user-input. [PR #4231](https://github.com/apollographql/apollo-server/pull/4231) diff --git a/docs/package-lock.json b/docs/package-lock.json index 31bf45e2802..73e58e20e25 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -5463,6 +5463,24 @@ } } }, + "@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "requires": { + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + }, "@mapbox/hast-util-table-cell-style": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz", @@ -15062,6 +15080,14 @@ "simple-git": "^1.105.0" }, "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, "fs-extra": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", @@ -15079,6 +15105,14 @@ "requires": { "glob": "^7.1.3" } + }, + "simple-git": { + "version": "1.132.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-1.132.0.tgz", + "integrity": "sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg==", + "requires": { + "debug": "^4.0.1" + } } } }, @@ -15214,9 +15248,9 @@ } }, "gatsby-theme-apollo-core": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/gatsby-theme-apollo-core/-/gatsby-theme-apollo-core-3.0.12.tgz", - "integrity": "sha512-kvTvIRRo3zYmxWj0Jo15vhlOBfmjqK8ucEt4AtmQiEhaotwwnGwHqATyxqphAUKtu0fU1AuvAO2q0rlqYkT4uw==", + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/gatsby-theme-apollo-core/-/gatsby-theme-apollo-core-3.0.14.tgz", + "integrity": "sha512-NZqgV1sYpr9MMgGQFK1MBG26nsrBJkmKIvLxpX2+uJGiCUuq1t5fcka/LY5jQw5x5xt8KBeieWdkgqRSA03tVQ==", "requires": { "@apollo/space-kit": "^5.6.0", "@emotion/core": "^10.0.7", @@ -15235,9 +15269,9 @@ } }, "gatsby-theme-apollo-docs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/gatsby-theme-apollo-docs/-/gatsby-theme-apollo-docs-4.2.6.tgz", - "integrity": "sha512-9dmypf3BfZ+/KV7G6M6q+ZfLiRetl7wTDfoELRhqjaGtFA3Yc3UmkrEn0ZFTPpRaoU10X0mNJY4icSFarOlguA==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/gatsby-theme-apollo-docs/-/gatsby-theme-apollo-docs-4.2.9.tgz", + "integrity": "sha512-5MxSlwpOZkZX7Iks9F9WUlzJ+B4yf2/hwUUBGGGTCD3CVl3Dvdv6yrurCz0hUjqxQ94+wfuT0BFvI0tXx0AxkA==", "requires": { "@mdx-js/mdx": "^1.1.0", "@mdx-js/react": "^1.0.27", @@ -15253,7 +15287,7 @@ "gatsby-remark-rewrite-relative-links": "^1.0.8", "gatsby-source-filesystem": "^2.0.29", "gatsby-source-git": "^1.0.1", - "gatsby-theme-apollo-core": "^3.0.12", + "gatsby-theme-apollo-core": "^3.0.14", "gatsby-transformer-remark": "^2.6.30", "js-yaml": "^3.13.1", "prismjs": "^1.15.0", @@ -15263,6 +15297,7 @@ "remark": "^10.0.1", "remark-react": "^5.0.1", "remark-typescript": "^0.3.0", + "simple-git": "^2.7.0", "source-sans-pro": "^3.6.0", "striptags": "^3.1.1" } @@ -23830,11 +23865,12 @@ "integrity": "sha1-HdrOSYF5j5O9gzlzgD2A1S6TrWo=" }, "simple-git": { - "version": "1.132.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-1.132.0.tgz", - "integrity": "sha512-xauHm1YqCTom1sC9eOjfq3/9RKiUA9iPnxBbrY2DdL8l4ADMu0jjM5l5lphQP5YWNqAL2aXC/OeuQ76vHtW5fg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-2.7.0.tgz", + "integrity": "sha512-NpNNe0hOz3DMRWB9ewK83p/nMAkGrNO/VlWhMDaI3OdUO3UNoMv5+XlMOzO52jCyl+RZbVrIuNMpxtR4C1TQXw==", "requires": { - "debug": "^4.0.1" + "@kwsites/file-exists": "^1.1.1", + "debug": "^4.1.1" }, "dependencies": { "debug": { diff --git a/docs/package.json b/docs/package.json index 08c09ce9f49..a52efc72334 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "gatsby": "2.23.3", - "gatsby-theme-apollo-docs": "4.2.6", + "gatsby-theme-apollo-docs": "4.2.9", "react": "16.13.1", "react-dom": "16.13.1" } diff --git a/docs/source/api/apollo-server.md b/docs/source/api/apollo-server.md index d40acd1ca77..1d6012baccc 100644 --- a/docs/source/api/apollo-server.md +++ b/docs/source/api/apollo-server.md @@ -480,6 +480,18 @@ addMockFunctionsToSchema({ A human-readable name for the variant of a schema (i.e. staging, EU). Setting this value will cause metrics to be segmented in the Apollo Graph Manager UI. Additionally schema validation with a graph variant will only check metrics associated with the same string. +* `reportTiming`: Boolean | async (GraphQLRequestContextDidResolveOperation | GraphQLRequestContextDidEncounterErrors) => Boolean + + Specify whether to instrument an operation to send traces and metrics to Apollo. + This may resolve to a boolean or a async function returning a promise resolving to a boolean. + If the option resolves to false for an operation the operation will not be instrumented + and no metrics information will be sent to Apollo. + + The function will receive a `GraphQLRequestContextDidResolveOperation` with client and operation + information or a `GraphQLRequestContextDiDEncounterErrors` in the case an operation failed + to resolve properly. This allows the choice of whether to include a given request in trace + and metric reporting to be made on a per-request basis. The default value is true. + * `generateClientInfo`: (GraphQLRequestContext) => ClientInfo **AS 2.2** Creates a client context(ClientInfo) based on the request pipeline's @@ -500,3 +512,30 @@ addMockFunctionsToSchema({ > [WARNING] If you specify a `clientReferenceId`, Graph Manager will treat the > `clientName` as a secondary lookup, so changing a `clientName` may result > in an unwanted experience. + +* `reportSchema`: boolean + + Enables the automatic schema reporting feature of Apollo Server, which will + cause it to periodically report the server's schema (when changes are + detected) along with details about the runtime environment to Apollo Graph + Manager. This feature removes the need to register schemas manually via + `apollo service:push` in CI/CD pipelines. + +* `overrideReportedSchema`: string + + By default, the schema reported to Apollo Graph Manager will be normalized, + which may shift ordering and comments and remove whitespace. This option can + be used to override the default schema. Any schema provided will not undergo + normalization, which can be helpful to preserve details that normalization + removes. + +* `schemaReportingInitialDelayMaxMs`: number + + By default, the schema reporter will wait a random amount of time between 0 + and 10 seconds before making its first report at reporter startup. A longer + range of times leads to more staggered starts, which reduces bandwidth + since it makes it less likely that multiple servers will get asked to upload + the same schema. However, in certain constrained environments (e.g. AWS + Lambda), this wait time may be less desirable. This option can be used to + change the maximum amount of time that the reporter will wait until it starts + sending reports. diff --git a/docs/source/integrations/middleware.md b/docs/source/integrations/middleware.md index 87034e184fe..e08bd7327bf 100644 --- a/docs/source/integrations/middleware.md +++ b/docs/source/integrations/middleware.md @@ -11,9 +11,15 @@ the core `apollo-server` package: | Middleware | Package | |---|---| | Express | `apollo-server-express` | -| Fastify | `apollo-server-fastify` | -| hapi | `apollo-server-hapi` | +| AWS Lambda | `apollo-server-lambda` | | Koa | `apollo-server-koa` | +| hapi | `apollo-server-hapi` | +| Micro | `apollo-server-micro` | +| Fastify | `apollo-server-fastify` | +| Google Cloud Functions | `apollo-server-cloud-functions` | +| Azure Functions | `apollo-server-azure-functions` | +| Cloudflare | `apollo-server-cloudflare` | + If you've already installed the core `apollo-server` package, you can `npm uninstall` it after installing an integration package. diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index 4b88ceb99a4..fa407f57946 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -1,6 +1,6 @@ { "name": "apollo-engine-reporting", - "version": "2.0.1", + "version": "2.2.0", "description": "Send reports about your GraphQL services to Apollo Graph Manager (previously known as Apollo Engine)", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 70a85d48fb4..8c80407b674 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -3,7 +3,7 @@ import { graphql, GraphQLError, printSchema } from 'graphql'; import { Request } from 'node-fetch'; import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin'; import { Headers } from 'apollo-server-env'; -import { AddTraceArgs, computeExecutableSchemaId } from '../agent'; +import { computeExecutableSchemaId } from '../agent'; import { Trace } from 'apollo-engine-reporting-protobuf'; import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; @@ -42,11 +42,23 @@ const query = ` } `; +const queryReport = ` + query report { + author(id: 5) { + name + posts(limit: 2) { + id + } + } + aBoolean + } +`; + describe('schema reporting', () => { const schema = makeExecutableSchema({ typeDefs }); addMockFunctionsToSchema({ schema }); - const addTrace = jest.fn().mockResolvedValue(undefined); + const addTrace = jest.fn(() => Promise.resolve()); const startSchemaReporting = jest.fn(); const executableSchemaIdGenerator = jest.fn(computeExecutableSchemaId); @@ -96,7 +108,7 @@ describe('schema reporting', () => { it('uses the override schema', async () => { const pluginInstance = plugin( { - experimental_overrideReportedSchema: typeDefs, + overrideReportedSchema: typeDefs, }, addTrace, { @@ -191,12 +203,9 @@ it('trace construction', async () => { const schema = makeExecutableSchema({ typeDefs }); addMockFunctionsToSchema({ schema }); - const traces: Array = []; - async function addTrace(args: AddTraceArgs) { - traces.push(args); - } const startSchemaReporting = jest.fn(); const executableSchemaIdGenerator = jest.fn(); + const addTrace = jest.fn(() => Promise.resolve()); const pluginInstance = plugin( { @@ -462,6 +471,164 @@ function makeTestHTTP(): Trace.HTTP { }); } +describe('tests for the "reportTiming', () => { + const schemaReportingFunctions = { + startSchemaReporting: jest.fn(), + executableSchemaIdGenerator: jest.fn(), + }; + const schema = makeExecutableSchema({ typeDefs }); + addMockFunctionsToSchema({ schema }); + + const addTrace = jest.fn(() => Promise.resolve()); + beforeEach(() => { + addTrace.mockClear(); + }); + + it('report no traces', async () => { + const pluginInstance = plugin( + { reportTiming: false }, + addTrace, + schemaReportingFunctions, + ); + + const context = await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + expect(context.metrics.captureTraces).toBeFalsy(); + }); + + it('report traces based on operation name', async () => { + const pluginInstance = plugin( + { + reportTiming: async request => { + return request.request.operationName === 'report'; + }, + }, + addTrace, + schemaReportingFunctions, + ); + + const context1 = await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query: queryReport, + operationName: 'report', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + expect(addTrace).toBeCalledTimes(1); + expect(context1.metrics.captureTraces).toBeTruthy(); + addTrace.mockClear(); + + const context2 = await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + expect(addTrace).not.toBeCalled(); + expect(context2.metrics.captureTraces).toBeFalsy(); + }); + + it('report traces async based on operation name', async () => { + const pluginInstance = plugin( + { + reportTiming: async request => { + return await (async () => { + return request.request.operationName === 'report'; + })(); + }, + }, + addTrace, + schemaReportingFunctions + ); + + const context1 = await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query: queryReport, + operationName: 'report', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + expect(addTrace).toBeCalledTimes(1); + expect(context1.metrics.captureTraces).toBeTruthy(); + addTrace.mockClear(); + + const context2 = await pluginTestHarness({ + pluginInstance, + schema, + graphqlRequest: { + query, + operationName: 'q', + extensions: { + clientName: 'testing suite', + }, + http: new Request('http://localhost:123/foo'), + }, + executor: async ({ request: { query: source } }) => { + return await graphql({ + schema, + source, + }); + }, + }); + + expect(addTrace).not.toBeCalled(); + expect(context2.metrics.captureTraces).toBeFalsy(); + }); +}); + /** * TESTS FOR THE sendHeaders REPORTING OPTION */ diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 5dc782da05e..cca8cb0c4bf 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -17,7 +17,12 @@ import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; import { plugin } from './plugin'; -import { GraphQLRequestContext, Logger } from 'apollo-server-types'; +import { + GraphQLRequestContext, + GraphQLRequestContextDidEncounterErrors, + GraphQLRequestContextDidResolveOperation, + Logger, +} from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; @@ -52,6 +57,14 @@ export type VariableValueOptions = } | SendValuesBaseOptions; +export type ReportTimingOptions = + | (( + request: + | GraphQLRequestContextDidResolveOperation + | GraphQLRequestContextDidEncounterErrors, + ) => Promise) + | boolean; + export type GenerateClientInfo = ( requestContext: GraphQLRequestContext, ) => ClientInfo; @@ -203,6 +216,45 @@ export interface EngineReportingOptions { * TODO(helen): LINK TO EXAMPLE FUNCTION? e.g. a function recursively search for keys to be blocklisted */ sendVariableValues?: VariableValueOptions; + /** + * This option allows configuring the behavior of request tracing and + * reporting to [Apollo Graph Manager](https://engine.apollographql.com/). + * + * By default, this is set to `true`, which results in *all* requests being + * traced and reported. This behavior can be _disabled_ by setting this option + * to `false`. Alternatively, it can be selectively enabled or disabled on a + * per-request basis using a predicate function. + * + * When specified as a predicate function, the _return value_ of its + * invocation (per request) will determine whether or not that request is + * traced and reported. The predicate function will receive the request + * context. If validation and parsing of the request succeeds the function will + * receive the request context in the + * [`GraphQLRequestContextDidResolveOperation`](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#didresolveoperation) + * phase, which permits tracing based on dynamic properties, e.g., HTTP + * headers or the `operationName` (when available), + * otherwise it will receive the request context in the [`GraphQLRequestContextDidEncounterError`](https://www.apollographql.com/docs/apollo-server/integrations/plugins/#didencountererrors) + * phase: + * + * **Example:** + * + * ```js + * reportTiming(requestContext) { + * // Always trace `query HomeQuery { ... }`. + * if (requestContext.operationName === "HomeQuery") return true; + * + * // Also trace if the "trace" header is set to "true". + * if (requestContext.request.http?.headers?.get("trace") === "true") { + * return true; + * } + * + * // Otherwise, do not trace! + * return false; + * }, + * ``` + * + */ + reportTiming?: ReportTimingOptions; /** * [DEPRECATED] Use sendVariableValues * Passing an array into privateVariables is equivalent to passing { exceptNames: array } into @@ -277,10 +329,9 @@ export interface EngineReportingOptions { generateClientInfo?: GenerateClientInfo; /** - * **(Experimental)** Enable schema reporting from this server with - * Apollo Graph Manager. + * Enable schema reporting from this server with Apollo Graph Manager. * - * The use of this option avoids the need to rgister schemas manually within + * The use of this option avoids the need to register schemas manually within * CI deployment pipelines using `apollo schema:push` by periodically * reporting this server's schema (when changes are detected) along with * additional details about its runtime environment to Apollo Graph Manager. @@ -289,14 +340,14 @@ export interface EngineReportingOptions { * documentation_](https://github.com/apollographql/apollo-schema-reporting-preview-docs) * for more information. */ - experimental_schemaReporting?: boolean; + reportSchema?: boolean; /** * Override the reported schema that is reported to AGM. * This schema does not go through any normalizations and the string is directly sent to Apollo Graph Manager. * This would be useful for comments or other ordering and whitespace changes that get stripped when generating a `GraphQLSchema` */ - experimental_overrideReportedSchema?: string; + overrideReportedSchema?: string; /** * The schema reporter waits before starting reporting. @@ -310,7 +361,7 @@ export interface EngineReportingOptions { * This number will be the max for the range in ms that the schema reporter will * wait before starting to report. */ - experimental_schemaReportingInitialDelayMaxMs?: number; + schemaReportingInitialDelayMaxMs?: number; /** * The URL to use for reporting schemas. @@ -323,6 +374,21 @@ export interface EngineReportingOptions { * `console` when that is not available. */ logger?: Logger; + + /** + * @deprecated use {@link reportSchema} instead + */ + experimental_schemaReporting?: boolean; + + /** + * @deprecated use {@link overrideReportedSchema} instead + */ + experimental_overrideReportedSchema?: string; + + /** + * @deprecated use {@link schemaReportingInitialDelayMaxMs} instead + */ + experimental_schemaReportingInitialDelayMaxMs?: number; } export interface AddTraceArgs { @@ -332,7 +398,7 @@ export interface AddTraceArgs { executableSchemaId: string; source?: string; document?: DocumentNode; - logger: Logger, + logger: Logger; } const serviceHeaderDefaults = { @@ -389,7 +455,7 @@ export class EngineReportingAgent { }; private readonly tracesEndpointUrl: string; - private readonly schemaReport: boolean; + readonly schemaReport: boolean; public constructor(options: EngineReportingOptions = {}) { this.options = options; @@ -408,9 +474,44 @@ export class EngineReportingAgent { ); } - if (options.experimental_schemaReporting !== undefined) { - this.schemaReport = options.experimental_schemaReporting; + this.logger.warn( + [ + '[deprecated] The "experimental_schemaReporting" option has been', + 'renamed to "reportSchema"' + ].join(' ') + ); + if (options.reportSchema === undefined) { + options.reportSchema = options.experimental_schemaReporting; + } + } + + if (options.experimental_overrideReportedSchema !== undefined) { + this.logger.warn( + [ + '[deprecated] The "experimental_overrideReportedSchema" option has', + 'been renamed to "overrideReportedSchema"' + ].join(' ') + ); + if (options.overrideReportedSchema === undefined) { + options.overrideReportedSchema = options.experimental_overrideReportedSchema; + } + } + + if (options.experimental_schemaReportingInitialDelayMaxMs !== undefined) { + this.logger.warn( + [ + '[deprecated] The "experimental_schemaReportingInitialDelayMaxMs"', + 'option has been renamed to "schemaReportingInitialDelayMaxMs"' + ].join(' ') + ); + if (options.schemaReportingInitialDelayMaxMs === undefined) { + options.schemaReportingInitialDelayMaxMs = options.experimental_schemaReportingInitialDelayMaxMs; + } + } + + if (options.reportSchema !== undefined) { + this.schemaReport = options.reportSchema; } else { this.schemaReport = process.env.APOLLO_SCHEMA_REPORTING === "true" } @@ -430,7 +531,7 @@ export class EngineReportingAgent { if (this.options.handleSignals !== false) { const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']; - signals.forEach((signal) => { + signals.forEach(signal => { // Note: Node only started sending signal names to signal events with // Node v10 so we can't use that feature here. const handler: NodeJS.SignalsListener = async () => { @@ -555,7 +656,7 @@ export class EngineReportingAgent { public async sendAllReports(): Promise { await Promise.all( - Object.keys(this.reportDataByExecutableSchemaId).map((id) => + Object.keys(this.reportDataByExecutableSchemaId).map(id => this.sendReport(id), ), ); @@ -630,9 +731,8 @@ export class EngineReportingAgent { if (curResponse.status >= 500 && curResponse.status < 600) { throw new Error( - `HTTP status ${curResponse.status}, ${ - (await curResponse.text()) || '(no body)' - }`, + `HTTP status ${curResponse.status}, ${(await curResponse.text()) || + '(no body)'}`, ); } else { return curResponse; @@ -678,18 +778,20 @@ export class EngineReportingAgent { executableSchemaId: string; executableSchema: string; }) { - this.logger.info('Starting schema reporter...') - - this.logger.info( - `Schema reporter options: ${JSON.stringify( - { - overrideReportedSchema: this.options.experimental_overrideReportedSchema, - schemaReportingInitialDelayMaxMs: this.options.experimental_schemaReportingInitialDelayMaxMs, - schemaReportingUrl: this.options.schemaReportingUrl, - } - )}` - ) - + this.logger.info('Starting schema reporter...'); + if (this.options.overrideReportedSchema !== undefined) { + this.logger.info('Schema to report has been overridden'); + } + if (this.options.schemaReportingInitialDelayMaxMs !== undefined) { + this.logger.info(`Schema reporting max initial delay override: ${ + this.options.schemaReportingInitialDelayMaxMs + } ms`); + } + if (this.options.schemaReportingUrl !== undefined) { + this.logger.info(`Schema reporting URL override: ${ + this.options.schemaReportingUrl + }`); + } if (this.currentSchemaReporter) { this.currentSchemaReporter.stop(); } @@ -714,13 +816,13 @@ export class EngineReportingAgent { }; this.logger.info( - `Schema reporter EdgeServerInfo: ${JSON.stringify(serverInfo)}` + `Schema reporting EdgeServerInfo: ${JSON.stringify(serverInfo)}` ) // Jitter the startup between 0 and 10 seconds const delay = Math.floor( Math.random() * - (this.options.experimental_schemaReportingInitialDelayMaxMs || 10_000), + (this.options.schemaReportingInitialDelayMaxMs || 10_000), ); const schemaReporter = new SchemaReporter( @@ -728,6 +830,7 @@ export class EngineReportingAgent { executableSchema, this.apiKey, this.options.schemaReportingUrl, + this.logger ); const fallbackReportingDelayInMs = 20_000; @@ -735,7 +838,7 @@ export class EngineReportingAgent { this.currentSchemaReporter = schemaReporter; const logger = this.logger; - setTimeout(function () { + setTimeout(function() { reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs); }, delay); } @@ -818,29 +921,25 @@ export class EngineReportingAgent { // either the request-specific logger on the request context (if available) // or to the `logger` that was passed into `EngineReportingOptions` which // is provided in the `EngineReportingAgent` constructor options. - this.signatureCache.set(cacheKey, generatedSignature) - .catch(err => { - logger.warn( - 'Could not store signature cache. ' + - (err && err.message) || err - ) - }); + this.signatureCache.set(cacheKey, generatedSignature).catch(err => { + logger.warn( + 'Could not store signature cache. ' + (err && err.message) || err, + ); + }); return generatedSignature; } private async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys( - this.reportDataByExecutableSchemaId, - ).map((executableSchemaId) => + Object.keys(this.reportDataByExecutableSchemaId).map(executableSchemaId => this.sendReportAndReportErrors(executableSchemaId), ), ); } private sendReportAndReportErrors(executableSchemaId: string): Promise { - return this.sendReport(executableSchemaId).catch((err) => { + return this.sendReport(executableSchemaId).catch(err => { // This catch block is primarily intended to catch network errors from // the retried request itself, which include network errors and non-2xx // HTTP errors. diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 15f242073ca..32873e5347b 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -3,6 +3,7 @@ import { Logger, GraphQLRequestContextDidEncounterErrors, GraphQLRequestContextWillSendResponse, + GraphQLRequestContextDidResolveOperation, } from 'apollo-server-types'; import { Headers } from 'apollo-server-env'; import { GraphQLSchema, printSchema } from 'graphql'; @@ -62,9 +63,9 @@ export const plugin = ( if (!schemaReport) return; startSchemaReporting({ executableSchema: - options.experimental_overrideReportedSchema || printSchema(schema), + options.overrideReportedSchema || printSchema(schema), executableSchemaId: executableSchemaIdGenerator( - options.experimental_overrideReportedSchema || schema, + options.overrideReportedSchema || schema, ), }); }, @@ -79,6 +80,19 @@ export const plugin = ( * request context when it's provided. */ const logger = requestLogger || loggerForPlugin; + if ( + !['function', 'boolean', 'undefined'].includes( + typeof options.reportTiming, + ) + ) { + throw new Error('Invalid option passed to `reportTiming`.'); + } + + // If the options are false don't do any metrics timing. + if (options.reportTiming === false) { + metrics.captureTraces = false; + return; + } const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder( { @@ -116,6 +130,39 @@ export const plugin = ( } } + async function shouldTraceOperation( + requestContext: + | GraphQLRequestContextDidResolveOperation + | GraphQLRequestContextDidEncounterErrors, + ): Promise { + // This could be hit if we call `shouldTraceOperation` more than once during a request. + // such as if `didEncounterError` gets called after `didResolveOperation`. + if (metrics.captureTraces !== undefined) + return; + + if (typeof options.reportTiming === 'boolean') { + metrics.captureTraces = options.reportTiming; + return; + } + + if (typeof options.reportTiming !== 'function') { + // Default case we always report + metrics.captureTraces = true; + return; + } + + metrics.captureTraces = await options.reportTiming(requestContext); + + // Help the user understand they've returned an unexpected value, + // which might be a subtle mistake. + if (typeof metrics.captureTraces !== 'boolean') { + logger.warn( + "The 'reportTiming' predicate function must return a boolean value.", + ); + metrics.captureTraces = true; + } + } + /** * Due to a number of exceptions in the request pipeline — which are * intended to preserve backwards compatible behavior with the @@ -130,12 +177,21 @@ export const plugin = ( function didEnd( requestContext: | GraphQLRequestContextWillSendResponse - | GraphQLRequestContextDidEncounterErrors, + | GraphQLRequestContextDidEncounterErrors + | GraphQLRequestContextDidResolveOperation, ) { if (endDone) return; endDone = true; treeBuilder.stopTiming(); + if (metrics.captureTraces === undefined) { + logger.warn( + "captureTrace is undefined at the end of the request. This is a bug in the Apollo Engine Reporting plugin." + ); + } + + if (metrics.captureTraces === false) return; + treeBuilder.trace.fullQueryCacheHit = !!metrics.responseCacheHit; treeBuilder.trace.forbiddenOperation = !!metrics.forbiddenOperation; treeBuilder.trace.registeredOperation = !!metrics.registeredOperation; @@ -178,7 +234,7 @@ export const plugin = ( source: requestContext.source, trace: treeBuilder.trace, executableSchemaId: executableSchemaIdGenerator( - options.experimental_overrideReportedSchema || schema, + options.overrideReportedSchema || schema, ), logger, }).catch(logger.error); @@ -225,8 +281,20 @@ export const plugin = ( treeBuilder.trace.clientName = clientName || ''; } }, + async didResolveOperation(requestContext) { + await shouldTraceOperation(requestContext); + if (metrics.captureTraces === false) { + // End early if we aren't going to send the trace so we continue to + // run the tree builder. + didEnd(requestContext); + } + }, executionDidStart() { + // If we stopped tracing early, return undefined so we don't trace + // an object + if (endDone) return; + return { willResolveField({ info }) { return treeBuilder.willResolveField(info); @@ -238,16 +306,21 @@ export const plugin = ( }, willSendResponse(requestContext) { + // shouldTraceOperation will be called before this in `didResolveOperation` + // so we don't need to call it again here. + // See comment above for why `didEnd` must be called in two hooks. didEnd(requestContext); }, - - didEncounterErrors(requestContext) { + async didEncounterErrors(requestContext) { // Search above for a comment about "didResolveSource" to see which // of the pre-source-resolution errors we are intentionally avoiding. - if (!didResolveSource) return; + if (!didResolveSource || endDone) return; treeBuilder.didEncounterErrors(requestContext.errors); + // This will exit early if we have already set metrics.captureTraces + await shouldTraceOperation(requestContext); + // See comment above for why `didEnd` must be called in two hooks. didEnd(requestContext); }, diff --git a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts index 1bf7b182d51..1d1d8579483 100644 --- a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts +++ b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts @@ -4,25 +4,42 @@ // This file was automatically generated and should not be edited. // ==================================================== -// GraphQL mutation operation: AutoregReportServerInfo +// GraphQL mutation operation: ReportServerInfo // ==================================================== import { GraphQLFormattedError } from 'graphql'; +export interface SchemaReportingServerInfoResult { + data?: ReportServerInfo; + errors?: ReadonlyArray; +} export interface ReportServerInfo_me_UserMutation { __typename: 'UserMutation'; } -export interface ReportServerInfo_me_ServiceMutation_reportServerInfo { +export interface ReportServerInfo_me_ServiceMutation_reportServerInfo_ReportServerInfoError { + __typename: 'ReportServerInfoError'; + message: string; + code: ReportServerInfoErrorCode; +} + +export interface ReportServerInfo_me_ServiceMutation_reportServerInfo_ReportServerInfoResponse { __typename: 'ReportServerInfoResponse'; inSeconds: number; withExecutableSchema: boolean; } +export type ReportServerInfo_me_ServiceMutation_reportServerInfo = + | ReportServerInfo_me_ServiceMutation_reportServerInfo_ReportServerInfoError + | ReportServerInfo_me_ServiceMutation_reportServerInfo_ReportServerInfoResponse; + export interface ReportServerInfo_me_ServiceMutation { __typename: 'ServiceMutation'; /** - * Schema auto-registration. Private alpha. + * Report information about a running GraphQL server instance, used for automatic + * schema reporting. This can optionally include an `executableSchema`, in the + * form of a GraphQL document, and should only do so if requested explicitly in + * response to a previous report that designates `withSchema: true` */ reportServerInfo: ReportServerInfo_me_ServiceMutation_reportServerInfo | null; } @@ -31,20 +48,30 @@ export type ReportServerInfo_me = | ReportServerInfo_me_UserMutation | ReportServerInfo_me_ServiceMutation; -export interface SchemaReportingServerInfo { +export interface ReportServerInfo { me: ReportServerInfo_me | null; } -export interface SchemaReportingServerInfoResult { - data?: SchemaReportingServerInfo; - errors?: ReadonlyArray; -} - export interface ReportServerInfoVariables { info: EdgeServerInfo; executableSchema?: string | null; } +export enum ReportServerInfoErrorCode { + BOOT_ID_IS_NOT_VALID_UUID = 'BOOT_ID_IS_NOT_VALID_UUID', + BOOT_ID_IS_REQUIRED = 'BOOT_ID_IS_REQUIRED', + EXECUTABLE_SCHEMA_ID_IS_NOT_SCHEMA_SHA256 = 'EXECUTABLE_SCHEMA_ID_IS_NOT_SCHEMA_SHA256', + EXECUTABLE_SCHEMA_ID_IS_REQUIRED = 'EXECUTABLE_SCHEMA_ID_IS_REQUIRED', + EXECUTABLE_SCHEMA_ID_IS_TOO_LONG = 'EXECUTABLE_SCHEMA_ID_IS_TOO_LONG', + GRAPH_VARIANT_DOES_NOT_MATCH_REGEX = 'GRAPH_VARIANT_DOES_NOT_MATCH_REGEX', + GRAPH_VARIANT_IS_REQUIRED = 'GRAPH_VARIANT_IS_REQUIRED', + LIBRARY_VERSION_IS_TOO_LONG = 'LIBRARY_VERSION_IS_TOO_LONG', + PLATFORM_IS_TOO_LONG = 'PLATFORM_IS_TOO_LONG', + RUNTIME_VERSION_IS_TOO_LONG = 'RUNTIME_VERSION_IS_TOO_LONG', + SERVER_ID_IS_TOO_LONG = 'SERVER_ID_IS_TOO_LONG', + USER_VERSION_IS_TOO_LONG = 'USER_VERSION_IS_TOO_LONG', +} + /** * Edge server info */ diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index a14cdc28353..b6900439734 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -12,14 +12,34 @@ export const reportServerInfoGql = ` __typename ... on ServiceMutation { reportServerInfo(info: $info, executableSchema: $executableSchema) { - inSeconds - withExecutableSchema + __typename + ... on ReportServerInfoError { + message + code + } + ... on ReportServerInfoResponse { + inSeconds + withExecutableSchema + } } } } } `; +export type ReportInfoResult = ReportInfoStop | ReportInfoNext; + +export interface ReportInfoNext { + kind: 'next'; + inSeconds: number; + withExecutableSchema: boolean; +} + +export interface ReportInfoStop { + kind: 'stop'; + stopReporting: true +} + export function reportingLoop( schemaReporter: SchemaReporter, logger: Logger, @@ -35,9 +55,17 @@ export function reportingLoop( // Apollo Graph Manager schemaReporter .reportServerInfo(sendNextWithExecutableSchema) - .then(({ inSeconds, withExecutableSchema }) => { - sendNextWithExecutableSchema = withExecutableSchema; - setTimeout(inner, inSeconds * 1000); + .then((result: ReportInfoResult) => { + switch(result.kind) { + case "next": { + sendNextWithExecutableSchema = result.withExecutableSchema; + setTimeout(inner, result.inSeconds * 1000); + return; + } + case "stop": { + return; + } + } }) .catch((error: any) => { // In the case of an error we want to continue looping @@ -54,11 +82,6 @@ export function reportingLoop( inner(); } -interface ReportServerInfoReturnVal { - inSeconds: number; - withExecutableSchema: boolean; -} - // This class is meant to be a thin shim around the gql mutations. export class SchemaReporter { // These mirror the gql variables @@ -68,12 +91,14 @@ export class SchemaReporter { private isStopped: boolean; private readonly headers: Headers; + private readonly logger: Logger; constructor( serverInfo: EdgeServerInfo, schemaSdl: string, apiKey: string, schemaReportingEndpoint: string | undefined, + logger: Logger, ) { this.headers = new Headers(); this.headers.set('Content-Type', 'application/json'); @@ -91,6 +116,7 @@ export class SchemaReporter { this.serverInfo = serverInfo; this.executableSchemaDocument = schemaSdl; this.isStopped = false; + this.logger = logger; } public stopped(): Boolean { @@ -103,7 +129,7 @@ export class SchemaReporter { public async reportServerInfo( withExecutableSchema: boolean, - ): Promise { + ): Promise { const { data, errors } = await this.graphManagerQuery({ info: this.serverInfo, executableSchema: withExecutableSchema @@ -139,14 +165,28 @@ export class SchemaReporter { 'https://engine.apollographql.com/ to obtain an API key for a service.', ].join(' '), ); - } else if (data.me.__typename === 'ServiceMutation') { - if (!data.me.reportServerInfo) { - throw new Error(msgForUnexpectedResponse(data)); + } else if ( + data.me.__typename === 'ServiceMutation' && + data.me.reportServerInfo + ) { + if (data.me.reportServerInfo.__typename == 'ReportServerInfoResponse') { + return { ...data.me.reportServerInfo, kind: 'next'}; + } else { + this.logger.error( + [ + 'Received input validation error from Graph Manager:', + data.me.reportServerInfo.message, + 'Stopping reporting. Please fix the input errors.', + ].join(' '), + ); + this.stop(); + return { + stopReporting: true, + kind: 'stop' + }; } - return data.me.reportServerInfo; - } else { - throw new Error(msgForUnexpectedResponse(data)); } + throw new Error(msgForUnexpectedResponse(data)); } private async graphManagerQuery( @@ -166,10 +206,12 @@ export class SchemaReporter { const httpResponse = await fetch(httpRequest); if (!httpResponse.ok) { - throw new Error([ - `An unexpected HTTP status code (${httpResponse.status}) was`, - 'encountered during schema reporting.' - ].join(' ')); + throw new Error( + [ + `An unexpected HTTP status code (${httpResponse.status}) was`, + 'encountered during schema reporting.', + ].join(' '), + ); } try { @@ -182,7 +224,7 @@ export class SchemaReporter { "Couldn't report server info to Apollo Graph Manager.", 'Parsing response as JSON failed.', 'If this continues please reach out to support@apollographql.com', - error + error, ].join(' '), ); } diff --git a/packages/apollo-federation/CHANGELOG.md b/packages/apollo-federation/CHANGELOG.md index c9000df4cbb..dcaf81e8931 100644 --- a/packages/apollo-federation/CHANGELOG.md +++ b/packages/apollo-federation/CHANGELOG.md @@ -6,6 +6,10 @@ - _Nothing yet! Stay tuned._ +## 0.16.7 + +- Only changes in the similarly versioned `@apollo/gateway` package. + ## 0.16.6 - In-house `Maybe` type which was previously imported from `graphql` and has been moved in `v15.1.0`. [#4230](https://github.com/apollographql/apollo-server/pull/4230) diff --git a/packages/apollo-federation/package.json b/packages/apollo-federation/package.json index a924e70761f..62a2069a406 100644 --- a/packages/apollo-federation/package.json +++ b/packages/apollo-federation/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/federation", - "version": "0.16.6", + "version": "0.16.8", "description": "Apollo Federation Utilities", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-gateway/CHANGELOG.md b/packages/apollo-gateway/CHANGELOG.md index 70f9c7ab29b..4497dd72045 100644 --- a/packages/apollo-gateway/CHANGELOG.md +++ b/packages/apollo-gateway/CHANGELOG.md @@ -6,6 +6,10 @@ - __FIX__: Continue resolving when an `@external` reference cannot be resolved. [#3914](https://github.com/apollographql/apollo-server/pull/3914) +## 0.16.7 + +- Bumped the version of `apollo-server-core`, but no other changes! + ## 0.16.6 - Only changes in the similarly versioned `@apollo/federation` package. diff --git a/packages/apollo-gateway/package.json b/packages/apollo-gateway/package.json index 98b630124cd..5a2208289e1 100644 --- a/packages/apollo-gateway/package.json +++ b/packages/apollo-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/gateway", - "version": "0.16.6", + "version": "0.16.8", "description": "Apollo Gateway", "author": "opensource@apollographql.com", "main": "dist/index.js", diff --git a/packages/apollo-server-azure-functions/package.json b/packages/apollo-server-azure-functions/package.json index 0e9293b9898..02ef30a8833 100644 --- a/packages/apollo-server-azure-functions/package.json +++ b/packages/apollo-server-azure-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-azure-functions", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Azure Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloud-functions/package.json b/packages/apollo-server-cloud-functions/package.json index 72a1a4a8377..a54a5fc8d57 100644 --- a/packages/apollo-server-cloud-functions/package.json +++ b/packages/apollo-server-cloud-functions/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloud-functions", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Google Cloud Functions", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-cloudflare/package.json b/packages/apollo-server-cloudflare/package.json index dbee0ef2dc8..7eb5a3fe2c2 100644 --- a/packages/apollo-server-cloudflare/package.json +++ b/packages/apollo-server-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cloudflare", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Cloudflare workers", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-core/package.json b/packages/apollo-server-core/package.json index 430cab790c7..ee7f4a508dd 100644 --- a/packages/apollo-server-core/package.json +++ b/packages/apollo-server-core/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-core", - "version": "2.14.4", + "version": "2.15.0", "description": "Core engine for Apollo GraphQL server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index fd8b6098e96..bdd93df6908 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -314,6 +314,16 @@ export class ApolloServerBase { // Don't add the extension here (we want to add it later in generateSchemaDerivedData). } + if (gateway && this.engineReportingAgent?.schemaReport) { + throw new Error( + [ + "Schema reporting is not yet compatible with the gateway. If you're", + "interested in using schema reporting with the gateway, please", + "contact Apollo support.", + ].join(' '), + ); + } + if (gateway && subscriptions !== false) { // TODO: this could be handled by adjusting the typings to keep gateway configs and non-gateway configs separate. throw new Error( @@ -691,9 +701,13 @@ export class ApolloServerBase { // survive transformations like monkey-patching a boolean field onto the // schema. // - // The only thing this is used for is determining whether traces should be - // added to responses if requested with an HTTP header; if there's a false - // positive, that feature can be disabled by specifying `engine: false`. + // This is used for two things: + // 1) determining whether traces should be added to responses if requested + // with an HTTP header; if there's a false positive, that feature can be + // disabled by specifying `engine: false`. + // 2) determining whether schema-reporting should be allowed; federated + // services shouldn't be reporting schemas, and we accordingly throw if + // it's attempted. private schemaIsFederated(schema: GraphQLSchema): boolean { const serviceType = schema.getType('_Service'); if (!(serviceType && isObjectType(serviceType))) { @@ -766,6 +780,16 @@ export class ApolloServerBase { 'to report metrics to Apollo Graph Manager. You should only configure your Apollo gateway ' + 'to report metrics to Apollo Graph Manager.', ); + + if (this.engineReportingAgent.schemaReport) { + throw Error( + [ + "Schema reporting is not yet compatible with federated services.", + "If you're interested in using schema reporting with federated", + "services, please contact Apollo support.", + ].join(' '), + ); + } } pluginsToInit.push(this.engineReportingAgent!.newPlugin()); } else if (engine !== false && federatedSchema) { @@ -847,7 +871,6 @@ export class ApolloServerBase { any >, parseOptions: this.parseOptions, - reporting: !!this.engineReportingAgent, ...this.requestOptions, }; } diff --git a/packages/apollo-server-core/src/graphqlOptions.ts b/packages/apollo-server-core/src/graphqlOptions.ts index b4c6f1a7b86..d9ed111052c 100644 --- a/packages/apollo-server-core/src/graphqlOptions.ts +++ b/packages/apollo-server-core/src/graphqlOptions.ts @@ -65,7 +65,6 @@ export interface GraphQLServerOptions< plugins?: ApolloServerPlugin[]; documentStore?: InMemoryLRUCache; parseOptions?: GraphQLParseOptions; - reporting?: boolean; } export type DataSources = { diff --git a/packages/apollo-server-core/src/requestPipeline.ts b/packages/apollo-server-core/src/requestPipeline.ts index d3588f04e1a..855cea3b192 100644 --- a/packages/apollo-server-core/src/requestPipeline.ts +++ b/packages/apollo-server-core/src/requestPipeline.ts @@ -131,12 +131,7 @@ export async function processGraphQLRequest( const logger = requestContext.logger || console; // If request context's `metrics` already exists, preserve it, but _ensure_ it - // exists there and shorthand it for use throughout this function. As of this - // comment, the sole known case where `metrics` already exists is when the - // `captureTraces` property is present and set to the result of the boolean - // `reporting` option on the legacy (V1) server options, here: - // https://git.io/Jfmsb. I suspect this disappears when this is the direct - // entry into request processing, rather than through, e.g. `runHttpQuery`. + // exists there and shorthand it for use throughout this function. const metrics = requestContext.metrics = requestContext.metrics || Object.create(null); diff --git a/packages/apollo-server-core/src/runHttpQuery.ts b/packages/apollo-server-core/src/runHttpQuery.ts index dd86f349464..58bb9b5206e 100644 --- a/packages/apollo-server-core/src/runHttpQuery.ts +++ b/packages/apollo-server-core/src/runHttpQuery.ts @@ -186,8 +186,6 @@ export async function runHttpQuery( debug: options.debug, plugins: options.plugins || [], - - reporting: options.reporting, }; return processHTTPRequest(config, request); @@ -265,9 +263,7 @@ export async function processHTTPRequest( context, cache: options.cache, debug: options.debug, - metrics: { - captureTraces: !!options.reporting, - }, + metrics: {}, }; } diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 9af86a5326a..9f2d9f51e3d 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -131,6 +131,12 @@ export default async function pluginTestHarness({ const executionListeners: GraphQLRequestExecutionListener[] = []; + // This logic is duplicated in the request pipeline right now. + await dispatcher.invokeHookAsync( + 'didResolveOperation', + requestContext as GraphQLRequestContextExecutionDidStart, + ); + // This execution dispatcher logic is duplicated in the request pipeline // right now. dispatcher.invokeHookSync( diff --git a/packages/apollo-server-express/package.json b/packages/apollo-server-express/package.json index 15c0ba46257..62115d478e8 100644 --- a/packages/apollo-server-express/package.json +++ b/packages/apollo-server-express/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-express", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Express and Connect", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-fastify/package.json b/packages/apollo-server-fastify/package.json index 16063fd76d0..0ff12ba6d4b 100644 --- a/packages/apollo-server-fastify/package.json +++ b/packages/apollo-server-fastify/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-fastify", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Fastify", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-hapi/package.json b/packages/apollo-server-hapi/package.json index dbef8fc7338..b0df1e0efe8 100644 --- a/packages/apollo-server-hapi/package.json +++ b/packages/apollo-server-hapi/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-hapi", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Hapi", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-integration-testsuite/package.json b/packages/apollo-server-integration-testsuite/package.json index 9b7c67c0308..47f683344b5 100644 --- a/packages/apollo-server-integration-testsuite/package.json +++ b/packages/apollo-server-integration-testsuite/package.json @@ -1,7 +1,7 @@ { "name": "apollo-server-integration-testsuite", "private": true, - "version": "2.14.4", + "version": "2.15.0", "description": "Apollo Server Integrations testsuite", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-koa/package.json b/packages/apollo-server-koa/package.json index a205d9555f4..efd26acbca9 100644 --- a/packages/apollo-server-koa/package.json +++ b/packages/apollo-server-koa/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-koa", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Koa", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-lambda/package.json b/packages/apollo-server-lambda/package.json index fa2b4686e05..52eea9de0b8 100644 --- a/packages/apollo-server-lambda/package.json +++ b/packages/apollo-server-lambda/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-lambda", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for AWS Lambda", "keywords": [ "GraphQL", diff --git a/packages/apollo-server-micro/package.json b/packages/apollo-server-micro/package.json index 3515f13a982..a5c2da0da15 100644 --- a/packages/apollo-server-micro/package.json +++ b/packages/apollo-server-micro/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-micro", - "version": "2.14.4", + "version": "2.15.0", "description": "Production-ready Node.js GraphQL server for Micro", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-operation-registry/CHANGELOG.md b/packages/apollo-server-plugin-operation-registry/CHANGELOG.md index 72b47ad66b6..7687cacf0cd 100644 --- a/packages/apollo-server-plugin-operation-registry/CHANGELOG.md +++ b/packages/apollo-server-plugin-operation-registry/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +### 0.4.1 + +- __BREAKING__: Use a content delivery network, fetch storage secrets and operation manifests from different domains: https://storage-secrets.api.apollographql.com and https://operations.api.apollographql.com. Please mind any firewall for outgoing traffic. + +### 0.4.0: + +- This version was accidentally skipped due to a manual update of `package.json`, see `v0.4.1` - @trevor-scheer + ### 0.3.1: - The `schemaTag` option is now deprecated and superseded by `graphVariant`. diff --git a/packages/apollo-server-plugin-operation-registry/package.json b/packages/apollo-server-plugin-operation-registry/package.json index 1e7a3c4763c..6edb623cc39 100644 --- a/packages/apollo-server-plugin-operation-registry/package.json +++ b/packages/apollo-server-plugin-operation-registry/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-plugin-operation-registry", - "version": "0.3.3", + "version": "0.4.2", "description": "Apollo Server operation registry", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server-plugin-operation-registry/src/common.ts b/packages/apollo-server-plugin-operation-registry/src/common.ts index 0577d681043..ae9093379a7 100644 --- a/packages/apollo-server-plugin-operation-registry/src/common.ts +++ b/packages/apollo-server-plugin-operation-registry/src/common.ts @@ -6,33 +6,25 @@ export const envOverrideOperationManifest = export const envOverrideStorageSecretBaseUrl = 'APOLLO_STORAGE_SECRET_BASE_URL'; -export const fakeTestBaseUrl = 'https://fake-host-for-apollo-op-reg-tests/'; +export const fakeTestBaseUrl = 'https://fake-host-for-apollo-op-reg-tests'; // Generate and cache our desired operation manifest URL. -export const urlOperationManifestBase: string = ((): string => { - const desiredUrl = - process.env[envOverrideOperationManifest] || - // See src/__tests__/jestSetup.ts for more details on this env. variable. - process.env['__APOLLO_OPERATION_REGISTRY_TESTS__'] === 'true' - ? fakeTestBaseUrl - : 'https://storage.googleapis.com/engine-op-manifest-storage-prod/'; - - // Make sure it has NO trailing slash. - return desiredUrl.replace(/\/$/, ''); -})(); +export const urlOperationManifestBase: string = + // Remove trailing slash if any. + process.env[envOverrideOperationManifest]?.replace(/\/$/, '') || + // See src/__tests__/jestSetup.ts for more details on this env. variable. + process.env['__APOLLO_OPERATION_REGISTRY_TESTS__'] === 'true' + ? fakeTestBaseUrl + : 'https://operations.api.apollographql.com'; // Generate and cache our desired storage secret URL. -export const urlStorageSecretBase: string = ((): string => { - const desiredUrl = - process.env[envOverrideStorageSecretBaseUrl] || - // See src/__tests__/jestSetup.ts for more details on this env. variable. - process.env['__APOLLO_OPERATION_REGISTRY_TESTS__'] === 'true' - ? fakeTestBaseUrl - : 'https://storage.googleapis.com/engine-partial-schema-prod/'; - - // Make sure it has NO trailing slash. - return desiredUrl.replace(/\/$/, ''); -})(); +export const urlStorageSecretBase: string = + // Remove trailing slash if any. + process.env[envOverrideStorageSecretBaseUrl]?.replace(/\/$/, '') || + // See src/__tests__/jestSetup.ts for more details on this env. variable. + process.env['__APOLLO_OPERATION_REGISTRY_TESTS__'] === 'true' + ? fakeTestBaseUrl + : 'https://storage-secrets.api.apollographql.com'; export const getStoreKey = (signature: string) => `${signature}`; diff --git a/packages/apollo-server-testing/package.json b/packages/apollo-server-testing/package.json index f83705d0fe2..698341bace3 100644 --- a/packages/apollo-server-testing/package.json +++ b/packages/apollo-server-testing/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-testing", - "version": "2.14.4", + "version": "2.15.0", "description": "Test utils for apollo-server", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apollo-server/package.json b/packages/apollo-server/package.json index b4317fa050c..0fa9112cf78 100644 --- a/packages/apollo-server/package.json +++ b/packages/apollo-server/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server", - "version": "2.14.4", + "version": "2.15.0", "description": "Production ready GraphQL Server", "author": "opensource@apollographql.com", "main": "dist/index.js",