-
Notifications
You must be signed in to change notification settings - Fork 374
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
base: main
Are you sure you want to change the base?
Changes from all commits
10b0d07
8eeb5eb
a3416ad
2a6d113
6733b47
065197a
e11a15a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import { | |
SessionContext, | ||
UserContext, | ||
} from '@clerk/shared/react'; | ||
import { eventThemeUsage } from '@clerk/shared/telemetry'; | ||
import type { ClientResource, InitialState, Resources } from '@clerk/types'; | ||
import React from 'react'; | ||
|
||
|
@@ -119,6 +120,13 @@ const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => { | |
void isomorphicClerkRef.current.__unstable__updateProps({ appearance: options.appearance }); | ||
}, [options.appearance]); | ||
|
||
// Record theme usage telemetry when appearance changes | ||
React.useEffect(() => { | ||
if (options.appearance && isomorphicClerkRef.current.telemetry) { | ||
isomorphicClerkRef.current.telemetry.record(eventThemeUsage(options.appearance)); | ||
} | ||
}, [options.appearance]); | ||
|
||
Comment on lines
+123
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard against repeated emissions when appearance object identity changes without an actual theme change The effect emits an event whenever options.appearance reference changes. In practice, this can emit duplicates on re-renders. Track the last emitted theme name and only record when it changes. Note: This does not fully avoid a possible duplicate with the core Clerk.load emission; combine with the core-side dedupe proposed in clerk.ts for complete coverage. // Record theme usage telemetry when appearance changes
- React.useEffect(() => {
- if (options.appearance && isomorphicClerkRef.current.telemetry) {
- isomorphicClerkRef.current.telemetry.record(eventThemeUsage(options.appearance));
- }
- }, [options.appearance]);
+ const lastThemeNameRef = React.useRef<string | undefined>(undefined);
+ React.useEffect(() => {
+ const telemetry = isomorphicClerkRef.current.telemetry;
+ if (!telemetry) {
+ return;
+ }
+ const evt = eventThemeUsage(options.appearance);
+ const nextName = evt.payload?.themeName;
+ if (nextName === lastThemeNameRef.current) {
+ return;
+ }
+ telemetry.record(evt);
+ lastThemeNameRef.current = nextName;
+ }, [options.appearance]); 🤖 Prompt for AI Agents
|
||
React.useEffect(() => { | ||
void isomorphicClerkRef.current.__unstable__updateProps({ options }); | ||
}, [options.localization]); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import { 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: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
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: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
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: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
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: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
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: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
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: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
payload: { themeName: 'shadcn' }, | ||
}); | ||
}); | ||
|
||
it('should handle undefined appearance', () => { | ||
const result = eventThemeUsage(); | ||
|
||
expect(result).toEqual({ | ||
event: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
payload: {}, | ||
}); | ||
}); | ||
|
||
it('should handle null appearance', () => { | ||
const result = eventThemeUsage(null as any); | ||
|
||
expect(result).toEqual({ | ||
event: 'THEME_USAGE', | ||
eventSamplingRate: 0.1, | ||
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'; | ||
|
||
const EVENT_THEME_USAGE = 'THEME_USAGE'; | ||
const EVENT_SAMPLING_RATE = 0.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, | ||
}); |
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.load
and the React<ClerkProvider>
will fire aTHEME_USAGE
event on mount with the sameappearance
.How to verify:
Clerk.load({ appearance: { /*…*/ } })
.<ClerkProvider appearance={/* same */}>
.THEME_USAGE
is emitted on startup, and a second only after you changeappearance
.🤖 Prompt for AI Agents