UNPKG

@graphql-hive/core

Version:
336 lines (335 loc) • 16.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createUsage = createUsage; exports.createCollector = createCollector; const tslib_1 = require("tslib"); const graphql_1 = require("graphql"); const tiny_lru_1 = tslib_1.__importDefault(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 utils_js_1 = require("./utils.js"); function isAbortAction(result) { return 'action' in result && result.action === 'abort'; } function createUsage(pluginOptions) { var _a, _b, _c, _d, _e, _f, _g, _h; if (!pluginOptions.usage || pluginOptions.enabled === false) { return { collect() { return async () => { }; }, collectRequest() { }, async dispose() { }, collectSubscription() { }, }; } let reportSize = 0; let reportMap = {}; let reportOperations = []; let reportSubscriptionOperations = []; const options = typeof pluginOptions.usage === 'boolean' ? {} : pluginOptions.usage; const selfHostingOptions = pluginOptions.selfHosting; const logger = (0, utils_js_1.createHiveLogger)((_b = (_a = pluginOptions.agent) === null || _a === void 0 ? void 0 : _a.logger) !== null && _b !== void 0 ? _b : console, '[hive][usage]'); const collector = (0, utils_js_1.memo)(createCollector, arg => arg.schema); const excludeSet = new Set((_c = options.exclude) !== null && _c !== void 0 ? _c : []); const baseEndpoint = (_e = (_d = selfHostingOptions === null || selfHostingOptions === void 0 ? void 0 : selfHostingOptions.usageEndpoint) !== null && _d !== void 0 ? _d : options.endpoint) !== null && _e !== void 0 ? _e : '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({}, ((_f = pluginOptions.agent) !== null && _f !== void 0 ? _f : { maxSize: 1500, })), { logger, endpoint, token: pluginOptions.token, enabled: pluginOptions.enabled, debug: pluginOptions.debug, fetch: (_g = pluginOptions.agent) === null || _g === void 0 ? void 0 : _g.fetch }), { data: { set(action) { var _a, _b; if (action.type === 'request') { const operation = action.data; reportOperations.push({ operationMapKey: operation.key, timestamp: operation.timestamp, execution: { ok: operation.execution.ok, duration: operation.execution.duration, errorsTotal: operation.execution.errorsTotal, }, metadata: { client: (_a = operation.client) !== null && _a !== void 0 ? _a : undefined, }, persistedDocumentHash: operation.persistedDocumentHash, }); } else if (action.type === 'subscription') { const operation = action.data; reportSubscriptionOperations.push({ operationMapKey: operation.key, timestamp: operation.timestamp, metadata: { client: (_b = operation.client) !== null && _b !== void 0 ? _b : 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() { return { 'graphql-client-name': 'Hive Client', 'graphql-client-version': 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)((_h = options.sampleRate) !== null && _h !== void 0 ? _h : 1.0); const collectRequest = args => { var _a, _b, _c, _d, _e, _f; let providedOperationName = undefined; try { if (isAbortAction(args.result)) { if (args.result.logging) { logger.info(args.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 excludeSet const isMatch = Array.from(excludeSet).some(excludingValue => excludingValue instanceof RegExp ? excludingValue.test(operationName) : operationName === excludingValue); if (!isMatch && shouldInclude({ operationName, document, variableValues: args.args.variableValues, contextValue: args.args.contextValue, })) { const errors = (_c = (_b = args.result.errors) === null || _b === void 0 ? void 0 : _b.map(error => { var _a; return ({ message: error.message, path: (_a = error.path) === null || _a === void 0 ? void 0 : _a.join('.'), }); })) !== null && _c !== void 0 ? _c : []; const collect = collector({ schema: args.args.schema, max: (_d = options.max) !== null && _d !== void 0 ? _d : 1000, ttl: options.ttl, processVariables: (_e = options.processVariables) !== null && _e !== void 0 ? _e : false, }); agent.capture(collect(document, (_f = args.args.variableValues) !== null && _f !== void 0 ? _f : null).then(({ key, value: info }) => { return { type: 'request', data: { key, timestamp: Date.now(), operationName, operation: info.document, fields: info.fields, execution: { ok: errors.length === 0, duration: args.duration, errorsTotal: errors.length, errors, }, // 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, collectRequest, collect() { const finish = (0, utils_js_1.measureDuration)(); return async function complete(args, result, experimental__persistedDocumentHash) { const duration = finish(); return collectRequest({ args, result, duration, experimental__persistedDocumentHash }); }; }, 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 excludeSet const isMatch = Array.from(excludeSet).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.default)(max, ttl)); } const defaultClientNameHeader = 'x-graphql-client-name'; const defaultClientVersionHeader = 'x-graphql-client-version'; function createDefaultClientInfo(config) { var _a, _b, _c, _d, _e, _f; const clientNameHeader = (_b = (_a = config === null || config === void 0 ? void 0 : config.http) === null || _a === void 0 ? void 0 : _a.clientHeaderName) !== null && _b !== void 0 ? _b : defaultClientNameHeader; const clientVersionHeader = (_d = (_c = config === null || config === void 0 ? void 0 : config.http) === null || _c === void 0 ? void 0 : _c.versionHeaderName) !== null && _d !== void 0 ? _d : defaultClientVersionHeader; const clientFieldName = (_f = (_e = config === null || config === void 0 ? void 0 : config.ws) === null || _e === void 0 ? void 0 : _e.clientFieldName) !== null && _f !== void 0 ? _f : 'client'; return function defaultClientInfo(context) { var _a, _b, _c, _d, _e, _f, _g; // 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 name = context.request.headers.get(clientNameHeader); const version = context.request.headers.get(clientVersionHeader); 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 name = context.req.headers[clientNameHeader]; const version = context.req.headers[clientVersionHeader]; 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 ((_e = context.req) === null || _e === void 0 ? void 0 : _e.headers) === 'object') { const name = context.req.headers[clientNameHeader]; const version = context.req.headers[clientVersionHeader]; if (typeof name === 'string' && typeof version === 'string') { return { name, version, }; } return null; } // GraphQL over WebSocket if (((_f = context === null || context === void 0 ? void 0 : context.connectionParams) === null || _f === void 0 ? void 0 : _f[clientFieldName]) && typeof ((_g = context.connectionParams) === null || _g === void 0 ? void 0 : _g[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, }; }