@graphql-yoga/plugin-apollo-usage-report 0.9.0-alpha-20250527093154-d0be5f4626600bfbe255c0ab57590d3963013823 → 0.9.0-alpha-20250602083944-892c9d0b95711d54bb94b0197f51122b6e7236af
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 +52 -21
- package/cjs/reporter.js +41 -38
- package/esm/index.js +53 -22
- package/esm/reporter.js +41 -38
- package/package.json +2 -2
- package/typings/index.d.cts +13 -2
- package/typings/index.d.ts +13 -2
- package/typings/reporter.d.cts +9 -1
- package/typings/reporter.d.ts +9 -1
package/cjs/index.js
CHANGED
|
@@ -10,10 +10,24 @@ const plugin_apollo_inline_trace_1 = require("@graphql-yoga/plugin-apollo-inline
|
|
|
10
10
|
const reporter_js_1 = require("./reporter.js");
|
|
11
11
|
function useApolloUsageReport(options = {}) {
|
|
12
12
|
const [instrumentation, ctxForReq] = (0, plugin_apollo_inline_trace_1.useApolloInstrumentation)(options);
|
|
13
|
+
const makeReporter = options.reporter ?? ((...args) => new reporter_js_1.Reporter(...args));
|
|
13
14
|
let schemaIdSet$;
|
|
14
15
|
let currentSchema;
|
|
15
16
|
let yoga;
|
|
16
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
|
+
};
|
|
17
31
|
const logger = Object.fromEntries(['error', 'warn', 'info', 'debug'].map(level => [
|
|
18
32
|
level,
|
|
19
33
|
(...messages) => yoga.logger[level]('[ApolloUsageReport]', ...messages),
|
|
@@ -29,53 +43,70 @@ function useApolloUsageReport(options = {}) {
|
|
|
29
43
|
return {
|
|
30
44
|
onPluginInit({ addPlugin }) {
|
|
31
45
|
addPlugin(instrumentation);
|
|
32
|
-
addPlugin({
|
|
46
|
+
addPlugin((0, utils_1.withState)(() => ({
|
|
33
47
|
onYogaInit(args) {
|
|
34
48
|
yoga = args.yoga;
|
|
35
|
-
reporter =
|
|
49
|
+
reporter = makeReporter(options, yoga, logger);
|
|
36
50
|
if (!(0, reporter_js_1.getEnvVar)('APOLLO_KEY', options.apiKey)) {
|
|
37
51
|
throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`);
|
|
38
52
|
}
|
|
39
53
|
if (!(0, reporter_js_1.getEnvVar)('APOLLO_GRAPH_REF', options.graphRef)) {
|
|
40
54
|
throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`);
|
|
41
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
|
+
}
|
|
42
64
|
},
|
|
43
65
|
onSchemaChange({ schema }) {
|
|
44
|
-
if (schema
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
schemaIdSet$ = undefined;
|
|
49
|
-
})
|
|
50
|
-
.catch(error => {
|
|
51
|
-
logger.error('Failed to calculate schema hash: ', error);
|
|
52
|
-
});
|
|
66
|
+
if (schema && // 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
|
+
yoga) {
|
|
69
|
+
schemaIdSet$ = setCurrentSchema(schema);
|
|
53
70
|
}
|
|
54
71
|
},
|
|
55
72
|
onRequestParse() {
|
|
56
73
|
return schemaIdSet$;
|
|
57
74
|
},
|
|
58
|
-
onParse() {
|
|
75
|
+
onParse({ state }) {
|
|
59
76
|
return function onParseEnd({ result, context }) {
|
|
60
|
-
if (!currentSchema) {
|
|
61
|
-
throw new Error("should not happen: schema doesn't exists");
|
|
62
|
-
}
|
|
63
77
|
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
64
78
|
if (!ctx) {
|
|
65
79
|
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
66
80
|
return;
|
|
67
81
|
}
|
|
68
82
|
ctx.schemaId = currentSchema.id;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
83
|
+
if (isDocumentNode(result)) {
|
|
84
|
+
state.forOperation.document = result;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
ctx.operationKey = `# ${context.params.operationName || '-'} \n${context.params.query ?? ''}`;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
onValidate({ state }) {
|
|
92
|
+
return ({ valid, context }) => {
|
|
93
|
+
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
94
|
+
if (!ctx) {
|
|
95
|
+
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (valid) {
|
|
99
|
+
if (!currentSchema) {
|
|
100
|
+
throw new Error("should not happen: schema doesn't exists");
|
|
101
|
+
}
|
|
102
|
+
const document = state.forOperation.document;
|
|
72
103
|
const opName = (0, graphql_1.getOperationAST)(document, context.params.operationName)?.name?.value;
|
|
73
104
|
ctx.referencedFieldsByType = (0, utils_usagereporting_1.calculateReferencedFieldsByType)({
|
|
74
105
|
document,
|
|
75
106
|
schema: currentSchema.schema,
|
|
76
107
|
resolvedOperationName: opName ?? null,
|
|
77
108
|
});
|
|
78
|
-
ctx.operationKey = `# ${opName || '-'}\n${
|
|
109
|
+
ctx.operationKey = `# ${opName || '-'}\n${(0, utils_usagereporting_1.usageReportingSignature)(document, opName ?? '')}`;
|
|
79
110
|
}
|
|
80
111
|
else {
|
|
81
112
|
ctx.operationKey = `# ${context.params.operationName || '-'} \n${context.params.query ?? ''}`;
|
|
@@ -109,7 +140,7 @@ function useApolloUsageReport(options = {}) {
|
|
|
109
140
|
serverContext.waitUntil(reporter.addTrace(currentSchema.id, {
|
|
110
141
|
statsReportKey: trace.operationKey,
|
|
111
142
|
trace: trace.trace,
|
|
112
|
-
referencedFieldsByType: trace.referencedFieldsByType,
|
|
143
|
+
referencedFieldsByType: trace.referencedFieldsByType ?? {},
|
|
113
144
|
asTrace: true, // TODO: allow to not always send traces
|
|
114
145
|
nonFtv1ErrorPaths: [],
|
|
115
146
|
maxTraceBytes: options.maxTraceSize,
|
|
@@ -119,7 +150,7 @@ function useApolloUsageReport(options = {}) {
|
|
|
119
150
|
async onDispose() {
|
|
120
151
|
await reporter?.flush();
|
|
121
152
|
},
|
|
122
|
-
});
|
|
153
|
+
})));
|
|
123
154
|
},
|
|
124
155
|
};
|
|
125
156
|
}
|
package/cjs/reporter.js
CHANGED
|
@@ -7,24 +7,25 @@ const usage_reporting_protobuf_1 = require("@apollo/usage-reporting-protobuf");
|
|
|
7
7
|
const stats_js_1 = require("./stats.js");
|
|
8
8
|
const DEFAULT_REPORTING_ENDPOINT = 'https://usage-reporting.api.apollographql.com/api/ingress/traces';
|
|
9
9
|
class Reporter {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
yoga;
|
|
11
|
+
logger;
|
|
12
|
+
reportHeaders;
|
|
13
|
+
options;
|
|
14
|
+
reportsBySchema = {};
|
|
15
|
+
nextSendAfterDelay;
|
|
16
|
+
sending = [];
|
|
17
17
|
constructor(options, yoga, logger) {
|
|
18
|
-
this
|
|
19
|
-
this
|
|
20
|
-
this
|
|
18
|
+
this.yoga = yoga;
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.options = {
|
|
21
21
|
...options,
|
|
22
22
|
maxBatchDelay: options.maxBatchDelay ?? 20_000, // 20s
|
|
23
23
|
maxBatchUncompressedSize: options.maxBatchUncompressedSize ?? 4 * 1024 * 1024, // 4mb
|
|
24
24
|
maxTraceSize: options.maxTraceSize ?? 10 * 1024 * 1024, // 10mb
|
|
25
25
|
exportTimeout: options.exportTimeout ?? 30_000, // 30s
|
|
26
|
+
onError: options.onError ?? (err => this.logger.error('Failed to send report', err)),
|
|
26
27
|
};
|
|
27
|
-
this
|
|
28
|
+
this.reportHeaders = {
|
|
28
29
|
graphRef: getGraphRef(options),
|
|
29
30
|
hostname: options.hostname ?? getEnvVar('HOSTNAME') ?? '',
|
|
30
31
|
uname: options.uname ?? '', // TODO: find a cross-platform way to get the uname
|
|
@@ -33,49 +34,50 @@ class Reporter {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
addTrace(schemaId, options) {
|
|
36
|
-
const report = this
|
|
37
|
+
const report = this.getReport(schemaId);
|
|
37
38
|
report.addTrace(options);
|
|
38
|
-
if (this
|
|
39
|
-
report.sizeEstimator.bytes >= this
|
|
40
|
-
return this.
|
|
39
|
+
if (this.options.alwaysSend ||
|
|
40
|
+
report.sizeEstimator.bytes >= this.options.maxBatchUncompressedSize) {
|
|
41
|
+
return this._sendReport(schemaId);
|
|
41
42
|
}
|
|
42
|
-
this
|
|
43
|
+
this.nextSendAfterDelay ||= setTimeout(() => this.flush(), this.options.maxBatchDelay);
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
async flush() {
|
|
46
47
|
return Promise.allSettled([
|
|
47
|
-
...this
|
|
48
|
-
...Object.keys(this
|
|
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)),
|
|
49
50
|
]);
|
|
50
51
|
}
|
|
51
52
|
async sendReport(schemaId) {
|
|
52
|
-
const sending = this
|
|
53
|
-
this
|
|
54
|
-
sending.finally(() => this
|
|
53
|
+
const sending = this._sendReport(schemaId);
|
|
54
|
+
this.sending.push(sending);
|
|
55
|
+
sending.finally(() => (this.sending = this.sending?.filter(p => p !== sending)));
|
|
55
56
|
return sending;
|
|
56
57
|
}
|
|
57
|
-
async
|
|
58
|
-
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this
|
|
59
|
-
const report = this
|
|
58
|
+
async _sendReport(schemaId) {
|
|
59
|
+
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.yoga;
|
|
60
|
+
const report = this.reportsBySchema[schemaId];
|
|
60
61
|
if (!report) {
|
|
61
62
|
throw new Error(`No report to send for schema ${schemaId}`);
|
|
62
63
|
}
|
|
63
|
-
if (this
|
|
64
|
-
clearTimeout(this
|
|
65
|
-
this
|
|
64
|
+
if (this.nextSendAfterDelay != null) {
|
|
65
|
+
clearTimeout(this.nextSendAfterDelay);
|
|
66
|
+
this.nextSendAfterDelay = undefined;
|
|
66
67
|
}
|
|
67
|
-
delete this
|
|
68
|
+
delete this.reportsBySchema[schemaId];
|
|
68
69
|
report.endTime = dateToProtoTimestamp(new Date());
|
|
69
70
|
report.ensureCountsAreIntegers();
|
|
70
71
|
const validationError = usage_reporting_protobuf_1.Report.verify(report);
|
|
71
72
|
if (validationError) {
|
|
72
73
|
throw new TypeError(`Invalid report: ${validationError}`);
|
|
73
74
|
}
|
|
74
|
-
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this
|
|
75
|
+
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this.options;
|
|
75
76
|
const encodedReport = usage_reporting_protobuf_1.Report.encode(report).finish();
|
|
77
|
+
let lastError;
|
|
76
78
|
for (let tries = 0; tries < 5; tries++) {
|
|
77
79
|
try {
|
|
78
|
-
this
|
|
80
|
+
this.logger.debug(`Sending report (try ${tries}/5)`);
|
|
79
81
|
const response = await fetch(endpoint, {
|
|
80
82
|
method: 'POST',
|
|
81
83
|
headers: {
|
|
@@ -91,28 +93,29 @@ class Reporter {
|
|
|
91
93
|
controller.close();
|
|
92
94
|
},
|
|
93
95
|
}).pipeThrough(new CompressionStream('gzip')),
|
|
94
|
-
signal: AbortSignal.timeout(this
|
|
96
|
+
signal: AbortSignal.timeout(this.options.exportTimeout),
|
|
95
97
|
});
|
|
96
98
|
const result = await response.text();
|
|
97
99
|
if (response.ok) {
|
|
98
|
-
this
|
|
100
|
+
this.logger.debug('Report sent:', result);
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
101
103
|
throw result;
|
|
102
104
|
}
|
|
103
105
|
catch (err) {
|
|
104
|
-
|
|
106
|
+
lastError = err;
|
|
107
|
+
this.logger.error('Failed to send report:', err);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
|
-
|
|
110
|
+
this.options.onError(new Error('Failed to send traces after 5 tries', { cause: lastError }));
|
|
108
111
|
}
|
|
109
|
-
|
|
110
|
-
const report = this
|
|
112
|
+
getReport(schemaId) {
|
|
113
|
+
const report = this.reportsBySchema[schemaId];
|
|
111
114
|
if (report) {
|
|
112
115
|
return report;
|
|
113
116
|
}
|
|
114
|
-
return (this
|
|
115
|
-
...this
|
|
117
|
+
return (this.reportsBySchema[schemaId] = new stats_js_1.OurReport(new usage_reporting_protobuf_1.ReportHeader({
|
|
118
|
+
...this.reportHeaders,
|
|
116
119
|
executableSchemaId: schemaId,
|
|
117
120
|
})));
|
|
118
121
|
}
|
package/esm/index.js
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { getOperationAST, Kind } from 'graphql';
|
|
2
2
|
import { isAsyncIterable, } from 'graphql-yoga';
|
|
3
3
|
import { calculateReferencedFieldsByType, usageReportingSignature, } from '@apollo/utils.usagereporting';
|
|
4
|
-
import { printSchemaWithDirectives } from '@graphql-tools/utils';
|
|
4
|
+
import { printSchemaWithDirectives, withState } from '@graphql-tools/utils';
|
|
5
5
|
import { useApolloInstrumentation, } from '@graphql-yoga/plugin-apollo-inline-trace';
|
|
6
6
|
import { getEnvVar, Reporter } from './reporter.js';
|
|
7
7
|
export function useApolloUsageReport(options = {}) {
|
|
8
8
|
const [instrumentation, ctxForReq] = useApolloInstrumentation(options);
|
|
9
|
+
const makeReporter = options.reporter ?? ((...args) => new Reporter(...args));
|
|
9
10
|
let schemaIdSet$;
|
|
10
11
|
let currentSchema;
|
|
11
12
|
let yoga;
|
|
12
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
|
+
};
|
|
13
27
|
const logger = Object.fromEntries(['error', 'warn', 'info', 'debug'].map(level => [
|
|
14
28
|
level,
|
|
15
29
|
(...messages) => yoga.logger[level]('[ApolloUsageReport]', ...messages),
|
|
@@ -25,53 +39,70 @@ export function useApolloUsageReport(options = {}) {
|
|
|
25
39
|
return {
|
|
26
40
|
onPluginInit({ addPlugin }) {
|
|
27
41
|
addPlugin(instrumentation);
|
|
28
|
-
addPlugin({
|
|
42
|
+
addPlugin(withState(() => ({
|
|
29
43
|
onYogaInit(args) {
|
|
30
44
|
yoga = args.yoga;
|
|
31
|
-
reporter =
|
|
45
|
+
reporter = makeReporter(options, yoga, logger);
|
|
32
46
|
if (!getEnvVar('APOLLO_KEY', options.apiKey)) {
|
|
33
47
|
throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`);
|
|
34
48
|
}
|
|
35
49
|
if (!getEnvVar('APOLLO_GRAPH_REF', options.graphRef)) {
|
|
36
50
|
throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`);
|
|
37
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
|
+
}
|
|
38
60
|
},
|
|
39
61
|
onSchemaChange({ schema }) {
|
|
40
|
-
if (schema
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
schemaIdSet$ = undefined;
|
|
45
|
-
})
|
|
46
|
-
.catch(error => {
|
|
47
|
-
logger.error('Failed to calculate schema hash: ', error);
|
|
48
|
-
});
|
|
62
|
+
if (schema && // 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
|
+
yoga) {
|
|
65
|
+
schemaIdSet$ = setCurrentSchema(schema);
|
|
49
66
|
}
|
|
50
67
|
},
|
|
51
68
|
onRequestParse() {
|
|
52
69
|
return schemaIdSet$;
|
|
53
70
|
},
|
|
54
|
-
onParse() {
|
|
71
|
+
onParse({ state }) {
|
|
55
72
|
return function onParseEnd({ result, context }) {
|
|
56
|
-
if (!currentSchema) {
|
|
57
|
-
throw new Error("should not happen: schema doesn't exists");
|
|
58
|
-
}
|
|
59
73
|
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
60
74
|
if (!ctx) {
|
|
61
75
|
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
62
76
|
return;
|
|
63
77
|
}
|
|
64
78
|
ctx.schemaId = currentSchema.id;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
if (isDocumentNode(result)) {
|
|
80
|
+
state.forOperation.document = result;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
ctx.operationKey = `# ${context.params.operationName || '-'} \n${context.params.query ?? ''}`;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
onValidate({ state }) {
|
|
88
|
+
return ({ valid, context }) => {
|
|
89
|
+
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
90
|
+
if (!ctx) {
|
|
91
|
+
logger.debug('operation tracing context not found, this operation will not be traced.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (valid) {
|
|
95
|
+
if (!currentSchema) {
|
|
96
|
+
throw new Error("should not happen: schema doesn't exists");
|
|
97
|
+
}
|
|
98
|
+
const document = state.forOperation.document;
|
|
68
99
|
const opName = getOperationAST(document, context.params.operationName)?.name?.value;
|
|
69
100
|
ctx.referencedFieldsByType = calculateReferencedFieldsByType({
|
|
70
101
|
document,
|
|
71
102
|
schema: currentSchema.schema,
|
|
72
103
|
resolvedOperationName: opName ?? null,
|
|
73
104
|
});
|
|
74
|
-
ctx.operationKey = `# ${opName || '-'}\n${
|
|
105
|
+
ctx.operationKey = `# ${opName || '-'}\n${usageReportingSignature(document, opName ?? '')}`;
|
|
75
106
|
}
|
|
76
107
|
else {
|
|
77
108
|
ctx.operationKey = `# ${context.params.operationName || '-'} \n${context.params.query ?? ''}`;
|
|
@@ -105,7 +136,7 @@ export function useApolloUsageReport(options = {}) {
|
|
|
105
136
|
serverContext.waitUntil(reporter.addTrace(currentSchema.id, {
|
|
106
137
|
statsReportKey: trace.operationKey,
|
|
107
138
|
trace: trace.trace,
|
|
108
|
-
referencedFieldsByType: trace.referencedFieldsByType,
|
|
139
|
+
referencedFieldsByType: trace.referencedFieldsByType ?? {},
|
|
109
140
|
asTrace: true, // TODO: allow to not always send traces
|
|
110
141
|
nonFtv1ErrorPaths: [],
|
|
111
142
|
maxTraceBytes: options.maxTraceSize,
|
|
@@ -115,7 +146,7 @@ export function useApolloUsageReport(options = {}) {
|
|
|
115
146
|
async onDispose() {
|
|
116
147
|
await reporter?.flush();
|
|
117
148
|
},
|
|
118
|
-
});
|
|
149
|
+
})));
|
|
119
150
|
},
|
|
120
151
|
};
|
|
121
152
|
}
|
package/esm/reporter.js
CHANGED
|
@@ -2,24 +2,25 @@ import { google, Report, ReportHeader } from '@apollo/usage-reporting-protobuf';
|
|
|
2
2
|
import { OurReport } from './stats.js';
|
|
3
3
|
const DEFAULT_REPORTING_ENDPOINT = 'https://usage-reporting.api.apollographql.com/api/ingress/traces';
|
|
4
4
|
export class Reporter {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
yoga;
|
|
6
|
+
logger;
|
|
7
|
+
reportHeaders;
|
|
8
|
+
options;
|
|
9
|
+
reportsBySchema = {};
|
|
10
|
+
nextSendAfterDelay;
|
|
11
|
+
sending = [];
|
|
12
12
|
constructor(options, yoga, logger) {
|
|
13
|
-
this
|
|
14
|
-
this
|
|
15
|
-
this
|
|
13
|
+
this.yoga = yoga;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
this.options = {
|
|
16
16
|
...options,
|
|
17
17
|
maxBatchDelay: options.maxBatchDelay ?? 20_000, // 20s
|
|
18
18
|
maxBatchUncompressedSize: options.maxBatchUncompressedSize ?? 4 * 1024 * 1024, // 4mb
|
|
19
19
|
maxTraceSize: options.maxTraceSize ?? 10 * 1024 * 1024, // 10mb
|
|
20
20
|
exportTimeout: options.exportTimeout ?? 30_000, // 30s
|
|
21
|
+
onError: options.onError ?? (err => this.logger.error('Failed to send report', err)),
|
|
21
22
|
};
|
|
22
|
-
this
|
|
23
|
+
this.reportHeaders = {
|
|
23
24
|
graphRef: getGraphRef(options),
|
|
24
25
|
hostname: options.hostname ?? getEnvVar('HOSTNAME') ?? '',
|
|
25
26
|
uname: options.uname ?? '', // TODO: find a cross-platform way to get the uname
|
|
@@ -28,49 +29,50 @@ export class Reporter {
|
|
|
28
29
|
};
|
|
29
30
|
}
|
|
30
31
|
addTrace(schemaId, options) {
|
|
31
|
-
const report = this
|
|
32
|
+
const report = this.getReport(schemaId);
|
|
32
33
|
report.addTrace(options);
|
|
33
|
-
if (this
|
|
34
|
-
report.sizeEstimator.bytes >= this
|
|
35
|
-
return this.
|
|
34
|
+
if (this.options.alwaysSend ||
|
|
35
|
+
report.sizeEstimator.bytes >= this.options.maxBatchUncompressedSize) {
|
|
36
|
+
return this._sendReport(schemaId);
|
|
36
37
|
}
|
|
37
|
-
this
|
|
38
|
+
this.nextSendAfterDelay ||= setTimeout(() => this.flush(), this.options.maxBatchDelay);
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
async flush() {
|
|
41
42
|
return Promise.allSettled([
|
|
42
|
-
...this
|
|
43
|
-
...Object.keys(this
|
|
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)),
|
|
44
45
|
]);
|
|
45
46
|
}
|
|
46
47
|
async sendReport(schemaId) {
|
|
47
|
-
const sending = this
|
|
48
|
-
this
|
|
49
|
-
sending.finally(() => this
|
|
48
|
+
const sending = this._sendReport(schemaId);
|
|
49
|
+
this.sending.push(sending);
|
|
50
|
+
sending.finally(() => (this.sending = this.sending?.filter(p => p !== sending)));
|
|
50
51
|
return sending;
|
|
51
52
|
}
|
|
52
|
-
async
|
|
53
|
-
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this
|
|
54
|
-
const report = this
|
|
53
|
+
async _sendReport(schemaId) {
|
|
54
|
+
const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.yoga;
|
|
55
|
+
const report = this.reportsBySchema[schemaId];
|
|
55
56
|
if (!report) {
|
|
56
57
|
throw new Error(`No report to send for schema ${schemaId}`);
|
|
57
58
|
}
|
|
58
|
-
if (this
|
|
59
|
-
clearTimeout(this
|
|
60
|
-
this
|
|
59
|
+
if (this.nextSendAfterDelay != null) {
|
|
60
|
+
clearTimeout(this.nextSendAfterDelay);
|
|
61
|
+
this.nextSendAfterDelay = undefined;
|
|
61
62
|
}
|
|
62
|
-
delete this
|
|
63
|
+
delete this.reportsBySchema[schemaId];
|
|
63
64
|
report.endTime = dateToProtoTimestamp(new Date());
|
|
64
65
|
report.ensureCountsAreIntegers();
|
|
65
66
|
const validationError = Report.verify(report);
|
|
66
67
|
if (validationError) {
|
|
67
68
|
throw new TypeError(`Invalid report: ${validationError}`);
|
|
68
69
|
}
|
|
69
|
-
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this
|
|
70
|
+
const { apiKey = getEnvVar('APOLLO_KEY'), endpoint = DEFAULT_REPORTING_ENDPOINT } = this.options;
|
|
70
71
|
const encodedReport = Report.encode(report).finish();
|
|
72
|
+
let lastError;
|
|
71
73
|
for (let tries = 0; tries < 5; tries++) {
|
|
72
74
|
try {
|
|
73
|
-
this
|
|
75
|
+
this.logger.debug(`Sending report (try ${tries}/5)`);
|
|
74
76
|
const response = await fetch(endpoint, {
|
|
75
77
|
method: 'POST',
|
|
76
78
|
headers: {
|
|
@@ -86,28 +88,29 @@ export class Reporter {
|
|
|
86
88
|
controller.close();
|
|
87
89
|
},
|
|
88
90
|
}).pipeThrough(new CompressionStream('gzip')),
|
|
89
|
-
signal: AbortSignal.timeout(this
|
|
91
|
+
signal: AbortSignal.timeout(this.options.exportTimeout),
|
|
90
92
|
});
|
|
91
93
|
const result = await response.text();
|
|
92
94
|
if (response.ok) {
|
|
93
|
-
this
|
|
95
|
+
this.logger.debug('Report sent:', result);
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
throw result;
|
|
97
99
|
}
|
|
98
100
|
catch (err) {
|
|
99
|
-
|
|
101
|
+
lastError = err;
|
|
102
|
+
this.logger.error('Failed to send report:', err);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
|
-
|
|
105
|
+
this.options.onError(new Error('Failed to send traces after 5 tries', { cause: lastError }));
|
|
103
106
|
}
|
|
104
|
-
|
|
105
|
-
const report = this
|
|
107
|
+
getReport(schemaId) {
|
|
108
|
+
const report = this.reportsBySchema[schemaId];
|
|
106
109
|
if (report) {
|
|
107
110
|
return report;
|
|
108
111
|
}
|
|
109
|
-
return (this
|
|
110
|
-
...this
|
|
112
|
+
return (this.reportsBySchema[schemaId] = new OurReport(new ReportHeader({
|
|
113
|
+
...this.reportHeaders,
|
|
111
114
|
executableSchemaId: schemaId,
|
|
112
115
|
})));
|
|
113
116
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphql-yoga/plugin-apollo-usage-report",
|
|
3
|
-
"version": "0.9.0-alpha-
|
|
3
|
+
"version": "0.9.0-alpha-20250602083944-892c9d0b95711d54bb94b0197f51122b6e7236af",
|
|
4
4
|
"description": "Apollo's GraphOS usage report plugin for GraphQL Yoga.",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"graphql": "^15.2.0 || ^16.0.0",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"@apollo/server-gateway-interface": "^1.1.1",
|
|
11
11
|
"@apollo/usage-reporting-protobuf": "^4.1.1",
|
|
12
12
|
"@apollo/utils.usagereporting": "^2.1.0",
|
|
13
|
-
"@graphql-tools/utils": "
|
|
13
|
+
"@graphql-tools/utils": "10.9.0-alpha-20250601230319-2f5f24f393b56915c1c913d3f057531f79991587",
|
|
14
14
|
"@whatwg-node/promise-helpers": "^1.2.4",
|
|
15
15
|
"tslib": "^2.8.1",
|
|
16
16
|
"@graphql-yoga/plugin-apollo-inline-trace": "^3.13.5"
|
package/typings/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type Maybe, type Plugin, type YogaInitialContext } from 'graphql-yoga';
|
|
1
|
+
import { YogaLogger, YogaServer, type Maybe, type Plugin, type YogaInitialContext } from 'graphql-yoga';
|
|
2
2
|
import { calculateReferencedFieldsByType } from '@apollo/utils.usagereporting';
|
|
3
3
|
import { ApolloInlineGraphqlTraceContext, ApolloInlineRequestTraceContext, ApolloInlineTracePluginOptions } from '@graphql-yoga/plugin-apollo-inline-trace';
|
|
4
|
+
import { Reporter } from './reporter.cjs';
|
|
4
5
|
export type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
|
|
5
6
|
/**
|
|
6
7
|
* The graph ref of the managed federation graph.
|
|
@@ -88,12 +89,22 @@ export type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
|
|
|
88
89
|
* @default 30s
|
|
89
90
|
*/
|
|
90
91
|
exportTimeout?: number;
|
|
92
|
+
/**
|
|
93
|
+
* The class to be used to keep track of traces and send them to the GraphOS endpoint
|
|
94
|
+
* Note: This option is aimed to be used for testing purposes
|
|
95
|
+
*/
|
|
96
|
+
reporter?: (options: ApolloUsageReportOptions, yoga: YogaServer<Record<string, unknown>, Record<string, unknown>>, logger: YogaLogger) => Reporter;
|
|
97
|
+
/**
|
|
98
|
+
* Called when all retry attempts to send a report to GraphOS endpoint failed.
|
|
99
|
+
* By default, the error is logged.
|
|
100
|
+
*/
|
|
101
|
+
onError?: (err: Error) => void;
|
|
91
102
|
};
|
|
92
103
|
export interface ApolloUsageReportRequestContext extends ApolloInlineRequestTraceContext {
|
|
93
104
|
traces: Map<YogaInitialContext, ApolloUsageReportGraphqlContext>;
|
|
94
105
|
}
|
|
95
106
|
export interface ApolloUsageReportGraphqlContext extends ApolloInlineGraphqlTraceContext {
|
|
96
|
-
referencedFieldsByType
|
|
107
|
+
referencedFieldsByType?: ReturnType<typeof calculateReferencedFieldsByType>;
|
|
97
108
|
operationKey?: string;
|
|
98
109
|
schemaId?: string;
|
|
99
110
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type Maybe, type Plugin, type YogaInitialContext } from 'graphql-yoga';
|
|
1
|
+
import { YogaLogger, YogaServer, type Maybe, type Plugin, type YogaInitialContext } from 'graphql-yoga';
|
|
2
2
|
import { calculateReferencedFieldsByType } from '@apollo/utils.usagereporting';
|
|
3
3
|
import { ApolloInlineGraphqlTraceContext, ApolloInlineRequestTraceContext, ApolloInlineTracePluginOptions } from '@graphql-yoga/plugin-apollo-inline-trace';
|
|
4
|
+
import { Reporter } from './reporter.js';
|
|
4
5
|
export type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
|
|
5
6
|
/**
|
|
6
7
|
* The graph ref of the managed federation graph.
|
|
@@ -88,12 +89,22 @@ export type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
|
|
|
88
89
|
* @default 30s
|
|
89
90
|
*/
|
|
90
91
|
exportTimeout?: number;
|
|
92
|
+
/**
|
|
93
|
+
* The class to be used to keep track of traces and send them to the GraphOS endpoint
|
|
94
|
+
* Note: This option is aimed to be used for testing purposes
|
|
95
|
+
*/
|
|
96
|
+
reporter?: (options: ApolloUsageReportOptions, yoga: YogaServer<Record<string, unknown>, Record<string, unknown>>, logger: YogaLogger) => Reporter;
|
|
97
|
+
/**
|
|
98
|
+
* Called when all retry attempts to send a report to GraphOS endpoint failed.
|
|
99
|
+
* By default, the error is logged.
|
|
100
|
+
*/
|
|
101
|
+
onError?: (err: Error) => void;
|
|
91
102
|
};
|
|
92
103
|
export interface ApolloUsageReportRequestContext extends ApolloInlineRequestTraceContext {
|
|
93
104
|
traces: Map<YogaInitialContext, ApolloUsageReportGraphqlContext>;
|
|
94
105
|
}
|
|
95
106
|
export interface ApolloUsageReportGraphqlContext extends ApolloInlineGraphqlTraceContext {
|
|
96
|
-
referencedFieldsByType
|
|
107
|
+
referencedFieldsByType?: ReturnType<typeof calculateReferencedFieldsByType>;
|
|
97
108
|
operationKey?: string;
|
|
98
109
|
schemaId?: string;
|
|
99
110
|
}
|
package/typings/reporter.d.cts
CHANGED
|
@@ -3,11 +3,19 @@ import { google } from '@apollo/usage-reporting-protobuf';
|
|
|
3
3
|
import type { ApolloUsageReportOptions } from './index';
|
|
4
4
|
import { OurReport } from './stats.cjs';
|
|
5
5
|
export declare class Reporter {
|
|
6
|
-
|
|
6
|
+
private yoga;
|
|
7
|
+
private logger;
|
|
8
|
+
private reportHeaders;
|
|
9
|
+
private options;
|
|
10
|
+
private reportsBySchema;
|
|
11
|
+
private nextSendAfterDelay?;
|
|
12
|
+
private sending;
|
|
7
13
|
constructor(options: ApolloUsageReportOptions, yoga: YogaServer<Record<string, unknown>, Record<string, unknown>>, logger: YogaLogger);
|
|
8
14
|
addTrace(schemaId: string, options: Parameters<OurReport['addTrace']>[0]): Promise<void> | undefined;
|
|
9
15
|
flush(): Promise<PromiseSettledResult<unknown>[]>;
|
|
10
16
|
sendReport(schemaId: string): Promise<void>;
|
|
17
|
+
private _sendReport;
|
|
18
|
+
private getReport;
|
|
11
19
|
}
|
|
12
20
|
export declare function getEnvVar<T>(name: string, defaultValue?: T): string | T | undefined;
|
|
13
21
|
export declare function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp;
|
package/typings/reporter.d.ts
CHANGED
|
@@ -3,11 +3,19 @@ import { google } from '@apollo/usage-reporting-protobuf';
|
|
|
3
3
|
import type { ApolloUsageReportOptions } from './index';
|
|
4
4
|
import { OurReport } from './stats.js';
|
|
5
5
|
export declare class Reporter {
|
|
6
|
-
|
|
6
|
+
private yoga;
|
|
7
|
+
private logger;
|
|
8
|
+
private reportHeaders;
|
|
9
|
+
private options;
|
|
10
|
+
private reportsBySchema;
|
|
11
|
+
private nextSendAfterDelay?;
|
|
12
|
+
private sending;
|
|
7
13
|
constructor(options: ApolloUsageReportOptions, yoga: YogaServer<Record<string, unknown>, Record<string, unknown>>, logger: YogaLogger);
|
|
8
14
|
addTrace(schemaId: string, options: Parameters<OurReport['addTrace']>[0]): Promise<void> | undefined;
|
|
9
15
|
flush(): Promise<PromiseSettledResult<unknown>[]>;
|
|
10
16
|
sendReport(schemaId: string): Promise<void>;
|
|
17
|
+
private _sendReport;
|
|
18
|
+
private getReport;
|
|
11
19
|
}
|
|
12
20
|
export declare function getEnvVar<T>(name: string, defaultValue?: T): string | T | undefined;
|
|
13
21
|
export declare function dateToProtoTimestamp(date: Date): google.protobuf.Timestamp;
|