Skip to content

Commit

Permalink
feat(clerk-js): Track usage of UI modals (#5185)
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef authored Feb 24, 2025
1 parent da82c6b commit 171719d
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 54 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-cobras-sin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': minor
---

Track usage of modal UI Components as part of telemetry.
76 changes: 36 additions & 40 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { parsePublishableKey } from '@clerk/shared/keys';
import { LocalStorageBroadcastChannel } from '@clerk/shared/localStorageBroadcastChannel';
import { logger } from '@clerk/shared/logger';
import { isHttpOrHttps, isValidProxyUrl, proxyUrlToAbsoluteURL } from '@clerk/shared/proxy';
import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry';
import {
eventPrebuiltComponentMounted,
eventPrebuiltComponentOpened,
TelemetryCollector,
} from '@clerk/shared/telemetry';
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
import { handleValueOrFn, noop } from '@clerk/shared/utils';
import type {
Expand Down Expand Up @@ -413,11 +417,12 @@ export class Clerk implements ClerkInterface {
};

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

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

public closeGoogleOneTap = (): void => {
Expand All @@ -438,6 +443,9 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.ensureMounted({ preloadHint: 'SignIn' })
.then(controls => controls.openModal('signIn', props || {}));

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

public closeSignIn = (): void => {
Expand All @@ -458,6 +466,8 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.ensureMounted({ preloadHint: 'UserVerification' })
.then(controls => controls.openModal('userVerification', props || {}));

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

public __internal_closeReverification = (): void => {
Expand Down Expand Up @@ -492,6 +502,8 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.ensureMounted({ preloadHint: 'SignUp' })
.then(controls => controls.openModal('signUp', props || {}));

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

public closeSignUp = (): void => {
Expand All @@ -512,6 +524,9 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.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));
};

public closeUserProfile = (): void => {
Expand Down Expand Up @@ -540,6 +555,8 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.ensureMounted({ preloadHint: 'OrganizationProfile' })
.then(controls => controls.openModal('organizationProfile', props || {}));

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

public closeOrganizationProfile = (): void => {
Expand All @@ -560,6 +577,8 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.ensureMounted({ preloadHint: 'CreateOrganization' })
.then(controls => controls.openModal('createOrganization', props || {}));

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

public closeCreateOrganization = (): void => {
Expand All @@ -572,6 +591,8 @@ export class Clerk implements ClerkInterface {
void this.#componentControls
.ensureMounted({ preloadHint: 'Waitlist' })
.then(controls => controls.openModal('waitlist', props || {}));

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

public closeWaitlist = (): void => {
Expand All @@ -589,17 +610,9 @@ export class Clerk implements ClerkInterface {
props,
}),
);
this.telemetry?.record(
eventPrebuiltComponentMounted(
'SignIn',
{
...props,
},
{
withSignUp: props?.withSignUp ?? this.#isCombinedSignInOrUpFlow(),
},
),
);

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

public unmountSignIn = (node: HTMLDivElement): void => {
Expand All @@ -621,7 +634,8 @@ export class Clerk implements ClerkInterface {
props,
}),
);
this.telemetry?.record(eventPrebuiltComponentMounted('SignUp', props));

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

public unmountSignUp = (node: HTMLDivElement): void => {
Expand Down Expand Up @@ -652,17 +666,8 @@ export class Clerk implements ClerkInterface {
}),
);

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

public unmountUserProfile = (node: HTMLDivElement): void => {
Expand Down Expand Up @@ -817,21 +822,12 @@ export class Clerk implements ClerkInterface {
}),
);

this.telemetry?.record(
eventPrebuiltComponentMounted('UserButton', props, {
...(props?.customMenuItems?.length || 0 > 0
? {
customItems: true,
}
: undefined),

...(props?.__experimental_asStandalone
? {
standalone: true,
}
: undefined),
}),
);
const additionalData = {
...(props?.customMenuItems?.length || 0 > 0 ? { customItems: true } : undefined),
...(props?.__experimental_asStandalone ? { standalone: true } : undefined),
};

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

public unmountUserButton = (node: HTMLDivElement): void => {
Expand Down
57 changes: 43 additions & 14 deletions packages/shared/src/telemetry/events/component-mounted.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { TelemetryEventRaw } from '@clerk/types';

const EVENT_COMPONENT_MOUNTED = 'COMPONENT_MOUNTED';
const EVENT_COMPONENT_OPENED = 'COMPONENT_OPENED';
const EVENT_SAMPLING_RATE = 0.1;

type ComponentMountedBase = {
component: string;
};

type EventPrebuiltComponentMounted = ComponentMountedBase & {
type EventPrebuiltComponent = ComponentMountedBase & {
appearanceProp: boolean;
elements: boolean;
variables: boolean;
Expand All @@ -16,6 +17,27 @@ type EventPrebuiltComponentMounted = ComponentMountedBase & {

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

function createPrebuiltComponentEvent(event: typeof EVENT_COMPONENT_MOUNTED | typeof EVENT_COMPONENT_OPENED) {
return function (
component: string,
props?: Record<string, any>,
additionalPayload?: TelemetryEventRaw['payload'],
): TelemetryEventRaw<EventPrebuiltComponent> {
return {
event,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: {
component,
appearanceProp: Boolean(props?.appearance),
baseTheme: Boolean(props?.appearance?.baseTheme),
elements: Boolean(props?.appearance?.elements),
variables: Boolean(props?.appearance?.variables),
...additionalPayload,
},
};
};
}

/**
* Helper function for `telemetry.record()`. Create a consistent event object for when a prebuilt (AIO) component is mounted.
*
Expand All @@ -30,19 +52,26 @@ export function eventPrebuiltComponentMounted(
component: string,
props?: Record<string, any>,
additionalPayload?: TelemetryEventRaw['payload'],
): TelemetryEventRaw<EventPrebuiltComponentMounted> {
return {
event: EVENT_COMPONENT_MOUNTED,
eventSamplingRate: EVENT_SAMPLING_RATE,
payload: {
component,
appearanceProp: Boolean(props?.appearance),
baseTheme: Boolean(props?.appearance?.baseTheme),
elements: Boolean(props?.appearance?.elements),
variables: Boolean(props?.appearance?.variables),
...additionalPayload,
},
};
): TelemetryEventRaw<EventPrebuiltComponent> {
return createPrebuiltComponentEvent(EVENT_COMPONENT_MOUNTED)(component, props, additionalPayload);
}

/**
* Helper function for `telemetry.record()`. Create a consistent event object for when a prebuilt (AIO) component is opened as a modal.
*
* @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.
*
* @example
* telemetry.record(eventPrebuiltComponentOpened('GoogleOneTap', props));
*/
export function eventPrebuiltComponentOpened(
component: string,
props?: Record<string, any>,
additionalPayload?: TelemetryEventRaw['payload'],
): TelemetryEventRaw<EventPrebuiltComponent> {
return createPrebuiltComponentEvent(EVENT_COMPONENT_OPENED)(component, props, additionalPayload);
}

/**
Expand Down

0 comments on commit 171719d

Please sign in to comment.