Skip to content

Commit 0ff648a

Browse files
authored
chore(clerk-js,shared): Increase sampling for ui components on keyless (#6514)
1 parent 773bf7f commit 0ff648a

File tree

8 files changed

+94
-25
lines changed

8 files changed

+94
-25
lines changed

.changeset/bitter-waves-burn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Use throttling instead of sampling for telemetry events of UI components on keyless apps.

.changeset/slow-shoes-give.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': minor
3+
---
4+
5+
Update TelemetryCollector to accept a `perEventSampling` property for controling per-event sampling rates.

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{ "path": "./dist/clerk.js", "maxSize": "621KB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
6-
{ "path": "./dist/clerk.headless*.js", "maxSize": "57KB" },
6+
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
77
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
88
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
99
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ export class Clerk implements ClerkInterface {
439439
this.telemetry = new TelemetryCollector({
440440
clerkVersion: Clerk.version,
441441
samplingRate: 1,
442+
perEventSampling: this.#options.__internal_keyless_claimKeylessApplicationUrl ? false : undefined,
442443
publishableKey: this.publishableKey,
443444
...this.#options.telemetry,
444445
});
@@ -537,12 +538,13 @@ export class Clerk implements ClerkInterface {
537538
};
538539

539540
public openGoogleOneTap = (props?: GoogleOneTapProps): void => {
541+
const component = 'GoogleOneTap';
540542
this.assertComponentsReady(this.#componentControls);
541543
void this.#componentControls
542-
.ensureMounted({ preloadHint: 'GoogleOneTap' })
544+
.ensureMounted({ preloadHint: component })
543545
.then(controls => controls.openModal('googleOneTap', props || {}));
544546

545-
this.telemetry?.record(eventPrebuiltComponentOpened(`GoogleOneTap`, props));
547+
this.telemetry?.record(eventPrebuiltComponentOpened(component, props));
546548
};
547549

548550
public closeGoogleOneTap = (): void => {
@@ -560,12 +562,13 @@ export class Clerk implements ClerkInterface {
560562
}
561563
return;
562564
}
565+
const component = 'SignIn';
563566
void this.#componentControls
564-
.ensureMounted({ preloadHint: 'SignIn' })
567+
.ensureMounted({ preloadHint: component })
565568
.then(controls => controls.openModal('signIn', props || {}));
566569

567570
const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() };
568-
this.telemetry?.record(eventPrebuiltComponentOpened(`SignIn`, props, additionalData));
571+
this.telemetry?.record(eventPrebuiltComponentOpened(component, props, additionalData));
569572
};
570573

571574
public closeSignIn = (): void => {
@@ -612,11 +615,12 @@ export class Clerk implements ClerkInterface {
612615
}
613616
return;
614617
}
618+
const component = 'PlanDetails';
615619
void this.#componentControls
616-
.ensureMounted({ preloadHint: 'PlanDetails' })
620+
.ensureMounted({ preloadHint: component })
617621
.then(controls => controls.openDrawer('planDetails', props || {}));
618622

619-
this.telemetry?.record(eventPrebuiltComponentOpened(`PlanDetails`, props));
623+
this.telemetry?.record(eventPrebuiltComponentOpened(component, props));
620624
};
621625

622626
public __internal_closePlanDetails = (): void => {
@@ -718,7 +722,7 @@ export class Clerk implements ClerkInterface {
718722
.ensureMounted({ preloadHint: 'UserProfile' })
719723
.then(controls => controls.openModal('userProfile', props || {}));
720724

721-
const additionalData = props?.customPages?.length || 0 > 0 ? { customPages: true } : undefined;
725+
const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined;
722726
this.telemetry?.record(eventPrebuiltComponentOpened('UserProfile', props, additionalData));
723727
};
724728

@@ -795,17 +799,18 @@ export class Clerk implements ClerkInterface {
795799

796800
public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => {
797801
this.assertComponentsReady(this.#componentControls);
798-
void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls =>
802+
const component = 'SignIn';
803+
void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls =>
799804
controls.mountComponent({
800-
name: 'SignIn',
805+
name: component,
801806
appearanceKey: 'signIn',
802807
node,
803808
props,
804809
}),
805810
);
806811

807812
const additionalData = { withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow() };
808-
this.telemetry?.record(eventPrebuiltComponentMounted(`SignIn`, props, additionalData));
813+
this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData));
809814
};
810815

811816
public unmountSignIn = (node: HTMLDivElement): void => {
@@ -819,16 +824,17 @@ export class Clerk implements ClerkInterface {
819824

820825
public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => {
821826
this.assertComponentsReady(this.#componentControls);
822-
void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls =>
827+
const component = 'SignUp';
828+
void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls =>
823829
controls.mountComponent({
824-
name: 'SignUp',
830+
name: component,
825831
appearanceKey: 'signUp',
826832
node,
827833
props,
828834
}),
829835
);
830836

831-
this.telemetry?.record(eventPrebuiltComponentMounted(`SignUp`, props));
837+
this.telemetry?.record(eventPrebuiltComponentMounted(component, props));
832838
};
833839

834840
public unmountSignUp = (node: HTMLDivElement): void => {
@@ -850,17 +856,18 @@ export class Clerk implements ClerkInterface {
850856
}
851857
return;
852858
}
853-
void this.#componentControls.ensureMounted({ preloadHint: 'UserProfile' }).then(controls =>
859+
const component = 'UserProfile';
860+
void this.#componentControls.ensureMounted({ preloadHint: component }).then(controls =>
854861
controls.mountComponent({
855-
name: 'UserProfile',
862+
name: component,
856863
appearanceKey: 'userProfile',
857864
node,
858865
props,
859866
}),
860867
);
861868

862-
const additionalData = props?.customPages?.length || 0 > 0 ? { customPages: true } : undefined;
863-
this.telemetry?.record(eventPrebuiltComponentMounted('UserProfile', props, additionalData));
869+
const additionalData = (props?.customPages?.length || 0) > 0 ? { customPages: true } : undefined;
870+
this.telemetry?.record(eventPrebuiltComponentMounted(component, props, additionalData));
864871
};
865872

866873
public unmountUserProfile = (node: HTMLDivElement): void => {

packages/shared/src/__tests__/telemetry.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,49 @@ describe('TelemetryCollector', () => {
200200

201201
randomSpy.mockRestore();
202202
});
203+
204+
test('ignores event-specific sampling rate when eventSampling is false', async () => {
205+
windowSpy.mockImplementation(() => undefined);
206+
207+
const randomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
208+
209+
const collector = new TelemetryCollector({
210+
publishableKey: TEST_PK,
211+
samplingRate: 1.0, // Global sampling rate allows all events
212+
perEventSampling: false, // Disable event-specific sampling
213+
});
214+
215+
// This event would normally be rejected due to low eventSamplingRate (0.1 < 0.5)
216+
// but should be sent because eventSampling is disabled
217+
collector.record({ event: 'TEST_EVENT', eventSamplingRate: 0.1, payload: {} });
218+
219+
jest.runAllTimers();
220+
221+
expect(fetchSpy).toHaveBeenCalled();
222+
223+
randomSpy.mockRestore();
224+
});
225+
226+
test('respects event-specific sampling rate when eventSampling is true (default)', async () => {
227+
windowSpy.mockImplementation(() => undefined);
228+
229+
const randomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.5);
230+
231+
const collector = new TelemetryCollector({
232+
publishableKey: TEST_PK,
233+
samplingRate: 1.0, // Global sampling rate allows all events
234+
perEventSampling: true, // Enable event-specific sampling (default)
235+
});
236+
237+
// This event should be rejected due to low eventSamplingRate (0.1 < 0.5)
238+
collector.record({ event: 'TEST_EVENT', eventSamplingRate: 0.1, payload: {} });
239+
240+
jest.runAllTimers();
241+
242+
expect(fetchSpy).not.toHaveBeenCalled();
243+
244+
randomSpy.mockRestore();
245+
});
203246
});
204247

205248
describe('with client-side throttling', () => {

packages/shared/src/telemetry/collector.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function isWindowClerkWithMetadata(clerk: unknown): clerk is { constructor: { sd
4646

4747
type TelemetryCollectorConfig = Pick<
4848
TelemetryCollectorOptions,
49-
'samplingRate' | 'disabled' | 'debug' | 'maxBufferSize'
49+
'samplingRate' | 'disabled' | 'debug' | 'maxBufferSize' | 'perEventSampling'
5050
> & {
5151
endpoint: string;
5252
};
@@ -80,6 +80,7 @@ export class TelemetryCollector implements TelemetryCollectorInterface {
8080
this.#config = {
8181
maxBufferSize: options.maxBufferSize ?? DEFAULT_CONFIG.maxBufferSize,
8282
samplingRate: options.samplingRate ?? DEFAULT_CONFIG.samplingRate,
83+
perEventSampling: options.perEventSampling ?? true,
8384
disabled: options.disabled ?? false,
8485
debug: options.debug ?? false,
8586
endpoint: DEFAULT_CONFIG.endpoint,
@@ -167,7 +168,9 @@ export class TelemetryCollector implements TelemetryCollectorInterface {
167168

168169
const toBeSampled =
169170
randomSeed <= this.#config.samplingRate &&
170-
(typeof eventSamplingRate === 'undefined' || randomSeed <= eventSamplingRate);
171+
(this.#config.perEventSampling === false ||
172+
typeof eventSamplingRate === 'undefined' ||
173+
randomSeed <= eventSamplingRate);
171174

172175
if (!toBeSampled) {
173176
return false;

packages/shared/src/telemetry/events/component-mounted.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ type EventPrebuiltComponent = ComponentMountedBase & {
1717

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

20+
/**
21+
* @internal
22+
*/
2023
function createPrebuiltComponentEvent(event: typeof EVENT_COMPONENT_MOUNTED | typeof EVENT_COMPONENT_OPENED) {
2124
return function (
2225
component: string,
@@ -44,7 +47,6 @@ function createPrebuiltComponentEvent(event: typeof EVENT_COMPONENT_MOUNTED | ty
4447
* @param component - The name of the component.
4548
* @param props - The props passed to the component. Will be filtered to a known list of props.
4649
* @param additionalPayload - Additional data to send with the event.
47-
*
4850
* @example
4951
* telemetry.record(eventPrebuiltComponentMounted('SignUp', props));
5052
*/
@@ -62,7 +64,6 @@ export function eventPrebuiltComponentMounted(
6264
* @param component - The name of the component.
6365
* @param props - The props passed to the component. Will be filtered to a known list of props.
6466
* @param additionalPayload - Additional data to send with the event.
65-
*
6667
* @example
6768
* telemetry.record(eventPrebuiltComponentOpened('GoogleOneTap', props));
6869
*/
@@ -81,7 +82,6 @@ export function eventPrebuiltComponentOpened(
8182
*
8283
* @param component - The name of the component.
8384
* @param props - The props passed to the component. Ideally you only pass a handful of props here.
84-
*
8585
* @example
8686
* telemetry.record(eventComponentMounted('SignUp', props));
8787
*/

packages/shared/src/telemetry/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ export type TelemetryCollectorOptions = {
88
*/
99
debug?: boolean;
1010
/**
11-
* Sampling rate, 0-1
11+
* Sampling rate, 0-1.
1212
*/
1313
samplingRate?: number;
1414
/**
15-
* Set a custom buffer size to control how often events are sent
15+
* If false, the sampling rates provided per event will be ignored and the global sampling rate will be used.
16+
*
17+
* @default true
18+
*/
19+
perEventSampling?: boolean;
20+
/**
21+
* Set a custom buffer size to control how often events are sent.
1622
*/
1723
maxBufferSize?: number;
1824
/**

0 commit comments

Comments
 (0)