Skip to content

Commit 3f03e7b

Browse files
authored
fix: usage reports are sent at set intervals (#5361)
1 parent 706b0c8 commit 3f03e7b

File tree

5 files changed

+244
-3
lines changed

5 files changed

+244
-3
lines changed

.changeset/healthy-pears-look.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@graphql-hive/apollo': patch
3+
'@graphql-hive/core': patch
4+
'@graphql-hive/yoga': patch
5+
'@graphql-hive/envelop': patch
6+
---
7+
8+
Fixed issue where usage reports were sent only on app disposal or max batch size, now also sent at
9+
set intervals.

packages/libraries/apollo/tests/apollo.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,117 @@ test('should capture client name and version headers', async () => {
169169
clean();
170170
}, 1_000);
171171

172+
test('send usage reports in intervals', async () => {
173+
const clean = handleProcess();
174+
const fetchSpy = vi.fn(async (_input: string | URL | globalThis.Request, _init?: RequestInit) =>
175+
Response.json({}, { status: 200 }),
176+
);
177+
178+
const apollo = new ApolloServer({
179+
typeDefs,
180+
resolvers,
181+
plugins: [
182+
useHive({
183+
enabled: true,
184+
debug: false,
185+
token: 'my-token',
186+
agent: {
187+
maxRetries: 0,
188+
sendInterval: 10,
189+
timeout: 50,
190+
__testing: {
191+
fetch: fetchSpy,
192+
},
193+
},
194+
reporting: false,
195+
usage: {
196+
endpoint: 'http://apollo.localhost:4200/usage',
197+
},
198+
}),
199+
],
200+
});
201+
202+
await startStandaloneServer(apollo);
203+
204+
await http.post(
205+
'http://localhost:4000/graphql',
206+
JSON.stringify({
207+
query: /* GraphQL */ `
208+
{
209+
hello
210+
}
211+
`,
212+
}),
213+
{
214+
headers: {
215+
'content-type': 'application/json',
216+
'x-graphql-client-name': 'vitest',
217+
'x-graphql-client-version': '1.0.0',
218+
},
219+
},
220+
);
221+
222+
await waitFor(50);
223+
expect(fetchSpy).toHaveBeenCalledWith(
224+
'http://apollo.localhost:4200/usage',
225+
expect.objectContaining({
226+
body: expect.stringContaining('"client":{"name":"vitest","version":"1.0.0"}'),
227+
}),
228+
);
229+
230+
await http.post(
231+
'http://localhost:4000/graphql',
232+
JSON.stringify({
233+
query: /* GraphQL */ `
234+
{
235+
hello
236+
}
237+
`,
238+
}),
239+
{
240+
headers: {
241+
'content-type': 'application/json',
242+
'x-graphql-client-name': 'vitest',
243+
'x-graphql-client-version': '2.0.0',
244+
},
245+
},
246+
);
247+
await waitFor(50);
248+
expect(fetchSpy).toHaveBeenCalledWith(
249+
'http://apollo.localhost:4200/usage',
250+
expect.objectContaining({
251+
body: expect.stringContaining('"client":{"name":"vitest","version":"2.0.0"}'),
252+
}),
253+
);
254+
255+
await http.post(
256+
'http://localhost:4000/graphql',
257+
JSON.stringify({
258+
query: /* GraphQL */ `
259+
{
260+
hello
261+
}
262+
`,
263+
}),
264+
{
265+
headers: {
266+
'content-type': 'application/json',
267+
'x-graphql-client-name': 'vitest',
268+
'x-graphql-client-version': '3.0.0',
269+
},
270+
},
271+
);
272+
273+
await apollo.stop();
274+
expect(fetchSpy).toHaveBeenCalledWith(
275+
'http://apollo.localhost:4200/usage',
276+
expect.objectContaining({
277+
body: expect.stringContaining('"client":{"name":"vitest","version":"3.0.0"}'),
278+
}),
279+
);
280+
clean();
281+
}, 1_000);
282+
172283
describe('supergraph SDL fetcher', async () => {
173284
test('createSupergraphSDLFetcher without ETag', async () => {
174285
const supergraphSdl = 'type SuperQuery { sdl: String }';

packages/libraries/core/src/client/agent.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,24 @@ export function createAgent<TEvent>(
133133

134134
if (data.size() >= options.maxSize) {
135135
debugLog('Sending immediately');
136-
setImmediate(() => send({ throwOnError: false }));
136+
setImmediate(() => send({ throwOnError: false, skipSchedule: true }));
137137
}
138138
}
139139

140140
function sendImmediately(event: TEvent): Promise<ReadOnlyResponse | null> {
141141
data.set(event);
142142
debugLog('Sending immediately');
143-
return send({ throwOnError: true });
143+
return send({ throwOnError: true, skipSchedule: true });
144144
}
145145

146-
async function send(sendOptions?: { throwOnError?: boolean }): Promise<ReadOnlyResponse | null> {
146+
async function send(sendOptions?: {
147+
throwOnError?: boolean;
148+
skipSchedule: boolean;
149+
}): Promise<ReadOnlyResponse | null> {
147150
if (!data.size() || !enabled) {
151+
if (!sendOptions?.skipSchedule) {
152+
schedule();
153+
}
148154
return null;
149155
}
150156

@@ -183,6 +189,11 @@ export function createAgent<TEvent>(
183189
}
184190

185191
return null;
192+
})
193+
.finally(() => {
194+
if (!sendOptions?.skipSchedule) {
195+
schedule();
196+
}
186197
});
187198

188199
return response;
@@ -199,6 +210,7 @@ export function createAgent<TEvent>(
199210
}
200211

201212
await send({
213+
skipSchedule: true,
202214
throwOnError: false,
203215
});
204216
}

packages/libraries/yoga/tests/yoga.spec.ts

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,114 @@ test('should capture client name and version headers', async () => {
181181
);
182182
}, 1_000);
183183

184+
test('send usage reports in intervals', async () => {
185+
const fetchSpy = vi.fn(async (_input: string | URL | globalThis.Request, _init?: RequestInit) =>
186+
Response.json({}, { status: 200 }),
187+
);
188+
const clean = handleProcess();
189+
const hive = createHive({
190+
enabled: true,
191+
debug: false,
192+
token: 'my-token',
193+
agent: {
194+
maxRetries: 0,
195+
sendInterval: 10,
196+
timeout: 50,
197+
__testing: {
198+
fetch: fetchSpy,
199+
},
200+
},
201+
reporting: false,
202+
usage: {
203+
endpoint: 'http://yoga.localhost:4200/usage',
204+
},
205+
});
206+
207+
const yoga = createYoga({
208+
schema: createSchema({
209+
typeDefs,
210+
resolvers,
211+
}),
212+
plugins: [useHive(hive)],
213+
logging: false,
214+
});
215+
216+
await yoga.fetch(`http://localhost/graphql`, {
217+
method: 'POST',
218+
body: JSON.stringify({
219+
query: /* GraphQL */ `
220+
{
221+
hello
222+
}
223+
`,
224+
}),
225+
headers: {
226+
'content-type': 'application/json',
227+
'x-graphql-client-name': 'vitest',
228+
'x-graphql-client-version': '1.0.0',
229+
},
230+
});
231+
232+
await waitFor(50);
233+
234+
expect(fetchSpy).toHaveBeenCalledWith(
235+
'http://yoga.localhost:4200/usage',
236+
expect.objectContaining({
237+
body: expect.stringContaining('"client":{"name":"vitest","version":"1.0.0"}'),
238+
}),
239+
);
240+
241+
await yoga.fetch(`http://localhost/graphql`, {
242+
method: 'POST',
243+
body: JSON.stringify({
244+
query: /* GraphQL */ `
245+
{
246+
hello
247+
}
248+
`,
249+
}),
250+
headers: {
251+
'content-type': 'application/json',
252+
'x-graphql-client-name': 'vitest',
253+
'x-graphql-client-version': '2.0.0',
254+
},
255+
});
256+
257+
await waitFor(50);
258+
259+
expect(fetchSpy).toHaveBeenCalledWith(
260+
'http://yoga.localhost:4200/usage',
261+
expect.objectContaining({
262+
body: expect.stringContaining('"client":{"name":"vitest","version":"2.0.0"}'),
263+
}),
264+
);
265+
266+
await yoga.fetch(`http://localhost/graphql`, {
267+
method: 'POST',
268+
body: JSON.stringify({
269+
query: /* GraphQL */ `
270+
{
271+
hello
272+
}
273+
`,
274+
}),
275+
headers: {
276+
'content-type': 'application/json',
277+
'x-graphql-client-name': 'vitest',
278+
'x-graphql-client-version': '3.0.0',
279+
},
280+
});
281+
282+
await hive.dispose();
283+
expect(fetchSpy).toHaveBeenCalledWith(
284+
'http://yoga.localhost:4200/usage',
285+
expect.objectContaining({
286+
body: expect.stringContaining('"client":{"name":"vitest","version":"3.0.0"}'),
287+
}),
288+
);
289+
clean();
290+
}, 1_000);
291+
184292
test('reports usage', async ({ expect }) => {
185293
const graphqlScope = nock('http://localhost')
186294
.post('/usage', body => {

pnpm-lock.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)