@graphql-yoga/plugin-apollo-usage-report 0.13.0 → 0.13.1-alpha-20260116132831-dc9fc0ad2f1ad6ee99bc438c173cf8496ae505a7
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/dist/index.cjs +152 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.mts +125 -0
- package/dist/index.mjs +151 -0
- package/dist/reporter.cjs +118 -0
- package/dist/reporter.d.cts +23 -0
- package/dist/reporter.d.mts +23 -0
- package/dist/reporter.mjs +117 -0
- package/dist/stats.cjs +344 -0
- package/dist/stats.d.cts +123 -0
- package/dist/stats.d.mts +123 -0
- package/dist/stats.mjs +344 -0
- package/package.json +53 -28
- package/LICENSE +0 -23
- package/cjs/index.js +0 -186
- package/cjs/package.json +0 -1
- package/cjs/reporter.js +0 -142
- package/cjs/stats.js +0 -547
- package/esm/index.js +0 -182
- package/esm/reporter.js +0 -136
- package/esm/stats.js +0 -539
- package/typings/index.d.cts +0 -122
- package/typings/index.d.ts +0 -122
- package/typings/reporter.d.cts +0 -21
- package/typings/reporter.d.ts +0 -21
- package/typings/stats.d.cts +0 -136
- package/typings/stats.d.ts +0 -136
package/esm/index.js
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import { getOperationAST, Kind } from 'graphql';
|
|
2
|
-
import { isAsyncIterable, } from 'graphql-yoga';
|
|
3
|
-
import { calculateReferencedFieldsByType, usageReportingSignature, } from '@apollo/utils.usagereporting';
|
|
4
|
-
import { printSchemaWithDirectives } from '@graphql-tools/utils';
|
|
5
|
-
import { useApolloInstrumentation, } from '@graphql-yoga/plugin-apollo-inline-trace';
|
|
6
|
-
import { getEnvVar, Reporter } from './reporter.js';
|
|
7
|
-
export function useApolloUsageReport(options = {}) {
|
|
8
|
-
const [instrumentation, ctxForReq] = useApolloInstrumentation(options);
|
|
9
|
-
const makeReporter = options.reporter ?? ((...args) => new Reporter(...args));
|
|
10
|
-
let schemaIdSet$;
|
|
11
|
-
let currentSchema;
|
|
12
|
-
let yoga;
|
|
13
|
-
let reporter;
|
|
14
|
-
const setCurrentSchema = async (schema) => {
|
|
15
|
-
try {
|
|
16
|
-
currentSchema = {
|
|
17
|
-
id: await hashSHA256(printSchemaWithDirectives(schema), yoga.fetchAPI),
|
|
18
|
-
schema,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
logger.error('Failed to calculate schema hash: ', error);
|
|
23
|
-
}
|
|
24
|
-
// We don't want to block server start even if we failed to compute schema id
|
|
25
|
-
schemaIdSet$ = undefined;
|
|
26
|
-
};
|
|
27
|
-
const logger = Object.fromEntries(['error', 'warn', 'info', 'debug'].map(level => [
|
|
28
|
-
level,
|
|
29
|
-
(...messages) => yoga.logger[level]('[ApolloUsageReport]', ...messages),
|
|
30
|
-
]));
|
|
31
|
-
let clientNameFactory = req => req.headers.get('apollographql-client-name');
|
|
32
|
-
if (typeof options.clientName === 'function') {
|
|
33
|
-
clientNameFactory = options.clientName;
|
|
34
|
-
}
|
|
35
|
-
let clientVersionFactory = req => req.headers.get('apollographql-client-version');
|
|
36
|
-
if (typeof options.clientVersion === 'function') {
|
|
37
|
-
clientVersionFactory = options.clientVersion;
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
onPluginInit({ addPlugin }) {
|
|
41
|
-
addPlugin(instrumentation);
|
|
42
|
-
addPlugin({
|
|
43
|
-
onYogaInit(args) {
|
|
44
|
-
yoga = args.yoga;
|
|
45
|
-
reporter = makeReporter(options, yoga, logger);
|
|
46
|
-
if (!getEnvVar('APOLLO_KEY', options.apiKey)) {
|
|
47
|
-
throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`);
|
|
48
|
-
}
|
|
49
|
-
if (!getEnvVar('APOLLO_GRAPH_REF', options.graphRef)) {
|
|
50
|
-
throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`);
|
|
51
|
-
}
|
|
52
|
-
if (!schemaIdSet$ && !currentSchema) {
|
|
53
|
-
// When the schema is static, the `onSchemaChange` hook is called before initialization
|
|
54
|
-
// We have to handle schema loading here in this case.
|
|
55
|
-
const { schema } = yoga.getEnveloped();
|
|
56
|
-
if (schema) {
|
|
57
|
-
schemaIdSet$ = setCurrentSchema(schema);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
onSchemaChange({ schema }) {
|
|
62
|
-
// When the schema is static, this hook is called before yoga initialization
|
|
63
|
-
// Since we need yoga.fetchAPI for id calculation, we need to wait for Yoga init
|
|
64
|
-
if (schema && yoga) {
|
|
65
|
-
schemaIdSet$ = setCurrentSchema(schema);
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
onRequestParse() {
|
|
69
|
-
return schemaIdSet$;
|
|
70
|
-
},
|
|
71
|
-
onParse() {
|
|
72
|
-
return function onParseEnd({ result, context }) {
|
|
73
|
-
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
74
|
-
if (!ctx) {
|
|
75
|
-
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
ctx.schemaId = currentSchema.id;
|
|
79
|
-
if (isDocumentNode(result)) {
|
|
80
|
-
if (getOperationAST(result, context.params.operationName)) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
ctx.operationKey = `## GraphQLUnknownOperationName\n`;
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
ctx.operationKey = '## GraphQLParseFailure\n';
|
|
87
|
-
}
|
|
88
|
-
if (!options.sendUnexecutableOperationDocuments) {
|
|
89
|
-
// To make sure the trace will not be sent, remove request's tracing context
|
|
90
|
-
ctxForReq.delete(context.request);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
ctx.trace.unexecutedOperationName = context.params.operationName || '';
|
|
94
|
-
ctx.trace.unexecutedOperationBody = context.params.query || '';
|
|
95
|
-
};
|
|
96
|
-
},
|
|
97
|
-
onValidate({ params: { documentAST: document } }) {
|
|
98
|
-
return ({ valid, context }) => {
|
|
99
|
-
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
100
|
-
if (!ctx) {
|
|
101
|
-
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (valid) {
|
|
105
|
-
if (!currentSchema) {
|
|
106
|
-
throw new Error("should not happen: schema doesn't exists");
|
|
107
|
-
}
|
|
108
|
-
const opName = getOperationAST(document, context.params.operationName)?.name?.value;
|
|
109
|
-
ctx.referencedFieldsByType = calculateReferencedFieldsByType({
|
|
110
|
-
document,
|
|
111
|
-
schema: currentSchema.schema,
|
|
112
|
-
resolvedOperationName: opName ?? null,
|
|
113
|
-
});
|
|
114
|
-
ctx.operationKey = `# ${opName || '-'}\n${usageReportingSignature(document, opName ?? '')}`;
|
|
115
|
-
}
|
|
116
|
-
else if (options.sendUnexecutableOperationDocuments) {
|
|
117
|
-
ctx.operationKey = '## GraphQLValidationFailure\n';
|
|
118
|
-
ctx.trace.unexecutedOperationName = context.params.operationName ?? '';
|
|
119
|
-
ctx.trace.unexecutedOperationBody = context.params.query ?? '';
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
// To make sure the trace will not be sent, remove request's tracing context
|
|
123
|
-
ctxForReq.delete(context.request);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
},
|
|
127
|
-
onResultProcess({ request, result, serverContext }) {
|
|
128
|
-
// TODO: Handle async iterables ?
|
|
129
|
-
if (isAsyncIterable(result)) {
|
|
130
|
-
logger.debug('async iterable results not implemented for now');
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const reqCtx = ctxForReq.get(request);
|
|
134
|
-
if (!reqCtx) {
|
|
135
|
-
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
for (const trace of reqCtx.traces.values()) {
|
|
139
|
-
if (!trace.schemaId || !trace.operationKey) {
|
|
140
|
-
logger.debug('Misformed trace, missing operation key or schema id');
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
const clientName = clientNameFactory(request);
|
|
144
|
-
if (clientName) {
|
|
145
|
-
trace.trace.clientName = clientName;
|
|
146
|
-
}
|
|
147
|
-
const clientVersion = clientVersionFactory(request);
|
|
148
|
-
if (clientVersion) {
|
|
149
|
-
trace.trace.clientVersion = clientVersion;
|
|
150
|
-
}
|
|
151
|
-
serverContext.waitUntil(reporter.addTrace(currentSchema.id, {
|
|
152
|
-
statsReportKey: trace.operationKey,
|
|
153
|
-
trace: trace.trace,
|
|
154
|
-
referencedFieldsByType: trace.referencedFieldsByType ?? {},
|
|
155
|
-
asTrace: true, // TODO: allow to not always send traces
|
|
156
|
-
nonFtv1ErrorPaths: [],
|
|
157
|
-
maxTraceBytes: options.maxTraceSize,
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
async onDispose() {
|
|
162
|
-
await reporter?.flush();
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
},
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
export async function hashSHA256(text, api = globalThis) {
|
|
169
|
-
const inputUint8Array = new api.TextEncoder().encode(text);
|
|
170
|
-
const arrayBuf = await api.crypto.subtle.digest({ name: 'SHA-256' }, inputUint8Array);
|
|
171
|
-
const outputUint8Array = new Uint8Array(arrayBuf);
|
|
172
|
-
let hash = '';
|
|
173
|
-
for (const byte of outputUint8Array) {
|
|
174
|
-
const hex = byte.toString(16);
|
|
175
|
-
hash += '00'.slice(0, Math.max(0, 2 - hex.length)) + hex;
|
|
176
|
-
}
|
|
177
|
-
return hash;
|
|
178
|
-
}
|
|
179
|
-
function isDocumentNode(data) {
|
|
180
|
-
const isObject = (data) => !!data && typeof data === 'object';
|
|
181
|
-
return isObject(data) && data['kind'] === Kind.DOCUMENT;
|
|
182
|
-
}
|
package/esm/reporter.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { google, Report, ReportHeader } from '@apollo/usage-reporting-protobuf';
|
|
2
|
-
import { OurReport } from './stats.js';
|
|
3
|
-
const DEFAULT_REPORTING_ENDPOINT = 'https://usage-reporting.api.apollographql.com/api/ingress/traces';
|
|
4
|
-
export class Reporter {
|
|
5
|
-
yoga;
|
|
6
|
-
logger;
|
|
7
|
-
reportHeaders;
|
|
8
|
-
options;
|
|
9
|
-
reportsBySchema = {};
|
|
10
|
-
nextSendAfterDelay;
|
|
11
|
-
sending = [];
|
|
12
|
-
constructor(options, yoga, logger) {
|
|
13
|
-
this.yoga = yoga;
|
|
14
|
-
this.logger = logger;
|
|
15
|
-
this.options = {
|
|
16
|
-
...options,
|
|
17
|
-
maxBatchDelay: options.maxBatchDelay ?? 20_000, // 20s
|
|
18
|
-
maxBatchUncompressedSize: options.maxBatchUncompressedSize ?? 4 * 1024 * 1024, // 4mb
|
|
19
|
-
maxTraceSize: options.maxTraceSize ?? 10 * 1024 * 1024, // 10mb
|
|
20
|
-
exportTimeout: options.exportTimeout ?? 30_000, // 30s
|
|
21
|
-
onError: options.onError ?? (err => this.logger.error('Failed to send report', err)),
|
|
22
|
-
};
|
|
23
|
-
this.reportHeaders = {
|
|
24
|
-
graphRef: getGraphRef(options),
|
|
25
|
-
hostname: options.hostname ?? getEnvVar('HOSTNAME') ?? '',
|
|
26
|
-
uname: options.uname ?? '', // TODO: find a cross-platform way to get the uname
|
|
27
|
-
runtimeVersion: options.runtimeVersion ?? '',
|
|
28
|
-
agentVersion: options.agentVersion || `graphql-yoga@${yoga.version}`,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
addTrace(schemaId, options) {
|
|
32
|
-
const report = this.getReport(schemaId);
|
|
33
|
-
report.addTrace(options);
|
|
34
|
-
if (this.options.alwaysSend ||
|
|
35
|
-
report.sizeEstimator.bytes >= this.options.maxBatchUncompressedSize) {
|
|
36
|
-
return this._sendReport(schemaId);
|
|
37
|
-
}
|
|
38
|
-
this.nextSendAfterDelay ||= setTimeout(() => this.flush(), this.options.maxBatchDelay);
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
async flush() {
|
|
42
|
-
return Promise.allSettled([
|
|
43
|
-
...this.sending, // When flushing, we want to also wait for previous traces to be sent, because it's mostly used for clean up
|
|
44
|
-
...Object.keys(this.reportsBySchema).map(schemaId => this._sendReport(schemaId)),
|
|
45
|
-
]);
|
|
46
|
-
}
|
|
47
|
-
async sendReport(schemaId) {
|
|
48
|
-
const sending = this._sendReport(schemaId);
|
|
49
|
-
this.sending.push(sending);
|
|
50
|
-
sending.finally(() => (this.sending = this.sending?.filter(p => p !== sending)));
|
|
51
|
-
return sending;
|
|
52
|
-
}
|
|
53
|
-
async _sendReport(schemaId) {
|
|
54
|
-
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.yoga;
|
|
55
|
-
const report = this.reportsBySchema[schemaId];
|
|
56
|
-
if (!report) {
|
|
57
|
-
throw new Error(`No report to send for schema ${schemaId}`);
|
|
58
|
-
}
|
|
59
|
-
if (this.nextSendAfterDelay != null) {
|
|
60
|
-
clearTimeout(this.nextSendAfterDelay);
|
|
61
|
-
this.nextSendAfterDelay = undefined;
|
|
62
|
-
}
|
|
63
|
-
delete this.reportsBySchema[schemaId];
|
|
64
|
-
report.endTime = dateToProtoTimestamp(new Date());
|
|
65
|
-
report.ensureCountsAreIntegers();
|
|
66
|
-
const validationError = Report.verify(report);
|
|
67
|
-
if (validationError) {
|
|
68
|
-
throw new TypeError(`Invalid report: ${validationError}`);
|
|
69
|
-
}
|
|
70
|
-
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this.options;
|
|
71
|
-
const encodedReport = Report.encode(report).finish();
|
|
72
|
-
let lastError;
|
|
73
|
-
for (let tries = 0; tries < 5; tries++) {
|
|
74
|
-
try {
|
|
75
|
-
this.logger.debug(`Sending report (try ${tries}/5)`);
|
|
76
|
-
const response = await fetch(endpoint, {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers: {
|
|
79
|
-
'content-type': 'application/protobuf',
|
|
80
|
-
'content-encoding': 'gzip',
|
|
81
|
-
// The presence of the api key is already checked at Yoga initialization time
|
|
82
|
-
'x-api-key': apiKey,
|
|
83
|
-
accept: 'application/json',
|
|
84
|
-
},
|
|
85
|
-
body: new ReadableStream({
|
|
86
|
-
start(controller) {
|
|
87
|
-
controller.enqueue(encodedReport);
|
|
88
|
-
controller.close();
|
|
89
|
-
},
|
|
90
|
-
}).pipeThrough(new CompressionStream('gzip')),
|
|
91
|
-
signal: AbortSignal.timeout(this.options.exportTimeout),
|
|
92
|
-
});
|
|
93
|
-
const result = await response.text();
|
|
94
|
-
if (response.ok) {
|
|
95
|
-
this.logger.debug('Report sent:', result);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
throw result;
|
|
99
|
-
}
|
|
100
|
-
catch (err) {
|
|
101
|
-
lastError = err;
|
|
102
|
-
this.logger.error('Failed to send report:', err);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
this.options.onError(new Error('Failed to send traces after 5 tries', { cause: lastError }));
|
|
106
|
-
}
|
|
107
|
-
getReport(schemaId) {
|
|
108
|
-
const report = this.reportsBySchema[schemaId];
|
|
109
|
-
if (report) {
|
|
110
|
-
return report;
|
|
111
|
-
}
|
|
112
|
-
return (this.reportsBySchema[schemaId] = new OurReport(new ReportHeader({
|
|
113
|
-
...this.reportHeaders,
|
|
114
|
-
executableSchemaId: schemaId,
|
|
115
|
-
})));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function getGraphRef(options) {
|
|
119
|
-
const graphRef = options.graphRef || getEnvVar('APOLLO_GRAPH_REF');
|
|
120
|
-
if (!graphRef) {
|
|
121
|
-
throw new Error('Missing GraphRef. Either provide `graphRef` option or `APOLLO_GRAPH_REF` environment variable');
|
|
122
|
-
}
|
|
123
|
-
return graphRef;
|
|
124
|
-
}
|
|
125
|
-
export function getEnvVar(name, defaultValue) {
|
|
126
|
-
return globalThis.process?.env?.[name] || defaultValue || undefined;
|
|
127
|
-
}
|
|
128
|
-
// Converts a JS Date into a Timestamp.
|
|
129
|
-
export function dateToProtoTimestamp(date) {
|
|
130
|
-
const totalMillis = date.getTime();
|
|
131
|
-
const millis = totalMillis % 1000;
|
|
132
|
-
return new google.protobuf.Timestamp({
|
|
133
|
-
seconds: (totalMillis - millis) / 1000,
|
|
134
|
-
nanos: millis * 1e6,
|
|
135
|
-
});
|
|
136
|
-
}
|