From 7f00caf703294177e2ba618cfbecf66e8d26b6a1 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 9 Nov 2023 09:21:49 -0600 Subject: [PATCH 1/7] feat(shared): Support telemetry opt-out, move TelemetryCollector initialization to Clerk.load, implement opt-out for Next --- packages/clerk-js/src/core/clerk.ts | 15 +++++++----- .../src/utils/mergeNextClerkPropsWithEnv.ts | 4 ++++ packages/shared/src/telemetry.ts | 23 ++++++++++++++++--- packages/types/src/clerk.ts | 8 +++++++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index d4067a3597c..f79ddb673b7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -308,12 +308,6 @@ export default class Clerk implements ClerkInterface { this.#instanceType = instanceType; } this.#fapiClient = createFapiClient(this); - this.#telemetry = new TelemetryCollector({ - clerkVersion: Clerk.version, - verbose: true, - samplingRate: 1, - publishableKey: key, - }); BaseResource.clerk = this; } @@ -331,6 +325,15 @@ export default class Clerk implements ClerkInterface { ...options, }; + this.#telemetry = new TelemetryCollector({ + clerkVersion: Clerk.version, + verbose: true, + samplingRate: 1, + // @ts-expect-error -- in v5 this will always be defined + publishableKey: this.publishableKey, + ...this.#options.telemetry, + }); + if (this.#options.standardBrowser) { this.#isReady = await this.#loadInStandardBrowser(); } else { diff --git a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts index 0cdd5894515..d2b742bd21c 100644 --- a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts +++ b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts @@ -15,6 +15,10 @@ export const mergeNextClerkPropsWithEnv = (props: Omit & { +type TelemetryCollectorConfig = Pick & { endpoint: string; maxBufferSize: number; }; @@ -108,6 +117,9 @@ export class TelemetryCollector { // this.#config.endpoint = 'https://telemetry-service-staging.bryce-clerk.workers.dev'; this.#config.endpoint = 'http://localhost:8787'; + + this.#config.disabled = options.disabled ?? false; + this.#config.debug = options.debug ?? false; } get isEnabled(): boolean { @@ -115,7 +127,12 @@ export class TelemetryCollector { return false; } - // TODO: check clerk environment / environment variable + // In browser or client environments, we most likely pass the disabled option to the collector, but in environments + // where environment variables are available we also check for `CLERK_TELEMETRY_DISABLED`. + if (this.#config.disabled || (typeof process !== 'undefined' && isTruthy(process.env.CLERK_TELEMETRY_DISABLED))) { + return false; + } + return true; } @@ -186,7 +203,7 @@ export class TelemetryCollector { 'Content-Type': 'application/json', }, }) - .catch(err => console.error(err)) + .catch(() => void 0) .then(() => { this.#buffer = []; }) diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 044d1b936a4..471f7ca2050 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -552,6 +552,14 @@ export interface ClerkOptions { */ isInterstitial?: boolean; isSatellite?: boolean | ((url: URL) => boolean); + + /** + * Telemetry options + */ + telemetry?: { + disabled?: boolean; + debug?: boolean; + }; } export interface Resources { From 22cfc85ef1ebc4bbe835ddeccf8d5c4e46772de3 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 9 Nov 2023 11:09:17 -0600 Subject: [PATCH 2/7] feat(remix): Pass telemetry options to ClerkProvider --- packages/clerk-js/src/core/clerk.ts | 6 +++++- packages/react/src/isomorphicClerk.ts | 10 +++++----- packages/remix/src/client/RemixClerkProvider.tsx | 6 ++++++ packages/remix/src/client/types.ts | 2 ++ packages/remix/src/ssr/utils.ts | 2 ++ packages/shared/src/telemetry.ts | 2 +- packages/types/src/clerk.ts | 2 ++ 7 files changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index f79ddb673b7..1dbdabd56c4 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -173,7 +173,7 @@ export default class Clerk implements ClerkInterface { #devBrowserHandler: DevBrowserHandler | null = null; #environment?: EnvironmentResource | null; #fapiClient: FapiClient; - #telemetry: TelemetryCollector; + #telemetry?: TelemetryCollector; #instanceType: InstanceType; #isReady = false; @@ -325,6 +325,10 @@ export default class Clerk implements ClerkInterface { ...options, }; + if (this.#options.sdkMetadata) { + Clerk.sdkMetadata = this.#options.sdkMetadata; + } + this.#telemetry = new TelemetryCollector({ clerkVersion: Clerk.version, verbose: true, diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 7e96ae53202..d9025f5cc27 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -132,6 +132,11 @@ export default class IsomorphicClerk { this.options = options; this.Clerk = Clerk; this.mode = inBrowser() ? 'browser' : 'server'; + + if (!this.options.sdkMetadata) { + this.options.sdkMetadata = SDK_METADATA; + } + void this.loadClerkJS(); } @@ -168,7 +173,6 @@ export default class IsomorphicClerk { domain: this.domain, } as any); - c.sdkMetadata = this.options.sdkMetadata ?? SDK_METADATA; await c.load(this.options); } else { // Otherwise use the instantiated Clerk object @@ -195,13 +199,9 @@ export default class IsomorphicClerk { throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.'); } - global.Clerk.sdkMetadata = this.options.sdkMetadata ?? SDK_METADATA; - await global.Clerk.load(this.options); } - global.Clerk.sdkMetadata = this.options.sdkMetadata ?? { name: PACKAGE_NAME, version: PACKAGE_VERSION }; - if (global.Clerk?.loaded || global.Clerk?.isReady()) { return this.hydrateClerkJS(global.Clerk); } diff --git a/packages/remix/src/client/RemixClerkProvider.tsx b/packages/remix/src/client/RemixClerkProvider.tsx index e01ba807e65..89793eddd66 100644 --- a/packages/remix/src/client/RemixClerkProvider.tsx +++ b/packages/remix/src/client/RemixClerkProvider.tsx @@ -56,6 +56,8 @@ export function ClerkProvider({ children, ...rest }: RemixClerkProviderProps): J __afterSignUpUrl, __clerkJSUrl, __clerkJSVersion, + __telemetryDisabled, + __telemetryDebug, } = clerkState?.__internal_clerk_state || {}; React.useEffect(() => { @@ -77,6 +79,10 @@ export function ClerkProvider({ children, ...rest }: RemixClerkProviderProps): J afterSignUpUrl: __afterSignUpUrl, clerkJSUrl: __clerkJSUrl, clerkJSVersion: __clerkJSVersion, + telemetry: { + disabled: __telemetryDisabled, + debug: __telemetryDebug, + }, }; return ( diff --git a/packages/remix/src/client/types.ts b/packages/remix/src/client/types.ts index 4fca0c9f8d2..beb58f4e29f 100644 --- a/packages/remix/src/client/types.ts +++ b/packages/remix/src/client/types.ts @@ -18,6 +18,8 @@ export type ClerkState = { __clerk_debug: any; __clerkJSUrl: string | undefined; __clerkJSVersion: string | undefined; + __telemetryDisabled: boolean | undefined; + __telemetryDebug: boolean | undefined; }; }; diff --git a/packages/remix/src/ssr/utils.ts b/packages/remix/src/ssr/utils.ts index 5c30cbaf225..45e2d5d8368 100644 --- a/packages/remix/src/ssr/utils.ts +++ b/packages/remix/src/ssr/utils.ts @@ -165,6 +165,8 @@ export function getResponseClerkState(requestState: RequestState, context: AppLo __clerk_debug: debugRequestState(requestState), __clerkJSUrl: getEnvVariable('CLERK_JS', context), __clerkJSVersion: getEnvVariable('CLERK_JS_VERSION', context), + __telemetryDisabled: getEnvVariable('CLERK_TELEMETRY_DISABLED', context), + __telemetryDebug: getEnvVariable('CLERK_TELEMETRY_DEBUG', context), }); const headers = observabilityHeadersFromRequestState(requestState); diff --git a/packages/shared/src/telemetry.ts b/packages/shared/src/telemetry.ts index 43c89337915..40e5e1aad7c 100644 --- a/packages/shared/src/telemetry.ts +++ b/packages/shared/src/telemetry.ts @@ -241,7 +241,7 @@ export class TelemetryCollector { // @ts-expect-error -- The global window.Clerk type is declared in clerk-js, but we can't rely on that here if (typeof window !== 'undefined' && window.Clerk) { // @ts-expect-error -- The global window.Clerk type is declared in clerk-js, but we can't rely on that here - sdkMetadata = { ...sdkMetadata, ...window.Clerk.constructor.prototype.sdkMetadata }; + sdkMetadata = { ...sdkMetadata, ...window.Clerk.constructor.sdkMetadata }; } return sdkMetadata; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 471f7ca2050..bd26d270d92 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -560,6 +560,8 @@ export interface ClerkOptions { disabled?: boolean; debug?: boolean; }; + + sdkMetadata?: SDKMetadata; } export interface Resources { From 92b16a2fa93d26ca8b999c55c52742f38f25dc3d Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 9 Nov 2023 11:56:21 -0600 Subject: [PATCH 3/7] feat(gatsby-plugin-clerk): Support telemetry opt-out in Gatsby --- packages/gatsby-plugin-clerk/src/GatsbyClerkProvider.tsx | 6 ++++++ packages/gatsby-plugin-clerk/src/constants.ts | 4 ++++ packages/remix/src/ssr/utils.ts | 5 +++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/gatsby-plugin-clerk/src/GatsbyClerkProvider.tsx b/packages/gatsby-plugin-clerk/src/GatsbyClerkProvider.tsx index 59f09296af3..74d558f0989 100644 --- a/packages/gatsby-plugin-clerk/src/GatsbyClerkProvider.tsx +++ b/packages/gatsby-plugin-clerk/src/GatsbyClerkProvider.tsx @@ -7,6 +7,8 @@ import { import { navigate } from 'gatsby'; import React from 'react'; +import { TELEMETRY_DEBUG, TELEMETRY_DISABLED } from './constants'; + const SDK_METADATA = { name: PACKAGE_NAME, version: PACKAGE_VERSION, @@ -28,6 +30,10 @@ export function ClerkProvider({ children, ...rest }: GatsbyClerkProviderProps) { navigate={to => navigate(to)} initialState={__clerk_ssr_state || {}} sdkMetadata={SDK_METADATA} + telemetry={{ + disabled: TELEMETRY_DISABLED, + debug: TELEMETRY_DEBUG, + }} {...restProps} > {__clerk_ssr_interstitial_html ? ( diff --git a/packages/gatsby-plugin-clerk/src/constants.ts b/packages/gatsby-plugin-clerk/src/constants.ts index 00d3f788c55..d3423677597 100644 --- a/packages/gatsby-plugin-clerk/src/constants.ts +++ b/packages/gatsby-plugin-clerk/src/constants.ts @@ -1,4 +1,5 @@ import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey'; +import { isTruthy } from '@clerk/shared/underscore'; export const PUBLISHABLE_KEY = process.env.GATSBY_CLERK_PUBLISHABLE_KEY || ''; export const API_URL = process.env.CLERK_API_URL || apiUrlFromPublishableKey(PUBLISHABLE_KEY); @@ -7,3 +8,6 @@ export const SECRET_KEY = process.env.CLERK_SECRET_KEY || ''; export const CLERK_JS = process.env.GATSBY_CLERK_JS; export const PROXY_URL = process.env.GATSBY_CLERK_PROXY_URL; + +export const TELEMETRY_DISABLED = isTruthy(process.env.GATSBY_CLERK_TELEMETRY_DISABLED); +export const TELEMETRY_DEBUG = isTruthy(process.env.GATSBY_CLERK_TELEMETRY_DEBUG); diff --git a/packages/remix/src/ssr/utils.ts b/packages/remix/src/ssr/utils.ts index 45e2d5d8368..37d08f55b53 100644 --- a/packages/remix/src/ssr/utils.ts +++ b/packages/remix/src/ssr/utils.ts @@ -1,5 +1,6 @@ import type { AuthObject, RequestState } from '@clerk/backend'; import { constants, debugRequestState, loadInterstitialFromLocal } from '@clerk/backend'; +import { isTruthy } from '@clerk/shared/underscore'; import type { AppLoadContext, defer } from '@remix-run/server-runtime'; import { json } from '@remix-run/server-runtime'; import cookie from 'cookie'; @@ -165,8 +166,8 @@ export function getResponseClerkState(requestState: RequestState, context: AppLo __clerk_debug: debugRequestState(requestState), __clerkJSUrl: getEnvVariable('CLERK_JS', context), __clerkJSVersion: getEnvVariable('CLERK_JS_VERSION', context), - __telemetryDisabled: getEnvVariable('CLERK_TELEMETRY_DISABLED', context), - __telemetryDebug: getEnvVariable('CLERK_TELEMETRY_DEBUG', context), + __telemetryDisabled: isTruthy(getEnvVariable('CLERK_TELEMETRY_DISABLED', context)), + __telemetryDebug: isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG', context)), }); const headers = observabilityHeadersFromRequestState(requestState); From 7136d30f48ed06b56060c4fcaa0ad7c1b65a33f7 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 9 Nov 2023 15:19:22 -0600 Subject: [PATCH 4/7] feat(shared): Support debug telemetry mode --- packages/shared/src/telemetry.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/telemetry.ts b/packages/shared/src/telemetry.ts index 40e5e1aad7c..2209572e5e8 100644 --- a/packages/shared/src/telemetry.ts +++ b/packages/shared/src/telemetry.ts @@ -95,6 +95,8 @@ export class TelemetryCollector { ...DEFAULT_CONFIG, samplingRate: options.samplingRate, verbose: options.verbose, + disabled: options.disabled ?? false, + debug: options.debug ?? false, } as Required; if (!options.clerkVersion && typeof window === 'undefined') { @@ -117,9 +119,6 @@ export class TelemetryCollector { // this.#config.endpoint = 'https://telemetry-service-staging.bryce-clerk.workers.dev'; this.#config.endpoint = 'http://localhost:8787'; - - this.#config.disabled = options.disabled ?? false; - this.#config.debug = options.debug ?? false; } get isEnabled(): boolean { @@ -137,10 +136,6 @@ export class TelemetryCollector { } record(event: TelemetryEvent['event'], payload: TelemetryEvent['payload']) { - if (!this.isEnabled) { - return; - } - const preparedPayload = this.#preparePayload(event, payload); this.#logEvent(preparedPayload.event, preparedPayload); @@ -155,7 +150,7 @@ export class TelemetryCollector { } #shouldRecord(): boolean { - return Math.random() <= this.#config.samplingRate; + return this.isEnabled && Math.random() <= this.#config.samplingRate; } #scheduleFlush(): void { @@ -211,10 +206,10 @@ export class TelemetryCollector { } /** - * If running in verbose mode, log the event and its payload to the console. + * If running in debug mode, log the event and its payload to the console. */ #logEvent(event: TelemetryEvent['event'], payload: Record) { - if (!this.#config.verbose) { + if (!this.#config.debug) { return; } From 0c17fd77c8ad88473dc6329216f14e922aa75e6f Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 9 Nov 2023 17:05:35 -0600 Subject: [PATCH 5/7] feat(shared): Add tests for TelemetryCollector --- packages/shared/.gitignore | 2 +- packages/shared/jest.config.js | 3 + .../shared/src/__tests__/telemetry.test.ts | 89 +++++++++++++++++++ packages/shared/src/telemetry.ts | 20 +++-- 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 packages/shared/src/__tests__/telemetry.test.ts diff --git a/packages/shared/.gitignore b/packages/shared/.gitignore index 87400d96af4..a525d2ce5cb 100644 --- a/packages/shared/.gitignore +++ b/packages/shared/.gitignore @@ -1,2 +1,2 @@ -*/ +/*/ !src/ diff --git a/packages/shared/jest.config.js b/packages/shared/jest.config.js index f9da269204a..9dd261d1d2f 100644 --- a/packages/shared/jest.config.js +++ b/packages/shared/jest.config.js @@ -4,6 +4,9 @@ const { name } = require('./package.json'); const config = { displayName: name.replace('@clerk', ''), injectGlobals: true, + globals: { + fetch: global.fetch, + }, testEnvironment: 'jsdom', roots: ['/src'], diff --git a/packages/shared/src/__tests__/telemetry.test.ts b/packages/shared/src/__tests__/telemetry.test.ts new file mode 100644 index 00000000000..9d60bb2beda --- /dev/null +++ b/packages/shared/src/__tests__/telemetry.test.ts @@ -0,0 +1,89 @@ +import { TelemetryCollector } from '../telemetry'; + +jest.useFakeTimers(); + +describe('TelemetryCollector', () => { + test('does nothing when disabled', async () => { + const fetchSpy = jest.spyOn(global, 'fetch'); + + const collector = new TelemetryCollector({ + disabled: true, + publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + }); + + collector.record('TEST_EVENT', {}); + + jest.runAllTimers(); + + expect(fetchSpy).not.toHaveBeenCalled(); + + fetchSpy.mockRestore(); + }); + + test('does not send events when debug is enabled, logs them instead', async () => { + const fetchSpy = jest.spyOn(global, 'fetch'); + const consoleSpy = jest.spyOn(global.console, 'log'); + + const collector = new TelemetryCollector({ + debug: true, + publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + }); + + collector.record('TEST_EVENT', {}); + + jest.runAllTimers(); + + expect(fetchSpy).not.toHaveBeenCalled(); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "cv": "", + "event": "TEST_EVENT", + "it": "development", + "payload": {}, + "pk": "pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk", + "sdk": undefined, + "sdkv": undefined, + }, + ], + ] + `); + + consoleSpy.mockRestore(); + fetchSpy.mockRestore(); + }); + + test('sends events after a delay when buffer is not full', async () => { + const fetchSpy = jest.spyOn(global, 'fetch'); + + const collector = new TelemetryCollector({ + publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + }); + + collector.record('TEST_EVENT', {}); + + jest.runAllTimers(); + + expect(fetchSpy).toHaveBeenCalled(); + + fetchSpy.mockRestore(); + }); + + test('sends events immediately when the buffer limit is reached', async () => { + const fetchSpy = jest.spyOn(global, 'fetch'); + + const collector = new TelemetryCollector({ + maxBufferSize: 2, + publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + }); + + collector.record('TEST_EVENT', {}); + collector.record('TEST_EVENT', {}); + + expect(fetchSpy).toHaveBeenCalled(); + + fetchSpy.mockRestore(); + }); +}); diff --git a/packages/shared/src/telemetry.ts b/packages/shared/src/telemetry.ts index 2209572e5e8..88af4e7564f 100644 --- a/packages/shared/src/telemetry.ts +++ b/packages/shared/src/telemetry.ts @@ -16,6 +16,10 @@ type TelemetryCollectorOptions = { * Sampling rate, 0-1 */ samplingRate?: number; + /** + * Set a custom buffer size to control how often events are sent + */ + maxBufferSize?: number; /** * Determines whether or not events will be logged to the console. */ @@ -38,9 +42,11 @@ type TelemetryCollectorOptions = { sdkVersion?: string; }; -type TelemetryCollectorConfig = Pick & { +type TelemetryCollectorConfig = Pick< + TelemetryCollectorOptions, + 'samplingRate' | 'verbose' | 'disabled' | 'debug' | 'maxBufferSize' +> & { endpoint: string; - maxBufferSize: number; }; type TelemetryMetadata = Required< @@ -92,9 +98,9 @@ export class TelemetryCollector { constructor(options: TelemetryCollectorOptions) { this.#config = { - ...DEFAULT_CONFIG, - samplingRate: options.samplingRate, - verbose: options.verbose, + maxBufferSize: options.maxBufferSize ?? DEFAULT_CONFIG.maxBufferSize, + samplingRate: options.samplingRate ?? DEFAULT_CONFIG.samplingRate, + verbose: options.verbose ?? DEFAULT_CONFIG.verbose, disabled: options.disabled ?? false, debug: options.debug ?? false, } as Required; @@ -150,7 +156,7 @@ export class TelemetryCollector { } #shouldRecord(): boolean { - return this.isEnabled && Math.random() <= this.#config.samplingRate; + return this.isEnabled && !this.#config.debug && Math.random() <= this.#config.samplingRate; } #scheduleFlush(): void { @@ -165,7 +171,7 @@ export class TelemetryCollector { // If the buffer is full, flush immediately to make sure we minimize the chance of event loss. // Cancel any pending flushes as we're going to flush immediately if (this.#pendingFlush) { - const cancel = cancelIdleCallback || clearTimeout; + const cancel = global.cancelIdleCallback || global.clearTimeout; cancel(this.#pendingFlush); } this.#flush(); From a85cd00c250d76a5273354b7641e5774c4b6d4fa Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 9 Nov 2023 17:15:43 -0600 Subject: [PATCH 6/7] feat(shared): Don't use global to get cancelIdleCallback or clearTimeout --- packages/shared/src/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/src/telemetry.ts b/packages/shared/src/telemetry.ts index 88af4e7564f..f77bf9ecd76 100644 --- a/packages/shared/src/telemetry.ts +++ b/packages/shared/src/telemetry.ts @@ -171,7 +171,7 @@ export class TelemetryCollector { // If the buffer is full, flush immediately to make sure we minimize the chance of event loss. // Cancel any pending flushes as we're going to flush immediately if (this.#pendingFlush) { - const cancel = global.cancelIdleCallback || global.clearTimeout; + const cancel = typeof cancelIdleCallback !== 'undefined' ? cancelIdleCallback : clearTimeout; cancel(this.#pendingFlush); } this.#flush(); From 2f72334b8c2262685bf0b2a29b6b84d221570417 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 13 Nov 2023 09:54:33 -0600 Subject: [PATCH 7/7] chore(shared): Add additional test cases --- .../shared/src/__tests__/telemetry.test.ts | 68 +++++++++++++++++-- packages/shared/src/telemetry.ts | 8 ++- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/__tests__/telemetry.test.ts b/packages/shared/src/__tests__/telemetry.test.ts index 9d60bb2beda..16e0211ff29 100644 --- a/packages/shared/src/__tests__/telemetry.test.ts +++ b/packages/shared/src/__tests__/telemetry.test.ts @@ -2,13 +2,33 @@ import { TelemetryCollector } from '../telemetry'; jest.useFakeTimers(); +const TEST_PK = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk'; + describe('TelemetryCollector', () => { test('does nothing when disabled', async () => { const fetchSpy = jest.spyOn(global, 'fetch'); const collector = new TelemetryCollector({ disabled: true, - publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + publishableKey: TEST_PK, + }); + + collector.record('TEST_EVENT', {}); + + jest.runAllTimers(); + + expect(fetchSpy).not.toHaveBeenCalled(); + + fetchSpy.mockRestore(); + }); + + test('does nothing when CLERK_TELEMETRY_DISABLED is set', async () => { + process.env.CLERK_TELEMETRY_DISABLED = '1'; + + const fetchSpy = jest.spyOn(global, 'fetch'); + + const collector = new TelemetryCollector({ + publishableKey: TEST_PK, }); collector.record('TEST_EVENT', {}); @@ -18,6 +38,8 @@ describe('TelemetryCollector', () => { expect(fetchSpy).not.toHaveBeenCalled(); fetchSpy.mockRestore(); + + process.env.CLERK_TELEMETRY_DISABLED = undefined; }); test('does not send events when debug is enabled, logs them instead', async () => { @@ -26,7 +48,7 @@ describe('TelemetryCollector', () => { const collector = new TelemetryCollector({ debug: true, - publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + publishableKey: TEST_PK, }); collector.record('TEST_EVENT', {}); @@ -55,11 +77,49 @@ describe('TelemetryCollector', () => { fetchSpy.mockRestore(); }); + test('enables debug via environment variable', async () => { + process.env.CLERK_TELEMETRY_DEBUG = '1'; + + const fetchSpy = jest.spyOn(global, 'fetch'); + const consoleSpy = jest.spyOn(global.console, 'log'); + + const collector = new TelemetryCollector({ + publishableKey: TEST_PK, + }); + + collector.record('TEST_EVENT', {}); + + jest.runAllTimers(); + + expect(fetchSpy).not.toHaveBeenCalled(); + + expect(consoleSpy.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "cv": "", + "event": "TEST_EVENT", + "it": "development", + "payload": {}, + "pk": "pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk", + "sdk": undefined, + "sdkv": undefined, + }, + ], + ] + `); + + consoleSpy.mockRestore(); + fetchSpy.mockRestore(); + + process.env.CLERK_TELEMETRY_DEBUG = undefined; + }); + test('sends events after a delay when buffer is not full', async () => { const fetchSpy = jest.spyOn(global, 'fetch'); const collector = new TelemetryCollector({ - publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + publishableKey: TEST_PK, }); collector.record('TEST_EVENT', {}); @@ -76,7 +136,7 @@ describe('TelemetryCollector', () => { const collector = new TelemetryCollector({ maxBufferSize: 2, - publishableKey: 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk', + publishableKey: TEST_PK, }); collector.record('TEST_EVENT', {}); diff --git a/packages/shared/src/telemetry.ts b/packages/shared/src/telemetry.ts index f77bf9ecd76..3be742efd88 100644 --- a/packages/shared/src/telemetry.ts +++ b/packages/shared/src/telemetry.ts @@ -141,6 +141,10 @@ export class TelemetryCollector { return true; } + get isDebug(): boolean { + return this.#config.debug || (typeof process !== 'undefined' && isTruthy(process.env.CLERK_TELEMETRY_DEBUG)); + } + record(event: TelemetryEvent['event'], payload: TelemetryEvent['payload']) { const preparedPayload = this.#preparePayload(event, payload); @@ -156,7 +160,7 @@ export class TelemetryCollector { } #shouldRecord(): boolean { - return this.isEnabled && !this.#config.debug && Math.random() <= this.#config.samplingRate; + return this.isEnabled && !this.isDebug && Math.random() <= this.#config.samplingRate; } #scheduleFlush(): void { @@ -215,7 +219,7 @@ export class TelemetryCollector { * If running in debug mode, log the event and its payload to the console. */ #logEvent(event: TelemetryEvent['event'], payload: Record) { - if (!this.#config.debug) { + if (!this.isDebug) { return; }