@graphql-yoga/plugin-apollo-usage-report 0.13.0 → 0.13.1-alpha-20260116132159-d18a95d04a1e11d197fdf672a8be4836ceed0818

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