187 lines
4.4 KiB
JavaScript
187 lines
4.4 KiB
JavaScript
'use strict';
|
|
const path = require('path');
|
|
const childProcess = require('child_process');
|
|
const osName = require('os-name');
|
|
const Conf = require('conf');
|
|
const chalk = require('chalk');
|
|
const debounce = require('lodash.debounce');
|
|
const inquirer = require('inquirer');
|
|
const uuid = require('uuid');
|
|
const providers = require('./providers.js');
|
|
|
|
const DEBOUNCE_MS = 100;
|
|
|
|
class Insight {
|
|
constructor(options) {
|
|
options = options || {};
|
|
options.pkg = options.pkg || {};
|
|
|
|
// Deprecated options
|
|
// TODO: Remove these at some point in the future
|
|
if (options.packageName) {
|
|
options.pkg.name = options.packageName;
|
|
}
|
|
|
|
if (options.packageVersion) {
|
|
options.pkg.version = options.packageVersion;
|
|
}
|
|
|
|
if (!options.trackingCode || !options.pkg.name) {
|
|
throw new Error('trackingCode and pkg.name required');
|
|
}
|
|
|
|
this.trackingCode = options.trackingCode;
|
|
this.trackingProvider = options.trackingProvider || 'google';
|
|
this.packageName = options.pkg.name;
|
|
this.packageVersion = options.pkg.version || 'undefined';
|
|
this.os = osName();
|
|
this.nodeVersion = process.version;
|
|
this.appVersion = this.packageVersion;
|
|
this.config = options.config || new Conf({
|
|
configName: `insight-${this.packageName}`,
|
|
defaults: {
|
|
clientId: options.clientId || Math.floor(Date.now() * Math.random()),
|
|
},
|
|
});
|
|
this._queue = {};
|
|
this._permissionTimeout = 30;
|
|
this._debouncedSend = debounce(this._send, DEBOUNCE_MS, {leading: true});
|
|
}
|
|
|
|
get optOut() {
|
|
return this.config.get('optOut');
|
|
}
|
|
|
|
set optOut(value) {
|
|
this.config.set('optOut', value);
|
|
}
|
|
|
|
get clientId() {
|
|
return this.config.get('clientId');
|
|
}
|
|
|
|
set clientId(value) {
|
|
this.config.set('clientId', value);
|
|
}
|
|
|
|
_save() {
|
|
setImmediate(() => {
|
|
this._debouncedSend();
|
|
});
|
|
}
|
|
|
|
_send() {
|
|
const pending = Object.keys(this._queue).length;
|
|
if (pending === 0) {
|
|
return;
|
|
}
|
|
|
|
this._fork(this._getPayload());
|
|
this._queue = {};
|
|
}
|
|
|
|
_fork(payload) {
|
|
// Extracted to a method so it can be easily mocked
|
|
const cp = childProcess.fork(path.join(__dirname, 'push.js'), {silent: true});
|
|
cp.send(payload);
|
|
cp.unref();
|
|
cp.disconnect();
|
|
}
|
|
|
|
_getPayload() {
|
|
return {
|
|
queue: {...this._queue},
|
|
packageName: this.packageName,
|
|
packageVersion: this.packageVersion,
|
|
trackingCode: this.trackingCode,
|
|
trackingProvider: this.trackingProvider,
|
|
};
|
|
}
|
|
|
|
_getRequestObj(...args) {
|
|
return providers[this.trackingProvider].apply(this, args);
|
|
}
|
|
|
|
track(...args) {
|
|
if (this.optOut) {
|
|
return;
|
|
}
|
|
|
|
const path = '/' + args.map(element => String(element).trim().replace(/ /, '-')).join('/');
|
|
|
|
// Timestamp isn't unique enough since it can end up with duplicate entries
|
|
this._queue[`${Date.now()} ${uuid.v4()}`] = {
|
|
path,
|
|
type: 'pageview',
|
|
};
|
|
this._save();
|
|
}
|
|
|
|
trackEvent(options) {
|
|
if (this.optOut) {
|
|
return;
|
|
}
|
|
|
|
if (this.trackingProvider !== 'google') {
|
|
throw new Error('Event tracking is supported only for Google Analytics');
|
|
}
|
|
|
|
if (!options || !options.category || !options.action) {
|
|
throw new Error('`category` and `action` required');
|
|
}
|
|
|
|
// Timestamp isn't unique enough since it can end up with duplicate entries
|
|
this._queue[`${Date.now()} ${uuid.v4()}`] = {
|
|
category: options.category,
|
|
action: options.action,
|
|
label: options.label,
|
|
value: options.value,
|
|
type: 'event',
|
|
};
|
|
this._save();
|
|
}
|
|
|
|
askPermission(message) {
|
|
const defaultMessage = `May ${chalk.cyan(this.packageName)} anonymously report usage statistics to improve the tool over time?`;
|
|
|
|
if (!process.stdout.isTTY || process.argv.includes('--no-insight') || process.env.CI) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const prompt = inquirer.prompt({
|
|
type: 'confirm',
|
|
name: 'optIn',
|
|
message: message || defaultMessage,
|
|
default: true,
|
|
});
|
|
|
|
// Set a 30 sec timeout before giving up on getting an answer
|
|
let permissionTimeout;
|
|
const timeoutPromise = new Promise(resolve => {
|
|
permissionTimeout = setTimeout(() => {
|
|
// Stop listening for stdin
|
|
prompt.ui.close();
|
|
|
|
// Automatically opt out
|
|
this.optOut = true;
|
|
resolve(false);
|
|
}, this._permissionTimeout * 1000);
|
|
});
|
|
|
|
const promise = (async () => {
|
|
const {optIn} = await prompt;
|
|
|
|
// Clear the permission timeout upon getting an answer
|
|
clearTimeout(permissionTimeout);
|
|
|
|
this.optOut = !optIn;
|
|
return optIn;
|
|
})();
|
|
|
|
// Return the result of the prompt if it finishes first otherwise default to the timeout's value.
|
|
return Promise.race([promise, timeoutPromise]);
|
|
}
|
|
}
|
|
|
|
module.exports = Insight;
|