From 54144f318716f0fc0df01aef4da68443d0542c3e Mon Sep 17 00:00:00 2001 From: Jude Gao Date: Thu, 12 Dec 2024 15:45:02 -0500 Subject: [PATCH] refactor post-payload --- .../telemetry/post-telemetry-payload.test.ts | 89 +++++++++++++++++++ ...t-payload.ts => post-telemetry-payload.ts} | 21 ++++- packages/next/src/telemetry/storage.ts | 31 ++----- 3 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 packages/next/src/telemetry/post-telemetry-payload.test.ts rename packages/next/src/telemetry/{post-payload.ts => post-telemetry-payload.ts} (64%) diff --git a/packages/next/src/telemetry/post-telemetry-payload.test.ts b/packages/next/src/telemetry/post-telemetry-payload.test.ts new file mode 100644 index 00000000000000..3bb166e3642566 --- /dev/null +++ b/packages/next/src/telemetry/post-telemetry-payload.test.ts @@ -0,0 +1,89 @@ +import { postNextTelemetryPayload } from './post-telemetry-payload' + +describe('postNextTelemetryPayload', () => { + let originalFetch: typeof fetch + + beforeEach(() => { + originalFetch = global.fetch + }) + + afterEach(() => { + global.fetch = originalFetch + }) + + it('sends telemetry payload successfully', async () => { + const mockFetch = jest.fn().mockResolvedValue({ + ok: true, + }) + global.fetch = mockFetch + + const payload = { + meta: { version: '1.0' }, + context: { + anonymousId: 'test-id', + projectId: 'test-project', + sessionId: 'test-session', + }, + events: [ + { + eventName: 'test-event', + fields: { foo: 'bar' }, + }, + ], + } + + await postNextTelemetryPayload(payload) + + expect(mockFetch).toHaveBeenCalledWith( + 'https://telemetry.nextjs.org/api/v1/record', + { + method: 'POST', + body: JSON.stringify(payload), + headers: { 'content-type': 'application/json' }, + signal: expect.any(AbortSignal), + } + ) + }) + + it('retries on failure', async () => { + const mockFetch = jest + .fn() + .mockRejectedValueOnce(new Error('Network error')) + .mockResolvedValueOnce({ ok: true }) + global.fetch = mockFetch + + const payload = { + meta: {}, + context: { + anonymousId: 'test-id', + projectId: 'test-project', + sessionId: 'test-session', + }, + events: [], + } + + await postNextTelemetryPayload(payload) + + expect(mockFetch).toHaveBeenCalledTimes(2) + }) + + it('swallows errors after retries exhausted', async () => { + const mockFetch = jest.fn().mockRejectedValue(new Error('Network error')) + global.fetch = mockFetch + + const payload = { + meta: {}, + context: { + anonymousId: 'test-id', + projectId: 'test-project', + sessionId: 'test-session', + }, + events: [], + } + + // Should not throw + await postNextTelemetryPayload(payload) + + expect(mockFetch).toHaveBeenCalledTimes(2) // Initial try + 1 retry + }) +}) diff --git a/packages/next/src/telemetry/post-payload.ts b/packages/next/src/telemetry/post-telemetry-payload.ts similarity index 64% rename from packages/next/src/telemetry/post-payload.ts rename to packages/next/src/telemetry/post-telemetry-payload.ts index a3052c668ee5a2..65517ada5e1640 100644 --- a/packages/next/src/telemetry/post-payload.ts +++ b/packages/next/src/telemetry/post-telemetry-payload.ts @@ -1,15 +1,30 @@ import retry from 'next/dist/compiled/async-retry' -export function _postPayload(endpoint: string, body: object, signal?: any) { +interface Payload { + meta: { [key: string]: unknown } + + context: { + anonymousId: string + projectId: string + sessionId: string + } + + events: Array<{ + eventName: string + fields: object + }> +} + +export function postNextTelemetryPayload(payload: Payload, signal?: any) { if (!signal && 'timeout' in AbortSignal) { signal = AbortSignal.timeout(5000) } return ( retry( () => - fetch(endpoint, { + fetch('https://telemetry.nextjs.org/api/v1/record', { method: 'POST', - body: JSON.stringify(body), + body: JSON.stringify(payload), headers: { 'content-type': 'application/json' }, signal, }).then((res) => { diff --git a/packages/next/src/telemetry/storage.ts b/packages/next/src/telemetry/storage.ts index 7f75421efbf71d..c3636174e7edb8 100644 --- a/packages/next/src/telemetry/storage.ts +++ b/packages/next/src/telemetry/storage.ts @@ -7,7 +7,7 @@ import path from 'path' import { getAnonymousMeta } from './anonymous-meta' import * as ciEnvironment from '../server/ci-info' -import { _postPayload } from './post-payload' +import { postNextTelemetryPayload } from './post-telemetry-payload' import { getRawProjectId } from './project-id' import { AbortController } from 'next/dist/compiled/@edge-runtime/ponyfill' import fs from 'fs' @@ -30,16 +30,6 @@ const TELEMETRY_KEY_ID = `telemetry.anonymousId` const TELEMETRY_KEY_SALT = `telemetry.salt` export type TelemetryEvent = { eventName: string; payload: object } -type EventContext = { - anonymousId: string - projectId: string - sessionId: string -} -type EventMeta = { [key: string]: unknown } -type EventBatchShape = { - eventName: string - fields: object -} type RecordObject = { isFulfilled: boolean @@ -302,22 +292,19 @@ export class Telemetry { return Promise.resolve() } - const context: EventContext = { - anonymousId: this.anonymousId, - projectId: await this.getProjectId(), - sessionId: this.sessionId, - } - const meta: EventMeta = getAnonymousMeta() const postController = new AbortController() - const res = _postPayload( - `https://telemetry.nextjs.org/api/v1/record`, + const res = postNextTelemetryPayload( { - context, - meta, + context: { + anonymousId: this.anonymousId, + projectId: await this.getProjectId(), + sessionId: this.sessionId, + }, + meta: getAnonymousMeta(), events: events.map(({ eventName, payload }) => ({ eventName, fields: payload, - })) as Array, + })), }, postController.signal )