diff --git a/.changeset/serious-pumas-hear.md b/.changeset/serious-pumas-hear.md new file mode 100644 index 000000000..283f2c2fb --- /dev/null +++ b/.changeset/serious-pumas-hear.md @@ -0,0 +1,5 @@ +--- +"@apollo/gateway": patch +--- + +Remove out-of-band reporting in the gateway and provide a warning for users who have the endpoint configured. diff --git a/codegen.yml b/codegen.yml index bceb472bd..7fbca7664 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,11 +1,9 @@ overwrite: true schema: [ "https://uplink.api.apollographql.com/", - "https://outofbandreporter.api.apollographql.com/", ] documents: - gateway-js/src/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.ts - - gateway-js/src/supergraphManagers/UplinkSupergraphManager/outOfBandReporter.ts generates: gateway-js/src/__generated__/graphqlTypes.ts: plugins: diff --git a/gateway-js/src/__generated__/graphqlTypes.ts b/gateway-js/src/__generated__/graphqlTypes.ts index 6999d89a2..b6055a227 100644 --- a/gateway-js/src/__generated__/graphqlTypes.ts +++ b/gateway-js/src/__generated__/graphqlTypes.ts @@ -10,36 +10,9 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; - /** ISO 8601, extended format with nanoseconds, Zulu (or "[+-]seconds" as a string or number relative to now) */ Timestamp: any; }; -export type ApiMonitoringReport = { - endedAt: Scalars['Timestamp']; - error: Error; - request: Request; - response?: InputMaybe; - startedAt: Scalars['Timestamp']; - /** Tags can include things like version and package name */ - tags?: InputMaybe>; -}; - -/** Input type for providing error details in field arguments. */ -export type Error = { - /** The error code. */ - code: ErrorCode; - /** The error message. */ - message?: InputMaybe; -}; - -export enum ErrorCode { - ConnectionFailed = 'CONNECTION_FAILED', - InvalidBody = 'INVALID_BODY', - Other = 'OTHER', - Timeout = 'TIMEOUT', - UnexpectedResponse = 'UNEXPECTED_RESPONSE' -} - export type FetchError = { __typename?: 'FetchError'; code: FetchErrorCode; @@ -61,11 +34,6 @@ export enum FetchErrorCode { UnknownRef = 'UNKNOWN_REF' } -export type HttpHeader = { - name: Scalars['String']; - value?: InputMaybe; -}; - export type Message = { __typename?: 'Message'; body: Scalars['String']; @@ -78,16 +46,6 @@ export enum MessageLevel { Warn = 'WARN' } -export type Mutation = { - __typename?: 'Mutation'; - reportError: Scalars['Boolean']; -}; - - -export type MutationReportErrorArgs = { - report?: InputMaybe; -}; - /** A chunk of persisted queries */ export type PersistedQueriesChunk = { __typename?: 'PersistedQueriesChunk'; @@ -111,7 +69,6 @@ export type PersistedQueriesResult = { export type Query = { __typename?: 'Query'; - _empty?: Maybe; /** Fetch the persisted queries for a router. */ persistedQueries: PersistedQueriesResponse; /** Fetch the configuration for a router. */ @@ -141,18 +98,6 @@ export type QueryRouterEntitlementsArgs = { ref: Scalars['String']; }; -export type Request = { - body?: InputMaybe; - headers?: InputMaybe>; - url: Scalars['String']; -}; - -export type Response = { - body?: InputMaybe; - headers?: InputMaybe>; - httpStatusCode: Scalars['Int']; -}; - export type RouterConfigResponse = FetchError | RouterConfigResult | Unchanged; export type RouterConfigResult = { @@ -214,10 +159,3 @@ export type SupergraphSdlQueryVariables = Exact<{ export type SupergraphSdlQuery = { __typename?: 'Query', routerConfig: { __typename: 'FetchError', code: FetchErrorCode, message: string } | { __typename: 'RouterConfigResult', id: string, minDelaySeconds: number, supergraphSdl: string } | { __typename: 'Unchanged' } }; - -export type OobReportMutationVariables = Exact<{ - input?: InputMaybe; -}>; - - -export type OobReportMutation = { __typename?: 'Mutation', reportError: boolean }; diff --git a/gateway-js/src/__tests__/integration/nockMocks.ts b/gateway-js/src/__tests__/integration/nockMocks.ts index 64a17fd68..873a1f9b9 100644 --- a/gateway-js/src/__tests__/integration/nockMocks.ts +++ b/gateway-js/src/__tests__/integration/nockMocks.ts @@ -80,9 +80,6 @@ export const mockCloudConfigUrl2 = export const mockCloudConfigUrl3 = 'https://example3.cloud-config-url.com/cloudconfig/'; -export const mockOutOfBandReporterUrl = - 'https://example.outofbandreporter.com/monitoring/'; - export function mockSupergraphSdlRequestIfAfter(ifAfter: string | null, url: string | RegExp = mockCloudConfigUrl1) { return gatewayNock(url).post('/', { query: SUPERGRAPH_SDL_QUERY, @@ -140,18 +137,3 @@ export function mockSupergraphSdlRequestIfAfterUnchanged( export function mockSupergraphSdlRequestSuccess({supergraphSdl = getTestingSupergraphSdl(), url = mockCloudConfigUrl1}: {supergraphSdl?: string, url?: string | RegExp} = {}) { return mockSupergraphSdlRequestSuccessIfAfter(null, undefined, supergraphSdl, url); } - -export function mockOutOfBandReportRequest() { - return gatewayNock(mockOutOfBandReporterUrl).post('/', () => true); -} - -export function mockOutOfBandReportRequestSuccess() { - return mockOutOfBandReportRequest().reply( - 200, - JSON.stringify({ - data: { - reportError: true - }, - }), - ); -} diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts index 90672ed87..f6dfd1316 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/__tests__/loadSupergraphSdlFromStorage.test.ts @@ -10,9 +10,7 @@ import { apiKey, mockCloudConfigUrl1, mockCloudConfigUrl2, - mockOutOfBandReporterUrl, mockSupergraphSdlRequest, - mockOutOfBandReportRequestSuccess, mockSupergraphSdlRequestSuccess, mockSupergraphSdlRequestIfAfterUnchanged, mockSupergraphSdlRequestIfAfter, @@ -43,7 +41,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: undefined, fetcher, requestTimeoutMs, compositionId: null, @@ -77,7 +74,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2], - errorReportingEndpoint: undefined, fetcher, requestTimeoutMs, compositionId: 'originalId-1234', @@ -116,7 +112,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2], - errorReportingEndpoint: undefined, fetcher, requestTimeoutMs, compositionId: 'originalId-1234', @@ -148,7 +143,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2], - errorReportingEndpoint: undefined, fetcher, requestTimeoutMs, compositionId: 'originalId-1234', @@ -172,7 +166,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, fetcher, requestTimeoutMs, compositionId: null, @@ -197,7 +190,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, fetcher, requestTimeoutMs, compositionId: null, @@ -212,14 +204,12 @@ describe('loadSupergraphSdlFromStorage', () => { it("throws on non-OK status codes when `errors` isn't present in a JSON response", async () => { mockSupergraphSdlRequest().reply(500); - mockOutOfBandReportRequestSuccess(); await expect( loadSupergraphSdlFromStorage({ graphRef, apiKey, endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, fetcher, requestTimeoutMs, compositionId: null, @@ -231,204 +221,6 @@ describe('loadSupergraphSdlFromStorage', () => { ), ); }); - - // if an additional request were made by the out of band reporter, nock would throw since it's unmocked - // and this test would fail - it("Out of band reporting doesn't submit reports when endpoint is not configured", async () => { - mockSupergraphSdlRequest().reply(400); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 400 invalid json response body at https://example1.cloud-config-url.com/cloudconfig/ reason: Unexpected end of JSON input', - ), - ); - }); - - it('throws on 400 status response and does not submit an out of band error', async () => { - mockSupergraphSdlRequest().reply(400); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 400 invalid json response body at https://example1.cloud-config-url.com/cloudconfig/ reason: Unexpected end of JSON input', - ), - ); - }); - - it('throws on 413 status response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().reply(413); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 413 Payload Too Large', - ), - ); - }); - - it('throws on 422 status response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().reply(422); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 422 Unprocessable Entity', - ), - ); - }); - - it('throws on 408 status response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().reply(408); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 408 Request Timeout', - ), - ); - }); - }); - - it('throws on 504 status response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().reply(504); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 504 Gateway Timeout', - ), - ); - }); - - it('throws when there is no response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().replyWithError('no response'); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: request to https://example1.cloud-config-url.com/cloudconfig/ failed, reason: no response', - ), - ); - }); - - it('throws on 502 status response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().reply(502); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 502 Bad Gateway', - ), - ); - }); - - it('throws on 503 status response and successfully submits an out of band error', async () => { - mockSupergraphSdlRequest().reply(503); - mockOutOfBandReportRequestSuccess(); - - await expect( - loadSupergraphSdlFromStorage({ - graphRef, - apiKey, - endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, - fetcher, - requestTimeoutMs, - compositionId: null, - logger, - }), - ).rejects.toThrowError( - new UplinkFetcherError( - 'An error occurred while fetching your schema from Apollo: 503 Service Unavailable', - ), - ); }); it('successfully responds to SDL unchanged by returning null', async () => { @@ -438,7 +230,6 @@ describe('loadSupergraphSdlFromStorage', () => { graphRef, apiKey, endpoint: mockCloudConfigUrl1, - errorReportingEndpoint: mockOutOfBandReporterUrl, fetcher, requestTimeoutMs, compositionId: 'id-1234', @@ -460,7 +251,6 @@ describe('loadSupergraphSdlFromUplinks', () => { graphRef, apiKey, endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2], - errorReportingEndpoint: mockOutOfBandReporterUrl, fetcher: (url: string, init?: FetcherRequestInit) => { calls++; return fetcher(url, init); @@ -499,7 +289,6 @@ describe('loadSupergraphSdlFromUplinks', () => { graphRef, apiKey, endpoints: [mockCloudConfigUrl1, mockCloudConfigUrl2], - errorReportingEndpoint: undefined, fetcher, requestTimeoutMs, compositionId: 'originalId-1234', diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts index 4edb97cce..a5f8119ae 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/index.ts @@ -73,8 +73,6 @@ export class UplinkSupergraphManager implements SupergraphManager { private onFailureToFetchSupergraphSdlAfterInit?: FailureToFetchSupergraphSdlAfterInit; private timerRef: NodeJS.Timeout | null = null; private state: State; - private errorReportingEndpoint: string | undefined = - process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT ?? undefined; private compositionId?: string; private fetchCount: number = 0; private mostRecentSuccessfulFetchAt?: Date; @@ -135,6 +133,9 @@ export class UplinkSupergraphManager implements SupergraphManager { this.onFailureToFetchSupergraphSdlAfterInit = onFailureToFetchSupergraphSdlAfterInit; + if (!!process.env.APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT) { + this.logger.warn('Out-of-band error reporting is no longer used by Apollo. You may remove the `APOLLO_OUT_OF_BAND_REPORTER_ENDPOINT` environment variable at your convenience.'); + } this.state = { phase: 'constructed' }; } @@ -208,7 +209,6 @@ export class UplinkSupergraphManager implements SupergraphManager { graphRef: this.graphRef, apiKey: this.apiKey, endpoints: this.uplinkEndpoints, - errorReportingEndpoint: this.errorReportingEndpoint, fetcher: this.fetcher, compositionId: this.compositionId ?? null, maxRetries, diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.ts index 510ed11cc..25b8ba717 100644 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.ts +++ b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/loadSupergraphSdlFromStorage.ts @@ -2,7 +2,6 @@ import { GraphQLError } from 'graphql'; import retry from 'async-retry'; import { AbortController } from "node-abort-controller"; import { SupergraphSdlUpdate } from '../../config'; -import { submitOutOfBandReportIfConfigured } from './outOfBandReporter'; import { SupergraphSdlQuery } from '../../__generated__/graphqlTypes'; import type { FetcherResponse, @@ -58,7 +57,6 @@ export async function loadSupergraphSdlFromUplinks({ graphRef, apiKey, endpoints, - errorReportingEndpoint, fetcher, compositionId, maxRetries, @@ -69,7 +67,6 @@ export async function loadSupergraphSdlFromUplinks({ graphRef: string; apiKey: string; endpoints: string[]; - errorReportingEndpoint: string | undefined, fetcher: Fetcher; compositionId: string | null; maxRetries: number, @@ -86,7 +83,6 @@ export async function loadSupergraphSdlFromUplinks({ graphRef, apiKey, endpoint: endpoints[roundRobinSeed++ % endpoints.length], - errorReportingEndpoint, fetcher, requestTimeoutMs, compositionId, @@ -106,7 +102,6 @@ export async function loadSupergraphSdlFromStorage({ graphRef, apiKey, endpoint, - errorReportingEndpoint, fetcher, requestTimeoutMs, compositionId, @@ -115,7 +110,6 @@ export async function loadSupergraphSdlFromStorage({ graphRef: string; apiKey: string; endpoint: string; - errorReportingEndpoint?: string; fetcher: Fetcher; requestTimeoutMs: number; compositionId: string | null; @@ -150,29 +144,15 @@ export async function loadSupergraphSdlFromStorage({ logger.debug(`🔧 Fetching ${graphRef} supergraph schema from ${endpoint} ifAfterId ${compositionId}`); - const startTime = new Date(); let result: FetcherResponse; try { result = await fetcher(endpoint, requestDetails); } catch (e) { - const endTime = new Date(); - - await submitOutOfBandReportIfConfigured({ - error: e, - requestEndpoint: endpoint, - requestBody, - endpoint: errorReportingEndpoint, - startedAt: startTime, - endedAt: endTime, - fetcher, - }); - throw new UplinkFetcherError(fetchErrorMsg + (e.message ?? e)); } finally { clearTimeout(signal); } - const endTime = new Date(); let response: SupergraphSdlQueryResult; if (result.ok || result.status === 400) { @@ -191,16 +171,6 @@ export async function loadSupergraphSdlFromStorage({ ); } } else { - await submitOutOfBandReportIfConfigured({ - error: new UplinkFetcherError(fetchErrorMsg + result.status + ' ' + result.statusText), - requestEndpoint: endpoint, - requestBody, - endpoint: errorReportingEndpoint, - response: result, - startedAt: startTime, - endedAt: endTime, - fetcher, - }); throw new UplinkFetcherError(fetchErrorMsg + result.status + ' ' + result.statusText); } diff --git a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/outOfBandReporter.ts b/gateway-js/src/supergraphManagers/UplinkSupergraphManager/outOfBandReporter.ts deleted file mode 100644 index f0cafa4f9..000000000 --- a/gateway-js/src/supergraphManagers/UplinkSupergraphManager/outOfBandReporter.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { FetcherResponse, type Fetcher } from '@apollo/utils.fetcher'; -import { GraphQLError } from 'graphql'; -import { - ErrorCode, - OobReportMutation, - OobReportMutationVariables, -} from '../../__generated__/graphqlTypes'; - -// Magic /* GraphQL */ comment below is for codegen, do not remove -export const OUT_OF_BAND_REPORTER_QUERY = /* GraphQL */`#graphql - mutation OOBReport($input: APIMonitoringReport) { - reportError(report: $input) - } -`; - -const { name, version } = require('../../../package.json'); - -type OobReportMutationResult = - | OobReportMutationSuccess - | OobReportMutationFailure; - -interface OobReportMutationSuccess { - data: OobReportMutation; -} - -interface OobReportMutationFailure { - data?: OobReportMutation; - errors: GraphQLError[]; -} - -export async function submitOutOfBandReportIfConfigured({ - error, - requestEndpoint, - requestBody, - endpoint, - response, - startedAt, - endedAt, - tags, - fetcher, -}: { - error: Error; - requestEndpoint: string; - requestBody: string; - endpoint: string | undefined; - response?: FetcherResponse; - startedAt: Date; - endedAt: Date; - tags?: string[]; - fetcher: Fetcher; -}) { - // don't send report if the endpoint url is not configured - if (!endpoint) { - return; - } - - let errorCode: ErrorCode; - if (!response) { - errorCode = ErrorCode.ConnectionFailed; - } else { - // possible error situations to check against - switch (response.status) { - case 400: - case 413: - case 422: - errorCode = ErrorCode.InvalidBody; - break; - case 408: - case 504: - errorCode = ErrorCode.Timeout; - break; - case 502: - case 503: - errorCode = ErrorCode.ConnectionFailed; - break; - default: - errorCode = ErrorCode.Other; - } - } - - const responseBody: string | undefined = await response?.text(); - - const variables: OobReportMutationVariables = { - input: { - error: { - code: errorCode, - message: error.message, - }, - request: { - url: requestEndpoint, - body: requestBody, - }, - response: response - ? { - httpStatusCode: response.status, - body: responseBody, - } - : null, - startedAt: startedAt.toISOString(), - endedAt: endedAt.toISOString(), - tags: tags, - }, - }; - - try { - const oobResponse = await fetcher(endpoint, { - method: 'POST', - body: JSON.stringify({ - query: OUT_OF_BAND_REPORTER_QUERY, - variables, - }), - headers: { - 'apollographql-client-name': name, - 'apollographql-client-version': version, - 'user-agent': `${name}/${version}`, - 'content-type': 'application/json', - }, - }); - const parsedResponse: OobReportMutationResult = await oobResponse.json(); - if (!parsedResponse?.data?.reportError) { - throw new Error( - `Out-of-band error reporting failed: ${oobResponse.status} ${oobResponse.statusText}`, - ); - } - } catch (e) { - throw new Error(`Out-of-band error reporting failed: ${e.message ?? e}`); - } -}