From ad3073e10f7c5e8b1ce738197443530f13babf06 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 11:24:17 -0400 Subject: [PATCH 01/30] Initial Schema Reporting commit --- .../src/reports.proto | 5 +- packages/apollo-engine-reporting/package.json | 6 +- packages/apollo-engine-reporting/src/agent.ts | 200 +++++++++++++++--- .../apollo-engine-reporting/src/plugin.ts | 35 ++- .../src/reportingOperationTypes.ts | 57 +++++ .../src/schemaReporter.ts | 165 +++++++++++++++ 6 files changed, 425 insertions(+), 43 deletions(-) create mode 100644 packages/apollo-engine-reporting/src/reportingOperationTypes.ts create mode 100644 packages/apollo-engine-reporting/src/schemaReporter.ts diff --git a/packages/apollo-engine-reporting-protobuf/src/reports.proto b/packages/apollo-engine-reporting-protobuf/src/reports.proto index 9f106f1c50b..7e01601f957 100644 --- a/packages/apollo-engine-reporting-protobuf/src/reports.proto +++ b/packages/apollo-engine-reporting-protobuf/src/reports.proto @@ -270,7 +270,10 @@ message ReportHeader { // eg "current", "prod" string schema_tag = 10; // The hex representation of the sha512 of the introspection response - string schema_hash = 11; + string deprecated_schema_hash = 11; + + // An id that is used to represent the schema to Apollo Graph Manager + string schema_id = 12; reserved 3; // removed string service = 3; } diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index 93259c7143f..3128894f355 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -11,6 +11,8 @@ "node": ">=6.0" }, "dependencies": { + "@types/sha.js": "^2.4.0", + "@types/uuid": "^7.0.3", "apollo-engine-reporting-protobuf": "file:../apollo-engine-reporting-protobuf", "apollo-graphql": "^0.4.0", "apollo-server-caching": "file:../apollo-server-caching", @@ -18,7 +20,9 @@ "apollo-server-errors": "file:../apollo-server-errors", "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "apollo-server-types": "file:../apollo-server-types", - "async-retry": "^1.2.1" + "async-retry": "^1.2.1", + "sha.js": "^2.4.11", + "uuid": "^8.0.0" }, "peerDependencies": { "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index c7e6a28d083..8528df0e5a8 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -1,21 +1,32 @@ import os from 'os'; import { gzip } from 'zlib'; -import { DocumentNode, GraphQLError } from 'graphql'; +import { + buildSchema, + DocumentNode, + GraphQLError, + GraphQLSchema, + lexicographicSortSchema, + printSchema, + stripIgnoredCharacters, +} from 'graphql'; import { ReportHeader, Trace, Report, - TracesAndStats + TracesAndStats, } from 'apollo-engine-reporting-protobuf'; import { fetch, RequestAgent, Response } from 'apollo-server-env'; import retry from 'async-retry'; import { plugin } from './plugin'; -import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types'; +import { GraphQLRequestContext, Logger } from 'apollo-server-types'; import { InMemoryLRUCache } from 'apollo-server-caching'; import { defaultEngineReportingSignature } from 'apollo-graphql'; -import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { ApolloServerPlugin } from 'apollo-server-plugin-base'; +import { reportingLoop, SchemaReporter } from './schemaReporter'; +import { v4 as uuidv4 } from 'uuid'; +import { sha256 } from 'sha.js'; let warnedOnDeprecatedApiKey = false; @@ -122,7 +133,7 @@ export interface EngineReportingOptions { /** * The URL of the Engine report ingress server. */ - endpointUrl?: string; + metricEndpointUrl?: string; /** * If set, prints all reports as JSON when they are sent. */ @@ -239,6 +250,31 @@ export interface EngineReportingOptions { */ generateClientInfo?: GenerateClientInfo; + /** + * Enable experimental schema reporting. Look at https://github.com/apollographql/apollo-schema-reporting-preview-docs for more information. + */ + experimental_schemaReporting?: 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 | GraphQLSchema; + + /** + * This is a function that can be used to generate an executable schema id. + * This is used to determine whether a schema should be sent to graph manager + */ + experimental_overrideSchemaIdGenerator?: ( + schema: string | GraphQLSchema, + ) => string; + + /** + * The URL to use for reporting schemas. + */ + schemaReportingUrl?: string; + /** * A logger interface to be used for output and errors. When not provided * it will default to the server's own `logger` implementation and use @@ -251,7 +287,7 @@ export interface AddTraceArgs { trace: Trace; operationName: string; queryHash: string; - schemaHash: SchemaHash; + schemaId: string; source?: string; document?: DocumentNode; } @@ -270,11 +306,10 @@ const serviceHeaderDefaults = { export class EngineReportingAgent { private readonly options: EngineReportingOptions; private readonly apiKey: string; - private logger: Logger = console; - private graphVariant: string; - private reports: { [schemaHash: string]: Report } = Object.create( - null, - ); + private readonly logger: Logger = console; + private readonly graphVariant: string; + + private reports: { [schemaHash: string]: Report } = Object.create(null); private reportSizes: { [schemaHash: string]: number } = Object.create(null); private reportTimer: any; // timer typing is weird and node-specific private readonly sendReportsImmediately?: boolean; @@ -286,11 +321,20 @@ export class EngineReportingAgent { private signalHandlers = new Map(); + private currentSchemaReporter?: SchemaReporter; + + private readonly bootId: string; + private readonly schemaIdGenerator: ( + schema: string | GraphQLSchema, + ) => string; + public constructor(options: EngineReportingOptions = {}) { this.options = options; this.apiKey = getEngineApiKey({engine: this.options, skipWarn: false, logger: this.logger}); if (options.logger) this.logger = options.logger; + this.bootId = uuidv4(); this.graphVariant = getEngineGraphVariant(options, this.logger) || ''; + if (!this.apiKey) { throw new Error( `To use EngineReportingAgent, you must specify an API key via the apiKey option or the APOLLO_KEY environment variable.`, @@ -325,12 +369,55 @@ export class EngineReportingAgent { }); } + if (this.options.experimental_overrideSchemaIdGenerator) { + this.schemaIdGenerator = this.options.experimental_overrideSchemaIdGenerator; + } else { + this.schemaIdGenerator = (schema: string | GraphQLSchema) => { + let graphQLSchema = + typeof schema === 'string' ? buildSchema(schema) : schema; + + return new sha256() + .update( + stripIgnoredCharacters( + printSchema(lexicographicSortSchema(graphQLSchema)), + ), + ) + .digest('hex'); + }; + } + // Handle the legacy options: privateVariables and privateHeaders handleLegacyOptions(this.options); } + private overrideSchema(): { + schemaDocument: string; + schemaId: string; + } | null { + if (!this.options.experimental_overrideReportedSchema) { + return null; + } + + let schemaId = this.schemaIdGenerator( + this.options.experimental_overrideReportedSchema, + ); + + let schemaDocument: string = + typeof this.options.experimental_overrideReportedSchema === 'string' + ? this.options.experimental_overrideReportedSchema + : printSchema(this.options.experimental_overrideReportedSchema); + + return { schemaDocument, schemaId }; + } + public newPlugin(): ApolloServerPlugin { - return plugin(this.options, this.addTrace.bind(this)); + return plugin( + this.options, + this.addTrace.bind(this), + this.startSchemaReporting.bind(this), + this.overrideSchema(), + this.schemaIdGenerator, + ); } public async addTrace({ @@ -339,23 +426,23 @@ export class EngineReportingAgent { document, operationName, source, - schemaHash, + schemaId, }: AddTraceArgs): Promise { // Ignore traces that come in after stop(). if (this.stopped) { return; } - if (!(schemaHash in this.reports)) { - this.reportHeaders[schemaHash] = new ReportHeader({ + if (!(schemaId in this.reports)) { + this.reportHeaders[schemaId] = new ReportHeader({ ...serviceHeaderDefaults, - schemaHash, + schemaId, schemaTag: this.graphVariant, }); // initializes this.reports[reportHash] - this.resetReport(schemaHash); + this.resetReport(schemaId); } - const report = this.reports[schemaHash]; + const report = this.reports[schemaId]; const protobufError = Trace.verify(trace); if (protobufError) { @@ -380,28 +467,26 @@ export class EngineReportingAgent { (report.tracesPerQuery[statsReportKey] as any).encodedTraces.push( encodedTrace, ); - this.reportSizes[schemaHash] += + this.reportSizes[schemaId] += encodedTrace.length + Buffer.byteLength(statsReportKey); // If the buffer gets big (according to our estimate), send. if ( this.sendReportsImmediately || - this.reportSizes[schemaHash] >= + this.reportSizes[schemaId] >= (this.options.maxUncompressedReportSize || 4 * 1024 * 1024) ) { - await this.sendReportAndReportErrors(schemaHash); + await this.sendReportAndReportErrors(schemaId); } } public async sendAllReports(): Promise { - await Promise.all( - Object.keys(this.reports).map(hash => this.sendReport(hash)), - ); + await Promise.all(Object.keys(this.reports).map(id => this.sendReport(id))); } - public async sendReport(schemaHash: string): Promise { - const report = this.reports[schemaHash]; - this.resetReport(schemaHash); + public async sendReport(schemaId: string): Promise { + const report = this.reports[schemaId]; + this.resetReport(schemaId); if (Object.keys(report.tracesPerQuery).length === 0) { return; @@ -450,8 +535,8 @@ export class EngineReportingAgent { }); const endpointUrl = - (this.options.endpointUrl || 'https://engine-report.apollodata.com') + - '/api/ingress/traces'; + (this.options.metricEndpointUrl || + 'https://engine-report.apollodata.com') + '/api/ingress/traces'; // Wrap fetch with async-retry for automatic retrying const response: Response = await retry( @@ -511,6 +596,51 @@ export class EngineReportingAgent { } } + public startSchemaReporting({ + executableSchemaId, + executableSchema, + }: { + executableSchemaId: string; + executableSchema: string; + }) { + if (this.currentSchemaReporter) { + this.currentSchemaReporter.stop(); + } + + const serverInfo = { + bootId: this.bootId, + graphVariant: this.graphVariant, + platform: process.env.APOLLO_SERVER_PLATFORM || 'local', + runtimeVersion: `node ${process.version}`, + executableSchemaId: executableSchemaId, + userVersion: process.env.APOLLO_SERVER_USER_VERSION, + serverId: + process.env.APOLLO_SERVER_ID || process.env.HOSTNAME || os.hostname(), + libraryVersion: `apollo-engine-reporting@${ + require('../package.json').version + }`, + }; + + // Jitter the startup between 0 and 5 seconds + const delay = Math.floor(Math.random() * 5000); + + const schemaReporter = new SchemaReporter( + serverInfo, + executableSchema, + this.apiKey, + this.options.schemaReportingUrl + ); + + const fallbackReportingDelayInMs = 20_000; + + this.currentSchemaReporter = schemaReporter; + const logger = this.logger; + + setTimeout(function() { + reportingLoop(schemaReporter, logger, false, fallbackReportingDelayInMs); + }, delay); + } + // Stop prevents reports from being sent automatically due to time or buffer // size, and stop buffering new traces. You may still manually send a last // report by calling sendReport(). @@ -525,6 +655,10 @@ export class EngineReportingAgent { this.reportTimer = undefined; } + if (this.currentSchemaReporter) { + this.currentSchemaReporter.stop(); + } + this.stopped = true; } @@ -598,11 +732,11 @@ export class EngineReportingAgent { }); } - private resetReport(schemaHash: string) { - this.reports[schemaHash] = new Report({ - header: this.reportHeaders[schemaHash], + private resetReport(schemaId: string) { + this.reports[schemaId] = new Report({ + header: this.reportHeaders[schemaId], }); - this.reportSizes[schemaHash] = 0; + this.reportSizes[schemaId] = 0; } } diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 84ead380e19..bf764e54a47 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -5,17 +5,18 @@ import { GraphQLRequestContextDidEncounterErrors, } from 'apollo-server-types'; import { Headers } from 'apollo-server-env'; +import { GraphQLSchema, printSchema } from 'graphql'; import { Trace } from 'apollo-engine-reporting-protobuf'; import { + AddTraceArgs, EngineReportingOptions, GenerateClientInfo, - AddTraceArgs, - VariableValueOptions, SendValuesBaseOptions, + VariableValueOptions, } from './agent'; import { EngineReportingTreeBuilder } from './treeBuilder'; -import { ApolloServerPlugin } from "apollo-server-plugin-base"; +import { ApolloServerPlugin } from 'apollo-server-plugin-base'; const clientNameHeaderKey = 'apollographql-client-name'; const clientReferenceIdHeaderKey = 'apollographql-client-reference-id'; @@ -30,18 +31,35 @@ const clientVersionHeaderKey = 'apollographql-client-version'; export const plugin = ( options: EngineReportingOptions = Object.create(null), addTrace: (args: AddTraceArgs) => Promise, - // schemaHash: string, + startSchemaReporting: ({ + executableSchema, + executableSchemaId, + }: { + executableSchema: string; + executableSchemaId: string; + }) => void, + overrideReportedSchema: { schemaDocument: string; schemaId: string } | null, + schemaIdGenerator: (schema: string | GraphQLSchema) => string, ): ApolloServerPlugin => { const logger: Logger = options.logger || console; const generateClientInfo: GenerateClientInfo = options.generateClientInfo || defaultGenerateClientInfo; - return { + serverWillStart: function({ schema }) { + if (options.experimental_schemaReporting) { + startSchemaReporting({ + executableSchema: + overrideReportedSchema?.schemaDocument || printSchema(schema), + executableSchemaId: + overrideReportedSchema?.schemaId || schemaIdGenerator(schema), + }); + } + }, requestDidStart({ logger: requestLogger, - schemaHash, metrics, + schema, request: { http, variables }, }) { const treeBuilder: EngineReportingTreeBuilder = new EngineReportingTreeBuilder( @@ -124,7 +142,8 @@ export const plugin = ( document: requestContext.document, source: requestContext.source, trace: treeBuilder.trace, - schemaHash, + schemaId: + overrideReportedSchema?.schemaId || schemaIdGenerator(schema), }); } @@ -190,7 +209,7 @@ export const plugin = ( didEnd(requestContext); }, }; - } + }, }; }; diff --git a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts new file mode 100644 index 00000000000..55fa0d19c32 --- /dev/null +++ b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts @@ -0,0 +1,57 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL mutation operation: AutoregReportServerInfo +// ==================================================== + +export interface ReportServerInfo_me_UserMutation { + __typename: 'UserMutation'; +} + +export interface ReportServerInfo_me_ServiceMutation_reportServerInfo { + __typename: 'ReportServerInfoResponse'; + inSeconds: number; + withExecutableSchema: boolean; +} + +export interface ReportServerInfo_me_ServiceMutation { + __typename: 'ServiceMutation'; + /** + * Schema auto-registration. Private alpha. + */ + reportServerInfo: ReportServerInfo_me_ServiceMutation_reportServerInfo | null; +} + +export type ReportServerInfo_me = + | ReportServerInfo_me_UserMutation + | ReportServerInfo_me_ServiceMutation; + +export interface AutoregReportServerInfo { + me: ReportServerInfo_me | null; +} + +export interface ReportServerInfoVariables { + info: EdgeServerInfo; + executableSchema?: string | null; +} + +/** + * Edge server info + */ +export interface EdgeServerInfo { + bootId: string; + executableSchemaId: string; + graphVariant: string; + libraryVersion?: string | null; + platform?: string | null; + runtimeVersion?: string | null; + serverId?: string | null; + userVersion?: string | null; +} + +//============================================================== +// END Enums and Input Objects +//============================================================== diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts new file mode 100644 index 00000000000..0ddae171baa --- /dev/null +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -0,0 +1,165 @@ +// Fields required for the trial related emails +import gql from 'graphql-tag'; +import { + AutoregReportServerInfo, + ReportServerInfoVariables, + EdgeServerInfo, +} from './reportingOperationTypes'; +import { DocumentNode, execute, makePromise, Observable } from 'apollo-link'; +import { FetchResult } from 'apollo-link/lib/types'; +import { HttpLink } from 'apollo-link-http'; +import { fetch } from 'apollo-server-env'; +import { Logger } from 'apollo-server-types'; + +const reportServerInfoGql = gql` + mutation ReportServerInfo($info: EdgeServerInfo!, $executableSchema: String) { + me { + __typename + ... on ServiceMutation { + reportServerInfo(info: $info, executableSchema: $executableSchema) { + inSeconds + withExecutableSchema + } + } + } + } +`; + +class ReportingError extends Error {} + +export function reportingLoop( + schemaReporter: SchemaReporter, + logger: Logger, + sendNextWithExecutableSchema: boolean, + fallbackReportingDelayInMs: number, +) { + function inner() { + // Bail out permanently + if (schemaReporter.stopped()) return; + + schemaReporter + .reportServerInfo(sendNextWithExecutableSchema) + .then(({ inSeconds, withExecutableSchema }) => { + sendNextWithExecutableSchema = withExecutableSchema; + setTimeout(inner, inSeconds * 1000); + }) + .catch((error: any) => { + // In the case of an error we want to continue looping + // We can add hardcoded backoff in the future, + // or on repeated failures stop responding reporting. + logger.warn(`Error in reportingServerInfo: ${error}`); + sendNextWithExecutableSchema = false; + setTimeout(inner, fallbackReportingDelayInMs); + }); + } + + inner(); +} + +interface ReportServerInfoReturnVal { + inSeconds: number; + withExecutableSchema: boolean; +} + +// This class is meant to be a thin shim around the gql mutations. +export class SchemaReporter { + private readonly apiKey: string; + private readonly graphManagerHttpLink: HttpLink; + + // These mirror the gql variables + private readonly serverInfo: EdgeServerInfo; + private readonly executableSchemaDocument: any; + private isStopped: boolean; + + constructor( + serverInfo: EdgeServerInfo, + schemaSdl: string, + apiKey: string, + reportingEndpoint: string | undefined, + ) { + if (apiKey === '') { + throw new Error('No api key defined'); + } + + this.apiKey = apiKey; + + this.graphManagerHttpLink = new HttpLink({ + uri: + reportingEndpoint || + 'https://engine-graphql.apollographql.com/api/graphql', + fetch, + headers: { 'x-api-key': this.apiKey }, + }); + + this.serverInfo = serverInfo; + this.executableSchemaDocument = schemaSdl; + this.isStopped = false; + } + + public stopped(): Boolean { + return this.isStopped; + } + + public stop() { + this.isStopped = true; + } + + public async reportServerInfo( + withExecutableSchema: boolean, + ): Promise { + const { data, errors } = await this.graphManagerQuery< + AutoregReportServerInfo + >(reportServerInfoGql, { + info: this.serverInfo, + executableSchema: withExecutableSchema + ? this.executableSchemaDocument + : null, + } as ReportServerInfoVariables); + + if (errors) { + throw new ReportingError((errors || []).map(x => x.message).join('\n')); + } + + if (!data || !data.me || !data.me.__typename) { + throw new ReportingError(` +Heartbeat response error. Received incomplete data from Apollo graph manager. +If this continues please reach out at support@apollographql.com. +Got response: "${JSON.stringify(data)}" + `); + } + + if (data.me.__typename == 'UserMutation') { + this.isStopped = true; + throw new ReportingError(` + User tokens cannot be used for schema reporting. Only service tokens. + `); + } else if (data.me.__typename == 'ServiceMutation') { + if (!data.me.reportServerInfo) { + throw new ReportingError(` +Heartbeat response error. Received incomplete data from Apollo graph manager. +If this continues please reach out at support@apollographql.com. +Got response: "${JSON.stringify(data)}" + `); + } + return data.me.reportServerInfo; + } else { + throw new ReportingError(` +Unexpected response. Received unexpected data from Apollo Graph Manager +If this continues please reach out at support@apollographql.com. +Got response: "${JSON.stringify(data)}" + `); + } + } + + private async graphManagerQuery< + Result = Record, + Variables = Record + >(query: DocumentNode, variables: Variables): Promise> { + return makePromise>( + execute(this.graphManagerHttpLink, { + query, + variables, + }) as Observable>, + ); + } +} From 764b1ce8f00a0f937123a36347e208dccf4c48b9 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Tue, 12 May 2020 16:49:28 -0400 Subject: [PATCH 02/30] Remove apollo link --- package-lock.json | 26 +++++++- packages/apollo-engine-reporting/src/agent.ts | 6 +- .../src/reportingOperationTypes.ts | 7 ++ .../src/schemaReporter.ts | 65 +++++++++---------- 4 files changed, 67 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index eff11b7b2ed..0ae772bea5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5382,6 +5382,14 @@ "@types/mime": "*" } }, + "@types/sha.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.0.tgz", + "integrity": "sha512-amxKgPy6WJTKuw8mpUwjX2BSxuBtBmZfRwIUDIuPJKNwGN8CWDli8JTg5ONTWOtcTkHIstvT7oAhhYXqEjStHQ==", + "requires": { + "@types/node": "*" + } + }, "@types/shot": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/shot/-/shot-4.0.0.tgz", @@ -5440,6 +5448,11 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==" + }, "@types/ws": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.4.tgz", @@ -5649,6 +5662,8 @@ "apollo-engine-reporting": { "version": "file:packages/apollo-engine-reporting", "requires": { + "@types/sha.js": "^2.4.0", + "@types/uuid": "^7.0.3", "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", "apollo-graphql": "^0.4.0", "apollo-server-caching": "file:packages/apollo-server-caching", @@ -5656,7 +5671,16 @@ "apollo-server-errors": "file:packages/apollo-server-errors", "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "apollo-server-types": "file:packages/apollo-server-types", - "async-retry": "^1.2.1" + "async-retry": "^1.2.1", + "sha.js": "^2.4.11", + "uuid": "^8.0.0" + }, + "dependencies": { + "uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==" + } } }, "apollo-engine-reporting-protobuf": { diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 8528df0e5a8..e05825b7890 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -133,7 +133,7 @@ export interface EngineReportingOptions { /** * The URL of the Engine report ingress server. */ - metricEndpointUrl?: string; + metricsEndpointUrl?: string; /** * If set, prints all reports as JSON when they are sent. */ @@ -535,7 +535,7 @@ export class EngineReportingAgent { }); const endpointUrl = - (this.options.metricEndpointUrl || + (this.options.metricsEndpointUrl || 'https://engine-report.apollodata.com') + '/api/ingress/traces'; // Wrap fetch with async-retry for automatic retrying @@ -628,7 +628,7 @@ export class EngineReportingAgent { serverInfo, executableSchema, this.apiKey, - this.options.schemaReportingUrl + this.options.schemaReportingUrl, ); const fallbackReportingDelayInMs = 20_000; diff --git a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts index 55fa0d19c32..68877ad39da 100644 --- a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts +++ b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts @@ -7,6 +7,8 @@ // GraphQL mutation operation: AutoregReportServerInfo // ==================================================== +import { GraphQLFormattedError } from "graphql"; + export interface ReportServerInfo_me_UserMutation { __typename: 'UserMutation'; } @@ -33,6 +35,11 @@ export interface AutoregReportServerInfo { me: ReportServerInfo_me | null; } +export interface AutoregReportServerInfoResult { + data?: AutoregReportServerInfo, + errors?: ReadonlyArray; +} + export interface ReportServerInfoVariables { info: EdgeServerInfo; executableSchema?: string | null; diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 0ddae171baa..edf91ed4ee0 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -1,17 +1,13 @@ // Fields required for the trial related emails -import gql from 'graphql-tag'; import { - AutoregReportServerInfo, ReportServerInfoVariables, EdgeServerInfo, + AutoregReportServerInfoResult, } from './reportingOperationTypes'; -import { DocumentNode, execute, makePromise, Observable } from 'apollo-link'; -import { FetchResult } from 'apollo-link/lib/types'; -import { HttpLink } from 'apollo-link-http'; -import { fetch } from 'apollo-server-env'; -import { Logger } from 'apollo-server-types'; +import { fetch, Headers, Request } from 'apollo-server-env'; +import { GraphQLRequest, Logger } from 'apollo-server-types'; -const reportServerInfoGql = gql` +const reportServerInfoGql = ` mutation ReportServerInfo($info: EdgeServerInfo!, $executableSchema: String) { me { __typename @@ -63,13 +59,13 @@ interface ReportServerInfoReturnVal { // This class is meant to be a thin shim around the gql mutations. export class SchemaReporter { - private readonly apiKey: string; - private readonly graphManagerHttpLink: HttpLink; - // These mirror the gql variables private readonly serverInfo: EdgeServerInfo; private readonly executableSchemaDocument: any; + private readonly url: string; + private isStopped: boolean; + private readonly headers: Headers; constructor( serverInfo: EdgeServerInfo, @@ -81,15 +77,13 @@ export class SchemaReporter { throw new Error('No api key defined'); } - this.apiKey = apiKey; + this.headers = new Headers(); + this.headers.set('Content-Type', 'application/json'); + this.headers.set('x-api-key', apiKey); - this.graphManagerHttpLink = new HttpLink({ - uri: - reportingEndpoint || - 'https://engine-graphql.apollographql.com/api/graphql', - fetch, - headers: { 'x-api-key': this.apiKey }, - }); + this.url = + reportingEndpoint || + 'https://engine-graphql.apollographql.com/api/graphql'; this.serverInfo = serverInfo; this.executableSchemaDocument = schemaSdl; @@ -107,9 +101,7 @@ export class SchemaReporter { public async reportServerInfo( withExecutableSchema: boolean, ): Promise { - const { data, errors } = await this.graphManagerQuery< - AutoregReportServerInfo - >(reportServerInfoGql, { + const { data, errors } = await this.graphManagerQuery({ info: this.serverInfo, executableSchema: withExecutableSchema ? this.executableSchemaDocument @@ -117,7 +109,9 @@ export class SchemaReporter { } as ReportServerInfoVariables); if (errors) { - throw new ReportingError((errors || []).map(x => x.message).join('\n')); + throw new ReportingError( + (errors || []).map((x: any) => x.message).join('\n'), + ); } if (!data || !data.me || !data.me.__typename) { @@ -151,15 +145,20 @@ Got response: "${JSON.stringify(data)}" } } - private async graphManagerQuery< - Result = Record, - Variables = Record - >(query: DocumentNode, variables: Variables): Promise> { - return makePromise>( - execute(this.graphManagerHttpLink, { - query, - variables, - }) as Observable>, - ); + private async graphManagerQuery( + variables: ReportServerInfoVariables, + ): Promise { + const request: GraphQLRequest = { + query: reportServerInfoGql, + operationName: 'ReportServerInfo', + variables: variables, + }; + const httpRequest = new Request(this.url, { + method: 'POST', + headers: this.headers, + body: JSON.stringify(request), + }); + const httpResponse = await fetch(httpRequest); + return httpResponse.json(); } } From 67c57bfbcfe8c2793c48f62c53fe106433e5774d Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Tue, 12 May 2020 13:42:28 -0400 Subject: [PATCH 03/30] Rename all schema hash and schema id to executable schema Id --- .../src/reports.proto | 2 +- packages/apollo-engine-reporting/package.json | 1 + packages/apollo-engine-reporting/src/agent.ts | 71 ++++++++++--------- .../apollo-engine-reporting/src/plugin.ts | 18 +++-- 4 files changed, 52 insertions(+), 40 deletions(-) diff --git a/packages/apollo-engine-reporting-protobuf/src/reports.proto b/packages/apollo-engine-reporting-protobuf/src/reports.proto index 7e01601f957..2aac12da898 100644 --- a/packages/apollo-engine-reporting-protobuf/src/reports.proto +++ b/packages/apollo-engine-reporting-protobuf/src/reports.proto @@ -273,7 +273,7 @@ message ReportHeader { string deprecated_schema_hash = 11; // An id that is used to represent the schema to Apollo Graph Manager - string schema_id = 12; + string executable_schema_id = 12; reserved 3; // removed string service = 3; } diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index 3128894f355..b8796c71547 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -15,6 +15,7 @@ "@types/uuid": "^7.0.3", "apollo-engine-reporting-protobuf": "file:../apollo-engine-reporting-protobuf", "apollo-graphql": "^0.4.0", + "apollo-link": "^1.2.14", "apollo-server-caching": "file:../apollo-server-caching", "apollo-server-env": "file:../apollo-server-env", "apollo-server-errors": "file:../apollo-server-errors", diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index e05825b7890..deb373c7dc1 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -266,7 +266,7 @@ export interface EngineReportingOptions { * This is a function that can be used to generate an executable schema id. * This is used to determine whether a schema should be sent to graph manager */ - experimental_overrideSchemaIdGenerator?: ( + experimental_overrideExecutableSchemaIdGenerator?: ( schema: string | GraphQLSchema, ) => string; @@ -287,7 +287,7 @@ export interface AddTraceArgs { trace: Trace; operationName: string; queryHash: string; - schemaId: string; + executableSchemaId: string; source?: string; document?: DocumentNode; } @@ -309,14 +309,19 @@ export class EngineReportingAgent { private readonly logger: Logger = console; private readonly graphVariant: string; - private reports: { [schemaHash: string]: Report } = Object.create(null); - private reportSizes: { [schemaHash: string]: number } = Object.create(null); + private reports: { [executableSchemaId: string]: Report } = Object.create( + null, + ); + private reportSizes: { [executableSchemaId: string]: number } = Object.create( + null, + ); + private reportTimer: any; // timer typing is weird and node-specific private readonly sendReportsImmediately?: boolean; private stopped: boolean = false; - private reportHeaders: { [schemaHash: string]: ReportHeader } = Object.create( - null, - ); + private reportHeaders: { + [executableSchemaId: string]: ReportHeader; + } = Object.create(null); private signatureCache: InMemoryLRUCache; private signalHandlers = new Map(); @@ -324,7 +329,7 @@ export class EngineReportingAgent { private currentSchemaReporter?: SchemaReporter; private readonly bootId: string; - private readonly schemaIdGenerator: ( + private readonly executableSchemaIdGenerator: ( schema: string | GraphQLSchema, ) => string; @@ -369,10 +374,10 @@ export class EngineReportingAgent { }); } - if (this.options.experimental_overrideSchemaIdGenerator) { - this.schemaIdGenerator = this.options.experimental_overrideSchemaIdGenerator; + if (this.options.experimental_overrideExecutableSchemaIdGenerator) { + this.executableSchemaIdGenerator = this.options.experimental_overrideExecutableSchemaIdGenerator; } else { - this.schemaIdGenerator = (schema: string | GraphQLSchema) => { + this.executableSchemaIdGenerator = (schema: string | GraphQLSchema) => { let graphQLSchema = typeof schema === 'string' ? buildSchema(schema) : schema; @@ -391,23 +396,23 @@ export class EngineReportingAgent { } private overrideSchema(): { - schemaDocument: string; - schemaId: string; + executableSchemaDocument: string; + executableSchemaId: string; } | null { if (!this.options.experimental_overrideReportedSchema) { return null; } - let schemaId = this.schemaIdGenerator( + let executableSchemaId = this.executableSchemaIdGenerator( this.options.experimental_overrideReportedSchema, ); - let schemaDocument: string = + let executableSchemaDocument: string = typeof this.options.experimental_overrideReportedSchema === 'string' ? this.options.experimental_overrideReportedSchema : printSchema(this.options.experimental_overrideReportedSchema); - return { schemaDocument, schemaId }; + return { executableSchemaDocument, executableSchemaId }; } public newPlugin(): ApolloServerPlugin { @@ -416,7 +421,7 @@ export class EngineReportingAgent { this.addTrace.bind(this), this.startSchemaReporting.bind(this), this.overrideSchema(), - this.schemaIdGenerator, + this.executableSchemaIdGenerator, ); } @@ -426,23 +431,23 @@ export class EngineReportingAgent { document, operationName, source, - schemaId, + executableSchemaId, }: AddTraceArgs): Promise { // Ignore traces that come in after stop(). if (this.stopped) { return; } - if (!(schemaId in this.reports)) { - this.reportHeaders[schemaId] = new ReportHeader({ + if (!(executableSchemaId in this.reports)) { + this.reportHeaders[executableSchemaId] = new ReportHeader({ ...serviceHeaderDefaults, - schemaId, + executableSchemaId: executableSchemaId, schemaTag: this.graphVariant, }); // initializes this.reports[reportHash] - this.resetReport(schemaId); + this.resetReport(executableSchemaId); } - const report = this.reports[schemaId]; + const report = this.reports[executableSchemaId]; const protobufError = Trace.verify(trace); if (protobufError) { @@ -467,16 +472,16 @@ export class EngineReportingAgent { (report.tracesPerQuery[statsReportKey] as any).encodedTraces.push( encodedTrace, ); - this.reportSizes[schemaId] += + this.reportSizes[executableSchemaId] += encodedTrace.length + Buffer.byteLength(statsReportKey); // If the buffer gets big (according to our estimate), send. if ( this.sendReportsImmediately || - this.reportSizes[schemaId] >= + this.reportSizes[executableSchemaId] >= (this.options.maxUncompressedReportSize || 4 * 1024 * 1024) ) { - await this.sendReportAndReportErrors(schemaId); + await this.sendReportAndReportErrors(executableSchemaId); } } @@ -484,9 +489,9 @@ export class EngineReportingAgent { await Promise.all(Object.keys(this.reports).map(id => this.sendReport(id))); } - public async sendReport(schemaId: string): Promise { - const report = this.reports[schemaId]; - this.resetReport(schemaId); + public async sendReport(executableSchemaId: string): Promise { + const report = this.reports[executableSchemaId]; + this.resetReport(executableSchemaId); if (Object.keys(report.tracesPerQuery).length === 0) { return; @@ -713,14 +718,14 @@ export class EngineReportingAgent { private async sendAllReportsAndReportErrors(): Promise { await Promise.all( - Object.keys(this.reports).map(schemaHash => - this.sendReportAndReportErrors(schemaHash), + Object.keys(this.reports).map(executableSchemaId => + this.sendReportAndReportErrors(executableSchemaId), ), ); } - private sendReportAndReportErrors(schemaHash: string): Promise { - return this.sendReport(schemaHash).catch(err => { + private sendReportAndReportErrors(executableSchemaId: string): Promise { + 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 bf764e54a47..5859a922dd9 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -38,8 +38,11 @@ export const plugin = ( executableSchema: string; executableSchemaId: string; }) => void, - overrideReportedSchema: { schemaDocument: string; schemaId: string } | null, - schemaIdGenerator: (schema: string | GraphQLSchema) => string, + overrideReportedSchema: { + executableSchemaDocument: string; + executableSchemaId: string; + } | null, + executableSchemaIdGenerator: (schema: string | GraphQLSchema) => string, ): ApolloServerPlugin => { const logger: Logger = options.logger || console; const generateClientInfo: GenerateClientInfo = @@ -50,9 +53,11 @@ export const plugin = ( if (options.experimental_schemaReporting) { startSchemaReporting({ executableSchema: - overrideReportedSchema?.schemaDocument || printSchema(schema), + overrideReportedSchema?.executableSchemaDocument || + printSchema(schema), executableSchemaId: - overrideReportedSchema?.schemaId || schemaIdGenerator(schema), + overrideReportedSchema?.executableSchemaId || + executableSchemaIdGenerator(schema), }); } }, @@ -142,8 +147,9 @@ export const plugin = ( document: requestContext.document, source: requestContext.source, trace: treeBuilder.trace, - schemaId: - overrideReportedSchema?.schemaId || schemaIdGenerator(schema), + executableSchemaId: + overrideReportedSchema?.executableSchemaId || + executableSchemaIdGenerator(schema), }); } From de1523bbe8a58cff5cb41001013d24e615a44a8a Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Wed, 13 May 2020 17:49:33 -0400 Subject: [PATCH 04/30] Remove override executable schema id generator --- packages/apollo-engine-reporting/src/agent.ts | 61 ++++--------------- .../apollo-engine-reporting/src/plugin.ts | 12 +--- 2 files changed, 14 insertions(+), 59 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index deb373c7dc1..60e55005f25 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -1,7 +1,6 @@ import os from 'os'; import { gzip } from 'zlib'; import { - buildSchema, DocumentNode, GraphQLError, GraphQLSchema, @@ -27,6 +26,7 @@ import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { sha256 } from 'sha.js'; +import { isString } from "util"; let warnedOnDeprecatedApiKey = false; @@ -260,15 +260,7 @@ export interface EngineReportingOptions { * 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 | GraphQLSchema; - - /** - * This is a function that can be used to generate an executable schema id. - * This is used to determine whether a schema should be sent to graph manager - */ - experimental_overrideExecutableSchemaIdGenerator?: ( - schema: string | GraphQLSchema, - ) => string; + experimental_overrideReportedSchema?: string; /** * The URL to use for reporting schemas. @@ -327,11 +319,7 @@ export class EngineReportingAgent { private signalHandlers = new Map(); private currentSchemaReporter?: SchemaReporter; - private readonly bootId: string; - private readonly executableSchemaIdGenerator: ( - schema: string | GraphQLSchema, - ) => string; public constructor(options: EngineReportingOptions = {}) { this.options = options; @@ -374,45 +362,19 @@ export class EngineReportingAgent { }); } - if (this.options.experimental_overrideExecutableSchemaIdGenerator) { - this.executableSchemaIdGenerator = this.options.experimental_overrideExecutableSchemaIdGenerator; - } else { - this.executableSchemaIdGenerator = (schema: string | GraphQLSchema) => { - let graphQLSchema = - typeof schema === 'string' ? buildSchema(schema) : schema; - - return new sha256() - .update( - stripIgnoredCharacters( - printSchema(lexicographicSortSchema(graphQLSchema)), - ), - ) - .digest('hex'); - }; - } - // Handle the legacy options: privateVariables and privateHeaders handleLegacyOptions(this.options); } - private overrideSchema(): { - executableSchemaDocument: string; - executableSchemaId: string; - } | null { - if (!this.options.experimental_overrideReportedSchema) { - return null; - } - - let executableSchemaId = this.executableSchemaIdGenerator( - this.options.experimental_overrideReportedSchema, - ); - - let executableSchemaDocument: string = - typeof this.options.experimental_overrideReportedSchema === 'string' - ? this.options.experimental_overrideReportedSchema - : printSchema(this.options.experimental_overrideReportedSchema); + public executableSchemaIdGenerator(schema: string | GraphQLSchema) { + // TODO caching in the agent so we don't do expensive operations on each request. + let schemaDocument = isString(schema) ? schema : + stripIgnoredCharacters( + printSchema(lexicographicSortSchema(schema)) + ); - return { executableSchemaDocument, executableSchemaId }; + return new sha256().update(schemaDocument) + .digest("hex"); } public newPlugin(): ApolloServerPlugin { @@ -420,8 +382,7 @@ export class EngineReportingAgent { this.options, this.addTrace.bind(this), this.startSchemaReporting.bind(this), - this.overrideSchema(), - this.executableSchemaIdGenerator, + this.executableSchemaIdGenerator.bind(this) ); } diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 5859a922dd9..0fd442efd80 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -38,10 +38,6 @@ export const plugin = ( executableSchema: string; executableSchemaId: string; }) => void, - overrideReportedSchema: { - executableSchemaDocument: string; - executableSchemaId: string; - } | null, executableSchemaIdGenerator: (schema: string | GraphQLSchema) => string, ): ApolloServerPlugin => { const logger: Logger = options.logger || console; @@ -53,11 +49,10 @@ export const plugin = ( if (options.experimental_schemaReporting) { startSchemaReporting({ executableSchema: - overrideReportedSchema?.executableSchemaDocument || + options.experimental_overrideReportedSchema || printSchema(schema), executableSchemaId: - overrideReportedSchema?.executableSchemaId || - executableSchemaIdGenerator(schema), + executableSchemaIdGenerator(options.experimental_overrideReportedSchema || schema) }); } }, @@ -148,8 +143,7 @@ export const plugin = ( source: requestContext.source, trace: treeBuilder.trace, executableSchemaId: - overrideReportedSchema?.executableSchemaId || - executableSchemaIdGenerator(schema), + executableSchemaIdGenerator(options.experimental_overrideReportedSchema || schema), }); } From 0413c2085f6932d21adca03e14ff2af76c2e01e0 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 11:18:52 -0400 Subject: [PATCH 05/30] Add and fix tests --- .../src/__tests__/plugin.test.ts | 179 +++++++++++++++++- packages/apollo-engine-reporting/src/agent.ts | 4 +- .../src/utils/pluginTestHarness.ts | 12 +- .../src/ApolloServer.ts | 4 +- 4 files changed, 187 insertions(+), 12 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 2a8d3313b12..494885e76c2 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -1,14 +1,22 @@ import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; -import { graphql, GraphQLError } from 'graphql'; +import { + graphql, + GraphQLError, + GraphQLSchema, + lexicographicSortSchema, + printSchema, + stripIgnoredCharacters, +} from 'graphql'; import { Request } from 'node-fetch'; import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin'; import { Headers } from 'apollo-server-env'; import { AddTraceArgs } from '../agent'; import { Trace } from 'apollo-engine-reporting-protobuf'; import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; +import { sha256 } from 'sha.js'; +import { isString } from 'util'; -it('trace construction', async () => { - const typeDefs = ` +const typeDefs = ` type User { id: Int name: String @@ -31,7 +39,7 @@ it('trace construction', async () => { } `; - const query = ` +const query = ` query q { author(id: 5) { name @@ -43,6 +51,154 @@ it('trace construction', async () => { } `; +describe('schema reporting', () => { + const schema = makeExecutableSchema({ typeDefs }); + addMockFunctionsToSchema({ schema }); + + const addTrace = jest.fn(); + const startSchemaReporting = jest.fn(); + const executableSchemaIdGenerator = jest.fn( + (schema: string | GraphQLSchema) => { + let schemaDocument = isString(schema) + ? schema + : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); + + return new sha256().update(schemaDocument).digest('hex'); + }, + ); + + beforeEach(() => { + addTrace.mockClear(); + startSchemaReporting.mockClear(); + executableSchemaIdGenerator.mockClear(); + }); + + it('starts reporing if enabled', async () => { + const pluginInstance = plugin( + { + experimental_schemaReporting: true, + }, + addTrace, + startSchemaReporting, + executableSchemaIdGenerator, + ); + + 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(startSchemaReporting).toBeCalledTimes(1); + expect(startSchemaReporting).toBeCalledWith({ + executableSchema: printSchema(schema), + executableSchemaId: executableSchemaIdGenerator(schema), + }); + }); + + it('uses the override schema', async () => { + const pluginInstance = plugin( + { + experimental_schemaReporting: true, + experimental_overrideReportedSchema: typeDefs, + }, + addTrace, + startSchemaReporting, + executableSchemaIdGenerator, + ); + + 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, + }); + }, + }); + + const expectedExecutableSchemaId = executableSchemaIdGenerator(typeDefs); + expect(startSchemaReporting).toBeCalledTimes(1); + expect(startSchemaReporting).toBeCalledWith({ + executableSchema: typeDefs, + executableSchemaId: expectedExecutableSchemaId, + }); + + // Get the first argument from the first time this is called. + // Not using called with because that has to be exhaustive and this isn't + // testing trace generation + expect(addTrace.mock.calls[0][0].executableSchemaId).toBe( + expectedExecutableSchemaId, + ); + }); + + it('uses the same executable schema id for metric reporting', async () => { + const pluginInstance = plugin( + { + experimental_schemaReporting: true, + }, + addTrace, + startSchemaReporting, + executableSchemaIdGenerator, + ); + + 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, + }); + }, + }); + + const expectedExecutableSchemaId = executableSchemaIdGenerator(schema); + expect(startSchemaReporting).toBeCalledTimes(1); + expect(startSchemaReporting).toBeCalledWith({ + executableSchema: printSchema(schema), + executableSchemaId: expectedExecutableSchemaId, + }); + // Get the first argument from the first time this is called. + // Not using called with because that has to be exhaustive and this isn't + // testing trace generation + expect(addTrace.mock.calls[0][0].executableSchemaId).toBe( + expectedExecutableSchemaId, + ); + }); +}); + +it('trace construction', async () => { const schema = makeExecutableSchema({ typeDefs }); addMockFunctionsToSchema({ schema }); @@ -50,10 +206,19 @@ it('trace construction', async () => { async function addTrace(args: AddTraceArgs) { traces.push(args); } + const startSchemaReporting = jest.fn(); + const executableSchemaIdGenerator = jest.fn(); - const pluginInstance = plugin({ /* no options!*/ }, addTrace); + const pluginInstance = plugin( + { + /* no options!*/ + }, + addTrace, + startSchemaReporting, + executableSchemaIdGenerator, + ); - pluginTestHarness({ + await pluginTestHarness({ pluginInstance, schema, graphqlRequest: { @@ -260,7 +425,7 @@ describe('variableJson output for sendVariableValues transform: custom function ).toEqual(JSON.stringify(null)); }); - const errorThrowingModifier = (input: { + const errorThrowingModifier = (_input: { variables: Record; }): Record => { throw new GraphQLError('testing error handling'); diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 60e55005f25..50c37764841 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -500,7 +500,7 @@ export class EngineReportingAgent { }); }); - const endpointUrl = + const metricsEndpointUrl = (this.options.metricsEndpointUrl || 'https://engine-report.apollodata.com') + '/api/ingress/traces'; @@ -509,7 +509,7 @@ export class EngineReportingAgent { // Retry on network errors and 5xx HTTP // responses. async () => { - const curResponse = await fetch(endpointUrl, { + const curResponse = await fetch(metricsEndpointUrl, { method: 'POST', headers: { 'user-agent': 'apollo-engine-reporting', diff --git a/packages/apollo-server-core/src/utils/pluginTestHarness.ts b/packages/apollo-server-core/src/utils/pluginTestHarness.ts index 5c790c70f6c..86dd63460c9 100644 --- a/packages/apollo-server-core/src/utils/pluginTestHarness.ts +++ b/packages/apollo-server-core/src/utils/pluginTestHarness.ts @@ -81,7 +81,6 @@ export default async function pluginTestHarness({ */ context?: TContext; }): Promise> { - if (!schema) { schema = new GraphQLSchema({ query: new GraphQLObjectType({ @@ -98,6 +97,17 @@ export default async function pluginTestHarness({ }); } + const schemaHash = generateSchemaHash(schema); + if (typeof pluginInstance.serverWillStart === 'function') { + pluginInstance.serverWillStart({ + logger: logger || console, + schema, + schemaHash, + engine: {}, + }); + } + + const requestContext: GraphQLRequestContext = { logger: logger || console, schema, diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 521c52bcabd..4259f720048 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -852,7 +852,7 @@ export function testApolloServer( public engineOptions(): Partial> { return { - endpointUrl: this.getUrl(), + metricsEndpointUrl: this.getUrl(), }; } @@ -2170,7 +2170,7 @@ export function testApolloServer( resolvers: { Query: { something: () => 'hello' } }, engine: { apiKey: 'service:my-app:secret', - endpointUrl: fakeEngineUrl, + metricsEndpointUrl: fakeEngineUrl, reportIntervalMs: 1, maxAttempts: 3, requestAgent, From 75d9f9402399e02f72b9ece680137a2e2eb3d8e4 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 12:20:16 -0400 Subject: [PATCH 06/30] Add simple caching --- packages/apollo-engine-reporting/src/agent.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 50c37764841..6f4a233b0b6 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -320,6 +320,7 @@ export class EngineReportingAgent { private currentSchemaReporter?: SchemaReporter; private readonly bootId: string; + private executableSchemaToIdMap = new Map(); public constructor(options: EngineReportingOptions = {}) { this.options = options; @@ -367,14 +368,19 @@ export class EngineReportingAgent { } public executableSchemaIdGenerator(schema: string | GraphQLSchema) { - // TODO caching in the agent so we don't do expensive operations on each request. + // TODO: At somepoint fix this caching for managed federation so we treat it as an LRU and gc old schemas. + const cachedId = this.executableSchemaToIdMap.get(schema); + if (cachedId) { + return cachedId; + } let schemaDocument = isString(schema) ? schema : stripIgnoredCharacters( printSchema(lexicographicSortSchema(schema)) ); - return new sha256().update(schemaDocument) - .digest("hex"); + const id = new sha256().update(schemaDocument).digest("hex"); + this.executableSchemaToIdMap.set(schema, id); + return id; } public newPlugin(): ApolloServerPlugin { From 1ef5ec5b80196f1cfa99e35017909eea5d2a9515 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 12:32:05 -0400 Subject: [PATCH 07/30] Add documentation --- packages/apollo-engine-reporting/src/agent.ts | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 6f4a233b0b6..ab59e3e8271 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -26,7 +26,7 @@ import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { sha256 } from 'sha.js'; -import { isString } from "util"; +import { isString } from 'util'; let warnedOnDeprecatedApiKey = false; @@ -251,7 +251,9 @@ export interface EngineReportingOptions { generateClientInfo?: GenerateClientInfo; /** - * Enable experimental schema reporting. Look at https://github.com/apollographql/apollo-schema-reporting-preview-docs for more information. + * Enable experimental apollo schema reporting, letting the server report its schema to the Apollo registry. + * This will start a thread within the apollo agent that will periodically report server info and a schema potentially + * Look at https://github.com/apollographql/apollo-schema-reporting-preview-docs for more information about configuration and use cases. */ experimental_schemaReporting?: boolean; @@ -320,7 +322,7 @@ export class EngineReportingAgent { private currentSchemaReporter?: SchemaReporter; private readonly bootId: string; - private executableSchemaToIdMap = new Map(); + private executableSchemaToIdMap = new Map(); public constructor(options: EngineReportingOptions = {}) { this.options = options; @@ -373,12 +375,11 @@ export class EngineReportingAgent { if (cachedId) { return cachedId; } - let schemaDocument = isString(schema) ? schema : - stripIgnoredCharacters( - printSchema(lexicographicSortSchema(schema)) - ); + let schemaDocument = isString(schema) + ? schema + : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); - const id = new sha256().update(schemaDocument).digest("hex"); + const id = new sha256().update(schemaDocument).digest('hex'); this.executableSchemaToIdMap.set(schema, id); return id; } @@ -388,7 +389,7 @@ export class EngineReportingAgent { this.options, this.addTrace.bind(this), this.startSchemaReporting.bind(this), - this.executableSchemaIdGenerator.bind(this) + this.executableSchemaIdGenerator.bind(this), ); } @@ -582,10 +583,15 @@ export class EngineReportingAgent { const serverInfo = { bootId: this.bootId, graphVariant: this.graphVariant, + // The infra environment in which this edge server is running, e.g. localhost, Kubernetes + // Length must be <= 256 characters. platform: process.env.APOLLO_SERVER_PLATFORM || 'local', runtimeVersion: `node ${process.version}`, executableSchemaId: executableSchemaId, + // An identifier used to distinguish the version of the server code such as git or docker sha. + // Length must be <= 256 charecters userVersion: process.env.APOLLO_SERVER_USER_VERSION, + // "An identifier for the server instance. Length must be <= 256 characters. serverId: process.env.APOLLO_SERVER_ID || process.env.HOSTNAME || os.hostname(), libraryVersion: `apollo-engine-reporting@${ From 4b00bf28324b7469680827b891190c2d53ba1b62 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 12:54:00 -0400 Subject: [PATCH 08/30] Add schema id generator tests --- .../src/__tests__/agent.test.ts | 58 ++++++++++++++++++- .../src/__tests__/plugin.test.ts | 12 +--- packages/apollo-engine-reporting/src/agent.ts | 12 ++-- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts index ff6ac43ab6a..31250818b2f 100644 --- a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts @@ -1,8 +1,9 @@ import { signatureCacheKey, handleLegacyOptions, - EngineReportingOptions, -} from '../agent'; + EngineReportingOptions, computeExecutableSchemaId +} from "../agent"; +import { buildSchema } from "graphql"; describe('signature cache key', () => { it('generates without the operationName', () => { @@ -16,6 +17,59 @@ describe('signature cache key', () => { }); }); +describe('Executable Schema Id', () => { + const unsortedGQLSchemaDocument = ` + directive @example on FIELD + union AccountOrUser = Account | User + type Query { + userOrAccount(name: String, id: String): AccountOrUser + } + + type User { + accounts: [Account!] + email: String + name: String! + } + + type Account { + name: String! + id: ID! + } + `; + + const sortedGQLSchemaDocument = ` + directive @example on FIELD + union AccountOrUser = Account | User + + type Account { + name: String! + id: ID! + } + + type Query { + userOrAccount(id: String, name: String): AccountOrUser + } + + type User { + accounts: [Account!] + email: String + name: String! + } + + `; + it('to be normalized for graphql schemas', () => { + expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).toEqual( + computeExecutableSchemaId(buildSchema(sortedGQLSchemaDocument)) + ); + }); + it('to not be normalized for strings', () => { + expect(computeExecutableSchemaId(unsortedGQLSchemaDocument)).not.toEqual( + computeExecutableSchemaId(sortedGQLSchemaDocument) + ); + }); +}); + + 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 = { diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 494885e76c2..52e7861785e 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -10,7 +10,7 @@ import { import { Request } from 'node-fetch'; import { makeTraceDetails, makeHTTPRequestHeaders, plugin } from '../plugin'; import { Headers } from 'apollo-server-env'; -import { AddTraceArgs } from '../agent'; +import { AddTraceArgs, computeExecutableSchemaId } from "../agent"; import { Trace } from 'apollo-engine-reporting-protobuf'; import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; import { sha256 } from 'sha.js'; @@ -57,15 +57,7 @@ describe('schema reporting', () => { const addTrace = jest.fn(); const startSchemaReporting = jest.fn(); - const executableSchemaIdGenerator = jest.fn( - (schema: string | GraphQLSchema) => { - let schemaDocument = isString(schema) - ? schema - : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); - - return new sha256().update(schemaDocument).digest('hex'); - }, - ); + const executableSchemaIdGenerator = jest.fn(computeExecutableSchemaId); beforeEach(() => { addTrace.mockClear(); diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index ab59e3e8271..244cac15da0 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -375,11 +375,7 @@ export class EngineReportingAgent { if (cachedId) { return cachedId; } - let schemaDocument = isString(schema) - ? schema - : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); - - const id = new sha256().update(schemaDocument).digest('hex'); + const id = computeExecutableSchemaId(schema); this.executableSchemaToIdMap.set(schema, id); return id; } @@ -825,3 +821,9 @@ function makeSendValuesBaseOptionsFromLegacy( ? { none: true } : { all: true }; } + +export function computeExecutableSchemaId(schema: string | GraphQLSchema): string { + let schemaDocument = isString(schema) ? schema + : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); + return new sha256().update(schemaDocument).digest('hex'); +} From 968e94b90d9763da78c18cb8d9448759c1b3ec31 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 17:09:03 -0400 Subject: [PATCH 09/30] Increase delay time --- packages/apollo-engine-reporting/src/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 244cac15da0..79e81b1cff6 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -596,7 +596,7 @@ export class EngineReportingAgent { }; // Jitter the startup between 0 and 5 seconds - const delay = Math.floor(Math.random() * 5000); + const delay = Math.floor(Math.random() * 10_000); const schemaReporter = new SchemaReporter( serverInfo, From 6af84671885578fc7b5f3b90bfee9ef3d9a6b796 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 17:56:47 -0400 Subject: [PATCH 10/30] Fix equality --- packages/apollo-engine-reporting/src/schemaReporter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index edf91ed4ee0..c9e7d0a4f1a 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -122,12 +122,12 @@ Got response: "${JSON.stringify(data)}" `); } - if (data.me.__typename == 'UserMutation') { + if (data.me.__typename === 'UserMutation') { this.isStopped = true; throw new ReportingError(` User tokens cannot be used for schema reporting. Only service tokens. `); - } else if (data.me.__typename == 'ServiceMutation') { + } else if (data.me.__typename === 'ServiceMutation') { if (!data.me.reportServerInfo) { throw new ReportingError(` Heartbeat response error. Received incomplete data from Apollo graph manager. From f094ebe00db6aaf42369fb5749c0347c6b1fe24f Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Thu, 14 May 2020 23:47:19 -0400 Subject: [PATCH 11/30] Address comments --- .../src/reports.proto | 7 +++---- .../src/__tests__/plugin.test.ts | 2 +- packages/apollo-engine-reporting/src/agent.ts | 20 ++++++++++++------- .../src/schemaReporter.ts | 15 +++++++------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/apollo-engine-reporting-protobuf/src/reports.proto b/packages/apollo-engine-reporting-protobuf/src/reports.proto index 2aac12da898..0c6bc319d78 100644 --- a/packages/apollo-engine-reporting-protobuf/src/reports.proto +++ b/packages/apollo-engine-reporting-protobuf/src/reports.proto @@ -269,11 +269,10 @@ message ReportHeader { string uname = 9; // eg "current", "prod" string schema_tag = 10; - // The hex representation of the sha512 of the introspection response - string deprecated_schema_hash = 11; - // An id that is used to represent the schema to Apollo Graph Manager - string executable_schema_id = 12; + // Using this in place of what used to be schema_hash, since that is no longer + // attached to a schema in the backend. + string executable_schema_id = 11; reserved 3; // removed string service = 3; } diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 52e7861785e..e473e3e096b 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -65,7 +65,7 @@ describe('schema reporting', () => { executableSchemaIdGenerator.mockClear(); }); - it('starts reporing if enabled', async () => { + it('starts reporting if enabled', async () => { const pluginInstance = plugin( { experimental_schemaReporting: true, diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 79e81b1cff6..a23f975e6bd 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -322,7 +322,10 @@ export class EngineReportingAgent { private currentSchemaReporter?: SchemaReporter; private readonly bootId: string; - private executableSchemaToIdMap = new Map(); + private lastSeenExecutableSchemaToId?: { + executabeSchema: string | GraphQLSchema, + executableSchemaId: string, + } public constructor(options: EngineReportingOptions = {}) { this.options = options; @@ -370,13 +373,16 @@ export class EngineReportingAgent { } public executableSchemaIdGenerator(schema: string | GraphQLSchema) { - // TODO: At somepoint fix this caching for managed federation so we treat it as an LRU and gc old schemas. - const cachedId = this.executableSchemaToIdMap.get(schema); - if (cachedId) { - return cachedId; + if (this.lastSeenExecutableSchemaToId?.executabeSchema === schema) { + return this.lastSeenExecutableSchemaToId.executableSchemaId } const id = computeExecutableSchemaId(schema); - this.executableSchemaToIdMap.set(schema, id); + + this.lastSeenExecutableSchemaToId = { + executabeSchema: schema, + executableSchemaId: id, + } + return id; } @@ -595,7 +601,7 @@ export class EngineReportingAgent { }`, }; - // Jitter the startup between 0 and 5 seconds + // Jitter the startup between 0 and 10 seconds const delay = Math.floor(Math.random() * 10_000); const schemaReporter = new SchemaReporter( diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index c9e7d0a4f1a..74acb6d2fac 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -71,18 +71,17 @@ export class SchemaReporter { serverInfo: EdgeServerInfo, schemaSdl: string, apiKey: string, - reportingEndpoint: string | undefined, + schemaReportingEndpoint: string | undefined, ) { - if (apiKey === '') { - throw new Error('No api key defined'); - } this.headers = new Headers(); this.headers.set('Content-Type', 'application/json'); this.headers.set('x-api-key', apiKey); + this.headers.set('apollographql-client-name', 'apollo-server'); + this.headers.set('apollographql-client-version', require('../package.json').version); this.url = - reportingEndpoint || + schemaReportingEndpoint || 'https://engine-graphql.apollographql.com/api/graphql'; this.serverInfo = serverInfo; @@ -116,7 +115,7 @@ export class SchemaReporter { if (!data || !data.me || !data.me.__typename) { throw new ReportingError(` -Heartbeat response error. Received incomplete data from Apollo graph manager. +Heartbeat response error. Received incomplete data from Apollo Graph Manager. If this continues please reach out at support@apollographql.com. Got response: "${JSON.stringify(data)}" `); @@ -126,11 +125,13 @@ Got response: "${JSON.stringify(data)}" this.isStopped = true; throw new ReportingError(` User tokens cannot be used for schema reporting. Only service tokens. + Please go to https://engine.apollographql.com/graph/$INSERT_YOUR_GRAPH/settings + to get a token `); } else if (data.me.__typename === 'ServiceMutation') { if (!data.me.reportServerInfo) { throw new ReportingError(` -Heartbeat response error. Received incomplete data from Apollo graph manager. +Heartbeat response error. Received incomplete data from Apollo Graph Manager. If this continues please reach out at support@apollographql.com. Got response: "${JSON.stringify(data)}" `); From 465ebe54a1e1b1ab06abe9e0d49747c3487476c9 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Sun, 17 May 2020 19:06:28 -0400 Subject: [PATCH 12/30] Address comments --- package-lock.json | 14 +-- package.json | 1 + packages/apollo-engine-reporting/package.json | 4 - .../src/__tests__/agent.test.ts | 7 +- .../src/__tests__/plugin.test.ts | 19 ++-- packages/apollo-engine-reporting/src/agent.ts | 100 +++++++++++++----- .../apollo-engine-reporting/src/plugin.ts | 41 +++---- .../src/schemaReporter.ts | 35 +++--- 8 files changed, 125 insertions(+), 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0ae772bea5e..4b30e2797ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5382,14 +5382,6 @@ "@types/mime": "*" } }, - "@types/sha.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.0.tgz", - "integrity": "sha512-amxKgPy6WJTKuw8mpUwjX2BSxuBtBmZfRwIUDIuPJKNwGN8CWDli8JTg5ONTWOtcTkHIstvT7oAhhYXqEjStHQ==", - "requires": { - "@types/node": "*" - } - }, "@types/shot": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/shot/-/shot-4.0.0.tgz", @@ -5451,7 +5443,8 @@ "@types/uuid": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==" + "integrity": "sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw==", + "dev": true }, "@types/ws": { "version": "7.2.4", @@ -5662,8 +5655,6 @@ "apollo-engine-reporting": { "version": "file:packages/apollo-engine-reporting", "requires": { - "@types/sha.js": "^2.4.0", - "@types/uuid": "^7.0.3", "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", "apollo-graphql": "^0.4.0", "apollo-server-caching": "file:packages/apollo-server-caching", @@ -5672,7 +5663,6 @@ "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", "apollo-server-types": "file:packages/apollo-server-types", "async-retry": "^1.2.1", - "sha.js": "^2.4.11", "uuid": "^8.0.0" }, "dependencies": { diff --git a/package.json b/package.json index 30d30708265..114019cf3ba 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@types/supertest": "^2.0.8", "@types/test-listen": "1.1.0", "@types/type-is": "1.6.3", + "@types/uuid": "^7.0.3", "@types/ws": "7.2.4", "apollo-fetch": "0.7.0", "apollo-link": "1.2.14", diff --git a/packages/apollo-engine-reporting/package.json b/packages/apollo-engine-reporting/package.json index b8796c71547..9ce287bb57f 100644 --- a/packages/apollo-engine-reporting/package.json +++ b/packages/apollo-engine-reporting/package.json @@ -11,18 +11,14 @@ "node": ">=6.0" }, "dependencies": { - "@types/sha.js": "^2.4.0", - "@types/uuid": "^7.0.3", "apollo-engine-reporting-protobuf": "file:../apollo-engine-reporting-protobuf", "apollo-graphql": "^0.4.0", - "apollo-link": "^1.2.14", "apollo-server-caching": "file:../apollo-server-caching", "apollo-server-env": "file:../apollo-server-env", "apollo-server-errors": "file:../apollo-server-errors", "apollo-server-plugin-base": "file:../apollo-server-plugin-base", "apollo-server-types": "file:../apollo-server-types", "async-retry": "^1.2.1", - "sha.js": "^2.4.11", "uuid": "^8.0.0" }, "peerDependencies": { diff --git a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts index 31250818b2f..24429461b8e 100644 --- a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts @@ -1,7 +1,8 @@ import { signatureCacheKey, handleLegacyOptions, - EngineReportingOptions, computeExecutableSchemaId + EngineReportingOptions, + computeExecutableSchemaId } from "../agent"; import { buildSchema } from "graphql"; @@ -57,12 +58,12 @@ describe('Executable Schema Id', () => { } `; - it('to be normalized for graphql schemas', () => { + it('does normalize GraphQL schemas', () => { expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).toEqual( computeExecutableSchemaId(buildSchema(sortedGQLSchemaDocument)) ); }); - it('to not be normalized for strings', () => { + it('does not normalize strings', () => { expect(computeExecutableSchemaId(unsortedGQLSchemaDocument)).not.toEqual( computeExecutableSchemaId(sortedGQLSchemaDocument) ); diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index e473e3e096b..825bfe9f4f6 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -1,20 +1,11 @@ import { makeExecutableSchema, addMockFunctionsToSchema } from 'graphql-tools'; -import { - graphql, - GraphQLError, - GraphQLSchema, - lexicographicSortSchema, - printSchema, - stripIgnoredCharacters, -} from 'graphql'; +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 { AddTraceArgs, computeExecutableSchemaId } from '../agent'; import { Trace } from 'apollo-engine-reporting-protobuf'; import pluginTestHarness from 'apollo-server-core/dist/utils/pluginTestHarness'; -import { sha256 } from 'sha.js'; -import { isString } from 'util'; const typeDefs = ` type User { @@ -141,8 +132,10 @@ describe('schema reporting', () => { // Get the first argument from the first time this is called. // Not using called with because that has to be exhaustive and this isn't // testing trace generation - expect(addTrace.mock.calls[0][0].executableSchemaId).toBe( - expectedExecutableSchemaId, + expect(addTrace).toBeCalledWith( + expect.objectContaining({ + executableSchemaId: expectedExecutableSchemaId, + }), ); }); diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index a23f975e6bd..85c0e6e1d66 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -6,8 +6,8 @@ import { GraphQLSchema, lexicographicSortSchema, printSchema, - stripIgnoredCharacters, -} from 'graphql'; + stripIgnoredCharacters +} from "graphql"; import { ReportHeader, Trace, @@ -25,9 +25,10 @@ import { defaultEngineReportingSignature } from 'apollo-graphql'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; -import { sha256 } from 'sha.js'; import { isString } from 'util'; +const sha256 = module.require('crypto').createHash('sha256'); + let warnedOnDeprecatedApiKey = false; export interface ClientInfo { @@ -131,9 +132,14 @@ export interface EngineReportingOptions { */ maxUncompressedReportSize?: number; /** + * [DEPRECATED] this option was replaced by tracesEndpointUrl * The URL of the Engine report ingress server. */ - metricsEndpointUrl?: string; + endpointUrl?: string; + /** + * The URL of the Engine report ingress server. This was previously known as endpointUrl + */ + tracesEndpointUrl?: string; /** * If set, prints all reports as JSON when they are sent. */ @@ -251,9 +257,17 @@ export interface EngineReportingOptions { generateClientInfo?: GenerateClientInfo; /** - * Enable experimental apollo schema reporting, letting the server report its schema to the Apollo registry. - * This will start a thread within the apollo agent that will periodically report server info and a schema potentially - * Look at https://github.com/apollographql/apollo-schema-reporting-preview-docs for more information about configuration and use cases. + * **(Experimental)** Enable schema reporting from this server with + * Apollo Graph Manager. + * + * The use of this option avoids the need to rgister 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. + * + * See [our _preview + * documentation](https://github.com/apollographql/apollo-schema-reporting-preview-docs) + * for more information. */ experimental_schemaReporting?: boolean; @@ -264,6 +278,20 @@ export interface EngineReportingOptions { */ experimental_overrideReportedSchema?: string; + /** + * The schema reporter will wait before starting reporting. + * By default, the report will wait some random amount of time between 0 and 10 seconds. + * A longer interval leads to more staggered starts which means it is less likely + * multiple servers to get asked to upload a schema. + * + * If this server runs in lambda or in other constrained environments it would be useful + * to decrease the schema reporting max wait time to be less than default. + * + * This number will be the max for the range in ms that the schema reporter will + * wait before starting to report. + */ + experimental_schemaReportingInitialDelayMaxMs?: number; + /** * The URL to use for reporting schemas. */ @@ -323,9 +351,11 @@ export class EngineReportingAgent { private currentSchemaReporter?: SchemaReporter; private readonly bootId: string; private lastSeenExecutableSchemaToId?: { - executabeSchema: string | GraphQLSchema, - executableSchemaId: string, - } + executableSchema: string | GraphQLSchema; + executableSchemaId: string; + }; + + private readonly tracesEndpointUrl: string; public constructor(options: EngineReportingOptions = {}) { this.options = options; @@ -368,31 +398,41 @@ export class EngineReportingAgent { }); } + if (this.options.endpointUrl) { + this.logger.warn( + 'The endpointUrl option for engine has been deprecated. Please use tracesEndpointUrl instead', + ); + } + this.tracesEndpointUrl = + (this.options.endpointUrl || + this.options.tracesEndpointUrl || + 'https://engine-report.apollodata.com') + '/api/ingress/traces'; + // Handle the legacy options: privateVariables and privateHeaders handleLegacyOptions(this.options); } - public executableSchemaIdGenerator(schema: string | GraphQLSchema) { - if (this.lastSeenExecutableSchemaToId?.executabeSchema === schema) { - return this.lastSeenExecutableSchemaToId.executableSchemaId + private executableSchemaIdGenerator(schema: string | GraphQLSchema) { + if (this.lastSeenExecutableSchemaToId?.executableSchema === schema) { + return this.lastSeenExecutableSchemaToId.executableSchemaId; } const id = computeExecutableSchemaId(schema); + // We override this variable every time we get a new schema so we cache + // the last seen value. It mostly a cached pair. this.lastSeenExecutableSchemaToId = { - executabeSchema: schema, + executableSchema: schema, executableSchemaId: id, - } + }; return id; } public newPlugin(): ApolloServerPlugin { - return plugin( - this.options, - this.addTrace.bind(this), - this.startSchemaReporting.bind(this), - this.executableSchemaIdGenerator.bind(this), - ); + return plugin(this.options, this.addTrace.bind(this), { + startSchemaReporting: this.startSchemaReporting.bind(this), + executableSchemaIdGenerator: this.executableSchemaIdGenerator.bind(this), + }); } public async addTrace({ @@ -509,16 +549,12 @@ export class EngineReportingAgent { }); }); - const metricsEndpointUrl = - (this.options.metricsEndpointUrl || - 'https://engine-report.apollodata.com') + '/api/ingress/traces'; - // Wrap fetch with async-retry for automatic retrying const response: Response = await retry( // Retry on network errors and 5xx HTTP // responses. async () => { - const curResponse = await fetch(metricsEndpointUrl, { + const curResponse = await fetch(this.tracesEndpointUrl, { method: 'POST', headers: { 'user-agent': 'apollo-engine-reporting', @@ -602,7 +638,10 @@ export class EngineReportingAgent { }; // Jitter the startup between 0 and 10 seconds - const delay = Math.floor(Math.random() * 10_000); + const delay = Math.floor( + Math.random() * + (this.options.experimental_schemaReportingInitialDelayMaxMs || 10_000), + ); const schemaReporter = new SchemaReporter( serverInfo, @@ -828,8 +867,11 @@ function makeSendValuesBaseOptionsFromLegacy( : { all: true }; } -export function computeExecutableSchemaId(schema: string | GraphQLSchema): string { - let schemaDocument = isString(schema) ? schema +export function computeExecutableSchemaId( + schema: string | GraphQLSchema, +): string { + let schemaDocument = isString(schema) + ? schema : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); return new sha256().update(schemaDocument).digest('hex'); } diff --git a/packages/apollo-engine-reporting/src/plugin.ts b/packages/apollo-engine-reporting/src/plugin.ts index 0fd442efd80..4b8217813f6 100644 --- a/packages/apollo-engine-reporting/src/plugin.ts +++ b/packages/apollo-engine-reporting/src/plugin.ts @@ -31,14 +31,19 @@ const clientVersionHeaderKey = 'apollographql-client-version'; export const plugin = ( options: EngineReportingOptions = Object.create(null), addTrace: (args: AddTraceArgs) => Promise, - startSchemaReporting: ({ - executableSchema, - executableSchemaId, + { + startSchemaReporting, + executableSchemaIdGenerator, }: { - executableSchema: string; - executableSchemaId: string; - }) => void, - executableSchemaIdGenerator: (schema: string | GraphQLSchema) => string, + startSchemaReporting: ({ + executableSchema, + executableSchemaId, + }: { + executableSchema: string; + executableSchemaId: string; + }) => void; + executableSchemaIdGenerator: (schema: string | GraphQLSchema) => string; + }, ): ApolloServerPlugin => { const logger: Logger = options.logger || console; const generateClientInfo: GenerateClientInfo = @@ -46,15 +51,14 @@ export const plugin = ( return { serverWillStart: function({ schema }) { - if (options.experimental_schemaReporting) { - startSchemaReporting({ - executableSchema: - options.experimental_overrideReportedSchema || - printSchema(schema), - executableSchemaId: - executableSchemaIdGenerator(options.experimental_overrideReportedSchema || schema) - }); - } + if (!options.experimental_schemaReporting) return; + startSchemaReporting({ + executableSchema: + options.experimental_overrideReportedSchema || printSchema(schema), + executableSchemaId: executableSchemaIdGenerator( + options.experimental_overrideReportedSchema || schema, + ), + }); }, requestDidStart({ logger: requestLogger, @@ -142,8 +146,9 @@ export const plugin = ( document: requestContext.document, source: requestContext.source, trace: treeBuilder.trace, - executableSchemaId: - executableSchemaIdGenerator(options.experimental_overrideReportedSchema || schema), + executableSchemaId: executableSchemaIdGenerator( + options.experimental_overrideReportedSchema || schema, + ), }); } diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 74acb6d2fac..8ef3b48bc9c 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -1,4 +1,3 @@ -// Fields required for the trial related emails import { ReportServerInfoVariables, EdgeServerInfo, @@ -21,8 +20,6 @@ const reportServerInfoGql = ` } `; -class ReportingError extends Error {} - export function reportingLoop( schemaReporter: SchemaReporter, logger: Logger, @@ -33,6 +30,9 @@ export function reportingLoop( // Bail out permanently if (schemaReporter.stopped()) return; + // Not awaiting this. The callback is handled in the `then` and it calls inner() + // to report the server info in however many seconds we were told to wait from + // Apollo Graph Manager schemaReporter .reportServerInfo(sendNextWithExecutableSchema) .then(({ inSeconds, withExecutableSchema }) => { @@ -43,7 +43,9 @@ export function reportingLoop( // In the case of an error we want to continue looping // We can add hardcoded backoff in the future, // or on repeated failures stop responding reporting. - logger.warn(`Error in reportingServerInfo: ${error}`); + logger.error( + `Error in reporting server info to Apollo Graph Manager for schema reporting: ${error}`, + ); sendNextWithExecutableSchema = false; setTimeout(inner, fallbackReportingDelayInMs); }); @@ -73,12 +75,14 @@ export class SchemaReporter { apiKey: string, schemaReportingEndpoint: string | undefined, ) { - this.headers = new Headers(); this.headers.set('Content-Type', 'application/json'); this.headers.set('x-api-key', apiKey); - this.headers.set('apollographql-client-name', 'apollo-server'); - this.headers.set('apollographql-client-version', require('../package.json').version); + this.headers.set('apollographql-client-name', 'apollo-engine-reporting'); + this.headers.set( + 'apollographql-client-version', + require('../package.json').version, + ); this.url = schemaReportingEndpoint || @@ -108,13 +112,11 @@ export class SchemaReporter { } as ReportServerInfoVariables); if (errors) { - throw new ReportingError( - (errors || []).map((x: any) => x.message).join('\n'), - ); + throw new Error((errors || []).map((x: any) => x.message).join('\n')); } if (!data || !data.me || !data.me.__typename) { - throw new ReportingError(` + throw new Error(` Heartbeat response error. Received incomplete data from Apollo Graph Manager. If this continues please reach out at support@apollographql.com. Got response: "${JSON.stringify(data)}" @@ -123,14 +125,13 @@ Got response: "${JSON.stringify(data)}" if (data.me.__typename === 'UserMutation') { this.isStopped = true; - throw new ReportingError(` - User tokens cannot be used for schema reporting. Only service tokens. - Please go to https://engine.apollographql.com/graph/$INSERT_YOUR_GRAPH/settings - to get a token + throw new Error(` + This server was configured with an API key for a user. Only a service's API key may be used for schema reporting. + Please visit the settings for this graph at https://engine.apollographql.com/ to obtain an API key for a service. `); } else if (data.me.__typename === 'ServiceMutation') { if (!data.me.reportServerInfo) { - throw new ReportingError(` + throw new Error(` Heartbeat response error. Received incomplete data from Apollo Graph Manager. If this continues please reach out at support@apollographql.com. Got response: "${JSON.stringify(data)}" @@ -138,7 +139,7 @@ Got response: "${JSON.stringify(data)}" } return data.me.reportServerInfo; } else { - throw new ReportingError(` + throw new Error(` Unexpected response. Received unexpected data from Apollo Graph Manager If this continues please reach out at support@apollographql.com. Got response: "${JSON.stringify(data)}" From 6ac44dad5f1dd366711b138787d2edd43635ad31 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Sun, 17 May 2020 19:18:05 -0400 Subject: [PATCH 13/30] Cleanup --- packages/apollo-engine-reporting/src/agent.ts | 14 +++++++------- .../src/ApolloServer.ts | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 85c0e6e1d66..80176339d1c 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -279,10 +279,10 @@ export interface EngineReportingOptions { experimental_overrideReportedSchema?: string; /** - * The schema reporter will wait before starting reporting. - * By default, the report will wait some random amount of time between 0 and 10 seconds. + * The schema reporter waits before starting reporting. + * By default, the report waits some random amount of time between 0 and 10 seconds. * A longer interval leads to more staggered starts which means it is less likely - * multiple servers to get asked to upload a schema. + * multiple servers will get asked to upload the same schema. * * If this server runs in lambda or in other constrained environments it would be useful * to decrease the schema reporting max wait time to be less than default. @@ -751,11 +751,11 @@ export class EngineReportingAgent { }); } - private resetReport(schemaId: string) { - this.reports[schemaId] = new Report({ - header: this.reportHeaders[schemaId], + private resetReport(executableSchemaId: string) { + this.reports[executableSchemaId] = new Report({ + header: this.reportHeaders[executableSchemaId], }); - this.reportSizes[schemaId] = 0; + this.reportSizes[executableSchemaId] = 0; } } diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 4259f720048..85257054458 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -852,7 +852,7 @@ export function testApolloServer( public engineOptions(): Partial> { return { - metricsEndpointUrl: this.getUrl(), + tracesEndpointUrl: this.getUrl(), }; } @@ -2170,7 +2170,7 @@ export function testApolloServer( resolvers: { Query: { something: () => 'hello' } }, engine: { apiKey: 'service:my-app:secret', - metricsEndpointUrl: fakeEngineUrl, + tracesEndpointUrl: fakeEngineUrl, reportIntervalMs: 1, maxAttempts: 3, requestAgent, From 15fd0e6ebf1351292403e58b329f916e98c678c2 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Sun, 17 May 2020 19:33:43 -0400 Subject: [PATCH 14/30] Fix sha256 test --- packages/apollo-engine-reporting/src/agent.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 80176339d1c..c8fe60c68e4 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -27,7 +27,6 @@ import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { isString } from 'util'; -const sha256 = module.require('crypto').createHash('sha256'); let warnedOnDeprecatedApiKey = false; @@ -870,8 +869,10 @@ function makeSendValuesBaseOptionsFromLegacy( export function computeExecutableSchemaId( schema: string | GraphQLSchema, ): string { + // Can't call digest on this object twice. Creating new object each function call + const sha256 = module.require('crypto').createHash('sha256'); let schemaDocument = isString(schema) ? schema : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); - return new sha256().update(schemaDocument).digest('hex'); + return sha256.update(schemaDocument).digest('hex'); } From d9f0d8e16965a9bbaeed2ffd6de823003a715f51 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Sun, 17 May 2020 19:39:28 -0400 Subject: [PATCH 15/30] Fix plugin tests --- .../src/__tests__/plugin.test.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 825bfe9f4f6..223352326dc 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -62,8 +62,10 @@ describe('schema reporting', () => { experimental_schemaReporting: true, }, addTrace, - startSchemaReporting, - executableSchemaIdGenerator, + { + startSchemaReporting, + executableSchemaIdGenerator, + } ); await pluginTestHarness({ @@ -99,8 +101,10 @@ describe('schema reporting', () => { experimental_overrideReportedSchema: typeDefs, }, addTrace, - startSchemaReporting, - executableSchemaIdGenerator, + { + startSchemaReporting, + executableSchemaIdGenerator, + } ); await pluginTestHarness({ @@ -145,8 +149,10 @@ describe('schema reporting', () => { experimental_schemaReporting: true, }, addTrace, - startSchemaReporting, - executableSchemaIdGenerator, + { + startSchemaReporting, + executableSchemaIdGenerator, + } ); await pluginTestHarness({ @@ -199,8 +205,10 @@ it('trace construction', async () => { /* no options!*/ }, addTrace, - startSchemaReporting, - executableSchemaIdGenerator, + { + startSchemaReporting, + executableSchemaIdGenerator, + } ); await pluginTestHarness({ From 977a89e58b2e48e2c248acb6ced517cfbd76be81 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Sun, 17 May 2020 22:29:52 -0400 Subject: [PATCH 16/30] Prettier --- .../src/__tests__/plugin.test.ts | 10 +++++----- packages/apollo-engine-reporting/src/agent.ts | 5 ++--- .../src/reportingOperationTypes.ts | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts index 223352326dc..56c2809a3db 100644 --- a/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/plugin.test.ts @@ -65,7 +65,7 @@ describe('schema reporting', () => { { startSchemaReporting, executableSchemaIdGenerator, - } + }, ); await pluginTestHarness({ @@ -104,7 +104,7 @@ describe('schema reporting', () => { { startSchemaReporting, executableSchemaIdGenerator, - } + }, ); await pluginTestHarness({ @@ -152,7 +152,7 @@ describe('schema reporting', () => { { startSchemaReporting, executableSchemaIdGenerator, - } + }, ); await pluginTestHarness({ @@ -208,7 +208,7 @@ it('trace construction', async () => { { startSchemaReporting, executableSchemaIdGenerator, - } + }, ); await pluginTestHarness({ @@ -222,7 +222,7 @@ it('trace construction', async () => { }, http: new Request('http://localhost:123/foo'), }, - executor: async ({ request: { query: source }}) => { + executor: async ({ request: { query: source } }) => { return await graphql({ schema, source, diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index c8fe60c68e4..70c8f2d313d 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -6,8 +6,8 @@ import { GraphQLSchema, lexicographicSortSchema, printSchema, - stripIgnoredCharacters -} from "graphql"; + stripIgnoredCharacters, +} from 'graphql'; import { ReportHeader, Trace, @@ -27,7 +27,6 @@ import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; import { isString } from 'util'; - let warnedOnDeprecatedApiKey = false; export interface ClientInfo { diff --git a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts index 68877ad39da..4b0b4812c34 100644 --- a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts +++ b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts @@ -7,7 +7,7 @@ // GraphQL mutation operation: AutoregReportServerInfo // ==================================================== -import { GraphQLFormattedError } from "graphql"; +import { GraphQLFormattedError } from 'graphql'; export interface ReportServerInfo_me_UserMutation { __typename: 'UserMutation'; @@ -36,7 +36,7 @@ export interface AutoregReportServerInfo { } export interface AutoregReportServerInfoResult { - data?: AutoregReportServerInfo, + data?: AutoregReportServerInfo; errors?: ReadonlyArray; } From f9751fe703e74fc4c37c55d56ee5322c81b5ad04 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Mon, 18 May 2020 08:17:44 -0400 Subject: [PATCH 17/30] Parially address comments --- packages/apollo-engine-reporting/src/agent.ts | 10 +-- .../src/reportingOperationTypes.ts | 6 +- .../src/schemaReporter.ts | 61 ++++++++++++------- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 70c8f2d313d..8a7e20b0281 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -25,7 +25,6 @@ import { defaultEngineReportingSignature } from 'apollo-graphql'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; -import { isString } from 'util'; let warnedOnDeprecatedApiKey = false; @@ -398,7 +397,7 @@ export class EngineReportingAgent { if (this.options.endpointUrl) { this.logger.warn( - 'The endpointUrl option for engine has been deprecated. Please use tracesEndpointUrl instead', + '[deprecated] The `endpointUrl` option within `engine` has been renamed to `tracesEndpointUrl`.', ); } this.tracesEndpointUrl = @@ -870,8 +869,9 @@ export function computeExecutableSchemaId( ): string { // Can't call digest on this object twice. Creating new object each function call const sha256 = module.require('crypto').createHash('sha256'); - let schemaDocument = isString(schema) - ? schema - : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); + const schemaDocument = + typeof schema === 'string' + ? schema + : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); return sha256.update(schemaDocument).digest('hex'); } diff --git a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts index 4b0b4812c34..1bf7b182d51 100644 --- a/packages/apollo-engine-reporting/src/reportingOperationTypes.ts +++ b/packages/apollo-engine-reporting/src/reportingOperationTypes.ts @@ -31,12 +31,12 @@ export type ReportServerInfo_me = | ReportServerInfo_me_UserMutation | ReportServerInfo_me_ServiceMutation; -export interface AutoregReportServerInfo { +export interface SchemaReportingServerInfo { me: ReportServerInfo_me | null; } -export interface AutoregReportServerInfoResult { - data?: AutoregReportServerInfo; +export interface SchemaReportingServerInfoResult { + data?: SchemaReportingServerInfo; errors?: ReadonlyArray; } diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 8ef3b48bc9c..3be0d30a140 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -1,7 +1,7 @@ import { ReportServerInfoVariables, EdgeServerInfo, - AutoregReportServerInfoResult, + SchemaReportingServerInfoResult, } from './reportingOperationTypes'; import { fetch, Headers, Request } from 'apollo-server-env'; import { GraphQLRequest, Logger } from 'apollo-server-types'; @@ -109,47 +109,62 @@ export class SchemaReporter { executableSchema: withExecutableSchema ? this.executableSchemaDocument : null, - } as ReportServerInfoVariables); + }); if (errors) { throw new Error((errors || []).map((x: any) => x.message).join('\n')); } if (!data || !data.me || !data.me.__typename) { - throw new Error(` -Heartbeat response error. Received incomplete data from Apollo Graph Manager. -If this continues please reach out at support@apollographql.com. -Got response: "${JSON.stringify(data)}" - `); + throw new Error( + [ + 'Unexpected response shape from Apollo Graph Manager when', + 'reporting server information for schema reporting. If', + 'this continues, please reach out to support@apollographql.com.', + 'Received response:', + JSON.stringify(data), + ].join(' '), + ); } - if (data.me.__typename === 'UserMutation') { this.isStopped = true; - throw new Error(` - This server was configured with an API key for a user. Only a service's API key may be used for schema reporting. - Please visit the settings for this graph at https://engine.apollographql.com/ to obtain an API key for a service. - `); + throw new Error( + [ + 'This server was configured with an API key for a user.', + "Only a service's API key may be used for schema reporting.", + 'Please visit the settings for this graph at', + '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(` -Heartbeat response error. Received incomplete data from Apollo Graph Manager. -If this continues please reach out at support@apollographql.com. -Got response: "${JSON.stringify(data)}" - `); + throw new Error( + [ + 'Unexpected response shape from Apollo Graph Manager when', + 'reporting server information during schema reporting. If', + 'this continues, please reach out to support@apollographql.com.', + 'Received response:', + JSON.stringify(data), + ].join(' '), + ); } return data.me.reportServerInfo; } else { - throw new Error(` -Unexpected response. Received unexpected data from Apollo Graph Manager -If this continues please reach out at support@apollographql.com. -Got response: "${JSON.stringify(data)}" - `); + throw new Error( + [ + 'Unexpected response shape from Apollo Graph Manager when', + 'reporting server information during schema reporting. If', + 'this continues, please reach out to support@apollographql.com.', + 'Received response:', + JSON.stringify(data), + ].join(' '), + ); } } private async graphManagerQuery( variables: ReportServerInfoVariables, - ): Promise { + ): Promise { const request: GraphQLRequest = { query: reportServerInfoGql, operationName: 'ReportServerInfo', From 2551f932110743a6818f18fc674ff596517b55fe Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Mon, 18 May 2020 08:34:50 -0400 Subject: [PATCH 18/30] Add error handling around http --- .../src/schemaReporter.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 3be0d30a140..85c51c3c9e1 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -175,7 +175,24 @@ export class SchemaReporter { headers: this.headers, body: JSON.stringify(request), }); - const httpResponse = await fetch(httpRequest); - return httpResponse.json(); + try { + const httpResponse = await fetch(httpRequest); + if (!httpResponse.ok) { + throw new Error("Failed to get a 200 response from graph manager"); + } + + const json = await httpResponse.json(); + return json; + } catch (error) { + throw new Error( + [ + 'Unexpected http error from Apollo Graph Manager when reporting server info.', + 'Did not receive a valid json response from Graph Manager', + 'If this continues to happen please reach out to support@apollographql.com', + 'Error:', + error + ].join(' '), + ); + } } } From 490a02e1ee859758397483ace5e98ed9363f4344 Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Mon, 18 May 2020 09:47:52 -0400 Subject: [PATCH 19/30] Adds test --- .../src/__tests__/schemaReporter.test.ts | 162 ++++++++++++++++++ .../src/schemaReporter.ts | 2 +- 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts diff --git a/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts new file mode 100644 index 00000000000..b623a97c43d --- /dev/null +++ b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts @@ -0,0 +1,162 @@ +import nock from 'nock'; +import { reportServerInfoGql, SchemaReporter } from '../schemaReporter'; + +function mockReporterRequest(url: any, variables?: any) { + if (variables) + return nock(url).post( + '/', + JSON.stringify({ + query: reportServerInfoGql, + operationName: 'ReportServerInfo', + variables, + }), + ); + return nock(url).post('/'); +} + +beforeEach(() => { + if (!nock.isActive()) nock.activate(); +}); + +afterEach(() => { + expect(nock.isDone()).toBeTruthy(); + nock.cleanAll(); + nock.restore(); +}); + +const serverInfo = { + bootId: 'string', + executableSchemaId: 'string', + graphVariant: 'string', +}; + +const url = 'http://localhost:4000'; + +describe('Schema reporter', () => { + it('return correct values if no errors', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + mockReporterRequest(url).reply(200, { + data: { + me: { + __typename: 'ServiceMutation', + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 30, + withExecutableSchema: false, + }, + }, + }, + }); + + let { + inSeconds, + withExecutableSchema, + } = await schemaReporter.reportServerInfo(false); + expect(inSeconds).toBe(30); + expect(withExecutableSchema).toBe(false); + + mockReporterRequest(url).reply(200, { + data: { + me: { + __typename: 'ServiceMutation', + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 60, + withExecutableSchema: true, + }, + }, + }, + }); + ({ + inSeconds, + withExecutableSchema, + } = await schemaReporter.reportServerInfo(false)); + expect(inSeconds).toBe(60); + expect(withExecutableSchema).toBe(true); + }); + + it('throws on 500 response', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + mockReporterRequest(url).reply(500, { + data: { + me: { + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 30, + withExecutableSchema: false, + }, + }, + }, + }); + + await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); + }); + + it('throws on 200 malformed response', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + mockReporterRequest(url).reply(200, { + data: { + me: { + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + }, + }, + }, + }); + + await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); + + mockReporterRequest(url).reply(200, { + data: { + me: { + __typename: 'UserMutation', + }, + }, + }); + await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); + }); + + it('sends schema if withExecutableSchema is true.', async () => { + const schemaReporter = new SchemaReporter( + serverInfo, + 'schemaSdl', + 'apiKey', + url, + ); + + const variables = { + info: serverInfo, + executableSchema: 'schemaSdl' + } + {} + mockReporterRequest(url, variables).reply(200, { + data: { + me: { + __typename: 'ServiceMutation', + reportServerInfo: { + __typename: 'ReportServerInfoResponse', + inSeconds: 30, + withExecutableSchema: false, + }, + }, + }, + }); + + await schemaReporter.reportServerInfo(true); + }); +}); diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 85c51c3c9e1..596e96d7a55 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -6,7 +6,7 @@ import { import { fetch, Headers, Request } from 'apollo-server-env'; import { GraphQLRequest, Logger } from 'apollo-server-types'; -const reportServerInfoGql = ` +export const reportServerInfoGql = ` mutation ReportServerInfo($info: EdgeServerInfo!, $executableSchema: String) { me { __typename From ce2c8bf9118aca1224884555fab6c11a689249ff Mon Sep 17 00:00:00 2001 From: Joshua Segaran Date: Mon, 18 May 2020 15:30:51 -0400 Subject: [PATCH 20/30] Don't normalize GraphQL Schema in apollo-engine-reporting --- packages/apollo-engine-reporting/src/__tests__/agent.test.ts | 4 ++-- packages/apollo-engine-reporting/src/agent.ts | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts index 24429461b8e..565cc5e50d5 100644 --- a/packages/apollo-engine-reporting/src/__tests__/agent.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/agent.test.ts @@ -58,8 +58,8 @@ describe('Executable Schema Id', () => { } `; - it('does normalize GraphQL schemas', () => { - expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).toEqual( + it('does not normalize GraphQL schemas', () => { + expect(computeExecutableSchemaId(buildSchema(unsortedGQLSchemaDocument))).not.toEqual( computeExecutableSchemaId(buildSchema(sortedGQLSchemaDocument)) ); }); diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 8a7e20b0281..d4dac3dd57e 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -4,9 +4,7 @@ import { DocumentNode, GraphQLError, GraphQLSchema, - lexicographicSortSchema, printSchema, - stripIgnoredCharacters, } from 'graphql'; import { ReportHeader, @@ -872,6 +870,6 @@ export function computeExecutableSchemaId( const schemaDocument = typeof schema === 'string' ? schema - : stripIgnoredCharacters(printSchema(lexicographicSortSchema(schema))); + : printSchema(schema); return sha256.update(schemaDocument).digest('hex'); } From a25a78555909f0c8d18a80d38d3eb66f030063c4 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 11:11:43 +0300 Subject: [PATCH 21/30] Apply suggestions from my own code review --- .../src/__tests__/schemaReporter.test.ts | 3 +-- .../apollo-engine-reporting/src/schemaReporter.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts index b623a97c43d..6863c3488a4 100644 --- a/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts @@ -142,8 +142,7 @@ describe('Schema reporter', () => { const variables = { info: serverInfo, executableSchema: 'schemaSdl' - } - {} + }; mockReporterRequest(url, variables).reply(200, { data: { me: { diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 596e96d7a55..ae1dea27981 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -178,16 +178,18 @@ export class SchemaReporter { try { const httpResponse = await fetch(httpRequest); if (!httpResponse.ok) { - throw new Error("Failed to get a 200 response from graph manager"); + throw new Error([ + `An unexpected HTTP status code (${http.response.status}) was` + 'encountered during schema reporting.' + ].join(' ')); } - const json = await httpResponse.json(); - return json; + return await httpResponse.json(); } catch (error) { throw new Error( [ - 'Unexpected http error from Apollo Graph Manager when reporting server info.', - 'Did not receive a valid json response from Graph Manager', + 'Unexpected HTTP error from Apollo Graph Manager when reporting server info.', + 'Did not receive a valid JSON response from Apollo Graph Manager', 'If this continues to happen please reach out to support@apollographql.com', 'Error:', error From 5b8a766d971f096ba3795dfebb671635c90dc469 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 08:23:26 +0000 Subject: [PATCH 22/30] Fix mistakes in my own GitHub suggestion. Having a code editor is nice. --- packages/apollo-engine-reporting/src/schemaReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index ae1dea27981..c56806a955c 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -179,7 +179,7 @@ export class SchemaReporter { const httpResponse = await fetch(httpRequest); if (!httpResponse.ok) { throw new Error([ - `An unexpected HTTP status code (${http.response.status}) was` + `An unexpected HTTP status code (${httpResponse.status}) was`, 'encountered during schema reporting.' ].join(' ')); } From fd196fbfc7c0efc0b9c0069c94b03481ed7c5943 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 08:35:14 +0000 Subject: [PATCH 23/30] Separate JSON parsing failures from general `fetch` errors. The specificity of the `try`/`catch` around JSON parsing will ensure that HTML parsed as errors is insinuated clearly, whereas any general `fetch` error will propagate up to the error boundary provided by the `catch` that lives within the `reportingLoop` function after `schemaReporter.reportServerInfo` is invoked. --- .../src/schemaReporter.ts | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index c56806a955c..97400f6163f 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -175,23 +175,26 @@ export class SchemaReporter { headers: this.headers, body: JSON.stringify(request), }); - try { - const httpResponse = await fetch(httpRequest); - if (!httpResponse.ok) { - throw new Error([ - `An unexpected HTTP status code (${httpResponse.status}) was`, - 'encountered during schema reporting.' - ].join(' ')); - } + const httpResponse = await fetch(httpRequest); + + if (!httpResponse.ok) { + throw new Error([ + `An unexpected HTTP status code (${httpResponse.status}) was`, + 'encountered during schema reporting.' + ].join(' ')); + } + + try { + // JSON parsing failure due to malformed data is the likely failure case + // here. Any non-JSON response (e.g. HTML) is usually the suspect. return await httpResponse.json(); } catch (error) { throw new Error( [ - 'Unexpected HTTP error from Apollo Graph Manager when reporting server info.', - 'Did not receive a valid JSON response from Apollo Graph Manager', - 'If this continues to happen please reach out to support@apollographql.com', - 'Error:', + "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 ].join(' '), ); From 4836f7593d574cb7068d5a24edc317d39c5f247c Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 08:39:54 +0000 Subject: [PATCH 24/30] Tweak error message phrasing. --- packages/apollo-engine-reporting/src/schemaReporter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 97400f6163f..888ac0133b5 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -44,7 +44,7 @@ export function reportingLoop( // We can add hardcoded backoff in the future, // or on repeated failures stop responding reporting. logger.error( - `Error in reporting server info to Apollo Graph Manager for schema reporting: ${error}`, + `Error reporting server info to Apollo Graph Manager during schema reporting: ${error}`, ); sendNextWithExecutableSchema = false; setTimeout(inner, fallbackReportingDelayInMs); From 3ddddd4e6eaa3c8b552045a05293e3d6b4dc4354 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 08:40:15 +0000 Subject: [PATCH 25/30] Apply specificity to the errors we are expecting to encounter. Ref: https://github.com/apollographql/apollo-server/pull/4084/files#r427109239 Ref: https://github.com/apollographql/apollo-server/pull/4084/files#r427109399 --- .../src/__tests__/schemaReporter.test.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts index 6863c3488a4..44b317c7fd0 100644 --- a/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts +++ b/packages/apollo-engine-reporting/src/__tests__/schemaReporter.test.ts @@ -99,7 +99,11 @@ describe('Schema reporter', () => { }, }); - await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); + await expect( + schemaReporter.reportServerInfo(false), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"An unexpected HTTP status code (500) was encountered during schema reporting."`, + ); }); it('throws on 200 malformed response', async () => { @@ -119,7 +123,11 @@ describe('Schema reporter', () => { }, }); - await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); + await expect( + schemaReporter.reportServerInfo(false), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unexpected response shape from Apollo Graph Manager when reporting server information for schema reporting. If this continues, please reach out to support@apollographql.com. Received response: {\\"me\\":{\\"reportServerInfo\\":{\\"__typename\\":\\"ReportServerInfoResponse\\"}}}"`, + ); mockReporterRequest(url).reply(200, { data: { @@ -128,7 +136,11 @@ describe('Schema reporter', () => { }, }, }); - await expect(schemaReporter.reportServerInfo(false)).rejects.toThrow(); + await expect( + schemaReporter.reportServerInfo(false), + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"This server was configured with an API key for a user. Only a service's API key may be used for schema reporting. Please visit the settings for this graph at https://engine.apollographql.com/ to obtain an API key for a service."`, + ); }); it('sends schema if withExecutableSchema is true.', async () => { From 003b435e20a24e16430f8a36237b1e56feb386e3 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 09:07:08 +0000 Subject: [PATCH 26/30] DRY up repeated error message for unexpected response shape. Ref: https://github.com/apollographql/apollo-server/pull/4084#discussion_r427104984 --- .../src/schemaReporter.ts | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 888ac0133b5..8c58818a542 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -2,6 +2,7 @@ import { ReportServerInfoVariables, EdgeServerInfo, SchemaReportingServerInfoResult, + SchemaReportingServerInfo, } from './reportingOperationTypes'; import { fetch, Headers, Request } from 'apollo-server-env'; import { GraphQLRequest, Logger } from 'apollo-server-types'; @@ -115,17 +116,20 @@ export class SchemaReporter { throw new Error((errors || []).map((x: any) => x.message).join('\n')); } + function msgForUnexpectedResponse(data: SchemaReportingServerInfo): string { + return [ + 'Unexpected response shape from Apollo Graph Manager when', + 'reporting server information for schema reporting. If', + 'this continues, please reach out to support@apollographql.com.', + 'Received response:', + JSON.stringify(data), + ].join(' '); + } + if (!data || !data.me || !data.me.__typename) { - throw new Error( - [ - 'Unexpected response shape from Apollo Graph Manager when', - 'reporting server information for schema reporting. If', - 'this continues, please reach out to support@apollographql.com.', - 'Received response:', - JSON.stringify(data), - ].join(' '), - ); + throw new Error(msgForUnexpectedResponse(data)); } + if (data.me.__typename === 'UserMutation') { this.isStopped = true; throw new Error( @@ -138,27 +142,11 @@ export class SchemaReporter { ); } else if (data.me.__typename === 'ServiceMutation') { if (!data.me.reportServerInfo) { - throw new Error( - [ - 'Unexpected response shape from Apollo Graph Manager when', - 'reporting server information during schema reporting. If', - 'this continues, please reach out to support@apollographql.com.', - 'Received response:', - JSON.stringify(data), - ].join(' '), - ); + throw new Error(msgForUnexpectedResponse(data)); } return data.me.reportServerInfo; } else { - throw new Error( - [ - 'Unexpected response shape from Apollo Graph Manager when', - 'reporting server information during schema reporting. If', - 'this continues, please reach out to support@apollographql.com.', - 'Received response:', - JSON.stringify(data), - ].join(' '), - ); + throw new Error(msgForUnexpectedResponse(data)); } } From cb56ca2c4a429df6bfb6fca070a9cef927ed2a5b Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 12:08:37 +0300 Subject: [PATCH 27/30] Update "Engine" reference to "Apollo Graph Manager". --- packages/apollo-engine-reporting/src/agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index d4dac3dd57e..95456c6f5fe 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -132,7 +132,8 @@ export interface EngineReportingOptions { */ endpointUrl?: string; /** - * The URL of the Engine report ingress server. This was previously known as endpointUrl + * The URL to the Apollo Graph Manager ingress endpoint. + * (Previously, this was `endpointUrl`, which will be removed in AS3). */ tracesEndpointUrl?: string; /** From e3b95e9b6b252ff6063f79f42509d15183684e66 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 12:09:00 +0300 Subject: [PATCH 28/30] Fix my own markdown error. --- packages/apollo-engine-reporting/src/agent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index 95456c6f5fe..a46e66d09d3 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -262,7 +262,7 @@ export interface EngineReportingOptions { * additional details about its runtime environment to Apollo Graph Manager. * * See [our _preview - * documentation](https://github.com/apollographql/apollo-schema-reporting-preview-docs) + * documentation_](https://github.com/apollographql/apollo-schema-reporting-preview-docs) * for more information. */ experimental_schemaReporting?: boolean; From 8598437e04c2df854ab453dbcd0c54bc52ea93c7 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 09:09:54 +0000 Subject: [PATCH 29/30] Just allow `any` type for the unexpected error msg. --- packages/apollo-engine-reporting/src/schemaReporter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/apollo-engine-reporting/src/schemaReporter.ts b/packages/apollo-engine-reporting/src/schemaReporter.ts index 8c58818a542..5dae15d7b61 100644 --- a/packages/apollo-engine-reporting/src/schemaReporter.ts +++ b/packages/apollo-engine-reporting/src/schemaReporter.ts @@ -2,7 +2,6 @@ import { ReportServerInfoVariables, EdgeServerInfo, SchemaReportingServerInfoResult, - SchemaReportingServerInfo, } from './reportingOperationTypes'; import { fetch, Headers, Request } from 'apollo-server-env'; import { GraphQLRequest, Logger } from 'apollo-server-types'; @@ -116,7 +115,7 @@ export class SchemaReporter { throw new Error((errors || []).map((x: any) => x.message).join('\n')); } - function msgForUnexpectedResponse(data: SchemaReportingServerInfo): string { + function msgForUnexpectedResponse(data: any): string { return [ 'Unexpected response shape from Apollo Graph Manager when', 'reporting server information for schema reporting. If', From bf5379a21aaad3c7e8c3898641966b233fe610ea Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 19 May 2020 09:10:59 +0000 Subject: [PATCH 30/30] Don't dynamically import `crypto` to use `createHash`. In the future, this might change back, but this can just be a top-level `import` right now. Ref: https://github.com/apollographql/apollo-server/pull/4084/files#r427103593 --- packages/apollo-engine-reporting/src/agent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/apollo-engine-reporting/src/agent.ts b/packages/apollo-engine-reporting/src/agent.ts index a46e66d09d3..bd3a13b327f 100644 --- a/packages/apollo-engine-reporting/src/agent.ts +++ b/packages/apollo-engine-reporting/src/agent.ts @@ -23,6 +23,7 @@ import { defaultEngineReportingSignature } from 'apollo-graphql'; import { ApolloServerPlugin } from 'apollo-server-plugin-base'; import { reportingLoop, SchemaReporter } from './schemaReporter'; import { v4 as uuidv4 } from 'uuid'; +import { createHash } from 'crypto'; let warnedOnDeprecatedApiKey = false; @@ -867,7 +868,7 @@ export function computeExecutableSchemaId( schema: string | GraphQLSchema, ): string { // Can't call digest on this object twice. Creating new object each function call - const sha256 = module.require('crypto').createHash('sha256'); + const sha256 = createHash('sha256'); const schemaDocument = typeof schema === 'string' ? schema