Skip to content

Commit

Permalink
feat(apollo-usage-report): send Apollo clientName and clientVersion w…
Browse files Browse the repository at this point in the history
…ith trace (#3448)

* feat(apollo-usage-report): send Apollo clientName and clientVersion with trace

* Inject version

* Changeset

---------

Co-authored-by: Tomas Kroupa <[email protected]>
Co-authored-by: Arda TANRIKULU <[email protected]>
  • Loading branch information
3 people committed Oct 31, 2024
1 parent ac2967d commit 18fe916
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-apollo-usage-report": patch
---

- Send Apollo `clientName`, `clientVersion` and `agentVersion` (agent name) with trace.
5 changes: 5 additions & 0 deletions .changeset/early-ghosts-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'graphql-yoga': minor
---

Add `version` property to get version of Yoga
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"apollo"
],
"scripts": {
"build": "pnpm --filter=@graphql-yoga/graphiql run build && pnpm --filter=@graphql-yoga/render-graphiql run build && pnpm --filter=graphql-yoga run generate-graphiql-html && bob build",
"build": "pnpm --filter=@graphql-yoga/graphiql run build && pnpm --filter=@graphql-yoga/render-graphiql run build && pnpm --filter=graphql-yoga run generate-graphiql-html&& pnpm --filter=graphql-yoga run inject-version && bob build",
"build-website": "pnpm build && cd website && pnpm build",
"changeset": "changeset",
"check": "pnpm -r run check",
Expand Down
3 changes: 3 additions & 0 deletions packages/graphql-yoga/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"scripts": {
"check": "tsc --pretty --noEmit",
"generate-graphiql-html": "node scripts/generate-graphiql-html.js",
"inject-version": "node scripts/inject-version.js",
"postinstall": "node node_modules/puppeteer/install.js",
"prepack": "bob prepack"
},
Expand Down Expand Up @@ -69,7 +70,9 @@
"@jest/globals": "^29.2.1",
"@n1ru4l/in-memory-live-query-store": "0.10.0",
"@repeaterjs/repeater": "^3.0.4",
"@types/globby": "^9.1.0",
"@types/node": "20.14.12",
"globby": "^14.0.2",
"graphql": "^16.0.1",
"graphql-http": "^1.18.0",
"graphql-scalars": "1.22.2",
Expand Down
12 changes: 12 additions & 0 deletions packages/graphql-yoga/scripts/inject-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { promises as fs } from 'node:fs';
import { globby } from 'globby';
import packageJson from '../package.json' with { type: 'json' };

const files = await globby(['dist/**/*.js']);

const yogaVersion = packageJson.version;

for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
await fs.writeFile(file, content.replace(/__YOGA_VERSION__/g, yogaVersion));
}
1 change: 1 addition & 0 deletions packages/graphql-yoga/src/landing-page.html
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@
</svg>
</div>
<h1>GraphQL Yoga</h1>
<p>Version: __YOGA_VERSION__</p>
</div>
<h2>The batteries-included cross-platform GraphQL Server.</h2>
<div class="buttons">
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql-yoga/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ export class YogaServer<
private maskedErrorsOpts: YogaMaskedErrorOpts | null;
private id: string;

version = '__YOGA_VERSION__';

constructor(options?: YogaServerOptions<TServerContext, TUserContext>) {
this.id = options?.id ?? 'yoga';

Expand Down
65 changes: 61 additions & 4 deletions packages/plugins/apollo-usage-report/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { printSchema, stripIgnoredCharacters } from 'graphql';
import {
isAsyncIterable,
Maybe,
Plugin,
YogaInitialContext,
YogaLogger,
Expand Down Expand Up @@ -39,6 +40,18 @@ type ApolloUsageReportOptions = ApolloInlineTracePluginOptions & {
* Defaults to GraphOS endpoint (https://usage-reporting.api.apollographql.com/api/ingress/traces)
*/
endpoint?: string;
/**
* Agent Version to report to the usage reporting API
*/
agentVersion?: string;
/**
* Client name to report to the usage reporting API
*/
clientName?: StringFromRequestFn | string;
/**
* Client version to report to the usage reporting API
*/
clientVersion?: StringFromRequestFn | string;
};

export interface ApolloUsageReportRequestContext extends ApolloInlineRequestTraceContext {
Expand All @@ -57,6 +70,8 @@ function getEnvVar<T>(name: string, defaultValue?: T) {
const DEFAULT_REPORTING_ENDPOINT =
'https://usage-reporting.api.apollographql.com/api/ingress/traces';

type StringFromRequestFn = (req: Request) => Maybe<string>;

export function useApolloUsageReport(options: ApolloUsageReportOptions = {}): Plugin {
const [instrumentation, ctxForReq] = useApolloInstrumentation(options) as [
Plugin,
Expand All @@ -67,11 +82,36 @@ export function useApolloUsageReport(options: ApolloUsageReportOptions = {}): Pl
let fetchAPI: FetchAPI;
let schemaId: string;

let yogaVersion: string;
let agentVersion = '0.0.0';

let clientNameFactory: StringFromRequestFn = req =>
req.headers.get('apollographql-client-name') || 'graphql-yoga';

if (typeof options.clientName === 'string') {
const clientName = options.clientName;
clientNameFactory = () => clientName;
} else if (typeof options.clientName === 'function') {
clientNameFactory = options.clientName;
}

let clientVersionFactory: StringFromRequestFn = req =>
req.headers.get('apollographql-client-version') || yogaVersion;

if (typeof options.clientVersion === 'string') {
const clientVersion = options.clientVersion;
clientVersionFactory = () => clientVersion;
} else if (typeof options.clientVersion === 'function') {
clientVersionFactory = options.clientVersion;
}

return {
onPluginInit({ addPlugin }) {
addPlugin(instrumentation);
addPlugin({
onYogaInit(args) {
yogaVersion = args.yoga.version;
agentVersion = options.agentVersion || `graphql-yoga@${yogaVersion}`;
fetchAPI = args.yoga.fetchAPI;
logger = Object.fromEntries(
(['error', 'warn', 'info', 'debug'] as const).map(level => [
Expand Down Expand Up @@ -134,18 +174,32 @@ export function useApolloUsageReport(options: ApolloUsageReportOptions = {}): Pl
// Each operation in a batched request can belongs to a different schema.
// Apollo doesn't allow to send batch queries for multiple schemas in the same batch
const tracesPerSchema: Record<string, Report['tracesPerQuery']> = {};

for (const trace of reqCtx.traces.values()) {
if (!trace.schemaId || !trace.operationKey) {
throw new TypeError('Misformed trace, missing operation key or schema id');
}

const clientName = clientNameFactory(request);
if (clientName) {
trace.trace.clientName = clientName;
}

const clientVersion = clientVersionFactory(request);
if (clientVersion) {
trace.trace.clientVersion = clientVersion;
}

tracesPerSchema[trace.schemaId] ||= {};
tracesPerSchema[trace.schemaId][trace.operationKey] ||= { trace: [] };
tracesPerSchema[trace.schemaId][trace.operationKey].trace?.push(trace.trace);
}

for (const schemaId in tracesPerSchema) {
const tracesPerQuery = tracesPerSchema[schemaId];
serverContext.waitUntil(sendTrace(options, logger, fetchAPI, schemaId, tracesPerQuery));
serverContext.waitUntil(
sendTrace(options, logger, fetchAPI.fetch, schemaId, tracesPerQuery, agentVersion),
);
}
},
});
Expand Down Expand Up @@ -174,9 +228,10 @@ export async function hashSHA256(
async function sendTrace(
options: ApolloUsageReportOptions,
logger: YogaLogger,
{ fetch }: FetchAPI,
fetch: FetchAPI['fetch'],
schemaId: string,
tracesPerQuery: Report['tracesPerQuery'],
agentVersion: string,
) {
const {
graphRef = getEnvVar('APOLLO_GRAPH_REF'),
Expand All @@ -187,6 +242,7 @@ async function sendTrace(
try {
const body = Report.encode({
header: {
agentVersion,
graphRef,
executableSchemaId: schemaId,
},
Expand All @@ -204,10 +260,11 @@ async function sendTrace(
},
body,
});
const responseText = await response.text();
if (response.ok) {
logger.debug('Traces sent:', await response.text());
logger.debug('Traces sent:', responseText);
} else {
logger.error('Failed to send trace:', await response.text());
logger.error('Failed to send trace:', responseText);
}
} catch (err) {
logger.error('Failed to send trace:', err);
Expand Down
57 changes: 57 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 18fe916

Please sign in to comment.