From 91820dd2b5f3f4e57fb56f215ededbf880592331 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Wed, 6 May 2026 22:17:43 +0530 Subject: [PATCH 01/10] feat: structure for telemetry --- .../core/src/api/__tests__/telemetry.test.ts | 41 ++++++++++++++++++ packages/core/src/api/index.ts | 1 + packages/core/src/api/telemetry.ts | 43 +++++++++++++++++++ .../__tests__/my-account-client.test.ts | 7 ++- .../services/my-account/my-account-client.ts | 11 +++-- .../__tests__/my-organization-client.test.ts | 7 ++- .../my-organization/my-organization-client.ts | 11 +++-- 7 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/api/__tests__/telemetry.test.ts create mode 100644 packages/core/src/api/telemetry.ts diff --git a/packages/core/src/api/__tests__/telemetry.test.ts b/packages/core/src/api/__tests__/telemetry.test.ts new file mode 100644 index 000000000..57817f709 --- /dev/null +++ b/packages/core/src/api/__tests__/telemetry.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import { getClientInfo, SDK_VERSION, TELEMETRY_NAME } from '../telemetry'; + +describe('telemetry', () => { + describe('getClientInfo', () => { + it('should return correct client info for proxy mode', () => { + const result = getClientInfo(true); + + expect(result).toEqual({ + name: TELEMETRY_NAME, + version: SDK_VERSION, + env: { + is_proxy_mode: 'true', + }, + }); + }); + + it('should return correct client info for SPA mode', () => { + const result = getClientInfo(false); + + expect(result).toEqual({ + name: TELEMETRY_NAME, + version: SDK_VERSION, + env: { + is_proxy_mode: 'false', + }, + }); + }); + }); + + describe('constants', () => { + it('should have correct telemetry name', () => { + expect(TELEMETRY_NAME).toBe('auth0-ui-components'); + }); + + it('should have a valid version format', () => { + expect(SDK_VERSION).toMatch(/^\d+\.\d+\.\d+/); + }); + }); +}); diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index 26410fbc6..b5455f001 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -8,3 +8,4 @@ export * from './api-error'; export * from './business-error'; export * from './error-utils'; export * from './http-constants'; +export * from './telemetry'; diff --git a/packages/core/src/api/telemetry.ts b/packages/core/src/api/telemetry.ts new file mode 100644 index 000000000..5eb9484b8 --- /dev/null +++ b/packages/core/src/api/telemetry.ts @@ -0,0 +1,43 @@ +/** + * Telemetry utilities for Auth0 UI Components. + * @module telemetry + * @internal + */ + +/** + * Client information for Auth0 telemetry headers. + */ +export interface ClientInfo { + name: string; + version: string; + env?: Record; + [key: string]: unknown; +} + +/** + * The package name used in telemetry. + */ +export const TELEMETRY_NAME = 'auth0-ui-components'; + +/** + * The current SDK version. Updated during release. + */ +export const SDK_VERSION = '2.0.0'; + +/** + * Generates client information for Auth0 UI Components telemetry. + * This information is sent via the Auth0-Client header. + * + * @param isProxyMode - Whether the client is operating in proxy mode + * @returns Client info object for telemetry + * @internal + */ +export function getClientInfo(isProxyMode: boolean): ClientInfo { + return { + name: TELEMETRY_NAME, + version: SDK_VERSION, + env: { + is_proxy_mode: String(isProxyMode), + }, + }; +} diff --git a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts index 609aedfc2..edad0c292 100644 --- a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts +++ b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { stubFetch } from '../../../api/__tests__/__mocks__/api-utils.mocks'; import { AUTH0_SCOPE_HEADER } from '../../../api/api-utils'; +import { getClientInfo } from '../../../api/telemetry'; import type { FetcherSupplier, SpaAuthConfig } from '../../../auth/auth-types'; import { createMockContextInterface, @@ -45,7 +46,8 @@ describe('createMyAccountClient', () => { expect(MyAccountClient).toHaveBeenCalledWith( expect.objectContaining({ baseUrl: new URL(MY_ACCOUNT_PROXY_PATH, mockProxyConfig.proxyUrl).href, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(true), }), ); }); @@ -56,7 +58,8 @@ describe('createMyAccountClient', () => { expect(MyAccountClient).toHaveBeenCalledWith( expect.objectContaining({ domain: TEST_DOMAIN, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(false), }), ); }); diff --git a/packages/core/src/services/my-account/my-account-client.ts b/packages/core/src/services/my-account/my-account-client.ts index 82bbe0c3a..b2399bf97 100644 --- a/packages/core/src/services/my-account/my-account-client.ts +++ b/packages/core/src/services/my-account/my-account-client.ts @@ -7,6 +7,7 @@ import { MyAccountClient } from '@auth0/myaccount-js'; import { createProxyFetcher, createSpaFetcher } from '../../api/api-utils'; +import { getClientInfo } from '../../api/telemetry'; import type { ClientAuthConfig } from '../../auth/auth-types'; export const MY_ACCOUNT_PROXY_PATH = 'me'; @@ -19,18 +20,22 @@ export const MY_ACCOUNT_DPOP_NONCE_ID = '__auth0_my_account_api__'; * @internal */ export function createMyAccountClient(config: ClientAuthConfig) { - if (config.mode === 'proxy') { + const isProxyMode = config.mode === 'proxy'; + + if (isProxyMode) { return new MyAccountClient({ domain: config.domain ?? '', baseUrl: new URL(MY_ACCOUNT_PROXY_PATH, config.proxyUrl).href, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(true), fetcher: createProxyFetcher(config.fetcher), }); } return new MyAccountClient({ domain: config.domain, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(false), fetcher: createSpaFetcher(config, MY_ACCOUNT_DPOP_NONCE_ID), }); } diff --git a/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts b/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts index 6c3445d5e..15de3203b 100644 --- a/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts +++ b/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { stubFetch } from '../../../api/__tests__/__mocks__/api-utils.mocks'; import { AUTH0_SCOPE_HEADER } from '../../../api/api-utils'; +import { getClientInfo } from '../../../api/telemetry'; import type { FetcherSupplier, SpaAuthConfig } from '../../../auth/auth-types'; import { createMockContextInterface, @@ -45,7 +46,8 @@ describe('createMyOrganizationClient', () => { expect(MyOrganizationClient).toHaveBeenCalledWith( expect.objectContaining({ baseUrl: new URL(MY_ORGANIZATION_PROXY_PATH, mockProxyConfig.proxyUrl).href, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(true), }), ); }); @@ -56,7 +58,8 @@ describe('createMyOrganizationClient', () => { expect(MyOrganizationClient).toHaveBeenCalledWith( expect.objectContaining({ domain: TEST_DOMAIN, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(false), }), ); }); diff --git a/packages/core/src/services/my-organization/my-organization-client.ts b/packages/core/src/services/my-organization/my-organization-client.ts index f2efb5834..d8230fffe 100644 --- a/packages/core/src/services/my-organization/my-organization-client.ts +++ b/packages/core/src/services/my-organization/my-organization-client.ts @@ -7,6 +7,7 @@ import { MyOrganizationClient } from '@auth0/myorganization-js'; import { createProxyFetcher, createSpaFetcher } from '../../api/api-utils'; +import { getClientInfo } from '../../api/telemetry'; import type { ClientAuthConfig } from '../../auth/auth-types'; export const MY_ORGANIZATION_PROXY_PATH = 'my-org'; @@ -19,18 +20,22 @@ export const MY_ORGANIZATION_DPOP_NONCE_ID = '__auth0_my_organization_api__'; * @internal */ export function createMyOrganizationClient(config: ClientAuthConfig) { - if (config.mode === 'proxy') { + const isProxyMode = config.mode === 'proxy'; + + if (isProxyMode) { return new MyOrganizationClient({ domain: config.domain ?? '', baseUrl: new URL(MY_ORGANIZATION_PROXY_PATH, config.proxyUrl).href, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(true), fetcher: createProxyFetcher(config.fetcher), }); } return new MyOrganizationClient({ domain: config.domain, - telemetry: false, + telemetry: true, + clientInfo: getClientInfo(false), fetcher: createSpaFetcher(config, MY_ORGANIZATION_DPOP_NONCE_ID), }); } From e1cf4929c92d6ddae866a602f341c6cfcd0ec4c3 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Mon, 25 May 2026 14:43:16 +0530 Subject: [PATCH 02/10] feat: telemetry intial structure --- .../core/src/api/__tests__/api-utils.test.ts | 132 +++++++++++++-- .../core/src/api/__tests__/telemetry.test.ts | 154 +++++++++++++++--- packages/core/src/api/api-utils.ts | 39 ++++- packages/core/src/api/telemetry.ts | 120 +++++++++++--- .../src/auth/__tests__/core-client.test.ts | 74 +++++---- packages/core/src/auth/core-client.ts | 9 +- packages/core/src/index.ts | 7 + .../__tests__/my-account-client.test.ts | 48 ++++-- .../services/my-account/my-account-client.ts | 18 +- .../__tests__/my-organization-client.test.ts | 40 +++-- .../my-organization/my-organization-client.ts | 18 +- .../use-core-client-initialization.test.tsx | 24 ++- .../shared/use-core-client-initialization.ts | 19 ++- packages/react/src/lib/utils/css-detection.ts | 42 +++++ .../__tests__/proxy-provider.test.tsx | 2 +- .../providers/__tests__/spa-provider.test.tsx | 2 +- .../react/src/providers/proxy-provider.tsx | 86 +++++++--- packages/react/src/providers/spa-provider.tsx | 87 +++++++--- packages/react/tsup.config.ts | 3 + 19 files changed, 731 insertions(+), 193 deletions(-) create mode 100644 packages/react/src/lib/utils/css-detection.ts diff --git a/packages/core/src/api/__tests__/api-utils.test.ts b/packages/core/src/api/__tests__/api-utils.test.ts index 5d27276c9..f752e6a6b 100644 --- a/packages/core/src/api/__tests__/api-utils.test.ts +++ b/packages/core/src/api/__tests__/api-utils.test.ts @@ -5,11 +5,23 @@ import { createMockContextInterface, TEST_DOMAIN, } from '../../internals/__mocks__/shared/api-service.mocks'; -import { AUTH0_SCOPE_HEADER, createProxyFetcher, createSpaFetcher } from '../api-utils'; +import { + AUTH0_CLIENT_HEADER, + AUTH0_SCOPE_HEADER, + createProxyFetcher, + createSpaFetcher, +} from '../api-utils'; import { ContentType, HeaderName } from '../http-constants'; +import type { TelemetryConfig } from '../telemetry'; import { stubFetch } from './__mocks__/api-utils.mocks'; +const defaultTelemetry: TelemetryConfig = { + css: 'unknown', + distribution: 'npm', + framework: 'react', +}; + describe('api-utils', () => { describe('createProxyFetcher', () => { afterEach(() => { @@ -18,7 +30,7 @@ describe('api-utils', () => { it('sets content-type header to application/json', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher(); + const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); await fetcher('https://example.com/api', { method: 'POST' }, undefined); @@ -28,7 +40,7 @@ describe('api-utils', () => { it('sets auth0-scope header when scope array is provided', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher(); + const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); await fetcher( 'https://example.com/api', @@ -45,7 +57,7 @@ describe('api-utils', () => { it('does not set auth0-scope header when scope array is empty', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher(); + const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] }); @@ -55,7 +67,7 @@ describe('api-utils', () => { it('does not set auth0-scope header when authParams is undefined', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher(); + const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); await fetcher('https://example.com/api', { method: 'GET' }, undefined); @@ -65,7 +77,7 @@ describe('api-utils', () => { it('preserves existing headers from init', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher(); + const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); const customHeaders = new Headers({ 'X-Custom': 'value' }); await fetcher( @@ -82,7 +94,7 @@ describe('api-utils', () => { it('preserves other init options', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher(); + const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); const body = JSON.stringify({ data: 'test' }); await fetcher( @@ -96,6 +108,55 @@ describe('api-utils', () => { expect(requestInit?.body).toBe(body); expect(requestInit?.credentials).toBe('include'); }); + + it('sets Auth0-Client telemetry header with proxy mode', async () => { + const mockFetch = stubFetch(); + const fetcher = createProxyFetcher({ + telemetry: { css: 'tailwind', distribution: 'npm', framework: 'react' }, + }); + + await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined); + + const [, requestInit] = mockFetch.mock.calls[0]!; + const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER); + expect(header).toBeTruthy(); + + const decoded = JSON.parse(atob(header!)); + expect(decoded.is_proxy_mode).toBe(true); + expect(decoded.component).toBe('user-mfa-management'); + expect(decoded.name).toBe('universal-components'); + expect(decoded.css).toBe('tailwind'); + expect(decoded.distribution).toBe('npm'); + expect(decoded.framework).toBe('react'); + }); + + it('sets correct component based on URL path', async () => { + const mockFetch = stubFetch(); + const fetcher = createProxyFetcher({ + telemetry: { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + }); + + await fetcher('https://example.com/my-org/identity-providers', { method: 'GET' }, undefined); + + const [, requestInit] = mockFetch.mock.calls[0]!; + const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER); + const decoded = JSON.parse(atob(header!)); + expect(decoded.component).toBe('organization-sso-configuration'); + expect(decoded.css).toBe('scoped'); + expect(decoded.distribution).toBe('shadcn'); + }); + + it('uses custom fetcher when provided', async () => { + const customFetcher = vi.fn().mockResolvedValue(new Response()); + const fetcher = createProxyFetcher({ customFetcher, telemetry: defaultTelemetry }); + + await fetcher('https://example.com/api', { method: 'GET' }, undefined); + + expect(customFetcher).toHaveBeenCalledWith( + 'https://example.com/api', + expect.objectContaining({ method: 'GET' }), + ); + }); }); describe('createSpaFetcher', () => { @@ -124,14 +185,14 @@ describe('api-utils', () => { const config = createSpaConfig(); const dpopNonceId = '__test_dpop_nonce__'; - createSpaFetcher(config, dpopNonceId); + createSpaFetcher(config, dpopNonceId, defaultTelemetry); expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId }); }); it('sets Content-Type header to application/json', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); await fetcher('https://example.com/api', { method: 'POST' }, undefined); @@ -141,7 +202,7 @@ describe('api-utils', () => { it('preserves existing headers from init when adding Content-Type', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); const customHeaders = new Headers({ 'X-Custom': 'value' }); await fetcher( @@ -158,7 +219,7 @@ describe('api-utils', () => { it('preserves other init options when adding Content-Type header', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); const body = JSON.stringify({ data: 'test' }); await fetcher( @@ -176,7 +237,7 @@ describe('api-utils', () => { it('delegates to SDK fetchWithAuth with scope and audience', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); await fetcher( 'https://example.com/api', @@ -193,7 +254,7 @@ describe('api-utils', () => { it('handles undefined authParams', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); await fetcher('https://example.com/api', { method: 'GET' }, undefined); @@ -206,7 +267,7 @@ describe('api-utils', () => { it('handles empty scope array', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] }); @@ -219,7 +280,7 @@ describe('api-utils', () => { it('handles undefined init parameter', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__'); + const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); await fetcher('https://example.com/api', undefined, { scope: ['read:users'] }); @@ -232,5 +293,46 @@ describe('api-utils', () => { }, ); }); + + it('sets Auth0-Client telemetry header with SPA mode', async () => { + const config = createSpaConfig(); + const fetcher = createSpaFetcher(config, '__test_nonce__', { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); + + await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined); + + const [, requestInit] = mockFetchWithAuth.mock.calls[0]!; + const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER); + expect(header).toBeTruthy(); + + const decoded = JSON.parse(atob(header!)); + expect(decoded.is_proxy_mode).toBe(false); + expect(decoded.component).toBe('user-mfa-management'); + expect(decoded.name).toBe('universal-components'); + expect(decoded.css).toBe('tailwind'); + expect(decoded.distribution).toBe('npm'); + expect(decoded.framework).toBe('react'); + }); + + it('sets correct component based on URL path', async () => { + const config = createSpaConfig(); + const fetcher = createSpaFetcher(config, '__test_nonce__', { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); + + await fetcher('https://example.com/my-org/domains', { method: 'GET' }, undefined); + + const [, requestInit] = mockFetchWithAuth.mock.calls[0]!; + const header = (requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER); + const decoded = JSON.parse(atob(header!)); + expect(decoded.component).toBe('organization-domain-management'); + expect(decoded.css).toBe('scoped'); + expect(decoded.distribution).toBe('shadcn'); + }); }); }); diff --git a/packages/core/src/api/__tests__/telemetry.test.ts b/packages/core/src/api/__tests__/telemetry.test.ts index 57817f709..714e3bc4b 100644 --- a/packages/core/src/api/__tests__/telemetry.test.ts +++ b/packages/core/src/api/__tests__/telemetry.test.ts @@ -1,41 +1,145 @@ import { describe, expect, it } from 'vitest'; -import { getClientInfo, SDK_VERSION, TELEMETRY_NAME } from '../telemetry'; +import { + buildTelemetryHeader, + getComponentFromUrl, + PACKAGE_VERSION, + TELEMETRY_NAME, +} from '../telemetry'; describe('telemetry', () => { - describe('getClientInfo', () => { - it('should return correct client info for proxy mode', () => { - const result = getClientInfo(true); - - expect(result).toEqual({ - name: TELEMETRY_NAME, - version: SDK_VERSION, - env: { - is_proxy_mode: 'true', - }, + describe('constants', () => { + it('should have correct telemetry name', () => { + expect(TELEMETRY_NAME).toBe('universal-components'); + }); + + it('should have a valid version format', () => { + expect(PACKAGE_VERSION).toMatch(/^\d+\.\d+\.\d+/); + }); + }); + + describe('getComponentFromUrl', () => { + it('should return user-mfa-management for authentication-methods URLs', () => { + expect(getComponentFromUrl('https://example.com/me/authentication-methods')).toBe( + 'user-mfa-management', + ); + expect(getComponentFromUrl('https://example.com/me/authentication-methods/123')).toBe( + 'user-mfa-management', + ); + }); + + it('should return organization-sso-configuration for identity-providers URLs', () => { + expect(getComponentFromUrl('https://example.com/my-org/identity-providers')).toBe( + 'organization-sso-configuration', + ); + expect(getComponentFromUrl('https://example.com/my-org/identity-providers/456/domains')).toBe( + 'organization-sso-configuration', + ); + }); + + it('should return organization-domain-management for domains URLs', () => { + expect(getComponentFromUrl('https://example.com/my-org/domains')).toBe( + 'organization-domain-management', + ); + expect(getComponentFromUrl('https://example.com/my-org/domains/789/verify')).toBe( + 'organization-domain-management', + ); + }); + + it('should return organization-details for configuration URLs', () => { + expect(getComponentFromUrl('https://example.com/my-org/configuration')).toBe( + 'organization-details', + ); + }); + + it('should return unknown for unrecognized URLs', () => { + expect(getComponentFromUrl('https://example.com/unknown/endpoint')).toBe('unknown'); + expect(getComponentFromUrl('https://example.com/api/users')).toBe('unknown'); + }); + }); + + describe('buildTelemetryHeader', () => { + it('should return base64-encoded JSON for proxy mode with full telemetry config', () => { + const header = buildTelemetryHeader('https://example.com/me/authentication-methods', { + isProxyMode: true, + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); + + const decoded = JSON.parse(atob(header)); + expect(decoded).toEqual({ + name: 'universal-components', + version: PACKAGE_VERSION, + is_proxy_mode: true, + framework: 'react', + component: 'user-mfa-management', + distribution: 'npm', + css: 'tailwind', }); }); - it('should return correct client info for SPA mode', () => { - const result = getClientInfo(false); + it('should return base64-encoded JSON for SPA mode with shadcn distribution', () => { + const header = buildTelemetryHeader('https://example.com/my-org/identity-providers', { + isProxyMode: false, + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); - expect(result).toEqual({ - name: TELEMETRY_NAME, - version: SDK_VERSION, - env: { - is_proxy_mode: 'false', - }, + const decoded = JSON.parse(atob(header)); + expect(decoded).toEqual({ + name: 'universal-components', + version: PACKAGE_VERSION, + is_proxy_mode: false, + framework: 'react', + component: 'organization-sso-configuration', + distribution: 'shadcn', + css: 'scoped', }); }); - }); - describe('constants', () => { - it('should have correct telemetry name', () => { - expect(TELEMETRY_NAME).toBe('auth0-ui-components'); + it('should set component to unknown for unrecognized URLs', () => { + const header = buildTelemetryHeader('https://example.com/unknown', { + isProxyMode: false, + css: 'unknown', + distribution: 'npm', + framework: 'react', + }); + + const decoded = JSON.parse(atob(header)); + expect(decoded.component).toBe('unknown'); + expect(decoded.css).toBe('unknown'); }); - it('should have a valid version format', () => { - expect(SDK_VERSION).toMatch(/^\d+\.\d+\.\d+/); + it('should include all required telemetry fields', () => { + const header = buildTelemetryHeader('https://example.com/my-org/domains', { + isProxyMode: true, + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); + + const decoded = JSON.parse(atob(header)); + expect(decoded).toHaveProperty('name'); + expect(decoded).toHaveProperty('version'); + expect(decoded).toHaveProperty('is_proxy_mode'); + expect(decoded).toHaveProperty('framework'); + expect(decoded).toHaveProperty('component'); + expect(decoded).toHaveProperty('distribution'); + expect(decoded).toHaveProperty('css'); + }); + + it('should use provided framework value', () => { + const header = buildTelemetryHeader('https://example.com/me/authentication-methods', { + isProxyMode: false, + css: 'tailwind', + distribution: 'npm', + framework: 'vue', + }); + + const decoded = JSON.parse(atob(header)); + expect(decoded.framework).toBe('vue'); }); }); }); diff --git a/packages/core/src/api/api-utils.ts b/packages/core/src/api/api-utils.ts index 08365f9e7..b364f120a 100644 --- a/packages/core/src/api/api-utils.ts +++ b/packages/core/src/api/api-utils.ts @@ -7,42 +7,65 @@ import type { FetcherSupplier, SpaAuthConfig } from '../auth/auth-types'; import { ContentType, HeaderName } from './http-constants'; +import { buildTelemetryHeader, type TelemetryConfig } from './telemetry'; export const AUTH0_SCOPE_HEADER = HeaderName.Auth0Scope; +export const AUTH0_CLIENT_HEADER = 'Auth0-Client'; + +/** + * Configuration for proxy mode fetcher with telemetry. + */ +export interface ProxyFetcherConfig { + customFetcher?: (url: string, init?: RequestInit) => Promise; + telemetry: TelemetryConfig; +} /** * Creates a fetcher function for proxy mode that injects scopes via auth0-scope header. * The proxy will extract scopes from the header and request the appropriate token. - * @param customFetcher - Optional custom fetch implementation to use instead of global fetch - * @returns Fetcher function that sets auth0-scope and content-type headers + * Also adds telemetry header with component info derived from the URL. + * @param config - Fetcher configuration with optional custom fetcher and telemetry config + * @returns Fetcher function that sets auth0-scope, content-type, and telemetry headers * @internal */ -export function createProxyFetcher( - customFetcher?: (url: string, init?: RequestInit) => Promise, -): FetcherSupplier { - const fetchFn = customFetcher ?? fetch; +export function createProxyFetcher(config: ProxyFetcherConfig): FetcherSupplier { + const fetchFn = config.customFetcher ?? fetch; return async (url, init, authParams) => { const headers = new Headers(init?.headers); headers.set(HeaderName.ContentType, ContentType.JSON); if (authParams?.scope?.length) { headers.set(HeaderName.Auth0Scope, authParams.scope.join(' ')); } + headers.set( + AUTH0_CLIENT_HEADER, + buildTelemetryHeader(url, { isProxyMode: true, ...config.telemetry }), + ); return fetchFn(url, { ...init, headers }); }; } /** * Creates a fetcher function for SPA mode using Auth0 SDK's createFetcher. + * Also adds telemetry header with component info derived from the URL. * @param config - SPA auth configuration with context interface * @param dpopNonceId - Unique identifier for DPoP nonce management - * @returns Fetcher function that delegates to SDK's fetchWithAuth with JSON content-type + * @param telemetry - Telemetry configuration + * @returns Fetcher function that delegates to SDK's fetchWithAuth with JSON content-type and telemetry * @internal */ -export function createSpaFetcher(config: SpaAuthConfig, dpopNonceId: string): FetcherSupplier { +export function createSpaFetcher( + config: SpaAuthConfig, + dpopNonceId: string, + telemetry: TelemetryConfig, +): FetcherSupplier { const sdkFetcher = config.contextInterface.createFetcher({ dpopNonceId }); return (url, init, authParams) => { const headers = new Headers(init?.headers); headers.set(HeaderName.ContentType, ContentType.JSON); + headers.set( + AUTH0_CLIENT_HEADER, + buildTelemetryHeader(url, { isProxyMode: false, ...telemetry }), + ); return sdkFetcher.fetchWithAuth( url, { ...init, headers }, diff --git a/packages/core/src/api/telemetry.ts b/packages/core/src/api/telemetry.ts index 5eb9484b8..766fa733d 100644 --- a/packages/core/src/api/telemetry.ts +++ b/packages/core/src/api/telemetry.ts @@ -4,40 +4,118 @@ * @internal */ +import pkg from '../../package.json'; + /** - * Client information for Auth0 telemetry headers. + * The package name used in telemetry. */ -export interface ClientInfo { - name: string; - version: string; - env?: Record; - [key: string]: unknown; +export const TELEMETRY_NAME = 'universal-components'; + +/** + * The current package version. Read from package.json at build time. + */ +export const PACKAGE_VERSION: string = pkg.version; + +/** + * CSS implementation type for telemetry. + */ +export type CssImplementation = 'scoped' | 'tailwind' | 'unknown'; + +/** + * Distribution channel type. + */ +export type DistributionChannel = 'npm' | 'shadcn'; + +/** + * Framework type for telemetry. + */ +export type Framework = 'react' | 'vue' | 'angular'; + +/** + * Telemetry configuration passed from framework packages. + * Groups all telemetry-related settings in one object. + */ +export interface TelemetryConfig { + css: CssImplementation; + distribution: DistributionChannel; + framework: Framework; } /** - * The package name used in telemetry. + * Map of Auth0 API URL paths to component telemetry names. + * Only includes currently exposed block components. + * @internal */ -export const TELEMETRY_NAME = 'auth0-ui-components'; +const URL_TO_COMPONENT_MAP: Record = { + // MyAccount API (me/) - UserMFAMgmt component + '/authentication-methods': 'user-mfa-management', + + // MyOrganization API (my-org/) - SSO components + '/identity-providers': 'organization-sso-configuration', + + // MyOrganization API (my-org/) - DomainTable component + '/domains': 'organization-domain-management', + + // MyOrganization API (my-org/) - OrganizationDetailsEdit component + '/details': 'organization-details', + + // MyOrganization API (my-org/) - Organization configuration + '/configuration': 'organization-details', +}; /** - * The current SDK version. Updated during release. + * Extracts the component name from an API URL. + * @param url - The API URL being called + * @returns The component telemetry name or 'unknown' + * @internal */ -export const SDK_VERSION = '2.0.0'; +export function getComponentFromUrl(url: string): string { + for (const [path, component] of Object.entries(URL_TO_COMPONENT_MAP)) { + if (url.includes(path)) { + return component; + } + } + return 'unknown'; +} /** - * Generates client information for Auth0 UI Components telemetry. - * This information is sent via the Auth0-Client header. - * - * @param isProxyMode - Whether the client is operating in proxy mode - * @returns Client info object for telemetry + * Telemetry payload structure sent in the Auth0-Client header. + */ +export interface TelemetryPayload { + name: string; + version: string; + is_proxy_mode: boolean; + framework: string; + component: string; + distribution: DistributionChannel; + css: CssImplementation; +} + +/** + * Configuration options for building telemetry header. + * Extends TelemetryConfig with request-specific options. + */ +export interface TelemetryOptions extends TelemetryConfig { + isProxyMode: boolean; +} + +/** + * Builds the base64-encoded telemetry header value. + * @param url - The API URL being called (used to determine component) + * @param options - Telemetry configuration options + * @returns Base64-encoded JSON telemetry payload * @internal */ -export function getClientInfo(isProxyMode: boolean): ClientInfo { - return { +export function buildTelemetryHeader(url: string, options: TelemetryOptions): string { + const payload: TelemetryPayload = { name: TELEMETRY_NAME, - version: SDK_VERSION, - env: { - is_proxy_mode: String(isProxyMode), - }, + version: PACKAGE_VERSION, + is_proxy_mode: options.isProxyMode, + framework: options.framework, + component: getComponentFromUrl(url), + distribution: options.distribution, + css: options.css, }; + + return btoa(JSON.stringify(payload)); } diff --git a/packages/core/src/auth/__tests__/core-client.test.ts b/packages/core/src/auth/__tests__/core-client.test.ts index 9f82b287b..5b0106562 100644 --- a/packages/core/src/auth/__tests__/core-client.test.ts +++ b/packages/core/src/auth/__tests__/core-client.test.ts @@ -63,10 +63,16 @@ describe('createCoreClient', () => { initializeMfaStepUpClientMock.mockReturnValue(mockMfaApiClient); }); + const defaultTelemetry = { + css: 'unknown' as const, + distribution: 'npm' as const, + framework: 'react' as const, + }; + describe('i18n initialization', () => { it('initializes i18n with default options when none are provided', async () => { const authDetails = createAuthDetails(); - await createCoreClient(authDetails); + await createCoreClient(authDetails, undefined, defaultTelemetry); expect(createI18nServiceMock).toHaveBeenCalledWith({ currentLanguage: 'en-US', @@ -77,14 +83,14 @@ describe('createCoreClient', () => { it('initializes i18n with provided language options', async () => { const i18nOptions = { currentLanguage: 'es', fallbackLanguage: 'en' }; const authDetails = createAuthDetails(); - await createCoreClient(authDetails, i18nOptions); + await createCoreClient(authDetails, i18nOptions, defaultTelemetry); expect(createI18nServiceMock).toHaveBeenCalledWith(i18nOptions); }); it('exposes i18nService on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.i18nService).toBe(mockI18nService); }); @@ -93,42 +99,52 @@ describe('createCoreClient', () => { describe('isProxyMode', () => { it('returns false when authProxyUrl is undefined', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.isProxyMode()).toBe(false); }); it('returns true when authProxyUrl is set', async () => { const authDetails = createAuthDetails({ authProxyUrl: 'https://proxy.auth0.com' }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.isProxyMode()).toBe(true); }); it('returns false when authProxyUrl is empty string', async () => { const authDetails = createAuthDetails({ authProxyUrl: '' }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.isProxyMode()).toBe(false); }); }); describe('API client initialization', () => { - it('initializes MyOrg client with auth details', async () => { + it('initializes MyOrg client with auth details and telemetry config', async () => { const authDetails = createAuthDetails(); - await createCoreClient(authDetails); + await createCoreClient(authDetails, undefined, { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); expect(createMyOrganizationClientMock).toHaveBeenCalledWith( expect.objectContaining({ mode: 'spa', domain: TEST_DOMAIN }), + { css: 'tailwind', distribution: 'npm', framework: 'react' }, ); }); - it('initializes MyAccount client with auth details', async () => { + it('initializes MyAccount client with auth details and telemetry config', async () => { const authDetails = createAuthDetails(); - await createCoreClient(authDetails); + await createCoreClient(authDetails, undefined, { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); expect(createMyAccountClientMock).toHaveBeenCalledWith( expect.objectContaining({ mode: 'spa', domain: TEST_DOMAIN }), + { css: 'scoped', distribution: 'shadcn', framework: 'react' }, ); }); }); @@ -136,28 +152,28 @@ describe('createCoreClient', () => { describe('API client access', () => { it('exposes myAccountApiClient directly on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.myAccountApiClient).toBe(mockMyAccountClient); }); it('exposes myOrganizationApiClient directly on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.myOrganizationApiClient).toBe(mockMyOrganizationClient); }); it('returns myAccountApiClient when available via getter', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getMyAccountApiClient()).toBe(mockMyAccountClient); }); it('returns myOrganizationApiClient when available via getter', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getMyOrganizationApiClient()).toBe(mockMyOrganizationClient); }); @@ -166,7 +182,7 @@ describe('createCoreClient', () => { createMyAccountClientMock.mockReturnValueOnce(null as unknown as MyAccountClient); const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(() => client.getMyAccountApiClient()).toThrow( 'myAccountApiClient is not enabled. Please use it within Auth0ComponentProvider.', @@ -176,7 +192,7 @@ describe('createCoreClient', () => { it('throws when myOrganizationApiClient is not available', async () => { createMyOrganizationClientMock.mockReturnValueOnce(null as unknown as MyOrganizationClient); const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(() => client.getMyOrganizationApiClient()).toThrow( 'myOrganizationApiClient is not enabled. Please ensure you are in an Auth0 Organization context.', @@ -185,7 +201,7 @@ describe('createCoreClient', () => { it('returns mfaApiClient via getter', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getMFAStepUpApiClient()).toBe(mockMfaApiClient); }); @@ -194,14 +210,14 @@ describe('createCoreClient', () => { describe('client properties', () => { it('exposes auth details on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.auth).toEqual(authDetails); }); it('preserves authProxyUrl in auth details', async () => { const authDetails = createAuthDetails({ authProxyUrl: 'https://custom-proxy.com' }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.auth.authProxyUrl).toBe('https://custom-proxy.com'); }); @@ -209,7 +225,7 @@ describe('createCoreClient', () => { it('preserves contextInterface in auth details', async () => { const customContext = createMockContextInterface(); const authDetails = createAuthDetails({ contextInterface: customContext }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.auth.contextInterface).toBe(customContext); }); @@ -218,7 +234,7 @@ describe('createCoreClient', () => { describe('getDomain', () => { it('returns domain in SPA mode', async () => { const authDetails = createAuthDetails({ domain: TEST_DOMAIN }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getDomain()).toBe(TEST_DOMAIN); }); @@ -228,7 +244,7 @@ describe('createCoreClient', () => { authProxyUrl: 'https://proxy.auth0.com', domain: TEST_DOMAIN, }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getDomain()).toBe(TEST_DOMAIN); }); @@ -238,7 +254,7 @@ describe('createCoreClient', () => { authProxyUrl: 'https://proxy.auth0.com', domain: undefined, }); - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getDomain()).toBeUndefined(); }); @@ -247,7 +263,7 @@ describe('createCoreClient', () => { describe('previewMode', () => { it('returns a core client with previewMode and disables API clients', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.auth).toEqual({}); expect(client.myAccountApiClient).toBeUndefined(); @@ -257,35 +273,35 @@ describe('createCoreClient', () => { it('isProxyMode returns false in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.isProxyMode()).toBe(false); }); it('getMyAccountApiClient throws in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(() => client.getMyAccountApiClient()).toThrow('Function not implemented.'); }); it('getMyOrganizationApiClient throws in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(() => client.getMyOrganizationApiClient()).toThrow('Function not implemented.'); }); it('getMFAStepUpApiClient throws in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(() => client.getMFAStepUpApiClient()).toThrow('Function not implemented.'); }); it('getDomain returns undefined in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails); + const client = await createCoreClient(authDetails, undefined, defaultTelemetry); expect(client.getDomain()).toBeUndefined(); }); diff --git a/packages/core/src/auth/core-client.ts b/packages/core/src/auth/core-client.ts index f97fae60c..939d8d4f1 100644 --- a/packages/core/src/auth/core-client.ts +++ b/packages/core/src/auth/core-client.ts @@ -6,6 +6,7 @@ import { initializeMfaStepUpClient } from '@core/services/mfa-step-up/mfa-step-up-api-service'; +import type { TelemetryConfig } from '../api/telemetry'; import type { I18nInitOptions } from '../i18n'; import { createI18nService } from '../i18n'; import { createMyAccountClient } from '../services/my-account/my-account-client'; @@ -20,11 +21,13 @@ import { AuthUtils } from './auth-utils'; * * @param authDetails - Authentication configuration details * @param i18nOptions - Internationalization options + * @param telemetry - Telemetry configuration (css, distribution, framework) * @returns Promise resolving to the initialized CoreClient */ export async function createCoreClient( authDetails: AuthDetails, - i18nOptions?: I18nInitOptions, + i18nOptions: I18nInitOptions | undefined, + telemetry: TelemetryConfig, ): Promise { const i18nService = await createI18nService( i18nOptions || { currentLanguage: 'en-US', fallbackLanguage: 'en-US' }, @@ -57,8 +60,8 @@ export async function createCoreClient( const authConfig = AuthUtils.resolveAuthConfig(authDetails); - const myOrganizationApiClient = createMyOrganizationClient(authConfig); - const myAccountApiClient = createMyAccountClient(authConfig); + const myOrganizationApiClient = createMyOrganizationClient(authConfig, telemetry); + const myAccountApiClient = createMyAccountClient(authConfig, telemetry); const mfaApiClient = initializeMfaStepUpClient(authConfig); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f8ccf3956..4a4282e24 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,6 +20,13 @@ export { createCoreClient } from './auth/core-client'; export { AuthDetails, CoreClientInterface, BasicAuth0ContextInterface } from './auth/auth-types'; +export { + type CssImplementation, + type DistributionChannel, + type Framework, + type TelemetryConfig, +} from './api/telemetry'; + export * from './schemas'; export * from './theme'; diff --git a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts index edad0c292..59dbde85f 100644 --- a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts +++ b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { stubFetch } from '../../../api/__tests__/__mocks__/api-utils.mocks'; import { AUTH0_SCOPE_HEADER } from '../../../api/api-utils'; -import { getClientInfo } from '../../../api/telemetry'; +import type { TelemetryConfig } from '../../../api/telemetry'; import type { FetcherSupplier, SpaAuthConfig } from '../../../auth/auth-types'; import { createMockContextInterface, @@ -18,6 +18,12 @@ import { vi.mock('@auth0/myaccount-js', () => ({ MyAccountClient: vi.fn() })); +const defaultTelemetry: TelemetryConfig = { + css: 'unknown', + distribution: 'npm', + framework: 'react', +}; + describe('createMyAccountClient', () => { const mockFetchWithAuth = vi.fn().mockResolvedValue(new Response()); const mockCreateFetcher = vi.fn().mockReturnValue({ @@ -41,31 +47,37 @@ describe('createMyAccountClient', () => { }); it('creates client with baseUrl in proxy mode', () => { - createMyAccountClient(mockProxyConfig); + createMyAccountClient(mockProxyConfig, { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); expect(MyAccountClient).toHaveBeenCalledWith( expect.objectContaining({ baseUrl: new URL(MY_ACCOUNT_PROXY_PATH, mockProxyConfig.proxyUrl).href, - telemetry: true, - clientInfo: getClientInfo(true), + telemetry: false, }), ); }); it('creates client with domain in SPA mode', () => { - createMyAccountClient(createSpaConfig()); + createMyAccountClient(createSpaConfig(), { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); expect(MyAccountClient).toHaveBeenCalledWith( expect.objectContaining({ domain: TEST_DOMAIN, - telemetry: true, - clientInfo: getClientInfo(false), + telemetry: false, }), ); }); it('calls SDK createFetcher with correct dpopNonceId in SPA mode', () => { - createMyAccountClient(createSpaConfig()); + createMyAccountClient(createSpaConfig(), defaultTelemetry); expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId: MY_ACCOUNT_DPOP_NONCE_ID, @@ -79,7 +91,11 @@ describe('createMyAccountClient', () => { it('sets auth0-scope header when authParams has scope array', async () => { const mockFetch = stubFetch(); - createMyAccountClient(mockProxyConfig); + createMyAccountClient(mockProxyConfig, { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -103,7 +119,11 @@ describe('createMyAccountClient', () => { it('does not set auth0-scope header when authParams has empty scope array', async () => { const mockFetch = stubFetch(); - createMyAccountClient(mockProxyConfig); + createMyAccountClient(mockProxyConfig, { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -117,7 +137,7 @@ describe('createMyAccountClient', () => { describe('SPA mode fetcher', () => { it('calls SDK fetchWithAuth with scope and audience', async () => { - createMyAccountClient(createSpaConfig()); + createMyAccountClient(createSpaConfig(), defaultTelemetry); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -136,7 +156,11 @@ describe('createMyAccountClient', () => { }); it('handles undefined authParams', async () => { - createMyAccountClient(createSpaConfig()); + createMyAccountClient(createSpaConfig(), { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; diff --git a/packages/core/src/services/my-account/my-account-client.ts b/packages/core/src/services/my-account/my-account-client.ts index b2399bf97..9c06496de 100644 --- a/packages/core/src/services/my-account/my-account-client.ts +++ b/packages/core/src/services/my-account/my-account-client.ts @@ -7,7 +7,7 @@ import { MyAccountClient } from '@auth0/myaccount-js'; import { createProxyFetcher, createSpaFetcher } from '../../api/api-utils'; -import { getClientInfo } from '../../api/telemetry'; +import type { TelemetryConfig } from '../../api/telemetry'; import type { ClientAuthConfig } from '../../auth/auth-types'; export const MY_ACCOUNT_PROXY_PATH = 'me'; @@ -16,26 +16,28 @@ export const MY_ACCOUNT_DPOP_NONCE_ID = '__auth0_my_account_api__'; /** * Creates a MyAccountClient configured for the given auth mode. * @param config - Auth configuration (proxy or SPA mode) + * @param telemetry - Telemetry configuration (css, distribution, framework) * @returns Configured MyAccountClient instance * @internal */ -export function createMyAccountClient(config: ClientAuthConfig) { +export function createMyAccountClient(config: ClientAuthConfig, telemetry: TelemetryConfig) { const isProxyMode = config.mode === 'proxy'; if (isProxyMode) { return new MyAccountClient({ domain: config.domain ?? '', baseUrl: new URL(MY_ACCOUNT_PROXY_PATH, config.proxyUrl).href, - telemetry: true, - clientInfo: getClientInfo(true), - fetcher: createProxyFetcher(config.fetcher), + telemetry: false, // We handle telemetry in our custom fetcher + fetcher: createProxyFetcher({ + customFetcher: config.fetcher, + telemetry, + }), }); } return new MyAccountClient({ domain: config.domain, - telemetry: true, - clientInfo: getClientInfo(false), - fetcher: createSpaFetcher(config, MY_ACCOUNT_DPOP_NONCE_ID), + telemetry: false, // We handle telemetry in our custom fetcher + fetcher: createSpaFetcher(config, MY_ACCOUNT_DPOP_NONCE_ID, telemetry), }); } diff --git a/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts b/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts index 15de3203b..54618c258 100644 --- a/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts +++ b/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { stubFetch } from '../../../api/__tests__/__mocks__/api-utils.mocks'; import { AUTH0_SCOPE_HEADER } from '../../../api/api-utils'; -import { getClientInfo } from '../../../api/telemetry'; +import type { TelemetryConfig } from '../../../api/telemetry'; import type { FetcherSupplier, SpaAuthConfig } from '../../../auth/auth-types'; import { createMockContextInterface, @@ -18,6 +18,12 @@ import { vi.mock('@auth0/myorganization-js', () => ({ MyOrganizationClient: vi.fn() })); +const defaultTelemetry: TelemetryConfig = { + css: 'unknown', + distribution: 'npm', + framework: 'react', +}; + describe('createMyOrganizationClient', () => { const mockFetchWithAuth = vi.fn().mockResolvedValue(new Response()); const mockCreateFetcher = vi.fn().mockReturnValue({ @@ -41,31 +47,37 @@ describe('createMyOrganizationClient', () => { }); it('creates client with baseUrl in proxy mode', () => { - createMyOrganizationClient(mockProxyConfig); + createMyOrganizationClient(mockProxyConfig, { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); expect(MyOrganizationClient).toHaveBeenCalledWith( expect.objectContaining({ baseUrl: new URL(MY_ORGANIZATION_PROXY_PATH, mockProxyConfig.proxyUrl).href, - telemetry: true, - clientInfo: getClientInfo(true), + telemetry: false, }), ); }); it('creates client with domain in SPA mode', () => { - createMyOrganizationClient(createSpaConfig()); + createMyOrganizationClient(createSpaConfig(), { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); expect(MyOrganizationClient).toHaveBeenCalledWith( expect.objectContaining({ domain: TEST_DOMAIN, - telemetry: true, - clientInfo: getClientInfo(false), + telemetry: false, }), ); }); it('calls SDK createFetcher with correct dpopNonceId in SPA mode', () => { - createMyOrganizationClient(createSpaConfig()); + createMyOrganizationClient(createSpaConfig(), defaultTelemetry); expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId: MY_ORGANIZATION_DPOP_NONCE_ID, @@ -79,7 +91,11 @@ describe('createMyOrganizationClient', () => { it('sets auth0-scope header when authParams has scope array', async () => { const mockFetch = stubFetch(); - createMyOrganizationClient(mockProxyConfig); + createMyOrganizationClient(mockProxyConfig, { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }); const constructorOptions = vi.mocked(MyOrganizationClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -97,7 +113,11 @@ describe('createMyOrganizationClient', () => { describe('SPA mode fetcher', () => { it('calls SDK fetchWithAuth with scope and audience', async () => { - createMyOrganizationClient(createSpaConfig()); + createMyOrganizationClient(createSpaConfig(), { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }); const constructorOptions = vi.mocked(MyOrganizationClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; diff --git a/packages/core/src/services/my-organization/my-organization-client.ts b/packages/core/src/services/my-organization/my-organization-client.ts index d8230fffe..ab7e941ae 100644 --- a/packages/core/src/services/my-organization/my-organization-client.ts +++ b/packages/core/src/services/my-organization/my-organization-client.ts @@ -7,7 +7,7 @@ import { MyOrganizationClient } from '@auth0/myorganization-js'; import { createProxyFetcher, createSpaFetcher } from '../../api/api-utils'; -import { getClientInfo } from '../../api/telemetry'; +import type { TelemetryConfig } from '../../api/telemetry'; import type { ClientAuthConfig } from '../../auth/auth-types'; export const MY_ORGANIZATION_PROXY_PATH = 'my-org'; @@ -16,26 +16,28 @@ export const MY_ORGANIZATION_DPOP_NONCE_ID = '__auth0_my_organization_api__'; /** * Creates a MyOrganizationClient configured for the given auth mode. * @param config - Auth configuration (proxy or SPA mode) + * @param telemetry - Telemetry configuration (css, distribution, framework) * @returns Configured MyOrganizationClient instance * @internal */ -export function createMyOrganizationClient(config: ClientAuthConfig) { +export function createMyOrganizationClient(config: ClientAuthConfig, telemetry: TelemetryConfig) { const isProxyMode = config.mode === 'proxy'; if (isProxyMode) { return new MyOrganizationClient({ domain: config.domain ?? '', baseUrl: new URL(MY_ORGANIZATION_PROXY_PATH, config.proxyUrl).href, - telemetry: true, - clientInfo: getClientInfo(true), - fetcher: createProxyFetcher(config.fetcher), + telemetry: false, // We handle telemetry in our custom fetcher + fetcher: createProxyFetcher({ + customFetcher: config.fetcher, + telemetry, + }), }); } return new MyOrganizationClient({ domain: config.domain, - telemetry: true, - clientInfo: getClientInfo(false), - fetcher: createSpaFetcher(config, MY_ORGANIZATION_DPOP_NONCE_ID), + telemetry: false, // We handle telemetry in our custom fetcher + fetcher: createSpaFetcher(config, MY_ORGANIZATION_DPOP_NONCE_ID, telemetry), }); } diff --git a/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx b/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx index 0a854cf9e..cccf8e1c1 100644 --- a/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx +++ b/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx @@ -1,4 +1,4 @@ -import type { CoreClientInterface } from '@auth0/universal-components-core'; +import type { CoreClientInterface, TelemetryConfig } from '@auth0/universal-components-core'; import { renderHook, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; @@ -12,10 +12,17 @@ describe('useCoreClientInitialization', () => { initialize: vi.fn(), } as unknown as CoreClientInterface; + const defaultTelemetry: TelemetryConfig = { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }; + const defaultProps = { authDetails: { authProxyUrl: '/api/auth', }, + telemetry: defaultTelemetry, }; beforeEach(() => { @@ -39,7 +46,11 @@ describe('useCoreClientInitialization', () => { expect(result.current).toBe(mockCoreClient); }); - expect(createCoreClient).toHaveBeenCalledWith(defaultProps.authDetails, undefined); + expect(createCoreClient).toHaveBeenCalledWith( + defaultProps.authDetails, + undefined, + defaultTelemetry, + ); }); it('should pass i18nOptions to createCoreClient', async () => { @@ -48,6 +59,11 @@ describe('useCoreClientInitialization', () => { const propsWithI18n = { authDetails: { authProxyUrl: '/api/auth' }, i18nOptions: { currentLanguage: 'es', fallbackLanguage: 'en' }, + telemetry: { + css: 'scoped' as const, + distribution: 'shadcn' as const, + framework: 'react' as const, + }, }; const { result } = renderHook(() => useCoreClientInitialization(propsWithI18n)); @@ -59,6 +75,7 @@ describe('useCoreClientInitialization', () => { expect(createCoreClient).toHaveBeenCalledWith( propsWithI18n.authDetails, propsWithI18n.i18nOptions, + propsWithI18n.telemetry, ); }); @@ -90,6 +107,7 @@ describe('useCoreClientInitialization', () => { rerender({ authDetails: { authProxyUrl: '/api/auth-v2' }, + telemetry: defaultTelemetry, }); await waitFor(() => { @@ -102,6 +120,7 @@ describe('useCoreClientInitialization', () => { const propsWithDomain = { authDetails: { authProxyUrl: '/api/auth', domain: 'test.auth0.com' }, + telemetry: defaultTelemetry, }; const { result, rerender } = renderHook((props) => useCoreClientInitialization(props), { @@ -114,6 +133,7 @@ describe('useCoreClientInitialization', () => { rerender({ authDetails: { authProxyUrl: '/api/auth', domain: 'new.auth0.com' }, + telemetry: defaultTelemetry, }); expect(createCoreClient).toHaveBeenCalledTimes(1); diff --git a/packages/react/src/hooks/shared/use-core-client-initialization.ts b/packages/react/src/hooks/shared/use-core-client-initialization.ts index 9c9ad0ae8..5c2db3d11 100644 --- a/packages/react/src/hooks/shared/use-core-client-initialization.ts +++ b/packages/react/src/hooks/shared/use-core-client-initialization.ts @@ -8,6 +8,7 @@ import type { CoreClientInterface, AuthDetails, I18nInitOptions, + TelemetryConfig, } from '@auth0/universal-components-core'; import { createCoreClient } from '@auth0/universal-components-core'; import * as React from 'react'; @@ -15,6 +16,7 @@ import * as React from 'react'; interface UseCoreClientInitializationProps { authDetails: AuthDetails; i18nOptions?: I18nInitOptions; + telemetry: TelemetryConfig; } /** @@ -25,21 +27,34 @@ interface UseCoreClientInitializationProps { export const useCoreClientInitialization = ({ authDetails, i18nOptions, + telemetry, }: UseCoreClientInitializationProps): CoreClientInterface | null => { const { authProxyUrl } = authDetails; const [coreClient, setCoreClient] = React.useState(null); + // Extract primitive values from telemetry to avoid re-runs on object reference changes + const { css, distribution, framework } = telemetry; + React.useEffect(() => { + // Wait for CSS detection to complete before initializing + if (css === 'unknown') { + return; + } + const initializeCoreClient = async () => { try { - const initializedCoreClient = await createCoreClient(authDetails, i18nOptions); + const initializedCoreClient = await createCoreClient(authDetails, i18nOptions, { + css, + distribution, + framework, + }); setCoreClient(initializedCoreClient); } catch (error) { console.error(error); } }; initializeCoreClient(); - }, [authProxyUrl, i18nOptions]); + }, [authProxyUrl, i18nOptions, css, distribution, framework]); return coreClient; }; diff --git a/packages/react/src/lib/utils/css-detection.ts b/packages/react/src/lib/utils/css-detection.ts new file mode 100644 index 000000000..1a220dc54 --- /dev/null +++ b/packages/react/src/lib/utils/css-detection.ts @@ -0,0 +1,42 @@ +/** + * CSS implementation detection for telemetry. + * @module css-detection + * @internal + */ + +import type { CssImplementation } from '@auth0/universal-components-core'; + +/** + * Detects the CSS implementation being used by the application. + * + * Scoped CSS: imports `@auth0/universal-components-react/styles` which has + * all styles prefixed with `.auth0-universal` selector (e.g., `.auth0-universal .sr-only`). + * + * Tailwind CSS: imports `@auth0/universal-components-react/tailwind` or uses + * Tailwind directly where utilities like `.sr-only` are global. + * + * Detection: Check if `.sr-only` styles only apply inside `.auth0-universal` wrapper. + * - Scoped: `.sr-only` without wrapper has no effect, with wrapper it works + * - Tailwind: `.sr-only` works globally without needing wrapper + * + * @param element - DOM element to use for detection (typically the provider's container) + * @returns The detected CSS implementation ('scoped' or 'tailwind') + * @internal + */ +export function detectCssImplementation(element: HTMLElement): CssImplementation { + // Create probe without .auth0-universal wrapper + const probeWithoutWrapper = document.createElement('div'); + probeWithoutWrapper.className = 'sr-only'; + probeWithoutWrapper.style.visibility = 'hidden'; + probeWithoutWrapper.setAttribute('aria-hidden', 'true'); + element.appendChild(probeWithoutWrapper); + + const computedWithout = getComputedStyle(probeWithoutWrapper); + // In Tailwind mode, .sr-only works globally (position: absolute) + // In Scoped mode, .sr-only only works inside .auth0-universal wrapper + const srOnlyWorksGlobally = computedWithout.position === 'absolute'; + + element.removeChild(probeWithoutWrapper); + + return srOnlyWorksGlobally ? 'tailwind' : 'scoped'; +} diff --git a/packages/react/src/providers/__tests__/proxy-provider.test.tsx b/packages/react/src/providers/__tests__/proxy-provider.test.tsx index 2fdd3db47..476cda2e4 100644 --- a/packages/react/src/providers/__tests__/proxy-provider.test.tsx +++ b/packages/react/src/providers/__tests__/proxy-provider.test.tsx @@ -135,7 +135,7 @@ describe('Auth0ComponentProvider', () => { }); it('should render fallback when coreClient is not initialized', () => { - mockUseCoreClientInitialization.mockReturnValueOnce(null as never); + mockUseCoreClientInitialization.mockReturnValue(null as never); render( { }); it('should render fallback when coreClient is not initialized', () => { - mockUseCoreClientInitialization.mockReturnValueOnce(null as never); + mockUseCoreClientInitialization.mockReturnValue(null as never); render( diff --git a/packages/react/src/providers/proxy-provider.tsx b/packages/react/src/providers/proxy-provider.tsx index 1d576567b..a0bfb9049 100644 --- a/packages/react/src/providers/proxy-provider.tsx +++ b/packages/react/src/providers/proxy-provider.tsx @@ -5,7 +5,12 @@ 'use client'; -import type { AuthDetails } from '@auth0/universal-components-core'; +import type { + AuthDetails, + CssImplementation, + TelemetryConfig, +} from '@auth0/universal-components-core'; +import type { DistributionChannel } from '@auth0/universal-components-core'; import * as React from 'react'; import { Toaster } from '@/components/auth0/shared/sonner'; @@ -14,10 +19,21 @@ import { Spinner } from '@/components/ui/spinner'; import { CoreClientContext } from '@/hooks/shared/use-core-client'; import { useCoreClientInitialization } from '@/hooks/shared/use-core-client-initialization'; import { useToastProvider } from '@/hooks/shared/use-toast-provider'; +import { detectCssImplementation } from '@/lib/utils/css-detection'; import { QueryProvider } from '@/providers/query-provider'; import { ThemeProvider } from '@/providers/theme-provider'; import type { Auth0ComponentProviderProps } from '@/types/auth-types'; +/** + * Build-time constant for distribution channel. + * - npm build: tsup replaces __DISTRIBUTION__ with 'npm' + * - shadcn copy: __DISTRIBUTION__ undefined, falls back to 'shadcn' + */ +declare const __DISTRIBUTION__: DistributionChannel; +const DISTRIBUTION: DistributionChannel = + typeof __DISTRIBUTION__ !== 'undefined' ? __DISTRIBUTION__ : 'shadcn'; +const FRAMEWORK = 'react' as const; + /** * Auth0 provider for RWAs using backend proxy auth. * @param props - Provider configuration including domain, proxyConfig, i18n, themeSettings, toastSettings, cacheConfig, loader, and children. @@ -45,6 +61,16 @@ export const Auth0ComponentProvider = ({ const mergedToastSettings = useToastProvider(toastSettings); const { baseUrl, fetcher } = proxyConfig; + // CSS detection for telemetry + const containerRef = React.useRef(null); + const [css, setCss] = React.useState('unknown'); + + React.useLayoutEffect(() => { + if (containerRef.current) { + setCss(detectCssImplementation(containerRef.current)); + } + }, []); + const memoizedAuthDetails = React.useMemo( () => ({ domain, @@ -55,9 +81,19 @@ export const Auth0ComponentProvider = ({ [domain, baseUrl, fetcher, previewMode], ); + const telemetry = React.useMemo( + () => ({ + css, + distribution: DISTRIBUTION, + framework: FRAMEWORK, + }), + [css], + ); + const coreClient = useCoreClientInitialization({ authDetails: memoizedAuthDetails, i18nOptions: i18n, + telemetry, }); const coreClientValue = React.useMemo( @@ -76,29 +112,31 @@ export const Auth0ComponentProvider = ({ ); return ( - - {mergedToastSettings.provider === 'sonner' && ( - - )} - {coreClient ? ( - - {children} - - ) : ( - fallback - )} - +
+ + {mergedToastSettings.provider === 'sonner' && ( + + )} + {coreClient ? ( + + {children} + + ) : ( + fallback + )} + +
); }; diff --git a/packages/react/src/providers/spa-provider.tsx b/packages/react/src/providers/spa-provider.tsx index 4ecc5b0eb..8e209bac4 100644 --- a/packages/react/src/providers/spa-provider.tsx +++ b/packages/react/src/providers/spa-provider.tsx @@ -6,7 +6,13 @@ 'use client'; import { useAuth0 } from '@auth0/auth0-react'; -import type { AuthDetails, BasicAuth0ContextInterface } from '@auth0/universal-components-core'; +import type { + AuthDetails, + BasicAuth0ContextInterface, + CssImplementation, + TelemetryConfig, +} from '@auth0/universal-components-core'; +import type { DistributionChannel } from '@auth0/universal-components-core'; import * as React from 'react'; import { Toaster } from '@/components/auth0/shared/sonner'; @@ -15,10 +21,21 @@ import { Spinner } from '@/components/ui/spinner'; import { CoreClientContext } from '@/hooks/shared/use-core-client'; import { useCoreClientInitialization } from '@/hooks/shared/use-core-client-initialization'; import { useToastProvider } from '@/hooks/shared/use-toast-provider'; +import { detectCssImplementation } from '@/lib/utils/css-detection'; import { QueryProvider } from '@/providers/query-provider'; import { ThemeProvider } from '@/providers/theme-provider'; import type { Auth0ComponentProviderProps } from '@/types/auth-types'; +/** + * Build-time constant for distribution channel. + * - npm build: tsup replaces __DISTRIBUTION__ with 'npm' + * - shadcn copy: __DISTRIBUTION__ undefined, falls back to 'shadcn' + */ +declare const __DISTRIBUTION__: DistributionChannel; +const DISTRIBUTION: DistributionChannel = + typeof __DISTRIBUTION__ !== 'undefined' ? __DISTRIBUTION__ : 'shadcn'; +const FRAMEWORK = 'react' as const; + /** * Auth0 provider for SPAs. Wraps components with required contexts. * @param props - Provider configuration including domain, mode, authContext, i18n, themeSettings, toastSettings, cacheConfig, loader, and children. @@ -47,6 +64,16 @@ export const Auth0ComponentProvider = ( } = props; const mergedToastSettings = useToastProvider(toastSettings); + // CSS detection for telemetry + const containerRef = React.useRef(null); + const [css, setCss] = React.useState('unknown'); + + React.useLayoutEffect(() => { + if (containerRef.current) { + setCss(detectCssImplementation(containerRef.current)); + } + }, []); + const auth0ReactContext = useAuth0(); const resolvedAuthContext = React.useMemo(() => { @@ -70,9 +97,19 @@ export const Auth0ComponentProvider = ( [resolvedAuthContext, previewMode], ); + const telemetry = React.useMemo( + () => ({ + css, + distribution: DISTRIBUTION, + framework: FRAMEWORK, + }), + [css], + ); + const coreClient = useCoreClientInitialization({ authDetails: memoizedAuthDetails, i18nOptions: i18n, + telemetry, }); const coreClientValue = React.useMemo( @@ -91,29 +128,31 @@ export const Auth0ComponentProvider = ( ); return ( - - {mergedToastSettings.provider === 'sonner' && ( - - )} - {coreClient ? ( - - {children} - - ) : ( - fallback - )} - +
+ + {mergedToastSettings.provider === 'sonner' && ( + + )} + {coreClient ? ( + + {children} + + ) : ( + fallback + )} + +
); }; diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts index 1dfd8f643..006196d63 100644 --- a/packages/react/tsup.config.ts +++ b/packages/react/tsup.config.ts @@ -15,4 +15,7 @@ export default defineConfig({ banner: { js: '"use client";', }, + define: { + __DISTRIBUTION__: JSON.stringify('npm'), + }, }); From 5b80981f9706342f1552bab95eb1805136f19381 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Thu, 28 May 2026 13:58:04 +0530 Subject: [PATCH 03/10] chore: updated nextjs-auth0 to 4.21.0 --- examples/next-rwa/package.json | 2 +- pnpm-lock.yaml | 39 +++++++--------------------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/examples/next-rwa/package.json b/examples/next-rwa/package.json index 01d77337f..8d6d70293 100644 --- a/examples/next-rwa/package.json +++ b/examples/next-rwa/package.json @@ -10,7 +10,7 @@ "lint": "next lint" }, "dependencies": { - "@auth0/nextjs-auth0": "^4.20.0", + "@auth0/nextjs-auth0": "^4.21.0", "@auth0/universal-components-react": "workspace:*", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/postcss": "^4.1.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1208c1f1..2b8f1ebf1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,8 +188,8 @@ importers: examples/next-rwa: dependencies: '@auth0/nextjs-auth0': - specifier: ^4.20.0 - version: 4.20.0(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: ^4.21.0 + version: 4.21.0(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@auth0/universal-components-react': specifier: workspace:* version: link:../../packages/react @@ -737,8 +737,8 @@ packages: resolution: {integrity: sha512-mYGa95tFj3xgUKKVSi4B95Yt4FPppFfbtmWM9fvXUEgwSgmLHre6vHLwcnsXTPB/rF7ATpAtMMIsWq1N5h9Y4w==} engines: {node: '>=20.0.0'} - '@auth0/nextjs-auth0@4.20.0': - resolution: {integrity: sha512-F6kpRKcLUqtxE622+oazsXTbay+19G+jhIN8IamiGHGLjNdFgaDwgHCaWnZNuHQrhvlqoGLpJ+5bjfGh4v38cw==} + '@auth0/nextjs-auth0@4.21.0': + resolution: {integrity: sha512-J8EM8nixcCWx7hlGVlV4HzsYI67wrDwWhaW1HYr+7AY2e+XMBJCcKmj/JAh8l0akUf/XwDyCPnqBQIMRZjvVyw==} peerDependencies: next: ^14.2.35 || ~15.0.7 || ~15.1.11 || ~15.2.8 || ~15.3.8 || ~15.4.10 || ~15.5.9 || ^16.0.10 react: 19.2.1 @@ -5336,12 +5336,6 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - jose@6.1.2: - resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==} - - jose@6.1.3: - resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} - jose@6.2.1: resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} @@ -5855,9 +5849,6 @@ packages: nwsapi@2.2.22: resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} - oauth4webapi@3.8.3: - resolution: {integrity: sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw==} - oauth4webapi@3.8.5: resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} @@ -5919,9 +5910,6 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} - openid-client@6.8.1: - resolution: {integrity: sha512-VoYT6enBo6Vj2j3Q5Ec0AezS+9YGzQo1f5Xc42lreMGlfP4ljiXPKVDvCADh+XHCV/bqPu/wWSiCVXbJKvrODw==} - openid-client@6.8.2: resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==} @@ -7543,14 +7531,14 @@ snapshots: dependencies: '@auth0/auth0-auth-js': 1.5.0 - '@auth0/nextjs-auth0@4.20.0(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@auth0/nextjs-auth0@4.21.0(next@16.1.5(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@edge-runtime/cookies': 5.0.2 '@panva/hkdf': 1.2.1 - jose: 6.1.3 + jose: 6.2.1 next: 16.1.5(@babel/core@7.28.5)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - oauth4webapi: 3.8.3 - openid-client: 6.8.1 + oauth4webapi: 3.8.5 + openid-client: 6.8.2 react: 19.2.1 react-dom: 19.2.1(react@19.2.1) swr: 2.3.7(react@19.2.1) @@ -12296,10 +12284,6 @@ snapshots: jiti@2.6.1: {} - jose@6.1.2: {} - - jose@6.1.3: {} - jose@6.2.1: {} joycon@3.1.1: {} @@ -12789,8 +12773,6 @@ snapshots: nwsapi@2.2.22: {} - oauth4webapi@3.8.3: {} - oauth4webapi@3.8.5: {} object-assign@4.1.1: {} @@ -12859,11 +12841,6 @@ snapshots: dependencies: mimic-function: 5.0.1 - openid-client@6.8.1: - dependencies: - jose: 6.1.2 - oauth4webapi: 3.8.3 - openid-client@6.8.2: dependencies: jose: 6.2.1 From cb973d6df8ca98d7aded9af98b91c286bdc6c07c Mon Sep 17 00:00:00 2001 From: rax7389 Date: Thu, 28 May 2026 20:11:48 +0530 Subject: [PATCH 04/10] test: fixed test case failure --- packages/core/src/api/__tests__/api-utils.test.ts | 1 + .../src/services/my-account/__tests__/my-account-client.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/core/src/api/__tests__/api-utils.test.ts b/packages/core/src/api/__tests__/api-utils.test.ts index f752e6a6b..b70c2855e 100644 --- a/packages/core/src/api/__tests__/api-utils.test.ts +++ b/packages/core/src/api/__tests__/api-utils.test.ts @@ -155,6 +155,7 @@ describe('api-utils', () => { expect(customFetcher).toHaveBeenCalledWith( 'https://example.com/api', expect.objectContaining({ method: 'GET' }), + undefined, ); }); }); diff --git a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts index 59dbde85f..61909583a 100644 --- a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts +++ b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts @@ -109,6 +109,7 @@ describe('createMyAccountClient', () => { expect(mockFetch).toHaveBeenCalledWith( 'https://example.com', expect.objectContaining({ method: 'GET' }), + { scope: ['read:users', 'write:users'], audience: 'test-audience' }, ); const [, requestInit] = mockFetch.mock.calls[0]!; From ffc715f0d9d39d6618c2702f22a0d43b962ed8c0 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Fri, 29 May 2026 00:00:58 +0530 Subject: [PATCH 05/10] feat: refactored telemetry code --- .../core/src/api/__tests__/api-utils.test.ts | 117 ++++++++--- .../core/src/api/__tests__/telemetry.test.ts | 64 ++---- packages/core/src/api/api-utils.ts | 27 ++- packages/core/src/api/telemetry.ts | 51 +---- .../src/auth/__tests__/core-client.test.ts | 195 ++++++++++++++---- packages/core/src/auth/core-client.ts | 8 +- packages/core/src/index.ts | 1 + .../__tests__/my-account-client.test.ts | 76 ++++--- .../services/my-account/my-account-client.ts | 12 +- .../__tests__/my-organization-client.test.ts | 60 ++++-- .../my-organization/my-organization-client.ts | 12 +- .../auth0/my-account/user-mfa-management.tsx | 3 + .../auth0/my-organization/domain-table.tsx | 3 + .../organization-details-edit.tsx | 3 + .../my-organization/sso-provider-create.tsx | 3 + .../my-organization/sso-provider-edit.tsx | 3 + .../my-organization/sso-provider-table.tsx | 3 + .../use-core-client-initialization.test.tsx | 9 + .../shared/use-core-client-initialization.ts | 16 +- .../react/src/hooks/shared/use-telemetry.ts | 28 +++ .../react/src/providers/proxy-provider.tsx | 57 ++--- packages/react/src/providers/spa-provider.tsx | 57 ++--- .../src/providers/telemetry-provider.tsx | 45 ++++ .../react/src/tests/utils/test-provider.tsx | 12 +- 24 files changed, 590 insertions(+), 275 deletions(-) create mode 100644 packages/react/src/hooks/shared/use-telemetry.ts create mode 100644 packages/react/src/providers/telemetry-provider.tsx diff --git a/packages/core/src/api/__tests__/api-utils.test.ts b/packages/core/src/api/__tests__/api-utils.test.ts index b70c2855e..afd5c3dbf 100644 --- a/packages/core/src/api/__tests__/api-utils.test.ts +++ b/packages/core/src/api/__tests__/api-utils.test.ts @@ -22,6 +22,8 @@ const defaultTelemetry: TelemetryConfig = { framework: 'react', }; +const mockGetComponent = () => 'test-component'; + describe('api-utils', () => { describe('createProxyFetcher', () => { afterEach(() => { @@ -30,7 +32,10 @@ describe('api-utils', () => { it('sets content-type header to application/json', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); await fetcher('https://example.com/api', { method: 'POST' }, undefined); @@ -40,7 +45,10 @@ describe('api-utils', () => { it('sets auth0-scope header when scope array is provided', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); await fetcher( 'https://example.com/api', @@ -57,7 +65,10 @@ describe('api-utils', () => { it('does not set auth0-scope header when scope array is empty', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] }); @@ -67,7 +78,10 @@ describe('api-utils', () => { it('does not set auth0-scope header when authParams is undefined', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); await fetcher('https://example.com/api', { method: 'GET' }, undefined); @@ -77,7 +91,10 @@ describe('api-utils', () => { it('preserves existing headers from init', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); const customHeaders = new Headers({ 'X-Custom': 'value' }); await fetcher( @@ -94,7 +111,10 @@ describe('api-utils', () => { it('preserves other init options', async () => { const mockFetch = stubFetch(); - const fetcher = createProxyFetcher({ telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); const body = JSON.stringify({ data: 'test' }); await fetcher( @@ -113,6 +133,7 @@ describe('api-utils', () => { const mockFetch = stubFetch(); const fetcher = createProxyFetcher({ telemetry: { css: 'tailwind', distribution: 'npm', framework: 'react' }, + getComponent: () => 'user-mfa-management', }); await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined); @@ -130,10 +151,11 @@ describe('api-utils', () => { expect(decoded.framework).toBe('react'); }); - it('sets correct component based on URL path', async () => { + it('uses component from getComponent callback', async () => { const mockFetch = stubFetch(); const fetcher = createProxyFetcher({ telemetry: { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + getComponent: () => 'organization-sso-configuration', }); await fetcher('https://example.com/my-org/identity-providers', { method: 'GET' }, undefined); @@ -148,7 +170,11 @@ describe('api-utils', () => { it('uses custom fetcher when provided', async () => { const customFetcher = vi.fn().mockResolvedValue(new Response()); - const fetcher = createProxyFetcher({ customFetcher, telemetry: defaultTelemetry }); + const fetcher = createProxyFetcher({ + customFetcher, + telemetry: defaultTelemetry, + getComponent: mockGetComponent, + }); await fetcher('https://example.com/api', { method: 'GET' }, undefined); @@ -186,14 +212,19 @@ describe('api-utils', () => { const config = createSpaConfig(); const dpopNonceId = '__test_dpop_nonce__'; - createSpaFetcher(config, dpopNonceId, defaultTelemetry); + createSpaFetcher(config, dpopNonceId, defaultTelemetry, mockGetComponent); expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId }); }); it('sets Content-Type header to application/json', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); await fetcher('https://example.com/api', { method: 'POST' }, undefined); @@ -203,7 +234,12 @@ describe('api-utils', () => { it('preserves existing headers from init when adding Content-Type', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); const customHeaders = new Headers({ 'X-Custom': 'value' }); await fetcher( @@ -220,7 +256,12 @@ describe('api-utils', () => { it('preserves other init options when adding Content-Type header', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); const body = JSON.stringify({ data: 'test' }); await fetcher( @@ -238,7 +279,12 @@ describe('api-utils', () => { it('delegates to SDK fetchWithAuth with scope and audience', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); await fetcher( 'https://example.com/api', @@ -255,7 +301,12 @@ describe('api-utils', () => { it('handles undefined authParams', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); await fetcher('https://example.com/api', { method: 'GET' }, undefined); @@ -268,7 +319,12 @@ describe('api-utils', () => { it('handles empty scope array', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] }); @@ -281,7 +337,12 @@ describe('api-utils', () => { it('handles undefined init parameter', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', defaultTelemetry); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + defaultTelemetry, + mockGetComponent, + ); await fetcher('https://example.com/api', undefined, { scope: ['read:users'] }); @@ -297,11 +358,12 @@ describe('api-utils', () => { it('sets Auth0-Client telemetry header with SPA mode', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + { css: 'tailwind', distribution: 'npm', framework: 'react' }, + () => 'user-mfa-management', + ); await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined); @@ -318,13 +380,14 @@ describe('api-utils', () => { expect(decoded.framework).toBe('react'); }); - it('sets correct component based on URL path', async () => { + it('uses component from getComponent callback', async () => { const config = createSpaConfig(); - const fetcher = createSpaFetcher(config, '__test_nonce__', { - css: 'scoped', - distribution: 'shadcn', - framework: 'react', - }); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + () => 'organization-domain-management', + ); await fetcher('https://example.com/my-org/domains', { method: 'GET' }, undefined); diff --git a/packages/core/src/api/__tests__/telemetry.test.ts b/packages/core/src/api/__tests__/telemetry.test.ts index 714e3bc4b..9f58d8446 100644 --- a/packages/core/src/api/__tests__/telemetry.test.ts +++ b/packages/core/src/api/__tests__/telemetry.test.ts @@ -1,11 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { - buildTelemetryHeader, - getComponentFromUrl, - PACKAGE_VERSION, - TELEMETRY_NAME, -} from '../telemetry'; +import { buildTelemetryHeader, PACKAGE_VERSION, TELEMETRY_NAME } from '../telemetry'; describe('telemetry', () => { describe('constants', () => { @@ -18,50 +13,11 @@ describe('telemetry', () => { }); }); - describe('getComponentFromUrl', () => { - it('should return user-mfa-management for authentication-methods URLs', () => { - expect(getComponentFromUrl('https://example.com/me/authentication-methods')).toBe( - 'user-mfa-management', - ); - expect(getComponentFromUrl('https://example.com/me/authentication-methods/123')).toBe( - 'user-mfa-management', - ); - }); - - it('should return organization-sso-configuration for identity-providers URLs', () => { - expect(getComponentFromUrl('https://example.com/my-org/identity-providers')).toBe( - 'organization-sso-configuration', - ); - expect(getComponentFromUrl('https://example.com/my-org/identity-providers/456/domains')).toBe( - 'organization-sso-configuration', - ); - }); - - it('should return organization-domain-management for domains URLs', () => { - expect(getComponentFromUrl('https://example.com/my-org/domains')).toBe( - 'organization-domain-management', - ); - expect(getComponentFromUrl('https://example.com/my-org/domains/789/verify')).toBe( - 'organization-domain-management', - ); - }); - - it('should return organization-details for configuration URLs', () => { - expect(getComponentFromUrl('https://example.com/my-org/configuration')).toBe( - 'organization-details', - ); - }); - - it('should return unknown for unrecognized URLs', () => { - expect(getComponentFromUrl('https://example.com/unknown/endpoint')).toBe('unknown'); - expect(getComponentFromUrl('https://example.com/api/users')).toBe('unknown'); - }); - }); - describe('buildTelemetryHeader', () => { it('should return base64-encoded JSON for proxy mode with full telemetry config', () => { - const header = buildTelemetryHeader('https://example.com/me/authentication-methods', { + const header = buildTelemetryHeader({ isProxyMode: true, + component: 'user-mfa-management', css: 'tailwind', distribution: 'npm', framework: 'react', @@ -80,8 +36,9 @@ describe('telemetry', () => { }); it('should return base64-encoded JSON for SPA mode with shadcn distribution', () => { - const header = buildTelemetryHeader('https://example.com/my-org/identity-providers', { + const header = buildTelemetryHeader({ isProxyMode: false, + component: 'organization-sso-configuration', css: 'scoped', distribution: 'shadcn', framework: 'react', @@ -99,9 +56,10 @@ describe('telemetry', () => { }); }); - it('should set component to unknown for unrecognized URLs', () => { - const header = buildTelemetryHeader('https://example.com/unknown', { + it('should use provided component value', () => { + const header = buildTelemetryHeader({ isProxyMode: false, + component: 'unknown', css: 'unknown', distribution: 'npm', framework: 'react', @@ -113,8 +71,9 @@ describe('telemetry', () => { }); it('should include all required telemetry fields', () => { - const header = buildTelemetryHeader('https://example.com/my-org/domains', { + const header = buildTelemetryHeader({ isProxyMode: true, + component: 'organization-domain-management', css: 'tailwind', distribution: 'npm', framework: 'react', @@ -131,8 +90,9 @@ describe('telemetry', () => { }); it('should use provided framework value', () => { - const header = buildTelemetryHeader('https://example.com/me/authentication-methods', { + const header = buildTelemetryHeader({ isProxyMode: false, + component: 'user-mfa-management', css: 'tailwind', distribution: 'npm', framework: 'vue', diff --git a/packages/core/src/api/api-utils.ts b/packages/core/src/api/api-utils.ts index 7794ff97c..ab06010ff 100644 --- a/packages/core/src/api/api-utils.ts +++ b/packages/core/src/api/api-utils.ts @@ -7,7 +7,11 @@ import type { FetcherAuthParams, FetcherSupplier, SpaAuthConfig } from '../auth/auth-types'; import { ContentType, HeaderName } from './http-constants'; -import { buildTelemetryHeader, type TelemetryConfig } from './telemetry'; +import { + buildTelemetryHeader, + type TelemetryComponentGetter, + type TelemetryConfig, +} from './telemetry'; export const AUTH0_SCOPE_HEADER = HeaderName.Auth0Scope; export const AUTH0_CLIENT_HEADER = 'Auth0-Client'; @@ -22,13 +26,14 @@ export interface ProxyFetcherConfig { authParams?: FetcherAuthParams, ) => Promise; telemetry: TelemetryConfig; + getComponent: TelemetryComponentGetter; } /** * Creates a fetcher function for proxy mode that injects scopes via auth0-scope header. * The proxy will extract scopes from the header and request the appropriate token. - * Also adds telemetry header with component info derived from the URL. - * @param config - Fetcher configuration with optional custom fetcher and telemetry config + * Also adds telemetry header with component info from getComponent callback. + * @param config - Fetcher configuration with optional custom fetcher, telemetry config, and getComponent callback * @returns Fetcher function that sets auth0-scope, content-type, and telemetry headers * @internal */ @@ -42,7 +47,11 @@ export function createProxyFetcher(config: ProxyFetcherConfig): FetcherSupplier } headers.set( AUTH0_CLIENT_HEADER, - buildTelemetryHeader(url, { isProxyMode: true, ...config.telemetry }), + buildTelemetryHeader({ + isProxyMode: true, + component: config.getComponent(), + ...config.telemetry, + }), ); if (fetchFn) { return fetchFn(url, { ...init, headers }, authParams); @@ -53,10 +62,11 @@ export function createProxyFetcher(config: ProxyFetcherConfig): FetcherSupplier /** * Creates a fetcher function for SPA mode using Auth0 SDK's createFetcher. - * Also adds telemetry header with component info derived from the URL. + * Also adds telemetry header with component info from getComponent callback. * @param config - SPA auth configuration with context interface * @param dpopNonceId - Unique identifier for DPoP nonce management * @param telemetry - Telemetry configuration + * @param getComponent - Callback to get current component name * @returns Fetcher function that delegates to SDK's fetchWithAuth with JSON content-type and telemetry * @internal */ @@ -64,6 +74,7 @@ export function createSpaFetcher( config: SpaAuthConfig, dpopNonceId: string, telemetry: TelemetryConfig, + getComponent: TelemetryComponentGetter, ): FetcherSupplier { const sdkFetcher = config.contextInterface.createFetcher({ dpopNonceId }); return (url, init, authParams) => { @@ -71,7 +82,11 @@ export function createSpaFetcher( headers.set(HeaderName.ContentType, ContentType.JSON); headers.set( AUTH0_CLIENT_HEADER, - buildTelemetryHeader(url, { isProxyMode: false, ...telemetry }), + buildTelemetryHeader({ + isProxyMode: false, + component: getComponent(), + ...telemetry, + }), ); return sdkFetcher.fetchWithAuth( url, diff --git a/packages/core/src/api/telemetry.ts b/packages/core/src/api/telemetry.ts index 766fa733d..459dceade 100644 --- a/packages/core/src/api/telemetry.ts +++ b/packages/core/src/api/telemetry.ts @@ -31,6 +31,12 @@ export type DistributionChannel = 'npm' | 'shadcn'; */ export type Framework = 'react' | 'vue' | 'angular'; +/** + * Callback to get current component name from React context. + * Called by fetchers on each request to get the component that initiated the call. + */ +export type TelemetryComponentGetter = () => string; + /** * Telemetry configuration passed from framework packages. * Groups all telemetry-related settings in one object. @@ -41,43 +47,6 @@ export interface TelemetryConfig { framework: Framework; } -/** - * Map of Auth0 API URL paths to component telemetry names. - * Only includes currently exposed block components. - * @internal - */ -const URL_TO_COMPONENT_MAP: Record = { - // MyAccount API (me/) - UserMFAMgmt component - '/authentication-methods': 'user-mfa-management', - - // MyOrganization API (my-org/) - SSO components - '/identity-providers': 'organization-sso-configuration', - - // MyOrganization API (my-org/) - DomainTable component - '/domains': 'organization-domain-management', - - // MyOrganization API (my-org/) - OrganizationDetailsEdit component - '/details': 'organization-details', - - // MyOrganization API (my-org/) - Organization configuration - '/configuration': 'organization-details', -}; - -/** - * Extracts the component name from an API URL. - * @param url - The API URL being called - * @returns The component telemetry name or 'unknown' - * @internal - */ -export function getComponentFromUrl(url: string): string { - for (const [path, component] of Object.entries(URL_TO_COMPONENT_MAP)) { - if (url.includes(path)) { - return component; - } - } - return 'unknown'; -} - /** * Telemetry payload structure sent in the Auth0-Client header. */ @@ -97,22 +66,22 @@ export interface TelemetryPayload { */ export interface TelemetryOptions extends TelemetryConfig { isProxyMode: boolean; + component: string; } /** * Builds the base64-encoded telemetry header value. - * @param url - The API URL being called (used to determine component) - * @param options - Telemetry configuration options + * @param options - Telemetry configuration options including component name * @returns Base64-encoded JSON telemetry payload * @internal */ -export function buildTelemetryHeader(url: string, options: TelemetryOptions): string { +export function buildTelemetryHeader(options: TelemetryOptions): string { const payload: TelemetryPayload = { name: TELEMETRY_NAME, version: PACKAGE_VERSION, is_proxy_mode: options.isProxyMode, framework: options.framework, - component: getComponentFromUrl(url), + component: options.component, distribution: options.distribution, css: options.css, }; diff --git a/packages/core/src/auth/__tests__/core-client.test.ts b/packages/core/src/auth/__tests__/core-client.test.ts index 5b0106562..77373be75 100644 --- a/packages/core/src/auth/__tests__/core-client.test.ts +++ b/packages/core/src/auth/__tests__/core-client.test.ts @@ -69,10 +69,12 @@ describe('createCoreClient', () => { framework: 'react' as const, }; + const mockGetComponent = () => 'test-component'; + describe('i18n initialization', () => { it('initializes i18n with default options when none are provided', async () => { const authDetails = createAuthDetails(); - await createCoreClient(authDetails, undefined, defaultTelemetry); + await createCoreClient(authDetails, undefined, defaultTelemetry, mockGetComponent); expect(createI18nServiceMock).toHaveBeenCalledWith({ currentLanguage: 'en-US', @@ -83,14 +85,19 @@ describe('createCoreClient', () => { it('initializes i18n with provided language options', async () => { const i18nOptions = { currentLanguage: 'es', fallbackLanguage: 'en' }; const authDetails = createAuthDetails(); - await createCoreClient(authDetails, i18nOptions, defaultTelemetry); + await createCoreClient(authDetails, i18nOptions, defaultTelemetry, mockGetComponent); expect(createI18nServiceMock).toHaveBeenCalledWith(i18nOptions); }); it('exposes i18nService on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.i18nService).toBe(mockI18nService); }); @@ -99,52 +106,71 @@ describe('createCoreClient', () => { describe('isProxyMode', () => { it('returns false when authProxyUrl is undefined', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.isProxyMode()).toBe(false); }); it('returns true when authProxyUrl is set', async () => { const authDetails = createAuthDetails({ authProxyUrl: 'https://proxy.auth0.com' }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.isProxyMode()).toBe(true); }); it('returns false when authProxyUrl is empty string', async () => { const authDetails = createAuthDetails({ authProxyUrl: '' }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.isProxyMode()).toBe(false); }); }); describe('API client initialization', () => { - it('initializes MyOrg client with auth details and telemetry config', async () => { + it('initializes MyOrg client with auth details, telemetry config, and getComponent', async () => { const authDetails = createAuthDetails(); - await createCoreClient(authDetails, undefined, { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + await createCoreClient( + authDetails, + undefined, + { css: 'tailwind', distribution: 'npm', framework: 'react' }, + mockGetComponent, + ); expect(createMyOrganizationClientMock).toHaveBeenCalledWith( expect.objectContaining({ mode: 'spa', domain: TEST_DOMAIN }), { css: 'tailwind', distribution: 'npm', framework: 'react' }, + mockGetComponent, ); }); - it('initializes MyAccount client with auth details and telemetry config', async () => { + it('initializes MyAccount client with auth details, telemetry config, and getComponent', async () => { const authDetails = createAuthDetails(); - await createCoreClient(authDetails, undefined, { - css: 'scoped', - distribution: 'shadcn', - framework: 'react', - }); + await createCoreClient( + authDetails, + undefined, + { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + mockGetComponent, + ); expect(createMyAccountClientMock).toHaveBeenCalledWith( expect.objectContaining({ mode: 'spa', domain: TEST_DOMAIN }), { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + mockGetComponent, ); }); }); @@ -152,28 +178,48 @@ describe('createCoreClient', () => { describe('API client access', () => { it('exposes myAccountApiClient directly on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.myAccountApiClient).toBe(mockMyAccountClient); }); it('exposes myOrganizationApiClient directly on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.myOrganizationApiClient).toBe(mockMyOrganizationClient); }); it('returns myAccountApiClient when available via getter', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getMyAccountApiClient()).toBe(mockMyAccountClient); }); it('returns myOrganizationApiClient when available via getter', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getMyOrganizationApiClient()).toBe(mockMyOrganizationClient); }); @@ -182,7 +228,12 @@ describe('createCoreClient', () => { createMyAccountClientMock.mockReturnValueOnce(null as unknown as MyAccountClient); const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(() => client.getMyAccountApiClient()).toThrow( 'myAccountApiClient is not enabled. Please use it within Auth0ComponentProvider.', @@ -192,7 +243,12 @@ describe('createCoreClient', () => { it('throws when myOrganizationApiClient is not available', async () => { createMyOrganizationClientMock.mockReturnValueOnce(null as unknown as MyOrganizationClient); const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(() => client.getMyOrganizationApiClient()).toThrow( 'myOrganizationApiClient is not enabled. Please ensure you are in an Auth0 Organization context.', @@ -201,7 +257,12 @@ describe('createCoreClient', () => { it('returns mfaApiClient via getter', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getMFAStepUpApiClient()).toBe(mockMfaApiClient); }); @@ -210,14 +271,24 @@ describe('createCoreClient', () => { describe('client properties', () => { it('exposes auth details on the client', async () => { const authDetails = createAuthDetails(); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.auth).toEqual(authDetails); }); it('preserves authProxyUrl in auth details', async () => { const authDetails = createAuthDetails({ authProxyUrl: 'https://custom-proxy.com' }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.auth.authProxyUrl).toBe('https://custom-proxy.com'); }); @@ -225,7 +296,12 @@ describe('createCoreClient', () => { it('preserves contextInterface in auth details', async () => { const customContext = createMockContextInterface(); const authDetails = createAuthDetails({ contextInterface: customContext }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.auth.contextInterface).toBe(customContext); }); @@ -234,7 +310,12 @@ describe('createCoreClient', () => { describe('getDomain', () => { it('returns domain in SPA mode', async () => { const authDetails = createAuthDetails({ domain: TEST_DOMAIN }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getDomain()).toBe(TEST_DOMAIN); }); @@ -244,7 +325,12 @@ describe('createCoreClient', () => { authProxyUrl: 'https://proxy.auth0.com', domain: TEST_DOMAIN, }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getDomain()).toBe(TEST_DOMAIN); }); @@ -254,7 +340,12 @@ describe('createCoreClient', () => { authProxyUrl: 'https://proxy.auth0.com', domain: undefined, }); - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getDomain()).toBeUndefined(); }); @@ -263,7 +354,12 @@ describe('createCoreClient', () => { describe('previewMode', () => { it('returns a core client with previewMode and disables API clients', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.auth).toEqual({}); expect(client.myAccountApiClient).toBeUndefined(); @@ -273,35 +369,60 @@ describe('createCoreClient', () => { it('isProxyMode returns false in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.isProxyMode()).toBe(false); }); it('getMyAccountApiClient throws in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(() => client.getMyAccountApiClient()).toThrow('Function not implemented.'); }); it('getMyOrganizationApiClient throws in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(() => client.getMyOrganizationApiClient()).toThrow('Function not implemented.'); }); it('getMFAStepUpApiClient throws in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(() => client.getMFAStepUpApiClient()).toThrow('Function not implemented.'); }); it('getDomain returns undefined in previewMode', async () => { const authDetails = { ...createAuthDetails(), previewMode: true }; - const client = await createCoreClient(authDetails, undefined, defaultTelemetry); + const client = await createCoreClient( + authDetails, + undefined, + defaultTelemetry, + mockGetComponent, + ); expect(client.getDomain()).toBeUndefined(); }); diff --git a/packages/core/src/auth/core-client.ts b/packages/core/src/auth/core-client.ts index 939d8d4f1..cedd07281 100644 --- a/packages/core/src/auth/core-client.ts +++ b/packages/core/src/auth/core-client.ts @@ -6,7 +6,7 @@ import { initializeMfaStepUpClient } from '@core/services/mfa-step-up/mfa-step-up-api-service'; -import type { TelemetryConfig } from '../api/telemetry'; +import type { TelemetryComponentGetter, TelemetryConfig } from '../api/telemetry'; import type { I18nInitOptions } from '../i18n'; import { createI18nService } from '../i18n'; import { createMyAccountClient } from '../services/my-account/my-account-client'; @@ -22,12 +22,14 @@ import { AuthUtils } from './auth-utils'; * @param authDetails - Authentication configuration details * @param i18nOptions - Internationalization options * @param telemetry - Telemetry configuration (css, distribution, framework) + * @param getComponent - Callback to get current component name from React context * @returns Promise resolving to the initialized CoreClient */ export async function createCoreClient( authDetails: AuthDetails, i18nOptions: I18nInitOptions | undefined, telemetry: TelemetryConfig, + getComponent: TelemetryComponentGetter, ): Promise { const i18nService = await createI18nService( i18nOptions || { currentLanguage: 'en-US', fallbackLanguage: 'en-US' }, @@ -60,8 +62,8 @@ export async function createCoreClient( const authConfig = AuthUtils.resolveAuthConfig(authDetails); - const myOrganizationApiClient = createMyOrganizationClient(authConfig, telemetry); - const myAccountApiClient = createMyAccountClient(authConfig, telemetry); + const myOrganizationApiClient = createMyOrganizationClient(authConfig, telemetry, getComponent); + const myAccountApiClient = createMyAccountClient(authConfig, telemetry, getComponent); const mfaApiClient = initializeMfaStepUpClient(authConfig); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e2a751f8a..fde15fe15 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,7 @@ export { type CssImplementation, type DistributionChannel, type Framework, + type TelemetryComponentGetter, type TelemetryConfig, } from './api/telemetry'; diff --git a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts index 61909583a..ce0918c73 100644 --- a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts +++ b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts @@ -24,6 +24,8 @@ const defaultTelemetry: TelemetryConfig = { framework: 'react', }; +const mockGetComponent = () => 'test-component'; + describe('createMyAccountClient', () => { const mockFetchWithAuth = vi.fn().mockResolvedValue(new Response()); const mockCreateFetcher = vi.fn().mockReturnValue({ @@ -47,11 +49,15 @@ describe('createMyAccountClient', () => { }); it('creates client with baseUrl in proxy mode', () => { - createMyAccountClient(mockProxyConfig, { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + createMyAccountClient( + mockProxyConfig, + { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }, + mockGetComponent, + ); expect(MyAccountClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -62,11 +68,15 @@ describe('createMyAccountClient', () => { }); it('creates client with domain in SPA mode', () => { - createMyAccountClient(createSpaConfig(), { - css: 'scoped', - distribution: 'shadcn', - framework: 'react', - }); + createMyAccountClient( + createSpaConfig(), + { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }, + mockGetComponent, + ); expect(MyAccountClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -77,7 +87,7 @@ describe('createMyAccountClient', () => { }); it('calls SDK createFetcher with correct dpopNonceId in SPA mode', () => { - createMyAccountClient(createSpaConfig(), defaultTelemetry); + createMyAccountClient(createSpaConfig(), defaultTelemetry, mockGetComponent); expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId: MY_ACCOUNT_DPOP_NONCE_ID, @@ -91,11 +101,15 @@ describe('createMyAccountClient', () => { it('sets auth0-scope header when authParams has scope array', async () => { const mockFetch = stubFetch(); - createMyAccountClient(mockProxyConfig, { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + createMyAccountClient( + mockProxyConfig, + { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }, + mockGetComponent, + ); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -120,11 +134,15 @@ describe('createMyAccountClient', () => { it('does not set auth0-scope header when authParams has empty scope array', async () => { const mockFetch = stubFetch(); - createMyAccountClient(mockProxyConfig, { - css: 'scoped', - distribution: 'shadcn', - framework: 'react', - }); + createMyAccountClient( + mockProxyConfig, + { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }, + mockGetComponent, + ); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -138,7 +156,7 @@ describe('createMyAccountClient', () => { describe('SPA mode fetcher', () => { it('calls SDK fetchWithAuth with scope and audience', async () => { - createMyAccountClient(createSpaConfig(), defaultTelemetry); + createMyAccountClient(createSpaConfig(), defaultTelemetry, mockGetComponent); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -157,11 +175,15 @@ describe('createMyAccountClient', () => { }); it('handles undefined authParams', async () => { - createMyAccountClient(createSpaConfig(), { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + createMyAccountClient( + createSpaConfig(), + { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }, + mockGetComponent, + ); const constructorOptions = vi.mocked(MyAccountClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; diff --git a/packages/core/src/services/my-account/my-account-client.ts b/packages/core/src/services/my-account/my-account-client.ts index 9c06496de..f45592f58 100644 --- a/packages/core/src/services/my-account/my-account-client.ts +++ b/packages/core/src/services/my-account/my-account-client.ts @@ -7,7 +7,7 @@ import { MyAccountClient } from '@auth0/myaccount-js'; import { createProxyFetcher, createSpaFetcher } from '../../api/api-utils'; -import type { TelemetryConfig } from '../../api/telemetry'; +import type { TelemetryComponentGetter, TelemetryConfig } from '../../api/telemetry'; import type { ClientAuthConfig } from '../../auth/auth-types'; export const MY_ACCOUNT_PROXY_PATH = 'me'; @@ -17,10 +17,15 @@ export const MY_ACCOUNT_DPOP_NONCE_ID = '__auth0_my_account_api__'; * Creates a MyAccountClient configured for the given auth mode. * @param config - Auth configuration (proxy or SPA mode) * @param telemetry - Telemetry configuration (css, distribution, framework) + * @param getComponent - Callback to get current component name * @returns Configured MyAccountClient instance * @internal */ -export function createMyAccountClient(config: ClientAuthConfig, telemetry: TelemetryConfig) { +export function createMyAccountClient( + config: ClientAuthConfig, + telemetry: TelemetryConfig, + getComponent: TelemetryComponentGetter, +) { const isProxyMode = config.mode === 'proxy'; if (isProxyMode) { @@ -31,6 +36,7 @@ export function createMyAccountClient(config: ClientAuthConfig, telemetry: Telem fetcher: createProxyFetcher({ customFetcher: config.fetcher, telemetry, + getComponent, }), }); } @@ -38,6 +44,6 @@ export function createMyAccountClient(config: ClientAuthConfig, telemetry: Telem return new MyAccountClient({ domain: config.domain, telemetry: false, // We handle telemetry in our custom fetcher - fetcher: createSpaFetcher(config, MY_ACCOUNT_DPOP_NONCE_ID, telemetry), + fetcher: createSpaFetcher(config, MY_ACCOUNT_DPOP_NONCE_ID, telemetry, getComponent), }); } diff --git a/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts b/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts index 54618c258..c708947c4 100644 --- a/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts +++ b/packages/core/src/services/my-organization/__tests__/my-organization-client.test.ts @@ -24,6 +24,8 @@ const defaultTelemetry: TelemetryConfig = { framework: 'react', }; +const mockGetComponent = () => 'test-component'; + describe('createMyOrganizationClient', () => { const mockFetchWithAuth = vi.fn().mockResolvedValue(new Response()); const mockCreateFetcher = vi.fn().mockReturnValue({ @@ -47,11 +49,15 @@ describe('createMyOrganizationClient', () => { }); it('creates client with baseUrl in proxy mode', () => { - createMyOrganizationClient(mockProxyConfig, { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + createMyOrganizationClient( + mockProxyConfig, + { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }, + mockGetComponent, + ); expect(MyOrganizationClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -62,11 +68,15 @@ describe('createMyOrganizationClient', () => { }); it('creates client with domain in SPA mode', () => { - createMyOrganizationClient(createSpaConfig(), { - css: 'scoped', - distribution: 'shadcn', - framework: 'react', - }); + createMyOrganizationClient( + createSpaConfig(), + { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }, + mockGetComponent, + ); expect(MyOrganizationClient).toHaveBeenCalledWith( expect.objectContaining({ @@ -77,7 +87,7 @@ describe('createMyOrganizationClient', () => { }); it('calls SDK createFetcher with correct dpopNonceId in SPA mode', () => { - createMyOrganizationClient(createSpaConfig(), defaultTelemetry); + createMyOrganizationClient(createSpaConfig(), defaultTelemetry, mockGetComponent); expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId: MY_ORGANIZATION_DPOP_NONCE_ID, @@ -91,11 +101,15 @@ describe('createMyOrganizationClient', () => { it('sets auth0-scope header when authParams has scope array', async () => { const mockFetch = stubFetch(); - createMyOrganizationClient(mockProxyConfig, { - css: 'tailwind', - distribution: 'npm', - framework: 'react', - }); + createMyOrganizationClient( + mockProxyConfig, + { + css: 'tailwind', + distribution: 'npm', + framework: 'react', + }, + mockGetComponent, + ); const constructorOptions = vi.mocked(MyOrganizationClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; @@ -113,11 +127,15 @@ describe('createMyOrganizationClient', () => { describe('SPA mode fetcher', () => { it('calls SDK fetchWithAuth with scope and audience', async () => { - createMyOrganizationClient(createSpaConfig(), { - css: 'scoped', - distribution: 'shadcn', - framework: 'react', - }); + createMyOrganizationClient( + createSpaConfig(), + { + css: 'scoped', + distribution: 'shadcn', + framework: 'react', + }, + mockGetComponent, + ); const constructorOptions = vi.mocked(MyOrganizationClient).mock.calls[0]![0]; const fetcher = constructorOptions.fetcher as FetcherSupplier; diff --git a/packages/core/src/services/my-organization/my-organization-client.ts b/packages/core/src/services/my-organization/my-organization-client.ts index ab7e941ae..9340ffa59 100644 --- a/packages/core/src/services/my-organization/my-organization-client.ts +++ b/packages/core/src/services/my-organization/my-organization-client.ts @@ -7,7 +7,7 @@ import { MyOrganizationClient } from '@auth0/myorganization-js'; import { createProxyFetcher, createSpaFetcher } from '../../api/api-utils'; -import type { TelemetryConfig } from '../../api/telemetry'; +import type { TelemetryComponentGetter, TelemetryConfig } from '../../api/telemetry'; import type { ClientAuthConfig } from '../../auth/auth-types'; export const MY_ORGANIZATION_PROXY_PATH = 'my-org'; @@ -17,10 +17,15 @@ export const MY_ORGANIZATION_DPOP_NONCE_ID = '__auth0_my_organization_api__'; * Creates a MyOrganizationClient configured for the given auth mode. * @param config - Auth configuration (proxy or SPA mode) * @param telemetry - Telemetry configuration (css, distribution, framework) + * @param getComponent - Callback to get current component name * @returns Configured MyOrganizationClient instance * @internal */ -export function createMyOrganizationClient(config: ClientAuthConfig, telemetry: TelemetryConfig) { +export function createMyOrganizationClient( + config: ClientAuthConfig, + telemetry: TelemetryConfig, + getComponent: TelemetryComponentGetter, +) { const isProxyMode = config.mode === 'proxy'; if (isProxyMode) { @@ -31,6 +36,7 @@ export function createMyOrganizationClient(config: ClientAuthConfig, telemetry: fetcher: createProxyFetcher({ customFetcher: config.fetcher, telemetry, + getComponent, }), }); } @@ -38,6 +44,6 @@ export function createMyOrganizationClient(config: ClientAuthConfig, telemetry: return new MyOrganizationClient({ domain: config.domain, telemetry: false, // We handle telemetry in our custom fetcher - fetcher: createSpaFetcher(config, MY_ORGANIZATION_DPOP_NONCE_ID, telemetry), + fetcher: createSpaFetcher(config, MY_ORGANIZATION_DPOP_NONCE_ID, telemetry, getComponent), }); } diff --git a/packages/react/src/components/auth0/my-account/user-mfa-management.tsx b/packages/react/src/components/auth0/my-account/user-mfa-management.tsx index 5d54041d8..e13260aa0 100644 --- a/packages/react/src/components/auth0/my-account/user-mfa-management.tsx +++ b/packages/react/src/components/auth0/my-account/user-mfa-management.tsx @@ -15,6 +15,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardDescription, CardTitle } from '@/components/ui/card'; import { List, ListItem } from '@/components/ui/list'; import { useUserMFA } from '@/hooks/my-account/use-user-mfa'; +import { useTelemetry } from '@/hooks/shared/use-telemetry'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import { cn } from '@/lib/utils'; @@ -74,6 +75,8 @@ function UserMFAMgmt({ onBeforeAction, schema, }: UserMFAMgmtProps) { + useTelemetry('user-mfa-management'); + const { factorsByType, isLoadingFactors, diff --git a/packages/react/src/components/auth0/my-organization/domain-table.tsx b/packages/react/src/components/auth0/my-organization/domain-table.tsx index 10d01c95b..8f92706cf 100644 --- a/packages/react/src/components/auth0/my-organization/domain-table.tsx +++ b/packages/react/src/components/auth0/my-organization/domain-table.tsx @@ -16,6 +16,7 @@ import { StyledScope } from '@/components/auth0/shared/styled-scope'; import { Badge } from '@/components/ui/badge'; import { useDomainTable } from '@/hooks/my-organization/use-domain-table'; import { useDomainTableLogic } from '@/hooks/my-organization/use-domain-table-logic'; +import { useTelemetry } from '@/hooks/shared/use-telemetry'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import { getStatusBadgeVariant } from '@/lib/utils/my-organization/domain-management/domain-management-utils'; @@ -31,6 +32,8 @@ import type { * @internal */ function DomainTable(props: DomainTableProps) { + useTelemetry('domain-management'); + const { schema, hideHeader = false, diff --git a/packages/react/src/components/auth0/my-organization/organization-details-edit.tsx b/packages/react/src/components/auth0/my-organization/organization-details-edit.tsx index 98ee1df85..cc1d96f3f 100644 --- a/packages/react/src/components/auth0/my-organization/organization-details-edit.tsx +++ b/packages/react/src/components/auth0/my-organization/organization-details-edit.tsx @@ -8,6 +8,7 @@ import { GateKeeper } from '@/components/auth0/shared/gate-keeper/gate-keeper'; import { Header } from '@/components/auth0/shared/header'; import { StyledScope } from '@/components/auth0/shared/styled-scope'; import { useOrganizationDetailsEdit } from '@/hooks/my-organization/use-organization-details-edit'; +import { useTelemetry } from '@/hooks/shared/use-telemetry'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import type { @@ -30,6 +31,8 @@ import type { * @internal */ function OrganizationDetailsEdit(props: OrganizationDetailsEditProps): React.JSX.Element { + useTelemetry('organization-details'); + const { schema, customMessages = {}, diff --git a/packages/react/src/components/auth0/my-organization/sso-provider-create.tsx b/packages/react/src/components/auth0/my-organization/sso-provider-create.tsx index 3ede9ed0e..054faa81e 100644 --- a/packages/react/src/components/auth0/my-organization/sso-provider-create.tsx +++ b/packages/react/src/components/auth0/my-organization/sso-provider-create.tsx @@ -13,6 +13,7 @@ import { Wizard } from '@/components/auth0/shared/wizard'; import type { StepProps } from '@/components/auth0/shared/wizard'; import { useSsoProviderCreate } from '@/hooks/my-organization/use-sso-provider-create'; import { useSsoProviderCreateLogic } from '@/hooks/my-organization/use-sso-provider-create-logic'; +import { useTelemetry } from '@/hooks/shared/use-telemetry'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import type { @@ -36,6 +37,8 @@ import type { * @returns JSX element */ function SsoProviderCreate(props: SsoProviderCreateProps) { + useTelemetry('sso-create-configuration'); + const { createAction, backButton, diff --git a/packages/react/src/components/auth0/my-organization/sso-provider-edit.tsx b/packages/react/src/components/auth0/my-organization/sso-provider-edit.tsx index fc908ab08..3682959c7 100644 --- a/packages/react/src/components/auth0/my-organization/sso-provider-edit.tsx +++ b/packages/react/src/components/auth0/my-organization/sso-provider-edit.tsx @@ -14,6 +14,7 @@ import { StyledScope } from '@/components/auth0/shared/styled-scope'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useSsoProviderEdit } from '@/hooks/my-organization/use-sso-provider-edit'; import { useSsoProviderEditLogic } from '@/hooks/my-organization/use-sso-provider-edit-logic'; +import { useTelemetry } from '@/hooks/shared/use-telemetry'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import { cn } from '@/lib/utils'; @@ -41,6 +42,8 @@ import type { * @returns JSX element */ function SsoProviderEdit(props: SsoProviderEditProps) { + useTelemetry('sso-edit-configuration'); + const { providerId, backButton, diff --git a/packages/react/src/components/auth0/my-organization/sso-provider-table.tsx b/packages/react/src/components/auth0/my-organization/sso-provider-table.tsx index 9bfd07a6a..d7c4255b2 100644 --- a/packages/react/src/components/auth0/my-organization/sso-provider-table.tsx +++ b/packages/react/src/components/auth0/my-organization/sso-provider-table.tsx @@ -16,6 +16,7 @@ import { GateKeeper } from '@/components/auth0/shared/gate-keeper/gate-keeper'; import { Header } from '@/components/auth0/shared/header'; import { StyledScope } from '@/components/auth0/shared/styled-scope'; import { useSsoProviderTable } from '@/hooks/my-organization/use-sso-provider-table'; +import { useTelemetry } from '@/hooks/shared/use-telemetry'; import { useTheme } from '@/hooks/shared/use-theme'; import { useTranslator } from '@/hooks/shared/use-translator'; import type { @@ -38,6 +39,8 @@ import type { * @internal */ function SsoProviderTable(props: SsoProviderTableProps) { + useTelemetry('sso-table-configuration'); + const { customMessages = {}, styling = { variables: { common: {}, light: {}, dark: {} }, classes: {} }, diff --git a/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx b/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx index cccf8e1c1..bec005114 100644 --- a/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx +++ b/packages/react/src/hooks/__tests__/use-core-client-initialization.test.tsx @@ -18,11 +18,14 @@ describe('useCoreClientInitialization', () => { framework: 'react', }; + const mockGetComponent = vi.fn(() => 'test-component'); + const defaultProps = { authDetails: { authProxyUrl: '/api/auth', }, telemetry: defaultTelemetry, + getComponent: mockGetComponent, }; beforeEach(() => { @@ -50,6 +53,7 @@ describe('useCoreClientInitialization', () => { defaultProps.authDetails, undefined, defaultTelemetry, + mockGetComponent, ); }); @@ -64,6 +68,7 @@ describe('useCoreClientInitialization', () => { distribution: 'shadcn' as const, framework: 'react' as const, }, + getComponent: mockGetComponent, }; const { result } = renderHook(() => useCoreClientInitialization(propsWithI18n)); @@ -76,6 +81,7 @@ describe('useCoreClientInitialization', () => { propsWithI18n.authDetails, propsWithI18n.i18nOptions, propsWithI18n.telemetry, + mockGetComponent, ); }); @@ -108,6 +114,7 @@ describe('useCoreClientInitialization', () => { rerender({ authDetails: { authProxyUrl: '/api/auth-v2' }, telemetry: defaultTelemetry, + getComponent: mockGetComponent, }); await waitFor(() => { @@ -121,6 +128,7 @@ describe('useCoreClientInitialization', () => { const propsWithDomain = { authDetails: { authProxyUrl: '/api/auth', domain: 'test.auth0.com' }, telemetry: defaultTelemetry, + getComponent: mockGetComponent, }; const { result, rerender } = renderHook((props) => useCoreClientInitialization(props), { @@ -134,6 +142,7 @@ describe('useCoreClientInitialization', () => { rerender({ authDetails: { authProxyUrl: '/api/auth', domain: 'new.auth0.com' }, telemetry: defaultTelemetry, + getComponent: mockGetComponent, }); expect(createCoreClient).toHaveBeenCalledTimes(1); diff --git a/packages/react/src/hooks/shared/use-core-client-initialization.ts b/packages/react/src/hooks/shared/use-core-client-initialization.ts index 5c2db3d11..e6f3652bb 100644 --- a/packages/react/src/hooks/shared/use-core-client-initialization.ts +++ b/packages/react/src/hooks/shared/use-core-client-initialization.ts @@ -9,6 +9,7 @@ import type { AuthDetails, I18nInitOptions, TelemetryConfig, + TelemetryComponentGetter, } from '@auth0/universal-components-core'; import { createCoreClient } from '@auth0/universal-components-core'; import * as React from 'react'; @@ -17,6 +18,7 @@ interface UseCoreClientInitializationProps { authDetails: AuthDetails; i18nOptions?: I18nInitOptions; telemetry: TelemetryConfig; + getComponent: TelemetryComponentGetter; } /** @@ -28,6 +30,7 @@ export const useCoreClientInitialization = ({ authDetails, i18nOptions, telemetry, + getComponent, }: UseCoreClientInitializationProps): CoreClientInterface | null => { const { authProxyUrl } = authDetails; const [coreClient, setCoreClient] = React.useState(null); @@ -43,18 +46,19 @@ export const useCoreClientInitialization = ({ const initializeCoreClient = async () => { try { - const initializedCoreClient = await createCoreClient(authDetails, i18nOptions, { - css, - distribution, - framework, - }); + const initializedCoreClient = await createCoreClient( + authDetails, + i18nOptions, + { css, distribution, framework }, + getComponent, + ); setCoreClient(initializedCoreClient); } catch (error) { console.error(error); } }; initializeCoreClient(); - }, [authProxyUrl, i18nOptions, css, distribution, framework]); + }, [authProxyUrl, i18nOptions, css, distribution, framework, getComponent]); return coreClient; }; diff --git a/packages/react/src/hooks/shared/use-telemetry.ts b/packages/react/src/hooks/shared/use-telemetry.ts new file mode 100644 index 000000000..9ef29ea4f --- /dev/null +++ b/packages/react/src/hooks/shared/use-telemetry.ts @@ -0,0 +1,28 @@ +/** + * Hook for block components to declare their telemetry name. + * @module use-telemetry + * @internal + */ + +import * as React from 'react'; + +import { useTelemetryContext } from '@/providers/telemetry-provider'; + +/** + * Sets the telemetry component name for all API calls within this component tree. + * Call this at the top of block components. + * + * @param componentName - The telemetry name (e.g., 'organization-sso-configuration') + * @internal + */ +export function useTelemetry(componentName: string): void { + const { componentRef } = useTelemetryContext(); + + React.useEffect(() => { + const previous = componentRef.current; + componentRef.current = componentName; + return () => { + componentRef.current = previous; + }; + }, [componentRef, componentName]); +} diff --git a/packages/react/src/providers/proxy-provider.tsx b/packages/react/src/providers/proxy-provider.tsx index a0bfb9049..854eb4f90 100644 --- a/packages/react/src/providers/proxy-provider.tsx +++ b/packages/react/src/providers/proxy-provider.tsx @@ -8,6 +8,7 @@ import type { AuthDetails, CssImplementation, + TelemetryComponentGetter, TelemetryConfig, } from '@auth0/universal-components-core'; import type { DistributionChannel } from '@auth0/universal-components-core'; @@ -21,6 +22,7 @@ import { useCoreClientInitialization } from '@/hooks/shared/use-core-client-init import { useToastProvider } from '@/hooks/shared/use-toast-provider'; import { detectCssImplementation } from '@/lib/utils/css-detection'; import { QueryProvider } from '@/providers/query-provider'; +import { TelemetryProvider } from '@/providers/telemetry-provider'; import { ThemeProvider } from '@/providers/theme-provider'; import type { Auth0ComponentProviderProps } from '@/types/auth-types'; @@ -65,6 +67,12 @@ export const Auth0ComponentProvider = ({ const containerRef = React.useRef(null); const [css, setCss] = React.useState('unknown'); + // Component name ref - updated by useTelemetryTracker in block components + const componentRef = React.useRef('unknown'); + + // Stable callback for core package to call + const getComponent = React.useCallback(() => componentRef.current, []); + React.useLayoutEffect(() => { if (containerRef.current) { setCss(detectCssImplementation(containerRef.current)); @@ -94,6 +102,7 @@ export const Auth0ComponentProvider = ({ authDetails: memoizedAuthDetails, i18nOptions: i18n, telemetry, + getComponent, }); const coreClientValue = React.useMemo( @@ -113,29 +122,31 @@ export const Auth0ComponentProvider = ({ return (
- - {mergedToastSettings.provider === 'sonner' && ( - - )} - {coreClient ? ( - - {children} - - ) : ( - fallback - )} - + + + {mergedToastSettings.provider === 'sonner' && ( + + )} + {coreClient ? ( + + {children} + + ) : ( + fallback + )} + +
); }; diff --git a/packages/react/src/providers/spa-provider.tsx b/packages/react/src/providers/spa-provider.tsx index 8e209bac4..86fe9234f 100644 --- a/packages/react/src/providers/spa-provider.tsx +++ b/packages/react/src/providers/spa-provider.tsx @@ -10,6 +10,7 @@ import type { AuthDetails, BasicAuth0ContextInterface, CssImplementation, + TelemetryComponentGetter, TelemetryConfig, } from '@auth0/universal-components-core'; import type { DistributionChannel } from '@auth0/universal-components-core'; @@ -23,6 +24,7 @@ import { useCoreClientInitialization } from '@/hooks/shared/use-core-client-init import { useToastProvider } from '@/hooks/shared/use-toast-provider'; import { detectCssImplementation } from '@/lib/utils/css-detection'; import { QueryProvider } from '@/providers/query-provider'; +import { TelemetryProvider } from '@/providers/telemetry-provider'; import { ThemeProvider } from '@/providers/theme-provider'; import type { Auth0ComponentProviderProps } from '@/types/auth-types'; @@ -68,6 +70,12 @@ export const Auth0ComponentProvider = ( const containerRef = React.useRef(null); const [css, setCss] = React.useState('unknown'); + // Component name ref - updated by useTelemetryTracker in block components + const componentRef = React.useRef('unknown'); + + // Stable callback for core package to call + const getComponent = React.useCallback(() => componentRef.current, []); + React.useLayoutEffect(() => { if (containerRef.current) { setCss(detectCssImplementation(containerRef.current)); @@ -110,6 +118,7 @@ export const Auth0ComponentProvider = ( authDetails: memoizedAuthDetails, i18nOptions: i18n, telemetry, + getComponent, }); const coreClientValue = React.useMemo( @@ -129,29 +138,31 @@ export const Auth0ComponentProvider = ( return (
- - {mergedToastSettings.provider === 'sonner' && ( - - )} - {coreClient ? ( - - {children} - - ) : ( - fallback - )} - + + + {mergedToastSettings.provider === 'sonner' && ( + + )} + {coreClient ? ( + + {children} + + ) : ( + fallback + )} + +
); }; diff --git a/packages/react/src/providers/telemetry-provider.tsx b/packages/react/src/providers/telemetry-provider.tsx new file mode 100644 index 000000000..da3072768 --- /dev/null +++ b/packages/react/src/providers/telemetry-provider.tsx @@ -0,0 +1,45 @@ +/** + * Telemetry provider for tracking which block component initiated API calls. + * @module telemetry-provider + * @internal + */ + +import * as React from 'react'; + +interface TelemetryContextValue { + componentRef: { current: string }; +} + +export const TelemetryContext = React.createContext(null); + +interface TelemetryProviderProps { + children: React.ReactNode; + componentRef: { current: string }; +} + +/** + * Provider that wraps the component tree to enable telemetry tracking. + * @param props - Provider props + * @param props.children - Child components + * @param props.componentRef - Ref to track current component name + * @returns Provider component with telemetry context + * @internal + */ +export function TelemetryProvider({ children, componentRef }: TelemetryProviderProps) { + const value = React.useMemo(() => ({ componentRef }), [componentRef]); + return {children}; +} + +/** + * Hook to access the telemetry context. + * @returns The telemetry context value + * @throws Error if used outside of Auth0ComponentProvider + * @internal + */ +export function useTelemetryContext() { + const context = React.useContext(TelemetryContext); + if (!context) { + throw new Error('useTelemetry must be used within Auth0ComponentProvider'); + } + return context; +} diff --git a/packages/react/src/tests/utils/test-provider.tsx b/packages/react/src/tests/utils/test-provider.tsx index dc937f4c0..e837b673c 100644 --- a/packages/react/src/tests/utils/test-provider.tsx +++ b/packages/react/src/tests/utils/test-provider.tsx @@ -7,6 +7,7 @@ import type { FieldValues, UseFormReturn } from 'react-hook-form'; import { Form } from '@/components/ui/form'; import { CoreClientContext } from '@/hooks/shared/use-core-client'; import { GateKeeperContext } from '@/providers/gate-keeper-context'; +import { TelemetryProvider } from '@/providers/telemetry-provider'; import { createMockCoreClient } from '@/tests/utils/__mocks__/core/core-client.mocks'; // Create a new QueryClient for each test to avoid shared state @@ -68,11 +69,16 @@ export const TestProvider: React.FC = ({ [mockCoreClient], ); + // Create a ref for telemetry tracker + const componentRef = React.useRef('test-component'); + return ( - - {children} - + + + {children} + + ); }; From d42429d5c684c679f7c2ce572679226b398a3a78 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Thu, 4 Jun 2026 09:52:46 +0530 Subject: [PATCH 06/10] chore: self review and cleanup --- packages/core/src/api/api-utils.ts | 2 +- .../__tests__/my-account-client.test.ts | 6 -- .../react/src/hooks/shared/use-telemetry.ts | 2 +- .../shared/__tests__/css-detection.test.ts | 75 +++++++++++++++++++ .../lib/utils/{ => shared}/css-detection.ts | 7 +- .../react/src/providers/proxy-provider.tsx | 64 ++++++++-------- packages/react/src/providers/spa-provider.tsx | 64 ++++++++-------- 7 files changed, 142 insertions(+), 78 deletions(-) create mode 100644 packages/react/src/lib/utils/shared/__tests__/css-detection.test.ts rename packages/react/src/lib/utils/{ => shared}/css-detection.ts (90%) diff --git a/packages/core/src/api/api-utils.ts b/packages/core/src/api/api-utils.ts index ab06010ff..1e731f454 100644 --- a/packages/core/src/api/api-utils.ts +++ b/packages/core/src/api/api-utils.ts @@ -38,7 +38,7 @@ export interface ProxyFetcherConfig { * @internal */ export function createProxyFetcher(config: ProxyFetcherConfig): FetcherSupplier { - const fetchFn = config.customFetcher ?? fetch; + const fetchFn = config.customFetcher; return async (url, init, authParams) => { const headers = new Headers(init?.headers); headers.set(HeaderName.ContentType, ContentType.JSON); diff --git a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts index ce0918c73..6a330ed8b 100644 --- a/packages/core/src/services/my-account/__tests__/my-account-client.test.ts +++ b/packages/core/src/services/my-account/__tests__/my-account-client.test.ts @@ -120,12 +120,6 @@ describe('createMyAccountClient', () => { { scope: ['read:users', 'write:users'], audience: 'test-audience' }, ); - expect(mockFetch).toHaveBeenCalledWith( - 'https://example.com', - expect.objectContaining({ method: 'GET' }), - { scope: ['read:users', 'write:users'], audience: 'test-audience' }, - ); - const [, requestInit] = mockFetch.mock.calls[0]!; expect((requestInit?.headers as Headers).get(AUTH0_SCOPE_HEADER)).toBe( 'read:users write:users', diff --git a/packages/react/src/hooks/shared/use-telemetry.ts b/packages/react/src/hooks/shared/use-telemetry.ts index 9ef29ea4f..4dca0b143 100644 --- a/packages/react/src/hooks/shared/use-telemetry.ts +++ b/packages/react/src/hooks/shared/use-telemetry.ts @@ -18,7 +18,7 @@ import { useTelemetryContext } from '@/providers/telemetry-provider'; export function useTelemetry(componentName: string): void { const { componentRef } = useTelemetryContext(); - React.useEffect(() => { + React.useLayoutEffect(() => { const previous = componentRef.current; componentRef.current = componentName; return () => { diff --git a/packages/react/src/lib/utils/shared/__tests__/css-detection.test.ts b/packages/react/src/lib/utils/shared/__tests__/css-detection.test.ts new file mode 100644 index 000000000..bf63da7dc --- /dev/null +++ b/packages/react/src/lib/utils/shared/__tests__/css-detection.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +import { detectCssImplementation } from '@/lib/utils/shared/css-detection'; + +describe('detectCssImplementation', () => { + const originalDocument = global.document; + + afterEach(() => { + vi.restoreAllMocks(); + global.document = originalDocument; + }); + + describe('when document is undefined (SSR)', () => { + beforeEach(() => { + // @ts-expect-error - simulating SSR environment + global.document = undefined; + }); + + it('should return "unknown"', () => { + expect(detectCssImplementation()).toBe('unknown'); + }); + }); + + describe('when document is available (client)', () => { + let mockProbe: HTMLDivElement; + + beforeEach(() => { + mockProbe = document.createElement('div'); + vi.spyOn(document, 'createElement').mockReturnValue(mockProbe); + vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockProbe); + vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockProbe); + }); + + it('should return "tailwind" when .sr-only has position: absolute globally', () => { + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + position: 'absolute', + } as CSSStyleDeclaration); + + expect(detectCssImplementation()).toBe('tailwind'); + }); + + it('should return "scoped" when .sr-only does not have position: absolute globally', () => { + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + position: 'static', + } as CSSStyleDeclaration); + + expect(detectCssImplementation()).toBe('scoped'); + }); + + it('should create a probe element with correct attributes', () => { + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + position: 'static', + } as CSSStyleDeclaration); + + detectCssImplementation(); + + expect(mockProbe.className).toBe('sr-only'); + expect(mockProbe.style.visibility).toBe('hidden'); + expect(mockProbe.getAttribute('aria-hidden')).toBe('true'); + }); + + it('should clean up the probe element after detection', () => { + const appendSpy = vi.spyOn(document.body, 'appendChild'); + const removeSpy = vi.spyOn(document.body, 'removeChild'); + vi.spyOn(window, 'getComputedStyle').mockReturnValue({ + position: 'static', + } as CSSStyleDeclaration); + + detectCssImplementation(); + + expect(appendSpy).toHaveBeenCalledWith(mockProbe); + expect(removeSpy).toHaveBeenCalledWith(mockProbe); + }); + }); +}); diff --git a/packages/react/src/lib/utils/css-detection.ts b/packages/react/src/lib/utils/shared/css-detection.ts similarity index 90% rename from packages/react/src/lib/utils/css-detection.ts rename to packages/react/src/lib/utils/shared/css-detection.ts index 1a220dc54..7080e219e 100644 --- a/packages/react/src/lib/utils/css-detection.ts +++ b/packages/react/src/lib/utils/shared/css-detection.ts @@ -19,11 +19,14 @@ import type { CssImplementation } from '@auth0/universal-components-core'; * - Scoped: `.sr-only` without wrapper has no effect, with wrapper it works * - Tailwind: `.sr-only` works globally without needing wrapper * - * @param element - DOM element to use for detection (typically the provider's container) * @returns The detected CSS implementation ('scoped' or 'tailwind') * @internal */ -export function detectCssImplementation(element: HTMLElement): CssImplementation { +export function detectCssImplementation(): CssImplementation { + if (typeof document === 'undefined') { + return 'unknown'; + } + const element = document.body; // Create probe without .auth0-universal wrapper const probeWithoutWrapper = document.createElement('div'); probeWithoutWrapper.className = 'sr-only'; diff --git a/packages/react/src/providers/proxy-provider.tsx b/packages/react/src/providers/proxy-provider.tsx index 854eb4f90..bd72c6a0b 100644 --- a/packages/react/src/providers/proxy-provider.tsx +++ b/packages/react/src/providers/proxy-provider.tsx @@ -8,10 +8,10 @@ import type { AuthDetails, CssImplementation, + DistributionChannel, TelemetryComponentGetter, TelemetryConfig, } from '@auth0/universal-components-core'; -import type { DistributionChannel } from '@auth0/universal-components-core'; import * as React from 'react'; import { Toaster } from '@/components/auth0/shared/sonner'; @@ -20,7 +20,7 @@ import { Spinner } from '@/components/ui/spinner'; import { CoreClientContext } from '@/hooks/shared/use-core-client'; import { useCoreClientInitialization } from '@/hooks/shared/use-core-client-initialization'; import { useToastProvider } from '@/hooks/shared/use-toast-provider'; -import { detectCssImplementation } from '@/lib/utils/css-detection'; +import { detectCssImplementation } from '@/lib/utils/shared/css-detection'; import { QueryProvider } from '@/providers/query-provider'; import { TelemetryProvider } from '@/providers/telemetry-provider'; import { ThemeProvider } from '@/providers/theme-provider'; @@ -64,19 +64,17 @@ export const Auth0ComponentProvider = ({ const { baseUrl, fetcher } = proxyConfig; // CSS detection for telemetry - const containerRef = React.useRef(null); const [css, setCss] = React.useState('unknown'); - // Component name ref - updated by useTelemetryTracker in block components + // Component name ref - updated by useTelemetry in block components const componentRef = React.useRef('unknown'); // Stable callback for core package to call const getComponent = React.useCallback(() => componentRef.current, []); + // useLayoutEffect ensures CSS is detected before paint, avoiding incorrect telemetry on early API calls React.useLayoutEffect(() => { - if (containerRef.current) { - setCss(detectCssImplementation(containerRef.current)); - } + setCss(detectCssImplementation()); }, []); const memoizedAuthDetails = React.useMemo( @@ -121,33 +119,31 @@ export const Auth0ComponentProvider = ({ ); return ( -
- - - {mergedToastSettings.provider === 'sonner' && ( - - )} - {coreClient ? ( - - {children} - - ) : ( - fallback - )} - - -
+ + + {mergedToastSettings.provider === 'sonner' && ( + + )} + {coreClient ? ( + + {children} + + ) : ( + fallback + )} + + ); }; diff --git a/packages/react/src/providers/spa-provider.tsx b/packages/react/src/providers/spa-provider.tsx index 86fe9234f..52962003c 100644 --- a/packages/react/src/providers/spa-provider.tsx +++ b/packages/react/src/providers/spa-provider.tsx @@ -10,10 +10,10 @@ import type { AuthDetails, BasicAuth0ContextInterface, CssImplementation, + DistributionChannel, TelemetryComponentGetter, TelemetryConfig, } from '@auth0/universal-components-core'; -import type { DistributionChannel } from '@auth0/universal-components-core'; import * as React from 'react'; import { Toaster } from '@/components/auth0/shared/sonner'; @@ -22,7 +22,7 @@ import { Spinner } from '@/components/ui/spinner'; import { CoreClientContext } from '@/hooks/shared/use-core-client'; import { useCoreClientInitialization } from '@/hooks/shared/use-core-client-initialization'; import { useToastProvider } from '@/hooks/shared/use-toast-provider'; -import { detectCssImplementation } from '@/lib/utils/css-detection'; +import { detectCssImplementation } from '@/lib/utils/shared/css-detection'; import { QueryProvider } from '@/providers/query-provider'; import { TelemetryProvider } from '@/providers/telemetry-provider'; import { ThemeProvider } from '@/providers/theme-provider'; @@ -67,19 +67,17 @@ export const Auth0ComponentProvider = ( const mergedToastSettings = useToastProvider(toastSettings); // CSS detection for telemetry - const containerRef = React.useRef(null); const [css, setCss] = React.useState('unknown'); - // Component name ref - updated by useTelemetryTracker in block components + // Component name ref - updated by useTelemetry in block components const componentRef = React.useRef('unknown'); // Stable callback for core package to call const getComponent = React.useCallback(() => componentRef.current, []); + // useLayoutEffect ensures CSS is detected before paint, avoiding incorrect telemetry on early API calls React.useLayoutEffect(() => { - if (containerRef.current) { - setCss(detectCssImplementation(containerRef.current)); - } + setCss(detectCssImplementation()); }, []); const auth0ReactContext = useAuth0(); @@ -137,33 +135,31 @@ export const Auth0ComponentProvider = ( ); return ( -
- - - {mergedToastSettings.provider === 'sonner' && ( - - )} - {coreClient ? ( - - {children} - - ) : ( - fallback - )} - - -
+ + + {mergedToastSettings.provider === 'sonner' && ( + + )} + {coreClient ? ( + + {children} + + ) : ( + fallback + )} + + ); }; From f327b966044e52a6d81ee215347ddc92d7d9510d Mon Sep 17 00:00:00 2001 From: rax7389 Date: Thu, 4 Jun 2026 10:17:10 +0530 Subject: [PATCH 07/10] test: added unit test for telemetry --- .../hooks/__tests__/use-telemetry.test.tsx | 74 +++++++++++++++++ .../__tests__/telemetry-provider.test.tsx | 79 +++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 packages/react/src/hooks/__tests__/use-telemetry.test.tsx create mode 100644 packages/react/src/providers/__tests__/telemetry-provider.test.tsx diff --git a/packages/react/src/hooks/__tests__/use-telemetry.test.tsx b/packages/react/src/hooks/__tests__/use-telemetry.test.tsx new file mode 100644 index 000000000..2fa11df2f --- /dev/null +++ b/packages/react/src/hooks/__tests__/use-telemetry.test.tsx @@ -0,0 +1,74 @@ +import { renderHook } from '@testing-library/react'; +import React from 'react'; +import { describe, it, expect, beforeEach } from 'vitest'; + +import { useTelemetry } from '@/hooks/shared/use-telemetry'; +import { TelemetryProvider } from '@/providers/telemetry-provider'; + +describe('useTelemetry', () => { + let componentRef: { current: string }; + + beforeEach(() => { + componentRef = { current: 'unknown' }; + }); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + it('should set componentRef to the provided component name', () => { + renderHook(() => useTelemetry('test-component'), { wrapper }); + + expect(componentRef.current).toBe('test-component'); + }); + + it('should restore previous value on unmount', () => { + componentRef.current = 'previous-component'; + + const { unmount } = renderHook(() => useTelemetry('test-component'), { wrapper }); + + expect(componentRef.current).toBe('test-component'); + + unmount(); + + expect(componentRef.current).toBe('previous-component'); + }); + + it('should update componentRef when component name changes', () => { + const { rerender } = renderHook(({ name }) => useTelemetry(name), { + wrapper, + initialProps: { name: 'first-component' }, + }); + + expect(componentRef.current).toBe('first-component'); + + rerender({ name: 'second-component' }); + + expect(componentRef.current).toBe('second-component'); + }); + + it('should handle nested components correctly', () => { + const outerRef = { current: 'unknown' }; + const OuterWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + const { unmount: unmountOuter } = renderHook(() => useTelemetry('outer-component'), { + wrapper: OuterWrapper, + }); + + expect(outerRef.current).toBe('outer-component'); + + const { unmount: unmountInner } = renderHook(() => useTelemetry('inner-component'), { + wrapper: OuterWrapper, + }); + + expect(outerRef.current).toBe('inner-component'); + + unmountInner(); + expect(outerRef.current).toBe('outer-component'); + + unmountOuter(); + expect(outerRef.current).toBe('unknown'); + }); +}); diff --git a/packages/react/src/providers/__tests__/telemetry-provider.test.tsx b/packages/react/src/providers/__tests__/telemetry-provider.test.tsx new file mode 100644 index 000000000..69c578a11 --- /dev/null +++ b/packages/react/src/providers/__tests__/telemetry-provider.test.tsx @@ -0,0 +1,79 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { describe, it, expect } from 'vitest'; + +import { + TelemetryContext, + TelemetryProvider, + useTelemetryContext, +} from '@/providers/telemetry-provider'; + +describe('TelemetryProvider', () => { + it('should provide componentRef to children', () => { + const componentRef = { current: 'test-component' }; + + const TestChild = () => { + const context = React.useContext(TelemetryContext); + return
{context?.componentRef.current}
; + }; + + render( + + + , + ); + + expect(screen.getByTestId('result')).toHaveTextContent('test-component'); + }); + + it('should update when componentRef changes', () => { + const componentRef = { current: 'initial' }; + + const TestChild = () => { + const context = React.useContext(TelemetryContext); + return
{context?.componentRef.current}
; + }; + + render( + + + , + ); + + expect(screen.getByTestId('result')).toHaveTextContent('initial'); + + componentRef.current = 'updated'; + + expect(componentRef.current).toBe('updated'); + }); +}); + +describe('useTelemetryContext', () => { + it('should return context value when used within provider', () => { + const componentRef = { current: 'context-test' }; + + const TestChild = () => { + const context = useTelemetryContext(); + return
{context.componentRef.current}
; + }; + + render( + + + , + ); + + expect(screen.getByTestId('result')).toHaveTextContent('context-test'); + }); + + it('should throw error when used outside provider', () => { + const TestChild = () => { + useTelemetryContext(); + return
Should not render
; + }; + + expect(() => render()).toThrow( + 'useTelemetry must be used within Auth0ComponentProvider', + ); + }); +}); From e96eb8cf7a2502cb6340382f6b99c1adf4ad6253 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Sun, 7 Jun 2026 18:09:07 +0530 Subject: [PATCH 08/10] chore: review comments addressed --- .../core/src/api/__tests__/api-utils.test.ts | 28 +++++++++++++++ packages/core/src/api/api-utils.ts | 36 ++++++++++--------- packages/core/src/api/telemetry.ts | 1 + .../shared/use-core-client-initialization.ts | 9 ++--- .../react/src/providers/proxy-provider.tsx | 4 ++- packages/react/src/providers/spa-provider.tsx | 4 ++- packages/react/src/types/auth-types.ts | 1 + 7 files changed, 59 insertions(+), 24 deletions(-) diff --git a/packages/core/src/api/__tests__/api-utils.test.ts b/packages/core/src/api/__tests__/api-utils.test.ts index afd5c3dbf..d21981226 100644 --- a/packages/core/src/api/__tests__/api-utils.test.ts +++ b/packages/core/src/api/__tests__/api-utils.test.ts @@ -184,6 +184,19 @@ describe('api-utils', () => { undefined, ); }); + + it('does not set Auth0-Client header when telemetry is disabled', async () => { + const mockFetch = stubFetch(); + const fetcher = createProxyFetcher({ + telemetry: { ...defaultTelemetry, enabled: false }, + getComponent: mockGetComponent, + }); + + await fetcher('https://example.com/api', { method: 'GET' }, undefined); + + const [, requestInit] = mockFetch.mock.calls[0]!; + expect((requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER)).toBeNull(); + }); }); describe('createSpaFetcher', () => { @@ -398,5 +411,20 @@ describe('api-utils', () => { expect(decoded.css).toBe('scoped'); expect(decoded.distribution).toBe('shadcn'); }); + + it('does not set Auth0-Client header when telemetry is disabled', async () => { + const config = createSpaConfig(); + const fetcher = createSpaFetcher( + config, + '__test_nonce__', + { ...defaultTelemetry, enabled: false }, + mockGetComponent, + ); + + await fetcher('https://example.com/api', { method: 'GET' }, undefined); + + const [, requestInit] = mockFetchWithAuth.mock.calls[0]!; + expect((requestInit?.headers as Headers).get(AUTH0_CLIENT_HEADER)).toBeNull(); + }); }); }); diff --git a/packages/core/src/api/api-utils.ts b/packages/core/src/api/api-utils.ts index 1e731f454..545d4ca28 100644 --- a/packages/core/src/api/api-utils.ts +++ b/packages/core/src/api/api-utils.ts @@ -45,14 +45,16 @@ export function createProxyFetcher(config: ProxyFetcherConfig): FetcherSupplier if (authParams?.scope?.length) { headers.set(HeaderName.Auth0Scope, authParams.scope.join(' ')); } - headers.set( - AUTH0_CLIENT_HEADER, - buildTelemetryHeader({ - isProxyMode: true, - component: config.getComponent(), - ...config.telemetry, - }), - ); + if (config.telemetry.enabled) { + headers.set( + AUTH0_CLIENT_HEADER, + buildTelemetryHeader({ + isProxyMode: true, + component: config.getComponent(), + ...config.telemetry, + }), + ); + } if (fetchFn) { return fetchFn(url, { ...init, headers }, authParams); } @@ -80,14 +82,16 @@ export function createSpaFetcher( return (url, init, authParams) => { const headers = new Headers(init?.headers); headers.set(HeaderName.ContentType, ContentType.JSON); - headers.set( - AUTH0_CLIENT_HEADER, - buildTelemetryHeader({ - isProxyMode: false, - component: getComponent(), - ...telemetry, - }), - ); + if (telemetry.enabled) { + headers.set( + AUTH0_CLIENT_HEADER, + buildTelemetryHeader({ + isProxyMode: false, + component: getComponent(), + ...telemetry, + }), + ); + } return sdkFetcher.fetchWithAuth( url, { ...init, headers }, diff --git a/packages/core/src/api/telemetry.ts b/packages/core/src/api/telemetry.ts index 459dceade..dbb4ceeaf 100644 --- a/packages/core/src/api/telemetry.ts +++ b/packages/core/src/api/telemetry.ts @@ -45,6 +45,7 @@ export interface TelemetryConfig { css: CssImplementation; distribution: DistributionChannel; framework: Framework; + enabled?: boolean; } /** diff --git a/packages/react/src/hooks/shared/use-core-client-initialization.ts b/packages/react/src/hooks/shared/use-core-client-initialization.ts index e6f3652bb..e27f68ac9 100644 --- a/packages/react/src/hooks/shared/use-core-client-initialization.ts +++ b/packages/react/src/hooks/shared/use-core-client-initialization.ts @@ -35,12 +35,9 @@ export const useCoreClientInitialization = ({ const { authProxyUrl } = authDetails; const [coreClient, setCoreClient] = React.useState(null); - // Extract primitive values from telemetry to avoid re-runs on object reference changes - const { css, distribution, framework } = telemetry; - React.useEffect(() => { // Wait for CSS detection to complete before initializing - if (css === 'unknown') { + if (telemetry.css === 'unknown') { return; } @@ -49,7 +46,7 @@ export const useCoreClientInitialization = ({ const initializedCoreClient = await createCoreClient( authDetails, i18nOptions, - { css, distribution, framework }, + telemetry, getComponent, ); setCoreClient(initializedCoreClient); @@ -58,7 +55,7 @@ export const useCoreClientInitialization = ({ } }; initializeCoreClient(); - }, [authProxyUrl, i18nOptions, css, distribution, framework, getComponent]); + }, [authProxyUrl, i18nOptions, telemetry, getComponent]); return coreClient; }; diff --git a/packages/react/src/providers/proxy-provider.tsx b/packages/react/src/providers/proxy-provider.tsx index bd72c6a0b..060aa4750 100644 --- a/packages/react/src/providers/proxy-provider.tsx +++ b/packages/react/src/providers/proxy-provider.tsx @@ -59,6 +59,7 @@ export const Auth0ComponentProvider = ({ cacheConfig, loader, children, + telemetry: telemetryEnabled = true, }: Extract & { children: React.ReactNode }) => { const mergedToastSettings = useToastProvider(toastSettings); const { baseUrl, fetcher } = proxyConfig; @@ -92,8 +93,9 @@ export const Auth0ComponentProvider = ({ css, distribution: DISTRIBUTION, framework: FRAMEWORK, + enabled: telemetryEnabled, }), - [css], + [css, telemetryEnabled], ); const coreClient = useCoreClientInitialization({ diff --git a/packages/react/src/providers/spa-provider.tsx b/packages/react/src/providers/spa-provider.tsx index 52962003c..b6ce73f46 100644 --- a/packages/react/src/providers/spa-provider.tsx +++ b/packages/react/src/providers/spa-provider.tsx @@ -63,6 +63,7 @@ export const Auth0ComponentProvider = ( loader, children, authContext, + telemetry: telemetryEnabled = true, } = props; const mergedToastSettings = useToastProvider(toastSettings); @@ -108,8 +109,9 @@ export const Auth0ComponentProvider = ( css, distribution: DISTRIBUTION, framework: FRAMEWORK, + enabled: telemetryEnabled, }), - [css], + [css, telemetryEnabled], ); const coreClient = useCoreClientInitialization({ diff --git a/packages/react/src/types/auth-types.ts b/packages/react/src/types/auth-types.ts index a7beb2021..8de742407 100644 --- a/packages/react/src/types/auth-types.ts +++ b/packages/react/src/types/auth-types.ts @@ -38,4 +38,5 @@ export type Auth0ComponentProviderProps = ( /** TanStack Query cache config. Use `{ enabled: false }` to disable. */ cacheConfig?: QueryCacheConfig; previewMode?: boolean; + telemetry?: boolean; }; From 44857b63c63848852c7c85a035698a089c1d7d01 Mon Sep 17 00:00:00 2001 From: rax7389 Date: Sun, 7 Jun 2026 19:03:07 +0530 Subject: [PATCH 09/10] test: fixed broken test cases --- packages/core/src/api/__tests__/api-utils.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/src/api/__tests__/api-utils.test.ts b/packages/core/src/api/__tests__/api-utils.test.ts index d21981226..eb09d047c 100644 --- a/packages/core/src/api/__tests__/api-utils.test.ts +++ b/packages/core/src/api/__tests__/api-utils.test.ts @@ -20,6 +20,7 @@ const defaultTelemetry: TelemetryConfig = { css: 'unknown', distribution: 'npm', framework: 'react', + enabled: true, }; const mockGetComponent = () => 'test-component'; @@ -132,7 +133,7 @@ describe('api-utils', () => { it('sets Auth0-Client telemetry header with proxy mode', async () => { const mockFetch = stubFetch(); const fetcher = createProxyFetcher({ - telemetry: { css: 'tailwind', distribution: 'npm', framework: 'react' }, + telemetry: { css: 'tailwind', distribution: 'npm', framework: 'react', enabled: true }, getComponent: () => 'user-mfa-management', }); @@ -154,7 +155,7 @@ describe('api-utils', () => { it('uses component from getComponent callback', async () => { const mockFetch = stubFetch(); const fetcher = createProxyFetcher({ - telemetry: { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + telemetry: { css: 'scoped', distribution: 'shadcn', framework: 'react', enabled: true }, getComponent: () => 'organization-sso-configuration', }); @@ -374,7 +375,7 @@ describe('api-utils', () => { const fetcher = createSpaFetcher( config, '__test_nonce__', - { css: 'tailwind', distribution: 'npm', framework: 'react' }, + { css: 'tailwind', distribution: 'npm', framework: 'react', enabled: true }, () => 'user-mfa-management', ); @@ -398,7 +399,7 @@ describe('api-utils', () => { const fetcher = createSpaFetcher( config, '__test_nonce__', - { css: 'scoped', distribution: 'shadcn', framework: 'react' }, + { css: 'scoped', distribution: 'shadcn', framework: 'react', enabled: true }, () => 'organization-domain-management', ); From 95e962fdd9dff2e5e820162b232ed9b57aab0d5c Mon Sep 17 00:00:00 2001 From: rax7389 Date: Sun, 7 Jun 2026 19:23:38 +0530 Subject: [PATCH 10/10] chore: refactored --- .../src/hooks/shared/use-core-client-initialization.ts | 4 ++-- packages/react/src/providers/proxy-provider.tsx | 7 +++++-- packages/react/src/providers/spa-provider.tsx | 7 +++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/react/src/hooks/shared/use-core-client-initialization.ts b/packages/react/src/hooks/shared/use-core-client-initialization.ts index e27f68ac9..df0de3b5a 100644 --- a/packages/react/src/hooks/shared/use-core-client-initialization.ts +++ b/packages/react/src/hooks/shared/use-core-client-initialization.ts @@ -36,8 +36,8 @@ export const useCoreClientInitialization = ({ const [coreClient, setCoreClient] = React.useState(null); React.useEffect(() => { - // Wait for CSS detection to complete before initializing - if (telemetry.css === 'unknown') { + // Wait for CSS detection to complete before initializing (skip if telemetry disabled) + if (telemetry.enabled && telemetry.css === 'unknown') { return; } diff --git a/packages/react/src/providers/proxy-provider.tsx b/packages/react/src/providers/proxy-provider.tsx index 060aa4750..cb98a5089 100644 --- a/packages/react/src/providers/proxy-provider.tsx +++ b/packages/react/src/providers/proxy-provider.tsx @@ -74,9 +74,12 @@ export const Auth0ComponentProvider = ({ const getComponent = React.useCallback(() => componentRef.current, []); // useLayoutEffect ensures CSS is detected before paint, avoiding incorrect telemetry on early API calls + // Skip detection if telemetry is disabled since the value won't be used React.useLayoutEffect(() => { - setCss(detectCssImplementation()); - }, []); + if (telemetryEnabled) { + setCss(detectCssImplementation()); + } + }, [telemetryEnabled]); const memoizedAuthDetails = React.useMemo( () => ({ diff --git a/packages/react/src/providers/spa-provider.tsx b/packages/react/src/providers/spa-provider.tsx index b6ce73f46..eeaa6fee7 100644 --- a/packages/react/src/providers/spa-provider.tsx +++ b/packages/react/src/providers/spa-provider.tsx @@ -77,9 +77,12 @@ export const Auth0ComponentProvider = ( const getComponent = React.useCallback(() => componentRef.current, []); // useLayoutEffect ensures CSS is detected before paint, avoiding incorrect telemetry on early API calls + // Skip detection if telemetry is disabled since the value won't be used React.useLayoutEffect(() => { - setCss(detectCssImplementation()); - }, []); + if (telemetryEnabled) { + setCss(detectCssImplementation()); + } + }, [telemetryEnabled]); const auth0ReactContext = useAuth0();