Skip to content

Commit 923425d

Browse files
konstantin-msftsameeragCopilot
authored
Nest dynamic telemetry events (#8357)
This pull request introduces a mechanism for handling dynamic telemetry fields in performance events, allowing for more flexible and scalable telemetry data collection. The main change is the addition of an `ext` sub-object to the `PerformanceEvent` type, which stores fields whose names are constructed at runtime. The pull request also updates relevant methods and tests to support this new structure, and adds a lint rule to enforce correct usage. Dynamic telemetry fields support: * Added an `ext` property to the `PerformanceEvent` type to store dynamically-named fields, such as call counts and sub-measurement durations. These fields are routed to `ext` using the `"ext."` prefix when calling `addFields` or `incrementFields`. (`lib/msal-common/src/telemetry/performance/PerformanceEvent.ts`, [lib/msal-common/src/telemetry/performance/PerformanceEvent.tsR354-R364](diffhunk://#diff-2f63a38227d7a354d8c67987de2d283da038557a987db95576a08fe16cc60354R354-R364)) * Introduced the `EXT_FIELD_PREFIX` constant and updated `PerformanceClient` methods (`addFields`, `incrementFields`) to automatically route fields with this prefix to the `ext` sub-object, ensuring separation between static and dynamic fields. (`lib/msal-common/src/telemetry/performance/PerformanceEvent.ts`, [[1]](diffhunk://#diff-2f63a38227d7a354d8c67987de2d283da038557a987db95576a08fe16cc60354R379-R385); `lib/msal-common/src/telemetry/performance/PerformanceClient.ts`, [[2]](diffhunk://#diff-5c2e3cc078d85a3fa0eb4b4596ecee70cc03689d6ac7ab96c69b8628fd8e055fL524-R561) [[3]](diffhunk://#diff-5c2e3cc078d85a3fa0eb4b4596ecee70cc03689d6ac7ab96c69b8628fd8e055fR582-R606) [[4]](diffhunk://#diff-5c2e3cc078d85a3fa0eb4b4596ecee70cc03689d6ac7ab96c69b8628fd8e055fL456-R466) Code and test updates for dynamic fields: * Updated function wrappers to increment API call counts using the dynamic field mechanism, storing them in `ext` instead of as top-level properties. (`lib/msal-common/src/utils/FunctionWrappers.ts`, [[1]](diffhunk://#diff-71662df5e90583aee6710b2dbac9fe829522000bf6b03e1ab1bed5f5a6f64932L36-R39) [[2]](diffhunk://#diff-71662df5e90583aee6710b2dbac9fe829522000bf6b03e1ab1bed5f5a6f64932L92-R97) * Refactored tests to assert dynamic fields are correctly stored in `event.ext`, and added new tests to verify merging, routing, and absence of the `ext` object when not used. (`lib/msal-common/test/telemetry/PerformanceClient.spec.ts`, [[1]](diffhunk://#diff-a1c469f9aa50bb3cf9652bb95ab570e63f3445cc854ec1c098e7e7b45f824bd0L129-R129) [[2]](diffhunk://#diff-a1c469f9aa50bb3cf9652bb95ab570e63f3445cc854ec1c098e7e7b45f824bd0L215-R222) [[3]](diffhunk://#diff-a1c469f9aa50bb3cf9652bb95ab570e63f3445cc854ec1c098e7e7b45f824bd0L356-R366) [[4]](diffhunk://#diff-a1c469f9aa50bb3cf9652bb95ab570e63f3445cc854ec1c098e7e7b45f824bd0L406-R410) [[5]](diffhunk://#diff-a1c469f9aa50bb3cf9652bb95ab570e63f3445cc854ec1c098e7e7b45f824bd0L454-R460) [[6]](diffhunk://#diff-a1c469f9aa50bb3cf9652bb95ab570e63f3445cc854ec1c098e7e7b45f824bd0R1497-R1623) Linting and documentation: * Added a custom ESLint rule to enforce no dynamic telemetry fields at the top level, ensuring dynamic fields are always routed to `ext`. (`shared-configs/eslint-config-msal/index.js`, [shared-configs/eslint-config-msal/index.jsR88](diffhunk://#diff-2b30170ea2355b0ba1b936931fa57a0703830fa262c9ac8e5a64904d58245478R88)) * Updated API review and documentation comments to reflect the new structure and dynamic field handling. (`lib/msal-common/apiReview/msal-common.api.md`, [[1]](diffhunk://#diff-09087b913ebbfa828e5f36b7476a400328e0a7131db84f622cc5f6994759a117R3491) [[2]](diffhunk://#diff-09087b913ebbfa828e5f36b7476a400328e0a7131db84f622cc5f6994759a117L4794-R4800) [[3]](diffhunk://#diff-09087b913ebbfa828e5f36b7476a400328e0a7131db84f622cc5f6994759a117R4883-R4887) These changes provide a scalable foundation for telemetry data collection, making it easier to add new dynamic fields without bloating the top-level event structure. --------- Co-authored-by: Sameera Gajjarapu <sameera.gajjarapu@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent ed8afbb commit 923425d

11 files changed

Lines changed: 939 additions & 38 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Nest dynamic telemetry events [#8357](https://github.com/AzureAD/microsoft-authentication-library-for-js/pull/8357)",
4+
"packageName": "@azure/msal-common",
5+
"email": "kshabelko@microsoft.com",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-browser/test/app/PublicClientApplication.spec.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,9 +786,11 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
786786
expect(event.correlationId).toBeDefined();
787787
expect(event.success).toBeTruthy();
788788
expect(
789-
event["handleRedirectPromiseDurationMs"]
789+
event.ext?.["handleRedirectPromiseDurationMs"]
790790
).toBeGreaterThanOrEqual(0);
791-
expect(event["handleRedirectPromiseCallCount"]).toEqual(1);
791+
expect(event.ext?.["handleRedirectPromiseCallCount"]).toEqual(
792+
1
793+
);
792794
expect(event.success).toBeTruthy();
793795
expect(event.accountType).toEqual(undefined);
794796
pca.removePerformanceCallback(callbackId);
@@ -941,10 +943,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => {
941943
expect(event.correlationId).toBeDefined();
942944
expect(event.success).toBeTruthy();
943945
expect(
944-
event["handleNativeRedirectPromiseDurationMs"]
946+
event.ext?.["handleNativeRedirectPromiseDurationMs"]
945947
).toBeGreaterThanOrEqual(0);
946948
expect(
947-
event["handleNativeRedirectPromiseCallCount"]
949+
event.ext?.["handleNativeRedirectPromiseCallCount"]
948950
).toEqual(1);
949951
expect(event.success).toBeTruthy();
950952
expect(event.accountType).toEqual("MSA");

lib/msal-common/apiReview/msal-common.api.md

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3474,6 +3474,7 @@ export type PerformanceEvent = {
34743474
ntwkRtExpiresOnSeconds?: number;
34753475
extRtExpiresOnSeconds?: number;
34763476
rtOffsetSeconds?: number;
3477+
sidFromClaim?: boolean;
34773478
sidFromClaims?: boolean;
34783479
sidFromRequest?: boolean;
34793480
loginHintFromRequest?: boolean;
@@ -3487,7 +3488,41 @@ export type PerformanceEvent = {
34873488
navigateCallbackResult?: boolean;
34883489
dataBoundary?: DataBoundary;
34893490
logs?: string;
3491+
cloudDiscoverySource?: string;
3492+
authorityEndpointSource?: string;
3493+
accountsRemoved?: number;
3494+
accessTokensRemoved?: number;
3495+
removeTokenBindingKeyFailure?: number;
34903496
silentRefreshReason?: string;
3497+
deduped?: boolean;
3498+
kmsi?: boolean;
3499+
isBackground?: boolean;
3500+
preMigrateAcntCount?: number;
3501+
preMigrateATCount?: number;
3502+
preMigrateITCount?: number;
3503+
preMigrateRTCount?: number;
3504+
postMigrateAcntCount?: number;
3505+
postMigrateATCount?: number;
3506+
postMigrateITCount?: number;
3507+
postMigrateRTCount?: number;
3508+
oldAcntCount?: number;
3509+
oldATCount?: number;
3510+
oldITCount?: number;
3511+
oldRTCount?: number;
3512+
skipATMigrateCount?: number;
3513+
skipITMigrateCount?: number;
3514+
skipRTMigrateCount?: number;
3515+
migratedATCount?: number;
3516+
migratedITCount?: number;
3517+
migratedRTCount?: number;
3518+
expiredCacheRemovedCount?: number;
3519+
expiredAcntRemovedCount?: number;
3520+
invalidCacheCount?: number;
3521+
unencryptedCacheCount?: number;
3522+
encryptedCacheCount?: number;
3523+
encryptedCacheExpiredCount?: number;
3524+
encryptedCacheCorruptionCount?: number;
3525+
ext?: Record<string, string | number>;
34913526
};
34923527

34933528
declare namespace PerformanceEvents {
@@ -4788,12 +4823,12 @@ const X_MS_LIB_CAPABILITY_VALUE: string;
47884823
// src/response/ResponseHandler.ts:355:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
47894824
// src/response/ResponseHandler.ts:356:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
47904825
// src/response/ResponseHandler.ts:357:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
4791-
// src/telemetry/performance/PerformanceClient.ts:673:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
4792-
// src/telemetry/performance/PerformanceClient.ts:673:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
4793-
// src/telemetry/performance/PerformanceClient.ts:685:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
4794-
// src/telemetry/performance/PerformanceClient.ts:685:27 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
4795-
// src/telemetry/performance/PerformanceClient.ts:686:24 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
4796-
// src/telemetry/performance/PerformanceClient.ts:686:17 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
4826+
// src/telemetry/performance/PerformanceClient.ts:723:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
4827+
// src/telemetry/performance/PerformanceClient.ts:723:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
4828+
// src/telemetry/performance/PerformanceClient.ts:737:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
4829+
// src/telemetry/performance/PerformanceClient.ts:737:27 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}'
4830+
// src/telemetry/performance/PerformanceClient.ts:738:24 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
4831+
// src/telemetry/performance/PerformanceClient.ts:738:17 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
47974832
// src/telemetry/performance/PerformanceEvent.ts:37:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
47984833
// src/telemetry/performance/PerformanceEvent.ts:37:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
47994834
// src/telemetry/performance/PerformanceEvent.ts:37:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration
@@ -4873,8 +4908,8 @@ const X_MS_LIB_CAPABILITY_VALUE: string;
48734908
// src/telemetry/performance/PerformanceEvent.ts:310:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
48744909
// src/telemetry/performance/PerformanceEvent.ts:310:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
48754910
// src/telemetry/performance/PerformanceEvent.ts:310:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration
4876-
// src/telemetry/performance/PerformanceEvent.ts:351:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
4877-
// src/telemetry/performance/PerformanceEvent.ts:351:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
4878-
// src/telemetry/performance/PerformanceEvent.ts:351:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration
4911+
// src/telemetry/performance/PerformanceEvent.ts:378:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag
4912+
// src/telemetry/performance/PerformanceEvent.ts:378:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@"
4913+
// src/telemetry/performance/PerformanceEvent.ts:378:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration
48794914

48804915
```

lib/msal-common/src/telemetry/performance/PerformanceClient.ts

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
PerformanceCallbackFunction,
1212
} from "./IPerformanceClient.js";
1313
import {
14+
EXT_FIELD_PREFIX,
1415
IntFields,
1516
PerformanceEvent,
1617
PerformanceEventContext,
@@ -453,9 +454,15 @@ export abstract class PerformanceClient implements IPerformanceClient {
453454
addError(error, this.logger, rootEvent);
454455
}
455456

456-
// Add sub-measurement attribute to root event.
457+
// Add sub-measurement attribute to root event's ext field.
457458
if (!isRoot) {
458-
rootEvent[event.name + "DurationMs"] = Math.floor(event.durationMs);
459+
rootEvent.ext = {
460+
...rootEvent.ext,
461+
...event.ext,
462+
};
463+
rootEvent.ext[event.name + "DurationMs"] = Math.floor(
464+
event.durationMs
465+
);
459466
return { ...rootEvent };
460467
}
461468

@@ -521,10 +528,36 @@ export abstract class PerformanceClient implements IPerformanceClient {
521528
): void {
522529
const event = this.eventsByCorrelationId.get(correlationId);
523530
if (event) {
524-
this.eventsByCorrelationId.set(correlationId, {
531+
const staticFields: { [key: string]: {} | undefined } = {};
532+
const dynamicFields: Record<string, string | number> = {};
533+
534+
for (const key in fields) {
535+
if (key.startsWith(EXT_FIELD_PREFIX)) {
536+
const dynamicKey = key.slice(EXT_FIELD_PREFIX.length);
537+
const value = fields[key];
538+
if (
539+
typeof value === "string" ||
540+
typeof value === "number"
541+
) {
542+
dynamicFields[dynamicKey] = value;
543+
}
544+
} else {
545+
staticFields[key] = fields[key];
546+
}
547+
}
548+
549+
const updatedEvent: PerformanceEvent = {
525550
...event,
526-
...fields,
527-
});
551+
...staticFields,
552+
};
553+
if (Object.keys(dynamicFields).length) {
554+
updatedEvent.ext = {
555+
...updatedEvent.ext,
556+
...dynamicFields,
557+
};
558+
}
559+
560+
this.eventsByCorrelationId.set(correlationId, updatedEvent);
528561
} else {
529562
this.logger.trace(
530563
"PerformanceClient: Event not found for",
@@ -545,12 +578,29 @@ export abstract class PerformanceClient implements IPerformanceClient {
545578
const event = this.eventsByCorrelationId.get(correlationId);
546579
if (event) {
547580
for (const counter in fields) {
548-
if (!event.hasOwnProperty(counter)) {
549-
event[counter] = 0;
550-
} else if (isNaN(Number(event[counter]))) {
551-
return;
581+
if (counter.startsWith(EXT_FIELD_PREFIX)) {
582+
event.ext = event.ext || {};
583+
// Route to ext sub-object
584+
const dynamicKey = counter.slice(EXT_FIELD_PREFIX.length);
585+
const currentValue = event.ext[dynamicKey];
586+
if (currentValue === undefined) {
587+
event.ext[dynamicKey] = 0;
588+
} else if (isNaN(Number(currentValue))) {
589+
return;
590+
}
591+
event.ext[dynamicKey] =
592+
(Number(event.ext[dynamicKey]) || 0) +
593+
(fields[counter] ?? 0);
594+
} else {
595+
/* eslint-disable custom-msal/no-dynamic-telemetry-fields -- internal dispatching of static fields by name */
596+
if (!event.hasOwnProperty(counter)) {
597+
event[counter] = 0;
598+
} else if (isNaN(Number(event[counter]))) {
599+
return;
600+
}
601+
event[counter] += fields[counter];
602+
/* eslint-enable custom-msal/no-dynamic-telemetry-fields */
552603
}
553-
event[counter] += fields[counter];
554604
}
555605
} else {
556606
this.logger.trace(
@@ -674,9 +724,11 @@ export abstract class PerformanceClient implements IPerformanceClient {
674724
*/
675725
private truncateIntegralFields(event: PerformanceEvent): void {
676726
this.intFields.forEach((key) => {
727+
/* eslint-disable custom-msal/no-dynamic-telemetry-fields -- internal truncation of known integer fields */
677728
if (key in event && typeof event[key] === "number") {
678729
event[key] = Math.floor(event[key]);
679730
}
731+
/* eslint-enable custom-msal/no-dynamic-telemetry-fields */
680732
});
681733
}
682734

lib/msal-common/src/telemetry/performance/PerformanceEvent.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ export type PerformanceEvent = {
321321
extRtExpiresOnSeconds?: number;
322322
rtOffsetSeconds?: number;
323323

324+
sidFromClaim?: boolean;
325+
// Backward-compatible alias for sidFromClaim
324326
sidFromClaims?: boolean;
325327
sidFromRequest?: boolean;
326328
loginHintFromRequest?: boolean;
@@ -344,13 +346,114 @@ export type PerformanceEvent = {
344346
// Hashed logs in the format [millis1,hash1;millis2,hash2;...]
345347
logs?: string;
346348

349+
/**
350+
* Source of cloud discovery metadata (config, cache, network, hardcoded_values)
351+
*/
352+
cloudDiscoverySource?: string;
353+
354+
/**
355+
* Source of authority endpoint metadata (config, cache, network, hardcoded_values)
356+
*/
357+
authorityEndpointSource?: string;
358+
359+
/**
360+
* Number of accounts removed during cache cleanup
361+
*/
362+
accountsRemoved?: number;
363+
364+
/**
365+
* Number of access tokens removed during cache cleanup
366+
*/
367+
accessTokensRemoved?: number;
368+
369+
/**
370+
* Number of failures when removing token binding keys
371+
*/
372+
removeTokenBindingKeyFailure?: number;
373+
347374
/**
348375
* Reason for silent refresh fallback to iframe
349376
* Format: errorCode or errorCode|subError
350377
*
351378
* @type {?string}
352379
*/
353380
silentRefreshReason?: string;
381+
382+
/**
383+
* Whether this request was deduped with another in-flight request
384+
*/
385+
deduped?: boolean;
386+
387+
/**
388+
* Whether the user has "Keep Me Signed In" enabled
389+
*/
390+
kmsi?: boolean;
391+
392+
/**
393+
* Whether this event was executed in the background
394+
*/
395+
isBackground?: boolean;
396+
397+
/**
398+
* Cache migration telemetry — pre-migration counts
399+
*/
400+
preMigrateAcntCount?: number;
401+
preMigrateATCount?: number;
402+
preMigrateITCount?: number;
403+
preMigrateRTCount?: number;
404+
405+
/**
406+
* Cache migration telemetry — post-migration counts
407+
*/
408+
postMigrateAcntCount?: number;
409+
postMigrateATCount?: number;
410+
postMigrateITCount?: number;
411+
postMigrateRTCount?: number;
412+
413+
/**
414+
* Cache migration telemetry — old schema counts
415+
*/
416+
oldAcntCount?: number;
417+
oldATCount?: number;
418+
oldITCount?: number;
419+
oldRTCount?: number;
420+
421+
/**
422+
* Cache migration telemetry — skipped and migrated counts
423+
*/
424+
skipATMigrateCount?: number;
425+
skipITMigrateCount?: number;
426+
skipRTMigrateCount?: number;
427+
migratedATCount?: number;
428+
migratedITCount?: number;
429+
migratedRTCount?: number;
430+
431+
/**
432+
* Cache telemetry — expired, invalid, and removed counts
433+
*/
434+
expiredCacheRemovedCount?: number;
435+
expiredAcntRemovedCount?: number;
436+
invalidCacheCount?: number;
437+
438+
/**
439+
* Encrypted cache telemetry
440+
*/
441+
unencryptedCacheCount?: number;
442+
encryptedCacheCount?: number;
443+
encryptedCacheExpiredCount?: number;
444+
encryptedCacheCorruptionCount?: number;
445+
446+
/**
447+
* Container for dynamically-named telemetry fields.
448+
* Fields whose names are constructed at runtime (e.g., "[eventName]CallCount")
449+
* should be stored here instead of being set as top-level properties.
450+
* Use the "ext." prefix when calling addFields/incrementFields to automatically
451+
* route fields to this sub-object.
452+
*
453+
* @remarks
454+
* This property is typed as `Record<string, string | number>`.
455+
*/
456+
ext?: Record<string, string | number>;
354457
};
355458

356459
export type PerformanceEventContext = {
@@ -365,6 +468,13 @@ export type PerformanceEventStackedContext = PerformanceEventContext & {
365468
childErr?: string;
366469
};
367470

471+
/**
472+
* Prefix used to mark telemetry field names as dynamic.
473+
* Fields with this prefix in addFields/incrementFields calls will be routed
474+
* to the PerformanceEvent.ext sub-object.
475+
*/
476+
export const EXT_FIELD_PREFIX = "ext.";
477+
368478
export const IntFields: ReadonlySet<string> = new Set([
369479
"accessTokenSize",
370480
"durationMs",

lib/msal-common/src/utils/FunctionWrappers.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ export const invoke = <T extends Array<any>, U>(
3333
);
3434
if (correlationId) {
3535
// Track number of times this API is called in a single request
36-
const eventCount = eventName + "CallCount";
37-
telemetryClient.incrementFields({ [eventCount]: 1 }, correlationId);
36+
telemetryClient.incrementFields(
37+
{ [`ext.${eventName}CallCount`]: 1 },
38+
correlationId
39+
);
3840
}
3941
try {
4042
const result = callback(...args);
@@ -89,8 +91,10 @@ export const invokeAsync = <T extends Array<any>, U>(
8991
);
9092
if (correlationId) {
9193
// Track number of times this API is called in a single request
92-
const eventCount = eventName + "CallCount";
93-
telemetryClient.incrementFields({ [eventCount]: 1 }, correlationId);
94+
telemetryClient.incrementFields(
95+
{ [`ext.${eventName}CallCount`]: 1 },
96+
correlationId
97+
);
9498
}
9599
return callback(...args)
96100
.then((response) => {

0 commit comments

Comments
 (0)