Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 470
feat(clerk-js,themes,shared): Add theme-usage telemetry#6529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
10b0d078eeb5eba3416ad2a6d1136733b47065197ae11a15a1655a766770c316adad535478a666eac2d63400e8fdff19502a5782adf249632b11e9e847ca6a48b474e48e4dbd8c6cfc2b73ab43File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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: {}, | ||
| }); | ||
| }); | ||
| }); |
| 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'; |
| 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; | ||
| } |
| 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, | ||
| }); |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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:
Add this private field to the class (near other private fields):
Verification suggestion:
🏁 Script executed:
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 totelemetry.record(eventThemeUsage…)with a guard that skips recording if the theme name hasn’t changed since the last emission:Add this private field alongside the other
#-prefixed members of the class:Why?
Clerk.loadand the React<ClerkProvider>will fire aTHEME_USAGEevent on mount with the sameappearance.How to verify:
Clerk.load({ appearance: { /*…*/ } }).<ClerkProvider appearance={/* same */}>.THEME_USAGEis emitted on startup, and a second only after you changeappearance.🤖 Prompt for AI Agents