@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 +149 -86
- package/esm/index.js +149 -86
- package/package.json +3 -3
- package/typings/index.d.cts +34 -0
- package/typings/index.d.ts +34 -0
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
|
-
|
|
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 =
|
|
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
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
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 (
|
|
30
|
-
return
|
|
31
|
-
|
|
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(
|
|
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(
|
|
116
|
+
node.endTime = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
|
|
44
117
|
};
|
|
45
118
|
}));
|
|
46
119
|
},
|
|
47
120
|
onRequest({ request }) {
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
304
|
+
node.error.push(new usage_reporting_protobuf_1.Trace.Error({
|
|
242
305
|
message: errToReport.message,
|
|
243
|
-
location: (errToReport.locations || []).map(({ line, column }) => new
|
|
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
|
|
2
|
-
import {
|
|
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 =
|
|
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
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
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 (
|
|
26
|
-
return
|
|
27
|
-
|
|
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(
|
|
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(
|
|
112
|
+
node.endTime = hrTimeToDurationInNanos(process.hrtime(reqCtx.startHrTime));
|
|
40
113
|
};
|
|
41
114
|
}));
|
|
42
115
|
},
|
|
43
116
|
onRequest({ request }) {
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
300
|
+
node.error.push(new Trace.Error({
|
|
238
301
|
message: errToReport.message,
|
|
239
|
-
location: (errToReport.locations || []).map(({ line, column }) => new
|
|
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.
|
|
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.
|
|
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": {
|
package/typings/index.d.cts
CHANGED
|
@@ -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>];
|
package/typings/index.d.ts
CHANGED
|
@@ -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>];
|