From 56b3cbe51c6ce4fe23291fc1ac0c530f61d7bd37 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 12 Jan 2026 01:28:51 +0100 Subject: [PATCH 1/4] fix: add measure utils --- .../user-timing-extensibility-api-utils.ts | 194 ++++++- ...iming-extensibility-api-utils.unit.test.ts | 508 +++++++++++++++++- 2 files changed, 667 insertions(+), 35 deletions(-) diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts index 78a8e6d78..ed02634ba 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -1,3 +1,4 @@ +import { performance } from 'node:perf_hooks'; import type { DevToolsColor, DevToolsProperties, @@ -12,13 +13,20 @@ import type { const dataTypeTrackEntry = 'track-entry'; const dataTypeMarker = 'marker'; +export function mergePropertiesWithOverwrite< + const T extends DevToolsProperties, + const U extends DevToolsProperties, +>(baseProperties: T, overrideProperties: U): (T[number] | U[number])[]; +export function mergePropertiesWithOverwrite< + const T extends DevToolsProperties, +>(baseProperties: T): T; export function mergePropertiesWithOverwrite( - baseProperties: DevToolsProperties | undefined, - overrideProperties?: DevToolsProperties | undefined, -) { + baseProperties?: DevToolsProperties, + overrideProperties?: DevToolsProperties, +): DevToolsProperties { return [ ...new Map([...(baseProperties ?? []), ...(overrideProperties ?? [])]), - ] satisfies DevToolsProperties; + ]; } export function markerPayload(options?: Omit) { @@ -49,19 +57,15 @@ export function markerErrorPayload( } satisfies MarkerPayload; } -export function trackEntryErrorPayload< - T extends string, - C extends DevToolsColor, ->( +export function trackEntryErrorPayload( options: Omit & { track: T; - color?: C; }, ) { - const { track, color = 'error' as const, ...restOptions } = options; + const { track, ...restOptions } = options; return { dataType: dataTypeTrackEntry, - color, + color: 'error' as const, track, ...restOptions, } satisfies TrackEntryPayload; @@ -86,7 +90,7 @@ export function errorToEntryMeta( const { properties, tooltipText } = options ?? {}; const props = mergePropertiesWithOverwrite( errorToDevToolsProperties(e), - properties, + properties ?? [], ); return { properties: props, @@ -127,19 +131,6 @@ export function errorToMarkerPayload( } satisfies MarkerPayload; } -/** - * asOptions wraps a DevTools payload into the `detail` property of User Timing entry options. - * - * @example - * profiler.mark('mark', asOptions({ - * dataType: 'marker', - * color: 'error', - * tooltipText: 'This is a marker', - * properties: [ - * ['str', 'This is a detail property'] - * ], - * })); - */ export function asOptions( devtools?: T | null, ): MarkOptionsWithDevtools; @@ -151,5 +142,156 @@ export function asOptions( ): { detail?: WithDevToolsPayload; } { - return devtools == null ? { detail: {} } : { detail: { devtools } }; + if (devtools == null) { + return { detail: {} }; + } + + return { detail: { devtools } }; +} + +export type Names = { + startName: `${N}:start`; + endName: `${N}:end`; + measureName: N; +}; + +export function getNames(base: T): Names; +export function getNames( + base: T, + prefix?: P, +): Names<`${P}:${T}`>; +export function getNames(base: string, prefix?: string) { + const n = prefix ? `${prefix}:${base}` : base; + return { + startName: `${n}:start`, + endName: `${n}:end`, + measureName: n, + } as const; +} + +type Simplify = { [K in keyof T]: T[K] } & object; + +type MergeObjects = T extends readonly [ + infer F extends object, + ...infer R extends readonly object[], +] + ? Simplify> & MergeObjects> + : object; + +export type MergeResult< + P extends readonly Partial[], +> = MergeObjects

& { properties?: DevToolsProperties }; + +export function mergeDevtoolsPayload< + const P extends readonly Partial[], +>(...parts: P): MergeResult

{ + return parts.reduce( + (acc, cur) => ({ + ...acc, + ...cur, + ...(cur.properties || acc.properties + ? { + properties: mergePropertiesWithOverwrite( + acc.properties ?? [], + cur.properties ?? [], + ), + } + : {}), + }), + {} as Partial, + ) as MergeResult

; +} + +export function mergeDevtoolsPayloadAction< + const P extends readonly [ActionTrack, ...Partial[]], +>(...parts: P): MergeObjects

& { properties?: DevToolsProperties } { + return mergeDevtoolsPayload( + ...(parts as unknown as readonly Partial< + TrackEntryPayload | MarkerPayload + >[]), + ) as MergeObjects

& { properties?: DevToolsProperties }; +} + +export type ActionColorPayload = { + color?: DevToolsColor; +}; +export type ActionTrack = TrackEntryPayload & ActionColorPayload; + +export function setupTracks< + const T extends Record>, + const D extends ActionTrack, +>(defaults: D, tracks: T): Record { + return Object.entries(tracks).reduce( + (result, [key, track]) => ({ + ...result, + [key]: mergeDevtoolsPayload(defaults, track) as ActionTrack, + }), + {} as Record, + ); +} + +/** + * This is a helper function used to ensure that the marks used to create a measure do not contain UI interaction properties. + * @param devtools - The devtools payload to convert to mark options. + * @returns The mark options without tooltipText and properties. + */ +function toMarkMeasureOpts(devtools: TrackEntryPayload) { + const { tooltipText: _, properties: __, ...markDevtools } = devtools; + return { detail: { devtools: markDevtools } }; +} + +export type MeasureOptions = Partial & { + success?: (result: unknown) => EntryMeta; + error?: (error: unknown) => EntryMeta; +}; + +export type MeasureCtxOptions = ActionTrack & { + prefix?: string; +} & { + error?: (error: unknown) => EntryMeta; +}; +export function measureCtx(cfg: MeasureCtxOptions) { + const { prefix, error: globalErr, ...defaults } = cfg; + + return (event: string, opt?: MeasureOptions) => { + const { success, error, ...measurePayload } = opt ?? {}; + const merged = mergeDevtoolsPayloadAction(defaults, measurePayload, { + dataType: dataTypeTrackEntry, + }) as TrackEntryPayload; + + const { + startName: s, + endName: e, + measureName: m, + } = getNames(event, prefix); + + return { + start: () => performance.mark(s, toMarkMeasureOpts(merged)), + + success: (r: unknown) => { + const successPayload = mergeDevtoolsPayload(merged, success?.(r) ?? {}); + performance.mark(e, toMarkMeasureOpts(successPayload)); + performance.measure(m, { + start: s, + end: e, + ...asOptions(successPayload), + }); + }, + + error: (err: unknown) => { + const errorPayload = mergeDevtoolsPayload( + errorToEntryMeta(err), + globalErr?.(err) ?? {}, + error?.(err) ?? {}, + { ...merged, color: 'error' }, + ); + performance.mark(e, toMarkMeasureOpts(errorPayload)); + performance.measure(m, { + start: s, + end: e, + ...asOptions(errorPayload), + }); + }, + }; + }; } diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts index 440f74787..23c4a9a25 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts @@ -1,16 +1,29 @@ -import { describe, expect, it } from 'vitest'; +import { performance } from 'node:perf_hooks'; +import { threadId } from 'node:worker_threads'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { + type MeasureCtxOptions, + type MeasureOptions, asOptions, errorToDevToolsProperties, errorToEntryMeta, errorToMarkerPayload, errorToTrackEntryPayload, + getNames, markerErrorPayload, markerPayload, + measureCtx, + mergeDevtoolsPayload, mergePropertiesWithOverwrite, + setupTracks, trackEntryErrorPayload, trackEntryPayload, } from './user-timing-extensibility-api-utils.js'; +import type { + EntryMeta, + TrackEntryPayload, + TrackMeta, +} from './user-timing-extensibility-api.type.js'; describe('mergePropertiesWithOverwrite', () => { it('should merge properties with overwrite', () => { @@ -33,15 +46,15 @@ describe('mergePropertiesWithOverwrite', () => { }); it('should handle undefined base properties', () => { - expect( - mergePropertiesWithOverwrite(undefined, [['key', 'value']]), - ).toStrictEqual([['key', 'value']]); + expect(mergePropertiesWithOverwrite([['key', 'value']])).toStrictEqual([ + ['key', 'value'], + ]); }); it('should handle undefined override properties', () => { - expect( - mergePropertiesWithOverwrite([['key', 'value']], undefined), - ).toStrictEqual([['key', 'value']]); + expect(mergePropertiesWithOverwrite([['key', 'value']])).toStrictEqual([ + ['key', 'value'], + ]); }); }); @@ -137,13 +150,12 @@ describe('trackEntryErrorPayload', () => { trackEntryErrorPayload({ track: 'Custom Track', trackGroup: 'Custom Group', - color: 'warning', tooltipText: 'warning occurred', properties: [['level', 'high']], }), ).toStrictEqual({ dataType: 'track-entry', - color: 'warning', + color: 'error', track: 'Custom Track', trackGroup: 'Custom Group', tooltipText: 'warning occurred', @@ -219,6 +231,16 @@ describe('errorToEntryMeta', () => { ], }); }); + + it('should handle error with undefined options', () => { + const result = errorToEntryMeta(new Error('test error'), undefined); + expect(result).toStrictEqual({ + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ], + }); + }); }); describe('errorToTrackEntryPayload', () => { @@ -304,6 +326,140 @@ describe('errorToMarkerPayload', () => { }); }); +describe('getNames', () => { + it('should generate names without prefix', () => { + const result = getNames('test'); + expect(result).toStrictEqual({ + startName: 'test:start', + endName: 'test:end', + measureName: 'test', + }); + }); + + it('should generate names with prefix', () => { + const result = getNames('operation', 'db'); + expect(result).toStrictEqual({ + startName: 'db:operation:start', + endName: 'db:operation:end', + measureName: 'db:operation', + }); + }); + + it('should handle empty prefix', () => { + const result = getNames('task', ''); + expect(result).toStrictEqual({ + startName: 'task:start', + endName: 'task:end', + measureName: 'task', + }); + }); +}); + +describe('mergeDevtoolsPayload', () => { + it('should return empty object when no payloads provided', () => { + expect(mergeDevtoolsPayload()).toStrictEqual({}); + }); + + it('should return the same payload when single payload provided', () => { + const payload: TrackEntryPayload = { + dataType: 'track-entry', + track: 'Test Track', + color: 'primary', + properties: [['key1', 'value1']], + }; + expect(mergeDevtoolsPayload(payload)).toBe(payload); + }); + + it('should merge multiple track entry payloads', () => { + const payload1: TrackEntryPayload = { + track: 'Test Track', + color: 'primary', + }; + const payload2: Partial = { + trackGroup: 'Test Group', + tooltipText: 'Test tooltip', + properties: [['key2', 'value2']], + }; + const payload3: EntryMeta = { + properties: [['key3', 'value3']], + }; + + expect(mergeDevtoolsPayload(payload1, payload2, payload3)).toStrictEqual({ + track: 'Test Track', + color: 'primary', + trackGroup: 'Test Group', + tooltipText: 'Test tooltip', + properties: [ + ['key2', 'value2'], + ['key3', 'value3'], + ], + }); + }); + + it('should merge multiple property payloads with overwrite behavior', () => { + const payload1: EntryMeta = { + properties: [['key1', 'value1']], + }; + const payload2: EntryMeta = { + properties: [ + ['key1', 'overwrite'], + ['key2', 'value2'], + ], + }; + + expect(mergeDevtoolsPayload(payload1, payload2)).toStrictEqual({ + properties: [ + ['key1', 'overwrite'], + ['key2', 'value2'], + ], + }); + }); + + it('should handle undefined and empty properties', () => { + const payload1: TrackMeta = { + track: 'Test', + }; + const payload2: EntryMeta = { + properties: undefined, + }; + + expect(mergeDevtoolsPayload(payload1, payload2)).toStrictEqual({ + track: 'Test', + properties: [['key1', 'value1']], + }); + }); +}); + +describe('setupTracks', () => { + it('should create track definitions with defaults as base', () => { + const defaults: TrackEntryPayload = { + track: 'Main Track', + color: 'primary', + trackGroup: 'My Group', + }; + const tracks = { + main: { track: 'Main Track' }, + secondary: { track: 'Secondary Track' }, + }; + + const result = setupTracks(defaults, tracks); + expect(result).toStrictEqual({ + main: { + track: 'Main Track', + color: 'primary', + trackGroup: 'My Group', + dataType: 'track-entry', + }, + secondary: { + track: 'Secondary Track', + color: 'primary', + trackGroup: 'My Group', + dataType: 'track-entry', + }, + }); + }); +}); + describe('asOptions', () => { it('should convert marker payload to mark options', () => { const devtools = markerPayload({ color: 'primary' }); @@ -327,3 +483,337 @@ describe('asOptions', () => { expect(asOptions(undefined)).toStrictEqual({ detail: {} }); }); }); + +describe('measureCtx', () => { + beforeEach(() => { + vi.spyOn(performance, 'mark').mockImplementation(vi.fn()); + vi.spyOn(performance, 'measure').mockImplementation(vi.fn()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('creates measure context and supports measurement', () => { + // Your code to profile + const codeToProfile = ({ fail }: { fail?: boolean } = {}) => { + if (fail) { + throw new Error('test error'); + } + return 1; + }; + + // Base global config - define once + const globalDefaults: MeasureCtxOptions = { + track: 'Global Track', + properties: [['Global:Config', `Process ID ${process.pid}`]], + color: 'primary-dark', + error: (error: unknown) => ({ + properties: [['Global:Error', `Custom Error Info: ${String(error)}`]], + }), + } as const; + + // Local overrides - define once + const localOverrides: MeasureOptions = { + color: 'primary', + properties: [['Runtime:Config', `Thread ID ${threadId}`]], + success: (result: unknown) => ({ + properties: [['Runtime:Result', String(result)]], + }), + error: (error: unknown) => ({ + properties: [ + ['Runtime:Error', `Stack Trace: ${String((error as Error)?.stack)}`], + ], + }), + } as const; + + const profilerCtx = measureCtx(globalDefaults); + const { start, success } = profilerCtx('utils', localOverrides); + + start(); // <= start mark + expect(performance.mark).toHaveBeenCalledWith('utils:start', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'primary', // local override wins + }, + }, + }); + + const result = codeToProfile(); + success(result); // <= end mark + measure (success) + expect(performance.mark).toHaveBeenLastCalledWith('utils:end', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'primary', + }, + }, + }); + expect(performance.measure).toHaveBeenCalledWith('utils', { + start: 'utils:start', + end: 'utils:end', + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'primary', + properties: [ + ['Global:Config', `Process ID ${process.pid}`], + ['Runtime:Config', `Thread ID ${threadId}`], + ['Runtime:Result', String(result)], + ], + }, + }, + }); + }); + + it('creates measure context with minimal config', () => { + expect(measureCtx({ track: 'Global Track' })('utils')).toStrictEqual({ + start: expect.any(Function), + success: expect.any(Function), + error: expect.any(Function), + }); + }); + + it('creates start mark with global defaults', () => { + const { start } = measureCtx({ + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'primary-dark', + properties: [['Global:Config', `Process ID ${process.pid}`]], + })('load-cfg'); + start(); + expect(performance.mark).toHaveBeenCalledWith('load-cfg:start', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'primary-dark', + // Marks do not have EntryMeta as hover/click is rare + }, + }, + }); + }); + + it('creates start mark with local overrides', () => { + const { start } = measureCtx({ + track: 'Global Track', + color: 'primary-dark', + })('load-cfg', { + color: 'primary', + properties: [['Runtime:Config', `Thread ID ${threadId}`]], + }); + start(); + expect(performance.mark).toHaveBeenCalledWith('load-cfg:start', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'primary', + // Marks do not have EntryMeta as hover/click is rare + }, + }, + }); + }); + + it('creates success mark and measure with global defaults', () => { + const { success } = measureCtx({ + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'primary-dark', + properties: [['Global:Config', `Process ID ${process.pid}`]], + })('load-cfg'); + success(1); + expect(performance.mark).toHaveBeenCalledWith('load-cfg:end', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'primary-dark', + // Marks do not have EntryMeta as hover/click is rare + }, + }, + }); + expect(performance.measure).toHaveBeenCalledWith('load-cfg', { + start: 'load-cfg:start', + end: 'load-cfg:end', + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'primary-dark', + properties: [['Global:Config', `Process ID ${process.pid}`]], + }, + }, + }); + }); + + it('creates success mark and measure with local overrides and success handler', () => { + const { success } = measureCtx({ + track: 'Global Track', + color: 'primary-dark', + properties: [['Global:Config', `Process ID ${process.pid}`]], + })('test', { + color: 'primary', + properties: [['Runtime:Config', `Thread ID ${threadId}`]], + success: (result: unknown) => ({ + properties: [['Runtime:Result', String(result)]], + }), + }); + success(1); + expect(performance.mark).toHaveBeenCalledWith('test:end', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'primary', + // Marks do not have EntryMeta as hover/click is rare + }, + }, + }); + expect(performance.measure).toHaveBeenCalledWith('test', { + start: 'test:start', + end: 'test:end', + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'primary', + properties: [ + ['Global:Config', `Process ID ${process.pid}`], + ['Runtime:Config', `Thread ID ${threadId}`], + ['Runtime:Result', '1'], + ], + }, + }, + }); + }); + + it('creates error mark and measure with global defaults', () => { + const error = new Error('test error'); + const { error: errorFn } = measureCtx({ + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'primary-dark', + properties: [['Global:Config', `Process ID ${process.pid}`]], + })('load-cfg'); + errorFn(error); + expect(performance.mark).toHaveBeenCalledWith('load-cfg:end', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'error', + // Marks do not have EntryMeta as hover/click is rare + }, + }, + }); + expect(performance.measure).toHaveBeenCalledWith('load-cfg', { + start: 'load-cfg:start', + end: 'load-cfg:end', + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + trackGroup: 'Global Track Group', + color: 'error', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ['Global:Config', `Process ID ${process.pid}`], + ], + }, + }, + }); + }); + + it('creates error mark and measure with local overrides and error handler', () => { + const error = new Error('test error'); + const { error: errorFn } = measureCtx({ + track: 'Global Track', + color: 'primary-dark', + properties: [['Global:Config', `Process ID ${process.pid}`]], + })('test', { + color: 'primary', + properties: [['Runtime:Config', `Thread ID ${threadId}`]], + error: (err: unknown) => ({ + properties: [ + ['Runtime:Error', `Stack Trace: ${String((err as Error)?.stack)}`], + ], + }), + }); + errorFn(error); + expect(performance.mark).toHaveBeenCalledWith('test:end', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'error', + // Marks do not have EntryMeta as hover/click is rare + }, + }, + }); + expect(performance.measure).toHaveBeenCalledWith('test', { + start: 'test:start', + end: 'test:end', + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'error', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + [ + 'Runtime:Error', + `Stack Trace: ${String((error as Error)?.stack)}`, + ], + ['Global:Config', `Process ID ${process.pid}`], + ['Runtime:Config', `Thread ID ${threadId}`], + ], + }, + }, + }); + }); + + it('creates error mark and measure with no error handlers', () => { + const error = new Error('test error'); + const { error: errorFn } = measureCtx({ + track: 'Global Track', + properties: [['Global:Config', `Process ID ${process.pid}`]], + })('test'); + errorFn(error); + expect(performance.mark).toHaveBeenCalledWith('test:end', { + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'error', + }, + }, + }); + expect(performance.measure).toHaveBeenCalledWith('test', { + start: 'test:start', + end: 'test:end', + detail: { + devtools: { + dataType: 'track-entry', + track: 'Global Track', + color: 'error', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ['Global:Config', `Process ID ${process.pid}`], + ], + }, + }, + }); + }); +}); From 4f1602b37cc8da09e699a844f03d8fd806d93563 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 12 Jan 2026 01:36:18 +0100 Subject: [PATCH 2/4] fix: fix lint --- .../user-timing-extensibility-api-utils.ts | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts index ed02634ba..befa8a03a 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -1,4 +1,5 @@ import { performance } from 'node:perf_hooks'; +import { objectToEntries } from './transform.js'; import type { DevToolsColor, DevToolsProperties, @@ -185,21 +186,18 @@ export type MergeResult< export function mergeDevtoolsPayload< const P extends readonly Partial[], >(...parts: P): MergeResult

{ - return parts.reduce( - (acc, cur) => ({ - ...acc, - ...cur, - ...(cur.properties || acc.properties - ? { - properties: mergePropertiesWithOverwrite( - acc.properties ?? [], - cur.properties ?? [], - ), - } - : {}), - }), - {} as Partial, - ) as MergeResult

; + return parts.reduce((acc, cur) => ({ + ...acc, + ...cur, + ...(cur.properties || acc.properties + ? { + properties: mergePropertiesWithOverwrite( + acc.properties ?? [], + cur.properties ?? [], + ), + } + : {}), + })) as MergeResult

; } export function mergeDevtoolsPayloadAction< @@ -220,14 +218,11 @@ export type ActionTrack = TrackEntryPayload & ActionColorPayload; export function setupTracks< const T extends Record>, const D extends ActionTrack, ->(defaults: D, tracks: T): Record { - return Object.entries(tracks).reduce( - (result, [key, track]) => ({ - ...result, - [key]: mergeDevtoolsPayload(defaults, track) as ActionTrack, - }), - {} as Record, - ); +>(defaults: D, tracks: T) { + return objectToEntries(tracks).reduce((result, [key, track]) => ({ + ...result, + [key]: mergeDevtoolsPayload(defaults, track), + })) as Record; } /** From 749cf546bba90533d8dc413d2fa21b738a9c00ed Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 12 Jan 2026 03:14:11 +0100 Subject: [PATCH 3/4] fix: unit tests --- .../user-timing-extensibility-api-utils.ts | 40 +++++++++++-------- ...iming-extensibility-api-utils.unit.test.ts | 4 +- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts index befa8a03a..0d36c0060 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -186,18 +186,21 @@ export type MergeResult< export function mergeDevtoolsPayload< const P extends readonly Partial[], >(...parts: P): MergeResult

{ - return parts.reduce((acc, cur) => ({ - ...acc, - ...cur, - ...(cur.properties || acc.properties - ? { - properties: mergePropertiesWithOverwrite( - acc.properties ?? [], - cur.properties ?? [], - ), - } - : {}), - })) as MergeResult

; + return parts.reduce( + (acc, cur) => ({ + ...acc, + ...cur, + ...(cur.properties || acc.properties + ? { + properties: mergePropertiesWithOverwrite( + acc.properties ?? [], + cur.properties ?? [], + ), + } + : {}), + }), + {}, + ) as MergeResult

; } export function mergeDevtoolsPayloadAction< @@ -219,10 +222,15 @@ export function setupTracks< const T extends Record>, const D extends ActionTrack, >(defaults: D, tracks: T) { - return objectToEntries(tracks).reduce((result, [key, track]) => ({ - ...result, - [key]: mergeDevtoolsPayload(defaults, track), - })) as Record; + return objectToEntries(tracks).reduce( + (result, [key, track]) => ({ + ...result, + [key]: mergeDevtoolsPayload(defaults, track, { + dataType: dataTypeTrackEntry, + }), + }), + {} as Record, + ) as Record; } /** diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts index 23c4a9a25..70fd3072c 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts @@ -367,7 +367,7 @@ describe('mergeDevtoolsPayload', () => { color: 'primary', properties: [['key1', 'value1']], }; - expect(mergeDevtoolsPayload(payload)).toBe(payload); + expect(mergeDevtoolsPayload(payload)).toStrictEqual(payload); }); it('should merge multiple track entry payloads', () => { @@ -425,7 +425,7 @@ describe('mergeDevtoolsPayload', () => { expect(mergeDevtoolsPayload(payload1, payload2)).toStrictEqual({ track: 'Test', - properties: [['key1', 'value1']], + properties: undefined, }); }); }); From e08c722bb472107fcd2acd82f6aa23471912bd13 Mon Sep 17 00:00:00 2001 From: John Doe Date: Mon, 12 Jan 2026 03:21:47 +0100 Subject: [PATCH 4/4] fix: lint --- packages/utils/src/lib/user-timing-extensibility-api-utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts index 0d36c0060..16c79d58b 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -229,6 +229,7 @@ export function setupTracks< dataType: dataTypeTrackEntry, }), }), + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions {} as Record, ) as Record; }