@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 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 = new reporter_js_1.Reporter(options, yoga, logger);
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
- schemaIdSet$ = hashSHA256((0, utils_1.printSchemaWithDirectives)(schema), yoga.fetchAPI)
46
- .then(id => {
47
- currentSchema = { id, schema };
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
- // It is possible that the result is not a document when the parsing fails
70
- const document = isDocumentNode(result) ? result : null;
71
- if (document) {
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${opName && (0, utils_usagereporting_1.usageReportingSignature)(document, opName)}`;
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
- #yoga;
11
- #logger = console;
12
- #reportHeaders;
13
- #options;
14
- #reportsBySchema = {};
15
- #nextSendAfterDelay;
16
- #sending = [];
10
+ yoga;
11
+ logger;
12
+ reportHeaders;
13
+ options;
14
+ reportsBySchema = {};
15
+ nextSendAfterDelay;
16
+ sending = [];
17
17
  constructor(options, yoga, logger) {
18
- this.#logger = logger;
19
- this.#yoga = yoga;
20
- this.#options = {
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.#reportHeaders = {
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.#getReport(schemaId);
37
+ const report = this.getReport(schemaId);
37
38
  report.addTrace(options);
38
- if (this.#options.alwaysSend ||
39
- report.sizeEstimator.bytes >= this.#options.maxBatchUncompressedSize) {
40
- return this.sendReport(schemaId);
39
+ if (this.options.alwaysSend ||
40
+ report.sizeEstimator.bytes >= this.options.maxBatchUncompressedSize) {
41
+ return this._sendReport(schemaId);
41
42
  }
42
- this.#nextSendAfterDelay ||= setTimeout(() => this.flush(), this.#options.maxBatchDelay);
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.#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)),
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.#sendReport(schemaId);
53
- this.#sending.push(sending);
54
- sending.finally(() => this.#sending?.filter(p => p !== sending));
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 #sendReport(schemaId) {
58
- const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.#yoga;
59
- const report = this.#reportsBySchema[schemaId];
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.#nextSendAfterDelay != null) {
64
- clearTimeout(this.#nextSendAfterDelay);
65
- this.#nextSendAfterDelay = undefined;
64
+ if (this.nextSendAfterDelay != null) {
65
+ clearTimeout(this.nextSendAfterDelay);
66
+ this.nextSendAfterDelay = undefined;
66
67
  }
67
- delete this.#reportsBySchema[schemaId];
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.#options;
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.#logger?.debug(`Sending report (try ${tries}/5)`);
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.#options.exportTimeout),
96
+ signal: AbortSignal.timeout(this.options.exportTimeout),
95
97
  });
96
98
  const result = await response.text();
97
99
  if (response.ok) {
98
- this.#logger?.debug('Report sent:', result);
100
+ this.logger.debug('Report sent:', result);
99
101
  return;
100
102
  }
101
103
  throw result;
102
104
  }
103
105
  catch (err) {
104
- this.#logger?.error('Failed to send report:', err);
106
+ lastError = err;
107
+ this.logger.error('Failed to send report:', err);
105
108
  }
106
109
  }
107
- throw new Error('Failed to send traces after 5 tries');
110
+ this.options.onError(new Error('Failed to send traces after 5 tries', { cause: lastError }));
108
111
  }
109
- #getReport(schemaId) {
110
- const report = this.#reportsBySchema[schemaId];
112
+ getReport(schemaId) {
113
+ const report = this.reportsBySchema[schemaId];
111
114
  if (report) {
112
115
  return report;
113
116
  }
114
- return (this.#reportsBySchema[schemaId] = new stats_js_1.OurReport(new usage_reporting_protobuf_1.ReportHeader({
115
- ...this.#reportHeaders,
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 = new Reporter(options, yoga, logger);
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
- schemaIdSet$ = hashSHA256(printSchemaWithDirectives(schema), yoga.fetchAPI)
42
- .then(id => {
43
- currentSchema = { id, schema };
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
- // It is possible that the result is not a document when the parsing fails
66
- const document = isDocumentNode(result) ? result : null;
67
- if (document) {
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${opName && usageReportingSignature(document, opName)}`;
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
- #yoga;
6
- #logger = console;
7
- #reportHeaders;
8
- #options;
9
- #reportsBySchema = {};
10
- #nextSendAfterDelay;
11
- #sending = [];
5
+ yoga;
6
+ logger;
7
+ reportHeaders;
8
+ options;
9
+ reportsBySchema = {};
10
+ nextSendAfterDelay;
11
+ sending = [];
12
12
  constructor(options, yoga, logger) {
13
- this.#logger = logger;
14
- this.#yoga = yoga;
15
- this.#options = {
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.#reportHeaders = {
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.#getReport(schemaId);
32
+ const report = this.getReport(schemaId);
32
33
  report.addTrace(options);
33
- if (this.#options.alwaysSend ||
34
- report.sizeEstimator.bytes >= this.#options.maxBatchUncompressedSize) {
35
- return this.sendReport(schemaId);
34
+ if (this.options.alwaysSend ||
35
+ report.sizeEstimator.bytes >= this.options.maxBatchUncompressedSize) {
36
+ return this._sendReport(schemaId);
36
37
  }
37
- this.#nextSendAfterDelay ||= setTimeout(() => this.flush(), this.#options.maxBatchDelay);
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.#sending, // When flushing, we want to also wait for previous traces to be sent, because it's mostly used for clean up
43
- ...Object.keys(this.#reportsBySchema).map(schemaId => this.sendReport(schemaId)),
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.#sendReport(schemaId);
48
- this.#sending.push(sending);
49
- sending.finally(() => this.#sending?.filter(p => p !== sending));
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 #sendReport(schemaId) {
53
- const { fetchAPI: { fetch, CompressionStream, ReadableStream }, } = this.#yoga;
54
- const report = this.#reportsBySchema[schemaId];
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.#nextSendAfterDelay != null) {
59
- clearTimeout(this.#nextSendAfterDelay);
60
- this.#nextSendAfterDelay = undefined;
59
+ if (this.nextSendAfterDelay != null) {
60
+ clearTimeout(this.nextSendAfterDelay);
61
+ this.nextSendAfterDelay = undefined;
61
62
  }
62
- delete this.#reportsBySchema[schemaId];
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.#options;
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.#logger?.debug(`Sending report (try ${tries}/5)`);
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.#options.exportTimeout),
91
+ signal: AbortSignal.timeout(this.options.exportTimeout),
90
92
  });
91
93
  const result = await response.text();
92
94
  if (response.ok) {
93
- this.#logger?.debug('Report sent:', result);
95
+ this.logger.debug('Report sent:', result);
94
96
  return;
95
97
  }
96
98
  throw result;
97
99
  }
98
100
  catch (err) {
99
- this.#logger?.error('Failed to send report:', err);
101
+ lastError = err;
102
+ this.logger.error('Failed to send report:', err);
100
103
  }
101
104
  }
102
- throw new Error('Failed to send traces after 5 tries');
105
+ this.options.onError(new Error('Failed to send traces after 5 tries', { cause: lastError }));
103
106
  }
104
- #getReport(schemaId) {
105
- const report = this.#reportsBySchema[schemaId];
107
+ getReport(schemaId) {
108
+ const report = this.reportsBySchema[schemaId];
106
109
  if (report) {
107
110
  return report;
108
111
  }
109
- return (this.#reportsBySchema[schemaId] = new OurReport(new ReportHeader({
110
- ...this.#reportHeaders,
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-20250527093154-d0be5f4626600bfbe255c0ab57590d3963013823",
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": "^10.8.6",
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"
@@ -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: ReturnType<typeof calculateReferencedFieldsByType>;
107
+ referencedFieldsByType?: ReturnType<typeof calculateReferencedFieldsByType>;
97
108
  operationKey?: string;
98
109
  schemaId?: string;
99
110
  }
@@ -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: ReturnType<typeof calculateReferencedFieldsByType>;
107
+ referencedFieldsByType?: ReturnType<typeof calculateReferencedFieldsByType>;
97
108
  operationKey?: string;
98
109
  schemaId?: string;
99
110
  }
@@ -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
- #private;
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;
@@ -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
- #private;
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;