@graphql-hive/core
Version:
336 lines (335 loc) • 16.9 kB
JavaScript
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,
};
}
;