Skip to content
This repository has been archived by the owner on Aug 10, 2023. It is now read-only.

Commit

Permalink
Implementation (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
omrilotan authored Jul 5, 2018
1 parent e8154d0 commit 0e25702
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 11 deletions.
5 changes: 4 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ jobs:
- restore_cache:
key: dependency-cache-{{ checksum "package.json" }}
- run:
name: Test code and lint
name: Run tests
command: npm t
- run:
name: Code linting
command: npm run lint
publish:
<<: *defaults
steps:
Expand Down
2 changes: 1 addition & 1 deletion .karma/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const {resolve} = require('path');
const browsers = ['Chrome'];
const watch = process.env.npm_config_watch;
const ENV_SETUP = './env-setup.js';
const ENV_SETUP = resolve(`${__dirname}/env-setup.js`);
const browserNoActivityTimeout = 1 * 60 * 1000;
const pattern = ((args) =>
`../src/${args.length ? `**/+(${args.join('|')})` : '**'}/spec.js`
Expand Down
23 changes: 23 additions & 0 deletions .webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const {resolve} = require('path');

module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: resolve(__dirname, 'dist'),
filename: 'main.js',
library: 'MyLibrary',
libraryTarget: 'umd2',
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: resolve('../src'),
sideEffects: false,
},
],
},
target: 'web',
};
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,20 @@ const event = {
timings: {},
page: "homepage_loggedout"
};
const results = pageTiming({
pageTiming({
reducer,
accumulator: event.timings,
metrics,
});

sendPreformanceEvent(...results);
sendPreformanceEvent(event);
```
Output
```json
{
"group": "performance",
"type": "browser_performance",
"metrics": {
"timings": {
"domInteractive": 208.01,
"loadEventEnd": 431.01
},
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
{
"name": "page-timing",
"version": "0.0.1-alpha",
"version": "1.0.0",
"description": "Measure browser timing and do something with it",
"author": "Fiverr SRE",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/fiverr/page-timing.git"
},
"homepage": "https://github.com/fiverr/page-timing#readme",
"main": "dist/main.js",
"scripts": {
"prepublish": "npm run build",
"build": "webpack",
"prepublishOnly": "npm run build",
"build": "webpack --config .webpack.config.js",
"test": "NODE_ENV=test node --max_old_space_size=2048 ./node_modules/karma/bin/karma start .karma/index.js --loglevel debug",
"lint": "eslint -c .eslintrc.js src/*.js src/**/*.js --quiet"
},
Expand Down
39 changes: 38 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
export default () => null;
import { performanceMetrics } from './performance-metrics';
import { measure } from './measure';
import { supported } from './supported';

const DEFAULT_REDUCER = (accumulator, [key, value]) => [...accumulator, [key, value]];
const DEFAULT_ACCUMULATOR = [];

/**
* An opinionated reducer on performance.timing object
* @param {String[]} [options.metrics] Requested metrics
* @param {Function} [options.reducer] Reducer function
* @param {Any} [options.accumulator] Reducer accumulator
* @return {Any}
*/
export const pageTiming = ({metrics = performanceMetrics, reducer = DEFAULT_REDUCER, accumulator = DEFAULT_ACCUMULATOR} = {}) =>
supported() ?
metrics
.reduce(
(results, metric, index, metrics) => {

// Ignore invalid performance metrics
if (!performanceMetrics.includes(metric)) {
return results;
}
const value = measure(metric);

// Ignore empty entries
if (value <= 0) {
return results;
}

return reducer(results, [metric, value], index, metrics);
},
accumulator
)
:
accumulator
;
77 changes: 77 additions & 0 deletions src/integration/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const timeOrigin = 1;

const mock = {
unloadEventStart: 0,
unloadEventEnd: 0,
secureConnectionStart: 0,
redirectStart: 0,
redirectEnd: 0,
navigationStart: 1,
connectStart: 110,
connectEnd: 120,
domainLookupEnd: 130,
domainLookupStart: 140,
fetchStart: 150,
requestStart: 170,
responseStart: 180,
domLoading: 190,
responseEnd: 200,
domInteractive: 210,
domContentLoadedEventStart: 220,
domContentLoadedEventEnd: 230,
loadEventStart: 240,
domComplete: 250,
loadEventEnd: 260,
};

const results = Object.entries(mock).reduce(
(results, [key, value]) =>
value > timeOrigin
?
Object.assign(results, {[key]: value - timeOrigin})
:
results,
{}
);

describe('Integration', async() => {
delete require.cache[require.resolve('../')];
delete require.cache[require.resolve('../start')];
delete require.cache[require.resolve('../measure')];
require('../supported').supported(); // cached result

const performance = Object.getOwnPropertyDescriptor(window, 'performance');

beforeEach(() => {
window.performance = {
timeOrigin,
timing: mock,
};
});
afterEach(() => {
Object.defineProperty(window, 'performance', performance);
});

it('Should extract all performance metrics to a structured object, and skip 0 values', async() => {
await onload();
await sleep(400);

const pageTiming = require('../').pageTiming;

const obj = {key: 'value', metrics: {}};
const reducer = (accumulator, [key, value]) => Object.assign(
accumulator,
{[key]: parseInt(value, 10)}
);

pageTiming({
reducer,
accumulator: obj.metrics,
});

expect(obj).to.deep.equal({
key: 'value',
metrics: results,
});
});
});
8 changes: 8 additions & 0 deletions src/measure/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { start } from '../start';

/**
* Get the measurement diff from start
* @param {String} measurement
* @return {Number}
*/
export const measure = (measurement) => Math.max(window.performance.timing[measurement] - start(), 0);
29 changes: 29 additions & 0 deletions src/measure/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { measure } = require('.');

describe('measure', () => {
const performance = Object.getOwnPropertyDescriptor(window, 'performance');

afterEach(() =>
Object.defineProperty(window, 'performance', performance)
);

it('Should return the diff between timing metric to start point', () => {
window.performance = {
timeOrigin: 100,
timing: {
something: 250,
},
};
expect(measure('something')).to.equal(150);
});

it('Should return 0 if the time unit it lower than start (e.g. 0)', () => {
window.performance = {
timeOrigin: 100,
timing: {
something: 50,
},
};
expect(measure('something')).to.equal(0);
});
});
9 changes: 9 additions & 0 deletions src/memoise/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const mem = new WeakMap();

/**
* Memoises value by reference
* @param {Object} key
* @param {Function} fn
* @return {Any}
*/
export const memoise = (key, fn) => mem.has(key) ? mem.get(key) : mem.set(key, fn()).get(key);
32 changes: 32 additions & 0 deletions src/memoise/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
describe('memoise', () => {
let memoise;
const key = {};

beforeEach(() => {
delete require.cache[require.resolve('.')];
memoise = require('.').memoise;
});

it('Should uses the getter to retrieve the value', () => {
memoise(key, () => 'Thang');
expect(memoise(key)).to.equal('Thang');
});

it('Should return the value', () =>
expect(memoise(key, () => 'Thang')).to.equal('Thang')
);

it('Should memoise the result', () => {
memoise(key, () => 'Thing');
memoise(key, () => 'Thang');
expect(memoise(key)).to.equal('Thing');
});

it('Should throw error when trying to memoise primitives', () =>
[
'',
Symbol(),
4,
].forEach((key) => expect(() => memoise(key, () => 'Thing')).to.throw())
);
});
23 changes: 23 additions & 0 deletions src/performance-metrics/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const performanceMetrics = [
'connectEnd',
'connectStart',
'domComplete',
'domContentLoadedEventEnd',
'domContentLoadedEventStart',
'domInteractive',
'domLoading',
'domainLookupEnd',
'domainLookupStart',
'fetchStart',
'loadEventEnd',
'loadEventStart',
'navigationStart',
'redirectEnd',
'redirectStart',
'requestStart',
'responseEnd',
'responseStart',
'secureConnectionStart',
'unloadEventEnd',
'unloadEventStart',
];
13 changes: 13 additions & 0 deletions src/performance-metrics/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const { performanceMetrics } = require('.');
const IGNORE = [
'toJSON',
];

describe('performance-metrics', () => {
it('Should include all performance metrics', () => {
const windowMetrics = Object.keys(window.performance.timing.constructor.prototype)
.filter((item) => !IGNORE.includes(item));

expect(performanceMetrics).to.have.members(windowMetrics);
});
});
48 changes: 46 additions & 2 deletions src/spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
import { pageTiming } from '.';

const SLEEP_FOR = 400;

describe('page-timing', async() => {
it('wip', () => {
assert(true);
it('Should return result by default', async() => {
await onload();
await sleep(SLEEP_FOR);
const measurements = pageTiming();
expect(measurements).to.have.lengthOf.at.least(5);
});

it('Should collect metrics as arrays of [key, value]', async() => {
await onload();
await sleep(SLEEP_FOR);
const measurements = pageTiming({
metrics: ['domInteractive', 'loadEventEnd'],
});
expect(measurements).to.have.lengthOf(2);
measurements.forEach(
([key, value]) => {
expect(key).to.be.a('string');
expect(value).to.be.a('number');
}
);
});

it('Should ignore metrics that are not valid', async() => {
await onload();
await sleep(SLEEP_FOR);
const measurements = pageTiming({
metrics: ['domInteractive', 'loadEventEnd', 'another'],
});
expect(measurements).to.have.lengthOf(2);
});

it('Should format metrics', async() => {
await onload();
await sleep(SLEEP_FOR);
const [interactive, load] = pageTiming({
metrics: ['domInteractive', 'loadEventEnd'],
reducer: (accumulator, [key, value]) => [
...accumulator,
['prefix', key, Math.round(value)].join(':'),
],
});

expect(interactive).to.match(/prefix:domInteractive:(\d{2,4})/);
expect(load).to.match(/prefix:loadEventEnd:(\d{2,4})/);
});
});
4 changes: 4 additions & 0 deletions src/start/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { memoise } from '../memoise';
const key = {};

export const start = () => memoise(key, () => window.performance.timeOrigin || window.performance.timing.navigationStart);
Loading

0 comments on commit 0e25702

Please sign in to comment.