Skip to content
5 changes: 5 additions & 0 deletions .changeset/bitter-waves-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Use throttling instead of sampling for telemetry events of UI components on keyless apps.
5 changes: 5 additions & 0 deletions .changeset/slow-shoes-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/shared': minor
---

Update `eventPrebuiltComponentMounted` and `eventPrebuiltComponentOpened` to accept a samplingRate.
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
112 changes: 74 additions & 38 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ export class Clerk implements ClerkInterface {
return Clerk._apiKeys;
}

private get telemetrySamplingRate(): number | undefined {
return this.#options.__internal_keyless_claimKeylessApplicationUrl ? 1 : undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add tests for telemetry sampling rate logic

Consider adding tests to verify:

  1. The telemetrySamplingRate getter returns 1 for keyless apps and undefined for regular apps
  2. The sampling rate is correctly passed to all telemetry event methods
  3. The telemetry behavior differs appropriately between keyless and non-keyless applications

Example test structure:

describe('telemetry sampling', () => {
  it('should use full sampling rate for keyless apps', () => {
    const clerk = new Clerk(key, {
      __internal_keyless_claimKeylessApplicationUrl: 'https://example.com'
    });
    // Verify telemetry calls receive samplingRate = 1
  });
  
  it('should use default sampling for regular apps', () => {
    const clerk = new Clerk(key);
    // Verify telemetry calls receive samplingRate = undefined
  });
});
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/clerk.ts around lines 350 to 352, add unit tests
that verify the telemetrySamplingRate getter and its propagation: create a Clerk
instance with __internal_keyless_claimKeylessApplicationUrl set (keyless) and
assert telemetrySamplingRate === 1 and that all telemetry event calls receive
samplingRate = 1 (use spies/mocks on telemetry methods); create a Clerk instance
without that option and assert telemetrySamplingRate === undefined and telemetry
event calls receive samplingRate = undefined; ensure tests cover multiple
telemetry methods and both behaviors so keyless and non-keyless flows are
exercised.


__experimental_checkout(options: __experimental_CheckoutOptions): __experimental_CheckoutInstance {
if (!this._checkout) {
this._checkout = params => createCheckoutInstance(this, params);
Expand Down Expand Up @@ -537,12 +541,13 @@ export class Clerk implements ClerkInterface {
};

public openGoogleOneTap = (props?: GoogleOneTapProps): void => {
const component = 'GoogleOneTap';
this.assertComponentsReady(this.#componentControls);
void this.#componentControls
.ensureMounted({ preloadHint: 'GoogleOneTap' })
.ensureMounted({ preloadHint: component })
.then(controls => controls.openModal('googleOneTap', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened(`GoogleOneTap`, props));
this.telemetry?.record(eventPrebuiltComponentOpened(component, props, undefined, this.telemetrySamplingRate));
};

public closeGoogleOneTap = (): void => {
Expand All @@ -560,12 +565,13 @@ export class Clerk implements ClerkInterface {
}
return;
}
const component = 'SignIn';
void this.#componentControls
.ensureMounted({ preloadHint: 'SignIn' })
.ensureMounted({ preloadHint: component })
.then(controls => controls.openModal('signIn', props || {}));

const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() };
this.telemetry?.record(eventPrebuiltComponentOpened(`SignIn`, props, additionalData));
this.telemetry?.record(eventPrebuiltComponentOpened(component, props, additionalData, this.telemetrySamplingRate));
};

public closeSignIn = (): void => {
Expand Down Expand Up @@ -612,11 +618,12 @@ export class Clerk implements ClerkInterface {
}
return;
}
const component = 'PlanDetails';
void this.#componentControls
.ensureMounted({ preloadHint: 'PlanDetails' })
.ensureMounted({ preloadHint: component })
.then(controls => controls.openDrawer('planDetails', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened(`PlanDetails`, props));
this.telemetry?.record(eventPrebuiltComponentOpened(component, props, undefined, this.telemetrySamplingRate));
};

public __internal_closePlanDetails = (): void => {
Expand Down Expand Up @@ -650,7 +657,9 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'UserVerification' })
.then(controls => controls.openModal('userVerification', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened(`UserVerification`, props));
this.telemetry?.record(
eventPrebuiltComponentOpened(`UserVerification`, props, undefined, this.telemetrySamplingRate),
);
};

public __internal_closeReverification = (): void => {
Expand Down Expand Up @@ -696,7 +705,7 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'SignUp' })
.then(controls => controls.openModal('signUp', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened('SignUp', props));
this.telemetry?.record(eventPrebuiltComponentOpened('SignUp', props, undefined, this.telemetrySamplingRate));
};

public closeSignUp = (): void => {
Expand All @@ -718,8 +727,10 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'UserProfile' })
.then(controls => controls.openModal('userProfile', props || {}));

const additionalData = props?.customPages?.length || 0 > 0 ? { customPages: true } : undefined;
this.telemetry?.record(eventPrebuiltComponentOpened('UserProfile', props, additionalData));
const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined;
this.telemetry?.record(
eventPrebuiltComponentOpened('UserProfile', props, additionalData, this.telemetrySamplingRate),
);
};

public closeUserProfile = (): void => {
Expand Down Expand Up @@ -749,7 +760,9 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'OrganizationProfile' })
.then(controls => controls.openModal('organizationProfile', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened('OrganizationProfile', props));
this.telemetry?.record(
eventPrebuiltComponentOpened('OrganizationProfile', props, undefined, this.telemetrySamplingRate),
);
};

public closeOrganizationProfile = (): void => {
Expand All @@ -771,7 +784,9 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'CreateOrganization' })
.then(controls => controls.openModal('createOrganization', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened('CreateOrganization', props));
this.telemetry?.record(
eventPrebuiltComponentOpened('CreateOrganization', props, undefined, this.telemetrySamplingRate),
);
};

public closeCreateOrganization = (): void => {
Expand All @@ -785,7 +800,7 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'Waitlist' })
.then(controls => controls.openModal('waitlist', props || {}));

this.telemetry?.record(eventPrebuiltComponentOpened('Waitlist', props));
this.telemetry?.record(eventPrebuiltComponentOpened('Waitlist', props, undefined, this.telemetrySamplingRate));
};

public closeWaitlist = (): void => {
Expand All @@ -795,17 +810,18 @@ export class Clerk implements ClerkInterface {

public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls =>
const component = 'SignIn';
void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls =>
controls.mountComponent({
name: 'SignIn',
name: component,
appearanceKey: 'signIn',
node,
props,
}),
);

const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() };
this.telemetry?.record(eventPrebuiltComponentMounted(`SignIn`, props, additionalData));
this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData, this.telemetrySamplingRate));
};

public unmountSignIn = (node: HTMLDivElement): void => {
Expand All @@ -819,16 +835,17 @@ export class Clerk implements ClerkInterface {

public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls =>
const component = 'SignUp';
void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls =>
controls.mountComponent({
name: 'SignUp',
name: component,
appearanceKey: 'signUp',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted(`SignUp`, props));
this.telemetry?.record(eventPrebuiltComponentMounted(component, props, undefined, this.telemetrySamplingRate));
};

public unmountSignUp = (node: HTMLDivElement): void => {
Expand All @@ -850,17 +867,18 @@ export class Clerk implements ClerkInterface {
}
return;
}
void this.#componentControls.ensureMounted({ preloadHint: 'UserProfile' }).then(controls =>
const component = 'UserProfile';
void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls =>
controls.mountComponent({
name: 'UserProfile',
name: component,
appearanceKey: 'userProfile',
node,
props,
}),
);

const additionalData = props?.customPages?.length || 0 > 0 ? { customPages: true } : undefined;
this.telemetry?.record(eventPrebuiltComponentMounted('UserProfile', props, additionalData));
const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined;
this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData, this.telemetrySamplingRate));
};

public unmountUserProfile = (node: HTMLDivElement): void => {
Expand Down Expand Up @@ -900,7 +918,9 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('OrganizationProfile', props));
this.telemetry?.record(
eventPrebuiltComponentMounted('OrganizationProfile', props, undefined, this.telemetrySamplingRate),
);
};

public unmountOrganizationProfile = (node: HTMLDivElement) => {
Expand Down Expand Up @@ -931,7 +951,9 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('CreateOrganization', props));
this.telemetry?.record(
eventPrebuiltComponentMounted('CreateOrganization', props, undefined, this.telemetrySamplingRate),
);
};

public unmountCreateOrganization = (node: HTMLDivElement) => {
Expand Down Expand Up @@ -963,10 +985,15 @@ export class Clerk implements ClerkInterface {
);

this.telemetry?.record(
eventPrebuiltComponentMounted('OrganizationSwitcher', {
...props,
forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection,
}),
eventPrebuiltComponentMounted(
'OrganizationSwitcher',
{
...props,
forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection,
},
undefined,
this.telemetrySamplingRate,
),
);
};

Expand Down Expand Up @@ -1002,10 +1029,15 @@ export class Clerk implements ClerkInterface {
);

this.telemetry?.record(
eventPrebuiltComponentMounted('OrganizationList', {
...props,
forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection,
}),
eventPrebuiltComponentMounted(
'OrganizationList',
{
...props,
forceOrganizationSelection: this.environment?.organizationSettings.forceOrganizationSelection,
},
undefined,
this.telemetrySamplingRate,
),
);
};

Expand All @@ -1030,7 +1062,9 @@ export class Clerk implements ClerkInterface {
...(props?.__experimental_asStandalone ? { standalone: true } : undefined),
};

this.telemetry?.record(eventPrebuiltComponentMounted('UserButton', props, additionalData));
this.telemetry?.record(
eventPrebuiltComponentMounted('UserButton', props, additionalData, this.telemetrySamplingRate),
);
};

public unmountUserButton = (node: HTMLDivElement): void => {
Expand All @@ -1049,7 +1083,7 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('Waitlist', props));
this.telemetry?.record(eventPrebuiltComponentMounted('Waitlist', props, undefined, this.telemetrySamplingRate));
};

public unmountWaitlist = (node: HTMLDivElement): void => {
Expand All @@ -1076,7 +1110,7 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props));
this.telemetry?.record(eventPrebuiltComponentMounted('PricingTable', props, undefined, this.telemetrySamplingRate));
};

public unmountPricingTable = (node: HTMLDivElement): void => {
Expand Down Expand Up @@ -1145,7 +1179,7 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('APIKeys', props));
this.telemetry?.record(eventPrebuiltComponentMounted('APIKeys', props, undefined, this.telemetrySamplingRate));
};

/**
Expand Down Expand Up @@ -1183,7 +1217,9 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted('TaskChooseOrganization', props));
this.telemetry?.record(
eventPrebuiltComponentMounted('TaskChooseOrganization', props, undefined, this.telemetrySamplingRate),
);
};

public unmountTaskChooseOrganization = (node: HTMLDivElement) => {
Expand Down
22 changes: 16 additions & 6 deletions packages/shared/src/telemetry/events/component-mounted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,24 @@ type EventPrebuiltComponent = ComponentMountedBase & {

type EventComponentMounted = ComponentMountedBase & TelemetryEventRaw['payload'];

/**
* @internal
*/
function createPrebuiltComponentEvent(event: typeof EVENT_COMPONENT_MOUNTED | typeof EVENT_COMPONENT_OPENED) {
return function (
component: string,
props?: Record<string, any>,
additionalPayload?: TelemetryEventRaw['payload'],
samplingRate?: number,
): TelemetryEventRaw<EventPrebuiltComponent> {
const normalizedSampling =
typeof samplingRate === 'number' && !Number.isFinite(samplingRate)
? Math.min(1, Math.max(0, samplingRate))
: undefined;

return {
event,
eventSamplingRate: EVENT_SAMPLING_RATE,
eventSamplingRate: normalizedSampling ?? EVENT_SAMPLING_RATE,
payload: {
component,
appearanceProp: Boolean(props?.appearance),
Expand All @@ -44,16 +53,17 @@ function createPrebuiltComponentEvent(event: typeof EVENT_COMPONENT_MOUNTED | ty
* @param component - The name of the component.
* @param props - The props passed to the component. Will be filtered to a known list of props.
* @param additionalPayload - Additional data to send with the event.
*
* @param samplingRate - The sampling rate for the event.
* @example
* telemetry.record(eventPrebuiltComponentMounted('SignUp', props));
*/
export function eventPrebuiltComponentMounted(
component: string,
props?: Record<string, any>,
additionalPayload?: TelemetryEventRaw['payload'],
samplingRate?: number,
): TelemetryEventRaw<EventPrebuiltComponent> {
return createPrebuiltComponentEvent(EVENT_COMPONENT_MOUNTED)(component, props, additionalPayload);
return createPrebuiltComponentEvent(EVENT_COMPONENT_MOUNTED)(component, props, additionalPayload, samplingRate);
}

/**
Expand All @@ -62,16 +72,17 @@ export function eventPrebuiltComponentMounted(
* @param component - The name of the component.
* @param props - The props passed to the component. Will be filtered to a known list of props.
* @param additionalPayload - Additional data to send with the event.
*
* @param samplingRate - The sampling rate for the event.
* @example
* telemetry.record(eventPrebuiltComponentOpened('GoogleOneTap', props));
*/
export function eventPrebuiltComponentOpened(
component: string,
props?: Record<string, any>,
additionalPayload?: TelemetryEventRaw['payload'],
samplingRate?: number,
): TelemetryEventRaw<EventPrebuiltComponent> {
return createPrebuiltComponentEvent(EVENT_COMPONENT_OPENED)(component, props, additionalPayload);
return createPrebuiltComponentEvent(EVENT_COMPONENT_OPENED)(component, props, additionalPayload, samplingRate);
}

/**
Expand All @@ -81,7 +92,6 @@ export function eventPrebuiltComponentOpened(
*
* @param component - The name of the component.
* @param props - The props passed to the component. Ideally you only pass a handful of props here.
*
* @example
* telemetry.record(eventComponentMounted('SignUp', props));
*/
Expand Down
Loading