From 3845068b197ed2500d71de638e61932000aeb640 Mon Sep 17 00:00:00 2001 From: Edmund Hung Date: Thu, 26 Sep 2024 14:13:59 +0100 Subject: [PATCH] fix(create-cloudflare): ignore network errors during telemetry collection --- .changeset/twelve-years-search.md | 5 + .../src/__tests__/metrics.test.ts | 413 +++++++++++------- .../create-cloudflare/src/helpers/sparrow.ts | 27 +- packages/create-cloudflare/src/metrics.ts | 30 +- 4 files changed, 294 insertions(+), 181 deletions(-) create mode 100644 .changeset/twelve-years-search.md diff --git a/.changeset/twelve-years-search.md b/.changeset/twelve-years-search.md new file mode 100644 index 000000000000..71762348b71f --- /dev/null +++ b/.changeset/twelve-years-search.md @@ -0,0 +1,5 @@ +--- +"create-cloudflare": patch +--- + +fix: prevent the cli from crashing by silently ignoring network errors during telemetry collection diff --git a/packages/create-cloudflare/src/__tests__/metrics.test.ts b/packages/create-cloudflare/src/__tests__/metrics.test.ts index d60edce5a7c6..b3a77f440efd 100644 --- a/packages/create-cloudflare/src/__tests__/metrics.test.ts +++ b/packages/create-cloudflare/src/__tests__/metrics.test.ts @@ -56,22 +56,25 @@ describe("createReporter", () => { promise: () => deferred.promise, }); - expect(sendEvent).toBeCalledWith({ - event: "c3 session started", - deviceId, - timestamp: now, - properties: { - c3Version, - platform, - packageManager, - isFirstUsage: false, - amplitude_session_id: now, - amplitude_event_id: 0, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session started", + deviceId, + timestamp: now, + properties: { + c3Version, + platform, + packageManager, + isFirstUsage: false, + amplitude_session_id: now, + amplitude_event_id: 0, + args: { + projectName: "app", + }, }, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(1); deferred.resolve("test result"); @@ -80,25 +83,95 @@ describe("createReporter", () => { await expect(operation).resolves.toBe("test result"); - expect(sendEvent).toBeCalledWith({ - event: "c3 session completed", - deviceId, - timestamp: now + 1234, - properties: { - c3Version, - platform, - packageManager, - isFirstUsage: false, - amplitude_session_id: now, - amplitude_event_id: 1, + expect(sendEvent).toBeCalledWith( + { + event: "c3 session completed", + deviceId, + timestamp: now + 1234, + properties: { + c3Version, + platform, + packageManager, + isFirstUsage: false, + amplitude_session_id: now, + amplitude_event_id: 1, + args: { + projectName: "app", + }, + durationMs: 1234, + durationSeconds: 1234 / 1000, + durationMinutes: 1234 / 1000 / 60, + }, + }, + false, + ); + expect(sendEvent).toBeCalledTimes(2); + }); + + test("sends event with logs enabled if CREATE_CLOUDFLARE_TELEMETRY_DEBUG is set to `1`", async () => { + vi.stubEnv("CREATE_CLOUDFLARE_TELEMETRY_DEBUG", "1"); + + const deferred = promiseWithResolvers(); + const reporter = createReporter(); + const operation = reporter.collectAsyncMetrics({ + eventPrefix: "c3 session", + props: { args: { projectName: "app", }, - durationMs: 1234, - durationSeconds: 1234 / 1000, - durationMinutes: 1234 / 1000 / 60, }, + promise: () => deferred.promise, }); + + expect(sendEvent).toBeCalledWith( + { + event: "c3 session started", + deviceId, + timestamp: now, + properties: { + c3Version, + platform, + packageManager, + isFirstUsage: false, + amplitude_session_id: now, + amplitude_event_id: 0, + args: { + projectName: "app", + }, + }, + }, + true, + ); + expect(sendEvent).toBeCalledTimes(1); + + deferred.resolve("test result"); + + vi.advanceTimersByTime(1234); + + await expect(operation).resolves.toBe("test result"); + + expect(sendEvent).toBeCalledWith( + { + event: "c3 session completed", + deviceId, + timestamp: now + 1234, + properties: { + c3Version, + platform, + packageManager, + isFirstUsage: false, + amplitude_session_id: now, + amplitude_event_id: 1, + args: { + projectName: "app", + }, + durationMs: 1234, + durationSeconds: 1234 / 1000, + durationMinutes: 1234 / 1000 / 60, + }, + }, + true, + ); expect(sendEvent).toBeCalledTimes(2); }); @@ -195,22 +268,25 @@ describe("createReporter", () => { promise: () => deferred.promise, }); - expect(sendEvent).toBeCalledWith({ - event: "c3 session started", - deviceId, - timestamp: now, - properties: { - amplitude_session_id: now, - amplitude_event_id: 0, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session started", + deviceId, + timestamp: now, + properties: { + amplitude_session_id: now, + amplitude_event_id: 0, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, }, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(1); deferred.reject(new CancelError("test cancel")); @@ -218,25 +294,28 @@ describe("createReporter", () => { await expect(operation).rejects.toThrow(CancelError); - expect(sendEvent).toBeCalledWith({ - event: "c3 session cancelled", - deviceId, - timestamp: now + 1234, - properties: { - amplitude_session_id: now, - amplitude_event_id: 1, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session cancelled", + deviceId, + timestamp: now + 1234, + properties: { + amplitude_session_id: now, + amplitude_event_id: 1, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, + durationMs: 1234, + durationSeconds: 1234 / 1000, + durationMinutes: 1234 / 1000 / 60, }, - durationMs: 1234, - durationSeconds: 1234 / 1000, - durationMinutes: 1234 / 1000 / 60, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(2); }); @@ -251,22 +330,25 @@ describe("createReporter", () => { promise: () => deferred.promise, }); - expect(sendEvent).toBeCalledWith({ - event: "c3 session started", - deviceId, - timestamp: now, - properties: { - amplitude_session_id: now, - amplitude_event_id: 0, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session started", + deviceId, + timestamp: now, + properties: { + amplitude_session_id: now, + amplitude_event_id: 0, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, }, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(1); deferred.reject(new Error("test error")); @@ -274,29 +356,32 @@ describe("createReporter", () => { await expect(process).rejects.toThrow(Error); - expect(sendEvent).toBeCalledWith({ - event: "c3 session errored", - deviceId, - timestamp: now + 1234, - properties: { - amplitude_session_id: now, - amplitude_event_id: 1, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", - }, - durationMs: 1234, - durationSeconds: 1234 / 1000, - durationMinutes: 1234 / 1000 / 60, - error: { - message: "test error", - stack: expect.any(String), + expect(sendEvent).toBeCalledWith( + { + event: "c3 session errored", + deviceId, + timestamp: now + 1234, + properties: { + amplitude_session_id: now, + amplitude_event_id: 1, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, + durationMs: 1234, + durationSeconds: 1234 / 1000, + durationMinutes: 1234 / 1000 / 60, + error: { + message: "test error", + stack: expect.any(String), + }, }, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(2); }); @@ -314,22 +399,25 @@ describe("createReporter", () => { promise: () => deferred.promise, }); - expect(sendEvent).toBeCalledWith({ - event: "c3 session started", - deviceId, - timestamp: now, - properties: { - amplitude_session_id: now, - amplitude_event_id: 0, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session started", + deviceId, + timestamp: now, + properties: { + amplitude_session_id: now, + amplitude_event_id: 0, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, }, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(1); process.emit("SIGINT", "SIGINT"); @@ -337,26 +425,29 @@ describe("createReporter", () => { await expect(run).rejects.toThrow(CancelError); - expect(sendEvent).toBeCalledWith({ - event: "c3 session cancelled", - deviceId, - timestamp: now + 1234, - properties: { - amplitude_session_id: now, - amplitude_event_id: 1, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session cancelled", + deviceId, + timestamp: now + 1234, + properties: { + amplitude_session_id: now, + amplitude_event_id: 1, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, + signal: "SIGINT", + durationMs: 1234, + durationSeconds: 1234 / 1000, + durationMinutes: 1234 / 1000 / 60, }, - signal: "SIGINT", - durationMs: 1234, - durationSeconds: 1234 / 1000, - durationMinutes: 1234 / 1000 / 60, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(2); }); @@ -373,22 +464,25 @@ describe("createReporter", () => { promise: () => deferred.promise, }); - expect(sendEvent).toBeCalledWith({ - event: "c3 session started", - deviceId, - timestamp: now, - properties: { - amplitude_session_id: now, - amplitude_event_id: 0, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session started", + deviceId, + timestamp: now, + properties: { + amplitude_session_id: now, + amplitude_event_id: 0, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, }, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(1); process.emit("SIGTERM", "SIGTERM"); @@ -396,26 +490,29 @@ describe("createReporter", () => { await expect(run).rejects.toThrow(CancelError); - expect(sendEvent).toBeCalledWith({ - event: "c3 session cancelled", - deviceId, - timestamp: now + 1234, - properties: { - amplitude_session_id: now, - amplitude_event_id: 1, - c3Version, - platform, - packageManager, - isFirstUsage: false, - args: { - projectName: "app", + expect(sendEvent).toBeCalledWith( + { + event: "c3 session cancelled", + deviceId, + timestamp: now + 1234, + properties: { + amplitude_session_id: now, + amplitude_event_id: 1, + c3Version, + platform, + packageManager, + isFirstUsage: false, + args: { + projectName: "app", + }, + signal: "SIGTERM", + durationMs: 1234, + durationSeconds: 1234 / 1000, + durationMinutes: 1234 / 1000 / 60, }, - signal: "SIGTERM", - durationMs: 1234, - durationSeconds: 1234 / 1000, - durationMinutes: 1234 / 1000 / 60, }, - }); + false, + ); expect(sendEvent).toBeCalledTimes(2); }); }); diff --git a/packages/create-cloudflare/src/helpers/sparrow.ts b/packages/create-cloudflare/src/helpers/sparrow.ts index ee25f33b7c40..09160732700c 100644 --- a/packages/create-cloudflare/src/helpers/sparrow.ts +++ b/packages/create-cloudflare/src/helpers/sparrow.ts @@ -16,17 +16,24 @@ export function hasSparrowSourceKey() { return SPARROW_SOURCE_KEY !== ""; } -export async function sendEvent(payload: EventPayload) { - if (process.env.CREATE_CLOUDFLARE_TELEMETRY_DEBUG === "1") { +export async function sendEvent(payload: EventPayload, enableLog?: boolean) { + if (enableLog) { console.log("[telemetry]", JSON.stringify(payload, null, 2)); } - await fetch(`${SPARROW_URL}/api/v1/event`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Sparrow-Source-Key": SPARROW_SOURCE_KEY, - }, - body: JSON.stringify(payload), - }); + try { + await fetch(`${SPARROW_URL}/api/v1/event`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Sparrow-Source-Key": SPARROW_SOURCE_KEY, + }, + body: JSON.stringify(payload), + }); + } catch (error) { + // Ignore any network errors + if (enableLog) { + console.log("[telemetry]", error); + } + } } diff --git a/packages/create-cloudflare/src/metrics.ts b/packages/create-cloudflare/src/metrics.ts index ea14d1df6142..1f6517549b33 100644 --- a/packages/create-cloudflare/src/metrics.ts +++ b/packages/create-cloudflare/src/metrics.ts @@ -71,6 +71,7 @@ export function createReporter() { const packageManager = detectPackageManager(); const platform = getPlatform(); const amplitude_session_id = Date.now(); + const enableLog = process.env.CREATE_CLOUDFLARE_TELEMETRY_DEBUG === "1"; // The event id is an incrementing counter to distinguish events with the same `user_id` and timestamp from each other. // @see https://amplitude.com/docs/apis/analytics/http-v2#event-array-keys @@ -84,20 +85,23 @@ export function createReporter() { return; } - const request = sparrow.sendEvent({ - event: name, - deviceId, - timestamp: Date.now(), - properties: { - amplitude_session_id, - amplitude_event_id: amplitude_event_id++, - platform, - c3Version, - isFirstUsage, - packageManager: packageManager.name, - ...properties, + const request = sparrow.sendEvent( + { + event: name, + deviceId, + timestamp: Date.now(), + properties: { + amplitude_session_id, + amplitude_event_id: amplitude_event_id++, + platform, + c3Version, + isFirstUsage, + packageManager: packageManager.name, + ...properties, + }, }, - }); + enableLog, + ); // TODO(consider): retry failed requests // TODO(consider): add a timeout to avoid the process staying alive for too long