Skip to content

Commit 068db0b

Browse files
committed
refactor: streamline Long Tasks and LoAF handling by removing unused interfaces and improving script serialization
1 parent 203f1b7 commit 068db0b

File tree

5 files changed

+155
-76
lines changed

5 files changed

+155
-76
lines changed

packages/javascript/src/addons/longTasks.ts

Lines changed: 18 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
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
*/
13573
function 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
*/
155101
function 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
*/
195139
function 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

packages/javascript/src/types/hawk-initial-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ export interface HawkInitialSettings {
105105
* Observes Long Tasks and Long Animation Frames (LoAF) via PerformanceObserver
106106
* and sends a dedicated event when blocking is detected.
107107
*
108+
* This is an umbrella option by design: Long Tasks and LoAF describe the same
109+
* domain (main-thread blocking), so both toggles live under one config key.
110+
*
108111
* Chromium-only (Chrome, Edge). On unsupported browsers the observers
109112
* simply won't start — no errors, no overhead.
110113
*
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Long Task attribution information from Performance API.
3+
* Describes the container associated with the long task.
4+
*/
5+
export interface LongTaskAttribution {
6+
/** Attribution source name (`self`, `same-origin-ancestor`, etc.) */
7+
name: string;
8+
/** Entry type name from the attribution object */
9+
entryType: string;
10+
/** Container type (`iframe`, `embed`, `object`) */
11+
containerType?: string;
12+
/** Source URL of the container */
13+
containerSrc?: string;
14+
/** DOM id of the container element */
15+
containerId?: string;
16+
/** DOM name of the container element */
17+
containerName?: string;
18+
}
19+
20+
/**
21+
* Long Task entry with attribution details.
22+
*/
23+
export interface LongTaskPerformanceEntry extends PerformanceEntry {
24+
/** Attribution list for the long task */
25+
attribution?: LongTaskAttribution[];
26+
}
27+
28+
/**
29+
* LoAF script timing information (PerformanceScriptTiming).
30+
*/
31+
export interface LoAFScript {
32+
/** Script display name */
33+
name: string;
34+
/** Script invoker (e.g. `TimerHandler:setTimeout`) */
35+
invoker?: string;
36+
/** Invoker type (`event-listener`, `user-callback`, etc.) */
37+
invokerType?: string;
38+
/** Source URL of the script */
39+
sourceURL?: string;
40+
/** Function name associated with the script execution */
41+
sourceFunctionName?: string;
42+
/** Character position in source */
43+
sourceCharPosition?: number;
44+
/** Script duration in milliseconds */
45+
duration: number;
46+
/** Start time in milliseconds from navigation start */
47+
startTime: number;
48+
/** Execution start timestamp */
49+
executionStart?: number;
50+
/** Forced style/layout duration in milliseconds */
51+
forcedStyleAndLayoutDuration?: number;
52+
/** Paused time in milliseconds */
53+
pauseDuration?: number;
54+
/** Window attribution (`self`, `ancestor`, `descendant`) */
55+
windowAttribution?: string;
56+
}
57+
58+
/**
59+
* Long Animation Frame entry shape.
60+
*/
61+
export interface LoAFEntry extends PerformanceEntry {
62+
/** Blocking duration in milliseconds */
63+
blockingDuration?: number;
64+
/** Render start timestamp */
65+
renderStart?: number;
66+
/** Style/layout start timestamp */
67+
styleAndLayoutStart?: number;
68+
/** First UI event timestamp */
69+
firstUIEventTimestamp?: number;
70+
/** Script timing records for the frame */
71+
scripts?: LoAFScript[];
72+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Json, JsonNode } from '@hawk.so/types';
2+
3+
/**
4+
* Build a JSON object from key-value pairs.
5+
* Drops `null`, `undefined`, and empty strings.
6+
*
7+
* Useful for compact event payload construction without repetitive `if` chains.
8+
*/
9+
export function compactJson(entries: [string, JsonNode | null | undefined][]): Json {
10+
const result: Json = {};
11+
12+
for (const [key, value] of entries) {
13+
if (value != null && value !== '') {
14+
result[key] = value;
15+
}
16+
}
17+
18+
return result;
19+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { compactJson } from '../src/utils/compactJson';
3+
4+
describe('compactJson', () => {
5+
it('should keep non-empty primitive values', () => {
6+
const result = compactJson([
7+
['name', 'hawk'],
8+
['count', 0],
9+
['enabled', false],
10+
]);
11+
12+
expect(result).toEqual({
13+
name: 'hawk',
14+
count: 0,
15+
enabled: false,
16+
});
17+
});
18+
19+
it('should drop null, undefined and empty string values', () => {
20+
const result = compactJson([
21+
['a', null],
22+
['b', undefined],
23+
['c', ''],
24+
['d', 'ok'],
25+
]);
26+
27+
expect(result).toEqual({
28+
d: 'ok',
29+
});
30+
});
31+
32+
it('should keep nested json objects', () => {
33+
const result = compactJson([
34+
['meta', { source: 'test' }],
35+
['duration', 123],
36+
]);
37+
38+
expect(result).toEqual({
39+
meta: { source: 'test' },
40+
duration: 123,
41+
});
42+
});
43+
});

0 commit comments

Comments
 (0)