Skip to content

Commit fb72596

Browse files
authored
Merge pull request #4 from adobe/metrics
feat(metrics): define some standard metrics/series that can be used consistently
2 parents 2d0a8b3 + ecb8147 commit fb72596

File tree

5 files changed

+377
-2
lines changed

5 files changed

+377
-2
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ logs
99
test-results.xml
1010
admin-idp-p*.*
1111
*.env
12+
lcov.info
13+
junit.xml

index.js

+24-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ import { DataChunks } from './distiller.js';
1313
import {
1414
tTest, zTestTwoProportions, samplingError, linearRegression, roundToConfidenceInterval,
1515
} from './stats.js';
16+
import {
17+
pageViews,
18+
visits,
19+
bounces,
20+
organic,
21+
earned,
22+
lcp,
23+
cls,
24+
inp,
25+
engagement,
26+
} from './series.js';
1627
import {
1728
isKnownFacet,
1829
scoreCWV,
@@ -46,4 +57,16 @@ const stats = {
4657
samplingError,
4758
};
4859

49-
export { DataChunks, utils, stats };
60+
const series = {
61+
pageViews,
62+
visits,
63+
bounces,
64+
organic,
65+
earned,
66+
lcp,
67+
cls,
68+
inp,
69+
engagement,
70+
};
71+
72+
export { DataChunks, utils, stats, series };

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@adobe/rum-distiller",
33
"version": "1.1.0",
44
"scripts": {
5-
"test": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=../../lcov.info --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=../../junit.xml",
5+
"test": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info --test-reporter=spec --test-reporter-destination=stdout --test-reporter=junit --test-reporter-destination=junit.xml",
66
"test-ci": "npm run test",
77
"lint": "eslint .",
88
"docs": "npx jsdoc2md -c .jsdoc.json --files 'src/*.js' > docs/API.md",

series.js

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
import { reclassifyAcquisition } from './utils.js';
13+
/**
14+
* @module series
15+
* @description This module provides a list of standardized series for use in analyzing web
16+
* experiences.
17+
* each series can be registered with a name using `DataChunks.addSeries(name, series)`.
18+
*/
19+
20+
/**
21+
* @typedef {import('./distiller.js').Bundle} Bundle
22+
*/
23+
24+
/**
25+
* A page view is an impression of a page. At this moment, pre-rendering is also
26+
* considered a page view.
27+
* @param {Bundle} bundle a series of events that belong to the same page view
28+
* @returns {number} the number of page views
29+
*/
30+
export const pageViews = (bundle) => bundle.weight;
31+
32+
/**
33+
* A visit is a page view that does not follow an internal link. This means a visit starts
34+
* when users follow an external link or enter the URL in the browser.
35+
* @param {Bundle} bundle a series of events that belong to the same page view
36+
* @returns {number} the number of visits
37+
*/
38+
export const visits = (bundle) => (bundle.visit ? bundle.weight : 0);
39+
40+
/**
41+
* A bounce is a visit that does not have any click events.
42+
* @param {Bundle} bundle a series of events that belong to the same page view
43+
* @returns {number} the number of bounces
44+
*/
45+
export const bounces = (bundle) => (bundle.visit && !bundle.events.find(({ checkpoint }) => checkpoint === 'click')
46+
? bundle.weight
47+
: 0);
48+
49+
/**
50+
* The largest contentful paint is the time it takes for the largest contentful element to load.
51+
* @param {Bundle} bundle a series of events that belong to the same page view
52+
* @returns {number} the largest contentful paint
53+
*/
54+
export const lcp = (bundle) => bundle.cwvLCP;
55+
56+
/**
57+
* The cumulative layout shift is the sum of all layout shifts in a page view.
58+
* @param {Bundle} bundle a series of events that belong to the same page view
59+
* @returns {number} the cumulative layout shift
60+
*/
61+
export const cls = (bundle) => bundle.cwvCLS;
62+
63+
/**
64+
* The interaction to next paint is the time it takes for the next paint after an interaction.
65+
* @param {Bundle} bundle a series of events that belong to the same page view
66+
* @returns {number} the interaction to next paint
67+
*/
68+
export const inp = (bundle) => bundle.cwvINP;
69+
70+
/**
71+
* The time to first byte is the time it takes for the first byte to arrive.
72+
* @param {Bundle} bundle a series of events that belong to the same page view
73+
*/
74+
75+
export const ttfb = (bundle) => bundle.cwvTTFB;
76+
77+
/**
78+
* A page view is considered engaged if there has been at least some user interaction
79+
* or significant content has been viewed, i.e. 4 or more viewmedia or viewblock events.
80+
* @param {Bundle} bundle a series of events that belong to the same page view
81+
* @returns {number} the number of engaged page views
82+
*/
83+
export const engagement = (bundle) => {
84+
const clickEngagement = bundle.events.filter((evt) => evt.checkpoint === 'click').length > 0
85+
? bundle.weight
86+
: 0;
87+
const contentEngagement = bundle.events
88+
.filter((evt) => evt.checkpoint === 'viewmedia' || evt.checkpoint === 'viewblock')
89+
.length > 3
90+
? bundle.weight
91+
: 0;
92+
return clickEngagement || contentEngagement;
93+
};
94+
95+
/**
96+
* The number of earned visits is the number of visits that are not paid or owned.
97+
* @param {Bundle} bundle a series of events that belong to the same page view
98+
* @returns {number} the number of earned conversions
99+
*/
100+
export const earned = (bundle) => {
101+
const reclassified = bundle.events.map(reclassifyAcquisition);
102+
if (!reclassified.find((evt) => evt.checkpoint === 'enter')) {
103+
// we only consider enter events
104+
return 0;
105+
}
106+
if (!reclassified.find((evt) => evt.checkpoint === 'acquisition')) {
107+
// this is fully organic, as there are no traces of any acquisition
108+
return bundle.weight;
109+
}
110+
if (reclassified.find((evt) => evt.checkpoint === 'acquisition' && evt.source.startsWith('paid'))) {
111+
// this is paid, as there is at least one paid acquisition
112+
return 0;
113+
}
114+
if (reclassified.find((evt) => evt.checkpoint === 'acquisition' && evt.source.startsWith('owned'))) {
115+
// owned does not count as earned
116+
return 0;
117+
}
118+
return bundle.weight;
119+
};
120+
121+
/**
122+
* The number of organic visits is the number of visits that are not paid.
123+
* @param {Bundle} bundle a series of events that belong to the same page view
124+
* @returns {number} the number of earned conversions
125+
*/
126+
export const organic = (bundle) => {
127+
const reclassified = bundle.events.map(reclassifyAcquisition);
128+
if (!reclassified.find((evt) => evt.checkpoint === 'enter')) {
129+
// we only consider enter events
130+
return 0;
131+
}
132+
if (!reclassified.find((evt) => evt.checkpoint === 'acquisition')) {
133+
// this is fully organic, as there are no traces of any acquisition
134+
return bundle.weight;
135+
}
136+
if (reclassified.find((evt) => evt.checkpoint === 'acquisition' && evt.source.startsWith('paid'))) {
137+
// this is paid, as there is at least one paid acquisition
138+
return 0;
139+
}
140+
return bundle.weight;
141+
};

0 commit comments

Comments
 (0)