Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
10b0d07
feat(clerk-js,themes,shared): Add theme-usage telemetry
alexcarpenter Aug 12, 2025
8eeb5eb
add to clerk
alexcarpenter Aug 12, 2025
a3416ad
refactor
alexcarpenter Aug 12, 2025
2a6d113
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Aug 13, 2025
6733b47
just use name
alexcarpenter Aug 13, 2025
065197a
Merge branch 'alexcarpenter/theme-usage-telemetry' of github.com:cler…
alexcarpenter Aug 13, 2025
e11a15a
Update bundlewatch.config.json
alexcarpenter Aug 13, 2025
1655a76
Update packages/shared/src/telemetry/events/theme-usage.ts
alexcarpenter Aug 27, 2025
6770c31
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Aug 27, 2025
6adad53
use consts
alexcarpenter Aug 27, 2025
5478a66
add changeset
alexcarpenter Aug 27, 2025
6eac2d6
dedupe
alexcarpenter Aug 27, 2025
3400e8f
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Aug 27, 2025
dff1950
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Sep 5, 2025
2a5782a
remove useEffect usage
alexcarpenter Sep 5, 2025
df24963
Update .changeset/orange-tips-turn.md
alexcarpenter Sep 5, 2025
2b11e9e
Update bundlewatch.config.json
alexcarpenter Sep 5, 2025
847ca6a
Merge branch 'alexcarpenter/theme-usage-telemetry' of github.com:cler…
alexcarpenter Sep 5, 2025
48b474e
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Sep 5, 2025
48e4dbd
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Sep 9, 2025
8c6cfc2
Update bundlewatch.config.json
alexcarpenter Sep 9, 2025
b73ab43
Merge branch 'main' into alexcarpenter/theme-usage-telemetry
alexcarpenter Sep 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/orange-tips-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/themes': patch
---

Add theme-usage telemetry
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"files": [
{ "path": "./dist/clerk.js", "maxSize": "819KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "79KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "120KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "120.2KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "61KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "117.1KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120KB" },
Expand Down
6 changes: 6 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/sh
import {
eventPrebuiltComponentMounted,
eventPrebuiltComponentOpened,
eventThemeUsage,
TelemetryCollector,
} from '@clerk/shared/telemetry';
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
Expand Down Expand Up @@ -462,6 +463,11 @@ export class Clerk implements ClerkInterface {
publishableKey: this.publishableKey,
...this.#options.telemetry,
});

// Record theme usage telemetry when appearance is provided
if (this.#options.appearance) {
this.telemetry.record(eventThemeUsage(this.#options.appearance));
}
Comment on lines +466 to +470
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Potential duplicate THEME_USAGE events (core load vs React provider); add simple de-duplication

Clerk.load records theme usage when appearance is provided. The React Provider also records on appearance changes. On initial mount, both can fire for the same theme, producing duplicates. Add a simple guard to skip recording the same theme name twice in a row.

Proposed change:

-      // Record theme usage telemetry when appearance is provided
-      if (this.#options.appearance) {
-        this.telemetry.record(eventThemeUsage(this.#options.appearance));
-      }
+      // Record theme usage telemetry when appearance is provided (dedupe consecutive identical names)
+      if (this.#options.appearance) {
+        const evt = eventThemeUsage(this.#options.appearance);
+        const currentThemeName = evt.payload?.themeName;
+        if (currentThemeName !== this.#lastThemeUsageName) {
+          this.telemetry.record(evt);
+          this.#lastThemeUsageName = currentThemeName;
+        }
+      }

Add this private field to the class (near other private fields):

// Tracks last emitted theme usage to avoid duplicates
#lastThemeUsageName?: string;

Verification suggestion:

  • In a React app, initialize Clerk with appearance and mount ClerkProvider with the same appearance. Confirm only one THEME_USAGE event is emitted on boot, and another only when theme changes.

🏁 Script executed:

#!/bin/bash
set -e

# 1. Locate all occurrences of eventThemeUsage across the repo
rg "eventThemeUsage" -n

# 2. Identify where telemetry.record is called with eventThemeUsage (including in React provider)
rg "record.*eventThemeUsage" -n

Length of output: 2277


Add de-duplication for consecutive THEME_USAGE events

In packages/clerk-js/src/core/clerk.ts (around lines 447–451), wrap the existing call to telemetry.record(eventThemeUsage…) with a guard that skips recording if the theme name hasn’t changed since the last emission:

--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -447,7 +447,14 @@ export class Clerk {
       // Record theme usage telemetry when appearance is provided
       if (this.#options.appearance) {
-        this.telemetry.record(eventThemeUsage(this.#options.appearance));
+        const evt = eventThemeUsage(this.#options.appearance);
+        const currentTheme = evt.payload.themeName;
+        if (currentTheme !== this.#lastThemeUsageName) {
+          this.telemetry.record(evt);
+          this.#lastThemeUsageName = currentTheme;
+        }
       }

Add this private field alongside the other #-prefixed members of the class:

// Tracks the last emitted theme name to prevent back-to-back duplicates
#lastThemeUsageName?: string;

Why?

  • Both Clerk.load and the React <ClerkProvider> will fire a THEME_USAGE event on mount with the same appearance.
  • Without this guard you’ll see two identical events at boot; with it, you get exactly one per theme and still record on real theme changes.

How to verify:

  1. In a test app call Clerk.load({ appearance: { /*…*/ } }).
  2. Mount <ClerkProvider appearance={/* same */}>.
  3. Confirm only one THEME_USAGE is emitted on startup, and a second only after you change appearance.
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/clerk.ts around lines 447–451, add a private field
on the class (next to the other # members) to track the last emitted theme name
(e.g. #lastThemeUsageName?: string) and wrap the
telemetry.record(eventThemeUsage(...)) call with a guard that compares the
current appearance name to #lastThemeUsageName; if they are equal, skip
recording, otherwise call telemetry.record(...) and update #lastThemeUsageName
to the current theme name. This prevents back-to-back duplicate THEME_USAGE
events while still recording on real theme changes.

}

try {
Expand Down
127 changes: 127 additions & 0 deletions packages/shared/src/telemetry/events/__tests__/theme-usage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { EVENT_SAMPLING_RATE, EVENT_THEME_USAGE, eventThemeUsage } from '../theme-usage';

describe('eventThemeUsage', () => {
it('should create telemetry event with shadcn theme name', () => {
const appearance = {
theme: {
__type: 'prebuilt_appearance' as const,
name: 'shadcn',
variables: { colorPrimary: 'var(--primary)' },
},
};

const result = eventThemeUsage(appearance);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: { themeName: 'shadcn' },
});
});

it('should handle string themes', () => {
const appearance = {
theme: 'clerk' as any, // String themes are valid at runtime
};

const result = eventThemeUsage(appearance);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: { themeName: 'clerk' },
});
});

it('should handle array of themes', () => {
const appearance = {
theme: [
'clerk' as any, // String themes are valid at runtime
{
__type: 'prebuilt_appearance' as const,
name: 'shadcn',
},
] as any,
};

const result = eventThemeUsage(appearance);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: { themeName: 'clerk' },
});
});

it('should handle themes without explicit names', () => {
const appearance = {
theme: {
__type: 'prebuilt_appearance' as const,
variables: { colorPrimary: 'blue' },
},
};

const result = eventThemeUsage(appearance);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: { themeName: undefined },
});
});

it('should prioritize theme over deprecated baseTheme', () => {
const appearance = {
theme: 'clerk' as any, // String themes are valid at runtime
baseTheme: {
__type: 'prebuilt_appearance' as const,
name: 'shadcn',
},
};

const result = eventThemeUsage(appearance);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: { themeName: 'clerk' },
});
});

it('should use baseTheme when theme is not provided', () => {
const appearance = {
baseTheme: {
__type: 'prebuilt_appearance' as const,
name: 'shadcn',
},
};

const result = eventThemeUsage(appearance);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: { themeName: 'shadcn' },
});
});

it('should handle undefined appearance', () => {
const result = eventThemeUsage();

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: {},
});
});

it('should handle null appearance', () => {
const result = eventThemeUsage(null as any);

expect(result).toEqual({
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: {},
});
});
});
1 change: 1 addition & 0 deletions packages/shared/src/telemetry/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './component-mounted';
export * from './method-called';
export * from './framework-metadata';
export * from './theme-usage';
83 changes: 83 additions & 0 deletions packages/shared/src/telemetry/events/theme-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Appearance, BaseTheme, TelemetryEventRaw } from '@clerk/types';

export const EVENT_THEME_USAGE = 'THEME_USAGE';
export const EVENT_SAMPLING_RATE = 1;

type EventThemeUsage = {
/**
* The name of the theme being used (e.g., "shadcn", "neobrutalism", etc.).
*/
themeName?: string;
};

/**
* Helper function for `telemetry.record()`. Create a consistent event object for tracking theme usage in ClerkProvider.
*
* @param appearance - The appearance prop from ClerkProvider.
* @example
* telemetry.record(eventThemeUsage(appearance));
*/
export function eventThemeUsage(appearance?: Appearance): TelemetryEventRaw<EventThemeUsage> {
const payload = analyzeThemeUsage(appearance);

return {
event: EVENT_THEME_USAGE,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload,
};
}

/**
* Analyzes the appearance prop to extract theme usage information for telemetry.
*
* @internal
*/
function analyzeThemeUsage(appearance?: Appearance): EventThemeUsage {
if (!appearance || typeof appearance !== 'object') {
return {};
}

// Prioritize the new theme property over deprecated baseTheme
const themeProperty = appearance.theme || appearance.baseTheme;

if (!themeProperty) {
return {};
}

let themeName: string | undefined;

if (Array.isArray(themeProperty)) {
// Look for the first identifiable theme name in the array
for (const theme of themeProperty) {
const name = extractThemeName(theme);
if (name) {
themeName = name;
break;
}
}
} else {
themeName = extractThemeName(themeProperty);
}

return { themeName };
}

/**
* Extracts the theme name from a theme object.
*
* @internal
*/
function extractThemeName(theme: BaseTheme): string | undefined {
if (typeof theme === 'string') {
return theme;
}

if (typeof theme === 'object' && theme !== null) {
// Check for explicit theme name
if ('name' in theme && typeof theme.name === 'string') {
return theme.name;
}
}

return undefined;
}
6 changes: 6 additions & 0 deletions packages/themes/src/createTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import type { Appearance, BaseTheme, DeepPartial, Elements, Theme } from '@clerk
import type { InternalTheme } from '../../clerk-js/src/ui/foundations';

interface CreateClerkThemeParams extends DeepPartial<Theme> {
/**
* Optional name for the theme, used for telemetry and debugging.
* @example 'shadcn', 'neobrutalism', 'custom-dark'
*/
name?: string;

/**
* {@link Theme.elements}
*/
Expand Down
1 change: 1 addition & 0 deletions packages/themes/src/themes/dark.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { experimental_createTheme } from '../createTheme';

export const dark = experimental_createTheme({
name: 'dark',
variables: {
colorBackground: '#212126',
colorNeutral: 'white',
Expand Down
1 change: 1 addition & 0 deletions packages/themes/src/themes/neobrutalism.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const shadowStyle = {
};

export const neobrutalism = experimental_createTheme({
name: 'neobrutalism',
//@ts-expect-error not public api
simpleStyles: true,
variables: {
Expand Down
1 change: 1 addition & 0 deletions packages/themes/src/themes/shadcn.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { experimental_createTheme } from '../createTheme';

export const shadcn = experimental_createTheme({
name: 'shadcn',
cssLayerName: 'components',
variables: {
colorBackground: 'var(--card)',
Expand Down
1 change: 1 addition & 0 deletions packages/themes/src/themes/shadesOfPurple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { experimental_createTheme } from '../createTheme';
import { dark } from './dark';

export const shadesOfPurple = experimental_createTheme({
name: 'shadesOfPurple',
baseTheme: dark,
variables: {
colorBackground: '#3f3c77',
Expand Down
1 change: 1 addition & 0 deletions packages/themes/src/themes/simple.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { experimental_createTheme } from '../createTheme';

export const experimental__simple = experimental_createTheme({
name: 'simple',
//@ts-expect-error not public api
simpleStyles: true,
});
1 change: 0 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading