From d65bd65469ed8a1ca8c2b6aea516f93470a2ce82 Mon Sep 17 00:00:00 2001 From: Laura Beatris Date: Mon, 22 Jun 2026 16:23:19 -0300 Subject: [PATCH 1/5] Add telemetry event --- .../shared/src/telemetry/events/flow-step.ts | 41 +++++++++++++++++++ packages/shared/src/telemetry/events/index.ts | 1 + 2 files changed, 42 insertions(+) create mode 100644 packages/shared/src/telemetry/events/flow-step.ts diff --git a/packages/shared/src/telemetry/events/flow-step.ts b/packages/shared/src/telemetry/events/flow-step.ts new file mode 100644 index 00000000000..056c1954f19 --- /dev/null +++ b/packages/shared/src/telemetry/events/flow-step.ts @@ -0,0 +1,41 @@ +import type { TelemetryEventRaw } from '../../types'; + +const EVENT_FLOW_STEP_MOUNTED = 'FLOW_STEP_MOUNTED'; +const EVENT_SAMPLING_RATE = 1; + +type EventFlowStepMounted = { + /** The flow the step belongs to, e.g. `configureSSO` (mirrors `Flow.Root`'s `flow`). */ + flow: string; + /** The step/part that mounted, e.g. `verify-domain` */ + step: string; + /** ISO-8601 timestamp, used to measure the time between steps of the same flow. */ + timestamp: string; +} & TelemetryEventRaw['payload']; + +/** + * Fires an event when a part of a multi-step flow becomes visible. + * + * @param flow - The flow identifier (matches `Flow.Root`'s `flow`). + * @param step - The step/part that mounted. + * @param payload - Extra, flow-specific metadata + * @param eventSamplingRate - Override the default full-capture sampling rate. + * @example + * telemetry.record(eventFlowStepMounted('configureSSO', 'verify-domain', { connectionStatus: 'unconfigured' })); + */ +export function eventFlowStepMounted( + flow: string, + step: string, + payload: TelemetryEventRaw['payload'] = {}, + eventSamplingRate: number = EVENT_SAMPLING_RATE, +): TelemetryEventRaw { + return { + event: EVENT_FLOW_STEP_MOUNTED, + eventSamplingRate, + payload: { + flow, + step, + timestamp: new Date().toISOString(), + ...payload, + }, + }; +} diff --git a/packages/shared/src/telemetry/events/index.ts b/packages/shared/src/telemetry/events/index.ts index 84b7c4eb5de..3258f7f8725 100644 --- a/packages/shared/src/telemetry/events/index.ts +++ b/packages/shared/src/telemetry/events/index.ts @@ -1,4 +1,5 @@ export * from './component-mounted'; +export * from './flow-step'; export * from './method-called'; export * from './framework-metadata'; export * from './theme-usage'; From 242878fee4a2f9abd3c80cbc5d43a967a6ca16cf Mon Sep 17 00:00:00 2001 From: Laura Beatris Date: Mon, 22 Jun 2026 16:31:57 -0300 Subject: [PATCH 2/5] Emit event on activate step --- .../ui/src/components/ConfigureSSO/steps/ActivateStep.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx index 5be4c8606d9..a845f51e280 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx @@ -1,3 +1,6 @@ +import { useClerk } from '@clerk/shared/react'; +import { eventFlowStepMounted } from '@clerk/shared/telemetry'; + import { Button, Col, descriptors, Flex, Flow, Heading, Icon, localizationKeys, Text } from '@/customizables'; import { useCardState } from '@/elements/contexts'; import { ChevronRight, DuotoneShieldCheck } from '@/icons'; @@ -15,6 +18,7 @@ export const ActivateStep = (): JSX.Element => { onExit, } = useConfigureSSO(); const card = useCardState(); + const clerk = useClerk(); // The activate step is only reachable with a configured connection, so the // domains are set; join multiples for the subtitle copy. @@ -31,6 +35,7 @@ export const ActivateStep = (): JSX.Element => { try { await setConnectionActive(enterpriseConnection.id, true); + clerk.telemetry?.record(eventFlowStepMounted('configureSSO', 'activate', { connectionStatus: 'active' })); onExit?.(); } catch (err) { handleError(err as Error, [], card.setError); From ab1cac90f1c46bcf589401b299c6c8c884118f93 Mon Sep 17 00:00:00 2001 From: Laura Beatris Date: Mon, 22 Jun 2026 16:32:52 -0300 Subject: [PATCH 3/5] Emit on verify-domain step --- .../steps/OrganizationDomainsStep.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx index f530e4977e2..4b02d2d7fe1 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx @@ -1,7 +1,8 @@ -import { useUser } from '@clerk/shared/react'; +import { useClerk, useUser } from '@clerk/shared/react'; +import { eventFlowStepMounted } from '@clerk/shared/telemetry'; import type { OrganizationDomainResource } from '@clerk/shared/types'; import type React from 'react'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Badge, @@ -40,14 +41,31 @@ export const OrganizationDomainsStep = (): JSX.Element => { const { enterpriseConnection, organizationDomains, + organizationEnterpriseConnection, contentRef, organizationDomainMutations: { createDomain, revalidate }, enterpriseConnectionMutations: { updateConnection }, } = useConfigureSSO(); const { goPrev, goNext, isFirstStep, isLastStep } = useWizard(); const card = useCardState(); + const clerk = useClerk(); const [domainToRemove, setDomainToRemove] = useState(null); + const hasRecordedTelemetryEvent = useRef(false); + useEffect(() => { + if (hasRecordedTelemetryEvent.current) { + return; + } + + hasRecordedTelemetryEvent.current = true; + clerk.telemetry?.record( + eventFlowStepMounted('configureSSO', 'verify-domain', { + connectionStatus: organizationEnterpriseConnection.status, + }), + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleCreateDomain = async (domain: string) => { card.setError(undefined); From f1c26755091e5a1a70367650e29aa69c9b268606 Mon Sep 17 00:00:00 2001 From: Laura Beatris Date: Mon, 22 Jun 2026 16:39:02 -0300 Subject: [PATCH 4/5] Emit with organization ID as metadata --- .../components/ConfigureSSO/steps/ActivateStep.tsx | 11 +++++++++-- .../ConfigureSSO/steps/OrganizationDomainsStep.tsx | 5 ++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx index a845f51e280..dd05d890185 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/shared/react'; +import { useClerk, useOrganization } from '@clerk/shared/react'; import { eventFlowStepMounted } from '@clerk/shared/telemetry'; import { Button, Col, descriptors, Flex, Flow, Heading, Icon, localizationKeys, Text } from '@/customizables'; @@ -19,6 +19,7 @@ export const ActivateStep = (): JSX.Element => { } = useConfigureSSO(); const card = useCardState(); const clerk = useClerk(); + const { organization } = useOrganization(); // The activate step is only reachable with a configured connection, so the // domains are set; join multiples for the subtitle copy. @@ -35,7 +36,13 @@ export const ActivateStep = (): JSX.Element => { try { await setConnectionActive(enterpriseConnection.id, true); - clerk.telemetry?.record(eventFlowStepMounted('configureSSO', 'activate', { connectionStatus: 'active' })); + clerk.telemetry?.record( + eventFlowStepMounted('configureSSO', 'activate', { + connectionStatus: 'active', + connectionId: enterpriseConnection.id, + organizationId: organization?.id ?? null, + }), + ); onExit?.(); } catch (err) { handleError(err as Error, [], card.setError); diff --git a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx index 4b02d2d7fe1..8ed97876738 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx @@ -1,4 +1,4 @@ -import { useClerk, useUser } from '@clerk/shared/react'; +import { useClerk, useOrganization, useUser } from '@clerk/shared/react'; import { eventFlowStepMounted } from '@clerk/shared/telemetry'; import type { OrganizationDomainResource } from '@clerk/shared/types'; import type React from 'react'; @@ -49,6 +49,7 @@ export const OrganizationDomainsStep = (): JSX.Element => { const { goPrev, goNext, isFirstStep, isLastStep } = useWizard(); const card = useCardState(); const clerk = useClerk(); + const { organization } = useOrganization(); const [domainToRemove, setDomainToRemove] = useState(null); const hasRecordedTelemetryEvent = useRef(false); @@ -61,6 +62,8 @@ export const OrganizationDomainsStep = (): JSX.Element => { clerk.telemetry?.record( eventFlowStepMounted('configureSSO', 'verify-domain', { connectionStatus: organizationEnterpriseConnection.status, + connectionId: enterpriseConnection?.id ?? null, + organizationId: organization?.id ?? null, }), ); // eslint-disable-next-line react-hooks/exhaustive-deps From 56719d33a8356ce45802a2d7a2b87d286b2d6b54 Mon Sep 17 00:00:00 2001 From: Laura Beatris Date: Mon, 22 Jun 2026 16:46:59 -0300 Subject: [PATCH 5/5] Add changeset --- .changeset/gold-ends-throw.md | 6 ++++++ packages/shared/src/telemetry/events/flow-step.ts | 15 +++++++-------- .../ConfigureSSO/steps/ActivateStep.tsx | 1 + .../steps/OrganizationDomainsStep.tsx | 1 + 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/gold-ends-throw.md diff --git a/.changeset/gold-ends-throw.md b/.changeset/gold-ends-throw.md new file mode 100644 index 00000000000..6d17c9ff9dc --- /dev/null +++ b/.changeset/gold-ends-throw.md @@ -0,0 +1,6 @@ +--- +'@clerk/shared': patch +'@clerk/ui': patch +--- + +Add a generic `FLOW_STEP_MOUNTED` telemetry event (`eventFlowStepMounted`) for measuring multi-step flow funnels, and wire it into the self-serve SSO flow diff --git a/packages/shared/src/telemetry/events/flow-step.ts b/packages/shared/src/telemetry/events/flow-step.ts index 056c1954f19..f6cc5f936bc 100644 --- a/packages/shared/src/telemetry/events/flow-step.ts +++ b/packages/shared/src/telemetry/events/flow-step.ts @@ -8,24 +8,24 @@ type EventFlowStepMounted = { flow: string; /** The step/part that mounted, e.g. `verify-domain` */ step: string; - /** ISO-8601 timestamp, used to measure the time between steps of the same flow. */ - timestamp: string; + /** Free-form, flow-specific metadata supplied by the caller (e.g. `timestamp`, `connectionStatus`). */ + metadata: TelemetryEventRaw['payload']; } & TelemetryEventRaw['payload']; /** - * Fires an event when a part of a multi-step flow becomes visible. + * Fires an event from a part of a multi-step flow. * * @param flow - The flow identifier (matches `Flow.Root`'s `flow`). * @param step - The step/part that mounted. - * @param payload - Extra, flow-specific metadata + * @param metadata - Flow-specific metadata sent under `payload.metadata`. * @param eventSamplingRate - Override the default full-capture sampling rate. * @example - * telemetry.record(eventFlowStepMounted('configureSSO', 'verify-domain', { connectionStatus: 'unconfigured' })); + * telemetry.record(eventFlowStepMounted('configureSSO', 'verify-domain', { timestamp: new Date().toISOString(), connectionStatus: 'unconfigured' })); */ export function eventFlowStepMounted( flow: string, step: string, - payload: TelemetryEventRaw['payload'] = {}, + metadata: TelemetryEventRaw['payload'] = {}, eventSamplingRate: number = EVENT_SAMPLING_RATE, ): TelemetryEventRaw { return { @@ -34,8 +34,7 @@ export function eventFlowStepMounted( payload: { flow, step, - timestamp: new Date().toISOString(), - ...payload, + metadata, }, }; } diff --git a/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx index dd05d890185..d9d24524ec8 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/ActivateStep.tsx @@ -38,6 +38,7 @@ export const ActivateStep = (): JSX.Element => { await setConnectionActive(enterpriseConnection.id, true); clerk.telemetry?.record( eventFlowStepMounted('configureSSO', 'activate', { + timestamp: new Date().toISOString(), connectionStatus: 'active', connectionId: enterpriseConnection.id, organizationId: organization?.id ?? null, diff --git a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx index 8ed97876738..60306b0b86c 100644 --- a/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx +++ b/packages/ui/src/components/ConfigureSSO/steps/OrganizationDomainsStep.tsx @@ -61,6 +61,7 @@ export const OrganizationDomainsStep = (): JSX.Element => { hasRecordedTelemetryEvent.current = true; clerk.telemetry?.record( eventFlowStepMounted('configureSSO', 'verify-domain', { + timestamp: new Date().toISOString(), connectionStatus: organizationEnterpriseConnection.status, connectionId: enterpriseConnection?.id ?? null, organizationId: organization?.id ?? null,