Skip to content

Commit d712e0d

Browse files
authored
Merge pull request #237 from GoogleChrome/attribution
Add a new attribution build for debugging issues in the field
2 parents c8d0ee2 + fd994ef commit d712e0d

Some content is hidden

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

59 files changed

+2877
-319
lines changed

README.md

Lines changed: 374 additions & 20 deletions
Large diffs are not rendered by default.

attribution.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
Copyright 2022 Google LLC
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
https://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
export * from './dist/modules/attribution.js';

attribution.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
Copyright 2022 Google LLC
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
https://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
// Creates the `web-vitals/attribution` import in node-based bundlers.
17+
// This will not be needed when export maps are widely supported.
18+
export * from './dist/web-vitals.attribution.js';

base.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
1+
/*
2+
Copyright 2022 Google LLC
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
https://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
116
export * from './dist/modules/index.js';

base.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
/*
2+
Copyright 2022 Google LLC
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
https://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
116
// Creates the `web-vitals/base` import in node-based bundlers.
217
// This will not be needed when export maps are widely supported.
318
export * from './dist/web-vitals.base.js';

package.json

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,88 @@
22
"name": "web-vitals",
33
"version": "3.0.0-beta.2",
44
"description": "Easily measure performance metrics in JavaScript",
5+
"type": "module",
6+
"typings": "dist/modules/index.d.ts",
57
"main": "dist/web-vitals.umd.js",
68
"module": "dist/web-vitals.js",
7-
"typings": "dist/modules/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/modules/index.d.ts",
12+
"require": "./dist/web-vitals.umd.js",
13+
"default": "./dist/web-vitals.js"
14+
},
15+
"./base": {
16+
"types": "./dist/modules/index.d.ts",
17+
"require": "./dist/web-vitals.base.umd.js",
18+
"default": "./dist/web-vitals.base.js"
19+
},
20+
"./base.js": {
21+
"types": "./dist/modules/index.d.ts",
22+
"require": "./dist/web-vitals.base.umd.js",
23+
"default": "./dist/web-vitals.base.js"
24+
},
25+
"./attribution": {
26+
"types": "./dist/modules/attribution.d.ts",
27+
"require": "./dist/web-vitals.attribution.umd.js",
28+
"default": "./dist/web-vitals.attribution.js"
29+
},
30+
"./attribution.js": {
31+
"types": "./dist/modules/attribution.d.ts",
32+
"require": "./dist/web-vitals.attribution.umd.js",
33+
"default": "./dist/web-vitals.attribution.js"
34+
},
35+
"./onCLS.js": {
36+
"types": "./dist/modules/onCLS.d.ts",
37+
"default": "./dist/modules/onCLS.js"
38+
},
39+
"./onFCP.js": {
40+
"types": "./dist/modules/onFCP.d.ts",
41+
"default": "./dist/modules/onFCP.js"
42+
},
43+
"./onFID.js": {
44+
"types": "./dist/modules/onFID.d.ts",
45+
"default": "./dist/modules/onFID.js"
46+
},
47+
"./onINP.js": {
48+
"types": "./dist/modules/onINP.d.ts",
49+
"default": "./dist/modules/onINP.js"
50+
},
51+
"./onLCP.js": {
52+
"types": "./dist/modules/onLCP.d.ts",
53+
"default": "./dist/modules/onLCP.js"
54+
},
55+
"./onTTFB.js": {
56+
"types": "./dist/modules/onTTFB.d.ts",
57+
"default": "./dist/modules/onTTFB.js"
58+
},
59+
"./attribution/onCLS.js": {
60+
"types": "./dist/modules/attribution/onCLS.d.ts",
61+
"default": "./dist/modules/attribution/onCLS.js"
62+
},
63+
"./attribution/onFCP.js": {
64+
"types": "./dist/modules/attribution/onFCP.d.ts",
65+
"default": "./dist/modules/attribution/onFCP.js"
66+
},
67+
"./attribution/onFID.js": {
68+
"types": "./dist/modules/attribution/onFID.d.ts",
69+
"default": "./dist/modules/attribution/onFID.js"
70+
},
71+
"./attribution/onINP.js": {
72+
"types": "./dist/modules/attribution/onINP.d.ts",
73+
"default": "./dist/modules/attribution/onINP.js"
74+
},
75+
"./attribution/onLCP.js": {
76+
"types": "./dist/modules/attribution/onLCP.d.ts",
77+
"default": "./dist/modules/attribution/onLCP.js"
78+
},
79+
"./attribution/onTTFB.js": {
80+
"types": "./dist/modules/attribution/onTTFB.d.ts",
81+
"default": "./dist/modules/attribution/onTTFB.js"
82+
}
83+
},
884
"files": [
85+
"attribution.js",
86+
"attribution.d.ts",
987
"base.js",
1088
"base.d.ts",
1189
"dist",
@@ -24,7 +102,7 @@
24102
"release:minor": "npm version minor -m 'Release v%s' && npm publish",
25103
"release:patch": "npm version patch -m 'Release v%s' && npm publish",
26104
"test": "npm-run-all build -p -r test:*",
27-
"test:e2e": "wdio wdio.conf.js",
105+
"test:e2e": "wdio wdio.conf.cjs",
28106
"test:server": "node test/server.js",
29107
"start": "run-s build:ts test:server watch",
30108
"watch": "run-p watch:*",
@@ -37,9 +115,11 @@
37115
"crux",
38116
"performance",
39117
"metrics",
118+
"Core Web Vitals",
40119
"CLS",
41120
"FCP",
42121
"FID",
122+
"INP",
43123
"LCP",
44124
"TTFB"
45125
],

rollup.config.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,32 @@ const configs = [
105105
},
106106
plugins: configurePlugins({module: false}),
107107
},
108+
{
109+
input: 'dist/modules/attribution.js',
110+
output: {
111+
format: 'esm',
112+
file: './dist/web-vitals.attribution.js',
113+
},
114+
plugins: configurePlugins({module: true, polyfill: false}),
115+
},
116+
{
117+
input: 'dist/modules/attribution.js',
118+
output: {
119+
format: 'umd',
120+
file: `./dist/web-vitals.attribution.umd.js`,
121+
name: 'webVitals',
122+
},
123+
plugins: configurePlugins({module: false, polyfill: false}),
124+
},
125+
{
126+
input: 'dist/modules/attribution.js',
127+
output: {
128+
format: 'iife',
129+
file: './dist/web-vitals.attribution.iife.js',
130+
name: 'webVitals',
131+
},
132+
plugins: configurePlugins({module: false, polyfill: false}),
133+
},
108134
];
109135

110136
export default configs;

src/attribution.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export {onCLS} from './attribution/onCLS.js';
18+
export {onFCP} from './attribution/onFCP.js';
19+
export {onFID} from './attribution/onFID.js';
20+
export {onINP} from './attribution/onINP.js';
21+
export {onLCP} from './attribution/onLCP.js';
22+
export {onTTFB} from './attribution/onTTFB.js';
23+
24+
export * from './types.js';

src/attribution/onCLS.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {getLoadState} from '../lib/getLoadState.js';
18+
import {getSelector} from '../lib/getSelector.js';
19+
import {onCLS as unattributedOnCLS} from '../onCLS.js';
20+
import {CLSAttribution, CLSReportCallback, CLSReportCallbackWithAttribution, CLSMetric, CLSMetricWithAttribution, ReportOpts} from '../types.js';
21+
22+
23+
const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => {
24+
return entries.reduce((a, b) => a && a.value > b.value ? a : b);
25+
}
26+
27+
const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => {
28+
return sources.find((s) => s.node && s.node.nodeType === 1) || sources[0];
29+
}
30+
31+
const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {
32+
let attribution: CLSAttribution = {};
33+
if (metric.entries.length) {
34+
const largestEntry = getLargestLayoutShiftEntry(metric.entries);
35+
if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
36+
const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
37+
if (largestSource) {
38+
attribution = {
39+
largestShiftTarget: getSelector(largestSource.node),
40+
largestShiftTime: largestEntry.startTime,
41+
largestShiftValue: largestEntry.value,
42+
largestShiftSource: largestSource,
43+
largestShiftEntry: largestEntry,
44+
loadState: getLoadState(largestEntry.startTime),
45+
};
46+
}
47+
}
48+
}
49+
return Object.assign(metric, {attribution});
50+
}
51+
52+
/**
53+
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
54+
* calls the `callback` function once the value is ready to be reported, along
55+
* with all `layout-shift` performance entries that were used in the metric
56+
* value calculation. The reported value is a `double` (corresponding to a
57+
* [layout shift score](https://web.dev/cls/#layout-shift-score)).
58+
*
59+
* If the `reportAllChanges` configuration option is set to `true`, the
60+
* `callback` function will be called as soon as the value is initially
61+
* determined as well as any time the value changes throughout the page
62+
* lifespan.
63+
*
64+
* _**Important:** CLS should be continually monitored for changes throughout
65+
* the entire lifespan of a page—including if the user returns to the page after
66+
* it's been hidden/backgrounded. However, since browsers often [will not fire
67+
* additional callbacks once the user has backgrounded a
68+
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
69+
* `callback` is always called when the page's visibility state changes to
70+
* hidden. As a result, the `callback` function might be called multiple times
71+
* during the same page load._
72+
*/
73+
export const onCLS = (onReport: CLSReportCallbackWithAttribution, opts?: ReportOpts) => {
74+
unattributedOnCLS(((metric: CLSMetric) => {
75+
onReport(attributeCLS(metric));
76+
}) as CLSReportCallback, opts);
77+
};

src/attribution/onFCP.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {getBFCacheRestoreTime} from '../lib/bfcache.js';
18+
import {getLoadState} from '../lib/getLoadState.js';
19+
import {getNavigationEntry} from '../lib/getNavigationEntry.js';
20+
import {onFCP as unattributedOnFCP} from '../onFCP.js';
21+
import {FCPMetricWithAttribution, FCPReportCallback, FCPReportCallbackWithAttribution, ReportOpts} from '../types.js';
22+
23+
24+
const attributeFCP = (metric: FCPMetricWithAttribution): void => {
25+
if (metric.entries.length) {
26+
const navigationEntry = getNavigationEntry();
27+
const fcpEntry = metric.entries[metric.entries.length - 1];
28+
29+
if (navigationEntry) {
30+
const activationStart = navigationEntry.activationStart || 0;
31+
const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);
32+
33+
metric.attribution = {
34+
timeToFirstByte: ttfb,
35+
firstByteToFCP: metric.value - ttfb,
36+
loadState: getLoadState(metric.entries[0].startTime),
37+
navigationEntry,
38+
fcpEntry,
39+
};
40+
}
41+
} else {
42+
// There are no entries when restored from bfcache.
43+
metric.attribution = {
44+
timeToFirstByte: 0,
45+
firstByteToFCP: metric.value,
46+
loadState: getLoadState(getBFCacheRestoreTime()),
47+
};
48+
}
49+
};
50+
51+
/**
52+
* Calculates the [FCP](https://web.dev/fcp/) value for the current page and
53+
* calls the `callback` function once the value is ready, along with the
54+
* relevant `paint` performance entry used to determine the value. The reported
55+
* value is a `DOMHighResTimeStamp`.
56+
*/
57+
export const onFCP = (onReport: FCPReportCallbackWithAttribution, opts?: ReportOpts) => {
58+
unattributedOnFCP(((metric: FCPMetricWithAttribution) => {
59+
attributeFCP(metric);
60+
onReport(metric);
61+
}) as FCPReportCallback, opts);
62+
};

0 commit comments

Comments
 (0)