@graphql-yoga/plugin-apollo-usage-report
Version:
Apollo's GraphOS usage report plugin for GraphQL Yoga.
140 lines (139 loc) • 5.89 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Reporter = void 0;
exports.getEnvVar = getEnvVar;
exports.dateToProtoTimestamp = dateToProtoTimestamp;
const usage_reporting_protobuf_1 = require("@apollo/usage-reporting-protobuf");
const stats_js_1 = require("./stats.js");
const DEFAULT_REPORTING_ENDPOINT = 'https://usage-reporting.api.apollographql.com/api/ingress/traces';
class Reporter {
#yoga;
#logger = console;
#reportHeaders;
#options;
#reportsBySchema = {};
#nextSendAfterDelay;
#sending = [];
constructor(options, yoga, logger) {
this.#logger = logger;
this.#yoga = yoga;
this.#options = {
...options,
maxBatchDelay: options.maxBatchDelay ?? 20_000, // 20s
maxBatchUncompressedSize: options.maxBatchUncompressedSize ?? 4 * 1024 * 1024, // 4mb
maxTraceSize: options.maxTraceSize ?? 10 * 1024 * 1024, // 10mb
exportTimeout: options.exportTimeout ?? 30_000, // 30s
};
this.#reportHeaders = {
graphRef: getGraphRef(options),
hostname: options.hostname ?? getEnvVar('HOSTNAME') ?? '',
uname: options.uname ?? '', // TODO: find a cross-platform way to get the uname
runtimeVersion: options.runtimeVersion ?? '',
agentVersion: options.agentVersion || `graphql-yoga@${yoga.version}`,
};
}
addTrace(schemaId, options) {
const report = this.#getReport(schemaId);
report.addTrace(options);
if (this.#options.alwaysSend ||
report.sizeEstimator.bytes >= this.#options.maxBatchUncompressedSize) {
return this.sendReport(schemaId);
}
this.#nextSendAfterDelay ||= setTimeout(() => this.flush(), this.#options.maxBatchDelay);
return;
}
async flush() {
return Promise.allSettled([
...this.#sending, // When flushing, we want to also wait for previous traces to be sent, because it's mostly used for clean up
...Object.keys(this.#reportsBySchema).map(schemaId => this.sendReport(schemaId)),
]);
}
async sendReport(schemaId) {
const sending = this.#sendReport(schemaId);
this.#sending.push(sending);
sending.finally(() => this.#sending?.filter(p => p !== sending));
return sending;
}
async #sendReport(schemaId) {
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.#yoga;
const report = this.#reportsBySchema[schemaId];
if (!report) {
throw new Error(`No report to send for schema ${schemaId}`);
}
if (this.#nextSendAfterDelay != null) {
clearTimeout(this.#nextSendAfterDelay);
this.#nextSendAfterDelay = undefined;
}
delete this.#reportsBySchema[schemaId];
report.endTime = dateToProtoTimestamp(new Date());
report.ensureCountsAreIntegers();
const validationError = usage_reporting_protobuf_1.Report.verify(report);
if (validationError) {
throw new TypeError(`Invalid report: ${validationError}`);
}
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this.#options;
const encodedReport = usage_reporting_protobuf_1.Report.encode(report).finish();
for (let tries = 0; tries < 5; tries++) {
try {
this.#logger?.debug(`Sending report (try ${tries}/5)`);
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/protobuf',
'content-encoding': 'gzip',
// The presence of the api key is already checked at Yoga initialization time
'x-api-key': apiKey,
accept: 'application/json',
},
body: new ReadableStream({
start(controller) {
controller.enqueue(encodedReport);
controller.close();
},
}).pipeThrough(new CompressionStream('gzip')),
signal: AbortSignal.timeout(this.#options.exportTimeout),
});
const result = await response.text();
if (response.ok) {
this.#logger?.debug('Report sent:', result);
return;
}
throw result;
}
catch (err) {
this.#logger?.error('Failed to send report:', err);
}
}
throw new Error('Failed to send traces after 5 tries');
}
#getReport(schemaId) {
const report = this.#reportsBySchema[schemaId];
if (report) {
return report;
}
return (this.#reportsBySchema[schemaId] = new stats_js_1.OurReport(new usage_reporting_protobuf_1.ReportHeader({
...this.#reportHeaders,
executableSchemaId: schemaId,
})));
}
}
exports.Reporter = Reporter;
function getGraphRef(options) {
const graphRef = options.graphRef || getEnvVar('APOLLO_GRAPH_REF');
if (!graphRef) {
throw new Error('Missing GraphRef. Either provide `graphRef` option or `APOLLO_GRAPH_REF` environment variable');
}
return graphRef;
}
function getEnvVar(name, defaultValue) {
return globalThis.process?.env?.[name] || defaultValue || undefined;
}
// Converts a JS Date into a Timestamp.
function dateToProtoTimestamp(date) {
const totalMillis = date.getTime();
const millis = totalMillis % 1000;
return new usage_reporting_protobuf_1.google.protobuf.Timestamp({
seconds: (totalMillis - millis) / 1000,
nanos: millis * 1e6,
});
}