diff --git a/.changeset/clean-kings-brake.md b/.changeset/clean-kings-brake.md new file mode 100644 index 0000000000..e0f62d24d6 --- /dev/null +++ b/.changeset/clean-kings-brake.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Add a custom logger to allow logging a message or warning to the console once per session, in order to avoid consecutive identical logs due to component rerenders. diff --git a/packages/clerk-js/src/utils/__tests__/url.test.ts b/packages/clerk-js/src/utils/__tests__/url.test.ts index 60b924fd19..79b9a8a7d6 100644 --- a/packages/clerk-js/src/utils/__tests__/url.test.ts +++ b/packages/clerk-js/src/utils/__tests__/url.test.ts @@ -1,3 +1,4 @@ +import { logger } from '@clerk/shared/logger'; import type { SignUpResource } from '@clerk/types'; import { @@ -473,7 +474,7 @@ describe('isAllowedRedirectOrigin', () => { ['https://test.clerk.com/foo?hello=1', [/https:\/\/www\.clerk\.com/], false], ]; - const warnMock = jest.spyOn(global.console, 'warn').mockImplementation(); + const warnMock = jest.spyOn(logger, 'warnOnce'); beforeEach(() => warnMock.mockClear()); afterAll(() => warnMock.mockRestore()); diff --git a/packages/clerk-js/src/utils/assertNoLegacyProp.ts b/packages/clerk-js/src/utils/assertNoLegacyProp.ts index 7a9137c51d..50a51d04e5 100644 --- a/packages/clerk-js/src/utils/assertNoLegacyProp.ts +++ b/packages/clerk-js/src/utils/assertNoLegacyProp.ts @@ -1,10 +1,12 @@ +import { logger } from '@clerk/shared/logger'; + export function assertNoLegacyProp(props: Record) { const legacyProps = ['redirectUrl', 'afterSignInUrl', 'afterSignUpUrl', 'after_sign_in_url', 'after_sign_up_url']; const legacyProp = Object.keys(props).find(key => legacyProps.includes(key)); if (legacyProp && props[legacyProp]) { // TODO: @nikos update with the docs link - console.warn( + logger.warnOnce( `Clerk: The prop "${legacyProp}" is deprecated and should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead.`, ); } @@ -18,7 +20,7 @@ export function warnForNewPropShadowingLegacyProp( ) { if (newValue && legacyValue) { // TODO: @nikos update with the docs link - console.warn( + logger.warnOnce( `Clerk: The "${newKey}" prop ("${newValue}") has priority over the legacy "${legacyKey}" (or "redirectUrl") ("${legacyValue}"), which will be completely ignored in this case. "${legacyKey}" (or "redirectUrl" prop) should be replaced with the new "fallbackRedirectUrl" or "forceRedirectUrl" props instead.`, ); } diff --git a/packages/clerk-js/src/utils/url.ts b/packages/clerk-js/src/utils/url.ts index afe03cce60..b5bd222624 100644 --- a/packages/clerk-js/src/utils/url.ts +++ b/packages/clerk-js/src/utils/url.ts @@ -1,5 +1,6 @@ import { globs } from '@clerk/shared/globs'; import { createDevOrStagingUrlCache } from '@clerk/shared/keys'; +import { logger } from '@clerk/shared/logger'; import { camelToSnake } from '@clerk/shared/underscore'; import { isCurrentDevAccountPortalOrigin, isLegacyDevAccountPortalOrigin } from '@clerk/shared/url'; import type { SignUpResource } from '@clerk/types'; @@ -116,7 +117,7 @@ export function buildURL(params: BuildURLParams, options: BuildURLOptions origin.test(trimTrailingSlash(url.origin))); if (!isAllowed) { - console.warn( + logger.warnOnce( `Clerk: Redirect URL ${url} is not on one of the allowedRedirectOrigins, falling back to the default redirect URL.`, ); } diff --git a/packages/shared/package.json b/packages/shared/package.json index e98ad2efd3..c9418bf66a 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -68,7 +68,8 @@ "constants", "apiUrlFromPublishableKey", "scripts", - "telemetry" + "telemetry", + "logger" ], "scripts": { "build": "tsup", diff --git a/packages/shared/src/__tests__/logger.test.ts b/packages/shared/src/__tests__/logger.test.ts new file mode 100644 index 0000000000..2b6de46131 --- /dev/null +++ b/packages/shared/src/__tests__/logger.test.ts @@ -0,0 +1,33 @@ +import { logger } from '../logger'; + +describe('logger', () => { + describe('warnOnce', () => { + const warnMock = jest.spyOn(global.console, 'warn').mockImplementation(); + + beforeEach(() => warnMock.mockClear()); + afterAll(() => warnMock.mockRestore()); + + test('warns only once per session', () => { + logger.warnOnce('testwarn'); + logger.warnOnce('testwarn'); + logger.warnOnce('testwarn'); + + expect(warnMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('logOnce', () => { + const logMock = jest.spyOn(global.console, 'log').mockImplementation(); + + beforeEach(() => logMock.mockClear()); + afterAll(() => logMock.mockRestore()); + + test('logs only once per session', () => { + logger.logOnce('testlog'); + logger.logOnce('testlog'); + logger.logOnce('testlog'); + + expect(logMock).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index c2d51b320c..f9152c1b5f 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -30,5 +30,6 @@ export * from './proxy'; export * from './underscore'; export * from './url'; export * from './object'; +export * from './logger'; export { createWorkerTimers } from './workerTimers'; export { DEV_BROWSER_JWT_KEY, extractDevBrowserJWTFromURL, setDevBrowserJWTInURL } from './devBrowser'; diff --git a/packages/shared/src/logger.ts b/packages/shared/src/logger.ts new file mode 100644 index 0000000000..60bc3d6556 --- /dev/null +++ b/packages/shared/src/logger.ts @@ -0,0 +1,24 @@ +const loggedMessages: Set = new Set(); + +export const logger = { + /** + * A custom logger that ensures messages are logged only once. + * Reduces noise and duplicated messages when logs are in a hot codepath. + */ + warnOnce: (msg: string) => { + if (loggedMessages.has(msg)) { + return; + } + + loggedMessages.add(msg); + console.warn(msg); + }, + logOnce: (msg: string) => { + if (loggedMessages.has(msg)) { + return; + } + + console.log(msg); + loggedMessages.add(msg); + }, +}; diff --git a/packages/shared/subpaths.mjs b/packages/shared/subpaths.mjs index 7aeddf9def..d6d1c47652 100644 --- a/packages/shared/subpaths.mjs +++ b/packages/shared/subpaths.mjs @@ -24,6 +24,7 @@ export const subpathNames = [ 'constants', 'apiUrlFromPublishableKey', 'telemetry', + 'logger', ]; export const subpathFoldersBarrel = ['react'];