UNPKG

@subsquid/apollo-server-core

Version:
884 lines (805 loc) 36.4 kB
import os from 'os'; import { promisify } from 'util'; import { gzip } from 'zlib'; import retry from 'async-retry'; import { Report, ReportHeader, Trace } from 'apollo-reporting-protobuf'; import { Response, fetch, Headers, RequestInit } from 'apollo-server-env'; import { AbortController } from 'node-abort-controller'; import type { GraphQLRequestListener, GraphQLServerListener, } from 'apollo-server-plugin-base'; import { CacheScope, GraphQLRequestContext, GraphQLServiceContext, GraphQLRequestContextDidResolveOperation, GraphQLRequestContextWillSendResponse, BaseContext, } from 'apollo-server-types'; import { createOperationDerivedDataCache, OperationDerivedData, operationDerivedDataCacheKey, } from './operationDerivedDataCache'; import { usageReportingSignature } from '@apollo/utils.usagereporting'; import type { ApolloServerPluginUsageReportingOptions, SendValuesBaseOptions, } from './options'; import { dateToProtoTimestamp, TraceTreeBuilder } from '../traceTreeBuilder'; import { makeTraceDetails } from './traceDetails'; import { GraphQLSchema, printSchema } from 'graphql'; import { computeCoreSchemaHash } from '../schemaReporting'; import type { InternalApolloServerPlugin } from '../../internalPlugin'; import { OurReport } from './stats'; import { defaultSendOperationsAsTrace } from './defaultSendOperationsAsTrace'; import { calculateReferencedFieldsByType, ReferencedFieldsByType, } from '@apollo/utils.usagereporting'; import type LRUCache from 'lru-cache'; const gzipPromise = promisify(gzip); const reportHeaderDefaults = { hostname: os.hostname(), agentVersion: `apollo-server-core@${ require('../../../package.json').version }`, runtimeVersion: `node ${process.version}`, // XXX not actually uname, but what node has easily. uname: `${os.platform()}, ${os.type()}, ${os.release()}, ${os.arch()})`, }; export function ApolloServerPluginUsageReporting<TContext extends BaseContext>( options: ApolloServerPluginUsageReportingOptions<TContext> = Object.create( null, ), ): InternalApolloServerPlugin { // Note: We'd like to change the default to false in Apollo Server 4, so that // the default usage reporting experience doesn't include *anything* that // could potentially be PII (like error messages) --- just operations and // numbers. const fieldLevelInstrumentationOption = options.fieldLevelInstrumentation; const fieldLevelInstrumentation = typeof fieldLevelInstrumentationOption === 'number' ? async () => Math.random() < fieldLevelInstrumentationOption ? 1 / fieldLevelInstrumentationOption : 0 : fieldLevelInstrumentationOption ? fieldLevelInstrumentationOption : async () => true; let requestDidStartHandler: ( requestContext: GraphQLRequestContext<TContext>, ) => GraphQLRequestListener<TContext>; return { __internal_plugin_id__() { return 'UsageReporting'; }, // We want to be able to access locals from `serverWillStart` in our `requestDidStart`, thus // this little hack. (Perhaps we should also allow GraphQLServerListener to contain // a requestDidStart?) async requestDidStart(requestContext: GraphQLRequestContext<TContext>) { if (!requestDidStartHandler) { throw Error( 'The usage reporting plugin has been asked to handle a request before the ' + 'server has started. See https://github.com/apollographql/apollo-server/issues/4588 ' + 'for more details.', ); } return requestDidStartHandler(requestContext); }, async serverWillStart({ logger: serverLogger, apollo, serverlessFramework, }: GraphQLServiceContext): Promise<GraphQLServerListener> { // Use the plugin-specific logger if one is provided; otherwise the general server one. const logger = options.logger ?? serverLogger; const { key, graphRef } = apollo; if (!(key && graphRef)) { throw new Error( "You've enabled usage reporting via ApolloServerPluginUsageReporting, " + 'but you also need to provide your Apollo API key and graph ref, via ' + 'the APOLLO_KEY/APOLLO_GRAPH_REF environment ' + 'variables or via `new ApolloServer({apollo: {key, graphRef})`.', ); } logger.info( 'Apollo usage reporting starting! See your graph at ' + `https://studio.apollographql.com/graph/${encodeURI(graphRef)}/`, ); // If sendReportsImmediately is not specified, we default to true if we're running // with the ApolloServer designed for Lambda or similar. That's because these // environments aren't designed around letting us run a background task to // send reports later or hook into container destruction to flush buffered reports. const sendReportsImmediately = options.sendReportsImmediately ?? serverlessFramework; // Since calculating the signature and referenced fields for usage // reporting is potentially an expensive operation, we'll cache the data // we generate and re-use them for repeated operations for the same // `queryHash`. However, because referenced fields depend on the current // schema, we want to throw it out entirely any time the schema changes. let operationDerivedDataCache: { forSchema: GraphQLSchema; cache: LRUCache<string, OperationDerivedData>; } | null = null; // This map maps from executable schema ID (schema hash, basically) to the // report we'll send about it. That's because when we're using a gateway, // the schema can change over time, but each report needs to be about a // single schema. We avoid having this function be a memory leak by // removing values from it when we're in the process of sending reports. // That means we have to be very careful never to pull a Report out of it // and hang on to it for a while before writing to it, because the report // might have gotten sent and discarded in the meantime. So you should // only access the values of this Map via // getReportWhichMustBeUsedImmediately and getAndDeleteReport, and never // hang on to the value returned by getReportWhichMustBeUsedImmediately. const reportByExecutableSchemaId = new Map<string, OurReport>(); const getReportWhichMustBeUsedImmediately = ( executableSchemaId: string, ): OurReport => { const existing = reportByExecutableSchemaId.get(executableSchemaId); if (existing) { return existing; } const report = new OurReport( new ReportHeader({ ...reportHeaderDefaults, executableSchemaId, graphRef, }), ); reportByExecutableSchemaId.set(executableSchemaId, report); return report; }; const getAndDeleteReport = ( executableSchemaId: string, ): OurReport | null => { const report = reportByExecutableSchemaId.get(executableSchemaId); if (report) { reportByExecutableSchemaId.delete(executableSchemaId); return report; } return null; }; const overriddenExecutableSchemaId = options.overrideReportedSchema ? computeCoreSchemaHash(options.overrideReportedSchema) : undefined; let lastSeenExecutableSchemaToId: | { executableSchema: GraphQLSchema; executableSchemaId: string; } | undefined; let reportTimer: NodeJS.Timer | undefined; if (!sendReportsImmediately) { reportTimer = setInterval( () => sendAllReportsAndReportErrors(), options.reportIntervalMs || 10 * 1000, ); } let graphMightSupportTraces = true; const sendOperationAsTrace = options.experimental_sendOperationAsTrace ?? defaultSendOperationsAsTrace(); const includeTracesContributingToStats = options.internal_includeTracesContributingToStats ?? false; let stopped = false; function executableSchemaIdForSchema(schema: GraphQLSchema) { if (lastSeenExecutableSchemaToId?.executableSchema === schema) { return lastSeenExecutableSchemaToId.executableSchemaId; } const id = computeCoreSchemaHash(printSchema(schema)); // We override this variable every time we get a new schema so we cache // the last seen value. It is a single-entry cache. lastSeenExecutableSchemaToId = { executableSchema: schema, executableSchemaId: id, }; return id; } async function sendAllReportsAndReportErrors(): Promise<void> { await Promise.all( [...reportByExecutableSchemaId.keys()].map((executableSchemaId) => sendReportAndReportErrors(executableSchemaId), ), ); } async function sendReportAndReportErrors( executableSchemaId: string, ): Promise<void> { return 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. if (options.reportErrorFunction) { options.reportErrorFunction(err); } else { logger.error(err.message); } }); } // Needs to be an arrow function to be confident that key is defined. const sendReport = async (executableSchemaId: string): Promise<void> => { let report = getAndDeleteReport(executableSchemaId); if ( !report || (Object.keys(report.tracesPerQuery).length === 0 && report.operationCount === 0) ) { return; } // Set the report's overall end time. This is the timestamp that will be // associated with the summarized statistics. report.endTime = dateToProtoTimestamp(new Date()); report.ensureCountsAreIntegers(); const protobufError = Report.verify(report); if (protobufError) { throw new Error(`Error verifying report: ${protobufError}`); } let message: Uint8Array | null = Report.encode(report).finish(); // Let the original protobuf object be garbage collected (helpful if the // HTTP request hangs). report = null; // Potential follow-up: we can compare message.length to // report.sizeEstimator.bytes and use it to "learn" if our estimation is // off and adjust it based on what we learn. if (options.debugPrintReports) { // In terms of verbosity, and as the name of this option suggests, // this message is either an "info" or a "debug" level message. // However, we are using `warn` here for compatibility reasons since // the `debugPrintReports` flag pre-dated the existence of log-levels // and changing this to also require `debug: true` (in addition to // `debugPrintReports`) just to reach the level of verbosity to // produce the output would be a breaking change. The "warn" level is // on by default. There is a similar theory and comment applied // below. // // We decode the report rather than printing the original `report` // so that it includes all of the pre-encoded traces. const decodedReport = Report.decode(message); logger.warn( `Apollo usage report: ${JSON.stringify(decodedReport.toJSON())}`, ); } const compressed = await gzipPromise(message); // Let the uncompressed message be garbage collected (helpful if the // HTTP request is slow). message = null; // Wrap fetcher with async-retry for automatic retrying const fetcher = options.fetcher ?? fetch; const response: Response = await retry( // Retry on network errors and 5xx HTTP // responses. async () => { // Note that once we require Node v16 we can use its global // AbortController instead of the one from `node-abort-controller`. const controller = new AbortController(); const abortTimeout = setTimeout(() => { controller.abort(); }, options.requestTimeoutMs ?? 30_000); let curResponse; try { const requestInit: RequestInit = { method: 'POST', headers: { 'user-agent': 'ApolloServerPluginUsageReporting', 'x-api-key': key, 'content-encoding': 'gzip', accept: 'application/json', }, body: compressed, agent: options.requestAgent, }; // The apollo-server-env Fetch API doesn't have `signal` in // RequestInit, but it does work in node-fetch. We've added it // already to our `Fetcher` interface (`@apollo/utils.fetcher`) // that we're using in AS4 but making changes to // `apollo-server-env` that could cause custom AS3 fetchers to not // compile feels like a bad idea. The worst case scenario of // passing in an ignored `signal` is the timeout doesn't work, in // which case you're not getting the new feature but can change // your fetcher to make it work. (requestInit as any).signal = controller.signal; curResponse = await fetcher( (options.endpointUrl || 'https://usage-reporting.api.apollographql.com') + '/api/ingress/traces', requestInit, ); } finally { clearTimeout(abortTimeout); } if (curResponse.status >= 500 && curResponse.status < 600) { throw new Error( `HTTP status ${curResponse.status}, ${ (await curResponse.text()) || '(no body)' }`, ); } else { return curResponse; } }, { retries: (options.maxAttempts || 5) - 1, minTimeout: options.minimumRetryDelayMs || 100, factor: 2, }, ).catch((err: Error) => { throw new Error( `Error sending report to Apollo servers: ${err.message}`, ); }); if (response.status < 200 || response.status >= 300) { // Note that we don't expect to see a 3xx here because request follows // redirects. throw new Error( `Error sending report to Apollo servers: HTTP status ${ response.status }, ${(await response.text()) || '(no body)'}`, ); } if ( graphMightSupportTraces && response.status === 200 && response.headers .get('content-type') ?.match(/^\s*application\/json\s*(?:;|$)/i) ) { const body = await response.text(); let parsedBody; try { parsedBody = JSON.parse(body); } catch (e) { throw new Error(`Error parsing response from Apollo servers: ${e}`); } if (parsedBody.tracesIgnored === true) { logger.debug( "This graph's organization does not have access to traces; sending all " + 'subsequent operations as traces.', ); graphMightSupportTraces = false; // XXX We could also parse traces that are already in the current // report and convert them to stats if we wanted? } } if (options.debugPrintReports) { // In terms of verbosity, and as the name of this option suggests, this // message is either an "info" or a "debug" level message. However, // we are using `warn` here for compatibility reasons since the // `debugPrintReports` flag pre-dated the existence of log-levels and // changing this to also require `debug: true` (in addition to // `debugPrintReports`) just to reach the level of verbosity to produce // the output would be a breaking change. The "warn" level is on by // default. There is a similar theory and comment applied above. logger.warn(`Apollo usage report: status ${response.status}`); } }; requestDidStartHandler = ({ logger: requestLogger, metrics, schema, request: { http, variables }, }): GraphQLRequestListener<TContext> => { // Request specific log output should go into the `logger` from the // request context when it's provided. const logger = requestLogger ?? options.logger ?? serverLogger; const treeBuilder: TraceTreeBuilder = new TraceTreeBuilder({ rewriteError: options.rewriteError, logger, }); treeBuilder.startTiming(); metrics.startHrTime = treeBuilder.startHrTime; let graphqlValidationFailure = false; let graphqlUnknownOperationName = false; let includeOperationInUsageReporting: boolean | null = null; if (http) { treeBuilder.trace.http = new Trace.HTTP({ method: Trace.HTTP.Method[ http.method as keyof typeof Trace.HTTP.Method ] || Trace.HTTP.Method.UNKNOWN, }); if (options.sendHeaders) { makeHTTPRequestHeaders( treeBuilder.trace.http, http.headers, options.sendHeaders, ); } } // After this function completes, includeOperationInUsageReporting is // defined. async function maybeCallIncludeRequestHook( requestContext: | GraphQLRequestContextDidResolveOperation<TContext> | GraphQLRequestContextWillSendResponse<TContext>, ): Promise<void> { // If this is the second call in `willSendResponse` after // `didResolveOperation`, we're done. if (includeOperationInUsageReporting !== null) return; if (typeof options.includeRequest !== 'function') { // Default case we always report includeOperationInUsageReporting = true; return; } includeOperationInUsageReporting = await options.includeRequest( requestContext, ); // Help the user understand they've returned an unexpected value, // which might be a subtle mistake. if (typeof includeOperationInUsageReporting !== 'boolean') { logger.warn( "The 'includeRequest' async predicate function must return a boolean value.", ); includeOperationInUsageReporting = true; } } // Our usage reporting groups everything by operation, so we don't // actually report about any issues that prevent us from getting an // operation string (eg, a missing operation, or APQ problems). // This is effectively bypassing the reporting of: // - PersistedQueryNotFoundError // - PersistedQueryNotSupportedError // - Missing `query` error // We may want to report them some other way later! let didResolveSource: boolean = false; return { async didResolveSource(requestContext) { didResolveSource = true; if (metrics.persistedQueryHit) { treeBuilder.trace.persistedQueryHit = true; } if (metrics.persistedQueryRegister) { treeBuilder.trace.persistedQueryRegister = true; } if (variables) { treeBuilder.trace.details = makeTraceDetails( variables, options.sendVariableValues, requestContext.source, ); } const clientInfo = ( options.generateClientInfo || defaultGenerateClientInfo )(requestContext); if (clientInfo) { // While there is a clientAddress protobuf field, the backend // doesn't pay attention to it yet, so we'll ignore it for now. const { clientName, clientVersion } = clientInfo; treeBuilder.trace.clientVersion = clientVersion || ''; treeBuilder.trace.clientName = clientName || ''; } }, async validationDidStart() { return async (validationErrors?: ReadonlyArray<Error>) => { graphqlValidationFailure = validationErrors ? validationErrors.length !== 0 : false; }; }, async didResolveOperation(requestContext) { // If operation is undefined then `getOperationAST` returned null // and an unknown operation was specified. graphqlUnknownOperationName = requestContext.operation === undefined; await maybeCallIncludeRequestHook(requestContext); if ( includeOperationInUsageReporting && // No need to capture traces if the operation is going to // immediately fail due to unknown operation name. !graphqlUnknownOperationName ) { if (metrics.captureTraces === undefined) { // We're not completely ignoring the operation. But should we // calculate a detailed trace of every field while we do so (either // directly in this plugin, or in a subgraph by sending the // apollo-federation-include-trace header)? That will allow this // operation to contribute to the "field executions" column in the // Studio Fields page, to the timing hints in Explorer and // vscode-graphql, and to the traces visible under Operations. (Note // that `true` here does not imply that this operation will // necessarily be *sent* to the usage-reporting endpoint in the form // of a trace --- it still might be aggregated into stats first. But // capturing a trace will mean we can understand exactly what fields // were executed and what their performance was, at the tradeoff of // some overhead for tracking the trace (and transmitting it between // subgraph and gateway). const rawWeight = await fieldLevelInstrumentation( requestContext, ); treeBuilder.trace.fieldExecutionWeight = typeof rawWeight === 'number' ? rawWeight : rawWeight ? 1 : 0; metrics.captureTraces = !!treeBuilder.trace.fieldExecutionWeight; } } }, async executionDidStart() { // If we're not capturing traces, don't return a willResolveField so // that we don't build up a detailed trace inside treeBuilder. (We still // will use treeBuilder as a convenient place to put top-level facts // about the operation which can end up aggregated as stats, and we do // eventually put *errors* onto the trace tree.) if (!metrics.captureTraces) return; return { willResolveField({ info }) { return treeBuilder.willResolveField(info); // We could save the error into the trace during the end handler, but // it won't have all the information that graphql-js adds to it later, // like 'locations'. }, }; }, async willSendResponse(requestContext) { // Search above for a comment about "didResolveSource" to see which // of the pre-source-resolution errors we are intentionally avoiding. if (!didResolveSource) return; if (requestContext.errors) { treeBuilder.didEncounterErrors(requestContext.errors); } const resolvedOperation = !!requestContext.operation; // If we got an error before we called didResolveOperation (eg parse or // validation error), check to see if we should include the request. await maybeCallIncludeRequestHook(requestContext); treeBuilder.stopTiming(); const executableSchemaId = overriddenExecutableSchemaId ?? executableSchemaIdForSchema(schema); if (includeOperationInUsageReporting === false) { if (resolvedOperation) getReportWhichMustBeUsedImmediately(executableSchemaId) .operationCount++; return; } treeBuilder.trace.fullQueryCacheHit = !!metrics.responseCacheHit; treeBuilder.trace.forbiddenOperation = !!metrics.forbiddenOperation; treeBuilder.trace.registeredOperation = !!metrics.registeredOperation; const policyIfCacheable = requestContext.overallCachePolicy.policyIfCacheable(); if (policyIfCacheable) { treeBuilder.trace.cachePolicy = new Trace.CachePolicy({ scope: policyIfCacheable.scope === CacheScope.Private ? Trace.CachePolicy.Scope.PRIVATE : policyIfCacheable.scope === CacheScope.Public ? Trace.CachePolicy.Scope.PUBLIC : Trace.CachePolicy.Scope.UNKNOWN, // Convert from seconds to ns. maxAgeNs: policyIfCacheable.maxAge * 1e9, }); } // If this was a federated operation and we're the gateway, add the query plan // to the trace. if (metrics.queryPlanTrace) { treeBuilder.trace.queryPlan = metrics.queryPlanTrace; } // Intentionally un-awaited so as not to block the response. Any // errors will be logged, but will not manifest a user-facing error. // The logger in this case is a request specific logger OR the logger // defined by the plugin if that's unavailable. The request-specific // logger is preferred since this is very much coupled directly to a // client-triggered action which might be more granularly tagged by // logging implementations. addTrace().catch(logger.error); async function addTrace(): Promise<void> { // Ignore traces that come in after stop(). if (stopped) { return; } // Ensure that the caller of addTrace (which does not await it) is // not blocked. We use setImmediate rather than process.nextTick or // just relying on the Promise microtask queue because setImmediate // comes after IO, which is what we want. await new Promise((res) => setImmediate(res)); const executableSchemaId = overriddenExecutableSchemaId ?? executableSchemaIdForSchema(schema); const { trace } = treeBuilder; let statsReportKey: string | undefined = undefined; let referencedFieldsByType: ReferencedFieldsByType; if (!requestContext.document) { statsReportKey = `## GraphQLParseFailure\n`; } else if (graphqlValidationFailure) { statsReportKey = `## GraphQLValidationFailure\n`; } else if (graphqlUnknownOperationName) { statsReportKey = `## GraphQLUnknownOperationName\n`; } const isExecutable = statsReportKey === undefined; if (statsReportKey) { if (options.sendUnexecutableOperationDocuments) { trace.unexecutedOperationBody = requestContext.source; // Get the operation name from the request (which might not // correspond to an actual operation). trace.unexecutedOperationName = requestContext.request.operationName || ''; } referencedFieldsByType = Object.create(null); } else { const operationDerivedData = getOperationDerivedData(); statsReportKey = `# ${requestContext.operationName || '-'}\n${ operationDerivedData.signature }`; referencedFieldsByType = operationDerivedData.referencedFieldsByType; } const protobufError = Trace.verify(trace); if (protobufError) { throw new Error(`Error encoding trace: ${protobufError}`); } if (resolvedOperation) { getReportWhichMustBeUsedImmediately(executableSchemaId) .operationCount++; } getReportWhichMustBeUsedImmediately(executableSchemaId).addTrace({ statsReportKey, trace, // We include the operation as a trace (rather than aggregated // into stats) only if we believe it's possible that our // organization's plan allows for viewing traces *and* we // actually captured this as a full trace *and* // sendOperationAsTrace says so. // // (As an edge case, if the reason metrics.captureTraces is // falsey is that this is an unexecutable operation and thus we // never ran the code in didResolveOperation that sets // metrics.captureTrace, we allow it to be sent as a trace. This // means we'll still send some parse and validation failures as // traces, for the sake of the Errors page.) asTrace: graphMightSupportTraces && (!isExecutable || !!metrics.captureTraces) && sendOperationAsTrace(trace, statsReportKey), includeTracesContributingToStats, referencedFieldsByType, }); // If the buffer gets big (according to our estimate), send. if ( sendReportsImmediately || getReportWhichMustBeUsedImmediately(executableSchemaId) .sizeEstimator.bytes >= (options.maxUncompressedReportSize || 4 * 1024 * 1024) ) { await sendReportAndReportErrors(executableSchemaId); } } // Calculates signature and referenced fields for the current document. // Only call this when the document properly parses and validates and // the given operation name (if any) is known! function getOperationDerivedData(): OperationDerivedData { if (!requestContext.document) { // This shouldn't happen: no document means parse failure, which // uses its own special statsReportKey. throw new Error('No document?'); } const cacheKey = operationDerivedDataCacheKey( requestContext.queryHash, requestContext.operationName || '', ); // Ensure that the cache we have is for the right schema. if ( !operationDerivedDataCache || operationDerivedDataCache.forSchema !== schema ) { operationDerivedDataCache = { forSchema: schema, cache: createOperationDerivedDataCache({ logger }), }; } // If we didn't have the signature in the cache, we'll resort to // calculating it. const cachedOperationDerivedData = operationDerivedDataCache.cache.get(cacheKey); if (cachedOperationDerivedData) { return cachedOperationDerivedData; } const generatedSignature = ( options.calculateSignature || usageReportingSignature )(requestContext.document, requestContext.operationName || ''); const generatedOperationDerivedData: OperationDerivedData = { signature: generatedSignature, referencedFieldsByType: calculateReferencedFieldsByType({ document: requestContext.document, schema, resolvedOperationName: requestContext.operationName ?? null, }), }; // Note that this cache is always an in-memory cache. // If we replace it with a more generic async cache, we should // not await the write operation. operationDerivedDataCache.cache.set( cacheKey, generatedOperationDerivedData, ); return generatedOperationDerivedData; } }, }; }; return { async serverWillStop() { if (reportTimer) { clearInterval(reportTimer); reportTimer = undefined; } stopped = true; await sendAllReportsAndReportErrors(); }, }; }, }; } export function makeHTTPRequestHeaders( http: Trace.IHTTP, headers: Headers, sendHeaders?: SendValuesBaseOptions, ): void { if ( !sendHeaders || ('none' in sendHeaders && sendHeaders.none) || ('all' in sendHeaders && !sendHeaders.all) ) { return; } for (const [key, value] of headers) { const lowerCaseKey = key.toLowerCase(); if ( ('exceptNames' in sendHeaders && // We assume that most users only have a few headers to hide, or will // just set {none: true} ; we can change this linear-time // operation if it causes real performance issues. sendHeaders.exceptNames.some((exceptHeader) => { // Headers are case-insensitive, and should be compared as such. return exceptHeader.toLowerCase() === lowerCaseKey; })) || ('onlyNames' in sendHeaders && !sendHeaders.onlyNames.some((header) => { return header.toLowerCase() === lowerCaseKey; })) ) { continue; } switch (key) { case 'authorization': case 'cookie': case 'set-cookie': break; default: http!.requestHeaders![key] = new Trace.HTTP.Values({ value: [value], }); } } } function defaultGenerateClientInfo({ request }: GraphQLRequestContext) { const clientNameHeaderKey = 'apollographql-client-name'; const clientVersionHeaderKey = 'apollographql-client-version'; // Default to using the `apollo-client-x` header fields if present. // If none are present, fallback on the `clientInfo` query extension // for backwards compatibility. // The default value if neither header values nor query extension is // set is the empty String for all fields (as per protobuf defaults) if ( request.http?.headers?.get(clientNameHeaderKey) || request.http?.headers?.get(clientVersionHeaderKey) ) { return { clientName: request.http?.headers?.get(clientNameHeaderKey), clientVersion: request.http?.headers?.get(clientVersionHeaderKey), }; } else if (request.extensions?.clientInfo) { return request.extensions.clientInfo; } else { return {}; } } // This plugin does nothing, but it ensures that ApolloServer won't try // to add a default ApolloServerPluginUsageReporting. export function ApolloServerPluginUsageReportingDisabled(): InternalApolloServerPlugin { return { __internal_plugin_id__() { return 'UsageReporting'; }, }; }