@applitools/eyes-storybook 3.65.8 → 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 CHANGED
@@ -1,5 +1,65 @@
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
+
3
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)
4
64
 
5
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applitools/eyes-storybook",
3
- "version": "3.65.8",
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.1",
63
- "@applitools/driver": "1.26.3",
64
- "@applitools/eyes": "1.43.3",
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.12",
67
+ "@applitools/logger": "2.2.13",
67
68
  "@applitools/monitoring-commons": "1.0.19",
68
- "@applitools/spec-driver-puppeteer": "1.8.3",
69
- "@applitools/ufg-client": "1.22.3",
70
- "@applitools/utils": "1.14.5",
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.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};
@@ -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
- const pagePool = createPagePool({initPage, logger});
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({iframeUrl, config, browser, logger, getTransitiongIntoIE, getRenderIE}) {
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);
@@ -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};