Skip to content

Commit 6ec3d83

Browse files
committed
fix: readme & tests
1 parent d905d8b commit 6ec3d83

5 files changed

Lines changed: 67 additions & 24 deletions

File tree

packages/javascript/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ Freeze detectors use two complementary APIs:
253253
- **Long Tasks API** — browser reports tasks taking longer than 50 ms.
254254
- **Long Animation Frames (LoAF)** — browser reports frames taking longer than 50 ms with richer script attribution (Chrome 123+, Edge 123+).
255255
256-
Both freeze detectors are enabled by default. If one API is unsupported, the other still works.
256+
Both freeze detectors are disabled by default. If enabled and one API is unsupported, the other still works.
257257
Each detected freeze is reported immediately with detailed context (duration, blocking time, scripts involved, etc.).
258258
`thresholdMs` is an additional Hawk filter on top of browser reporting. Hawk emits an issue when measured duration is equal to or greater than this value. Values below `50ms` are clamped to `50ms`.
259259
@@ -308,8 +308,8 @@ const hawk = new HawkCatcher({
308308
|--------|------|---------|-------------|
309309
| `errors` | `boolean` | `true` | Enable global errors handling (`window.onerror` and `unhandledrejection`). |
310310
| `webVitals` | `boolean` | `false` | Collect all Core Web Vitals and send one issue event when at least one metric is rated `poor`. Requires optional `web-vitals` dependency. |
311-
| `longTasks` | `false` or `{ thresholdMs?: number }` | `{ thresholdMs: 70 }` | Detect long tasks and emit issue events when duration is equal to or greater than the max allowed duration (`thresholdMs`, minimum effective value is `50ms`). |
312-
| `longAnimationFrames` | `false` or `{ thresholdMs?: number }` | `{ thresholdMs: 200 }` | Detect LoAF events and emit issue events when duration is equal to or greater than the max allowed duration (`thresholdMs`, minimum effective value is `50ms`). Requires Chrome 123+ / Edge 123+. |
311+
| `longTasks` | `boolean` or `{ thresholdMs?: number }` | `false` | `false` disables. `true` enables with default threshold. Object enables and uses `thresholdMs` when valid; otherwise fallback threshold `70ms` is used (minimum effective value `50ms`). |
312+
| `longAnimationFrames` | `boolean` or `{ thresholdMs?: number }` | `false` | `false` disables. `true` enables with default threshold. Object enables and uses `thresholdMs` when valid; otherwise fallback threshold `200ms` is used (minimum effective value `50ms`). Requires Chrome 123+ / Edge 123+. |
313313
314314
## Source maps consuming
315315

packages/javascript/src/addons/issues.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import type {
1212
import { compactJson } from '../utils/compactJson';
1313
import log from '../utils/log';
1414

15-
const DEFAULT_LONG_TASK_THRESHOLD_MS = 70;
16-
const DEFAULT_LOAF_THRESHOLD_MS = 200;
17-
const MIN_ISSUE_THRESHOLD_MS = 50;
18-
const WEB_VITALS_REPORT_TIMEOUT_MS = 10000;
15+
export const DEFAULT_LONG_TASK_THRESHOLD_MS = 70;
16+
export const DEFAULT_LOAF_THRESHOLD_MS = 200;
17+
export const MIN_ISSUE_THRESHOLD_MS = 50;
18+
export const WEB_VITALS_REPORT_TIMEOUT_MS = 10000;
1919

2020
const METRIC_THRESHOLDS: Record<string, [good: number, poor: number]> = {
2121
LCP: [2500, 4000],
@@ -54,16 +54,16 @@ export class IssuesMonitor {
5454
this.isInitialized = true;
5555
this.destroyed = false;
5656

57-
if (options.longTasks !== false) {
57+
if (options.longTasks !== undefined && options.longTasks !== false) {
5858
this.observeLongTasks(
59-
resolveThreshold(options.longTasks?.thresholdMs, DEFAULT_LONG_TASK_THRESHOLD_MS),
59+
resolveThreshold(resolveThresholdOption(options.longTasks), DEFAULT_LONG_TASK_THRESHOLD_MS),
6060
onIssue
6161
);
6262
}
6363

64-
if (options.longAnimationFrames !== false) {
64+
if (options.longAnimationFrames !== undefined && options.longAnimationFrames !== false) {
6565
this.observeLoAF(
66-
resolveThreshold(options.longAnimationFrames?.thresholdMs, DEFAULT_LOAF_THRESHOLD_MS),
66+
resolveThreshold(resolveThresholdOption(options.longAnimationFrames), DEFAULT_LOAF_THRESHOLD_MS),
6767
onIssue
6868
);
6969
}
@@ -355,6 +355,18 @@ function resolveThreshold(value: number | undefined, fallback: number): number {
355355
return Math.max(MIN_ISSUE_THRESHOLD_MS, Math.round(value));
356356
}
357357

358+
/**
359+
*
360+
* @param value
361+
*/
362+
function resolveThresholdOption(value: boolean | { thresholdMs?: number }): number | undefined {
363+
if (typeof value === 'object' && value !== null) {
364+
return value.thresholdMs;
365+
}
366+
367+
return undefined;
368+
}
369+
358370
/**
359371
*
360372
* @param name metric name

packages/javascript/src/catcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,8 @@ export default class Catcher {
326326
private configureIssues(settings: HawkInitialSettings): void {
327327
const issues = settings.issues ?? {};
328328
const shouldHandleGlobalErrors = settings.disableGlobalErrorsHandling !== true && issues.errors !== false;
329-
const shouldDetectPerformanceIssues = issues.longTasks !== false
330-
|| issues.longAnimationFrames !== false
329+
const shouldDetectPerformanceIssues = (issues.longTasks !== undefined && issues.longTasks !== false)
330+
|| (issues.longAnimationFrames !== undefined && issues.longAnimationFrames !== false)
331331
|| issues.webVitals === true;
332332

333333
if (shouldHandleGlobalErrors) {

packages/javascript/src/types/issues.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,24 @@ export interface IssuesOptions {
3030
webVitals?: boolean;
3131

3232
/**
33-
* Long Tasks options. Set `false` to disable.
33+
* Long Tasks options.
34+
* `false` disables the feature.
35+
* Any other value enables it with default threshold.
36+
* If `thresholdMs` is a valid number greater than or equal to 50, it is used.
3437
*
3538
* @default false
3639
*/
37-
longTasks?: false | IssueThresholdOptions;
40+
longTasks?: boolean | IssueThresholdOptions;
3841

3942
/**
40-
* Long Animation Frames options. Set `false` to disable.
43+
* Long Animation Frames options.
44+
* `false` disables the feature.
45+
* Any other value enables it with default threshold.
46+
* If `thresholdMs` is a valid number greater than or equal to 50, it is used.
4147
*
4248
* @default false
4349
*/
44-
longAnimationFrames?: false | IssueThresholdOptions;
50+
longAnimationFrames?: boolean | IssueThresholdOptions;
4551
}
4652

4753
/**

packages/javascript/tests/issues-monitor.test.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import type { Metric, ReportCallback } from 'web-vitals';
3+
import {
4+
DEFAULT_LONG_TASK_THRESHOLD_MS,
5+
MIN_ISSUE_THRESHOLD_MS,
6+
WEB_VITALS_REPORT_TIMEOUT_MS,
7+
} from '../src/addons/issues';
38

49
class MockPerformanceObserver {
510
public static supportedEntryTypes: string[] = ['longtask', 'long-animation-frame'];
@@ -96,10 +101,10 @@ describe('IssuesMonitor', () => {
96101

97102
expect(observer).toBeDefined();
98103

99-
observer!.emit([entry('longtask', 49)]);
104+
observer!.emit([entry('longtask', MIN_ISSUE_THRESHOLD_MS - 1)]);
100105
expect(onIssue).not.toHaveBeenCalled();
101106

102-
observer!.emit([entry('longtask', 50)]);
107+
observer!.emit([entry('longtask', MIN_ISSUE_THRESHOLD_MS)]);
103108
expect(onIssue).toHaveBeenCalledTimes(1);
104109
});
105110

@@ -109,17 +114,37 @@ describe('IssuesMonitor', () => {
109114
const onIssue = vi.fn();
110115
const monitor = new IssuesMonitor();
111116

112-
monitor.init({ longTasks: { thresholdMs: 70 }, longAnimationFrames: false, webVitals: false }, onIssue);
117+
const customThresholdMs = 75;
118+
119+
monitor.init({ longTasks: { thresholdMs: customThresholdMs }, longAnimationFrames: false, webVitals: false }, onIssue);
120+
const observer = MockPerformanceObserver.byType('longtask');
121+
122+
expect(observer).toBeDefined();
123+
124+
observer!.emit([entry('longtask', customThresholdMs - 1)]);
125+
expect(onIssue).not.toHaveBeenCalled();
126+
127+
observer!.emit([entry('longtask', customThresholdMs)]);
128+
expect(onIssue).toHaveBeenCalledTimes(1);
129+
expect(onIssue.mock.calls[0][0].title).toContain(`${customThresholdMs} ms`);
130+
});
131+
132+
it('should use default threshold when longTasks is true', async () => {
133+
mockWebVitals();
134+
const { IssuesMonitor } = await import('../src/addons/issues');
135+
const onIssue = vi.fn();
136+
const monitor = new IssuesMonitor();
137+
138+
monitor.init({ longTasks: true, longAnimationFrames: false, webVitals: false }, onIssue);
113139
const observer = MockPerformanceObserver.byType('longtask');
114140

115141
expect(observer).toBeDefined();
116142

117-
observer!.emit([entry('longtask', 55)]);
143+
observer!.emit([entry('longtask', DEFAULT_LONG_TASK_THRESHOLD_MS - 1)]);
118144
expect(onIssue).not.toHaveBeenCalled();
119145

120-
observer!.emit([entry('longtask', 75)]);
146+
observer!.emit([entry('longtask', DEFAULT_LONG_TASK_THRESHOLD_MS)]);
121147
expect(onIssue).toHaveBeenCalledTimes(1);
122-
expect(onIssue.mock.calls[0][0].title).toContain('75 ms');
123148
});
124149

125150
it('should ignore second init call and avoid duplicate observers', async () => {
@@ -177,7 +202,7 @@ describe('IssuesMonitor', () => {
177202
await vi.dynamicImportSettled();
178203

179204
webVitals.emit({ name: 'LCP', value: 5000, rating: 'poor', delta: 5000 });
180-
vi.advanceTimersByTime(10000);
205+
vi.advanceTimersByTime(WEB_VITALS_REPORT_TIMEOUT_MS);
181206

182207
expect(onIssue).toHaveBeenCalledTimes(1);
183208
expect(onIssue.mock.calls[0][0].title).toContain('Poor Web Vitals');

0 commit comments

Comments
 (0)