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.
Comment thread
panteliselef marked this conversation as resolved.
Outdated
86 changes: 59 additions & 27 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
Copy Markdown
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 @@ -542,7 +546,7 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'GoogleOneTap' })
.then(controls => controls.openModal('googleOneTap', props || {}));

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

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
public closeGoogleOneTap = (): void => {
Expand All @@ -565,7 +569,7 @@ export class Clerk implements ClerkInterface {
.then(controls => controls.openModal('signIn', props || {}));

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

public closeSignIn = (): void => {
Expand Down Expand Up @@ -616,7 +620,7 @@ export class Clerk implements ClerkInterface {
.ensureMounted({ preloadHint: 'PlanDetails' })
.then(controls => controls.openDrawer('planDetails', props || {}));

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

public __internal_closePlanDetails = (): void => {
Expand Down Expand Up @@ -650,7 +654,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 +702,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 @@ -719,7 +725,9 @@ export class Clerk implements ClerkInterface {
.then(controls => controls.openModal('userProfile', props || {}));

const additionalData = props?.customPages?.length || 0 > 0 ? { customPages: true } : undefined;
this.telemetry?.record(eventPrebuiltComponentOpened('UserProfile', props, additionalData));
this.telemetry?.record(
eventPrebuiltComponentOpened('UserProfile', props, additionalData, this.telemetrySamplingRate),
);
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

public closeUserProfile = (): void => {
Expand Down Expand Up @@ -749,7 +757,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 +781,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 +797,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 @@ -805,7 +817,7 @@ export class Clerk implements ClerkInterface {
);

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

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

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

public unmountSignUp = (node: HTMLDivElement): void => {
Expand Down Expand Up @@ -860,7 +872,9 @@ export class Clerk implements ClerkInterface {
);

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

public unmountUserProfile = (node: HTMLDivElement): void => {
Expand Down Expand Up @@ -900,7 +914,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 +947,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 +981,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 +1025,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 +1058,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 +1079,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 +1106,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 +1175,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 +1213,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
17 changes: 11 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,19 @@ 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> {
return {
event,
eventSamplingRate: EVENT_SAMPLING_RATE,
eventSamplingRate: samplingRate ?? EVENT_SAMPLING_RATE,
payload: {
Comment thread
panteliselef marked this conversation as resolved.
component,
appearanceProp: Boolean(props?.appearance),
Expand All @@ -44,16 +48,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 +67,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 +87,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