@graphql-yoga/plugin-apollo-inline-trace 3.6.3 → 3.7.0-alpha-20240815202356-b651177c

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
@@ -1,13 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useApolloInlineTrace = useApolloInlineTrace;
4
- const tslib_1 = require("tslib");
5
- const apollo_reporting_protobuf_1 = tslib_1.__importDefault(require("apollo-reporting-protobuf"));
6
- const graphql_1 = require("graphql");
4
+ exports.useApolloInstrumentation = useApolloInstrumentation;
7
5
  const graphql_yoga_1 = require("graphql-yoga");
6
+ const usage_reporting_protobuf_1 = require("@apollo/usage-reporting-protobuf");
8
7
  const on_resolve_1 = require("@envelop/on-resolve");
9
8
  const fetch_1 = require("@whatwg-node/fetch");
10
- const asArray = (x) => (Array.isArray(x) ? x : [x]);
11
9
  /**
12
10
  * Produces Apollo's base64 trace protocol containing timing, resolution and
13
11
  * errors information.
@@ -18,116 +16,181 @@ const asArray = (x) => (Array.isArray(x) ? x : [x]);
18
16
  * it to Apollo's usage reporting ingress.
19
17
  */
20
18
  function useApolloInlineTrace(options = {}) {
21
- const ctxForReq = new WeakMap();
19
+ const [instrumentation, ctxForReq] = useApolloInstrumentation({
20
+ ignoreRequest: request => request.headers.get('apollo-federation-include-trace') !== 'ftv1',
21
+ ...options,
22
+ });
22
23
  return {
24
+ onPluginInit({ addPlugin }) {
25
+ addPlugin(instrumentation);
26
+ addPlugin({
27
+ onExecutionResult({ request, result, setResult }) {
28
+ // TODO: should handle streaming results? how?
29
+ if (!result || (0, graphql_yoga_1.isAsyncIterable)(result)) {
30
+ return;
31
+ }
32
+ if (!result.extensions) {
33
+ return;
34
+ }
35
+ const { ftv1_context, ...extensions } = result.extensions;
36
+ if (!ftv1_context) {
37
+ return;
38
+ }
39
+ if (extensions?.ftv1 !== undefined) {
40
+ throw new Error('The `ftv1` extension is already present');
41
+ }
42
+ const reqCtx = ctxForReq.get(request);
43
+ if (!reqCtx) {
44
+ return;
45
+ }
46
+ const ctx = reqCtx.traces.get(ftv1_context);
47
+ if (!ctx) {
48
+ return;
49
+ }
50
+ const encodedUint8Array = usage_reporting_protobuf_1.Trace.encode(ctx.trace).finish();
51
+ const base64 = (0, fetch_1.btoa)(String.fromCharCode(...encodedUint8Array));
52
+ setResult({
53
+ ...result,
54
+ extensions: {
55
+ ...extensions,
56
+ ftv1: base64,
57
+ },
58
+ });
59
+ },
60
+ });
61
+ },
62
+ };
63
+ }
64
+ /**
65
+ * Instrument GraphQL request processing pipeline and creates Apollo compatible tracing data.
66
+ *
67
+ * This is meant as a helper, do not use it directly. Use `useApolloInlineTrace` or `useApolloUsageReport` instead.
68
+ * @param options
69
+ * @returns A tuple with the instrumentation plugin and a WeakMap containing the tracing data
70
+ */
71
+ function useApolloInstrumentation(options) {
72
+ const ctxForReq = new WeakMap();
73
+ let logger;
74
+ function createContext() {
75
+ return {
76
+ startHrTime: process.hrtime(),
77
+ traceStartTimestamp: nowTimestamp(),
78
+ traces: new Map(),
79
+ stopped: false,
80
+ };
81
+ }
82
+ function setNewContext(request) {
83
+ try {
84
+ ctxForReq.set(request, createContext());
85
+ }
86
+ catch (err) {
87
+ logger.error('Apollo inline error:', err);
88
+ }
89
+ }
90
+ const plugin = {
91
+ onYogaInit({ yoga }) {
92
+ logger = yoga.logger;
93
+ },
23
94
  onPluginInit: ({ addPlugin }) => {
24
- addPlugin((0, on_resolve_1.useOnResolve)(({ context: { request }, info }) => {
25
- const ctx = ctxForReq.get(request);
26
- if (!ctx)
95
+ addPlugin((0, on_resolve_1.useOnResolve)(({ context, info }) => {
96
+ const reqCtx = ctxForReq.get(context.request);
97
+ if (!reqCtx)
27
98
  return;
28
99
  // result was already shipped (see ApolloInlineTraceContext.stopped)
29
- if (ctx.stopped) {
30
- return () => {
31
- // noop
32
- };
100
+ if (reqCtx.stopped) {
101
+ return;
102
+ }
103
+ const ctx = reqCtx.traces.get(context);
104
+ if (!ctx) {
105
+ return;
33
106
  }
34
107
  const node = newTraceNode(ctx, info.path);
35
108
  node.type = info.returnType.toString();
36
109
  node.parentType = info.parentType.toString();
37
- node.startTime = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime));
110
+ node.startTime = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
38
111
  if (typeof info.path.key === 'string' && info.path.key !== info.fieldName) {
39
112
  // field was aliased, send the original field name too
40
113
  node.originalFieldName = info.fieldName;
41
114
  }
42
115
  return () => {
43
- node.endTime = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime));
116
+ node.endTime = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
44
117
  };
45
118
  }));
46
119
  },
47
120
  onRequest({ request }) {
48
- // must be ftv1 tracing protocol
49
- if (request.headers.get('apollo-federation-include-trace') !== 'ftv1') {
121
+ if (options.ignoreRequest) {
122
+ const res$ = options.ignoreRequest(request);
123
+ if (typeof res$ === 'boolean') {
124
+ if (res$) {
125
+ return;
126
+ }
127
+ }
128
+ else {
129
+ return res$.then(shouldIgnore => {
130
+ if (!shouldIgnore) {
131
+ setNewContext(request);
132
+ }
133
+ });
134
+ }
135
+ }
136
+ setNewContext(request);
137
+ },
138
+ onEnveloped({ context }) {
139
+ if (!context) {
50
140
  return;
51
141
  }
52
- const startHrTime = process.hrtime();
53
- const rootNode = new apollo_reporting_protobuf_1.default.Trace.Node();
54
- ctxForReq.set(request, {
55
- startHrTime,
142
+ const reqCtx = ctxForReq.get(context.request);
143
+ if (!reqCtx)
144
+ return;
145
+ const rootNode = new usage_reporting_protobuf_1.Trace.Node();
146
+ const ctx = {
56
147
  rootNode,
57
- trace: new apollo_reporting_protobuf_1.default.Trace({
148
+ trace: new usage_reporting_protobuf_1.Trace({
58
149
  root: rootNode,
59
150
  fieldExecutionWeight: 1, // Why 1? See: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L16-L23
60
- startTime: nowTimestamp(),
151
+ startTime: reqCtx.traceStartTimestamp,
61
152
  }),
62
153
  nodes: new Map([[responsePathToString(), rootNode]]),
63
- stopped: false,
64
- });
65
- },
66
- onParse() {
67
- return ({ context: { request }, result }) => {
68
- const ctx = ctxForReq.get(request);
69
- if (!ctx)
70
- return;
71
- if (result instanceof graphql_1.GraphQLError) {
72
- handleErrors(ctx, [result], options.rewriteError);
73
- }
74
- else if (result instanceof Error) {
75
- handleErrors(ctx, [
76
- (0, graphql_yoga_1.createGraphQLError)(result.message, {
77
- originalError: result,
78
- }),
79
- ], options.rewriteError);
80
- }
81
154
  };
155
+ reqCtx.traces.set(context, ctx);
82
156
  },
83
- onValidate() {
84
- return ({ context: { request }, result: errors }) => {
85
- if (errors.length) {
86
- const ctx = ctxForReq.get(request);
87
- if (ctx)
88
- // Envelop doesn't give GraphQLError type since it is agnostic
89
- handleErrors(ctx, errors, options.rewriteError);
90
- }
91
- };
92
- },
93
- onExecute() {
94
- return {
95
- onExecuteDone({ args: { contextValue: { request }, }, result, }) {
96
- // TODO: should handle streaming results? how?
97
- if (!(0, graphql_yoga_1.isAsyncIterable)(result) && result.errors?.length) {
98
- const ctx = ctxForReq.get(request);
99
- if (ctx)
100
- handleErrors(ctx, result.errors, options.rewriteError);
101
- }
157
+ onExecutionResult({ context, request, result, setResult }) {
158
+ // TODO: should handle streaming results? how?
159
+ if (result == null || (0, graphql_yoga_1.isAsyncIterable)(result)) {
160
+ return;
161
+ }
162
+ const reqCtx = ctxForReq.get(request);
163
+ const ctx = reqCtx?.traces.get(context);
164
+ if (!reqCtx || !ctx || reqCtx.stopped) {
165
+ return;
166
+ }
167
+ if (result.errors?.length && reqCtx && ctx) {
168
+ handleErrors(reqCtx, ctx, result.errors, options.rewriteError);
169
+ }
170
+ ctx.trace.durationNs = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
171
+ ctx.trace.endTime = nowTimestamp();
172
+ setResult({
173
+ ...result,
174
+ extensions: {
175
+ ...result.extensions,
176
+ ftv1_context: context,
102
177
  },
103
- };
178
+ });
104
179
  },
105
180
  onResultProcess({ request, result }) {
106
- const ctx = ctxForReq.get(request);
107
- if (!ctx)
108
- return;
109
181
  // TODO: should handle streaming results? how?
110
182
  if ((0, graphql_yoga_1.isAsyncIterable)(result))
111
183
  return;
112
- for (const singleResult of asArray(result)) {
113
- if (singleResult.extensions?.ftv1 !== undefined) {
114
- throw new Error('The `ftv1` extension is already present');
115
- }
116
- // onResultProcess will be called only once since we disallow async iterables
117
- if (ctx.stopped)
118
- throw new Error('Trace stopped multiple times');
119
- ctx.stopped = true;
120
- ctx.trace.durationNs = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime));
121
- ctx.trace.endTime = nowTimestamp();
122
- const encodedUint8Array = apollo_reporting_protobuf_1.default.Trace.encode(ctx.trace).finish();
123
- const base64 = (0, fetch_1.btoa)(String.fromCharCode(...encodedUint8Array));
124
- singleResult.extensions = {
125
- ...singleResult.extensions,
126
- ftv1: base64,
127
- };
128
- }
184
+ const reqCtx = ctxForReq.get(request);
185
+ if (!reqCtx)
186
+ return;
187
+ // onResultProcess will be called only once since we disallow async iterables
188
+ if (reqCtx.stopped)
189
+ throw new Error('Trace stopped multiple times');
190
+ reqCtx.stopped = true;
129
191
  },
130
192
  };
193
+ return [plugin, ctxForReq];
131
194
  }
132
195
  /**
133
196
  * Converts an hrtime array (as returned from process.hrtime) to nanoseconds.
@@ -148,7 +211,7 @@ function hrTimeToDurationInNanos(hrtime) {
148
211
  function nowTimestamp() {
149
212
  const totalMillis = Date.now();
150
213
  const millis = totalMillis % 1000;
151
- return new apollo_reporting_protobuf_1.default.google.protobuf.Timestamp({
214
+ return new usage_reporting_protobuf_1.google.protobuf.Timestamp({
152
215
  seconds: (totalMillis - millis) / 1000,
153
216
  nanos: millis * 1e6,
154
217
  });
@@ -179,7 +242,7 @@ function ensureParentTraceNode(ctx, path) {
179
242
  return newTraceNode(ctx, path.prev);
180
243
  }
181
244
  function newTraceNode(ctx, path) {
182
- const node = new apollo_reporting_protobuf_1.default.Trace.Node();
245
+ const node = new usage_reporting_protobuf_1.Trace.Node();
183
246
  const id = path.key;
184
247
  if (typeof id === 'number') {
185
248
  node.index = id;
@@ -192,8 +255,8 @@ function newTraceNode(ctx, path) {
192
255
  parentNode.child.push(node);
193
256
  return node;
194
257
  }
195
- function handleErrors(ctx, errors, rewriteError) {
196
- if (ctx.stopped) {
258
+ function handleErrors(reqCtx, ctx, errors, rewriteError) {
259
+ if (reqCtx.stopped) {
197
260
  throw new Error('Handling errors after tracing was stopped');
198
261
  }
199
262
  for (const err of errors) {
@@ -238,9 +301,9 @@ function handleErrors(ctx, errors, rewriteError) {
238
301
  throw new Error(`Could not find node with path ${errToReport.path.join('.')}`);
239
302
  }
240
303
  }
241
- node.error.push(new apollo_reporting_protobuf_1.default.Trace.Error({
304
+ node.error.push(new usage_reporting_protobuf_1.Trace.Error({
242
305
  message: errToReport.message,
243
- location: (errToReport.locations || []).map(({ line, column }) => new apollo_reporting_protobuf_1.default.Trace.Location({ line, column })),
306
+ location: (errToReport.locations || []).map(({ line, column }) => new usage_reporting_protobuf_1.Trace.Location({ line, column })),
244
307
  json: JSON.stringify(errToReport),
245
308
  }));
246
309
  }
package/esm/index.js CHANGED
@@ -1,9 +1,7 @@
1
- import ApolloReportingProtobuf from 'apollo-reporting-protobuf';
2
- import { GraphQLError } from 'graphql';
3
- import { createGraphQLError, isAsyncIterable } from 'graphql-yoga';
1
+ import { createGraphQLError, isAsyncIterable, } from 'graphql-yoga';
2
+ import { google, Trace } from '@apollo/usage-reporting-protobuf';
4
3
  import { useOnResolve } from '@envelop/on-resolve';
5
4
  import { btoa } from '@whatwg-node/fetch';
6
- const asArray = (x) => (Array.isArray(x) ? x : [x]);
7
5
  /**
8
6
  * Produces Apollo's base64 trace protocol containing timing, resolution and
9
7
  * errors information.
@@ -14,116 +12,181 @@ const asArray = (x) => (Array.isArray(x) ? x : [x]);
14
12
  * it to Apollo's usage reporting ingress.
15
13
  */
16
14
  export function useApolloInlineTrace(options = {}) {
17
- const ctxForReq = new WeakMap();
15
+ const [instrumentation, ctxForReq] = useApolloInstrumentation({
16
+ ignoreRequest: request => request.headers.get('apollo-federation-include-trace') !== 'ftv1',
17
+ ...options,
18
+ });
18
19
  return {
20
+ onPluginInit({ addPlugin }) {
21
+ addPlugin(instrumentation);
22
+ addPlugin({
23
+ onExecutionResult({ request, result, setResult }) {
24
+ // TODO: should handle streaming results? how?
25
+ if (!result || isAsyncIterable(result)) {
26
+ return;
27
+ }
28
+ if (!result.extensions) {
29
+ return;
30
+ }
31
+ const { ftv1_context, ...extensions } = result.extensions;
32
+ if (!ftv1_context) {
33
+ return;
34
+ }
35
+ if (extensions?.ftv1 !== undefined) {
36
+ throw new Error('The `ftv1` extension is already present');
37
+ }
38
+ const reqCtx = ctxForReq.get(request);
39
+ if (!reqCtx) {
40
+ return;
41
+ }
42
+ const ctx = reqCtx.traces.get(ftv1_context);
43
+ if (!ctx) {
44
+ return;
45
+ }
46
+ const encodedUint8Array = Trace.encode(ctx.trace).finish();
47
+ const base64 = btoa(String.fromCharCode(...encodedUint8Array));
48
+ setResult({
49
+ ...result,
50
+ extensions: {
51
+ ...extensions,
52
+ ftv1: base64,
53
+ },
54
+ });
55
+ },
56
+ });
57
+ },
58
+ };
59
+ }
60
+ /**
61
+ * Instrument GraphQL request processing pipeline and creates Apollo compatible tracing data.
62
+ *
63
+ * This is meant as a helper, do not use it directly. Use `useApolloInlineTrace` or `useApolloUsageReport` instead.
64
+ * @param options
65
+ * @returns A tuple with the instrumentation plugin and a WeakMap containing the tracing data
66
+ */
67
+ export function useApolloInstrumentation(options) {
68
+ const ctxForReq = new WeakMap();
69
+ let logger;
70
+ function createContext() {
71
+ return {
72
+ startHrTime: process.hrtime(),
73
+ traceStartTimestamp: nowTimestamp(),
74
+ traces: new Map(),
75
+ stopped: false,
76
+ };
77
+ }
78
+ function setNewContext(request) {
79
+ try {
80
+ ctxForReq.set(request, createContext());
81
+ }
82
+ catch (err) {
83
+ logger.error('Apollo inline error:', err);
84
+ }
85
+ }
86
+ const plugin = {
87
+ onYogaInit({ yoga }) {
88
+ logger = yoga.logger;
89
+ },
19
90
  onPluginInit: ({ addPlugin }) => {
20
- addPlugin(useOnResolve(({ context: { request }, info }) => {
21
- const ctx = ctxForReq.get(request);
22
- if (!ctx)
91
+ addPlugin(useOnResolve(({ context, info }) => {
92
+ const reqCtx = ctxForReq.get(context.request);
93
+ if (!reqCtx)
23
94
  return;
24
95
  // result was already shipped (see ApolloInlineTraceContext.stopped)
25
- if (ctx.stopped) {
26
- return () => {
27
- // noop
28
- };
96
+ if (reqCtx.stopped) {
97
+ return;
98
+ }
99
+ const ctx = reqCtx.traces.get(context);
100
+ if (!ctx) {
101
+ return;
29
102
  }
30
103
  const node = newTraceNode(ctx, info.path);
31
104
  node.type = info.returnType.toString();
32
105
  node.parentType = info.parentType.toString();
33
- node.startTime = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime));
106
+ node.startTime = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
34
107
  if (typeof info.path.key === 'string' && info.path.key !== info.fieldName) {
35
108
  // field was aliased, send the original field name too
36
109
  node.originalFieldName = info.fieldName;
37
110
  }
38
111
  return () => {
39
- node.endTime = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime));
112
+ node.endTime = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
40
113
  };
41
114
  }));
42
115
  },
43
116
  onRequest({ request }) {
44
- // must be ftv1 tracing protocol
45
- if (request.headers.get('apollo-federation-include-trace') !== 'ftv1') {
117
+ if (options.ignoreRequest) {
118
+ const res$ = options.ignoreRequest(request);
119
+ if (typeof res$ === 'boolean') {
120
+ if (res$) {
121
+ return;
122
+ }
123
+ }
124
+ else {
125
+ return res$.then(shouldIgnore => {
126
+ if (!shouldIgnore) {
127
+ setNewContext(request);
128
+ }
129
+ });
130
+ }
131
+ }
132
+ setNewContext(request);
133
+ },
134
+ onEnveloped({ context }) {
135
+ if (!context) {
46
136
  return;
47
137
  }
48
- const startHrTime = process.hrtime();
49
- const rootNode = new ApolloReportingProtobuf.Trace.Node();
50
- ctxForReq.set(request, {
51
- startHrTime,
138
+ const reqCtx = ctxForReq.get(context.request);
139
+ if (!reqCtx)
140
+ return;
141
+ const rootNode = new Trace.Node();
142
+ const ctx = {
52
143
  rootNode,
53
- trace: new ApolloReportingProtobuf.Trace({
144
+ trace: new Trace({
54
145
  root: rootNode,
55
146
  fieldExecutionWeight: 1, // Why 1? See: https://github.com/apollographql/apollo-server/blob/9389da785567a56e989430962564afc71e93bd7f/packages/apollo-server-core/src/plugin/traceTreeBuilder.ts#L16-L23
56
- startTime: nowTimestamp(),
147
+ startTime: reqCtx.traceStartTimestamp,
57
148
  }),
58
149
  nodes: new Map([[responsePathToString(), rootNode]]),
59
- stopped: false,
60
- });
61
- },
62
- onParse() {
63
- return ({ context: { request }, result }) => {
64
- const ctx = ctxForReq.get(request);
65
- if (!ctx)
66
- return;
67
- if (result instanceof GraphQLError) {
68
- handleErrors(ctx, [result], options.rewriteError);
69
- }
70
- else if (result instanceof Error) {
71
- handleErrors(ctx, [
72
- createGraphQLError(result.message, {
73
- originalError: result,
74
- }),
75
- ], options.rewriteError);
76
- }
77
150
  };
151
+ reqCtx.traces.set(context, ctx);
78
152
  },
79
- onValidate() {
80
- return ({ context: { request }, result: errors }) => {
81
- if (errors.length) {
82
- const ctx = ctxForReq.get(request);
83
- if (ctx)
84
- // Envelop doesn't give GraphQLError type since it is agnostic
85
- handleErrors(ctx, errors, options.rewriteError);
86
- }
87
- };
88
- },
89
- onExecute() {
90
- return {
91
- onExecuteDone({ args: { contextValue: { request }, }, result, }) {
92
- // TODO: should handle streaming results? how?
93
- if (!isAsyncIterable(result) && result.errors?.length) {
94
- const ctx = ctxForReq.get(request);
95
- if (ctx)
96
- handleErrors(ctx, result.errors, options.rewriteError);
97
- }
153
+ onExecutionResult({ context, request, result, setResult }) {
154
+ // TODO: should handle streaming results? how?
155
+ if (result == null || isAsyncIterable(result)) {
156
+ return;
157
+ }
158
+ const reqCtx = ctxForReq.get(request);
159
+ const ctx = reqCtx?.traces.get(context);
160
+ if (!reqCtx || !ctx || reqCtx.stopped) {
161
+ return;
162
+ }
163
+ if (result.errors?.length && reqCtx && ctx) {
164
+ handleErrors(reqCtx, ctx, result.errors, options.rewriteError);
165
+ }
166
+ ctx.trace.durationNs = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
167
+ ctx.trace.endTime = nowTimestamp();
168
+ setResult({
169
+ ...result,
170
+ extensions: {
171
+ ...result.extensions,
172
+ ftv1_context: context,
98
173
  },
99
- };
174
+ });
100
175
  },
101
176
  onResultProcess({ request, result }) {
102
- const ctx = ctxForReq.get(request);
103
- if (!ctx)
104
- return;
105
177
  // TODO: should handle streaming results? how?
106
178
  if (isAsyncIterable(result))
107
179
  return;
108
- for (const singleResult of asArray(result)) {
109
- if (singleResult.extensions?.ftv1 !== undefined) {
110
- throw new Error('The `ftv1` extension is already present');
111
- }
112
- // onResultProcess will be called only once since we disallow async iterables
113
- if (ctx.stopped)
114
- throw new Error('Trace stopped multiple times');
115
- ctx.stopped = true;
116
- ctx.trace.durationNs = hrTimeToDurationInNanos(process.hrtime(ctx.startHrTime));
117
- ctx.trace.endTime = nowTimestamp();
118
- const encodedUint8Array = ApolloReportingProtobuf.Trace.encode(ctx.trace).finish();
119
- const base64 = btoa(String.fromCharCode(...encodedUint8Array));
120
- singleResult.extensions = {
121
- ...singleResult.extensions,
122
- ftv1: base64,
123
- };
124
- }
180
+ const reqCtx = ctxForReq.get(request);
181
+ if (!reqCtx)
182
+ return;
183
+ // onResultProcess will be called only once since we disallow async iterables
184
+ if (reqCtx.stopped)
185
+ throw new Error('Trace stopped multiple times');
186
+ reqCtx.stopped = true;
125
187
  },
126
188
  };
189
+ return [plugin, ctxForReq];
127
190
  }
128
191
  /**
129
192
  * Converts an hrtime array (as returned from process.hrtime) to nanoseconds.
@@ -144,7 +207,7 @@ function hrTimeToDurationInNanos(hrtime) {
144
207
  function nowTimestamp() {
145
208
  const totalMillis = Date.now();
146
209
  const millis = totalMillis % 1000;
147
- return new ApolloReportingProtobuf.google.protobuf.Timestamp({
210
+ return new google.protobuf.Timestamp({
148
211
  seconds: (totalMillis - millis) / 1000,
149
212
  nanos: millis * 1e6,
150
213
  });
@@ -175,7 +238,7 @@ function ensureParentTraceNode(ctx, path) {
175
238
  return newTraceNode(ctx, path.prev);
176
239
  }
177
240
  function newTraceNode(ctx, path) {
178
- const node = new ApolloReportingProtobuf.Trace.Node();
241
+ const node = new Trace.Node();
179
242
  const id = path.key;
180
243
  if (typeof id === 'number') {
181
244
  node.index = id;
@@ -188,8 +251,8 @@ function newTraceNode(ctx, path) {
188
251
  parentNode.child.push(node);
189
252
  return node;
190
253
  }
191
- function handleErrors(ctx, errors, rewriteError) {
192
- if (ctx.stopped) {
254
+ function handleErrors(reqCtx, ctx, errors, rewriteError) {
255
+ if (reqCtx.stopped) {
193
256
  throw new Error('Handling errors after tracing was stopped');
194
257
  }
195
258
  for (const err of errors) {
@@ -234,9 +297,9 @@ function handleErrors(ctx, errors, rewriteError) {
234
297
  throw new Error(`Could not find node with path ${errToReport.path.join('.')}`);
235
298
  }
236
299
  }
237
- node.error.push(new ApolloReportingProtobuf.Trace.Error({
300
+ node.error.push(new Trace.Error({
238
301
  message: errToReport.message,
239
- location: (errToReport.locations || []).map(({ line, column }) => new ApolloReportingProtobuf.Trace.Location({ line, column })),
302
+ location: (errToReport.locations || []).map(({ line, column }) => new Trace.Location({ line, column })),
240
303
  json: JSON.stringify(errToReport),
241
304
  }));
242
305
  }
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@graphql-yoga/plugin-apollo-inline-trace",
3
- "version": "3.6.3",
3
+ "version": "3.7.0-alpha-20240815202356-b651177c",
4
4
  "description": "Apollo's federated tracing plugin for GraphQL Yoga.",
5
5
  "peerDependencies": {
6
6
  "@graphql-tools/utils": "^10.0.0",
7
7
  "@whatwg-node/fetch": "^0.9.17",
8
8
  "graphql": "^15.2.0 || ^16.0.0",
9
- "graphql-yoga": "^5.6.3"
9
+ "graphql-yoga": "^5.7.0-alpha-20240815202356-b651177c"
10
10
  },
11
11
  "dependencies": {
12
+ "@apollo/usage-reporting-protobuf": "^4.1.1",
12
13
  "@envelop/on-resolve": "^4.0.0",
13
- "apollo-reporting-protobuf": "^3.3.2",
14
14
  "tslib": "^2.5.2"
15
15
  },
16
16
  "repository": {
@@ -1,5 +1,24 @@
1
1
  import { GraphQLError } from 'graphql';
2
2
  import { Plugin, YogaInitialContext } from 'graphql-yoga';
3
+ import { google, Trace } from '@apollo/usage-reporting-protobuf';
4
+ export interface ApolloInlineRequestTraceContext {
5
+ startHrTime: [number, number];
6
+ traceStartTimestamp: google.protobuf.Timestamp;
7
+ traces: Map<YogaInitialContext, ApolloInlineGraphqlTraceContext>;
8
+ /**
9
+ * graphql-js can continue to execute more fields indefinitely after
10
+ * `execute()` resolves. That's because parallelism on a selection set
11
+ * is implemented using `Promise.all`, and as soon as one field
12
+ * throws an error, the combined Promise resolves, but there's no
13
+ * "cancellation" of the rest of Promises/fields in `Promise.all`.
14
+ */
15
+ stopped: boolean;
16
+ }
17
+ export interface ApolloInlineGraphqlTraceContext {
18
+ rootNode: Trace.Node;
19
+ trace: Trace;
20
+ nodes: Map<string, Trace.Node>;
21
+ }
3
22
  export interface ApolloInlineTracePluginOptions {
4
23
  /**
5
24
  * Format errors before being sent for tracing. Beware that only the error
@@ -8,6 +27,13 @@ export interface ApolloInlineTracePluginOptions {
8
27
  * Return `null` to skip reporting error.
9
28
  */
10
29
  rewriteError?: (err: GraphQLError) => GraphQLError | null;
30
+ /**
31
+ * Allows to entirely disable tracing based on the HTTP request
32
+ * @param request HTTP request from the execution context
33
+ * @returns If true is returned (either as is or wrapped in Promise), traces for this request will
34
+ * not be generated.
35
+ */
36
+ ignoreRequest?: (request: Request) => Promise<boolean> | boolean;
11
37
  }
12
38
  /**
13
39
  * Produces Apollo's base64 trace protocol containing timing, resolution and
@@ -19,3 +45,11 @@ export interface ApolloInlineTracePluginOptions {
19
45
  * it to Apollo's usage reporting ingress.
20
46
  */
21
47
  export declare function useApolloInlineTrace(options?: ApolloInlineTracePluginOptions): Plugin<YogaInitialContext>;
48
+ /**
49
+ * Instrument GraphQL request processing pipeline and creates Apollo compatible tracing data.
50
+ *
51
+ * This is meant as a helper, do not use it directly. Use `useApolloInlineTrace` or `useApolloUsageReport` instead.
52
+ * @param options
53
+ * @returns A tuple with the instrumentation plugin and a WeakMap containing the tracing data
54
+ */
55
+ export declare function useApolloInstrumentation(options: ApolloInlineTracePluginOptions): readonly [Plugin, WeakMap<Request, ApolloInlineRequestTraceContext>];
@@ -1,5 +1,24 @@
1
1
  import { GraphQLError } from 'graphql';
2
2
  import { Plugin, YogaInitialContext } from 'graphql-yoga';
3
+ import { google, Trace } from '@apollo/usage-reporting-protobuf';
4
+ export interface ApolloInlineRequestTraceContext {
5
+ startHrTime: [number, number];
6
+ traceStartTimestamp: google.protobuf.Timestamp;
7
+ traces: Map<YogaInitialContext, ApolloInlineGraphqlTraceContext>;
8
+ /**
9
+ * graphql-js can continue to execute more fields indefinitely after
10
+ * `execute()` resolves. That's because parallelism on a selection set
11
+ * is implemented using `Promise.all`, and as soon as one field
12
+ * throws an error, the combined Promise resolves, but there's no
13
+ * "cancellation" of the rest of Promises/fields in `Promise.all`.
14
+ */
15
+ stopped: boolean;
16
+ }
17
+ export interface ApolloInlineGraphqlTraceContext {
18
+ rootNode: Trace.Node;
19
+ trace: Trace;
20
+ nodes: Map<string, Trace.Node>;
21
+ }
3
22
  export interface ApolloInlineTracePluginOptions {
4
23
  /**
5
24
  * Format errors before being sent for tracing. Beware that only the error
@@ -8,6 +27,13 @@ export interface ApolloInlineTracePluginOptions {
8
27
  * Return `null` to skip reporting error.
9
28
  */
10
29
  rewriteError?: (err: GraphQLError) => GraphQLError | null;
30
+ /**
31
+ * Allows to entirely disable tracing based on the HTTP request
32
+ * @param request HTTP request from the execution context
33
+ * @returns If true is returned (either as is or wrapped in Promise), traces for this request will
34
+ * not be generated.
35
+ */
36
+ ignoreRequest?: (request: Request) => Promise<boolean> | boolean;
11
37
  }
12
38
  /**
13
39
  * Produces Apollo's base64 trace protocol containing timing, resolution and
@@ -19,3 +45,11 @@ export interface ApolloInlineTracePluginOptions {
19
45
  * it to Apollo's usage reporting ingress.
20
46
  */
21
47
  export declare function useApolloInlineTrace(options?: ApolloInlineTracePluginOptions): Plugin<YogaInitialContext>;
48
+ /**
49
+ * Instrument GraphQL request processing pipeline and creates Apollo compatible tracing data.
50
+ *
51
+ * This is meant as a helper, do not use it directly. Use `useApolloInlineTrace` or `useApolloUsageReport` instead.
52
+ * @param options
53
+ * @returns A tuple with the instrumentation plugin and a WeakMap containing the tracing data
54
+ */
55
+ export declare function useApolloInstrumentation(options: ApolloInlineTracePluginOptions): readonly [Plugin, WeakMap<Request, ApolloInlineRequestTraceContext>];