-
Notifications
You must be signed in to change notification settings - Fork 46
/
Copy pathutils.js
221 lines (205 loc) · 7.5 KB
/
utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
/* helpers */
export function scoreValue(value, ni, poor) {
if (value >= poor) return 'poor';
if (value >= ni) return 'ni';
return 'good';
}
export function isKnownFacet(key) {
return false // TODO: find a better way to filter out non-facet keys
|| key === 'userAgent'
|| key === 'url'
|| key === 'conversions'
// facets from sankey
|| key === 'trafficsource'
|| key === 'traffictype'
|| key === 'entryevent'
|| key === 'pagetype'
|| key === 'loadtype'
|| key === 'contenttype'
|| key === 'interaction'
|| key === 'clicktarget'
|| key === 'exit'
|| key === 'vitals'
|| key.endsWith('.source')
|| key.endsWith('.target')
|| key.endsWith('.histogram')
|| key === 'checkpoint';
}
export function scoreCWV(value, name) {
if (value === undefined || value === null) return null;
const limits = {
lcp: [2500, 4000],
cls: [0.1, 0.25],
inp: [200, 500],
ttfb: [800, 1800],
};
return scoreValue(value, ...limits[name]);
}
export const UA_KEY = 'userAgent';
export function toHumanReadable(num) {
const dp = 3;
let number = num;
const thresh = 1000;
if (Math.abs(num) < thresh) {
const precision = (Math.log10(number) < 0) ? 2 : (dp - 1) - Math.floor(Math.log10(number));
return `${number.toFixed(precision)}`;
}
const units = ['k', 'm', 'g', 't', 'p'];
let u = -1;
const r = 10 ** dp;
do {
number /= thresh;
u += 1;
} while (Math.round(Math.abs(number) * r) / r >= thresh && u < units.length - 1);
const precision = (dp - 1) - Math.floor(Math.log10(number));
return `${number.toFixed(precision)}${units[u]}`;
} export function toISOStringWithTimezone(date) {
// Pad a number to 2 digits
const pad = (n) => `${Math.floor(Math.abs(n))}`.padStart(2, '0');
// Get timezone offset in ISO format (+hh:mm or -hh:mm)
const getTimezoneOffset = () => {
const tzOffset = -date.getTimezoneOffset();
const diff = tzOffset >= 0 ? '+' : '-';
return `${diff}${pad(tzOffset / 60)}:${pad(tzOffset % 60)}`;
};
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${getTimezoneOffset()}`;
}
export function scoreBundle(bundle) {
// a bundle is good if all CWV that have a value are good
// a bundle is ni if all CWV that have a value are ni or good
// a bundle is poor if any CWV that have a value are poor
// a bundle has no CWV if no CWV have a value
const cwv = ['cwvLCP', 'cwvCLS', 'cwvINP'];
const scores = cwv
.filter((metric) => bundle[metric])
.map((metric) => scoreCWV(bundle[metric], metric.toLowerCase().slice(3)));
if (scores.length === 0) return null;
if (scores.every((s) => s === 'good')) return 'good';
if (scores.every((s) => s !== 'poor')) return 'ni';
return 'poor';
}
export const INTERPOLATION_THRESHOLD = 10;
export function simpleCWVInterpolationFn(metric, threshold) {
return (cwvs) => {
const valuedWeights = Object.values(cwvs)
.filter((value) => value.weight !== undefined)
.map((value) => value.weight)
.reduce((acc, value) => acc + value, 0);
return cwvs[threshold + metric].weight / valuedWeights;
};
}
export function cwvInterpolationFn(targetMetric, interpolateTo100) {
return (cwvs) => {
const valueCount = cwvs.goodCWV.count + cwvs.niCWV.count + cwvs.poorCWV.count;
const valuedWeights = cwvs.goodCWV.weight + cwvs.niCWV.weight + cwvs.poorCWV.weight;
if (interpolateTo100) {
return (cwvs[targetMetric].weight / valuedWeights);
}
if (valueCount < INTERPOLATION_THRESHOLD) {
// not enough data to interpolate
return 0;
}
// total weight
const totalWeight = cwvs.goodCWV.weight
+ cwvs.niCWV.weight
+ cwvs.poorCWV.weight
+ cwvs.noCWV.weight;
// share of targetMetric compared to all CWV
const share = cwvs[targetMetric].weight / (valuedWeights);
// interpolate the share to the total weight
return Math.round(share * totalWeight);
};
}
export function truncate(time, unit) {
const t = new Date(time);
// truncate to the beginning of the hour
t.setMinutes(0);
t.setSeconds(0);
// truncate to the beginning of the day
if (unit !== 'hour') t.setHours(0);
// truncate to the beginning of the week, if the unit is week
if (unit === 'week') t.setDate(t.getDate() - t.getDay());
// truncate to the beginning of the month, if the unit is month
if (unit === 'month') t.setDate(1);
return toISOStringWithTimezone(t);
}
export function escapeHTML(unsafe) {
return unsafe.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
}
export function cssVariable(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name);
}
let gradient;
let width;
let height;
export function getGradient(ctx, chartArea, from, to) {
const chartWidth = chartArea.right - chartArea.left;
const chartHeight = chartArea.bottom - chartArea.top;
if (!gradient || width !== chartWidth || height !== chartHeight) {
// Create the gradient because this is either the first render
// or the size of the chart has changed
width = chartWidth;
height = chartHeight;
gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
gradient.addColorStop(0, from);
gradient.addColorStop(1, to);
}
return gradient;
}
/**
* Function used for filtering wanted parameters. Its implementation depends on the context,
* for instance when parsing for conversion parameters we care about those that start with
* `conversion.`.
* @function filterFn
* @param {string} paramName - The parameter name.
* @returns {boolean} - Returns true if the parameter will be further parsed, false otherwise.
*/
/**
* In some cases, it may just be that the parameters need to be transformed in some way.
* For instance, when parsing conversion parameters we want to remove the `conversion.` prefix
* from the parameter name.
* @function transformFn
* @param {[string, string]} paramPair - The pair of parameter name and its value.
* @returns {[string, string]} - The result of the transformation.
*/
/**
* Parse search parameters and return a dictionary.
* @param {URLSearchParams} params - The search parameters.
* @param {filterFn} filterFn - The filtering function.
* @param {transformFn} transformFn - The transformation function.
* @returns {Object<string, string[]>} - The dictionary of parameters.
*/
export function parseSearchParams(params, filterFn, transformFn) {
return Array.from(params
.entries())
.filter(filterFn)
.map(transformFn)
.reduce((acc, [key, value]) => {
if (acc[key]) acc[key].push(value);
else acc[key] = [value];
return acc;
}, {});
}
const cached = {};
export function parseConversionSpec() {
if (cached.conversionSpec) return cached.conversionSpec;
const params = new URL(window.location).searchParams;
const transform = ([key, value]) => [key.replace('conversion.', ''), value];
const filter = ([key]) => (key.startsWith('conversion.'));
cached.conversionSpec = parseSearchParams(params, filter, transform);
return cached.conversionSpec;
}
/**
* Conversion rates are computed as the ratio of conversions to visits. The conversion rate is
* capped at 100%.
* @param conversions the number of conversions
* @param visits the number of visits
* @returns {number} the conversion rate as a percentage
*/
export function computeConversionRate(conversions, visits) {
const conversionRate = (100 * conversions) / visits;
if (conversionRate >= 0 && conversionRate <= 100) {
return conversionRate;
}
return 100;
}