Skip to content

Commit a4b15a0

Browse files
authored
Merge pull request #301 from auth0/feat/uic-telemetry-poc
feat: add telemetry tracking via auth0-client header
2 parents 06daa81 + cf61ad7 commit a4b15a0

37 files changed

Lines changed: 1338 additions & 176 deletions

examples/next-rwa/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint": "next lint"
1111
},
1212
"dependencies": {
13-
"@auth0/nextjs-auth0": "^4.20.0",
13+
"@auth0/nextjs-auth0": "^4.21.0",
1414
"@auth0/universal-components-react": "workspace:*",
1515
"@radix-ui/react-slot": "^1.2.3",
1616
"@tailwindcss/postcss": "^4.1.17",

packages/core/src/api/__tests__/api-utils.test.ts

Lines changed: 204 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@ import {
77
} from '../../internals/__mocks__/shared/api-service.mocks';
88
import { AUTH0_SCOPE_HEADER, createProxyFetcher, createSpaFetcher } from '../api-utils';
99
import { ContentType, HeaderName } from '../http-constants';
10+
import type { TelemetryConfig } from '../telemetry';
1011

1112
import { stubFetch } from './__mocks__/api-utils.mocks';
1213

14+
const defaultTelemetry: TelemetryConfig = {
15+
css: 'unknown',
16+
distribution: 'npm',
17+
framework: 'react',
18+
enabled: true,
19+
};
20+
21+
const mockGetComponent = () => 'test-component';
22+
1323
describe('api-utils', () => {
1424
describe('createProxyFetcher', () => {
1525
afterEach(() => {
@@ -18,7 +28,10 @@ describe('api-utils', () => {
1828

1929
it('sets content-type header to application/json', async () => {
2030
const mockFetch = stubFetch();
21-
const fetcher = createProxyFetcher();
31+
const fetcher = createProxyFetcher({
32+
telemetry: defaultTelemetry,
33+
getComponent: mockGetComponent,
34+
});
2235

2336
await fetcher('https://example.com/api', { method: 'POST' }, undefined);
2437

@@ -28,7 +41,10 @@ describe('api-utils', () => {
2841

2942
it('sets auth0-scope header when scope array is provided', async () => {
3043
const mockFetch = stubFetch();
31-
const fetcher = createProxyFetcher();
44+
const fetcher = createProxyFetcher({
45+
telemetry: defaultTelemetry,
46+
getComponent: mockGetComponent,
47+
});
3248

3349
await fetcher(
3450
'https://example.com/api',
@@ -45,7 +61,10 @@ describe('api-utils', () => {
4561

4662
it('does not set auth0-scope header when scope array is empty', async () => {
4763
const mockFetch = stubFetch();
48-
const fetcher = createProxyFetcher();
64+
const fetcher = createProxyFetcher({
65+
telemetry: defaultTelemetry,
66+
getComponent: mockGetComponent,
67+
});
4968

5069
await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] });
5170

@@ -55,7 +74,10 @@ describe('api-utils', () => {
5574

5675
it('does not set auth0-scope header when authParams is undefined', async () => {
5776
const mockFetch = stubFetch();
58-
const fetcher = createProxyFetcher();
77+
const fetcher = createProxyFetcher({
78+
telemetry: defaultTelemetry,
79+
getComponent: mockGetComponent,
80+
});
5981

6082
await fetcher('https://example.com/api', { method: 'GET' }, undefined);
6183

@@ -65,7 +87,10 @@ describe('api-utils', () => {
6587

6688
it('preserves existing headers from init', async () => {
6789
const mockFetch = stubFetch();
68-
const fetcher = createProxyFetcher();
90+
const fetcher = createProxyFetcher({
91+
telemetry: defaultTelemetry,
92+
getComponent: mockGetComponent,
93+
});
6994
const customHeaders = new Headers({ 'X-Custom': 'value' });
7095

7196
await fetcher(
@@ -82,7 +107,10 @@ describe('api-utils', () => {
82107

83108
it('preserves other init options', async () => {
84109
const mockFetch = stubFetch();
85-
const fetcher = createProxyFetcher();
110+
const fetcher = createProxyFetcher({
111+
telemetry: defaultTelemetry,
112+
getComponent: mockGetComponent,
113+
});
86114
const body = JSON.stringify({ data: 'test' });
87115

88116
await fetcher(
@@ -96,6 +124,75 @@ describe('api-utils', () => {
96124
expect(requestInit?.body).toBe(body);
97125
expect(requestInit?.credentials).toBe('include');
98126
});
127+
128+
it('sets Auth0-Client telemetry header with proxy mode', async () => {
129+
const mockFetch = stubFetch();
130+
const fetcher = createProxyFetcher({
131+
telemetry: { css: 'tailwind', distribution: 'npm', framework: 'react', enabled: true },
132+
getComponent: () => 'user-mfa-management',
133+
});
134+
135+
await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined);
136+
137+
const [, requestInit] = mockFetch.mock.calls[0]!;
138+
const header = (requestInit?.headers as Headers).get(HeaderName.Auth0Client);
139+
expect(header).toBeTruthy();
140+
141+
const decoded = JSON.parse(atob(header!));
142+
expect(decoded.is_proxy_mode).toBe(true);
143+
expect(decoded.component).toBe('user-mfa-management');
144+
expect(decoded.name).toBe('universal-components');
145+
expect(decoded.css).toBe('tailwind');
146+
expect(decoded.distribution).toBe('npm');
147+
expect(decoded.framework).toBe('react');
148+
});
149+
150+
it('uses component from getComponent callback', async () => {
151+
const mockFetch = stubFetch();
152+
const fetcher = createProxyFetcher({
153+
telemetry: { css: 'scoped', distribution: 'shadcn', framework: 'react', enabled: true },
154+
getComponent: () => 'organization-sso-configuration',
155+
});
156+
157+
await fetcher('https://example.com/my-org/identity-providers', { method: 'GET' }, undefined);
158+
159+
const [, requestInit] = mockFetch.mock.calls[0]!;
160+
const header = (requestInit?.headers as Headers).get(HeaderName.Auth0Client);
161+
const decoded = JSON.parse(atob(header!));
162+
expect(decoded.component).toBe('organization-sso-configuration');
163+
expect(decoded.css).toBe('scoped');
164+
expect(decoded.distribution).toBe('shadcn');
165+
});
166+
167+
it('uses custom fetcher when provided', async () => {
168+
const customFetcher = vi.fn().mockResolvedValue(new Response());
169+
const fetcher = createProxyFetcher({
170+
customFetcher,
171+
telemetry: defaultTelemetry,
172+
getComponent: mockGetComponent,
173+
});
174+
175+
await fetcher('https://example.com/api', { method: 'GET' }, undefined);
176+
177+
expect(customFetcher).toHaveBeenCalledWith(
178+
'https://example.com/api',
179+
expect.objectContaining({ method: 'GET' }),
180+
undefined,
181+
);
182+
});
183+
184+
it('does not set Auth0-Client header when telemetry is disabled', async () => {
185+
const mockFetch = stubFetch();
186+
const fetcher = createProxyFetcher({
187+
telemetry: { ...defaultTelemetry, enabled: false },
188+
getComponent: mockGetComponent,
189+
});
190+
191+
await fetcher('https://example.com/api', { method: 'GET' }, undefined);
192+
193+
const [, requestInit] = mockFetch.mock.calls[0]!;
194+
expect((requestInit?.headers as Headers).get(HeaderName.Auth0Client)).toBeNull();
195+
});
99196
});
100197

101198
describe('createSpaFetcher', () => {
@@ -124,14 +221,19 @@ describe('api-utils', () => {
124221
const config = createSpaConfig();
125222
const dpopNonceId = '__test_dpop_nonce__';
126223

127-
createSpaFetcher(config, dpopNonceId);
224+
createSpaFetcher(config, dpopNonceId, defaultTelemetry, mockGetComponent);
128225

129226
expect(mockCreateFetcher).toHaveBeenCalledWith({ dpopNonceId });
130227
});
131228

132229
it('sets Content-Type header to application/json', async () => {
133230
const config = createSpaConfig();
134-
const fetcher = createSpaFetcher(config, '__test_nonce__');
231+
const fetcher = createSpaFetcher(
232+
config,
233+
'__test_nonce__',
234+
defaultTelemetry,
235+
mockGetComponent,
236+
);
135237

136238
await fetcher('https://example.com/api', { method: 'POST' }, undefined);
137239

@@ -141,7 +243,12 @@ describe('api-utils', () => {
141243

142244
it('preserves existing headers from init when adding Content-Type', async () => {
143245
const config = createSpaConfig();
144-
const fetcher = createSpaFetcher(config, '__test_nonce__');
246+
const fetcher = createSpaFetcher(
247+
config,
248+
'__test_nonce__',
249+
defaultTelemetry,
250+
mockGetComponent,
251+
);
145252
const customHeaders = new Headers({ 'X-Custom': 'value' });
146253

147254
await fetcher(
@@ -158,7 +265,12 @@ describe('api-utils', () => {
158265

159266
it('preserves other init options when adding Content-Type header', async () => {
160267
const config = createSpaConfig();
161-
const fetcher = createSpaFetcher(config, '__test_nonce__');
268+
const fetcher = createSpaFetcher(
269+
config,
270+
'__test_nonce__',
271+
defaultTelemetry,
272+
mockGetComponent,
273+
);
162274
const body = JSON.stringify({ data: 'test' });
163275

164276
await fetcher(
@@ -176,7 +288,12 @@ describe('api-utils', () => {
176288

177289
it('delegates to SDK fetchWithAuth with scope and audience', async () => {
178290
const config = createSpaConfig();
179-
const fetcher = createSpaFetcher(config, '__test_nonce__');
291+
const fetcher = createSpaFetcher(
292+
config,
293+
'__test_nonce__',
294+
defaultTelemetry,
295+
mockGetComponent,
296+
);
180297

181298
await fetcher(
182299
'https://example.com/api',
@@ -193,7 +310,12 @@ describe('api-utils', () => {
193310

194311
it('handles undefined authParams', async () => {
195312
const config = createSpaConfig();
196-
const fetcher = createSpaFetcher(config, '__test_nonce__');
313+
const fetcher = createSpaFetcher(
314+
config,
315+
'__test_nonce__',
316+
defaultTelemetry,
317+
mockGetComponent,
318+
);
197319

198320
await fetcher('https://example.com/api', { method: 'GET' }, undefined);
199321

@@ -206,7 +328,12 @@ describe('api-utils', () => {
206328

207329
it('handles empty scope array', async () => {
208330
const config = createSpaConfig();
209-
const fetcher = createSpaFetcher(config, '__test_nonce__');
331+
const fetcher = createSpaFetcher(
332+
config,
333+
'__test_nonce__',
334+
defaultTelemetry,
335+
mockGetComponent,
336+
);
210337

211338
await fetcher('https://example.com/api', { method: 'GET' }, { scope: [] });
212339

@@ -219,7 +346,12 @@ describe('api-utils', () => {
219346

220347
it('handles undefined init parameter', async () => {
221348
const config = createSpaConfig();
222-
const fetcher = createSpaFetcher(config, '__test_nonce__');
349+
const fetcher = createSpaFetcher(
350+
config,
351+
'__test_nonce__',
352+
defaultTelemetry,
353+
mockGetComponent,
354+
);
223355

224356
await fetcher('https://example.com/api', undefined, { scope: ['read:users'] });
225357

@@ -232,5 +364,63 @@ describe('api-utils', () => {
232364
},
233365
);
234366
});
367+
368+
it('sets Auth0-Client telemetry header with SPA mode', async () => {
369+
const config = createSpaConfig();
370+
const fetcher = createSpaFetcher(
371+
config,
372+
'__test_nonce__',
373+
{ css: 'tailwind', distribution: 'npm', framework: 'react', enabled: true },
374+
() => 'user-mfa-management',
375+
);
376+
377+
await fetcher('https://example.com/me/authentication-methods', { method: 'GET' }, undefined);
378+
379+
const [, requestInit] = mockFetchWithAuth.mock.calls[0]!;
380+
const header = (requestInit?.headers as Headers).get(HeaderName.Auth0Client);
381+
expect(header).toBeTruthy();
382+
383+
const decoded = JSON.parse(atob(header!));
384+
expect(decoded.is_proxy_mode).toBe(false);
385+
expect(decoded.component).toBe('user-mfa-management');
386+
expect(decoded.name).toBe('universal-components');
387+
expect(decoded.css).toBe('tailwind');
388+
expect(decoded.distribution).toBe('npm');
389+
expect(decoded.framework).toBe('react');
390+
});
391+
392+
it('uses component from getComponent callback', async () => {
393+
const config = createSpaConfig();
394+
const fetcher = createSpaFetcher(
395+
config,
396+
'__test_nonce__',
397+
{ css: 'scoped', distribution: 'shadcn', framework: 'react', enabled: true },
398+
() => 'organization-domain-management',
399+
);
400+
401+
await fetcher('https://example.com/my-org/domains', { method: 'GET' }, undefined);
402+
403+
const [, requestInit] = mockFetchWithAuth.mock.calls[0]!;
404+
const header = (requestInit?.headers as Headers).get(HeaderName.Auth0Client);
405+
const decoded = JSON.parse(atob(header!));
406+
expect(decoded.component).toBe('organization-domain-management');
407+
expect(decoded.css).toBe('scoped');
408+
expect(decoded.distribution).toBe('shadcn');
409+
});
410+
411+
it('does not set Auth0-Client header when telemetry is disabled', async () => {
412+
const config = createSpaConfig();
413+
const fetcher = createSpaFetcher(
414+
config,
415+
'__test_nonce__',
416+
{ ...defaultTelemetry, enabled: false },
417+
mockGetComponent,
418+
);
419+
420+
await fetcher('https://example.com/api', { method: 'GET' }, undefined);
421+
422+
const [, requestInit] = mockFetchWithAuth.mock.calls[0]!;
423+
expect((requestInit?.headers as Headers).get(HeaderName.Auth0Client)).toBeNull();
424+
});
235425
});
236426
});

0 commit comments

Comments
 (0)