UNPKG

aixp-node-sdk

Version:

The hassle-free way to integrate analytics into any Node.js application

359 lines (302 loc) 9.3 kB
const assert = require("assert"); const removeSlash = require("remove-trailing-slash"); const looselyValidate = require("@segment/loosely-validate-event"); const axios = require("axios"); const axiosRetry = require("axios-retry"); const ms = require("ms"); const uuid = require("uuid/v4"); const md5 = require("md5"); const isString = require("lodash.isstring"); const version = require("./package.json").version; const setImmediate = global.setImmediate || process.nextTick.bind(process); const noop = () => {}; class 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} flushInterval (default: 10000) * @property {String} host (default: required) * @property {Boolean} enable (default: true) */ constructor(writeKey, dataPlaneURL, options) { options = options || {}; assert(writeKey, "You must pass your project's write key."); assert(dataPlaneURL, "You must pass your data plane url."); this.queue = []; this.writeKey = writeKey; this.host = removeSlash(dataPlaneURL); this.timeout = options.timeout || false; this.flushAt = Math.max(options.flushAt, 1) || 20; this.flushInterval = options.flushInterval || 10000; this.flushed = false; this.httpAgent = options.httpAgent || null this.httpsAgent = options.httpsAgent || null let axiosInstance = options.axiosInstance if (axiosInstance == null) { axiosInstance = axios.create(options.axiosConfig) } this.axiosInstance = axiosInstance Object.defineProperty(this, "enable", { configurable: false, writable: false, enumerable: true, value: typeof options.enable === "boolean" ? options.enable : true }); axiosRetry(this.axiosInstance, { retries: options.retryCount || 7, retryCondition: this._isErrorRetryable, retryDelay: axiosRetry.exponentialDelay }); } _validate(message, type) { try { looselyValidate(message, type); } catch (e) { if (e.message === "Your message must be < 32kb.") { console.log( "Your message must be < 32kb. This is currently surfaced as a warning to allow clients to update. Versions released after August 1, 2018 will throw an error instead. Please update your code before then.", message ); return; } throw e; } } /** * Send an identify `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Analytics} */ identify(message, callback) { this._validate(message, "identify"); this.enqueue("identify", message, callback); return this; } /** * Send a group `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Analytics} */ group(message, callback) { this._validate(message, "group"); this.enqueue("group", message, callback); return this; } /** * Send a track `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Analytics} */ track(message, callback) { this._validate(message, "track"); this.enqueue("track", message, callback); return this; } /** * Send a page `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Analytics} */ page(message, callback) { this._validate(message, "page"); this.enqueue("page", message, callback); return this; } /** * Send a screen `message`. * * @param {Object} message * @param {Function} fn (optional) * @return {Analytics} */ screen(message, callback) { this._validate(message, "screen"); this.enqueue("screen", message, callback); return this; } /** * Send an alias `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Analytics} */ alias(message, callback) { this._validate(message, "alias"); this.enqueue("alias", message, callback); return this; } /** * Add a `message` of type `type` to the queue and * check whether it should be flushed. * * @param {String} type * @param {Object} message * @param {Function} [callback] (optional) * @api private */ enqueue(type, message, callback) { callback = callback || noop; if (!this.enable) { return setImmediate(callback); } if (type == "identify") { if (message.traits) { if (!message.context) { message.context = {}; } message.context.traits = message.traits; } } message = { ...message }; message.type = type; message.context = { library: { name: "analytics-node", version }, ...message.context }; message._metadata = { nodeVersion: process.versions.node, ...message._metadata }; if (!message.originalTimestamp) { message.originalTimestamp = new Date(); } if (!message.messageId) { // We md5 the messaage to add more randomness. This is primarily meant // for use in the browser where the uuid package falls back to Math.random() // which is not a great source of randomness. // Borrowed from analytics.js (https://github.com/segment-integrations/analytics.js-integration-segmentio/blob/a20d2a2d222aeb3ab2a8c7e72280f1df2618440e/lib/index.js#L255-L256). message.messageId = `node-${md5(JSON.stringify(message))}-${uuid()}`; } // Historically this library has accepted strings and numbers as IDs. // However, our spec only allows strings. To avoid breaking compatibility, // we'll coerce these to strings if they aren't already. if (message.anonymousId && !isString(message.anonymousId)) { message.anonymousId = JSON.stringify(message.anonymousId); } if (message.userId && !isString(message.userId)) { message.userId = JSON.stringify(message.userId); } if (message.sourceId && !isString(message.sourceId)) { message.sourceId = JSON.stringify(message.sourceId) } this.queue.push({ message, callback }); if (!this.flushed) { this.flushed = true; this.flush(callback); return; } if (this.queue.length >= this.flushAt) { this.flush(callback); } if (this.flushInterval && !this.timer) { this.timer = setTimeout(this.flush.bind(this, callback), this.flushInterval); } } /** * Flush the current queue * * @param {Function} [callback] (optional) * @return {Analytics} */ flush(callback) { callback = callback || noop; if (!this.enable) { return setImmediate(callback); } if (this.timer) { clearTimeout(this.timer); this.timer = null; } if (!this.queue.length) { return setImmediate(callback); } const items = this.queue.splice(0, this.flushAt); const callbacks = items.map(item => item.callback); const messages = items.map(item => { // if someone mangles directly with queue if (typeof item.message == "object") { item.message.sentAt = new Date(); } return item.message; }); const data = { batch: messages, sentAt: new Date() }; // console.log("===data===", data); const done = err => { callbacks.forEach(callback => callback(err)) callback(err, data); }; // Don't set the user agent if we're not on a browser. The latest spec allows // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader), // but browsers such as Chrome and Safari have not caught up. const headers = {}; if (typeof window === "undefined") { headers["user-agent"] = `analytics-node/${version}`; } const req = { method: "POST", url: `${this.host}`, auth: { username: this.writeKey }, data, headers, proxy: false }; if (this.httpAgent) req.httpAgent = this.httpAgent if (this.httpsAgent) req.httpsAgent = this.httpsAgent if (this.timeout) { req.timeout = typeof this.timeout === "string" ? ms(this.timeout) : this.timeout; } // console.log("===making axios request==="); this.axiosInstance(req) .then(() => done()) .catch(err => { if (err.response) { const error = new Error(err.response.statusText); return done(error); } done(err); }); } _isErrorRetryable(error) { // Retry Network Errors. if (axiosRetry.isNetworkError(error)) { return true; } if (!error.response) { // Cannot determine if the request can be retried return false; } // Retry Server Errors (5xx). if (error.response.status >= 500 && error.response.status <= 599) { return true; } // Retry if rate limited. if (error.response.status === 429) { return true; } return false; } } module.exports = Analytics;