213 lines
4.9 KiB
JavaScript
213 lines
4.9 KiB
JavaScript
|
var assert = require('assert')
|
||
|
var clone = require('clone')
|
||
|
var debug = require('debug')('analytics-node')
|
||
|
var noop = function () {}
|
||
|
var request = require('superagent')
|
||
|
require('superagent-retry')(request)
|
||
|
var uid = require('crypto-token')
|
||
|
var version = require('../package.json').version
|
||
|
var extend = require('lodash/extend')
|
||
|
var validate = require('@segment/loosely-validate-event')
|
||
|
var removeSlash = require('remove-trailing-slash')
|
||
|
|
||
|
global.setImmediate = global.setImmediate || process.nextTick.bind(process)
|
||
|
|
||
|
/**
|
||
|
* Expose an `Analytics` client.
|
||
|
*/
|
||
|
|
||
|
module.exports = Analytics
|
||
|
|
||
|
/**
|
||
|
* Initialize a new `Analytics` with your Segment project's `writeKey` and an
|
||
|
* optional dictionary of `options`.
|
||
|
*
|
||
|
* @param {String} writeKey
|
||
|
* @param {Object} [options] (optional)
|
||
|
* @property {Number} flushAt (default: 20)
|
||
|
* @property {Number} flushAfter (default: 10000)
|
||
|
* @property {String} host (default: 'https://api.segment.io')
|
||
|
*/
|
||
|
|
||
|
function Analytics (writeKey, options) {
|
||
|
if (!(this instanceof Analytics)) return new Analytics(writeKey, options)
|
||
|
assert(writeKey, 'You must pass your Segment project\'s write key.')
|
||
|
options = options || {}
|
||
|
this.queue = []
|
||
|
this.writeKey = writeKey
|
||
|
this.host = removeSlash(options.host || 'https://api.segment.io')
|
||
|
this.flushAt = Math.max(options.flushAt, 1) || 20
|
||
|
this.flushAfter = options.flushAfter || 10000
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send an identify `message`.
|
||
|
*
|
||
|
* @param {Object} message
|
||
|
* @param {Function} [fn] (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.identify = function (message, fn) {
|
||
|
validate(message, 'identify')
|
||
|
this.enqueue('identify', message, fn)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send a group `message`.
|
||
|
*
|
||
|
* @param {Object} message
|
||
|
* @param {Function} [fn] (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.group = function (message, fn) {
|
||
|
validate(message, 'group')
|
||
|
this.enqueue('group', message, fn)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send a track `message`.
|
||
|
*
|
||
|
* @param {Object} message
|
||
|
* @param {Function} [fn] (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.track = function (message, fn) {
|
||
|
validate(message, 'track')
|
||
|
this.enqueue('track', message, fn)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send a page `message`.
|
||
|
*
|
||
|
* @param {Object} message
|
||
|
* @param {Function} [fn] (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.page = function (message, fn) {
|
||
|
validate(message, 'page')
|
||
|
this.enqueue('page', message, fn)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send a screen `message`.
|
||
|
*
|
||
|
* @param {Object} message
|
||
|
* @param {Function} fn (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.screen = function (message, fn) {
|
||
|
validate(message, 'screen')
|
||
|
this.enqueue('screen', message, fn)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send an alias `message`.
|
||
|
*
|
||
|
* @param {Object} message
|
||
|
* @param {Function} [fn] (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.alias = function (message, fn) {
|
||
|
validate(message, 'alias')
|
||
|
this.enqueue('alias', message, fn)
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Flush the current queue and callback `fn(err, batch)`.
|
||
|
*
|
||
|
* @param {Function} [fn] (optional)
|
||
|
* @return {Analytics}
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.flush = function (fn) {
|
||
|
fn = fn || noop
|
||
|
if (!this.queue.length) return setImmediate(fn)
|
||
|
|
||
|
var items = this.queue.splice(0, this.flushAt)
|
||
|
var fns = items.map(function (_) { return _.callback })
|
||
|
var batch = items.map(function (_) { return _.message })
|
||
|
|
||
|
var data = {
|
||
|
batch: batch,
|
||
|
timestamp: new Date(),
|
||
|
sentAt: new Date()
|
||
|
}
|
||
|
|
||
|
debug('flush: %o', data)
|
||
|
|
||
|
request
|
||
|
.post(this.host + '/v1/batch')
|
||
|
.auth(this.writeKey, '')
|
||
|
.retry(3)
|
||
|
.send(data)
|
||
|
.end(function (err, res) {
|
||
|
err = err || error(res)
|
||
|
fns.push(fn)
|
||
|
fns.forEach(function (fn) { fn(err, data) })
|
||
|
debug('flushed: %o', data)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add a `message` of type `type` to the queue and check whether it should be
|
||
|
* flushed.
|
||
|
*
|
||
|
* @param {String} type
|
||
|
* @param {Object} message
|
||
|
* @param {Functino} [fn] (optional)
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
Analytics.prototype.enqueue = function (type, message, fn) {
|
||
|
fn = fn || noop
|
||
|
message = clone(message)
|
||
|
message.type = type
|
||
|
message.context = extend(message.context || {}, {
|
||
|
library: {
|
||
|
name: 'analytics-node',
|
||
|
version: version
|
||
|
}
|
||
|
})
|
||
|
|
||
|
message._metadata = extend(message._metadata || {}, { nodeVersion: process.versions.node })
|
||
|
|
||
|
if (!message.timestamp) message.timestamp = new Date()
|
||
|
if (!message.messageId) message.messageId = 'node-' + uid(32)
|
||
|
|
||
|
debug('%s: %o', type, message)
|
||
|
this.queue.push({
|
||
|
message: message,
|
||
|
callback: fn
|
||
|
})
|
||
|
|
||
|
if (this.queue.length >= this.flushAt) this.flush()
|
||
|
if (this.timer) clearTimeout(this.timer)
|
||
|
if (this.flushAfter) this.timer = setTimeout(this.flush.bind(this), this.flushAfter)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get an error from a `res`.
|
||
|
*
|
||
|
* @param {Object} res
|
||
|
* @return {String}
|
||
|
*/
|
||
|
|
||
|
function error (res) {
|
||
|
if (!res.error) return
|
||
|
var body = res.body
|
||
|
var msg = (body.error && body.error.message) || res.status + ' ' + res.text
|
||
|
return new Error(msg)
|
||
|
}
|