Skip to content

Commit 2a1428b

Browse files
committed
feat(facets): extract common facets as reusable facet definitions
1 parent 26de21e commit 2a1428b

File tree

2 files changed

+193
-1
lines changed

2 files changed

+193
-1
lines changed

facets.js

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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 { reclassifyConsent, reclassifyAcquisition, scoreCWV } from './utils.js';
13+
/**
14+
* @import {Bundle} from './distiller.js'
15+
*/
16+
export const facets = {
17+
/**
18+
* Extracts each of device type and operating system from
19+
* the simplified user agent string.
20+
* @param {Bundle} bundle the bundle of sampled rum events
21+
* @returns {string[]} a list of device types and operating systems
22+
*/
23+
userAgent: (bundle) => {
24+
const parts = bundle.userAgent.split(':');
25+
return parts.reduce((acc, _, i) => {
26+
acc.push(parts.slice(0, i + 1).join(':'));
27+
return acc;
28+
}, []);
29+
},
30+
/**
31+
* Extracts the path from the URL and removes potential PII such as
32+
* ids, hashes, and other encoded data.
33+
* @param {Bundle} bundle the bundle of sampled rum events
34+
* @returns {string} the path of the URL
35+
*/
36+
url: (bundle) => {
37+
if (bundle.domain) return bundle.domain;
38+
const u = new URL(bundle.url);
39+
u.pathname = u.pathname.split('/')
40+
.map((segment) => {
41+
// only numbers and longer than 5 characters: probably an id, censor it
42+
if (segment.length >= 5 && /^\d+$/.test(segment)) {
43+
return '<number>';
44+
}
45+
// only hex characters and longer than 8 characters: probably a hash, censor it
46+
if (segment.length >= 8 && /^[0-9a-f]+$/i.test(segment)) {
47+
return '<hex>';
48+
}
49+
// base64 encoded data, censor it
50+
if (segment.length > 32 && /^[a-zA-Z0-9+/]+$/.test(segment)) {
51+
return '<base64>';
52+
}
53+
// probable UUID, censor it
54+
if (segment.length > 35 && /^[0-9a-f-]+$/.test(segment)) {
55+
return '<uuid>';
56+
}
57+
// just too long
58+
if (segment.length > 60) {
59+
return '...';
60+
}
61+
return segment;
62+
}).join('/');
63+
return u.toString();
64+
},
65+
/**
66+
* Extracts the checkpoints from the bundle. Each checkpoint
67+
* that occurs at least once in the bundle is returned as a facet
68+
* value.
69+
* @param {Bundle} bundle the bundle of sampled rum events
70+
* @returns {string[]} a list of checkpoints
71+
*/
72+
checkpoint: (bundle) => Array.from(bundle.events
73+
.map(reclassifyConsent)
74+
.map(reclassifyAcquisition)
75+
.reduce((acc, evt) => {
76+
acc.add(evt.checkpoint);
77+
return acc;
78+
}, new Set())),
79+
/**
80+
* Classifies the bundle according to the Core Web Vitals metrics.
81+
* For each metric in `LCP`, `CLS`, and `INP`, the score is calculated
82+
* as `good`, `needs improvement`, or `poor`.
83+
* The result is a list of the form `[LCPgood, CLSpoor, INPni]`
84+
* @param {Bundle} bundle the bundle of sampled rum events
85+
* @returns {string[]} a list of CWV metrics
86+
*/
87+
vitals: (bundle) => {
88+
const cwv = ['cwvLCP', 'cwvCLS', 'cwvINP'];
89+
return cwv
90+
.filter((metric) => bundle[metric])
91+
.map((metric) => scoreCWV(bundle[metric], metric.toLowerCase().slice(3)) + metric.slice(3));
92+
},
93+
/**
94+
* Extracts the target of the Largest Contentful Paint (LCP) event from the bundle.
95+
* @param {Bundle} bundle the bundle of sampled rum events
96+
* @returns {string[]} a list of LCP targets
97+
*/
98+
lcpTarget: (bundle) => bundle.events
99+
.filter((evt) => evt.checkpoint === 'cwv-lcp')
100+
.map((evt) => evt.target)
101+
.filter((target) => target),
102+
103+
/**
104+
* Extracts the source of the Largest Contentful Paint (LCP) event from the bundle.
105+
* @param {Bundle} bundle the bundle of sampled rum events
106+
* @returns {string[]} a list of LCP sources
107+
*/
108+
lcpSource: (bundle) => bundle.events
109+
.filter((evt) => evt.checkpoint === 'cwv-lcp')
110+
.map((evt) => evt.source)
111+
.filter((source) => source),
112+
113+
/**
114+
* Extracts the acquisition source from the bundle. As acquisition sources
115+
* can be strings like `paid:video:youtube`, each of `paid`, `paid:video`,
116+
* and `paid:video:youtube` are returned as separate values.
117+
* @param {Bundle} bundle the bundle of sampled rum events
118+
* @returns {string[]} a list of acquisition sources
119+
*/
120+
acquisitionSource: (bundle) => Array.from(
121+
bundle.events
122+
.map(reclassifyAcquisition)
123+
.filter((evt) => evt.checkpoint === 'acquisition')
124+
.filter(({ source }) => source) // filter out empty sources
125+
.map(({ source }) => source.split(':'))
126+
.map((source) => source
127+
.reduce((acc, _, i) => {
128+
acc.push(source.slice(0, i + 1).join(':'));
129+
return acc;
130+
}, [])
131+
.filter((s) => s))
132+
.pop() || [],
133+
),
134+
mediaTarget: (bundle) => bundle.events
135+
.filter((evt) => evt.checkpoint === 'viewedia')
136+
.map((evt) => evt.target)
137+
.filter((target) => target)
138+
.map((target) => {
139+
const u = new URL(target, bundle.url);
140+
// strip query params, hash, and user/pass
141+
u.search = '';
142+
u.hash = '';
143+
u.username = '';
144+
u.password = '';
145+
if (u.hostname === bundle.host) {
146+
// relative URL is enough
147+
return u.pathname;
148+
}
149+
return u.toString();
150+
}),
151+
};
152+
/**
153+
* A collection of facet factory functions. Each function takes one or more
154+
* parameters and returns a facet function according to the parameters.
155+
*/
156+
export const facetFns = {
157+
/**
158+
* Returns a function that creates a facet function for the source of the given
159+
* checkpoint.
160+
* @param {string} cp the checkpoint
161+
* @returns {function(bundle: Bundle): Set<string>} a facet function
162+
*/
163+
checkpointSource: (cp) => (bundle) => Array.from(
164+
bundle.events
165+
.map(reclassifyConsent)
166+
.filter((evt) => evt.checkpoint === cp)
167+
.filter(({ source }) => source) // filter out empty sources
168+
.reduce((acc, { source }) => {
169+
acc.add(source);
170+
return acc;
171+
}, new Set()),
172+
),
173+
/**
174+
* Returns a function that creates a facet function for the target of the given
175+
* checkpoint.
176+
* @param {string} cp the checkpoint
177+
* @returns {function(bundle: Bundle): Set<string>} a facet function
178+
*/
179+
checkpointTarget: (cp) => (bundle) => Array.from(
180+
bundle.events
181+
.map(reclassifyConsent)
182+
.filter((evt) => evt.checkpoint === cp)
183+
.filter(({ target }) => target) // filter out empty targets
184+
.reduce((acc, { target }) => {
185+
acc.add(target);
186+
return acc;
187+
}, new Set()),
188+
),
189+
};

index.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
reclassifyEnter,
3737
addCalculatedProps,
3838
} from './utils.js';
39+
import { facets, facetFns } from './facets.js';
3940

4041
const utils = {
4142
isKnownFacet,
@@ -69,4 +70,6 @@ const series = {
6970
engagement,
7071
};
7172

72-
export { DataChunks, utils, stats, series };
73+
export {
74+
DataChunks, utils, stats, series, facets, facetFns,
75+
};

0 commit comments

Comments
 (0)