Skip to content

Commit 54b4b5a

Browse files
feat(clerk-js,themes,shared): Add theme-usage telemetry (#6529)
Co-authored-by: Mike Wickett <[email protected]>
1 parent b1d9aac commit 54b4b5a

File tree

13 files changed

+236
-2
lines changed

13 files changed

+236
-2
lines changed

.changeset/orange-tips-turn.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
'@clerk/themes': patch
5+
---
6+
7+
Add theme-usage telemetry

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"files": [
33
{ "path": "./dist/clerk.js", "maxSize": "819KB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "79KB" },
5-
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "120KB" },
5+
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "120.2KB" },
66
{ "path": "./dist/clerk.headless*.js", "maxSize": "61KB" },
77
{ "path": "./dist/ui-common*.js", "maxSize": "117.1KB" },
88
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120KB" },

packages/clerk-js/src/core/clerk.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/sh
1010
import {
1111
eventPrebuiltComponentMounted,
1212
eventPrebuiltComponentOpened,
13+
eventThemeUsage,
1314
TelemetryCollector,
1415
} from '@clerk/shared/telemetry';
1516
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
@@ -462,6 +463,11 @@ export class Clerk implements ClerkInterface {
462463
publishableKey: this.publishableKey,
463464
...this.#options.telemetry,
464465
});
466+
467+
// Record theme usage telemetry when appearance is provided
468+
if (this.#options.appearance) {
469+
this.telemetry.record(eventThemeUsage(this.#options.appearance));
470+
}
465471
}
466472

467473
try {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { EVENT_SAMPLING_RATE, EVENT_THEME_USAGE, eventThemeUsage } from '../theme-usage';
2+
3+
describe('eventThemeUsage', () => {
4+
it('should create telemetry event with shadcn theme name', () => {
5+
const appearance = {
6+
theme: {
7+
__type: 'prebuilt_appearance' as const,
8+
name: 'shadcn',
9+
variables: { colorPrimary: 'var(--primary)' },
10+
},
11+
};
12+
13+
const result = eventThemeUsage(appearance);
14+
15+
expect(result).toEqual({
16+
event: EVENT_THEME_USAGE,
17+
eventSamplingRate: EVENT_SAMPLING_RATE,
18+
payload: { themeName: 'shadcn' },
19+
});
20+
});
21+
22+
it('should handle string themes', () => {
23+
const appearance = {
24+
theme: 'clerk' as any, // String themes are valid at runtime
25+
};
26+
27+
const result = eventThemeUsage(appearance);
28+
29+
expect(result).toEqual({
30+
event: EVENT_THEME_USAGE,
31+
eventSamplingRate: EVENT_SAMPLING_RATE,
32+
payload: { themeName: 'clerk' },
33+
});
34+
});
35+
36+
it('should handle array of themes', () => {
37+
const appearance = {
38+
theme: [
39+
'clerk' as any, // String themes are valid at runtime
40+
{
41+
__type: 'prebuilt_appearance' as const,
42+
name: 'shadcn',
43+
},
44+
] as any,
45+
};
46+
47+
const result = eventThemeUsage(appearance);
48+
49+
expect(result).toEqual({
50+
event: EVENT_THEME_USAGE,
51+
eventSamplingRate: EVENT_SAMPLING_RATE,
52+
payload: { themeName: 'clerk' },
53+
});
54+
});
55+
56+
it('should handle themes without explicit names', () => {
57+
const appearance = {
58+
theme: {
59+
__type: 'prebuilt_appearance' as const,
60+
variables: { colorPrimary: 'blue' },
61+
},
62+
};
63+
64+
const result = eventThemeUsage(appearance);
65+
66+
expect(result).toEqual({
67+
event: EVENT_THEME_USAGE,
68+
eventSamplingRate: EVENT_SAMPLING_RATE,
69+
payload: { themeName: undefined },
70+
});
71+
});
72+
73+
it('should prioritize theme over deprecated baseTheme', () => {
74+
const appearance = {
75+
theme: 'clerk' as any, // String themes are valid at runtime
76+
baseTheme: {
77+
__type: 'prebuilt_appearance' as const,
78+
name: 'shadcn',
79+
},
80+
};
81+
82+
const result = eventThemeUsage(appearance);
83+
84+
expect(result).toEqual({
85+
event: EVENT_THEME_USAGE,
86+
eventSamplingRate: EVENT_SAMPLING_RATE,
87+
payload: { themeName: 'clerk' },
88+
});
89+
});
90+
91+
it('should use baseTheme when theme is not provided', () => {
92+
const appearance = {
93+
baseTheme: {
94+
__type: 'prebuilt_appearance' as const,
95+
name: 'shadcn',
96+
},
97+
};
98+
99+
const result = eventThemeUsage(appearance);
100+
101+
expect(result).toEqual({
102+
event: EVENT_THEME_USAGE,
103+
eventSamplingRate: EVENT_SAMPLING_RATE,
104+
payload: { themeName: 'shadcn' },
105+
});
106+
});
107+
108+
it('should handle undefined appearance', () => {
109+
const result = eventThemeUsage();
110+
111+
expect(result).toEqual({
112+
event: EVENT_THEME_USAGE,
113+
eventSamplingRate: EVENT_SAMPLING_RATE,
114+
payload: {},
115+
});
116+
});
117+
118+
it('should handle null appearance', () => {
119+
const result = eventThemeUsage(null as any);
120+
121+
expect(result).toEqual({
122+
event: EVENT_THEME_USAGE,
123+
eventSamplingRate: EVENT_SAMPLING_RATE,
124+
payload: {},
125+
});
126+
});
127+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './component-mounted';
22
export * from './method-called';
33
export * from './framework-metadata';
4+
export * from './theme-usage';
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { Appearance, BaseTheme, TelemetryEventRaw } from '@clerk/types';
2+
3+
export const EVENT_THEME_USAGE = 'THEME_USAGE';
4+
export const EVENT_SAMPLING_RATE = 1;
5+
6+
type EventThemeUsage = {
7+
/**
8+
* The name of the theme being used (e.g., "shadcn", "neobrutalism", etc.).
9+
*/
10+
themeName?: string;
11+
};
12+
13+
/**
14+
* Helper function for `telemetry.record()`. Create a consistent event object for tracking theme usage in ClerkProvider.
15+
*
16+
* @param appearance - The appearance prop from ClerkProvider.
17+
* @example
18+
* telemetry.record(eventThemeUsage(appearance));
19+
*/
20+
export function eventThemeUsage(appearance?: Appearance): TelemetryEventRaw<EventThemeUsage> {
21+
const payload = analyzeThemeUsage(appearance);
22+
23+
return {
24+
event: EVENT_THEME_USAGE,
25+
eventSamplingRate: EVENT_SAMPLING_RATE,
26+
payload,
27+
};
28+
}
29+
30+
/**
31+
* Analyzes the appearance prop to extract theme usage information for telemetry.
32+
*
33+
* @internal
34+
*/
35+
function analyzeThemeUsage(appearance?: Appearance): EventThemeUsage {
36+
if (!appearance || typeof appearance !== 'object') {
37+
return {};
38+
}
39+
40+
// Prioritize the new theme property over deprecated baseTheme
41+
const themeProperty = appearance.theme || appearance.baseTheme;
42+
43+
if (!themeProperty) {
44+
return {};
45+
}
46+
47+
let themeName: string | undefined;
48+
49+
if (Array.isArray(themeProperty)) {
50+
// Look for the first identifiable theme name in the array
51+
for (const theme of themeProperty) {
52+
const name = extractThemeName(theme);
53+
if (name) {
54+
themeName = name;
55+
break;
56+
}
57+
}
58+
} else {
59+
themeName = extractThemeName(themeProperty);
60+
}
61+
62+
return { themeName };
63+
}
64+
65+
/**
66+
* Extracts the theme name from a theme object.
67+
*
68+
* @internal
69+
*/
70+
function extractThemeName(theme: BaseTheme): string | undefined {
71+
if (typeof theme === 'string') {
72+
return theme;
73+
}
74+
75+
if (typeof theme === 'object' && theme !== null) {
76+
// Check for explicit theme name
77+
if ('name' in theme && typeof theme.name === 'string') {
78+
return theme.name;
79+
}
80+
}
81+
82+
return undefined;
83+
}

packages/themes/src/createTheme.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import type { Appearance, BaseTheme, DeepPartial, Elements, Theme } from '@clerk
55
import type { InternalTheme } from '../../clerk-js/src/ui/foundations';
66

77
interface CreateClerkThemeParams extends DeepPartial<Theme> {
8+
/**
9+
* Optional name for the theme, used for telemetry and debugging.
10+
* @example 'shadcn', 'neobrutalism', 'custom-dark'
11+
*/
12+
name?: string;
13+
814
/**
915
* {@link Theme.elements}
1016
*/

packages/themes/src/themes/dark.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { experimental_createTheme } from '../createTheme';
22

33
export const dark = experimental_createTheme({
4+
name: 'dark',
45
variables: {
56
colorBackground: '#212126',
67
colorNeutral: 'white',

packages/themes/src/themes/neobrutalism.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const shadowStyle = {
2020
};
2121

2222
export const neobrutalism = experimental_createTheme({
23+
name: 'neobrutalism',
2324
//@ts-expect-error not public api
2425
simpleStyles: true,
2526
variables: {

packages/themes/src/themes/shadcn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { experimental_createTheme } from '../createTheme';
22

33
export const shadcn = experimental_createTheme({
4+
name: 'shadcn',
45
cssLayerName: 'components',
56
variables: {
67
colorBackground: 'var(--card)',

0 commit comments

Comments
 (0)