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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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
Copy Markdown
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/bashset -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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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