@applitools/eyes-storybook 3.65.7 → 3.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/package.json +11 -9
- package/src/buildExecutionSummaryEvent.js +72 -0
- package/src/eyesStorybook.js +100 -1
- package/src/initPage.js +10 -1
- package/src/pagePool.js +2 -1
- package/src/renderStories.js +6 -0
- package/src/runMetrics.js +134 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.66.0](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.65.8...js/eyes-storybook@3.66.0) (2026-06-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* one-time per-execution summary logEvent in storybook | AD-14305 ([#3911](https://github.com/Applitools-Dev/sdk/issues/3911)) ([59e3a5d](https://github.com/Applitools-Dev/sdk/commit/59e3a5dccf74aa1ac1158024316bd305e3515afa))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Dependencies
|
|
12
|
+
|
|
13
|
+
* @applitools/utils bumped to 1.15.0
|
|
14
|
+
#### Features
|
|
15
|
+
|
|
16
|
+
* one-time per-execution summary logEvent in storybook | AD-14305 ([#3911](https://github.com/Applitools-Dev/sdk/issues/3911)) ([59e3a5d](https://github.com/Applitools-Dev/sdk/commit/59e3a5dccf74aa1ac1158024316bd305e3515afa))
|
|
17
|
+
* @applitools/req bumped to 1.11.1
|
|
18
|
+
#### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* bundle node-fetch into dist to drop node-domexception dep warning | FLD-4197 ([#3899](https://github.com/Applitools-Dev/sdk/issues/3899)) ([9c3dc52](https://github.com/Applitools-Dev/sdk/commit/9c3dc52866a4a665f1578c39ff43cd0ee5f63492))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
* @applitools/logger bumped to 2.2.13
|
|
25
|
+
|
|
26
|
+
* @applitools/dom-snapshot bumped to 4.17.4
|
|
27
|
+
|
|
28
|
+
* @applitools/socket bumped to 1.3.14
|
|
29
|
+
|
|
30
|
+
* @applitools/image bumped to 1.2.12
|
|
31
|
+
|
|
32
|
+
* @applitools/dom-capture bumped to 11.8.2
|
|
33
|
+
|
|
34
|
+
* @applitools/driver bumped to 1.26.4
|
|
35
|
+
|
|
36
|
+
* @applitools/spec-driver-webdriver bumped to 1.6.4
|
|
37
|
+
|
|
38
|
+
* @applitools/spec-driver-selenium bumped to 1.8.4
|
|
39
|
+
|
|
40
|
+
* @applitools/spec-driver-playwright bumped to 1.9.4
|
|
41
|
+
|
|
42
|
+
* @applitools/spec-driver-puppeteer bumped to 1.8.4
|
|
43
|
+
|
|
44
|
+
* @applitools/screenshoter bumped to 3.12.23
|
|
45
|
+
|
|
46
|
+
* @applitools/nml-client bumped to 1.11.33
|
|
47
|
+
|
|
48
|
+
* @applitools/tunnel-client bumped to 1.12.1
|
|
49
|
+
|
|
50
|
+
* @applitools/ufg-client bumped to 1.22.4
|
|
51
|
+
|
|
52
|
+
* @applitools/core-base bumped to 1.35.3
|
|
53
|
+
|
|
54
|
+
* @applitools/ec-client bumped to 1.12.35
|
|
55
|
+
|
|
56
|
+
* @applitools/core bumped to 4.65.2
|
|
57
|
+
|
|
58
|
+
* @applitools/eyes bumped to 1.43.4
|
|
59
|
+
|
|
60
|
+
* @applitools/test-server bumped to 1.4.5
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## [3.65.8](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.65.7...js/eyes-storybook@3.65.8) (2026-06-15)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
### Dependencies
|
|
67
|
+
|
|
68
|
+
* @applitools/core-base bumped to 1.35.2
|
|
69
|
+
#### Bug Fixes
|
|
70
|
+
|
|
71
|
+
* re-trigger release for packages missed in the AD-14089 release | AD-14089 ([#3925](https://github.com/Applitools-Dev/sdk/issues/3925)) ([0880c34](https://github.com/Applitools-Dev/sdk/commit/0880c34b66221025e70ca7e3e3fab7b45a9f8d9e))
|
|
72
|
+
* @applitools/nml-client bumped to 1.11.32
|
|
73
|
+
|
|
74
|
+
* @applitools/ec-client bumped to 1.12.34
|
|
75
|
+
|
|
76
|
+
* @applitools/core bumped to 4.65.1
|
|
77
|
+
|
|
78
|
+
* @applitools/eyes bumped to 1.43.3
|
|
79
|
+
|
|
80
|
+
|
|
3
81
|
## [3.65.7](https://github.com/Applitools-Dev/sdk/compare/js/eyes-storybook@3.65.6...js/eyes-storybook@3.65.7) (2026-06-14)
|
|
4
82
|
|
|
5
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applitools/eyes-storybook",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"applitools",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"test:it": "run --top-level mocha 'test/it/*.test.js'",
|
|
37
37
|
"test:versions": "yarn build && yarn test:e2e:versions && yarn lint",
|
|
38
38
|
"test:e2e": "MOCHA_GROUP=e2e run --top-level mocha 'test/e2e/*.e2e.test.js' --require ./test/mocha-global-setup.js",
|
|
39
|
+
"test:performance-logs": "MOCHA_GROUP=performance_logs run --top-level mocha 'test/e2e/execution-summary-event.e2e.test.js' --require ./test/mocha-global-setup.js --exit",
|
|
39
40
|
"test:e2e:versions": "MOCHA_GROUP=e2e_versions run --top-level mocha test/versions/*.e2e.versions.test.js",
|
|
40
41
|
"test:sanity": "run --top-level mocha 'test/sanity/*.test.js'",
|
|
41
42
|
"test:heavy": "node bin/eyes-storybook.js -f test/fixtures/heavyStorybook/applitools.config.js",
|
|
@@ -59,15 +60,15 @@
|
|
|
59
60
|
"up:framework": "cd test/fixtures/storybook-versions/${APPLITOOLS_FRAMEWORK_VERSION} && npm ci"
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
|
-
"@applitools/core": "4.65.
|
|
63
|
-
"@applitools/driver": "1.26.
|
|
64
|
-
"@applitools/eyes": "1.43.
|
|
63
|
+
"@applitools/core": "4.65.2",
|
|
64
|
+
"@applitools/driver": "1.26.4",
|
|
65
|
+
"@applitools/eyes": "1.43.4",
|
|
65
66
|
"@applitools/functional-commons": "1.6.0",
|
|
66
|
-
"@applitools/logger": "2.2.
|
|
67
|
+
"@applitools/logger": "2.2.13",
|
|
67
68
|
"@applitools/monitoring-commons": "1.0.19",
|
|
68
|
-
"@applitools/spec-driver-puppeteer": "1.8.
|
|
69
|
-
"@applitools/ufg-client": "1.22.
|
|
70
|
-
"@applitools/utils": "1.
|
|
69
|
+
"@applitools/spec-driver-puppeteer": "1.8.4",
|
|
70
|
+
"@applitools/ufg-client": "1.22.4",
|
|
71
|
+
"@applitools/utils": "1.15.0",
|
|
71
72
|
"@inquirer/prompts": "7.0.1",
|
|
72
73
|
"boxen": "4.2.0",
|
|
73
74
|
"chalk": "3.0.0",
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"devDependencies": {
|
|
84
85
|
"@applitools/bongo": "^5.10.0",
|
|
85
86
|
"@applitools/snaptdout": "^1.1.1",
|
|
86
|
-
"@applitools/test-server": "^1.4.
|
|
87
|
+
"@applitools/test-server": "^1.4.5",
|
|
87
88
|
"@applitools/test-utils": "^1.5.17",
|
|
88
89
|
"@storybook/addon-interactions": "^6.4.18",
|
|
89
90
|
"@storybook/react": "^6.5.16",
|
|
@@ -117,6 +118,7 @@
|
|
|
117
118
|
"core-js": false,
|
|
118
119
|
"puppeteer": true,
|
|
119
120
|
"@storybook/react>core-js": false,
|
|
121
|
+
"@storybook/react>@storybook/core-common>webpack>watchpack>watchpack-chokidar2>chokidar>fsevents": false,
|
|
120
122
|
"@applitools/ufg-client>@applitools/image>sharp": false,
|
|
121
123
|
"puppeteer>puppeteer-core>ws>bufferutil": false,
|
|
122
124
|
"puppeteer>puppeteer-core>ws>utf-8-validate": false
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Credentials/connection details that must never be sent to Coralogix. Everything else in
|
|
4
|
+
// the config (puppeteerOptions, hooks, flags, ...) is reported as-is.
|
|
5
|
+
const SETTINGS_DENY_LIST = ['apiKey', 'proxy', 'eyesServerUrl', 'serverUrl', 'accessKey'];
|
|
6
|
+
|
|
7
|
+
function pickSettings(config = {}) {
|
|
8
|
+
const flags = {};
|
|
9
|
+
for (const [key, value] of Object.entries(config)) {
|
|
10
|
+
if (!SETTINGS_DENY_LIST.includes(key)) flags[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return flags;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Assemble the one-time per-execution summary event payload. Pure function — all
|
|
17
|
+
* inputs are plain data; never throws on missing fields (best-effort observability).
|
|
18
|
+
*/
|
|
19
|
+
function buildExecutionSummaryEvent({
|
|
20
|
+
status, // 'finished' | 'failed' | 'aborted'
|
|
21
|
+
config = {},
|
|
22
|
+
environment = {},
|
|
23
|
+
account = {},
|
|
24
|
+
concurrentTabs,
|
|
25
|
+
counts = {},
|
|
26
|
+
summary, // TestResultSummary | undefined
|
|
27
|
+
metrics = {}, // runMetrics.summarize()
|
|
28
|
+
totalRunMs,
|
|
29
|
+
renderStoriesMs,
|
|
30
|
+
}) {
|
|
31
|
+
// keep the scalar counts (passed/failed/unresolved/...); drop the large per-test `results` array
|
|
32
|
+
let tests;
|
|
33
|
+
if (summary) {
|
|
34
|
+
tests = {...summary};
|
|
35
|
+
delete tests.results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
type: 'eyesStorybookExecutionFinished',
|
|
40
|
+
status,
|
|
41
|
+
sdkVersion: environment.sdk?.currentVersion,
|
|
42
|
+
addonVersion: environment.sdk?.addonVersion,
|
|
43
|
+
storybookVersion: config.sbVersion || environment.versions?.storybook,
|
|
44
|
+
framework: environment.sdk?.framework,
|
|
45
|
+
counts: {
|
|
46
|
+
totalStories: counts.totalStories,
|
|
47
|
+
afterFilter: counts.afterFilter,
|
|
48
|
+
afterVariations: counts.afterVariations,
|
|
49
|
+
normal: counts.normal,
|
|
50
|
+
fakeIE: counts.fakeIE,
|
|
51
|
+
tests,
|
|
52
|
+
},
|
|
53
|
+
timing: {
|
|
54
|
+
totalRunMs,
|
|
55
|
+
renderStoriesMs,
|
|
56
|
+
perStoryTotalMs: metrics.timing?.perStoryTotalMs,
|
|
57
|
+
},
|
|
58
|
+
memory: metrics.memory,
|
|
59
|
+
tabs: {
|
|
60
|
+
concurrentTabs,
|
|
61
|
+
crashed: metrics.tabs?.tabsCrashed,
|
|
62
|
+
replaced: metrics.tabs?.tabsReplaced,
|
|
63
|
+
},
|
|
64
|
+
settings: {
|
|
65
|
+
componentConcurrency: account.serverConcurrency?.componentConcurrency,
|
|
66
|
+
concurrentTabs,
|
|
67
|
+
flags: pickSettings(config),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {buildExecutionSummaryEvent, pickSettings, SETTINGS_DENY_LIST};
|
package/src/eyesStorybook.js
CHANGED
|
@@ -26,6 +26,8 @@ const {makeCore} = require('@applitools/core');
|
|
|
26
26
|
const makeGetStoriesWithConfig = require('./getStoriesWithConfig');
|
|
27
27
|
const {makeNetworkUtils} = require('./utils/pageNetworkUtils');
|
|
28
28
|
const {readStoriesTimeout: defaultReadStoriesTimeout} = require('./defaultConfig');
|
|
29
|
+
const {makeRunMetrics} = require('./runMetrics');
|
|
30
|
+
const {buildExecutionSummaryEvent} = require('./buildExecutionSummaryEvent');
|
|
29
31
|
|
|
30
32
|
async function eyesStorybook({
|
|
31
33
|
config,
|
|
@@ -43,6 +45,11 @@ async function eyesStorybook({
|
|
|
43
45
|
let transitioning = false;
|
|
44
46
|
logger.log('eyesStorybook started');
|
|
45
47
|
|
|
48
|
+
const runStart = Date.now();
|
|
49
|
+
// Run-scope counts, populated during the pipeline and read by the summary event
|
|
50
|
+
// even if the run later fails partway through (best-effort observability).
|
|
51
|
+
const counts = {};
|
|
52
|
+
|
|
46
53
|
const CONCURRENT_TABS = isNaN(Number(process.env.APPLITOOLS_CONCURRENT_TABS))
|
|
47
54
|
? 3
|
|
48
55
|
: Number(process.env.APPLITOOLS_CONCURRENT_TABS);
|
|
@@ -64,6 +71,11 @@ async function eyesStorybook({
|
|
|
64
71
|
const browser = await puppeteer.launch(config.puppeteerOptions);
|
|
65
72
|
logger.log('browser launched');
|
|
66
73
|
|
|
74
|
+
// Best-effort per-execution metrics (memory / timing / tab reliability). The
|
|
75
|
+
// sampler is unref'd and every sample is guarded — it can never keep the
|
|
76
|
+
// process alive or affect the run.
|
|
77
|
+
const metrics = makeRunMetrics({browser, logger});
|
|
78
|
+
|
|
67
79
|
const environment = extractEnvironment(addonVersion);
|
|
68
80
|
const core = await makeCore({spec, agentId: config.agentId, environment, logger});
|
|
69
81
|
const manager = await core.makeManager({
|
|
@@ -87,6 +99,11 @@ async function eyesStorybook({
|
|
|
87
99
|
throw error;
|
|
88
100
|
});
|
|
89
101
|
|
|
102
|
+
// Start the best-effort metrics sampler only once getAccountInfo has succeeded —
|
|
103
|
+
// its .catch closes the browser and rethrows without reaching the `finally` that
|
|
104
|
+
// would otherwise stop the sampler.
|
|
105
|
+
metrics.start();
|
|
106
|
+
|
|
90
107
|
const getStoriesWithConfig = makeGetStoriesWithConfig({config});
|
|
91
108
|
|
|
92
109
|
const initPage = makeInitPage({
|
|
@@ -96,8 +113,72 @@ async function eyesStorybook({
|
|
|
96
113
|
logger,
|
|
97
114
|
getTransitiongIntoIE,
|
|
98
115
|
getRenderIE,
|
|
116
|
+
onTabCrashed: metrics.incrementTabsCrashed,
|
|
117
|
+
});
|
|
118
|
+
const pagePool = createPagePool({
|
|
119
|
+
initPage,
|
|
120
|
+
logger,
|
|
121
|
+
onPageReplaced: metrics.incrementTabsReplaced,
|
|
99
122
|
});
|
|
100
|
-
|
|
123
|
+
|
|
124
|
+
let summaryEmitted = false;
|
|
125
|
+
let runStatus = 'finished';
|
|
126
|
+
let resultSummary;
|
|
127
|
+
|
|
128
|
+
// Emit the one-time per-execution summary event (-> Coralogix). Best-effort and
|
|
129
|
+
// idempotent: awaited so the batched logEvent actually flushes + sends before we
|
|
130
|
+
// exit, guarded so we never double-send, and fully swallowed on failure so it can
|
|
131
|
+
// never affect the run's outcome.
|
|
132
|
+
async function emitExecutionSummary(status, summary) {
|
|
133
|
+
if (summaryEmitted) return;
|
|
134
|
+
summaryEmitted = true;
|
|
135
|
+
try {
|
|
136
|
+
await metrics.stop(); // final sample while the browser is still alive
|
|
137
|
+
const event = buildExecutionSummaryEvent({
|
|
138
|
+
status,
|
|
139
|
+
config,
|
|
140
|
+
environment,
|
|
141
|
+
account,
|
|
142
|
+
concurrentTabs: CONCURRENT_TABS,
|
|
143
|
+
counts,
|
|
144
|
+
summary,
|
|
145
|
+
metrics: metrics.summarize(),
|
|
146
|
+
totalRunMs: Date.now() - runStart,
|
|
147
|
+
renderStoriesMs: performance['renderStories'],
|
|
148
|
+
});
|
|
149
|
+
logger.log('eyesStorybook sending execution summary event', event);
|
|
150
|
+
await core.logEvent({
|
|
151
|
+
settings: {
|
|
152
|
+
eyesServerUrl: config.eyesServerUrl,
|
|
153
|
+
apiKey: config.apiKey,
|
|
154
|
+
agentId: config.agentId,
|
|
155
|
+
proxy: config.proxy,
|
|
156
|
+
useDnsCache: config.useDnsCache,
|
|
157
|
+
level: 'Notice',
|
|
158
|
+
event,
|
|
159
|
+
},
|
|
160
|
+
logger,
|
|
161
|
+
});
|
|
162
|
+
} catch (err) {
|
|
163
|
+
logger.log('failed to emit execution summary event:', err && err.message);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// process 'exit' is synchronous-only and cannot perform the async, batched HTTP
|
|
168
|
+
// send — so cover hard terminations with signal handlers (which CAN run async).
|
|
169
|
+
// Registered now and removed in `finally` so we never leak a listener.
|
|
170
|
+
const makeSignalHandler = signalName => async () => {
|
|
171
|
+
logger.log(`eyesStorybook received ${signalName}, emitting summary before exit`);
|
|
172
|
+
await emitExecutionSummary('aborted', resultSummary);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
};
|
|
175
|
+
const signalHandlers = {
|
|
176
|
+
SIGINT: makeSignalHandler('SIGINT'),
|
|
177
|
+
SIGTERM: makeSignalHandler('SIGTERM'),
|
|
178
|
+
};
|
|
179
|
+
for (const [signalName, handler] of Object.entries(signalHandlers)) {
|
|
180
|
+
process.once(signalName, handler);
|
|
181
|
+
}
|
|
101
182
|
|
|
102
183
|
try {
|
|
103
184
|
const {stories, page} = await getStoriesWithSpinner();
|
|
@@ -121,6 +202,8 @@ async function eyesStorybook({
|
|
|
121
202
|
});
|
|
122
203
|
|
|
123
204
|
const filteredStories = filterStories({stories, config});
|
|
205
|
+
counts.totalStories = stories.length;
|
|
206
|
+
counts.afterFilter = filteredStories.length;
|
|
124
207
|
|
|
125
208
|
// Log filtering and sharding results
|
|
126
209
|
logger.log(`${stories.length} total stories found`);
|
|
@@ -141,10 +224,14 @@ async function eyesStorybook({
|
|
|
141
224
|
`there are ${storiesIncludingVariations.length} stories after filtering and adding variations `,
|
|
142
225
|
);
|
|
143
226
|
|
|
227
|
+
counts.afterVariations = storiesIncludingVariations.length;
|
|
228
|
+
|
|
144
229
|
const storiesByBrowserWithConfig = getStoriesWithConfig({
|
|
145
230
|
stories: storiesIncludingVariations,
|
|
146
231
|
logger,
|
|
147
232
|
});
|
|
233
|
+
counts.normal = storiesByBrowserWithConfig.stories.length;
|
|
234
|
+
counts.fakeIE = storiesByBrowserWithConfig.storiesWithIE.length;
|
|
148
235
|
|
|
149
236
|
logger.log(
|
|
150
237
|
`starting to run ${storiesByBrowserWithConfig.stories.length} normal stories ("non fake IE") and ${storiesByBrowserWithConfig.storiesWithIE.length} "fake IE stories"`,
|
|
@@ -178,6 +265,7 @@ async function eyesStorybook({
|
|
|
178
265
|
pagePool,
|
|
179
266
|
eventEmitter,
|
|
180
267
|
signal,
|
|
268
|
+
recordStoryTime: metrics.recordStoryTime,
|
|
181
269
|
});
|
|
182
270
|
|
|
183
271
|
logger.log('finished creating functions');
|
|
@@ -196,6 +284,7 @@ async function eyesStorybook({
|
|
|
196
284
|
}),
|
|
197
285
|
);
|
|
198
286
|
if (signal.aborted) {
|
|
287
|
+
runStatus = 'aborted';
|
|
199
288
|
if (error) {
|
|
200
289
|
const msg = refineErrorMessage({prefix: 'Error in executeRenders:', error});
|
|
201
290
|
logger.log('Error in executeRenders:', error);
|
|
@@ -211,17 +300,27 @@ async function eyesStorybook({
|
|
|
211
300
|
if (errorInGetResults) {
|
|
212
301
|
logger.log('failed to get results', errorInGetResults);
|
|
213
302
|
}
|
|
303
|
+
resultSummary = testResultsSummary;
|
|
214
304
|
|
|
215
305
|
if (error) {
|
|
306
|
+
runStatus = 'failed';
|
|
216
307
|
const msg = refineErrorMessage({prefix: 'Error in executeRenders:', error});
|
|
217
308
|
logger.log('Error in executeRenders:', error);
|
|
218
309
|
throw new Error(msg);
|
|
219
310
|
} else {
|
|
220
311
|
return {summary: testResultsSummary, results};
|
|
221
312
|
}
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (runStatus === 'finished') runStatus = 'failed';
|
|
315
|
+
throw err;
|
|
222
316
|
} finally {
|
|
223
317
|
logger.log('total time: ', performance['renderStories']);
|
|
224
318
|
logger.log('perf results', performance);
|
|
319
|
+
// emit BEFORE closing the browser so the final memory sample is still valid
|
|
320
|
+
await emitExecutionSummary(runStatus, resultSummary);
|
|
321
|
+
for (const [signalName, handler] of Object.entries(signalHandlers)) {
|
|
322
|
+
process.removeListener(signalName, handler);
|
|
323
|
+
}
|
|
225
324
|
pagePool.isClosed = true;
|
|
226
325
|
await browser.close();
|
|
227
326
|
}
|
package/src/initPage.js
CHANGED
|
@@ -4,7 +4,15 @@ const fakeIE = require('./fakeIE');
|
|
|
4
4
|
const {makeNetworkUtils} = require('./utils/pageNetworkUtils');
|
|
5
5
|
const utils = require('@applitools/utils');
|
|
6
6
|
|
|
7
|
-
function makeInitPage({
|
|
7
|
+
function makeInitPage({
|
|
8
|
+
iframeUrl,
|
|
9
|
+
config,
|
|
10
|
+
browser,
|
|
11
|
+
logger,
|
|
12
|
+
getTransitiongIntoIE,
|
|
13
|
+
getRenderIE,
|
|
14
|
+
onTabCrashed = () => {},
|
|
15
|
+
}) {
|
|
8
16
|
const networkUtils = makeNetworkUtils({logger});
|
|
9
17
|
return async function initPage({pageId, pagePool}) {
|
|
10
18
|
logger.log('initializing puppeteer page number ', pageId);
|
|
@@ -60,6 +68,7 @@ function makeInitPage({iframeUrl, config, browser, logger, getTransitiongIntoIE,
|
|
|
60
68
|
|
|
61
69
|
page.on('error', async err => {
|
|
62
70
|
logger.log(`Puppeteer error for page ${pageId}:`, err);
|
|
71
|
+
onTabCrashed(); // best-effort metric; the 'error' event is Chromium "Page crashed!"
|
|
63
72
|
await pagePool.removeAndAddPage(pageId);
|
|
64
73
|
});
|
|
65
74
|
|
package/src/pagePool.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
function createPagePool({initPage, logger, freePageTimeout = 300_000}) {
|
|
3
|
+
function createPagePool({initPage, logger, freePageTimeout = 300_000, onPageReplaced = () => {}}) {
|
|
4
4
|
let counter = 0;
|
|
5
5
|
const fullPageObjs = [];
|
|
6
6
|
logger.log(`[page pool] created`);
|
|
@@ -43,6 +43,7 @@ function createPagePool({initPage, logger, freePageTimeout = 300_000}) {
|
|
|
43
43
|
return pagePool;
|
|
44
44
|
|
|
45
45
|
async function removeAndAddPage(pageId) {
|
|
46
|
+
onPageReplaced(); // best-effort metric: a pooled tab was torn down and replaced
|
|
46
47
|
pagePool.removePage(pageId);
|
|
47
48
|
const {pageId: newPageId} = await pagePool.createPage();
|
|
48
49
|
pagePool.addToPool(newPageId);
|
package/src/renderStories.js
CHANGED
|
@@ -17,6 +17,7 @@ function makeRenderStories({
|
|
|
17
17
|
maxPageTTL = 60000,
|
|
18
18
|
eventEmitter = new EventEmitter(),
|
|
19
19
|
signal = new AbortController().signal,
|
|
20
|
+
recordStoryTime = () => {},
|
|
20
21
|
}) {
|
|
21
22
|
const sparePages = {
|
|
22
23
|
queue: [],
|
|
@@ -142,6 +143,9 @@ function makeRenderStories({
|
|
|
142
143
|
const story = stories[currIndex++];
|
|
143
144
|
const storyUrl = getStoryUrl(story, storybookUrl);
|
|
144
145
|
const title = getStoryBaselineName(story);
|
|
146
|
+
// measure the FULL per-story chain (snapshot + open + checkAndClose),
|
|
147
|
+
// unlike performance[baselineName] which covers only open + checkAndClose.
|
|
148
|
+
const storyStart = Date.now();
|
|
145
149
|
try {
|
|
146
150
|
let [error, storyData] = await presult(
|
|
147
151
|
getStoryData({
|
|
@@ -202,6 +206,8 @@ function makeRenderStories({
|
|
|
202
206
|
} catch (ex) {
|
|
203
207
|
logger.log(`[page ${pageId}] error while processing story "${title}". ${ex}`);
|
|
204
208
|
return onDoneStory(ex, story);
|
|
209
|
+
} finally {
|
|
210
|
+
recordStoryTime(Date.now() - storyStart);
|
|
205
211
|
}
|
|
206
212
|
}
|
|
207
213
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const utils = require('@applitools/utils');
|
|
4
|
+
|
|
5
|
+
// Collects per-execution metrics for the one-time summary logEvent (-> Coralogix).
|
|
6
|
+
//
|
|
7
|
+
// Everything here is ADDITIVE observability and best-effort: a failure to sample
|
|
8
|
+
// any metric is swallowed (logged at debug) and the run is unaffected. The memory
|
|
9
|
+
// sampler runs on an interval that is `.unref()`'d so it can NEVER keep the
|
|
10
|
+
// process alive past the end of a run.
|
|
11
|
+
|
|
12
|
+
const SAMPLE_INTERVAL_MS = 2000;
|
|
13
|
+
|
|
14
|
+
function makeStats() {
|
|
15
|
+
let count = 0;
|
|
16
|
+
let sum = 0;
|
|
17
|
+
let min = Infinity;
|
|
18
|
+
let max = -Infinity;
|
|
19
|
+
return {
|
|
20
|
+
add(n) {
|
|
21
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) return;
|
|
22
|
+
count += 1;
|
|
23
|
+
sum += n;
|
|
24
|
+
if (n < min) min = n;
|
|
25
|
+
if (n > max) max = n;
|
|
26
|
+
},
|
|
27
|
+
summary() {
|
|
28
|
+
if (count === 0) return null;
|
|
29
|
+
return {min, max, avg: Math.round(sum / count), samples: count};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {object} options
|
|
36
|
+
* @param {import('puppeteer').Browser} [options.browser]
|
|
37
|
+
* @param {{log: Function}} options.logger
|
|
38
|
+
*/
|
|
39
|
+
function makeRunMetrics({browser, logger} = {}) {
|
|
40
|
+
const nodeRss = makeStats();
|
|
41
|
+
const nodeHeapUsed = makeStats();
|
|
42
|
+
const browserTreeRss = makeStats();
|
|
43
|
+
const perStoryTotalMs = makeStats();
|
|
44
|
+
const counters = {tabsCrashed: 0, tabsReplaced: 0};
|
|
45
|
+
|
|
46
|
+
let timer;
|
|
47
|
+
let sampling = false;
|
|
48
|
+
let browserPid;
|
|
49
|
+
let started = false;
|
|
50
|
+
|
|
51
|
+
function start() {
|
|
52
|
+
if (started) return;
|
|
53
|
+
started = true;
|
|
54
|
+
try {
|
|
55
|
+
browserPid = browser?.process?.()?.pid;
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger?.log(`[metrics] could not resolve browser pid: ${err?.message}`);
|
|
58
|
+
}
|
|
59
|
+
// immediate first sample, then on interval
|
|
60
|
+
void sampleOnce();
|
|
61
|
+
timer = setInterval(() => void sampleOnce(), SAMPLE_INTERVAL_MS);
|
|
62
|
+
// never let the metrics sampler keep the process alive
|
|
63
|
+
timer.unref?.();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function sampleOnce() {
|
|
67
|
+
// node memory — synchronous, always cheap, always safe
|
|
68
|
+
try {
|
|
69
|
+
const mem = process.memoryUsage();
|
|
70
|
+
nodeRss.add(mem.rss);
|
|
71
|
+
nodeHeapUsed.add(mem.heapUsed);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger?.log(`[metrics] node memory sample failed: ${err?.message}`);
|
|
74
|
+
}
|
|
75
|
+
// browser process-tree memory — async, may be unavailable; skip if a previous
|
|
76
|
+
// browser sample is still in flight to avoid pile-up on slow platforms.
|
|
77
|
+
if (sampling || !browserPid) return;
|
|
78
|
+
sampling = true;
|
|
79
|
+
try {
|
|
80
|
+
const bytes = await utils.process.sampleProcessTreeMemory(browserPid, {logger});
|
|
81
|
+
if (typeof bytes === 'number') browserTreeRss.add(bytes);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logger?.log(`[metrics] browser memory sample failed: ${err?.message}`);
|
|
84
|
+
} finally {
|
|
85
|
+
sampling = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function recordStoryTime(ms) {
|
|
90
|
+
perStoryTotalMs.add(ms);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function incrementTabsCrashed() {
|
|
94
|
+
counters.tabsCrashed += 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function incrementTabsReplaced() {
|
|
98
|
+
counters.tabsReplaced += 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Stop sampling and take one final sample. Idempotent.
|
|
102
|
+
async function stop() {
|
|
103
|
+
if (timer) {
|
|
104
|
+
clearInterval(timer);
|
|
105
|
+
timer = undefined;
|
|
106
|
+
}
|
|
107
|
+
await sampleOnce();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function summarize() {
|
|
111
|
+
return {
|
|
112
|
+
memory: {
|
|
113
|
+
nodeRssBytes: nodeRss.summary(),
|
|
114
|
+
nodeHeapUsedBytes: nodeHeapUsed.summary(),
|
|
115
|
+
browserTreeRssBytes: browserTreeRss.summary(),
|
|
116
|
+
},
|
|
117
|
+
timing: {
|
|
118
|
+
perStoryTotalMs: perStoryTotalMs.summary(),
|
|
119
|
+
},
|
|
120
|
+
tabs: {...counters},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
start,
|
|
126
|
+
stop,
|
|
127
|
+
recordStoryTime,
|
|
128
|
+
incrementTabsCrashed,
|
|
129
|
+
incrementTabsReplaced,
|
|
130
|
+
summarize,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {makeRunMetrics, makeStats};
|