diff --git a/.changeset/fresh-pears-brake.md b/.changeset/fresh-pears-brake.md new file mode 100644 index 00000000000..7b833c4b2fb --- /dev/null +++ b/.changeset/fresh-pears-brake.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': minor +--- + +Track fapi requests triggered by UI components. diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index cf8258e7f07..b94ea1453dc 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -101,6 +101,7 @@ import { windowNavigate, } from '../utils'; import { assertNoLegacyProp } from '../utils/assertNoLegacyProp'; +import { usageByUIComponents } from '../utils/detect-ui-caller'; import { memoizeListenerCallback } from '../utils/memoizeStateListenerCallback'; import { RedirectUrls } from '../utils/redirectUrls'; import { AuthCookieService } from './auth/AuthCookieService'; @@ -323,6 +324,9 @@ export class Clerk implements ClerkInterface { getSessionId: () => { return this.session?.id; }, + isTriggeredByUI: () => { + return usageByUIComponents.get(); + }, proxyUrl: this.proxyUrl, }); // This line is used for the piggy-backing mechanism diff --git a/packages/clerk-js/src/core/fapiClient.ts b/packages/clerk-js/src/core/fapiClient.ts index f996ccd8db4..4f159fd7897 100644 --- a/packages/clerk-js/src/core/fapiClient.ts +++ b/packages/clerk-js/src/core/fapiClient.ts @@ -13,6 +13,7 @@ export type FapiRequestInit = RequestInit & { path?: string; search?: ConstructorParameters[0]; sessionId?: string; + uiTriggered?: boolean; rotatingTokenNonce?: string; pathPrefix?: string; url?: URL; @@ -67,6 +68,7 @@ type FapiClientOptions = { proxyUrl?: string; instanceType: InstanceType; getSessionId: () => string | undefined; + isTriggeredByUI: () => boolean; isSatellite?: boolean; }; @@ -102,7 +104,14 @@ export function createFapiClient(options: FapiClientOptions): FapiClient { return true; } - function buildQueryString({ method, path, sessionId, search, rotatingTokenNonce }: FapiRequestInit): string { + function buildQueryString({ + method, + path, + sessionId, + search, + rotatingTokenNonce, + uiTriggered, + }: FapiRequestInit): string { const searchParams = new URLSearchParams(search as any); // the above will parse {key: ['val1','val2']} as key: 'val1,val2' and we need to recreate the array bellow @@ -130,6 +139,10 @@ export function createFapiClient(options: FapiClientOptions): FapiClient { searchParams.append('_clerk_session_id', sessionId); } + if (uiTriggered) { + searchParams.append('_clerk_ui_triggered', 'true'); + } + // TODO: extract to generic helper const objParams = [...searchParams.entries()].reduce( (acc, [k, v]) => { @@ -191,6 +204,7 @@ export function createFapiClient(options: FapiClientOptions): FapiClient { ...requestInit, // TODO: Pass these values to the FAPI client instead of calculating them on the spot sessionId: options.getSessionId(), + uiTriggered: options.isTriggeredByUI(), }); // Normalize requestInit.headers diff --git a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx index 78b186bba95..dc3c8c32209 100644 --- a/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx +++ b/packages/clerk-js/src/ui/contexts/CoreClerkContextWrapper.tsx @@ -8,6 +8,7 @@ import { import type { Clerk, LoadedClerk, Resources } from '@clerk/types'; import React from 'react'; +import { makeUICaller } from '../../utils/detect-ui-caller'; import { assertClerkSingletonExists } from './utils'; type CoreClerkContextWrapperProps = { @@ -35,10 +36,11 @@ export function CoreClerkContextWrapper(props: CoreClerkContextWrapperProps): JS }, []); const { client, session, user, organization } = state; - const clerkCtx = React.useMemo(() => ({ value: clerk }), []); - const clientCtx = React.useMemo(() => ({ value: client }), [client]); - const sessionCtx = React.useMemo(() => ({ value: session }), [session]); - const userCtx = React.useMemo(() => ({ value: user }), [user]); + const clerkCtx = React.useMemo(() => ({ value: makeUICaller(clerk) }), []); + const clientCtx = React.useMemo(() => ({ value: makeUICaller(client) }), [client]); + const sessionCtx = React.useMemo(() => ({ value: makeUICaller(session) }), [session]); + const userCtx = React.useMemo(() => ({ value: makeUICaller(user) }), [user]); + const organizationCtx = React.useMemo( () => ({ value: { organization: organization }, diff --git a/packages/clerk-js/src/utils/detect-ui-caller.ts b/packages/clerk-js/src/utils/detect-ui-caller.ts new file mode 100644 index 00000000000..b4c4ec34663 --- /dev/null +++ b/packages/clerk-js/src/utils/detect-ui-caller.ts @@ -0,0 +1,49 @@ +function createStore(initialUsages: number) { + let state = initialUsages; + + return { + get: () => state > 0, + increment: () => { + state++; + }, + decrease: () => { + state = Math.max(state - 1, 0); + }, + }; +} + +export const usageByUIComponents = createStore(0); + +const isThenable = (value: unknown): value is Promise => { + return !!value && typeof (value as any).then === 'function'; +}; + +export const makeUICaller = (resource: T): T => { + if (!resource) return null as T; + // return resource + const resourceProxy = new Proxy(resource, { + get(target, prop) { + // @ts-expect-error + const value = target[prop]; + if (typeof value === 'function') { + // @ts-expect-error + return function (...args) { + usageByUIComponents.increment(); + const result = value.apply(target, args); + if (isThenable(result)) { + return result.finally(() => usageByUIComponents.decrease()); + } + usageByUIComponents.decrease(); + return result; + }; + } + + // Allows for nested objects to be proxied (e.g. `client.signIn.create()`) + if (typeof value === 'object') { + return makeUICaller(value); + } + return value; + }, + }); + return resourceProxy; +};