sitespeed.io 21.2.2 → 21.6.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,35 @@
1
1
  # CHANGELOG - sitespeed.io (we use [semantic versioning](https://semver.org))
2
2
 
3
+ ## 21.6.0 - 2022-01-24
4
+ ### Added
5
+ * Updated to Edge stable release in the Docker container.
6
+ * Remove Crux distribution table and use pie charts instead [#3537](https://github.com/sitespeedio/sitespeed.io/pull/3537)
7
+ * Add extra sleep time between Crux calls to make sure to not overload the API limit [#3536](https://github.com/sitespeedio/sitespeed.io/pull/3536).
8
+ * Added extra Crux enable command line `--crux.enable` to enable Crux [#3538](https://github.com/sitespeedio/sitespeed.io/pull/3538). Its default value is `true` and you also need to supply the Crux key to run Crux. The reason for the new parameter is that you can now configure the key in your configuration JSON and set the enable to false and then you enable it with the CLI parameter when you actually need to run Crux.
9
+ * Show Crux-metrics on the Summary page [#3540](https://github.com/sitespeedio/sitespeed.io/pull/3540).
10
+ * Updated summary metrics tables with headings to make it easier to read [#3541](https://github.com/sitespeedio/sitespeed.io/pull/3541).
11
+ * Added [Browsertime 14.17.0](https://github.com/sitespeedio/browsertime/blob/main/CHANGELOG.md#14170---2022-01-23) with new Select and click.byName commands. With that Browsertime version you also need to have ffprobe installed when you run Visual Metrics but that should already be installed.
12
+ * Added [Browsertime 14.18.0](https://github.com/sitespeedio/browsertime/blob/main/CHANGELOG.md#14180---2022-01-24) with a fix for Firefox [#1698](https://github.com/sitespeedio/browsertime/issues/1698)
13
+
14
+ ## 21.5.0 - 2022-01-14
15
+ ### Added
16
+ * Upgraded to [Browsertime 14.15.0](https://github.com/sitespeedio/browsertime/blob/main/CHANGELOG.md#14150---2022-01-12) that adds support for `--appendToUserAgent` for Chrome/Edge/Firefox. And then Browsertime 14.16.0 that supports Geckodriver for Raspberry Pi.
17
+ ## 21.4.0 - 2022-01-12
18
+ ### Added
19
+ * Updated to a new build of WebPageReplay in the Docker container
20
+ * Updated the Ubuntu base image to latest version and latest NodeJS in the Docketr container.
21
+ * Upgraded Browsertime [#3528](https://github.com/sitespeedio/sitespeed.io/pull/3528):
22
+ * Add support for Humble as connectivity engine for mobile phone testing. Make sure to setup Humble on a Raspberry Pi 4 and the choose engine with --connectivity.engine humble and set the URL to your instance --connectivity.humble.url http://raspberrypi.local:3000. Added in #1691.
23
+ * Upgraded to Chrome 97 and Edge 97 in the Docker container.
24
+ * Upgraded to Chromedriver 97.
25
+ ### Fixed
26
+ * Updated Chromedriver library that automatically picks up the Chromedriver if it's installed on Raspberry Pi.
27
+ ## 21.3.0 - 2022-01-01
28
+ ### Added
29
+ * Updated to [Browsertime 14.13.0](https://github.com/sitespeedio/browsertime/blob/main/CHANGELOG.md#14130---2021-12-30) with the following fixes for the user agent:
30
+ * Append text to Chrome/Edge user agent using `--chrome.appendToUserAgent`
31
+ * When you use Chrome/Edge and use a "emulated device" that will use the user agent that you provide using `--userAgent`. Else it will use the user agent from your emulated device setting.
32
+ * You can also use Edge to run emulated mobile with the same settings as Chrome.
3
33
  ## 21.2.2 - 2021-12-23
4
34
  ### Fixed
5
35
  * Fix the error introduced in 21.2.0 for "Include page summary URL in the result JSON"
@@ -10,7 +40,7 @@
10
40
  * It turns out that Firefox 95 doesn't work with the HAR export trigger and the workaround that worked in Firefox 94 seems to not work in 95 see https://github.com/sitespeedio/browsertime/issues/1671#issuecomment-999412035. That's why we are reverting to Firefox 94 in the Docker containers.
11
41
  ## 21.2.0 - 2021-12-22
12
42
  ### Fixed
13
- * The catching of errors in the queue was broken and reported the error x times (x=numbert of plugins). Also when we had an error the result JSON was not stored. [#3522](https://github.com/sitespeedio/sitespeed.io/pull/3522).
43
+ * The catching of errors in the queue was broken and reported the error x times (x=number of plugins). Also when we had an error the result JSON was not stored. [#3522](https://github.com/sitespeedio/sitespeed.io/pull/3522).
14
44
 
15
45
  ### Added
16
46
  * Updated to Firefox 95 and Edge 96 in the Docker container.
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM sitespeedio/webbrowsers:chrome-96.0-firefox-94.0-edge-95.0-dev
1
+ FROM sitespeedio/webbrowsers:chrome-97.0-firefox-94.0-edge-97.0-b
2
2
 
3
3
  ENV SITESPEED_IO_BROWSERTIME__XVFB true
4
4
  ENV SITESPEED_IO_BROWSERTIME__DOCKER true
package/lib/cli/cli.js CHANGED
@@ -113,6 +113,18 @@ function validateInput(argv) {
113
113
  return 'Error: You have a miss match between number of alias for groups and URLs.';
114
114
  }
115
115
 
116
+ if (
117
+ argv.browsertime.connectivity &&
118
+ argv.browsertime.connectivity.engine === 'humble'
119
+ ) {
120
+ if (
121
+ !argv.browsertime.connectivity.humble ||
122
+ !argv.browsertime.connectivity.humble.url
123
+ ) {
124
+ return 'You need to specify the URL to Humble by using the --browsertime.connectivity.humble.url option.';
125
+ }
126
+ }
127
+
116
128
  if (
117
129
  argv.browsertime.safari &&
118
130
  argv.browsertime.safari.useSimulator &&
@@ -291,12 +303,19 @@ module.exports.parseCommandLine = function parseCommandLine() {
291
303
  .option('browsertime.connectivity.engine', {
292
304
  alias: 'connectivity.engine',
293
305
  default: 'external',
294
- choices: ['external', 'throttle', 'tsproxy'],
306
+ choices: ['external', 'throttle', 'tsproxy', 'humble'],
295
307
  describe:
296
- 'The engine for connectivity. Throttle works on Mac and tc based Linux. Use external if you set the connectivity outside of Browsertime. Use tsproxy if you are using Kubernetes. More documentation at https://www.sitespeed.io/documentation/sitespeed.io/connectivity/.',
308
+ 'The engine for connectivity. Throttle works on Mac and tc based Linux. For mobile you can use Humble if you have a Humble setup. Use external if you set the connectivity outside of Browsertime. Use tsproxy if you are using Kubernetes. More documentation at https://www.sitespeed.io/documentation/sitespeed.io/connectivity/.',
297
309
  type: 'string',
298
310
  group: 'Browser'
299
311
  })
312
+ .option('browsertime.connectivity.humble.url', {
313
+ alias: 'connectivity.humble.url',
314
+ type: 'string',
315
+ describe:
316
+ 'The path to your Humble instance. For example http://raspberrypi:3000',
317
+ group: 'Browser'
318
+ })
300
319
  .option('browsertime.pageCompleteCheck', {
301
320
  alias: 'pageCompleteCheck',
302
321
  describe:
@@ -372,6 +391,12 @@ module.exports.parseCommandLine = function parseCommandLine() {
372
391
  'The full User Agent string, defaults to the User Agent used by the browsertime.browser option.',
373
392
  group: 'Browser'
374
393
  })
394
+ .option('browsertime.appendToUserAgent', {
395
+ alias: 'appendToUserAgent',
396
+ describe:
397
+ 'Append a String to the user agent. Works in Chrome/Edge and Firefox.',
398
+ group: 'Browser'
399
+ })
375
400
  .option('browsertime.preURL', {
376
401
  alias: 'preURL',
377
402
  describe:
@@ -715,6 +740,12 @@ module.exports.parseCommandLine = function parseCommandLine() {
715
740
  default: true,
716
741
  group: 'Chrome'
717
742
  })
743
+ .option('browsertime.chrome.appendToUserAgent', {
744
+ alias: 'chrome.appendToUserAgent',
745
+ type: 'string',
746
+ describe: 'Append to the user agent.',
747
+ group: 'Chrome'
748
+ })
718
749
  .option('browsertime.chrome.android.package', {
719
750
  alias: 'chrome.android.package',
720
751
  describe:
@@ -100,7 +100,7 @@ module.exports = {
100
100
  // set mobile options
101
101
  if (options.mobile) {
102
102
  btOptions.viewPort = '360x640';
103
- if (btOptions.browser === 'chrome') {
103
+ if (btOptions.browser === 'chrome' || btOptions.browser === 'edge') {
104
104
  const emulation = get(
105
105
  btOptions,
106
106
  'chrome.mobileEmulation.deviceName',
@@ -109,8 +109,9 @@ module.exports = {
109
109
  btOptions.chrome.mobileEmulation = {
110
110
  deviceName: emulation
111
111
  };
112
+ } else {
113
+ btOptions.userAgent = iphone6UserAgent;
112
114
  }
113
- btOptions.userAgent = iphone6UserAgent;
114
115
  }
115
116
  const scriptCategories = await browserScripts.allScriptCategories;
116
117
  let scriptsByCategory = await browserScripts.getScriptsForCategories(
@@ -4,6 +4,12 @@ module.exports = {
4
4
  'You need to use a key to get data from CrUx. Get the key from https://developers.google.com/web/tools/chrome-user-experience-report/api/guides/getting-started#APIKey',
5
5
  group: 'CrUx'
6
6
  },
7
+ enable: {
8
+ default: true,
9
+ describe:
10
+ 'Enable the CrUx plugin. This is on by defauly but you also need the Crux key. If you chose to disable it with this key, set this to false and you can still use the CrUx key in your configuration.',
11
+ group: 'CrUx'
12
+ },
7
13
  formFactor: {
8
14
  default: 'ALL',
9
15
  type: 'string',
@@ -23,6 +23,12 @@ const DEFAULT_METRICS_SUMMARY = [
23
23
  'originLoadingExperience.*.CUMULATIVE_LAYOUT_SHIFT_SCORE.*'
24
24
  ];
25
25
 
26
+ function wait(ms) {
27
+ return new Promise(resolve => setTimeout(resolve, ms));
28
+ }
29
+
30
+ const CRUX_WAIT_TIME = 300;
31
+
26
32
  module.exports = {
27
33
  name() {
28
34
  return path.basename(__dirname);
@@ -30,159 +36,171 @@ module.exports = {
30
36
  open(context, options) {
31
37
  this.make = context.messageMaker('crux').make;
32
38
  this.options = merge({}, defaultConfig, options.crux);
33
- this.testedOrigins = {};
34
- throwIfMissing(options.crux, ['key'], 'crux');
35
- this.formFactors = Array.isArray(this.options.formFactor)
36
- ? this.options.formFactor
37
- : [this.options.formFactor];
38
- this.pug = fs.readFileSync(
39
- path.resolve(__dirname, 'pug', 'index.pug'),
40
- 'utf8'
41
- );
39
+ if (this.options.enable === true) {
40
+ this.testedOrigins = {};
41
+ throwIfMissing(options.crux, ['key'], 'crux');
42
+ this.formFactors = Array.isArray(this.options.formFactor)
43
+ ? this.options.formFactor
44
+ : [this.options.formFactor];
45
+ this.pug = fs.readFileSync(
46
+ path.resolve(__dirname, 'pug', 'index.pug'),
47
+ 'utf8'
48
+ );
49
+
50
+ if (this.options.collect === 'ALL' || this.options.collect === 'URL') {
51
+ context.filterRegistry.registerFilterForType(
52
+ DEFAULT_METRICS_PAGESUMMARY,
53
+ 'crux.pageSummary'
54
+ );
55
+ }
42
56
 
43
- if (this.options.collect === 'ALL' || this.options.collect === 'URL') {
44
57
  context.filterRegistry.registerFilterForType(
45
- DEFAULT_METRICS_PAGESUMMARY,
46
- 'crux.pageSummary'
58
+ DEFAULT_METRICS_SUMMARY,
59
+ 'crux.summary'
47
60
  );
48
61
  }
49
-
50
- context.filterRegistry.registerFilterForType(
51
- DEFAULT_METRICS_SUMMARY,
52
- 'crux.summary'
53
- );
54
62
  },
55
63
  async processMessage(message, queue) {
56
- const make = this.make;
57
- switch (message.type) {
58
- case 'sitespeedio.setup': {
59
- queue.postMessage(make('crux.setup'));
60
- // Add the HTML pugs
61
- queue.postMessage(
62
- make('html.pug', {
63
- id: 'crux',
64
- name: 'CrUx',
65
- pug: this.pug,
66
- type: 'pageSummary'
67
- })
68
- );
69
- queue.postMessage(
70
- make('html.pug', {
71
- id: 'crux',
72
- name: 'CrUx',
73
- pug: this.pug,
74
- type: 'run'
75
- })
76
- );
77
- break;
78
- }
79
- case 'url': {
80
- let url = message.url;
81
- let group = message.group;
82
- const originResult = { originLoadingExperience: {} };
83
- if (
84
- !this.testedOrigins[group] &&
85
- (this.options.collect === 'ALL' || this.options.collect === 'ORIGIN')
86
- ) {
87
- this.testedOrigins[group] = true;
88
- log.info(`Get CrUx data for domain ${group}`);
89
- for (let formFactor of this.formFactors) {
90
- originResult.originLoadingExperience[formFactor] = await send.get(
91
- url,
92
- this.options.key,
93
- formFactor,
94
- false
95
- );
96
-
97
- if (originResult.originLoadingExperience[formFactor].error) {
98
- log.info(
99
- `${originResult.originLoadingExperience[formFactor].error.message} for domain ${url} using ${formFactor}`
64
+ if (this.options.enable === true) {
65
+ const make = this.make;
66
+ switch (message.type) {
67
+ case 'sitespeedio.setup': {
68
+ queue.postMessage(make('crux.setup'));
69
+ // Add the HTML pugs
70
+ queue.postMessage(
71
+ make('html.pug', {
72
+ id: 'crux',
73
+ name: 'CrUx',
74
+ pug: this.pug,
75
+ type: 'pageSummary'
76
+ })
77
+ );
78
+ queue.postMessage(
79
+ make('html.pug', {
80
+ id: 'crux',
81
+ name: 'CrUx',
82
+ pug: this.pug,
83
+ type: 'run'
84
+ })
85
+ );
86
+ break;
87
+ }
88
+ case 'url': {
89
+ let url = message.url;
90
+ let group = message.group;
91
+ const originResult = { originLoadingExperience: {} };
92
+ if (
93
+ !this.testedOrigins[group] &&
94
+ (this.options.collect === 'ALL' ||
95
+ this.options.collect === 'ORIGIN')
96
+ ) {
97
+ this.testedOrigins[group] = true;
98
+ log.info(`Get CrUx data for domain ${group}`);
99
+ for (let formFactor of this.formFactors) {
100
+ originResult.originLoadingExperience[formFactor] = await send.get(
101
+ url,
102
+ this.options.key,
103
+ formFactor,
104
+ false
100
105
  );
101
106
 
102
- queue.postMessage(
103
- make(
104
- 'error',
105
- `${originResult.originLoadingExperience[formFactor].error.message} for domain ${url} using ${formFactor}`,
106
- {
107
- url
108
- }
109
- )
110
- );
111
- } else {
112
- try {
113
- originResult.originLoadingExperience[formFactor] = repackage(
114
- originResult.originLoadingExperience[formFactor]
107
+ if (originResult.originLoadingExperience[formFactor].error) {
108
+ log.info(
109
+ `${originResult.originLoadingExperience[formFactor].error.message} for domain ${url} using ${formFactor}`
115
110
  );
116
- } catch (e) {
117
- log.error(
118
- 'Could not repackage the JSON for origin from CrUx, is it broken? %j',
119
- originResult.originLoadingExperience[formFactor]
111
+
112
+ queue.postMessage(
113
+ make(
114
+ 'error',
115
+ `${originResult.originLoadingExperience[formFactor].error.message} for domain ${url} using ${formFactor}`,
116
+ {
117
+ url
118
+ }
119
+ )
120
120
  );
121
+ } else {
122
+ try {
123
+ originResult.originLoadingExperience[formFactor] = repackage(
124
+ originResult.originLoadingExperience[formFactor]
125
+ );
126
+ } catch (e) {
127
+ log.error(
128
+ 'Could not repackage the JSON for origin from CrUx, is it broken? %j',
129
+ originResult.originLoadingExperience[formFactor]
130
+ );
131
+ }
121
132
  }
133
+ await wait(CRUX_WAIT_TIME);
122
134
  }
135
+ queue.postMessage(make('crux.summary', originResult, { group }));
123
136
  }
124
- queue.postMessage(make('crux.summary', originResult, { group }));
125
- }
126
-
127
- if (this.options.collect === 'ALL' || this.options.collect === 'URL') {
128
- log.info(`Get CrUx data for url ${url}`);
129
- const urlResult = { loadingExperience: {} };
130
- for (let formFactor of this.formFactors) {
131
- urlResult.loadingExperience[formFactor] = await send.get(
132
- url,
133
- this.options.key,
134
- formFactor,
135
- true
136
- );
137
137
 
138
- if (urlResult.loadingExperience[formFactor].error) {
139
- log.info(
140
- `${urlResult.loadingExperience[formFactor].error.message} for ${url} using ${formFactor}`
138
+ if (
139
+ this.options.collect === 'ALL' ||
140
+ this.options.collect === 'URL'
141
+ ) {
142
+ log.info(`Get CrUx data for url ${url}`);
143
+ const urlResult = { loadingExperience: {} };
144
+ for (let formFactor of this.formFactors) {
145
+ urlResult.loadingExperience[formFactor] = await send.get(
146
+ url,
147
+ this.options.key,
148
+ formFactor,
149
+ true
141
150
  );
142
151
 
143
- queue.postMessage(
144
- make(
145
- 'error',
146
- `${urlResult.loadingExperience[formFactor].error.message} for ${url} using ${formFactor}`,
147
- {
148
- url
149
- }
150
- )
151
- );
152
- } else {
153
- try {
154
- urlResult.loadingExperience[formFactor] = repackage(
155
- urlResult.loadingExperience[formFactor]
152
+ if (urlResult.loadingExperience[formFactor].error) {
153
+ log.info(
154
+ `${urlResult.loadingExperience[formFactor].error.message} for ${url} using ${formFactor}`
156
155
  );
157
- } catch (e) {
158
- log.error(
159
- 'Could not repackage the JSON from CrUx, is it broken? %j',
160
- urlResult.loadingExperience[formFactor]
156
+
157
+ queue.postMessage(
158
+ make(
159
+ 'error',
160
+ `${urlResult.loadingExperience[formFactor].error.message} for ${url} using ${formFactor}`,
161
+ {
162
+ url
163
+ }
164
+ )
161
165
  );
166
+ } else {
167
+ try {
168
+ urlResult.loadingExperience[formFactor] = repackage(
169
+ urlResult.loadingExperience[formFactor]
170
+ );
171
+ } catch (e) {
172
+ log.error(
173
+ 'Could not repackage the JSON from CrUx, is it broken? %j',
174
+ urlResult.loadingExperience[formFactor]
175
+ );
176
+ }
162
177
  }
178
+ await wait(CRUX_WAIT_TIME);
163
179
  }
164
- }
165
- // Attach origin result so we can show it in the HTML
166
- urlResult.originLoadingExperience =
167
- originResult.originLoadingExperience;
180
+ // Attach origin result so we can show it in the HTML
181
+ urlResult.originLoadingExperience =
182
+ originResult.originLoadingExperience;
168
183
 
169
- queue.postMessage(
170
- make('crux.pageSummary', urlResult, {
171
- url,
172
- group
173
- })
174
- );
175
- } else {
176
- queue.postMessage(
177
- make(
178
- 'crux.pageSummary',
179
- { originLoadingExperience: originResult.originLoadingExperience },
180
- {
184
+ queue.postMessage(
185
+ make('crux.pageSummary', urlResult, {
181
186
  url,
182
187
  group
183
- }
184
- )
185
- );
188
+ })
189
+ );
190
+ } else {
191
+ queue.postMessage(
192
+ make(
193
+ 'crux.pageSummary',
194
+ {
195
+ originLoadingExperience: originResult.originLoadingExperience
196
+ },
197
+ {
198
+ url,
199
+ group
200
+ }
201
+ )
202
+ );
203
+ }
186
204
  }
187
205
  }
188
206
  }
@@ -11,7 +11,7 @@ mixin sizeCell(title, size)
11
11
  td.number(data-title=title, data-value= size)= h.size.format(size)
12
12
 
13
13
  - const crux = pageInfo.data.crux.pageSummary;
14
- - const metrics = {first_contentful_paint:'First Contentful Paint (FCP)', largest_contentful_paint: 'Largest Contentful Paint (LCP)', first_input_delay:'First Input Delay (FID)', cumulative_layout_shift: 'Cumulative Layout Shift'};
14
+ - const metrics = {first_contentful_paint:'First Contentful Paint (FCP)', largest_contentful_paint: 'Largest Contentful Paint (LCP)', first_input_delay:'First Input Delay (FID)', cumulative_layout_shift: 'Cumulative Layout Shift (CLS)'};
15
15
  - const experiences = ['loadingExperience','originLoadingExperience'];
16
16
 
17
17
  small
@@ -25,6 +25,10 @@ a#crux
25
25
  h2 CrUx
26
26
  p.small Chrome User Experience Report (CrUx) is powered by real user measurement across the public web, aggregated from users who have opted-in to syncing their browsing history, have not set up a Sync passphrase, and have usage statistic reporting enabled and is using Chrome.
27
27
 
28
+ p.small The CrUx data has four different buckets (form factor) depending on device: DESKTOP, PHONE, TABLET and ALL. You can choose which data to get with
29
+ code --crux.formFactor
30
+ | .
31
+
28
32
  each experience in experiences
29
33
  if experience === 'loadingExperience' && crux[experience]
30
34
  p Over the last 30 days, this is the field data for this page for Chrome users.
@@ -47,29 +51,57 @@ each experience in experiences
47
51
  if crux[experience][formFactor].data.record.metrics[key]
48
52
  tr
49
53
  td #{name} 75 percentile
50
- td #{crux[experience][formFactor].data.record.metrics[key].percentiles.p75} #{key.indexOf('cumulative') > -1 ? '': 'ms'}
54
+ td #{key.indexOf('cumulative') > -1 ? crux[experience][formFactor].data.record.metrics[key].percentiles.p75 : h.time.ms(crux[experience][formFactor].data.record.metrics[key].percentiles.p75)}
51
55
 
52
56
  h4 Distribution
53
- table
54
- each name, key in metrics
55
- if crux[experience][formFactor].data.record.metrics[key] && crux[experience][formFactor].data.record.metrics[key].histogram
56
- tr
57
- th #{name}
58
- th Min
59
- th Max
60
- th Users
61
- tr
62
- td Fast
63
- td #{crux[experience][formFactor].data.record.metrics[key].histogram[0].start}
64
- td #{crux[experience][formFactor].data.record.metrics[key].histogram[0].end}
65
- td #{Number(crux[experience][formFactor].data.record.metrics[key].histogram[0].density * 100).toFixed(2)} %
66
- tr
67
- td Moderate
68
- td #{crux[experience][formFactor].data.record.metrics[key].histogram[1].start}
69
- td #{crux[experience][formFactor].data.record.metrics[key].histogram[1].end}
70
- td #{Number(crux[experience][formFactor].data.record.metrics[key].histogram[1].density * 100).toFixed(2)} %
71
- tr
72
- td Slow
73
- td #{crux[experience][formFactor].data.record.metrics[key].histogram[2].start}
74
- td #{crux[experience][formFactor].data.record.metrics[key].histogram[2].end}
75
- td #{Number(crux[experience][formFactor].data.record.metrics[key].histogram[2].density * 100).toFixed(2)} %
57
+ - let cruxus = `${experience}.${formFactor}.data.record.metrics`;
58
+ - let FCPs = [Number(get(crux, `${cruxus}.first_contentful_paint.histogram[0].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.first_contentful_paint.histogram[1].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.first_contentful_paint.histogram[2].density`, 0)*100).toFixed(2)];
59
+
60
+ - let LCPs = [Number(get(crux, `${cruxus}.largest_contentful_paint.histogram[0].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.largest_contentful_paint.histogram[1].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.largest_contentful_paint.histogram[2].density`, 0)*100).toFixed(2)];
61
+
62
+ - let FIDs = [Number(get(crux, `${cruxus}.first_input_delay.histogram[0].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.first_input_delay.histogram[1].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.first_input_delay.histogram[2].density`, 0)*100).toFixed(2)];
63
+
64
+ - let CLSs = [Number(get(crux, `${cruxus}.cumulative_layout_shift.histogram[0].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.cumulative_layout_shift.histogram[1].density`, 0) * 100).toFixed(2), Number(get(crux, `${cruxus}.cumulative_layout_shift.histogram[2].density`, 0)*100).toFixed(2)];
65
+
66
+ script(type='text/javascript').
67
+ document.addEventListener("DOMContentLoaded", function() {
68
+
69
+ function drawPie(id, series, labels) {
70
+ new Chartist.Pie(id, {
71
+ series,
72
+ labels,
73
+ }, {
74
+ showLabel: false,
75
+ plugins: [
76
+ Chartist.plugins.legend(
77
+ { clickable: false,
78
+ position: 'bottom'
79
+ }
80
+ )
81
+ ]
82
+ });
83
+ }
84
+
85
+ drawPie('#chartFCP#{experience + formFactor}', [#{FCPs}], ['Fast: #{FCPs[0]}%', 'Moderate #{FCPs[1]}%', 'Slow: #{FCPs[2]}%']);
86
+ drawPie('#chartLCP#{experience + formFactor}', [#{LCPs}], ['Fast: #{LCPs[0]}%', 'Moderate #{LCPs[1]}%', 'Slow: #{LCPs[2]}%']);
87
+ drawPie('#chartFID#{experience + formFactor}', [#{FIDs}], ['Fast: #{FIDs[0]}%', 'Moderate #{FIDs[1]}%', 'Slow: #{FIDs[2]}%']);
88
+ drawPie('#chartCLS#{experience + formFactor}', [#{CLSs}], ['Good: #{CLSs[0]}%', 'Need improvement: #{CLSs[1]}%', 'Poor: #{CLSs[2]}%']);
89
+
90
+
91
+ });
92
+ .responsive
93
+ table
94
+ tr
95
+ th #{metrics['first_contentful_paint']}
96
+ th #{metrics['largest_contentful_paint']}
97
+ th #{metrics['first_input_delay']}
98
+ th #{metrics['cumulative_layout_shift']}
99
+ tr
100
+ td(data-title=metrics['first_contentful_paint'])
101
+ .ct-chart(id='chartFCP' + experience + formFactor)
102
+ td(data-title=metrics['largest_contentful_paint'])
103
+ .ct-chart(id='chartLCP' + experience + formFactor)
104
+ td(data-title=metrics['first_input_delay'])
105
+ .ct-chart(id='chartFID' + experience + formFactor)
106
+ td(data-title=metrics['cumulative_layout_shift'])
107
+ .ct-chart(id='chartCLS' + experience + formFactor)