Skip to content

Commit 78f2481

Browse files
authored
Merge pull request #11 from zendive/next
v1.0.7
2 parents 946d783 + abf2ed5 commit 78f2481

File tree

80 files changed

+4705
-4961
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

80 files changed

+4705
-4961
lines changed

Makefile

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,19 @@ install:
1010

1111
dev:
1212
rm -rf ./public/build
13-
NODE_OPTIONS="--import=tsx" \
14-
pnpm exec webpack --progress --watch --mode=development
15-
16-
lint:
17-
pnpm exec prettier . --write
18-
pnpm exec svelte-check
13+
NODE_ENV=development \
14+
deno run --watch --allow-env --allow-read --allow-run deno-bundle.ts
1915

2016
test:
17+
pnpm exec prettier . --write
2118
pnpm exec jest --config jest/jest.config.js
19+
pnpm exec svelte-check
2220

23-
prod: lint test
21+
prod: test
2422
rm -rf ./public/build $(ZIP_CHROME_FILE)
25-
NODE_OPTIONS="--import=tsx" \
26-
time pnpm exec webpack --mode=production
23+
NODE_ENV=production \
24+
time deno run --allow-env --allow-read --allow-run deno-bundle.ts
2725
zip -r $(ZIP_CHROME_FILE) ./public ./manifest.json > /dev/null
2826

29-
.PHONY: clean install dev lint prod test
27+
.PHONY: clean install dev prod test
3028
.DEFAULT_GOAL := dev

README.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,55 @@
22

33
- Available in Chrome Web Store as [API Monitor](https://chromewebstore.google.com/detail/api-monitor/bghmfoakiidiedpheejcjhciekobjcjp)
44

5-
Whether you're developing a Single Page Application (SPA) and want to assess implementation correctness or are curious about how something works, this tool adds additional functionality to the Chrome browser DevTools to reveal timeouts, intervals, idle callbacks, animation frames and `eval` usages while mapping their invocation call stacks to a source code location. If the page has mounted `video` or `audio` media element's, their events and property state changes can be observed as they happen ([documentation](https://developer.mozilla.org/docs/Web/API/HTMLMediaElement), [example](https://www.w3.org/2010/05/video/mediaevents.html)).
5+
If you're web developer and want to assess implementation correctness - this tool adds additional panel to the browser’s DevTool that enables to see scheduled timeouts and active intervals, as well as to review and navigate to initiators of: eval, setTimeout, setInterval, requestAnimationFrame, requestIdleCallback and their terminator functions.
66

7-
### Motivation
7+
#### Allows:
88

9-
To expedite issues discovery, here are some examples from experience:
9+
- to measure callback execution self-time.
10+
- to see `requestAnimationFrame` callback request frame rate.
11+
- visit every function in the call stack (if available), bypass or pause while debugging.
12+
- detect eval ­function usage, see its argument and return value, same for setTimeout and setInterval when called with a string instead of a function.
13+
- for every mounted video or audio media element's to see it’s state and properties.
1014

11-
- A ~10ms delay interval constantly consuming approximately 10% of CPU from a third-party library, solely to check if the page was resized.
12-
- A bundled dependency library that utilizes the eval function, thereby preventing the removal of `unsafe-eval` from the Content Security Policy (CSP) header. It can be arguably intentional, but also may reveal a configuration issue when the package was bundled with webpack config's [devtool: 'eval'](https://webpack.js.org/configuration/devtool/) in production mode.
13-
- A substantial number of hidden video elements in the DOM that were consuming resources, unexpectedly limited to 100 medias per domain.
14-
- An unattended interval that was unintentionally left running and contributed to a slowly growing memory.
15+
#### Helps to spot:
1516

16-
To explore the internals of a complex systems.
17+
- incorrect timeout delay.
18+
- bad handler for terminator function.
19+
- terminating non existing or elapsed timeout.
20+
21+
#### Motivation:
22+
23+
- To expedite issues discovery.
24+
25+
#### Wrapped native functions:
26+
27+
- eval (by default off)
28+
- setTimeout
29+
- clearTimeout
30+
- setInterval
31+
- clearInterval
32+
- requestAnimationFrame
33+
- cancelAnimationFrame
34+
- requestIdleCallback
35+
- cancelIdleCallback
36+
37+
##### Note:
38+
39+
- while measuring performance of your code – consider disabling this extension as it may affect the results.
1740

1841
<details>
1942
<summary> <strong>Example</strong> </summary>
2043

21-
![screenshot](./doc/screenshot-01.png)
2244
![screenshot](./doc/screenshot-02.png)
23-
![screenshot](./doc/screenshot-03.png)
2445
![screenshot](./doc/screenshot-04.png)
2546

2647
</details>
2748

2849
### Build requirements
2950

3051
- OS: Linux
31-
- Node: 20.16.0 (LTS)
52+
- Node: 22.12.0 (LTS)
53+
- Deno: 2.1.7
3254

3355
### Build instructions
3456

deno-bundle.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { build, context, type BuildOptions } from 'esbuild';
2+
import esbuildSvelte from 'esbuild-svelte';
3+
import { sveltePreprocess } from 'svelte-preprocess';
4+
import manifest from './manifest.json' with { type: 'json' };
5+
6+
const isProd = process.env.NODE_ENV === 'production';
7+
console.log('🚧', process.env.NODE_ENV);
8+
9+
const buildOptions: BuildOptions = {
10+
plugins: [
11+
esbuildSvelte({
12+
preprocess: sveltePreprocess(),
13+
compilerOptions: { dev: !isProd },
14+
}),
15+
],
16+
entryPoints: [
17+
'./src/api-monitor-devtools.ts',
18+
'./src/api-monitor-cs-main.ts',
19+
'./src/api-monitor-cs-isolated.ts',
20+
'./src/api-monitor-devtools-panel.ts',
21+
],
22+
outdir: './public/build/',
23+
define: {
24+
__development__: `${!isProd}`,
25+
__app_name__: `"browser-api-monitor@${manifest.version}"`,
26+
__app_version__: `"${manifest.version}"`,
27+
__home_page__: `"${manifest.homepage_url}"`,
28+
},
29+
bundle: true,
30+
platform: 'browser',
31+
format: 'iife',
32+
target: 'esnext',
33+
conditions: [`${process.env.NODE_ENV}`],
34+
minify: isProd,
35+
sourcemap: false,
36+
treeShaking: true,
37+
logLevel: 'debug',
38+
};
39+
40+
if (isProd) {
41+
build(buildOptions).catch((error) => {
42+
console.error(error);
43+
});
44+
} else {
45+
const ctx = await context(buildOptions);
46+
await ctx.watch();
47+
}

doc/screenshot-01.png

-863 KB
Binary file not shown.

doc/screenshot-03.png

-402 KB
Binary file not shown.

doc/screenshot-04.png

-97 KB
Loading

jest/jest.config.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,4 @@ export default {
1616
},
1717
rootDir: '..', // . - /
1818
roots: ['jest/tests'], // . - /jest
19-
moduleNameMapper: {
20-
// duplicates tsconfig.json/compilerOptions/path & webpack.config.ts/resolve/alias
21-
'@/(.*)': '<rootDir>/src/$1',
22-
},
2319
};

jest/tests/AnimationWrapper.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
2+
import { AnimationWrapper } from '../../src/wrapper/AnimationWrapper.ts';
3+
import { TraceUtil } from '../../src/wrapper/TraceUtil.ts';
4+
import { TAG_EXCEPTION } from '../../src/api/clone.ts';
5+
import { TextEncoder } from 'node:util';
6+
7+
global.TextEncoder = TextEncoder;
8+
9+
describe('AnimationWrapper', () => {
10+
const traceUtil = new TraceUtil();
11+
let apiAnimation: AnimationWrapper;
12+
13+
beforeEach(() => {
14+
apiAnimation = new AnimationWrapper(traceUtil);
15+
apiAnimation.wrapRequestAnimationFrame();
16+
apiAnimation.wrapCancelAnimationFrame();
17+
});
18+
19+
afterEach(() => {
20+
apiAnimation.unwrapRequestAnimationFrame();
21+
apiAnimation.unwrapCancelAnimationFrame();
22+
});
23+
24+
test('rafHistory - recorded', async () => {
25+
let typeOfArgument = '';
26+
const handler = await new Promise((resolve) => {
27+
const handler = requestAnimationFrame((time) => {
28+
typeOfArgument = typeof time;
29+
resolve(handler);
30+
});
31+
});
32+
const rec = Array.from(apiAnimation.rafHistory.values())[0];
33+
34+
expect(typeOfArgument).toBe('number');
35+
expect(apiAnimation.rafHistory.size).toBe(1);
36+
expect(rec.handler).toBe(handler);
37+
expect(rec.calls).toBe(1);
38+
expect(rec.trace.length).toBeGreaterThan(1);
39+
expect(rec.traceId.length).toBeGreaterThan(1);
40+
expect(rec.selfTime).not.toBeNull();
41+
expect(apiAnimation.callCounter.requestAnimationFrame).toBe(1);
42+
});
43+
44+
test('cafHistory - recorded', async () => {
45+
const unchanged = 0,
46+
changed = 1;
47+
let changeable = unchanged;
48+
const handler = requestAnimationFrame(() => {
49+
changeable = changed;
50+
});
51+
cancelAnimationFrame(handler);
52+
53+
const rafRec = Array.from(apiAnimation.rafHistory.values())[0];
54+
const cafRec = Array.from(apiAnimation.cafHistory.values())[0];
55+
56+
expect(changeable).toBe(unchanged);
57+
expect(apiAnimation.rafHistory.size).toBe(1);
58+
expect(apiAnimation.cafHistory.size).toBe(1);
59+
expect(cafRec.handler).toBe(handler);
60+
expect(cafRec.calls).toBe(1);
61+
expect(cafRec.trace.length).toBeGreaterThan(1);
62+
expect(cafRec.traceId.length).toBeGreaterThan(1);
63+
expect(apiAnimation.callCounter.cancelAnimationFrame).toBe(1);
64+
expect(rafRec.canceledByTraceIds?.length).toBe(1);
65+
expect(rafRec.canceledCounter).toBe(1);
66+
});
67+
68+
test('cafHistory - invalid handler', () => {
69+
cancelAnimationFrame(0);
70+
71+
const rec = Array.from(apiAnimation.cafHistory?.values())[0];
72+
73+
expect(rec.handler).toBe(TAG_EXCEPTION(0));
74+
});
75+
});

jest/tests/EvalWrapper.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, expect, test, beforeEach, afterEach } from '@jest/globals';
2+
import { EvalWrapper } from '../../src/wrapper/EvalWrapper.ts';
3+
import { TimerWrapper } from '../../src/wrapper/TimerWrapper.ts';
4+
import { TraceUtil } from '../../src/wrapper/TraceUtil.ts';
5+
import { TAG_UNDEFINED } from '../../src/api/clone.ts';
6+
import { TextEncoder } from 'node:util';
7+
import {
8+
TAG_EVAL_RETURN_SET_INTERVAL,
9+
TAG_EVAL_RETURN_SET_TIMEOUT,
10+
} from '../../src/api/const.ts';
11+
12+
global.TextEncoder = TextEncoder;
13+
14+
describe('EvalWrapper', () => {
15+
const traceUtil = new TraceUtil();
16+
let apiEval: EvalWrapper;
17+
let apiTimer: TimerWrapper;
18+
19+
beforeEach(() => {
20+
apiEval = new EvalWrapper(traceUtil);
21+
apiEval.wrap();
22+
apiTimer = new TimerWrapper(traceUtil, apiEval);
23+
apiTimer.wrapSetTimeout();
24+
apiTimer.wrapSetInterval();
25+
});
26+
27+
afterEach(() => {
28+
apiEval.unwrap();
29+
apiTimer.unwrapSetTimeout();
30+
apiTimer.unwrapSetInterval();
31+
});
32+
33+
test('evalHistory - recorded', () => {
34+
const NUMBER_OF_INVOCATIONS = 2;
35+
const CODE = '(1+2)';
36+
const RESULT = 3;
37+
38+
for (let i = 0, I = NUMBER_OF_INVOCATIONS; i < I; i++) {
39+
window.eval(CODE);
40+
}
41+
expect(apiEval.evalHistory.size).toBe(1);
42+
43+
const rec = Array.from(apiEval.evalHistory.values())[0];
44+
45+
expect(rec.calls).toBe(NUMBER_OF_INVOCATIONS);
46+
expect(rec.usesLocalScope).toBe(false);
47+
expect(rec.code).toBe(CODE);
48+
expect(rec.returnedValue).toBe(RESULT);
49+
expect(rec.trace.length).toBeGreaterThan(1);
50+
expect(rec.traceId.length).toBeGreaterThan(0);
51+
expect(rec.selfTime).not.toBeNull();
52+
});
53+
54+
test('evalHistory - detects local scope usage', () => {
55+
const local_variable = 0;
56+
window.eval('(local_variable++)');
57+
58+
const rec = Array.from(apiEval.evalHistory.values())[0];
59+
60+
expect(rec.calls).toBe(1);
61+
expect(local_variable).toBe(0);
62+
expect(rec.usesLocalScope).toBe(true);
63+
expect(rec.returnedValue).toBe(TAG_UNDEFINED);
64+
});
65+
66+
test('setTimeoutHistory - isEval recorded', () => {
67+
const CODE = '(1+2)';
68+
setTimeout(CODE);
69+
const timerRec = Array.from(apiTimer.setTimeoutHistory.values())[0];
70+
const evalRec = Array.from(apiEval.evalHistory.values())[0];
71+
72+
expect(timerRec.isEval).toBe(true);
73+
expect(evalRec.code).toBe(CODE);
74+
expect(evalRec.returnedValue).toBe(TAG_EVAL_RETURN_SET_TIMEOUT);
75+
});
76+
77+
test('setIntervalHistory - isEval recorded', () => {
78+
const CODE = '(1+2)';
79+
const handler = setInterval(CODE, 123);
80+
const timerRec = Array.from(apiTimer.setIntervalHistory.values())[0];
81+
const evalRec = Array.from(apiEval.evalHistory.values())[0];
82+
83+
expect(timerRec.isEval).toBe(true);
84+
expect(evalRec.code).toBe(CODE);
85+
expect(evalRec.returnedValue).toBe(TAG_EVAL_RETURN_SET_INTERVAL);
86+
87+
clearInterval(handler);
88+
});
89+
});

0 commit comments

Comments
 (0)