@adobe/rum-distiller
Advanced tools
Comparing version 1.0.0 to 1.1.0
@@ -0,1 +1,8 @@ | ||
# [1.1.0](https://github.com/adobe/rum-distiller/compare/v1.0.0...v1.1.0) (2024-10-08) | ||
### Features | ||
* **index:** allow single entry point in main.js ([3d2df08](https://github.com/adobe/rum-distiller/commit/3d2df08d0c04e17742aa7eafafcb55ac2b6c440a)) | ||
# 1.0.0 (2024-10-08) | ||
@@ -2,0 +9,0 @@ |
{ | ||
"name": "@adobe/rum-distiller", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"scripts": { | ||
@@ -13,2 +13,3 @@ "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", | ||
"type": "module", | ||
"main": "index.js", | ||
"license": "Apache-2.0", | ||
@@ -15,0 +16,0 @@ "devDependencies": { |
import { describe, it } from 'node:test'; | ||
import assert from 'node:assert/strict'; | ||
import { roundToConfidenceInterval, samplingError } from '../utils.js'; | ||
import { roundToConfidenceInterval, samplingError } from '../stats.js'; | ||
describe('samplingError', () => { | ||
@@ -6,0 +7,0 @@ it('computes the sampling error', () => { |
import { describe, it } from 'node:test'; | ||
import assert from 'node:assert/strict'; | ||
import { | ||
escapeHTML, computeConversionRate, isKnownFacet, | ||
computeConversionRate, isKnownFacet, | ||
} from '../utils.js'; | ||
describe('escapeHTML', () => { | ||
it('escapes HTML entities', () => { | ||
assert.strictEqual(escapeHTML('<script>alert("xss")</script>'), '<script>alert("xss")</script>'); | ||
assert.strictEqual(escapeHTML("<script>alert('xss')</script>"), '<script>alert('xss')</script>'); | ||
assert.strictEqual(escapeHTML('<div>hello</div>'), '<div>hello</div>'); | ||
}); | ||
}); | ||
describe('computeConversionRate', () => { | ||
@@ -16,0 +8,0 @@ it('its 10% for 1 conversion and 10 visits', () => { |
170
utils.js
@@ -196,73 +196,3 @@ import classifyConsent from './consent.js'; | ||
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 | ||
@@ -282,75 +212,2 @@ * capped at 100%. | ||
/** | ||
* Determines the sampling error based on a binomial distribution. | ||
* Each sample is a Bernoulli trial, where the probability of success is the | ||
* proportion of the total population that has the attribute of interest. | ||
* The sampling error is calculated as the standard error of the proportion. | ||
* @param {number} total the expectation value of the total population | ||
* @param {number} samples the number of successful trials (i.e. samples) | ||
*/ | ||
export function samplingError(total, samples) { | ||
if (samples === 0) { | ||
return 0; | ||
} | ||
const weight = total / samples; | ||
const variance = weight * weight * samples; | ||
const standardError = Math.sqrt(variance); | ||
const marginOfError = 1.96 * standardError; | ||
// round up to the nearest integer | ||
return Math.round(marginOfError); | ||
} | ||
const vulgarFractions = { | ||
0: '0', | ||
0.125: '⅛', | ||
0.2: '⅕', | ||
0.25: '¼', | ||
0.333: '⅓', | ||
0.375: '⅜', | ||
0.5: '½', | ||
0.625: '⅝', | ||
0.666: '⅔', | ||
0.75: '¾', | ||
0.8: '⅘', | ||
0.875: '⅞', | ||
1: '1', | ||
}; | ||
export function findNearestVulgarFraction(fraction) { | ||
const closest = Object.keys(vulgarFractions).reduce((acc, key) => { | ||
if (Math.abs(fraction - key) < Math.abs(fraction - acc)) { | ||
return key; | ||
} | ||
return acc; | ||
}, 0); | ||
return vulgarFractions[closest]; | ||
} | ||
export function roundToConfidenceInterval( | ||
total, | ||
samples = total, | ||
maxPrecision = Infinity, | ||
) { | ||
const max = total + samplingError(total, samples); | ||
const min = total - samplingError(total, samples); | ||
// determine the number of significant digits that max and min have in common | ||
// e.g. 3.14 and 3.16 have 2 significant digits in common | ||
const maxStr = max.toPrecision(`${max}`.length); | ||
const minStr = min.toPrecision(`${min}`.length); | ||
const common = Math.min(maxStr.split('').reduce((acc, digit, i) => { | ||
if (digit === minStr[i]) { | ||
return acc + 1; | ||
} | ||
return acc; | ||
}, 0), Number.isNaN(maxPrecision) ? Infinity : maxPrecision); | ||
const precision = Math.max( | ||
Math.min(2, Number.isNaN(maxPrecision) ? Infinity : maxPrecision), | ||
common, | ||
); | ||
const rounded = toHumanReadable(total, precision); | ||
return rounded; | ||
} | ||
export function reclassifyConsent({ source, target, checkpoint }) { | ||
@@ -411,1 +268,28 @@ if (checkpoint === 'click' && source) { | ||
} | ||
/** | ||
* Calculates properties on the bundle, so that bundle-level filtering can be performed | ||
* @param {RawBundle} bundle the raw input bundle, without calculated properties | ||
* @returns {Bundle} a bundle with additional properties | ||
*/ | ||
export function addCalculatedProps(bundle) { | ||
bundle.events.forEach((e) => { | ||
if (e.checkpoint === 'enter') { | ||
bundle.visit = true; | ||
if (e.source === '') e.source = '(direct)'; | ||
} | ||
if (e.checkpoint === 'cwv-inp') { | ||
bundle.cwvINP = e.value; | ||
} | ||
if (e.checkpoint === 'cwv-lcp') { | ||
bundle.cwvLCP = Math.max(e.value || 0, bundle.cwvLCP || 0); | ||
} | ||
if (e.checkpoint === 'cwv-cls') { | ||
bundle.cwvCLS = Math.max(e.value || 0, bundle.cwvCLS || 0); | ||
} | ||
if (e.checkpoint === 'cwv-ttfb') { | ||
bundle.cwvTTFB = e.value; | ||
} | ||
}); | ||
return bundle; | ||
} |
19
2775066
88967