88 * Sets up observers and fires `onEntry` per detected entry — fire and forget.
99 */
1010
11- import type { EventContext , Json , JsonNode } from '@hawk.so/types' ;
12- import log from '../utils/log' ;
11+ import type { EventContext , Json } from '@hawk.so/types' ;
12+ import type { LoAFEntry , LoAFScript , LongTaskPerformanceEntry } from '../types/long-tasks' ;
13+ import { compactJson } from '../utils/compactJson' ;
1314
1415/**
1516 * Configuration for main-thread blocking detection
@@ -49,69 +50,6 @@ export interface LongTaskEvent {
4950 context : EventContext ;
5051}
5152
52- /**
53- * Long Task attribution (container-level info only)
54- */
55- interface LongTaskAttribution {
56- name : string ;
57- entryType : string ;
58- containerType ?: string ;
59- containerSrc ?: string ;
60- containerId ?: string ;
61- containerName ?: string ;
62- }
63-
64- /**
65- * Long Task entry with attribution
66- */
67- interface LongTaskPerformanceEntry extends PerformanceEntry {
68- attribution ?: LongTaskAttribution [ ] ;
69- }
70-
71- /**
72- * LoAF script timing (PerformanceScriptTiming)
73- */
74- interface LoAFScript {
75- name : string ;
76- invoker ?: string ;
77- invokerType ?: string ;
78- sourceURL ?: string ;
79- sourceFunctionName ?: string ;
80- sourceCharPosition ?: number ;
81- duration : number ;
82- startTime : number ;
83- executionStart ?: number ;
84- forcedStyleAndLayoutDuration ?: number ;
85- pauseDuration ?: number ;
86- windowAttribution ?: string ;
87- }
88-
89- /**
90- * LoAF entry shape (spec is still evolving)
91- */
92- interface LoAFEntry extends PerformanceEntry {
93- blockingDuration ?: number ;
94- renderStart ?: number ;
95- styleAndLayoutStart ?: number ;
96- firstUIEventTimestamp ?: number ;
97- scripts ?: LoAFScript [ ] ;
98- }
99-
100- /**
101- * Build a Json object from entries, dropping null / undefined / empty-string values
102- */
103- function compact ( entries : [ string , JsonNode | null | undefined ] [ ] ) : Json {
104- const result : Json = { } ;
105-
106- for ( const [ key , value ] of entries ) {
107- if ( value != null && value !== '' ) {
108- result [ key ] = value ;
109- }
110- }
111-
112- return result ;
113- }
114-
11553/**
11654 * Check whether the browser supports a given PerformanceObserver entry type
11755 *
@@ -133,7 +71,7 @@ function supportsEntryType(type: string): boolean {
13371 * Serialize a LoAF script entry into a Json-compatible object
13472 */
13573function serializeScript ( s : LoAFScript ) : Json {
136- return compact ( [
74+ return compactJson ( [
13775 [ 'invoker' , s . invoker ] ,
13876 [ 'invokerType' , s . invokerType ] ,
13977 [ 'sourceURL' , s . sourceURL ] ,
@@ -147,15 +85,21 @@ function serializeScript(s: LoAFScript): Json {
14785 ] ) ;
14886}
14987
88+ /**
89+ * Return LoAF scripts that contain useful source attribution for debugging.
90+ * We keep only scripts that have at least function name or source URL.
91+ */
92+ function getRelevantLoAFScripts ( loaf : LoAFEntry ) : LoAFScript [ ] {
93+ return loaf . scripts ?. filter ( ( s ) => s . sourceURL || s . sourceFunctionName ) ?? [ ] ;
94+ }
95+
15096/**
15197 * Subscribe to Long Tasks (>50 ms) via PerformanceObserver
15298 *
15399 * @param onEntry - callback fired for each detected long task
154100 */
155101function observeLongTasks ( onEntry : ( e : LongTaskEvent ) => void ) : void {
156102 if ( ! supportsEntryType ( 'longtask' ) ) {
157- log ( 'Long Tasks API is not supported in this browser' , 'info' ) ;
158-
159103 return ;
160104 }
161105
@@ -166,7 +110,7 @@ function observeLongTasks(onEntry: (e: LongTaskEvent) => void): void {
166110 const durationMs = Math . round ( task . duration ) ;
167111 const attr = task . attribution ?. [ 0 ] ;
168112
169- const details = compact ( [
113+ const details = compactJson ( [
170114 [ 'kind' , 'longtask' ] ,
171115 [ 'entryName' , task . name ] ,
172116 [ 'startTime' , Math . round ( task . startTime ) ] ,
@@ -194,8 +138,6 @@ function observeLongTasks(onEntry: (e: LongTaskEvent) => void): void {
194138 */
195139function observeLoAF ( onEntry : ( e : LongTaskEvent ) => void ) : void {
196140 if ( ! supportsEntryType ( 'long-animation-frame' ) ) {
197- log ( 'Long Animation Frames (LoAF) API is not supported in this browser' , 'info' ) ;
198-
199141 return ;
200142 }
201143
@@ -204,21 +146,21 @@ function observeLoAF(onEntry: (e: LongTaskEvent) => void): void {
204146 for ( const entry of list . getEntries ( ) ) {
205147 const loaf = entry as LoAFEntry ;
206148 const durationMs = Math . round ( loaf . duration ) ;
207- const blockingDurationMs = loaf . blockingDuration !== null
149+ const blockingDurationMs = loaf . blockingDuration !== undefined && loaf . blockingDuration !== null
208150 ? Math . round ( loaf . blockingDuration )
209151 : null ;
210152
211- const relevantScripts = loaf . scripts ?. filter ( ( s ) => s . sourceURL || s . sourceFunctionName ) ;
153+ const relevantScripts = getRelevantLoAFScripts ( loaf ) ;
212154
213- const scripts = relevantScripts ? .length
155+ const scripts = relevantScripts . length
214156 ? relevantScripts . reduce < Json > ( ( acc , s , i ) => {
215157 acc [ `script_${ i } ` ] = serializeScript ( s ) ;
216158
217159 return acc ;
218160 } , { } )
219161 : null ;
220162
221- const details = compact ( [
163+ const details = compactJson ( [
222164 [ 'kind' , 'loaf' ] ,
223165 [ 'startTime' , Math . round ( loaf . startTime ) ] ,
224166 [ 'durationMs' , durationMs ] ,
@@ -233,7 +175,7 @@ function observeLoAF(onEntry: (e: LongTaskEvent) => void): void {
233175 ? ` (blocking ${ blockingDurationMs } ms)`
234176 : '' ;
235177
236- const topScript = relevantScripts ?. [ 0 ] ;
178+ const topScript = relevantScripts [ 0 ] ;
237179 const culprit = topScript ?. sourceFunctionName
238180 || topScript ?. invoker
239181 || topScript ?. sourceURL
0 commit comments