UNPKG

@graphql-yoga/plugin-apollo-usage-report

Version:

Apollo's GraphOS usage report plugin for GraphQL Yoga.

138 lines (137 loc) • 6.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.useApolloUsageReport = useApolloUsageReport; exports.hashSHA256 = hashSHA256; const graphql_1 = require("graphql"); const graphql_yoga_1 = require("graphql-yoga"); const utils_usagereporting_1 = require("@apollo/utils.usagereporting"); const utils_1 = require("@graphql-tools/utils"); const plugin_apollo_inline_trace_1 = require("@graphql-yoga/plugin-apollo-inline-trace"); const reporter_js_1 = require("./reporter.js"); function useApolloUsageReport(options = {}) { const [instrumentation, ctxForReq] = (0, plugin_apollo_inline_trace_1.useApolloInstrumentation)(options); let schemaIdSet$; let currentSchema; let yoga; let reporter; const logger = Object.fromEntries(['error', 'warn', 'info', 'debug'].map(level => [ level, (...messages) => yoga.logger[level]('[ApolloUsageReport]', ...messages), ])); let clientNameFactory = req => req.headers.get('apollographql-client-name'); if (typeof options.clientName === 'function') { clientNameFactory = options.clientName; } let clientVersionFactory = req => req.headers.get('apollographql-client-version'); if (typeof options.clientVersion === 'function') { clientVersionFactory = options.clientVersion; } return { onPluginInit({ addPlugin }) { addPlugin(instrumentation); addPlugin({ onYogaInit(args) { yoga = args.yoga; reporter = new reporter_js_1.Reporter(options, yoga, logger); if (!(0, reporter_js_1.getEnvVar)('APOLLO_KEY', options.apiKey)) { throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`); } if (!(0, reporter_js_1.getEnvVar)('APOLLO_GRAPH_REF', options.graphRef)) { throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`); } }, onSchemaChange({ schema }) { if (schema) { schemaIdSet$ = hashSHA256((0, utils_1.printSchemaWithDirectives)(schema), yoga.fetchAPI) .then(id => { currentSchema = { id, schema }; schemaIdSet$ = undefined; }) .catch(error => { logger.error('Failed to calculate schema hash: ', error); }); } }, onRequestParse() { return schemaIdSet$; }, onParse() { return function onParseEnd({ result, context }) { if (!currentSchema) { throw new Error("should not happen: schema doesn't exists"); } const ctx = ctxForReq.get(context.request)?.traces.get(context); if (!ctx) { logger.debug('operation tracing context not found, this operation will not be traced.'); return; } const operationName = context.params.operationName ?? (isDocumentNode(result) ? (0, graphql_1.getOperationAST)(result)?.name?.value : undefined); const signature = operationName ? (0, utils_usagereporting_1.usageReportingSignature)(result, operationName) : (context.params.query ?? ''); ctx.referencedFieldsByType = (0, utils_usagereporting_1.calculateReferencedFieldsByType)({ document: result, schema: currentSchema.schema, resolvedOperationName: operationName ?? null, }); ctx.operationKey = `# ${operationName || '-'}\n${signature}`; ctx.schemaId = currentSchema.id; }; }, onResultProcess({ request, result, serverContext }) { // TODO: Handle async iterables ? if ((0, graphql_yoga_1.isAsyncIterable)(result)) { logger.debug('async iterable results not implemented for now'); return; } const reqCtx = ctxForReq.get(request); if (!reqCtx) { logger.debug('operation tracing context not found, this operation will not be traced.'); return; } for (const trace of reqCtx.traces.values()) { if (!trace.schemaId || !trace.operationKey) { logger.debug('Misformed trace, missing operation key or schema id'); continue; } const clientName = clientNameFactory(request); if (clientName) { trace.trace.clientName = clientName; } const clientVersion = clientVersionFactory(request); if (clientVersion) { trace.trace.clientVersion = clientVersion; } serverContext.waitUntil(reporter.addTrace(currentSchema.id, { statsReportKey: trace.operationKey, trace: trace.trace, referencedFieldsByType: trace.referencedFieldsByType, asTrace: true, // TODO: allow to not always send traces nonFtv1ErrorPaths: [], maxTraceBytes: options.maxTraceSize, })); } }, async onDispose() { await reporter?.flush(); }, }); }, }; } async function hashSHA256(text, api = globalThis) { const inputUint8Array = new api.TextEncoder().encode(text); const arrayBuf = await api.crypto.subtle.digest({ name: 'SHA-256' }, inputUint8Array); const outputUint8Array = new Uint8Array(arrayBuf); let hash = ''; for (const byte of outputUint8Array) { const hex = byte.toString(16); hash += '00'.slice(0, Math.max(0, 2 - hex.length)) + hex; } return hash; } function isDocumentNode(data) { const isObject = (data) => !!data && typeof data === 'object'; return isObject(data) && data['kind'] === graphql_1.Kind.DOCUMENT; }