@subsquid/apollo-server-core
Version:
Core engine for Apollo GraphQL server
884 lines (805 loc) • 36.4 kB
text/typescript
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';
},
};
}