Skip to content

Commit

Permalink
Merge pull request #1 from BitClock/feature
Browse files Browse the repository at this point in the history
beta v2
  • Loading branch information
bernardmcmanus authored May 13, 2017
2 parents 7296359 + bf1a1f7 commit 1a3750f
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 259 deletions.
21 changes: 0 additions & 21 deletions .eslintrc.json

This file was deleted.

25 changes: 25 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
parser: babel-eslint

extends:
- eslint:recommended
- plugin:import/errors
- plugin:import/warnings

plugins:
- import

env:
es6: true
node: true
mocha: true

rules:
semi: [error, always]
no-unused-vars: [error]

parserOptions:
sourceType: module

settings:
import/resolver:
webpack: { config: ./webpack.config.babel.js }
21 changes: 20 additions & 1 deletion lib/config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { get } from './helpers';

export const MIN_INTERVAL = 500;

export const CHUNK_SIZE_RANGE = [1, 1000];

export default {
env: get(global, ['process', 'env', 'NODE_ENV']),
debug: false,
token: null,
bucket: null,
maxChunkSize: 200,
reportingInterval: 5000,
reportingEndpoint: 'https://hub.bitclock.io'
};

export const MIN_INTERVAL = 500;
export function validateChunkSize(value) {
return Boolean(
value >= CHUNK_SIZE_RANGE[0]
&& value <= CHUNK_SIZE_RANGE[1]
&& Math.round(value) === value
);
}

export function validateReportingInterval(value) {
return Boolean(
value
&& value >= MIN_INTERVAL
&& value === Math.round(value)
);
}
1 change: 0 additions & 1 deletion lib/events/index.js

This file was deleted.

31 changes: 0 additions & 31 deletions lib/events/timing.js

This file was deleted.

78 changes: 25 additions & 53 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,10 @@ import { parse as parseCookie } from 'cookie';
import CONFIG from './config';
import MaybeWeakSet from './weak-set';

const { process, window, document, console } = global;
const { process, window, document } = global;

function padLeft(input, length = 0, chars = ' ') {
let result = String(input);
const diff = Math.max(length - result.length, 0);
for (let i = 0; i < diff; i++) {
result = `${chars}${result}`;
}
return result;
}

function consoleLabel(ansiColor, plainLabel) {
const date = new Date();
const hours = padLeft(date.getUTCHours(), 2, '0');
const minutes = padLeft(date.getUTCMinutes(), 2, '0');
const seconds = padLeft(date.getUTCSeconds(), 2, '0');
const timestamp = `${hours}:${minutes}:${seconds}`;
const label = isBrowser() ? plainLabel : `\x1B[${ansiColor}${plainLabel}\x1B[0m`;
return `[${timestamp}] - ${label}: [bitclock]`;
}

/**
* debug accepts a function to prevent unnecessary work
* when debug logging is not enabled
*/
export function debug(fn) {
/* eslint-disable no-console */
if (CONFIG.debug && typeof fn === 'function' && console && console.log) {
const value = fn();
const args = Array.isArray(value) ? value : [value];
console.log(`${consoleLabel('34m', 'debug')}`, ...args);
}
/* eslint-enable no-console */
}

export function warn(...args) {
/* eslint-disable no-console */
if (console && console.warn) {
console.warn(`${consoleLabel('33m', 'warning')}`, ...args);
}
/* eslint-enable no-console */
export function is(target, type) {
return typeof target === type;
}

export function get(obj, path, otherwise) {
Expand All @@ -69,22 +32,16 @@ export function once(fn) {
}

export function cloneDeep(obj) {
return JSON.parse(safeStringify(obj));
return obj && JSON.parse(safeStringify(obj));
}

export function getToken() {
let { token } = CONFIG;
if (!token) {
token = get(process, ['env', 'BITCLOCK_TOKEN']);
if (!token && document) {
({ BITCLOCK_TOKEN: token } = parseCookie(document.cookie || ''));
}
export function objectValues(obj) {
const vals = [];
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
vals.push(obj[keys[i]]);
}
return token;
}

export function isBrowser() {
return Boolean(window && document && ('onscroll' in global));
return vals;
}

export function safeStringify(obj) {
Expand All @@ -101,3 +58,18 @@ export function safeStringify(obj) {
return value;
});
}

export const getToken = once(() => {
let { token } = CONFIG;
if (!token) {
token = get(process, ['env', 'BITCLOCK_TOKEN']);
if (!token && document) {
({ BITCLOCK_TOKEN: token } = parseCookie(document.cookie || ''));
}
}
return token;
});

export const isBrowser = once(() => {
return Boolean(window && document && ('onscroll' in global));
});
71 changes: 50 additions & 21 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,52 +1,75 @@
import fetch from 'isomorphic-fetch';

import pkg from '../package.json';
import Stack from './stack';
import Transaction from './transaction';
import { debug, warn, once, getToken, cloneDeep, safeStringify } from './helpers';
import CONFIG, { MIN_INTERVAL } from './config';

const logEnvWarning = once(() => warn('Missing value for config.env'));
import createTransaction from './transaction';
import { debug, warn } from './logger';
import { get, isBrowser, once, getToken, cloneDeep, safeStringify } from './helpers';
import CONFIG, {
MIN_INTERVAL,
CHUNK_SIZE_RANGE,
validateChunkSize,
validateReportingInterval
} from './config';

const logEnvWarning = once(() => warn(() => 'Missing value for config.env'));
const stack = Stack();

const enqueue = (() => {
let lastSent;
let checkInProgress;

function checkTime() {
debug(() => `enqueue / checkTime / lastSent = ${lastSent}`);
const time = Date.now();
if (!lastSent || time - CONFIG.reportingInterval >= lastSent) {
lastSent = time;
debug(() => `enqueue / flush stack / stack.length = ${stack.length}`);
send(stack.flush());
debug(() => `enqueue / flush stack / stack.size = ${stack.size}`);
while (stack.size > 0) {
send(stack.flush());
}
}
if (stack.length) {
if (stack.size) {
setTimeout(checkTime, MIN_INTERVAL);
} else {
checkInProgress = false;
}
}

return (substack) => {
stack.put(() => substack.flush());
debug(() => {
return `enqueue / stack.length = ${stack.length} / substack = ${JSON.stringify(substack, null, 2)}`;
return `enqueue / stack.size = ${stack.size} / substack = ${JSON.stringify(substack, null, 2)}`;
});
// wrap this in a timeout so libs like bluebird
// don't complain about orphaned promises
setTimeout(checkTime);
if (!checkInProgress) {
checkInProgress = true;
setTimeout(checkTime);
}
};
})();

function send(body) {
function send(events) {
if (!CONFIG.env) {
logEnvWarning();
}

debug(() => `body = ${JSON.stringify(body, null, 2)}`);
debug(() => `events = ${JSON.stringify(events, null, 2)}`);

if (body.length < 1) {
if (events.length < 1) {
return Promise.resolve();
}

const { bucket, reportingEndpoint } = CONFIG;
const uri = `${reportingEndpoint}/v0/bucket/${bucket}/event`;

const { env } = CONFIG;
const { version } = pkg;
const userAgent = get(global, ['navigator', 'userAgent']);
const source = isBrowser() ? 'client' : 'server';
const body = { version, env, userAgent, source, events };

return fetch(uri, {
method: 'POST',
headers: {
Expand All @@ -58,20 +81,26 @@ function send(body) {
});
}

function config(opts = {}) {
export const Transaction = createTransaction.bind(null, enqueue);

export function config(opts = {}) {
const prevConfig = cloneDeep(CONFIG);
const nextConfig = Object.assign(prevConfig, opts);
const { reportingInterval } = nextConfig;
if (
!reportingInterval
|| reportingInterval < MIN_INTERVAL
|| reportingInterval !== Math.round(reportingInterval)
){
const { reportingInterval, maxChunkSize } = nextConfig;

if (!validateChunkSize(maxChunkSize)) {
throw new Error(`maxChunkSize must be an integer between ${CHUNK_SIZE_RANGE.join(' - ')}`);
}

if (!validateReportingInterval(reportingInterval)) {
throw new Error(`reportingInterval must be an integer of at least ${MIN_INTERVAL}`);
}

const updatedConfig = cloneDeep(Object.assign(CONFIG, nextConfig));

debug(() => `set config = ${JSON.stringify(updatedConfig, null, 2)}`);

return updatedConfig;
}

module.exports = { config, Transaction: (data) => Transaction(enqueue, data) };
export default { config, Transaction };
49 changes: 49 additions & 0 deletions lib/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import CONFIG from './config';
import { isBrowser } from './helpers';

const { console } = global;

/**
* debug accepts a function to prevent unnecessary work when debug logging is not enabled
*/
export function debug(fn) {
/* eslint-disable no-console */
if (CONFIG.debug && typeof fn === 'function' && console && console.log) {
const value = fn();
const args = Array.isArray(value) ? value : [value];
console.log(`${consoleLabel('34m', 'debug')}`, ...args);
}
/* eslint-enable no-console */
}

/**
* warn accepts a function to prevent unnecessary work when running in production
*/
export function warn(fn) {
/* eslint-disable no-console */
if (CONFIG.env !== 'production' && typeof fn === 'function' && console && console.warn) {
const value = fn();
const args = Array.isArray(value) ? value : [value];
console.warn(`${consoleLabel('33m', 'warning')}`, ...args);
}
/* eslint-enable no-console */
}

function padLeft(input, length = 0, chars = ' ') {
let result = String(input);
const diff = Math.max(length - result.length, 0);
for (let i = 0; i < diff; i++) {
result = `${chars}${result}`;
}
return result;
}

function consoleLabel(ansiColor, plainLabel) {
const date = new Date();
const hours = padLeft(date.getUTCHours(), 2, '0');
const minutes = padLeft(date.getUTCMinutes(), 2, '0');
const seconds = padLeft(date.getUTCSeconds(), 2, '0');
const timestamp = `${hours}:${minutes}:${seconds}`;
const label = isBrowser() ? plainLabel : `\x1B[${ansiColor}${plainLabel}\x1B[0m`;
return `[${timestamp}] - ${label}: [bitclock]`;
}
Loading

0 comments on commit 1a3750f

Please sign in to comment.