@graphql-yoga/plugin-apollo-usage-report 0.8.5 → 0.9.0-alpha-20250515114730-227992730df06d592514265ec4b8d341a03c3f88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/index.js +45 -67
- package/cjs/reporter.js +142 -0
- package/cjs/stats.js +547 -0
- package/esm/index.js +44 -66
- package/esm/reporter.js +136 -0
- package/esm/stats.js +539 -0
- package/package.json +3 -1
- package/typings/index.d.cts +55 -3
- package/typings/index.d.ts +55 -3
- package/typings/reporter.d.cts +13 -0
- package/typings/reporter.d.ts +13 -0
- package/typings/stats.d.cts +136 -0
- package/typings/stats.d.ts +136 -0
package/cjs/index.js
CHANGED
|
@@ -4,18 +4,15 @@ exports.useApolloUsageReport = useApolloUsageReport;
|
|
|
4
4
|
exports.hashSHA256 = hashSHA256;
|
|
5
5
|
const graphql_1 = require("graphql");
|
|
6
6
|
const graphql_yoga_1 = require("graphql-yoga");
|
|
7
|
-
const
|
|
7
|
+
const utils_usagereporting_1 = require("@apollo/utils.usagereporting");
|
|
8
8
|
const plugin_apollo_inline_trace_1 = require("@graphql-yoga/plugin-apollo-inline-trace");
|
|
9
|
-
const
|
|
10
|
-
function getEnvVar(name, defaultValue) {
|
|
11
|
-
return globalThis.process?.env?.[name] || defaultValue || undefined;
|
|
12
|
-
}
|
|
13
|
-
const DEFAULT_REPORTING_ENDPOINT = 'https://usage-reporting.api.apollographql.com/api/ingress/traces';
|
|
9
|
+
const reporter_js_1 = require("./reporter.js");
|
|
14
10
|
function useApolloUsageReport(options = {}) {
|
|
15
11
|
const [instrumentation, ctxForReq] = (0, plugin_apollo_inline_trace_1.useApolloInstrumentation)(options);
|
|
16
12
|
let schemaIdSet$;
|
|
17
|
-
let
|
|
13
|
+
let schema;
|
|
18
14
|
let yoga;
|
|
15
|
+
let reporter;
|
|
19
16
|
const logger = Object.fromEntries(['error', 'warn', 'info', 'debug'].map(level => [
|
|
20
17
|
level,
|
|
21
18
|
(...messages) => yoga.logger[level]('[ApolloUsageReport]', ...messages),
|
|
@@ -34,18 +31,23 @@ function useApolloUsageReport(options = {}) {
|
|
|
34
31
|
addPlugin({
|
|
35
32
|
onYogaInit(args) {
|
|
36
33
|
yoga = args.yoga;
|
|
37
|
-
|
|
34
|
+
reporter = new reporter_js_1.Reporter(options, yoga, logger);
|
|
35
|
+
if (!(0, reporter_js_1.getEnvVar)('APOLLO_KEY', options.apiKey)) {
|
|
38
36
|
throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`);
|
|
39
37
|
}
|
|
40
|
-
if (!getEnvVar('APOLLO_GRAPH_REF', options.graphRef)) {
|
|
38
|
+
if (!(0, reporter_js_1.getEnvVar)('APOLLO_GRAPH_REF', options.graphRef)) {
|
|
41
39
|
throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`);
|
|
42
40
|
}
|
|
43
41
|
},
|
|
44
42
|
onSchemaChange({ schema }) {
|
|
45
43
|
if (schema) {
|
|
46
|
-
schemaIdSet$ =
|
|
47
|
-
|
|
44
|
+
schemaIdSet$ = hashSHA256((0, graphql_1.printSchema)(schema), yoga.fetchAPI)
|
|
45
|
+
.then(id => {
|
|
46
|
+
schema = { id, schema };
|
|
48
47
|
schemaIdSet$ = undefined;
|
|
48
|
+
})
|
|
49
|
+
.catch(error => {
|
|
50
|
+
logger.error('Failed to calculate schema hash: ', error);
|
|
49
51
|
});
|
|
50
52
|
}
|
|
51
53
|
},
|
|
@@ -54,6 +56,9 @@ function useApolloUsageReport(options = {}) {
|
|
|
54
56
|
},
|
|
55
57
|
onParse() {
|
|
56
58
|
return function onParseEnd({ result, context }) {
|
|
59
|
+
if (!schema) {
|
|
60
|
+
throw new Error("should not happen: schema doesn't exists");
|
|
61
|
+
}
|
|
57
62
|
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
58
63
|
if (!ctx) {
|
|
59
64
|
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
@@ -61,11 +66,16 @@ function useApolloUsageReport(options = {}) {
|
|
|
61
66
|
}
|
|
62
67
|
const operationName = context.params.operationName ??
|
|
63
68
|
(isDocumentNode(result) ? (0, graphql_1.getOperationAST)(result)?.name?.value : undefined);
|
|
64
|
-
const signature =
|
|
65
|
-
? (0,
|
|
66
|
-
: '';
|
|
69
|
+
const signature = operationName
|
|
70
|
+
? (0, utils_usagereporting_1.usageReportingSignature)(result, operationName)
|
|
71
|
+
: (context.params.query ?? '');
|
|
72
|
+
ctx.referencedFieldsByType = (0, utils_usagereporting_1.calculateReferencedFieldsByType)({
|
|
73
|
+
document: result,
|
|
74
|
+
schema: schema.schema,
|
|
75
|
+
resolvedOperationName: operationName ?? null,
|
|
76
|
+
});
|
|
67
77
|
ctx.operationKey = `# ${operationName || '-'}\n${signature}`;
|
|
68
|
-
ctx.schemaId =
|
|
78
|
+
ctx.schemaId = schema.id;
|
|
69
79
|
};
|
|
70
80
|
},
|
|
71
81
|
onResultProcess({ request, result, serverContext }) {
|
|
@@ -79,9 +89,6 @@ function useApolloUsageReport(options = {}) {
|
|
|
79
89
|
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
80
90
|
return;
|
|
81
91
|
}
|
|
82
|
-
// Each operation in a batched request can belongs to a different schema.
|
|
83
|
-
// Apollo doesn't allow to send batch queries for multiple schemas in the same batch
|
|
84
|
-
const tracesPerSchema = {};
|
|
85
92
|
for (const trace of reqCtx.traces.values()) {
|
|
86
93
|
if (!trace.schemaId || !trace.operationKey) {
|
|
87
94
|
logger.debug('Misformed trace, missing operation key or schema id');
|
|
@@ -95,62 +102,33 @@ function useApolloUsageReport(options = {}) {
|
|
|
95
102
|
if (clientVersion) {
|
|
96
103
|
trace.trace.clientVersion = clientVersion;
|
|
97
104
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
serverContext.waitUntil(reporter.addTrace(schema.id, {
|
|
106
|
+
statsReportKey: trace.operationKey,
|
|
107
|
+
trace: trace.trace,
|
|
108
|
+
referencedFieldsByType: trace.referencedFieldsByType,
|
|
109
|
+
asTrace: true, // TODO: allow to not always send traces
|
|
110
|
+
nonFtv1ErrorPaths: [],
|
|
111
|
+
maxTraceBytes: options.maxTraceSize,
|
|
112
|
+
}));
|
|
106
113
|
}
|
|
107
114
|
},
|
|
115
|
+
async onDispose() {
|
|
116
|
+
await reporter?.flush();
|
|
117
|
+
},
|
|
108
118
|
});
|
|
109
119
|
},
|
|
110
120
|
};
|
|
111
121
|
}
|
|
112
|
-
function hashSHA256(text, api = globalThis) {
|
|
122
|
+
async function hashSHA256(text, api = globalThis) {
|
|
113
123
|
const inputUint8Array = new api.TextEncoder().encode(text);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
function sendTrace(options, logger, fetch, schemaId, tracesPerQuery, agentVersion) {
|
|
125
|
-
const { graphRef = getEnvVar('APOLLO_GRAPH_REF'), apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT, } = options;
|
|
126
|
-
const body = usage_reporting_protobuf_1.Report.encode({
|
|
127
|
-
header: {
|
|
128
|
-
agentVersion,
|
|
129
|
-
graphRef,
|
|
130
|
-
executableSchemaId: schemaId,
|
|
131
|
-
},
|
|
132
|
-
operationCount: 1,
|
|
133
|
-
tracesPerQuery,
|
|
134
|
-
}).finish();
|
|
135
|
-
return (0, promise_helpers_1.handleMaybePromise)(() => fetch(endpoint, {
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: {
|
|
138
|
-
'content-type': 'application/protobuf',
|
|
139
|
-
// The presence of the api key is already checked at Yoga initialization time
|
|
140
|
-
'x-api-key': apiKey,
|
|
141
|
-
accept: 'application/json',
|
|
142
|
-
},
|
|
143
|
-
body,
|
|
144
|
-
}), response => (0, promise_helpers_1.handleMaybePromise)(() => response.text(), responseText => {
|
|
145
|
-
if (response.ok) {
|
|
146
|
-
logger.debug('Traces sent:', responseText);
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
logger.error('Failed to send trace:', responseText);
|
|
150
|
-
}
|
|
151
|
-
}), err => {
|
|
152
|
-
logger.error('Failed to send trace:', err);
|
|
153
|
-
});
|
|
124
|
+
const arrayBuf = await api.crypto.subtle.digest({ name: 'SHA-256' }, inputUint8Array);
|
|
125
|
+
const outputUint8Array = new Uint8Array(arrayBuf);
|
|
126
|
+
let hash = '';
|
|
127
|
+
for (const byte of outputUint8Array) {
|
|
128
|
+
const hex = byte.toString(16);
|
|
129
|
+
hash += '00'.slice(0, Math.max(0, 2 - hex.length)) + hex;
|
|
130
|
+
}
|
|
131
|
+
return hash;
|
|
154
132
|
}
|
|
155
133
|
function isDocumentNode(data) {
|
|
156
134
|
const isObject = (data) => !!data && typeof data === 'object';
|
package/cjs/reporter.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Reporter = void 0;
|
|
4
|
+
exports.getEnvVar = getEnvVar;
|
|
5
|
+
exports.dateToProtoTimestamp = dateToProtoTimestamp;
|
|
6
|
+
const usage_reporting_protobuf_1 = require("@apollo/usage-reporting-protobuf");
|
|
7
|
+
const stats_js_1 = require("./stats.js");
|
|
8
|
+
const DEFAULT_REPORTING_ENDPOINT = 'https://usage-reporting.api.apollographql.com/api/ingress/traces';
|
|
9
|
+
class Reporter {
|
|
10
|
+
#yoga;
|
|
11
|
+
#logger = console;
|
|
12
|
+
#reportHeaders;
|
|
13
|
+
#options;
|
|
14
|
+
#reportsBySchema = {};
|
|
15
|
+
#nextSendAfterDelay;
|
|
16
|
+
#sending = [];
|
|
17
|
+
constructor(options, yoga, logger) {
|
|
18
|
+
this.#logger = logger;
|
|
19
|
+
this.#yoga = yoga;
|
|
20
|
+
this.#options = {
|
|
21
|
+
...options,
|
|
22
|
+
maxBatchDelay: options.maxBatchDelay ?? 20 * 60 * 1000, // 20min
|
|
23
|
+
maxBatchUncompressedSize: options.maxBatchUncompressedSize ?? 4 * 1024 * 1024, // 4mb
|
|
24
|
+
maxTraceSize: options.maxTraceSize ?? 10 * 1024 * 1024, // 10mb
|
|
25
|
+
exportTimeout: options.exportTimeout ?? 30 * 1000, // 60s
|
|
26
|
+
};
|
|
27
|
+
this.#reportHeaders = {
|
|
28
|
+
graphRef: getGraphRef(options),
|
|
29
|
+
hostname: options.hostname ?? getEnvVar('HOSTNAME') ?? '',
|
|
30
|
+
uname: options.uname ?? '', // TODO: find a cross-platform way to get the uname
|
|
31
|
+
runtimeVersion: options.runtimeVersion ?? '',
|
|
32
|
+
agentVersion: options.agentVersion || `graphql-yoga@${yoga.version}`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
addTrace(schemaId, options) {
|
|
36
|
+
const report = this.#getReport(schemaId);
|
|
37
|
+
report.addTrace(options);
|
|
38
|
+
if (this.#options.alwaysSend ||
|
|
39
|
+
report.sizeEstimator.bytes >= this.#options.maxBatchUncompressedSize) {
|
|
40
|
+
return this.sendReport(schemaId);
|
|
41
|
+
}
|
|
42
|
+
this.#nextSendAfterDelay ||= setTimeout(() => this.flush(), this.#options.maxBatchDelay);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
async flush() {
|
|
46
|
+
return Promise.allSettled([
|
|
47
|
+
...this.#sending, // When flushing, we want to also wait for previous traces to be sent, because it's mostly used for clean up
|
|
48
|
+
...Object.keys(this.#reportsBySchema).map(schemaId => this.sendReport(schemaId)),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
async sendReport(schemaId) {
|
|
52
|
+
const sending = this.#sendReport(schemaId);
|
|
53
|
+
this.#sending.push(sending);
|
|
54
|
+
sending.finally(() => this.#sending?.filter(p => p !== sending));
|
|
55
|
+
return sending;
|
|
56
|
+
}
|
|
57
|
+
async #sendReport(schemaId) {
|
|
58
|
+
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.#yoga;
|
|
59
|
+
const report = this.#reportsBySchema[schemaId];
|
|
60
|
+
if (!report) {
|
|
61
|
+
throw new Error(`No report to send for schema ${schemaId}`);
|
|
62
|
+
}
|
|
63
|
+
if (this.#nextSendAfterDelay != null) {
|
|
64
|
+
clearTimeout(this.#nextSendAfterDelay);
|
|
65
|
+
}
|
|
66
|
+
delete this.#reportsBySchema[schemaId];
|
|
67
|
+
report.endTime = dateToProtoTimestamp(new Date());
|
|
68
|
+
report.ensureCountsAreIntegers();
|
|
69
|
+
const validationError = usage_reporting_protobuf_1.Report.verify(report);
|
|
70
|
+
if (!validationError) {
|
|
71
|
+
throw new TypeError(`Invalid report: ${validationError}`);
|
|
72
|
+
}
|
|
73
|
+
const encodedReport = usage_reporting_protobuf_1.Report.encode(report).finish();
|
|
74
|
+
const compressedReport = new ReadableStream({
|
|
75
|
+
start(controller) {
|
|
76
|
+
controller.enqueue(encodedReport);
|
|
77
|
+
controller.close();
|
|
78
|
+
},
|
|
79
|
+
}).pipeThrough(new CompressionStream('gzip'));
|
|
80
|
+
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this.#options;
|
|
81
|
+
for (let tries = 0; tries < 5; tries++) {
|
|
82
|
+
try {
|
|
83
|
+
const abortCtl = new AbortController();
|
|
84
|
+
this.#logger?.debug(`Sending report (try ${tries}/5)`);
|
|
85
|
+
const timeout = setTimeout(() => abortCtl.abort(), this.#options.exportTimeout);
|
|
86
|
+
const response = await fetch(endpoint, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
'content-type': 'application/protobuf',
|
|
90
|
+
'content-encoding': 'gzip',
|
|
91
|
+
// The presence of the api key is already checked at Yoga initialization time
|
|
92
|
+
'x-api-key': apiKey,
|
|
93
|
+
accept: 'application/json',
|
|
94
|
+
},
|
|
95
|
+
body: compressedReport,
|
|
96
|
+
signal: abortCtl.signal,
|
|
97
|
+
});
|
|
98
|
+
clearTimeout(timeout);
|
|
99
|
+
const result = await response.text();
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
this.#logger?.debug('Report sent:', result);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
throw result;
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
this.#logger?.error('Failed to send report:', err);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
throw new Error('Failed to send traces after 5 tries');
|
|
111
|
+
}
|
|
112
|
+
#getReport(schemaId) {
|
|
113
|
+
const report = this.#reportsBySchema[schemaId];
|
|
114
|
+
if (report) {
|
|
115
|
+
return report;
|
|
116
|
+
}
|
|
117
|
+
return (this.#reportsBySchema[schemaId] = new stats_js_1.OurReport(new usage_reporting_protobuf_1.ReportHeader({
|
|
118
|
+
...this.#reportHeaders,
|
|
119
|
+
executableSchemaId: schemaId,
|
|
120
|
+
})));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.Reporter = Reporter;
|
|
124
|
+
function getGraphRef(options) {
|
|
125
|
+
const graphRef = options.graphRef || getEnvVar('APOLLO_GRAPH_REF');
|
|
126
|
+
if (!graphRef) {
|
|
127
|
+
throw new Error('Missing GraphRef. Either provide `graphRef` option or `APOLLO_GRAPH_REF` environment variable');
|
|
128
|
+
}
|
|
129
|
+
return graphRef;
|
|
130
|
+
}
|
|
131
|
+
function getEnvVar(name, defaultValue) {
|
|
132
|
+
return globalThis.process?.env?.[name] || defaultValue || undefined;
|
|
133
|
+
}
|
|
134
|
+
// Converts a JS Date into a Timestamp.
|
|
135
|
+
function dateToProtoTimestamp(date) {
|
|
136
|
+
const totalMillis = date.getTime();
|
|
137
|
+
const millis = totalMillis % 1000;
|
|
138
|
+
return new usage_reporting_protobuf_1.google.protobuf.Timestamp({
|
|
139
|
+
seconds: (totalMillis - millis) / 1000,
|
|
140
|
+
nanos: millis * 1e6,
|
|
141
|
+
});
|
|
142
|
+
}
|