diff --git a/.changeset/poor-cobras-sin.md b/.changeset/poor-cobras-sin.md new file mode 100644 index 00000000000..f775d708db8 --- /dev/null +++ b/.changeset/poor-cobras-sin.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': minor +--- + +Track usage of modal UI Components as part of telemetry. diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 3a9a4fa70c8..dabddd02c25 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -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 { @@ -404,11 +408,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 => { @@ -429,6 +434,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 => { @@ -449,6 +457,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 => { @@ -483,6 +493,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 => { @@ -503,6 +515,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 => { @@ -531,6 +546,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 => { @@ -551,6 +568,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 => { @@ -563,6 +582,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 => { @@ -580,17 +601,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 => { @@ -612,7 +625,8 @@ export class Clerk implements ClerkInterface { props, }), ); - this.telemetry?.record(eventPrebuiltComponentMounted('SignUp', props)); + + this.telemetry?.record(eventPrebuiltComponentMounted(`SignUp`, props)); }; public unmountSignUp = (node: HTMLDivElement): void => { @@ -643,17 +657,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 => { @@ -808,21 +813,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 => { diff --git a/packages/shared/src/telemetry/events/component-mounted.ts b/packages/shared/src/telemetry/events/component-mounted.ts index 41d5bf68747..7f3e7bbc5f3 100644 --- a/packages/shared/src/telemetry/events/component-mounted.ts +++ b/packages/shared/src/telemetry/events/component-mounted.ts @@ -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; @@ -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, + additionalPayload?: TelemetryEventRaw['payload'], + ): TelemetryEventRaw { + 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. * @@ -30,19 +52,26 @@ export function eventPrebuiltComponentMounted( component: string, props?: Record, additionalPayload?: TelemetryEventRaw['payload'], -): TelemetryEventRaw { - 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 { + 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, + additionalPayload?: TelemetryEventRaw['payload'], +): TelemetryEventRaw { + return createPrebuiltComponentEvent(EVENT_COMPONENT_OPENED)(component, props, additionalPayload); } /**