UNPKG

@graphql-hive/core

Version:
448 lines (447 loc) • 22.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createUsage = createUsage; exports.createCollector = createCollector; const graphql_1 = require("graphql"); const tiny_lru_1 = require("tiny-lru"); const operation_js_1 = require("../normalize/operation.js"); const version_js_1 = require("../version.js"); const agent_js_1 = require("./agent.js"); const collect_schema_coordinates_js_1 = require("./collect-schema-coordinates.js"); const sampling_js_1 = require("./sampling.js"); const error_path_to_coordinate_js_1 = require("./subrequests/error-path-to-coordinate.js"); const extract_coordinates_js_1 = require("./subrequests/extract-coordinates.js"); const utils_js_1 = require("./utils.js"); function isAbortAction(result) { return 'action' in result && result.action === 'abort'; } const noopUsageCollector = { collect() { return { subrequest() { return () => { }; }, async finish() { }, }; }, collectRequest() { }, async dispose() { }, collectSubscription() { }, }; function createUsage(pluginOptions) { var _a, _b, _c, _d, _e, _f; if (!pluginOptions.usage || pluginOptions.enabled === false) { return noopUsageCollector; } let reportSize = 0; let reportMap = {}; let reportOperations = []; let reportSubscriptionOperations = []; const options = typeof pluginOptions.usage === 'boolean' ? {} : pluginOptions.usage; const selfHostingOptions = pluginOptions.selfHosting; const logger = pluginOptions.logger.child({ module: 'hive-usage' }); const collector = (0, utils_js_1.memo)(createCollector, arg => arg.schema); const exclusions = (_a = options.exclude) !== null && _a !== void 0 ? _a : []; /** Access tokens using the `hvo1/` require a target. */ if (!options.target && !(0, utils_js_1.isLegacyAccessToken)(pluginOptions.token)) { logger.error("Your access token requires providing the 'target' option." + '\nUsage reporting is disabled.'); return noopUsageCollector; } if (options.target && (0, utils_js_1.isLegacyAccessToken)(pluginOptions.token)) { logger.error("Using the 'target' option requires using an organization access token (starting with 'hvo1/')." + '\nUsage reporting is disabled.'); return noopUsageCollector; } const baseEndpoint = (_c = (_b = selfHostingOptions === null || selfHostingOptions === void 0 ? void 0 : selfHostingOptions.usageEndpoint) !== null && _b !== void 0 ? _b : options.endpoint) !== null && _c !== void 0 ? _c : 'https://app.graphql-hive.com/usage'; const endpoint = baseEndpoint + ((options === null || options === void 0 ? void 0 : options.target) ? `/${options.target}` : ''); const agent = (0, agent_js_1.createAgent)(Object.assign(Object.assign({}, ((_d = pluginOptions.agent) !== null && _d !== void 0 ? _d : { maxSize: 1500, })), { logger, endpoint, token: pluginOptions.token, enabled: pluginOptions.enabled, debug: pluginOptions.debug, fetch: (_e = pluginOptions.agent) === null || _e === void 0 ? void 0 : _e.fetch }), { data: { set(action) { var _a, _b, _c; if (action.type === 'request') { const operation = action.data; const fetches = (_a = operation.execution.fetches) === null || _a === void 0 ? void 0 : _a.map((f) => { var _a, _b, _c, _d, _e, _f; const subgraphFields = (0, extract_coordinates_js_1.extractCoordinates)({ schema: f.subgraphSchema, document: f.document, resultData: (_a = f.result) === null || _a === void 0 ? void 0 : _a.data, }); const errors = []; for (const error of (_c = (_b = f.result) === null || _b === void 0 ? void 0 : _b.errors) !== null && _c !== void 0 ? _c : []) { const coordinate = error.path && (0, error_path_to_coordinate_js_1.errorPathToCoordinate)(f.subgraphSchema, error.path, f.document, (_d = f.result) === null || _d === void 0 ? void 0 : _d.data); if (coordinate) { errors.push({ coordinate, code: (_e = error.extensions) === null || _e === void 0 ? void 0 : _e.code, }); } } return { duration: f.duration, start: f.start, status: f.status, type: f.type, paths: (_f = f.paths) !== null && _f !== void 0 ? _f : 'Query', subgraph: f.subgraph, errors, fields: subgraphFields, }; }); reportOperations.push({ operationMapKey: operation.key, timestamp: operation.timestamp, execution: { ok: operation.execution.ok, duration: operation.execution.duration, errorsTotal: operation.execution.errorsTotal, fetches, }, metadata: { client: (_b = operation.client) !== null && _b !== void 0 ? _b : undefined, }, persistedDocumentHash: operation.persistedDocumentHash, }); } else if (action.type === 'subscription') { const operation = action.data; reportSubscriptionOperations.push({ operationMapKey: operation.key, timestamp: operation.timestamp, metadata: { client: (_c = operation.client) !== null && _c !== void 0 ? _c : undefined, }, persistedDocumentHash: operation.persistedDocumentHash, }); } reportSize += 1; if (!reportMap[action.data.key]) { reportMap[action.data.key] = { operation: action.data.operation, operationName: action.data.operationName, fields: action.data.fields, }; } }, size() { return reportSize; }, clear() { reportSize = 0; reportMap = {}; reportOperations = []; reportSubscriptionOperations = []; }, }, headers() { var _a, _b, _c, _d; return { 'graphql-client-name': (_b = (_a = pluginOptions.agent) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : 'Hive Client', 'graphql-client-version': (_d = (_c = pluginOptions.agent) === null || _c === void 0 ? void 0 : _c.version) !== null && _d !== void 0 ? _d : version_js_1.version, 'x-usage-api-version': '2', }; }, body() { const report = { size: reportSize, map: reportMap, operations: reportOperations.length ? reportOperations : undefined, subscriptionOperations: reportSubscriptionOperations.length ? reportSubscriptionOperations : undefined, }; return JSON.stringify(report); }, }); (0, utils_js_1.logIf)(typeof pluginOptions.token !== 'string' || pluginOptions.token.length === 0, 'token is missing', logger.error); const shouldInclude = options.sampler && typeof options.sampler === 'function' ? (0, sampling_js_1.dynamicSampling)(options.sampler) : (0, sampling_js_1.randomSampling)((_f = options.sampleRate) !== null && _f !== void 0 ? _f : 1.0); const collectRequest = args => { var _a, _b, _c, _d; let providedOperationName = undefined; try { const result = args.result; if (isAbortAction(result)) { if (result.logging) { logger.info(result.reason); } return; } const document = args.args.document; const rootOperation = document.definitions.find(o => o.kind === graphql_1.Kind.OPERATION_DEFINITION); providedOperationName = args.args.operationName || ((_a = rootOperation.name) === null || _a === void 0 ? void 0 : _a.value); const operationName = providedOperationName || 'anonymous'; // Check if operationName is a match with any string or regex in exclusions const isMatch = exclusions.some(excludingValue => excludingValue instanceof RegExp ? excludingValue.test(operationName) : operationName === excludingValue); if (!isMatch && shouldInclude({ operationName, document, variableValues: args.args.variableValues, contextValue: args.args.contextValue, })) { const collect = collector({ schema: args.args.schema, max: (_b = options.max) !== null && _b !== void 0 ? _b : 1000, ttl: options.ttl, processVariables: (_c = options.processVariables) !== null && _c !== void 0 ? _c : false, }); let fetches = args.fetches; if (!(fetches === null || fetches === void 0 ? void 0 : fetches.length) && options.fieldLevelMetricsEnabled) { /** * No subgraph requests, so this must be a monolith. * We still want to track the field metrics, so create an artificial * fetch that represents the local lookup. */ fetches = [ { document: args.args.document, duration: args.duration, start: 0, subgraph: '', subgraphSchema: args.args.schema, type: 'ROOT', paths: rootOperation.operation, result, // make sure this isnt taking too much memory to store. Can this be stripped out? }, ]; } agent.capture(collect(document, (_d = args.args.variableValues) !== null && _d !== void 0 ? _d : null).then(({ key, value: info }) => { var _a, _b, _c; return { type: 'request', data: { key, timestamp: Date.now(), operationName, operation: info.document, fields: info.fields, execution: { ok: !((_a = result.errors) === null || _a === void 0 ? void 0 : _a.length), duration: args.duration, errorsTotal: (_c = (_b = result.errors) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0, fetches, }, // TODO: operationHash is ready to accept hashes of persisted operations client: args.experimental__persistedDocumentHash ? undefined : pickClientInfoProperties(typeof args.args.contextValue !== 'undefined' && typeof options.clientInfo !== 'undefined' ? options.clientInfo(args.args.contextValue) : createDefaultClientInfo()(args.args.contextValue)), persistedDocumentHash: args.experimental__persistedDocumentHash, }, }; })); } } catch (error) { const details = providedOperationName ? ` (name: "${providedOperationName}")` : ''; logger.error(`Failed to collect operation${details}`, error); } }; return { dispose: agent.dispose, /** The raw request collection function */ collectRequest, /** * A more advanced method of collecting the request that includes calculating durations * automatically and supports subrequest data. */ collect() { const sinceStart = (0, utils_js_1.measureDuration)(); const subRequests = []; return { subrequest({ subgraph, type, paths }) { const start = sinceStart(); const sinceSubStart = (0, utils_js_1.measureDuration)(); return args => { const duration = sinceSubStart(); subRequests.push({ start, duration, status: args.status, subgraph, type, result: args.result, document: args.document, subgraphSchema: args.subgraphSchema, paths, }); }; }, async finish(args, result, experimental__persistedDocumentHash) { const duration = sinceStart(); return collectRequest({ args, result, duration, experimental__persistedDocumentHash, fetches: subRequests.length > 0 ? subRequests : null, }); }, }; }, async collectSubscription({ args, experimental__persistedDocumentHash }) { var _a, _b, _c, _d; const document = args.document; const rootOperation = document.definitions.find(o => o.kind === graphql_1.Kind.OPERATION_DEFINITION); const providedOperationName = args.operationName || ((_a = rootOperation.name) === null || _a === void 0 ? void 0 : _a.value); const operationName = providedOperationName || 'anonymous'; // Check if operationName is a match with any string or regex in exclusions const isMatch = exclusions.some(excludingValue => excludingValue instanceof RegExp ? excludingValue.test(operationName) : operationName === excludingValue); if (!isMatch && shouldInclude({ operationName, document, variableValues: args.variableValues, contextValue: args.contextValue, })) { const collect = collector({ schema: args.schema, max: (_b = options.max) !== null && _b !== void 0 ? _b : 1000, ttl: options.ttl, processVariables: (_c = options.processVariables) !== null && _c !== void 0 ? _c : false, }); agent.capture(collect(document, (_d = args.variableValues) !== null && _d !== void 0 ? _d : null).then(({ key, value: info }) => ({ type: 'subscription', data: { key, timestamp: Date.now(), operationName, operation: info.document, fields: info.fields, // when there is a persisted document hash, we don't need to send the client info, // as it's already included in the persisted document hash and usage ingestor will extract that info client: experimental__persistedDocumentHash ? undefined : typeof args.contextValue !== 'undefined' && typeof options.clientInfo !== 'undefined' ? options.clientInfo(args.contextValue) : createDefaultClientInfo()(args.contextValue), persistedDocumentHash: experimental__persistedDocumentHash, }, }))); } }, }; } function createCollector({ schema, max, ttl, processVariables = false, }) { const typeInfo = new graphql_1.TypeInfo(schema); function collect(doc, variables) { const entries = (0, collect_schema_coordinates_js_1.collectSchemaCoordinates)({ documentNode: doc, processVariables, schema, typeInfo, variables, }); return { document: (0, operation_js_1.normalizeOperation)({ document: doc, hideLiterals: true, removeAliases: true, }), fields: Array.from(entries), }; } return (0, utils_js_1.cache)(collect, function cacheKey(doc, variables) { return (0, utils_js_1.cacheDocumentKey)(doc, processVariables === true ? variables : null); }, (0, tiny_lru_1.lru)(max, ttl)); } const defaultClientNameHeaders = ['x-graphql-client-name', 'graphql-client-name']; const defaultClientVersionHeaders = ['x-graphql-client-version', 'graphql-client-version']; function lookupHeader(headerGetter, possibleNames) { for (const name of possibleNames) { const value = headerGetter(name); if (typeof value === 'string') { return value; } } return null; } function createDefaultClientInfo(config) { var _a, _b, _c, _d; const clientNameHeaders = new Set(((_a = config === null || config === void 0 ? void 0 : config.http) === null || _a === void 0 ? void 0 : _a.clientHeaderName) ? [config.http.clientHeaderName, ...defaultClientNameHeaders] : defaultClientNameHeaders); const clientVersionHeaders = new Set(((_b = config === null || config === void 0 ? void 0 : config.http) === null || _b === void 0 ? void 0 : _b.versionHeaderName) ? [config.http.versionHeaderName, ...defaultClientVersionHeaders] : defaultClientVersionHeaders); const clientFieldName = (_d = (_c = config === null || config === void 0 ? void 0 : config.ws) === null || _c === void 0 ? void 0 : _c.clientFieldName) !== null && _d !== void 0 ? _d : 'client'; return function defaultClientInfo(context) { var _a, _b, _c, _d, _e, _f; // whatwg Request if (typeof ((_b = (_a = context === null || context === void 0 ? void 0 : context.request) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.get) === 'function') { const headerGetter = (name) => { var _a, _b; return (_b = (_a = context === null || context === void 0 ? void 0 : context.request) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b.get(name); }; const name = lookupHeader(headerGetter, clientNameHeaders); const version = lookupHeader(headerGetter, clientVersionHeaders); if (typeof name === 'string' && typeof version === 'string') { return { name, version, }; } return null; } // Node.js IncomingMessage if (((_c = context === null || context === void 0 ? void 0 : context.req) === null || _c === void 0 ? void 0 : _c.headers) && typeof ((_d = context.req) === null || _d === void 0 ? void 0 : _d.headers) === 'object') { const headerGetter = (name) => context.req.headers[name]; const name = lookupHeader(headerGetter, clientNameHeaders); const version = lookupHeader(headerGetter, clientVersionHeaders); if (typeof name === 'string' && typeof version === 'string') { return { name, version, }; } return null; } // Plain headers object if ((context === null || context === void 0 ? void 0 : context.headers) && typeof context.headers === 'object') { const headerGetter = (name) => context.headers[name]; const name = lookupHeader(headerGetter, clientNameHeaders); const version = lookupHeader(headerGetter, clientVersionHeaders); if (typeof name === 'string' && typeof version === 'string') { return { name, version, }; } return null; } // GraphQL over WebSocket if (((_e = context === null || context === void 0 ? void 0 : context.connectionParams) === null || _e === void 0 ? void 0 : _e[clientFieldName]) && typeof ((_f = context.connectionParams) === null || _f === void 0 ? void 0 : _f[clientFieldName]) === 'object') { const name = context.connectionParams[clientFieldName].name; const version = context.connectionParams[clientFieldName].version; if (typeof name === 'string' && typeof version === 'string') { return { name, version, }; } return null; } return null; }; } function pickClientInfoProperties(info) { if (!info) { return null; } return { name: info.name, version: info.version, }; }