@graphql-yoga/plugin-apollo-usage-report 0.13.0 → 0.13.1-alpha-20260116132159-d18a95d04a1e11d197fdf672a8be4836ceed0818
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +152 -0
- package/dist/index.d.cts +125 -0
- package/dist/index.d.mts +125 -0
- package/dist/index.mjs +151 -0
- package/dist/reporter.cjs +118 -0
- package/dist/reporter.d.cts +23 -0
- package/dist/reporter.d.mts +23 -0
- package/dist/reporter.mjs +117 -0
- package/dist/stats.cjs +344 -0
- package/dist/stats.d.cts +123 -0
- package/dist/stats.d.mts +123 -0
- package/dist/stats.mjs +344 -0
- package/package.json +53 -28
- package/LICENSE +0 -23
- package/cjs/index.js +0 -186
- package/cjs/package.json +0 -1
- package/cjs/reporter.js +0 -142
- package/cjs/stats.js +0 -547
- package/esm/index.js +0 -182
- package/esm/reporter.js +0 -136
- package/esm/stats.js +0 -539
- package/typings/index.d.cts +0 -122
- package/typings/index.d.ts +0 -122
- package/typings/reporter.d.cts +0 -21
- package/typings/reporter.d.ts +0 -21
- package/typings/stats.d.cts +0 -136
- package/typings/stats.d.ts +0 -136
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const require_reporter = require('./reporter.cjs');
|
|
2
|
+
let graphql = require("graphql");
|
|
3
|
+
let graphql_yoga = require("graphql-yoga");
|
|
4
|
+
let _apollo_utils_usagereporting = require("@apollo/utils.usagereporting");
|
|
5
|
+
let _graphql_tools_utils = require("@graphql-tools/utils");
|
|
6
|
+
let _graphql_yoga_plugin_apollo_inline_trace = require("@graphql-yoga/plugin-apollo-inline-trace");
|
|
7
|
+
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
function useApolloUsageReport(options = {}) {
|
|
10
|
+
const [instrumentation, ctxForReq] = (0, _graphql_yoga_plugin_apollo_inline_trace.useApolloInstrumentation)(options);
|
|
11
|
+
const makeReporter = options.reporter ?? ((...args) => new require_reporter.Reporter(...args));
|
|
12
|
+
let schemaIdSet$;
|
|
13
|
+
let currentSchema;
|
|
14
|
+
let yoga;
|
|
15
|
+
let reporter;
|
|
16
|
+
const setCurrentSchema = async (schema) => {
|
|
17
|
+
try {
|
|
18
|
+
currentSchema = {
|
|
19
|
+
id: await hashSHA256((0, _graphql_tools_utils.printSchemaWithDirectives)(schema), yoga.fetchAPI),
|
|
20
|
+
schema
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error("Failed to calculate schema hash: ", error);
|
|
24
|
+
}
|
|
25
|
+
schemaIdSet$ = void 0;
|
|
26
|
+
};
|
|
27
|
+
const logger = Object.fromEntries([
|
|
28
|
+
"error",
|
|
29
|
+
"warn",
|
|
30
|
+
"info",
|
|
31
|
+
"debug"
|
|
32
|
+
].map((level) => [level, (...messages) => yoga.logger[level]("[ApolloUsageReport]", ...messages)]));
|
|
33
|
+
let clientNameFactory = (req) => req.headers.get("apollographql-client-name");
|
|
34
|
+
if (typeof options.clientName === "function") clientNameFactory = options.clientName;
|
|
35
|
+
let clientVersionFactory = (req) => req.headers.get("apollographql-client-version");
|
|
36
|
+
if (typeof options.clientVersion === "function") clientVersionFactory = options.clientVersion;
|
|
37
|
+
return { onPluginInit({ addPlugin }) {
|
|
38
|
+
addPlugin(instrumentation);
|
|
39
|
+
addPlugin({
|
|
40
|
+
onYogaInit(args) {
|
|
41
|
+
yoga = args.yoga;
|
|
42
|
+
reporter = makeReporter(options, yoga, logger);
|
|
43
|
+
if (!require_reporter.getEnvVar("APOLLO_KEY", options.apiKey)) throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`);
|
|
44
|
+
if (!require_reporter.getEnvVar("APOLLO_GRAPH_REF", options.graphRef)) throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`);
|
|
45
|
+
if (!schemaIdSet$ && !currentSchema) {
|
|
46
|
+
const { schema } = yoga.getEnveloped();
|
|
47
|
+
if (schema) schemaIdSet$ = setCurrentSchema(schema);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
onSchemaChange({ schema }) {
|
|
51
|
+
if (schema && yoga) schemaIdSet$ = setCurrentSchema(schema);
|
|
52
|
+
},
|
|
53
|
+
onRequestParse() {
|
|
54
|
+
return schemaIdSet$;
|
|
55
|
+
},
|
|
56
|
+
onParse() {
|
|
57
|
+
return function onParseEnd({ result, context }) {
|
|
58
|
+
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
59
|
+
if (!ctx) {
|
|
60
|
+
logger.debug("operation tracing context not found, this operation will not be traced.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.schemaId = currentSchema.id;
|
|
64
|
+
if (isDocumentNode(result)) {
|
|
65
|
+
if ((0, graphql.getOperationAST)(result, context.params.operationName)) return;
|
|
66
|
+
ctx.operationKey = `## GraphQLUnknownOperationName\n`;
|
|
67
|
+
} else ctx.operationKey = "## GraphQLParseFailure\n";
|
|
68
|
+
if (!options.sendUnexecutableOperationDocuments) {
|
|
69
|
+
ctxForReq.delete(context.request);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
ctx.trace.unexecutedOperationName = context.params.operationName || "";
|
|
73
|
+
ctx.trace.unexecutedOperationBody = context.params.query || "";
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
onValidate({ params: { documentAST: document } }) {
|
|
77
|
+
return ({ valid, context }) => {
|
|
78
|
+
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
79
|
+
if (!ctx) {
|
|
80
|
+
logger.debug("operation tracing context not found, this operation will not be traced.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (valid) {
|
|
84
|
+
if (!currentSchema) throw new Error("should not happen: schema doesn't exists");
|
|
85
|
+
const opName = (0, graphql.getOperationAST)(document, context.params.operationName)?.name?.value;
|
|
86
|
+
ctx.referencedFieldsByType = (0, _apollo_utils_usagereporting.calculateReferencedFieldsByType)({
|
|
87
|
+
document,
|
|
88
|
+
schema: currentSchema.schema,
|
|
89
|
+
resolvedOperationName: opName ?? null
|
|
90
|
+
});
|
|
91
|
+
ctx.operationKey = `# ${opName || "-"}\n${(0, _apollo_utils_usagereporting.usageReportingSignature)(document, opName ?? "")}`;
|
|
92
|
+
} else if (options.sendUnexecutableOperationDocuments) {
|
|
93
|
+
ctx.operationKey = "## GraphQLValidationFailure\n";
|
|
94
|
+
ctx.trace.unexecutedOperationName = context.params.operationName ?? "";
|
|
95
|
+
ctx.trace.unexecutedOperationBody = context.params.query ?? "";
|
|
96
|
+
} else ctxForReq.delete(context.request);
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
onResultProcess({ request, result, serverContext }) {
|
|
100
|
+
if ((0, graphql_yoga.isAsyncIterable)(result)) {
|
|
101
|
+
logger.debug("async iterable results not implemented for now");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const reqCtx = ctxForReq.get(request);
|
|
105
|
+
if (!reqCtx) {
|
|
106
|
+
logger.debug("operation tracing context not found, this operation will not be traced.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const trace of reqCtx.traces.values()) {
|
|
110
|
+
if (!trace.schemaId || !trace.operationKey) {
|
|
111
|
+
logger.debug("Misformed trace, missing operation key or schema id");
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const clientName = clientNameFactory(request);
|
|
115
|
+
if (clientName) trace.trace.clientName = clientName;
|
|
116
|
+
const clientVersion = clientVersionFactory(request);
|
|
117
|
+
if (clientVersion) trace.trace.clientVersion = clientVersion;
|
|
118
|
+
serverContext.waitUntil(reporter.addTrace(currentSchema.id, {
|
|
119
|
+
statsReportKey: trace.operationKey,
|
|
120
|
+
trace: trace.trace,
|
|
121
|
+
referencedFieldsByType: trace.referencedFieldsByType ?? {},
|
|
122
|
+
asTrace: true,
|
|
123
|
+
nonFtv1ErrorPaths: [],
|
|
124
|
+
maxTraceBytes: options.maxTraceSize
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async onDispose() {
|
|
129
|
+
await reporter?.flush();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
134
|
+
async function hashSHA256(text, api = globalThis) {
|
|
135
|
+
const inputUint8Array = new api.TextEncoder().encode(text);
|
|
136
|
+
const arrayBuf = await api.crypto.subtle.digest({ name: "SHA-256" }, inputUint8Array);
|
|
137
|
+
const outputUint8Array = new Uint8Array(arrayBuf);
|
|
138
|
+
let hash = "";
|
|
139
|
+
for (const byte of outputUint8Array) {
|
|
140
|
+
const hex = byte.toString(16);
|
|
141
|
+
hash += "00".slice(0, Math.max(0, 2 - hex.length)) + hex;
|
|
142
|
+
}
|
|
143
|
+
return hash;
|
|
144
|
+
}
|
|
145
|
+
function isDocumentNode(data) {
|
|
146
|
+
const isObject = (data$1) => !!data$1 && typeof data$1 === "object";
|
|
147
|
+
return isObject(data) && data["kind"] === graphql.Kind.DOCUMENT;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
exports.hashSHA256 = hashSHA256;
|
|
152
|
+
exports.useApolloUsageReport = useApolloUsageReport;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Reporter } from "./reporter.cjs";
|
|
2
|
+
import { Maybe, Plugin, YogaInitialContext, YogaLogger, YogaServer } from "graphql-yoga";
|
|
3
|
+
import { calculateReferencedFieldsByType } from "@apollo/utils.usagereporting";
|
|
4
|
+
import { ApolloInlineGraphqlTraceContext, ApolloInlineRequestTraceContext, ApolloInlineTracePluginOptions } from "@graphql-yoga/plugin-apollo-inline-trace";
|
|
5
|
+
|
|
6
|
+
//#region src/index.d.ts
|
|
7
|
+
type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
|
|
8
|
+
/**
|
|
9
|
+
* The graph ref of the managed federation graph.
|
|
10
|
+
* It is composed of the graph ID and the variant (`<YOUR_GRAPH_ID>@<VARIANT>`).
|
|
11
|
+
*
|
|
12
|
+
* If not provided, `APOLLO_GRAPH_REF` environment variable is used.
|
|
13
|
+
*
|
|
14
|
+
* You can find a a graph's ref at the top of its Schema Reference page in Apollo Studio.
|
|
15
|
+
*/
|
|
16
|
+
graphRef?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The API key to use to authenticate with the managed federation up link.
|
|
19
|
+
* It needs at least the `service:read` permission.
|
|
20
|
+
*
|
|
21
|
+
* If not provided, `APOLLO_KEY` environment variable will be used instead.
|
|
22
|
+
*
|
|
23
|
+
* [Learn how to create an API key](https://www.apollographql.com/docs/federation/v1/managed-federation/setup#4-connect-the-gateway-to-studio)
|
|
24
|
+
*/
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Usage report endpoint
|
|
28
|
+
*
|
|
29
|
+
* Defaults to GraphOS endpoint (https://usage-reporting.api.apollographql.com/api/ingress/traces)
|
|
30
|
+
*/
|
|
31
|
+
endpoint?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Agent Version to report to the usage reporting API
|
|
34
|
+
*/
|
|
35
|
+
agentVersion?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Client name to report to the usage reporting API
|
|
38
|
+
*/
|
|
39
|
+
clientName?: StringFromRequestFn;
|
|
40
|
+
/**
|
|
41
|
+
* Client version to report to the usage reporting API
|
|
42
|
+
*/
|
|
43
|
+
clientVersion?: StringFromRequestFn;
|
|
44
|
+
/**
|
|
45
|
+
* The version of the runtime (like 'node v23.7.0')
|
|
46
|
+
* @default empty string.
|
|
47
|
+
*/
|
|
48
|
+
runtimeVersion?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The hostname of the machine running this server
|
|
51
|
+
* @default $HOSTNAME environment variable
|
|
52
|
+
*/
|
|
53
|
+
hostname?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The OS identification string.
|
|
56
|
+
* The format is `${os.platform()}, ${os.type()}, ${os.release()}, ${os.arch()})`
|
|
57
|
+
* @default empty string
|
|
58
|
+
*/
|
|
59
|
+
uname?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The maximum estimated size of each traces in bytes. If the estimated size is higher than this threshold,
|
|
62
|
+
* the complete trace will not be sent and will be reduced to aggregated stats.
|
|
63
|
+
*
|
|
64
|
+
* Note: GraphOS only allow for traces of 10mb maximum
|
|
65
|
+
* @default 10 * 1024 * 1024 (10mb)
|
|
66
|
+
*/
|
|
67
|
+
maxTraceSize?: number;
|
|
68
|
+
/**
|
|
69
|
+
* The maximum uncompressed size of a report in bytes.
|
|
70
|
+
* The report will be sent once this threshold is reached, even if the delay between send is not
|
|
71
|
+
* yet expired.
|
|
72
|
+
*
|
|
73
|
+
* @default 4Mb
|
|
74
|
+
*/
|
|
75
|
+
maxBatchUncompressedSize?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The maximum time in ms between reports.
|
|
78
|
+
* @default 20s
|
|
79
|
+
*/
|
|
80
|
+
maxBatchDelay?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Control if traces should be always sent.
|
|
83
|
+
* If false, the traces will be batched until a delay or size is reached.
|
|
84
|
+
* Note: This is highly not recommended in a production environment
|
|
85
|
+
*
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
alwaysSend?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Timeout in ms of a trace export tentative
|
|
91
|
+
* @default 30s
|
|
92
|
+
*/
|
|
93
|
+
exportTimeout?: number;
|
|
94
|
+
/**
|
|
95
|
+
* The class to be used to keep track of traces and send them to the GraphOS endpoint
|
|
96
|
+
* Note: This option is aimed to be used for testing purposes
|
|
97
|
+
*/
|
|
98
|
+
reporter?: (options: ApolloUsageReportOptions, yoga: YogaServer<Record<string, unknown>, Record<string, unknown>>, logger: YogaLogger) => Reporter;
|
|
99
|
+
/**
|
|
100
|
+
* Called when all retry attempts to send a report to GraphOS endpoint failed.
|
|
101
|
+
* By default, the error is logged.
|
|
102
|
+
*/
|
|
103
|
+
onError?: (err: Error) => void;
|
|
104
|
+
/**
|
|
105
|
+
* If false, unexecutable operation (with parsing or validation error) will not be sent
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
sendUnexecutableOperationDocuments?: boolean;
|
|
109
|
+
};
|
|
110
|
+
interface ApolloUsageReportRequestContext extends ApolloInlineRequestTraceContext {
|
|
111
|
+
traces: Map<YogaInitialContext, ApolloUsageReportGraphqlContext>;
|
|
112
|
+
}
|
|
113
|
+
interface ApolloUsageReportGraphqlContext extends ApolloInlineGraphqlTraceContext {
|
|
114
|
+
referencedFieldsByType?: ReturnType<typeof calculateReferencedFieldsByType>;
|
|
115
|
+
operationKey?: string;
|
|
116
|
+
schemaId?: string;
|
|
117
|
+
}
|
|
118
|
+
type StringFromRequestFn = (req: Request) => Maybe<string>;
|
|
119
|
+
declare function useApolloUsageReport(options?: ApolloUsageReportOptions): Plugin;
|
|
120
|
+
declare function hashSHA256(text: string, api?: {
|
|
121
|
+
crypto: Crypto;
|
|
122
|
+
TextEncoder: (typeof globalThis)['TextEncoder'];
|
|
123
|
+
}): Promise<string>;
|
|
124
|
+
//#endregion
|
|
125
|
+
export { ApolloUsageReportGraphqlContext, ApolloUsageReportOptions, ApolloUsageReportRequestContext, hashSHA256, useApolloUsageReport };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Reporter } from "./reporter.mjs";
|
|
2
|
+
import { Maybe, Plugin, YogaInitialContext, YogaLogger, YogaServer } from "graphql-yoga";
|
|
3
|
+
import { calculateReferencedFieldsByType } from "@apollo/utils.usagereporting";
|
|
4
|
+
import { ApolloInlineGraphqlTraceContext, ApolloInlineRequestTraceContext, ApolloInlineTracePluginOptions } from "@graphql-yoga/plugin-apollo-inline-trace";
|
|
5
|
+
|
|
6
|
+
//#region src/index.d.ts
|
|
7
|
+
type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
|
|
8
|
+
/**
|
|
9
|
+
* The graph ref of the managed federation graph.
|
|
10
|
+
* It is composed of the graph ID and the variant (`<YOUR_GRAPH_ID>@<VARIANT>`).
|
|
11
|
+
*
|
|
12
|
+
* If not provided, `APOLLO_GRAPH_REF` environment variable is used.
|
|
13
|
+
*
|
|
14
|
+
* You can find a a graph's ref at the top of its Schema Reference page in Apollo Studio.
|
|
15
|
+
*/
|
|
16
|
+
graphRef?: string;
|
|
17
|
+
/**
|
|
18
|
+
* The API key to use to authenticate with the managed federation up link.
|
|
19
|
+
* It needs at least the `service:read` permission.
|
|
20
|
+
*
|
|
21
|
+
* If not provided, `APOLLO_KEY` environment variable will be used instead.
|
|
22
|
+
*
|
|
23
|
+
* [Learn how to create an API key](https://www.apollographql.com/docs/federation/v1/managed-federation/setup#4-connect-the-gateway-to-studio)
|
|
24
|
+
*/
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Usage report endpoint
|
|
28
|
+
*
|
|
29
|
+
* Defaults to GraphOS endpoint (https://usage-reporting.api.apollographql.com/api/ingress/traces)
|
|
30
|
+
*/
|
|
31
|
+
endpoint?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Agent Version to report to the usage reporting API
|
|
34
|
+
*/
|
|
35
|
+
agentVersion?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Client name to report to the usage reporting API
|
|
38
|
+
*/
|
|
39
|
+
clientName?: StringFromRequestFn;
|
|
40
|
+
/**
|
|
41
|
+
* Client version to report to the usage reporting API
|
|
42
|
+
*/
|
|
43
|
+
clientVersion?: StringFromRequestFn;
|
|
44
|
+
/**
|
|
45
|
+
* The version of the runtime (like 'node v23.7.0')
|
|
46
|
+
* @default empty string.
|
|
47
|
+
*/
|
|
48
|
+
runtimeVersion?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The hostname of the machine running this server
|
|
51
|
+
* @default $HOSTNAME environment variable
|
|
52
|
+
*/
|
|
53
|
+
hostname?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The OS identification string.
|
|
56
|
+
* The format is `${os.platform()}, ${os.type()}, ${os.release()}, ${os.arch()})`
|
|
57
|
+
* @default empty string
|
|
58
|
+
*/
|
|
59
|
+
uname?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The maximum estimated size of each traces in bytes. If the estimated size is higher than this threshold,
|
|
62
|
+
* the complete trace will not be sent and will be reduced to aggregated stats.
|
|
63
|
+
*
|
|
64
|
+
* Note: GraphOS only allow for traces of 10mb maximum
|
|
65
|
+
* @default 10 * 1024 * 1024 (10mb)
|
|
66
|
+
*/
|
|
67
|
+
maxTraceSize?: number;
|
|
68
|
+
/**
|
|
69
|
+
* The maximum uncompressed size of a report in bytes.
|
|
70
|
+
* The report will be sent once this threshold is reached, even if the delay between send is not
|
|
71
|
+
* yet expired.
|
|
72
|
+
*
|
|
73
|
+
* @default 4Mb
|
|
74
|
+
*/
|
|
75
|
+
maxBatchUncompressedSize?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The maximum time in ms between reports.
|
|
78
|
+
* @default 20s
|
|
79
|
+
*/
|
|
80
|
+
maxBatchDelay?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Control if traces should be always sent.
|
|
83
|
+
* If false, the traces will be batched until a delay or size is reached.
|
|
84
|
+
* Note: This is highly not recommended in a production environment
|
|
85
|
+
*
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
alwaysSend?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Timeout in ms of a trace export tentative
|
|
91
|
+
* @default 30s
|
|
92
|
+
*/
|
|
93
|
+
exportTimeout?: number;
|
|
94
|
+
/**
|
|
95
|
+
* The class to be used to keep track of traces and send them to the GraphOS endpoint
|
|
96
|
+
* Note: This option is aimed to be used for testing purposes
|
|
97
|
+
*/
|
|
98
|
+
reporter?: (options: ApolloUsageReportOptions, yoga: YogaServer<Record<string, unknown>, Record<string, unknown>>, logger: YogaLogger) => Reporter;
|
|
99
|
+
/**
|
|
100
|
+
* Called when all retry attempts to send a report to GraphOS endpoint failed.
|
|
101
|
+
* By default, the error is logged.
|
|
102
|
+
*/
|
|
103
|
+
onError?: (err: Error) => void;
|
|
104
|
+
/**
|
|
105
|
+
* If false, unexecutable operation (with parsing or validation error) will not be sent
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
sendUnexecutableOperationDocuments?: boolean;
|
|
109
|
+
};
|
|
110
|
+
interface ApolloUsageReportRequestContext extends ApolloInlineRequestTraceContext {
|
|
111
|
+
traces: Map<YogaInitialContext, ApolloUsageReportGraphqlContext>;
|
|
112
|
+
}
|
|
113
|
+
interface ApolloUsageReportGraphqlContext extends ApolloInlineGraphqlTraceContext {
|
|
114
|
+
referencedFieldsByType?: ReturnType<typeof calculateReferencedFieldsByType>;
|
|
115
|
+
operationKey?: string;
|
|
116
|
+
schemaId?: string;
|
|
117
|
+
}
|
|
118
|
+
type StringFromRequestFn = (req: Request) => Maybe<string>;
|
|
119
|
+
declare function useApolloUsageReport(options?: ApolloUsageReportOptions): Plugin;
|
|
120
|
+
declare function hashSHA256(text: string, api?: {
|
|
121
|
+
crypto: Crypto;
|
|
122
|
+
TextEncoder: (typeof globalThis)['TextEncoder'];
|
|
123
|
+
}): Promise<string>;
|
|
124
|
+
//#endregion
|
|
125
|
+
export { ApolloUsageReportGraphqlContext, ApolloUsageReportOptions, ApolloUsageReportRequestContext, hashSHA256, useApolloUsageReport };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Reporter, getEnvVar } from "./reporter.mjs";
|
|
2
|
+
import { Kind, getOperationAST } from "graphql";
|
|
3
|
+
import { isAsyncIterable } from "graphql-yoga";
|
|
4
|
+
import { calculateReferencedFieldsByType, usageReportingSignature } from "@apollo/utils.usagereporting";
|
|
5
|
+
import { printSchemaWithDirectives } from "@graphql-tools/utils";
|
|
6
|
+
import { useApolloInstrumentation } from "@graphql-yoga/plugin-apollo-inline-trace";
|
|
7
|
+
|
|
8
|
+
//#region src/index.ts
|
|
9
|
+
function useApolloUsageReport(options = {}) {
|
|
10
|
+
const [instrumentation, ctxForReq] = useApolloInstrumentation(options);
|
|
11
|
+
const makeReporter = options.reporter ?? ((...args) => new Reporter(...args));
|
|
12
|
+
let schemaIdSet$;
|
|
13
|
+
let currentSchema;
|
|
14
|
+
let yoga;
|
|
15
|
+
let reporter;
|
|
16
|
+
const setCurrentSchema = async (schema) => {
|
|
17
|
+
try {
|
|
18
|
+
currentSchema = {
|
|
19
|
+
id: await hashSHA256(printSchemaWithDirectives(schema), yoga.fetchAPI),
|
|
20
|
+
schema
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error("Failed to calculate schema hash: ", error);
|
|
24
|
+
}
|
|
25
|
+
schemaIdSet$ = void 0;
|
|
26
|
+
};
|
|
27
|
+
const logger = Object.fromEntries([
|
|
28
|
+
"error",
|
|
29
|
+
"warn",
|
|
30
|
+
"info",
|
|
31
|
+
"debug"
|
|
32
|
+
].map((level) => [level, (...messages) => yoga.logger[level]("[ApolloUsageReport]", ...messages)]));
|
|
33
|
+
let clientNameFactory = (req) => req.headers.get("apollographql-client-name");
|
|
34
|
+
if (typeof options.clientName === "function") clientNameFactory = options.clientName;
|
|
35
|
+
let clientVersionFactory = (req) => req.headers.get("apollographql-client-version");
|
|
36
|
+
if (typeof options.clientVersion === "function") clientVersionFactory = options.clientVersion;
|
|
37
|
+
return { onPluginInit({ addPlugin }) {
|
|
38
|
+
addPlugin(instrumentation);
|
|
39
|
+
addPlugin({
|
|
40
|
+
onYogaInit(args) {
|
|
41
|
+
yoga = args.yoga;
|
|
42
|
+
reporter = makeReporter(options, yoga, logger);
|
|
43
|
+
if (!getEnvVar("APOLLO_KEY", options.apiKey)) throw new Error(`[ApolloUsageReport] Missing API key. Please provide one in plugin options or with 'APOLLO_KEY' environment variable.`);
|
|
44
|
+
if (!getEnvVar("APOLLO_GRAPH_REF", options.graphRef)) throw new Error(`[ApolloUsageReport] Missing Graph Ref. Please provide one in plugin options or with 'APOLLO_GRAPH_REF' environment variable.`);
|
|
45
|
+
if (!schemaIdSet$ && !currentSchema) {
|
|
46
|
+
const { schema } = yoga.getEnveloped();
|
|
47
|
+
if (schema) schemaIdSet$ = setCurrentSchema(schema);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
onSchemaChange({ schema }) {
|
|
51
|
+
if (schema && yoga) schemaIdSet$ = setCurrentSchema(schema);
|
|
52
|
+
},
|
|
53
|
+
onRequestParse() {
|
|
54
|
+
return schemaIdSet$;
|
|
55
|
+
},
|
|
56
|
+
onParse() {
|
|
57
|
+
return function onParseEnd({ result, context }) {
|
|
58
|
+
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
59
|
+
if (!ctx) {
|
|
60
|
+
logger.debug("operation tracing context not found, this operation will not be traced.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
ctx.schemaId = currentSchema.id;
|
|
64
|
+
if (isDocumentNode(result)) {
|
|
65
|
+
if (getOperationAST(result, context.params.operationName)) return;
|
|
66
|
+
ctx.operationKey = `## GraphQLUnknownOperationName\n`;
|
|
67
|
+
} else ctx.operationKey = "## GraphQLParseFailure\n";
|
|
68
|
+
if (!options.sendUnexecutableOperationDocuments) {
|
|
69
|
+
ctxForReq.delete(context.request);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
ctx.trace.unexecutedOperationName = context.params.operationName || "";
|
|
73
|
+
ctx.trace.unexecutedOperationBody = context.params.query || "";
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
onValidate({ params: { documentAST: document } }) {
|
|
77
|
+
return ({ valid, context }) => {
|
|
78
|
+
const ctx = ctxForReq.get(context.request)?.traces.get(context);
|
|
79
|
+
if (!ctx) {
|
|
80
|
+
logger.debug("operation tracing context not found, this operation will not be traced.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (valid) {
|
|
84
|
+
if (!currentSchema) throw new Error("should not happen: schema doesn't exists");
|
|
85
|
+
const opName = getOperationAST(document, context.params.operationName)?.name?.value;
|
|
86
|
+
ctx.referencedFieldsByType = calculateReferencedFieldsByType({
|
|
87
|
+
document,
|
|
88
|
+
schema: currentSchema.schema,
|
|
89
|
+
resolvedOperationName: opName ?? null
|
|
90
|
+
});
|
|
91
|
+
ctx.operationKey = `# ${opName || "-"}\n${usageReportingSignature(document, opName ?? "")}`;
|
|
92
|
+
} else if (options.sendUnexecutableOperationDocuments) {
|
|
93
|
+
ctx.operationKey = "## GraphQLValidationFailure\n";
|
|
94
|
+
ctx.trace.unexecutedOperationName = context.params.operationName ?? "";
|
|
95
|
+
ctx.trace.unexecutedOperationBody = context.params.query ?? "";
|
|
96
|
+
} else ctxForReq.delete(context.request);
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
onResultProcess({ request, result, serverContext }) {
|
|
100
|
+
if (isAsyncIterable(result)) {
|
|
101
|
+
logger.debug("async iterable results not implemented for now");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const reqCtx = ctxForReq.get(request);
|
|
105
|
+
if (!reqCtx) {
|
|
106
|
+
logger.debug("operation tracing context not found, this operation will not be traced.");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
for (const trace of reqCtx.traces.values()) {
|
|
110
|
+
if (!trace.schemaId || !trace.operationKey) {
|
|
111
|
+
logger.debug("Misformed trace, missing operation key or schema id");
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const clientName = clientNameFactory(request);
|
|
115
|
+
if (clientName) trace.trace.clientName = clientName;
|
|
116
|
+
const clientVersion = clientVersionFactory(request);
|
|
117
|
+
if (clientVersion) trace.trace.clientVersion = clientVersion;
|
|
118
|
+
serverContext.waitUntil(reporter.addTrace(currentSchema.id, {
|
|
119
|
+
statsReportKey: trace.operationKey,
|
|
120
|
+
trace: trace.trace,
|
|
121
|
+
referencedFieldsByType: trace.referencedFieldsByType ?? {},
|
|
122
|
+
asTrace: true,
|
|
123
|
+
nonFtv1ErrorPaths: [],
|
|
124
|
+
maxTraceBytes: options.maxTraceSize
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
async onDispose() {
|
|
129
|
+
await reporter?.flush();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
} };
|
|
133
|
+
}
|
|
134
|
+
async function hashSHA256(text, api = globalThis) {
|
|
135
|
+
const inputUint8Array = new api.TextEncoder().encode(text);
|
|
136
|
+
const arrayBuf = await api.crypto.subtle.digest({ name: "SHA-256" }, inputUint8Array);
|
|
137
|
+
const outputUint8Array = new Uint8Array(arrayBuf);
|
|
138
|
+
let hash = "";
|
|
139
|
+
for (const byte of outputUint8Array) {
|
|
140
|
+
const hex = byte.toString(16);
|
|
141
|
+
hash += "00".slice(0, Math.max(0, 2 - hex.length)) + hex;
|
|
142
|
+
}
|
|
143
|
+
return hash;
|
|
144
|
+
}
|
|
145
|
+
function isDocumentNode(data) {
|
|
146
|
+
const isObject = (data$1) => !!data$1 && typeof data$1 === "object";
|
|
147
|
+
return isObject(data) && data["kind"] === Kind.DOCUMENT;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
export { hashSHA256, useApolloUsageReport };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const require_stats = require('./stats.cjs');
|
|
2
|
+
let _apollo_usage_reporting_protobuf = require("@apollo/usage-reporting-protobuf");
|
|
3
|
+
|
|
4
|
+
//#region src/reporter.ts
|
|
5
|
+
const DEFAULT_REPORTING_ENDPOINT = "https://usage-reporting.api.apollographql.com/api/ingress/traces";
|
|
6
|
+
var Reporter = class {
|
|
7
|
+
reportHeaders;
|
|
8
|
+
options;
|
|
9
|
+
reportsBySchema = {};
|
|
10
|
+
nextSendAfterDelay;
|
|
11
|
+
sending = [];
|
|
12
|
+
constructor(options, yoga, logger) {
|
|
13
|
+
this.yoga = yoga;
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
this.options = {
|
|
16
|
+
...options,
|
|
17
|
+
maxBatchDelay: options.maxBatchDelay ?? 2e4,
|
|
18
|
+
maxBatchUncompressedSize: options.maxBatchUncompressedSize ?? 4 * 1024 * 1024,
|
|
19
|
+
maxTraceSize: options.maxTraceSize ?? 10 * 1024 * 1024,
|
|
20
|
+
exportTimeout: options.exportTimeout ?? 3e4,
|
|
21
|
+
onError: options.onError ?? ((err) => this.logger.error("Failed to send report", err))
|
|
22
|
+
};
|
|
23
|
+
this.reportHeaders = {
|
|
24
|
+
graphRef: getGraphRef(options),
|
|
25
|
+
hostname: options.hostname ?? getEnvVar("HOSTNAME") ?? "",
|
|
26
|
+
uname: options.uname ?? "",
|
|
27
|
+
runtimeVersion: options.runtimeVersion ?? "",
|
|
28
|
+
agentVersion: options.agentVersion || `graphql-yoga@${yoga.version}`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
addTrace(schemaId, options) {
|
|
32
|
+
const report = this.getReport(schemaId);
|
|
33
|
+
report.addTrace(options);
|
|
34
|
+
if (this.options.alwaysSend || report.sizeEstimator.bytes >= this.options.maxBatchUncompressedSize) return this._sendReport(schemaId);
|
|
35
|
+
this.nextSendAfterDelay ||= setTimeout(() => this.flush(), this.options.maxBatchDelay);
|
|
36
|
+
}
|
|
37
|
+
async flush() {
|
|
38
|
+
return Promise.allSettled([...this.sending, ...Object.keys(this.reportsBySchema).map((schemaId) => this._sendReport(schemaId))]);
|
|
39
|
+
}
|
|
40
|
+
async sendReport(schemaId) {
|
|
41
|
+
const sending = this._sendReport(schemaId);
|
|
42
|
+
this.sending.push(sending);
|
|
43
|
+
sending.finally(() => this.sending = this.sending?.filter((p) => p !== sending));
|
|
44
|
+
return sending;
|
|
45
|
+
}
|
|
46
|
+
async _sendReport(schemaId) {
|
|
47
|
+
const { fetchAPI: { fetch, CompressionStream, ReadableStream } } = this.yoga;
|
|
48
|
+
const report = this.reportsBySchema[schemaId];
|
|
49
|
+
if (!report) throw new Error(`No report to send for schema ${schemaId}`);
|
|
50
|
+
if (this.nextSendAfterDelay != null) {
|
|
51
|
+
clearTimeout(this.nextSendAfterDelay);
|
|
52
|
+
this.nextSendAfterDelay = void 0;
|
|
53
|
+
}
|
|
54
|
+
delete this.reportsBySchema[schemaId];
|
|
55
|
+
report.endTime = dateToProtoTimestamp(/* @__PURE__ */ new Date());
|
|
56
|
+
report.ensureCountsAreIntegers();
|
|
57
|
+
const validationError = _apollo_usage_reporting_protobuf.Report.verify(report);
|
|
58
|
+
if (validationError) throw new TypeError(`Invalid report: ${validationError}`);
|
|
59
|
+
const { apiKey = getEnvVar("APOLLO_KEY"), endpoint = DEFAULT_REPORTING_ENDPOINT } = this.options;
|
|
60
|
+
const encodedReport = _apollo_usage_reporting_protobuf.Report.encode(report).finish();
|
|
61
|
+
let lastError;
|
|
62
|
+
for (let tries = 0; tries < 5; tries++) try {
|
|
63
|
+
this.logger.debug(`Sending report (try ${tries}/5)`);
|
|
64
|
+
const response = await fetch(endpoint, {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: {
|
|
67
|
+
"content-type": "application/protobuf",
|
|
68
|
+
"content-encoding": "gzip",
|
|
69
|
+
"x-api-key": apiKey,
|
|
70
|
+
accept: "application/json"
|
|
71
|
+
},
|
|
72
|
+
body: new ReadableStream({ start(controller) {
|
|
73
|
+
controller.enqueue(encodedReport);
|
|
74
|
+
controller.close();
|
|
75
|
+
} }).pipeThrough(new CompressionStream("gzip")),
|
|
76
|
+
signal: AbortSignal.timeout(this.options.exportTimeout)
|
|
77
|
+
});
|
|
78
|
+
const result = await response.text();
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
this.logger.debug("Report sent:", result);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
throw result;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
lastError = err;
|
|
86
|
+
this.logger.error("Failed to send report:", err);
|
|
87
|
+
}
|
|
88
|
+
this.options.onError(new Error("Failed to send traces after 5 tries", { cause: lastError }));
|
|
89
|
+
}
|
|
90
|
+
getReport(schemaId) {
|
|
91
|
+
const report = this.reportsBySchema[schemaId];
|
|
92
|
+
if (report) return report;
|
|
93
|
+
return this.reportsBySchema[schemaId] = new require_stats.OurReport(new _apollo_usage_reporting_protobuf.ReportHeader({
|
|
94
|
+
...this.reportHeaders,
|
|
95
|
+
executableSchemaId: schemaId
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function getGraphRef(options) {
|
|
100
|
+
const graphRef = options.graphRef || getEnvVar("APOLLO_GRAPH_REF");
|
|
101
|
+
if (!graphRef) throw new Error("Missing GraphRef. Either provide `graphRef` option or `APOLLO_GRAPH_REF` environment variable");
|
|
102
|
+
return graphRef;
|
|
103
|
+
}
|
|
104
|
+
function getEnvVar(name, defaultValue) {
|
|
105
|
+
return globalThis.process?.env?.[name] || defaultValue || void 0;
|
|
106
|
+
}
|
|
107
|
+
function dateToProtoTimestamp(date) {
|
|
108
|
+
const totalMillis = date.getTime();
|
|
109
|
+
const millis = totalMillis % 1e3;
|
|
110
|
+
return new _apollo_usage_reporting_protobuf.google.protobuf.Timestamp({
|
|
111
|
+
seconds: (totalMillis - millis) / 1e3,
|
|
112
|
+
nanos: millis * 1e6
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
exports.Reporter = Reporter;
|
|
118
|
+
exports.getEnvVar = getEnvVar;
|