From 1fa6d3d1866bf773538a6638363923826c30c4f5 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 8 Jan 2026 00:43:17 +0100 Subject: [PATCH 01/12] refactor: add extensibility API helper --- packages/utils/package.json | 2 +- .../user-timing-extensibility-api-utils.ts | 165 ++++++++ ...iming-extensibility-api-utils.unit.test.ts | 355 ++++++++++++++++++ .../lib/user-timing-extensibility-api.type.ts | 97 +++++ 4 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 packages/utils/src/lib/user-timing-extensibility-api-utils.ts create mode 100644 packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts create mode 100644 packages/utils/src/lib/user-timing-extensibility-api.type.ts diff --git a/packages/utils/package.json b/packages/utils/package.json index b4da9d7c1..f3114d403 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ }, "type": "module", "engines": { - "node": ">=17.0.0" + "node": ">=18.2.0" }, "dependencies": { "@code-pushup/models": "0.101.1", diff --git a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts new file mode 100644 index 000000000..45816a21b --- /dev/null +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -0,0 +1,165 @@ +import type { + DevToolsColor, + DevToolsProperties, + EntryMeta, + MarkOptionsWithDevtools, + MarkerPayload, + MeasureOptionsWithDevtools, + TrackEntryPayload, +} from './user-timing-extensibility-api.type.js'; + +const dataTypeTrackEntry = 'track-entry'; + +export function objToPropertiesPayload( + object: Record, +): DevToolsProperties { + return Object.entries(object); +} + +export function mergePropertiesWithOverwrite( + baseProperties: DevToolsProperties | undefined, + overrideProperties?: DevToolsProperties | undefined, +): DevToolsProperties { + return objToPropertiesPayload({ + ...Object.fromEntries( + (baseProperties ?? []).map(([key, value]) => [key, String(value)]), + ), + ...Object.fromEntries( + (overrideProperties ?? []).map(([key, value]) => [key, String(value)]), + ), + }); +} + +export function markerPayload( + options?: Omit, +): MarkerPayload { + return { + dataType: 'marker', + ...options, + }; +} + +export function trackEntryPayload( + options: Omit, +): TrackEntryPayload { + const { track, ...rest } = options; + return { + dataType: dataTypeTrackEntry, + track, + ...rest, + }; +} + +export function markerErrorPayload( + options?: Omit, +): MarkerPayload { + return { + dataType: 'marker', + color: 'error' as T, + ...options, + } satisfies MarkerPayload; +} + +export function trackEntryErrorPayload< + T extends string, + C extends DevToolsColor, +>( + options: Omit & { + track: T; + color?: C; + }, +): TrackEntryPayload { + const { track, color = 'error', ...restOptions } = options; + return { + dataType: dataTypeTrackEntry, + color, + track, + ...restOptions, + } satisfies TrackEntryPayload; +} + +export function errorToDevToolsProperties(e: unknown): DevToolsProperties { + const name = e instanceof Error ? e.name : 'UnknownError'; + const message = e instanceof Error ? e.message : String(e); + return [ + ['Error Type', name], + ['Error Message', message], + ]; +} + +export function errorToEntryMeta( + e: unknown, + options?: { + tooltipText?: string; + properties?: DevToolsProperties; + }, +): EntryMeta { + const { properties, tooltipText } = options ?? {}; + const props = mergePropertiesWithOverwrite( + errorToDevToolsProperties(e), + properties, + ); + return { + properties: props, + ...(tooltipText ? { tooltipText } : {}), + }; +} + +export function errorToTrackEntryPayload( + error: unknown, + detail: Omit & { + track: T; + }, +) { + const { properties, tooltipText, ...trackPayload } = detail; + return { + dataType: dataTypeTrackEntry, + color: 'error' as const, + ...trackPayload, + ...errorToEntryMeta(error, { + properties, + tooltipText, + }), + } satisfies TrackEntryPayload; +} + +export function errorToMarkerPayload( + error: unknown, + detail?: Omit, +): MarkerPayload { + const { properties, tooltipText } = detail ?? {}; + return { + dataType: 'marker', + color: 'error' as T, + ...errorToEntryMeta(error, { + properties, + tooltipText, + }), + } satisfies MarkerPayload; +} + +/** + * + * @example + * profiler.mark('mark', asOptions({ + * properties: [ + * ['str', 'This is a detail property'], + * ['num', 42], + * ['object', { str: '42', num: 42 }], + * ['array', [42, 42, 42]], + * ], + * })); + */ +export function asOptions( + devtools: MarkerPayload, +): Pick; +export function asOptions( + devtools: TrackEntryPayload, +): Pick; +export function asOptions( + devtools: MarkerPayload | TrackEntryPayload, +): + | Pick + | Pick { + return devtools ? { detail: { devtools } } : { detail: {} }; +} 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 new file mode 100644 index 000000000..dc5bc99c5 --- /dev/null +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.unit.test.ts @@ -0,0 +1,355 @@ +import { describe, expect, it } from 'vitest'; +import { + asOptions, + errorToDevToolsProperties, + errorToEntryMeta, + errorToMarkerPayload, + errorToTrackEntryPayload, + markerErrorPayload, + markerPayload, + mergePropertiesWithOverwrite, + objToPropertiesPayload, + trackEntryErrorPayload, + trackEntryPayload, +} from './user-timing-extensibility-api-utils.js'; + +describe('objToPropertiesPayload', () => { + it('should convert object to properties array', () => { + expect( + objToPropertiesPayload({ key: 'value', number: 42, bool: true }), + ).toStrictEqual([ + ['key', 'value'], + ['number', 42], + ['bool', true], + ]); + }); + + it('should keep undefined values', () => { + expect( + objToPropertiesPayload({ key: 'value', undef: undefined }), + ).toStrictEqual([ + ['key', 'value'], + ['undef', undefined], + ]); + }); + + it('should handle empty object', () => { + expect(objToPropertiesPayload({})).toStrictEqual([]); + }); +}); + +describe('mergePropertiesWithOverwrite', () => { + it('should merge properties with overwrite', () => { + expect( + mergePropertiesWithOverwrite( + [ + ['key1', 'value1'], + ['key2', 'value2'], + ], + [ + ['key2', 'overwritten'], + ['key3', 'value3'], + ], + ), + ).toStrictEqual([ + ['key1', 'value1'], + ['key2', 'overwritten'], + ['key3', 'value3'], + ]); + }); + + it('should handle undefined base properties', () => { + expect( + mergePropertiesWithOverwrite(undefined, [['key', 'value']]), + ).toStrictEqual([['key', 'value']]); + }); + + it('should handle undefined override properties', () => { + expect( + mergePropertiesWithOverwrite([['key', 'value']], undefined), + ).toStrictEqual([['key', 'value']]); + }); +}); + +describe('markerPayload', () => { + it('should create marker payload with defaults', () => { + expect(markerPayload()).toStrictEqual({ + dataType: 'marker', + }); + }); + + it('should create marker payload with options', () => { + expect( + markerPayload({ + color: 'primary', + tooltipText: 'test tooltip', + properties: [['key', 'value']], + }), + ).toStrictEqual({ + dataType: 'marker', + color: 'primary', + tooltipText: 'test tooltip', + properties: [['key', 'value']], + }); + }); +}); + +describe('trackEntryPayload', () => { + it('should create track entry payload with defaults', () => { + expect( + trackEntryPayload({ + track: 'Main', + }), + ).toStrictEqual({ + dataType: 'track-entry', + track: 'Main', + }); + }); + + it('should create track entry payload with options', () => { + expect( + trackEntryPayload({ + track: 'Custom Track', + trackGroup: 'Custom Group', + color: 'primary', + tooltipText: 'test', + properties: [['key', 'value']], + }), + ).toStrictEqual({ + dataType: 'track-entry', + track: 'Custom Track', + trackGroup: 'Custom Group', + color: 'primary', + tooltipText: 'test', + properties: [['key', 'value']], + }); + }); +}); + +describe('markerErrorPayload', () => { + it('should create error marker payload with default color', () => { + expect(markerErrorPayload()).toStrictEqual({ + dataType: 'marker', + color: 'error', + }); + }); + + it('should create error marker payload with custom options', () => { + expect( + markerErrorPayload({ + tooltipText: 'error occurred', + properties: [['code', '500']], + }), + ).toStrictEqual({ + dataType: 'marker', + color: 'error', + tooltipText: 'error occurred', + properties: [['code', '500']], + }); + }); +}); + +describe('trackEntryErrorPayload', () => { + it('should create error track entry payload with defaults', () => { + expect(trackEntryErrorPayload({ track: 'Test' })).toStrictEqual({ + dataType: 'track-entry', + color: 'error', + track: 'Test', + }); + }); + + it('should create error track entry payload with all options', () => { + expect( + trackEntryErrorPayload({ + track: 'Custom Track', + trackGroup: 'Custom Group', + color: 'warning', + tooltipText: 'warning occurred', + properties: [['level', 'high']], + }), + ).toStrictEqual({ + dataType: 'track-entry', + color: 'warning', + track: 'Custom Track', + trackGroup: 'Custom Group', + tooltipText: 'warning occurred', + properties: [['level', 'high']], + }); + }); +}); + +describe('errorToDevToolsProperties', () => { + it('should convert Error to properties', () => { + const error = new Error('test message'); + expect(errorToDevToolsProperties(error)).toStrictEqual([ + ['Error Type', 'Error'], + ['Error Message', 'test message'], + ]); + }); + + it('should convert non-Error to properties', () => { + expect(errorToDevToolsProperties('string error')).toStrictEqual([ + ['Error Type', 'UnknownError'], + ['Error Message', 'string error'], + ]); + }); + + it('should handle null', () => { + expect(errorToDevToolsProperties(null)).toStrictEqual([ + ['Error Type', 'UnknownError'], + ['Error Message', 'null'], + ]); + }); + + it('should handle undefined', () => { + expect(errorToDevToolsProperties(undefined)).toStrictEqual([ + ['Error Type', 'UnknownError'], + ['Error Message', 'undefined'], + ]); + }); +}); + +describe('errorToEntryMeta', () => { + it('should convert error to entry meta with defaults', () => { + const result = errorToEntryMeta(new Error('test error')); + expect(result).toStrictEqual({ + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ], + }); + }); + + it('should convert error to entry meta with custom options', () => { + const result = errorToEntryMeta(new Error('test error'), { + tooltipText: 'Custom tooltip', + properties: [['custom', 'value']], + }); + expect(result).toStrictEqual({ + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ['custom', 'value'], + ], + tooltipText: 'Custom tooltip', + }); + }); + + it('should handle error without properties', () => { + const error = new Error('test error'); + const result = errorToEntryMeta(error, { properties: [] }); + expect(result).toStrictEqual({ + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ], + }); + }); +}); + +describe('errorToTrackEntryPayload', () => { + it('should convert error to track entry payload', () => { + const error = new Error('test error'); + const result = errorToTrackEntryPayload(error, { track: 'Test' }); + expect(result).toStrictEqual({ + dataType: 'track-entry', + color: 'error', + track: 'Test', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ], + }); + }); + + it('should convert error to track entry payload with custom properties', () => { + const error = new Error('test error'); + const result = errorToTrackEntryPayload(error, { + track: 'Test', + tooltipText: 'Custom tooltip', + properties: [['custom', 'value']], + }); + expect(result).toStrictEqual({ + dataType: 'track-entry', + color: 'error', + track: 'Test', + tooltipText: 'Custom tooltip', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ['custom', 'value'], + ], + }); + }); + + it('should convert error to track entry payload with undefined detail', () => { + const error = new Error('test error'); + const result = errorToTrackEntryPayload(error, { track: 'Test' }); + expect(result).toStrictEqual({ + dataType: 'track-entry', + track: 'Test', + color: 'error', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ], + }); + }); +}); + +describe('errorToMarkerPayload', () => { + it('should convert error to marker payload with defaults', () => { + const error = new Error('test error'); + const result = errorToMarkerPayload(error); + expect(result).toStrictEqual({ + dataType: 'marker', + color: 'error', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ], + }); + }); + + it('should convert error to marker payload with custom options', () => { + const error = new Error('test error'); + const result = errorToMarkerPayload(error, { + tooltipText: 'Custom tooltip', + properties: [['custom', 'value']], + }); + expect(result).toStrictEqual({ + dataType: 'marker', + color: 'error', + tooltipText: 'Custom tooltip', + properties: [ + ['Error Type', 'Error'], + ['Error Message', 'test error'], + ['custom', 'value'], + ], + }); + }); +}); + +describe('asOptions', () => { + it('should convert marker payload to mark options', () => { + const devtools = markerPayload({ color: 'primary' }); + expect(asOptions(devtools)).toStrictEqual({ + detail: { devtools }, + }); + }); + + it('should convert track entry payload to measure options', () => { + const devtools = trackEntryPayload({ track: 'Custom' }); + expect(asOptions(devtools)).toStrictEqual({ + detail: { devtools }, + }); + }); + + it('should return empty detail for null input', () => { + expect(asOptions(null as any)).toStrictEqual({ detail: {} }); + }); + + it('should return empty detail for undefined input', () => { + expect(asOptions(undefined as any)).toStrictEqual({ detail: {} }); + }); +}); diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.ts new file mode 100644 index 000000000..9b1821758 --- /dev/null +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.ts @@ -0,0 +1,97 @@ +import type { + MarkOptions, + MeasureOptions, + PerformanceMark, + PerformanceMeasure, +} from 'node:perf_hooks'; + +export type DevToolsFeedbackColor = 'error' | 'warning'; + +export type DevToolsActionColor = + | 'primary' + | 'primary-dark' + | 'primary-light' + | 'secondary' + | 'secondary-dark' + | 'secondary-light' + | 'tertiary' + | 'tertiary-dark' + | 'tertiary-light'; + +export type DevToolsColor = DevToolsFeedbackColor | DevToolsActionColor; + +export type DevToolsDataType = 'marker' | 'track-entry'; + +export type DevToolsProperties = [ + key: string, + value: string | number | boolean | object | undefined, +][]; + +export type EntryMeta = { + tooltipText?: string; // Short description for tooltip on hover + properties?: DevToolsProperties; // Key-value pairs for detailed view on click +}; + +export type TrackStyle = { + color?: DevToolsColor; // rendered color of background and border, defaults to "primary" +}; + +export type TrackMeta = { + track: string; // Name of the custom track + trackGroup?: string; // Group for organizing tracks +}; + +export type ExtensionTrackBase = EntryMeta & TrackStyle; + +export type TrackEntryPayload = { + dataType?: 'track-entry'; // Defaults to "track-entry" +} & ExtensionTrackBase & + TrackMeta; + +export type MarkerPayload = { + dataType: 'marker'; // Identifies as a marker +} & ExtensionTrackBase; + +export type WithErrorColor = Omit< + T, + 'color' +> & { + color: 'error'; +}; +export type WithDevToolsPayload = { + devtools?: T; +}; +export type DevToolsPayload = TrackEntryPayload | MarkerPayload; +export type UserTimingDetailMeasurePayload = + WithDevToolsPayload & { + [k: string]: unknown; + }; + +export type UserTimingDetailMarkPayload = WithDevToolsPayload< + TrackEntryPayload | MarkerPayload +> & { + [k: string]: unknown; +}; + +export type MarkOptionsWithDevtools = { + detail?: WithDevToolsPayload; +} & Omit; + +export type MeasureOptionsWithDevtools = { + detail?: WithDevToolsPayload; +} & Omit; + +// Mimics performance.mark/measure API with devtools extensions +export type NativePerformanceAPI = { + mark: (name: string, options?: MarkOptionsWithDevtools) => PerformanceMark; + + measure: (( + name: string, + options: MeasureOptionsWithDevtools, + ) => PerformanceMeasure) & + (( + name: string, + startMark?: string, + endMark?: string, + ) => PerformanceMeasure); +}; From 44e22df32cdf006dc45fd7a03b54582ddb51a040 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 8 Jan 2026 01:06:31 +0100 Subject: [PATCH 02/12] refactor: add type tests --- ...timing-extensibility-api.type.unit.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/utils/src/lib/user-timing-extensibility-api.type.unit.test.ts diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.unit.test.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.unit.test.ts new file mode 100644 index 000000000..5f3773c84 --- /dev/null +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.unit.test.ts @@ -0,0 +1,44 @@ +import { describe, expectTypeOf, it } from 'vitest'; +import type { + DevToolsColor, + MarkerPayload, + TrackEntryPayload, + WithDevToolsPayload, + WithErrorColor, +} from './user-timing-extensibility-api.type.js'; + +describe('TrackEntryPayload', () => { + it('TrackEntryPayload extends ExtensionTrackBase and TrackMeta', () => { + expectTypeOf<{ + dataType?: 'track-entry'; + track: string; + }>().toMatchTypeOf(); + }); +}); + +describe('MarkerPayload', () => { + it('MarkerPayload extends ExtensionTrackBase with required dataType', () => { + expectTypeOf<{ + dataType: 'marker'; + }>().toMatchTypeOf(); + }); +}); + +describe('WithErrorColor', () => { + it('WithErrorColor removes optional color and adds required error color', () => { + expectTypeOf<{ + color: 'error'; + otherProp: string; + }>().toMatchTypeOf< + WithErrorColor<{ color?: DevToolsColor; otherProp: string }> + >(); + }); +}); + +describe('WithDevToolsPayload', () => { + it('WithDevToolsPayload makes devtools optional', () => { + expectTypeOf<{ + devtools?: TrackEntryPayload; + }>().toMatchTypeOf>(); + }); +}); From b979e1d5b34b95aed483a8caae9277777b6172d9 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 8 Jan 2026 01:08:49 +0100 Subject: [PATCH 03/12] refactor: remove unused type --- .../src/lib/user-timing-extensibility-api.type.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.ts index 9b1821758..a95abff55 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api.type.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.ts @@ -80,18 +80,3 @@ export type MarkOptionsWithDevtools = { export type MeasureOptionsWithDevtools = { detail?: WithDevToolsPayload; } & Omit; - -// Mimics performance.mark/measure API with devtools extensions -export type NativePerformanceAPI = { - mark: (name: string, options?: MarkOptionsWithDevtools) => PerformanceMark; - - measure: (( - name: string, - options: MeasureOptionsWithDevtools, - ) => PerformanceMeasure) & - (( - name: string, - startMark?: string, - endMark?: string, - ) => PerformanceMeasure); -}; From a4ef406d749f3610d51840c1582807e72aa4232a Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 8 Jan 2026 01:13:04 +0100 Subject: [PATCH 04/12] refactor: rename tests file --- ....test.ts => user-timing-extensibility-api.type.test.ts} | 0 .../utils/src/lib/user-timing-extensibility-api.type.ts | 7 +------ 2 files changed, 1 insertion(+), 6 deletions(-) rename packages/utils/src/lib/{user-timing-extensibility-api.type.unit.test.ts => user-timing-extensibility-api.type.test.ts} (100%) diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.unit.test.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.test.ts similarity index 100% rename from packages/utils/src/lib/user-timing-extensibility-api.type.unit.test.ts rename to packages/utils/src/lib/user-timing-extensibility-api.type.test.ts diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.ts index a95abff55..a9831d2e9 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api.type.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.ts @@ -1,9 +1,4 @@ -import type { - MarkOptions, - MeasureOptions, - PerformanceMark, - PerformanceMeasure, -} from 'node:perf_hooks'; +import type { MarkOptions, MeasureOptions } from 'node:perf_hooks'; export type DevToolsFeedbackColor = 'error' | 'warning'; From 24b5959e2be2e156475111f637bbf5d76babf68b Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 8 Jan 2026 01:18:53 +0100 Subject: [PATCH 05/12] refactor: wip --- .../utils/src/lib/user-timing-extensibility-api-utils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 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 45816a21b..1fdbb0270 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -9,6 +9,7 @@ import type { } from './user-timing-extensibility-api.type.js'; const dataTypeTrackEntry = 'track-entry'; +const dataTypeMarker = 'marker'; export function objToPropertiesPayload( object: Record, @@ -34,7 +35,7 @@ export function markerPayload( options?: Omit, ): MarkerPayload { return { - dataType: 'marker', + dataType: dataTypeMarker, ...options, }; } @@ -54,7 +55,7 @@ export function markerErrorPayload( options?: Omit, ): MarkerPayload { return { - dataType: 'marker', + dataType: dataTypeMarker, color: 'error' as T, ...options, } satisfies MarkerPayload; @@ -129,7 +130,7 @@ export function errorToMarkerPayload( ): MarkerPayload { const { properties, tooltipText } = detail ?? {}; return { - dataType: 'marker', + dataType: dataTypeMarker, color: 'error' as T, ...errorToEntryMeta(error, { properties, From a9088cdcfb479507d0c7744d4721de54a3f66902 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 9 Jan 2026 17:12:07 +0100 Subject: [PATCH 06/12] refactor: wip --- .../user-timing-extensibility-api-utils.ts | 92 ++++++------ ...iming-extensibility-api-utils.unit.test.ts | 26 ---- ...user-timing-extensibility-api.type.test.ts | 44 ------ .../lib/user-timing-extensibility-api.type.ts | 131 ++++++++++++++---- 4 files changed, 148 insertions(+), 145 deletions(-) delete mode 100644 packages/utils/src/lib/user-timing-extensibility-api.type.test.ts 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 1fdbb0270..5c5be5b8d 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 { objectFromEntries } from '@code-pushup/utils'; import type { DevToolsColor, DevToolsProperties, @@ -6,54 +7,43 @@ import type { MarkerPayload, MeasureOptionsWithDevtools, TrackEntryPayload, + WithDevToolsPayload, } from './user-timing-extensibility-api.type.js'; const dataTypeTrackEntry = 'track-entry'; const dataTypeMarker = 'marker'; -export function objToPropertiesPayload( - object: Record, -): DevToolsProperties { - return Object.entries(object); -} - export function mergePropertiesWithOverwrite( baseProperties: DevToolsProperties | undefined, overrideProperties?: DevToolsProperties | undefined, -): DevToolsProperties { - return objToPropertiesPayload({ - ...Object.fromEntries( - (baseProperties ?? []).map(([key, value]) => [key, String(value)]), - ), - ...Object.fromEntries( - (overrideProperties ?? []).map(([key, value]) => [key, String(value)]), - ), - }); +) { + return Object.entries({ + ...objectFromEntries(baseProperties ?? []), + ...objectFromEntries(overrideProperties ?? []), + }) satisfies DevToolsProperties; } -export function markerPayload( - options?: Omit, -): MarkerPayload { +export function markerPayload(options?: Omit) { return { dataType: dataTypeMarker, ...options, - }; + } satisfies MarkerPayload; } export function trackEntryPayload( options: Omit, -): TrackEntryPayload { +) { const { track, ...rest } = options; return { dataType: dataTypeTrackEntry, track, ...rest, - }; + } satisfies TrackEntryPayload; } export function markerErrorPayload( options?: Omit, -): MarkerPayload { +) { return { dataType: dataTypeMarker, color: 'error' as T, @@ -69,8 +59,8 @@ export function trackEntryErrorPayload< track: T; color?: C; }, -): TrackEntryPayload { - const { track, color = 'error', ...restOptions } = options; +) { + const { track, color = 'error' as const, ...restOptions } = options; return { dataType: dataTypeTrackEntry, color, @@ -79,13 +69,13 @@ export function trackEntryErrorPayload< } satisfies TrackEntryPayload; } -export function errorToDevToolsProperties(e: unknown): DevToolsProperties { +export function errorToDevToolsProperties(e: unknown) { const name = e instanceof Error ? e.name : 'UnknownError'; const message = e instanceof Error ? e.message : String(e); return [ - ['Error Type', name], - ['Error Message', message], - ]; + ['Error Type' as const, name], + ['Error Message' as const, message], + ] satisfies DevToolsProperties; } export function errorToEntryMeta( @@ -94,7 +84,7 @@ export function errorToEntryMeta( tooltipText?: string; properties?: DevToolsProperties; }, -): EntryMeta { +) { const { properties, tooltipText } = options ?? {}; const props = mergePropertiesWithOverwrite( errorToDevToolsProperties(e), @@ -103,7 +93,7 @@ export function errorToEntryMeta( return { properties: props, ...(tooltipText ? { tooltipText } : {}), - }; + } satisfies EntryMeta; } export function errorToTrackEntryPayload( @@ -124,14 +114,14 @@ export function errorToTrackEntryPayload( } satisfies TrackEntryPayload; } -export function errorToMarkerPayload( +export function errorToMarkerPayload( error: unknown, detail?: Omit, -): MarkerPayload { +) { const { properties, tooltipText } = detail ?? {}; return { dataType: dataTypeMarker, - color: 'error' as T, + color: 'error' as const, ...errorToEntryMeta(error, { properties, tooltipText, @@ -140,27 +130,33 @@ export function errorToMarkerPayload( } /** + * 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'], - * ['num', 42], - * ['object', { str: '42', num: 42 }], - * ['array', [42, 42, 42]], + * ['str', 'This is a detail property'] * ], * })); */ -export function asOptions( - devtools: MarkerPayload, -): Pick; -export function asOptions( - devtools: TrackEntryPayload, -): Pick; -export function asOptions( - devtools: MarkerPayload | TrackEntryPayload, -): - | Pick - | Pick { +export function asOptions( + devtools: T, +): MarkOptionsWithDevtools; +export function asOptions( + devtools: T, +): MeasureOptionsWithDevtools; +export function asOptions( + devtools?: T, +): { + detail?: WithDevToolsPayload; +} { return devtools ? { detail: { devtools } } : { detail: {} }; } + +const o = asOptions({ + dataType: 'marker', + color: 'error', +}); 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 dc5bc99c5..a31dc0c94 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 @@ -8,36 +8,10 @@ import { markerErrorPayload, markerPayload, mergePropertiesWithOverwrite, - objToPropertiesPayload, trackEntryErrorPayload, trackEntryPayload, } from './user-timing-extensibility-api-utils.js'; -describe('objToPropertiesPayload', () => { - it('should convert object to properties array', () => { - expect( - objToPropertiesPayload({ key: 'value', number: 42, bool: true }), - ).toStrictEqual([ - ['key', 'value'], - ['number', 42], - ['bool', true], - ]); - }); - - it('should keep undefined values', () => { - expect( - objToPropertiesPayload({ key: 'value', undef: undefined }), - ).toStrictEqual([ - ['key', 'value'], - ['undef', undefined], - ]); - }); - - it('should handle empty object', () => { - expect(objToPropertiesPayload({})).toStrictEqual([]); - }); -}); - describe('mergePropertiesWithOverwrite', () => { it('should merge properties with overwrite', () => { expect( diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.test.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.test.ts deleted file mode 100644 index 5f3773c84..000000000 --- a/packages/utils/src/lib/user-timing-extensibility-api.type.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expectTypeOf, it } from 'vitest'; -import type { - DevToolsColor, - MarkerPayload, - TrackEntryPayload, - WithDevToolsPayload, - WithErrorColor, -} from './user-timing-extensibility-api.type.js'; - -describe('TrackEntryPayload', () => { - it('TrackEntryPayload extends ExtensionTrackBase and TrackMeta', () => { - expectTypeOf<{ - dataType?: 'track-entry'; - track: string; - }>().toMatchTypeOf(); - }); -}); - -describe('MarkerPayload', () => { - it('MarkerPayload extends ExtensionTrackBase with required dataType', () => { - expectTypeOf<{ - dataType: 'marker'; - }>().toMatchTypeOf(); - }); -}); - -describe('WithErrorColor', () => { - it('WithErrorColor removes optional color and adds required error color', () => { - expectTypeOf<{ - color: 'error'; - otherProp: string; - }>().toMatchTypeOf< - WithErrorColor<{ color?: DevToolsColor; otherProp: string }> - >(); - }); -}); - -describe('WithDevToolsPayload', () => { - it('WithDevToolsPayload makes devtools optional', () => { - expectTypeOf<{ - devtools?: TrackEntryPayload; - }>().toMatchTypeOf>(); - }); -}); diff --git a/packages/utils/src/lib/user-timing-extensibility-api.type.ts b/packages/utils/src/lib/user-timing-extensibility-api.type.ts index a9831d2e9..0e75be77b 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api.type.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api.type.ts @@ -1,7 +1,28 @@ import type { MarkOptions, MeasureOptions } from 'node:perf_hooks'; +/** + * Color options for feedback states in DevTools. + * Used for error and warning states on marker and track entries. + * @example + * - 'error' - red + * - 'warning' - yellow + */ export type DevToolsFeedbackColor = 'error' | 'warning'; +/** + * Color options for action states in DevTools. + * Used for valid states on marker and track entries. + * @example + * - 'primary' - blue (default) + * - 'primary-dark' - dark blue + * - 'primary-light' - light blue + * - 'secondary' - purple + * - 'secondary-dark' - dark purple + * - 'secondary-light' - light purple + * - 'tertiary' - green + * - 'tertiary-dark' - dark green + * - 'tertiary-light' - light green + */ export type DevToolsActionColor = | 'primary' | 'primary-dark' @@ -13,65 +34,121 @@ export type DevToolsActionColor = | 'tertiary-dark' | 'tertiary-light'; +/** + * Union type of all available DevTools color options. + */ export type DevToolsColor = DevToolsFeedbackColor | DevToolsActionColor; -export type DevToolsDataType = 'marker' | 'track-entry'; - +/** + * Array of key-value pairs for detailed DevTools properties. + */ export type DevToolsProperties = [ key: string, value: string | number | boolean | object | undefined, ][]; +/** + * EntryMeta is used to store metadata about a track entry. + * @property {string} [tooltipText] - Short description for tooltip on hover + * @property {DevToolsProperties} [properties] - Key-value pairs for detailed view on click. + * It provides better styling of values including features like automatic links rendering. + */ export type EntryMeta = { - tooltipText?: string; // Short description for tooltip on hover - properties?: DevToolsProperties; // Key-value pairs for detailed view on click + tooltipText?: string; + properties?: DevToolsProperties; }; +/** + * Styling options for track entries in DevTools. + * @property {DevToolsColor} [color] - rendered color of background and border, defaults to "primary" + */ export type TrackStyle = { - color?: DevToolsColor; // rendered color of background and border, defaults to "primary" + color?: DevToolsColor; }; +/** + * Metadata for organizing track entries in DevTools. + * @property {string} track - Name of the custom track + * @property {string} [trackGroup] - Group for organizing tracks + */ export type TrackMeta = { - track: string; // Name of the custom track - trackGroup?: string; // Group for organizing tracks + track: string; + trackGroup?: string; }; -export type ExtensionTrackBase = EntryMeta & TrackStyle; +/** + * Base type combining entry metadata and styling for DevTools tracks. + */ +export type TrackBase = EntryMeta & TrackStyle; +/** + * Payload for track entries in DevTools Performance panel. + * @property {'track-entry'} [dataType] - Defaults to "track-entry" + * + * This type is visible in a custom track with name defined in `track` property. + */ export type TrackEntryPayload = { - dataType?: 'track-entry'; // Defaults to "track-entry" -} & ExtensionTrackBase & + dataType?: 'track-entry'; +} & TrackBase & TrackMeta; +/** + * Payload for marker entries in DevTools Performance panel. + * @property {'marker'} dataType - Identifies as a marker + * This type is visible as a marker on top of all tracks and in addition creates a vertical line spanning all lanes in the performance palen. + */ export type MarkerPayload = { - dataType: 'marker'; // Identifies as a marker -} & ExtensionTrackBase; + dataType: 'marker'; +} & TrackBase; +/** + * Utility type that forces a color property to be 'error'. + */ export type WithErrorColor = Omit< T, 'color' > & { color: 'error'; }; +/** + * Utility type that adds an optional devtools payload property. + */ export type WithDevToolsPayload = { devtools?: T; }; -export type DevToolsPayload = TrackEntryPayload | MarkerPayload; -export type UserTimingDetailMeasurePayload = - WithDevToolsPayload & { - [k: string]: unknown; - }; - -export type UserTimingDetailMarkPayload = WithDevToolsPayload< - TrackEntryPayload | MarkerPayload -> & { - [k: string]: unknown; -}; -export type MarkOptionsWithDevtools = { - detail?: WithDevToolsPayload; +/** + * Extended MarkOptions that supports DevTools payload in detail. + * @example + * const options: MarkOptionsWithDevtools = { + * detail: { + * devtools: { + * dataType: 'marker', + * color: 'error', + * }, + * }, + * } + * profiler.mark('start-program', options); + */ +export type MarkOptionsWithDevtools< + T extends TrackEntryPayload | MarkerPayload, +> = { + detail?: WithDevToolsPayload; } & Omit; -export type MeasureOptionsWithDevtools = { - detail?: WithDevToolsPayload; +/** + * Extended MeasureOptions that supports DevTools payload in detail. + * @example + * const options: MeasureOptionsWithDevtools = { + * detail: { + * devtools: { + * dataType: 'track-entry', + * color: 'primary', + * } + * } + * } + * profiler.measure('load-program', 'start-program', 'end-program', options); + */ +export type MeasureOptionsWithDevtools = { + detail?: WithDevToolsPayload; } & Omit; From 815a61ebeeb9f0cb62ce6e7644c35a9f942bbb1c Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 9 Jan 2026 17:26:02 +0100 Subject: [PATCH 07/12] refactor: fix lint --- packages/utils/package.json | 2 +- .../utils/src/lib/user-timing-extensibility-api-utils.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index f3114d403..ddf9d14f6 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ }, "type": "module", "engines": { - "node": ">=18.2.0" + "node": "17.0.0" }, "dependencies": { "@code-pushup/models": "0.101.1", 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 5c5be5b8d..cb20723ae 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,4 @@ -import { objectFromEntries } from '@code-pushup/utils'; +import { objectFromEntries } from './transform.js'; import type { DevToolsColor, DevToolsProperties, @@ -155,8 +155,3 @@ export function asOptions( } { return devtools ? { detail: { devtools } } : { detail: {} }; } - -const o = asOptions({ - dataType: 'marker', - color: 'error', -}); From 1514523e6b4da70dc32d66c8c89ee852f5bde395 Mon Sep 17 00:00:00 2001 From: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> Date: Fri, 9 Jan 2026 21:41:00 +0100 Subject: [PATCH 08/12] Update packages/utils/src/lib/user-timing-extensibility-api-utils.ts Co-authored-by: Hanna Skryl <80118140+hanna-skryl@users.noreply.github.com> --- .../utils/src/lib/user-timing-extensibility-api-utils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 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 cb20723ae..2813a95b8 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -17,10 +17,9 @@ export function mergePropertiesWithOverwrite( baseProperties: DevToolsProperties | undefined, overrideProperties?: DevToolsProperties | undefined, ) { - return Object.entries({ - ...objectFromEntries(baseProperties ?? []), - ...objectFromEntries(overrideProperties ?? []), - }) satisfies DevToolsProperties; + return [ + ...new Map([...(baseProperties ?? []), ...(overrideProperties ?? [])]), + ] satisfies DevToolsProperties; } export function markerPayload(options?: Omit) { From ebf934a050c15ed5123d6d149a7ce845b58a211b Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 9 Jan 2026 21:45:55 +0100 Subject: [PATCH 09/12] refactor: adjust types --- .../utils/src/lib/user-timing-extensibility-api-utils.ts | 8 ++++---- .../lib/user-timing-extensibility-api-utils.unit.test.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 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 2813a95b8..6ac286571 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -142,15 +142,15 @@ export function errorToMarkerPayload( * })); */ export function asOptions( - devtools: T, + devtools?: T | null, ): MarkOptionsWithDevtools; export function asOptions( - devtools: T, + devtools?: T | null, ): MeasureOptionsWithDevtools; export function asOptions( - devtools?: T, + devtools?: T | null, ): { detail?: WithDevToolsPayload; } { - return devtools ? { detail: { devtools } } : { detail: {} }; + return devtools != null ? { detail: { devtools } } : { detail: {} }; } 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 a31dc0c94..440f74787 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 @@ -320,10 +320,10 @@ describe('asOptions', () => { }); it('should return empty detail for null input', () => { - expect(asOptions(null as any)).toStrictEqual({ detail: {} }); + expect(asOptions(null)).toStrictEqual({ detail: {} }); }); it('should return empty detail for undefined input', () => { - expect(asOptions(undefined as any)).toStrictEqual({ detail: {} }); + expect(asOptions(undefined)).toStrictEqual({ detail: {} }); }); }); From 0f61e62404bd988b6a03097085d903b2a24911c1 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 9 Jan 2026 21:46:52 +0100 Subject: [PATCH 10/12] refactor: revert --- packages/utils/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/package.json b/packages/utils/package.json index ddf9d14f6..b4da9d7c1 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ }, "type": "module", "engines": { - "node": "17.0.0" + "node": ">=17.0.0" }, "dependencies": { "@code-pushup/models": "0.101.1", From 4a422703e62cd9a54acab6d330bbfd4c5656f690 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 9 Jan 2026 21:49:33 +0100 Subject: [PATCH 11/12] refactor: fix lint --- packages/utils/src/lib/user-timing-extensibility-api-utils.ts | 1 - 1 file changed, 1 deletion(-) 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 6ac286571..983ca897a 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,3 @@ -import { objectFromEntries } from './transform.js'; import type { DevToolsColor, DevToolsProperties, From 26358b6ab0d2f8d6c0ad80ce88b5705bf3f2a960 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 9 Jan 2026 21:53:09 +0100 Subject: [PATCH 12/12] refactor: fix lint --- packages/utils/src/lib/user-timing-extensibility-api-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 983ca897a..78a8e6d78 100644 --- a/packages/utils/src/lib/user-timing-extensibility-api-utils.ts +++ b/packages/utils/src/lib/user-timing-extensibility-api-utils.ts @@ -151,5 +151,5 @@ export function asOptions( ): { detail?: WithDevToolsPayload; } { - return devtools != null ? { detail: { devtools } } : { detail: {} }; + return devtools == null ? { detail: {} } : { detail: { devtools } }; }