UNPKG

quala-node

Version:
344 lines (275 loc) 7.81 kB
'use strict' import * as assert from 'assert'; import axios, { AxiosError, AxiosRequestConfig } from 'axios' const axiosRetry = require('axios-retry') import {exponentialDelay, isNetworkError} from 'axios-retry' import ms from 'ms' import removeSlash from './removeSlash' import { IsNotEmpty, IsOptional, validateSync } from 'class-validator' import Timeout = NodeJS.Timeout const version = require('../package.json').version const setImmediate = global.setImmediate || process.nextTick.bind(process) const noop = () => {} interface ObjectLiteral { [key: string]: any; } interface Options { timeout?: boolean flushInterval?: number flushAt?: number enable?: boolean retryCount?: number host?: string } export enum MessageType { 'IDENTIFY' = 'IDENTIFY', 'IDENTIFY_COMPANY' = 'IDENTIFY_COMPANY', 'TRACK' = 'TRACK', } export class Message { timestamp?: Date @IsNotEmpty() userId: string } export class BatchMessage extends Message { @IsNotEmpty() type: MessageType } export class CompanyTraits { name?: string industry?: string plan?: string renewalDate?: Date accountCreatedDate?: Date arr?: number } export class UserTraits { avatar?: string email?: string description?: string firstName?: string lastName?: string name?: string phone?: string title?: string username?: string website?: string } export class IndentifyMessage extends Message { @IsNotEmpty() companyId: string @IsOptional() traits?: UserTraits & ObjectLiteral @IsOptional() companyTraits?: CompanyTraits & ObjectLiteral } class IndentifyCompanyMessage extends Message { @IsNotEmpty() companyId: string @IsOptional() companyTraits: CompanyTraits & ObjectLiteral } class Properties { value?: number currency?: string revenue?: number } export class TrackMessage extends Message { @IsNotEmpty() signal: string @IsOptional() properties?: Properties & ObjectLiteral } export default class Beacon { private queue: {message: BatchMessage, callback: any}[] private writeKey: string private host: string private timeout: number | ProgressEvent | any private flushAt: number private flushInterval: number private flushed: boolean private enable: boolean private timer: Timeout = null /** * Initialize a new `Beacon` with your Quala project's `writeKey` and an * optional dictionary of `options`. * * @param {String} apiKey * @param {Object} [options] (optional) * @property {Number} flushAt (default: 20) * @property {Number} flushInterval (default: 10000) * @property {String} host (default: 'https://api.quala.io') * @property {Boolean} enable (default: true) */ constructor (writeKey: string, options: Options = {}) { assert(writeKey, 'You must pass your Quala project\'s writeKey.') this.queue = [] this.writeKey = writeKey this.host = removeSlash(options.host || 'https://beacon.quala.io') this.timeout = options.timeout || false this.flushAt = Math.max(options.flushAt, 1) || 20 this.flushInterval = options.flushInterval || 10000 this.flushed = false this.enable = options.enable || true // Object.defineProperty(this, 'enable', { // configurable: false, // writable: false, // enumerable: true, // value: typeof options.enable === 'boolean' ? options.enable : true // }) axiosRetry(axios, { retries: options.retryCount || 3, retryCondition: this._isErrorRetryable, retryDelay: exponentialDelay }) } static _validate (object: Object) { validateSync(object) } /** * Send an identify `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Beacon} */ identify (message: IndentifyMessage, callback: any = undefined) { Beacon._validate(message) this.enqueue(MessageType.IDENTIFY, message, callback) return this } /** * Send an identifyCompany `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Beacon} */ identifyCompany (message: IndentifyCompanyMessage, callback: any = undefined) { Beacon._validate(message) this.enqueue(MessageType.IDENTIFY_COMPANY, message, callback) return this } /** * Send a track `message`. * * @param {Object} message * @param {Function} [callback] (optional) * @return {Beacon} */ track (message: TrackMessage, callback: any = undefined) { Beacon._validate(message) this.enqueue(MessageType.TRACK, 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: MessageType, message: Message, callback: any) { callback = callback || noop if (!this.enable) { return setImmediate(callback) } let batchMessage = Object.assign(new BatchMessage(), message) batchMessage.type = type if (!batchMessage.timestamp) { batchMessage.timestamp = new Date() } this.queue.push({ message: batchMessage, callback }) if (!this.flushed) { this.flushed = true this.flush() return } if (this.queue.length >= this.flushAt) { this.flush() } if (this.flushInterval && !this.timer) { this.timer = setTimeout(this.flush.bind(this), this.flushInterval) } } /** * Flush the current queue * * @param {Function} [callback] (optional) * @return {Beacon} */ flush (callback:any = undefined) { 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 => item.message) const data = { batch: messages, timestamp: new Date(), sentAt: new Date() } const done = (err:any = undefined) => { 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: any = { 'authorization': `writeKey: ${this.writeKey}` } if (typeof window === 'undefined') { headers['user-agent'] = `beacon-node/${version}` } const req: AxiosRequestConfig = { method: 'POST', url: `${this.host}/v1/batch`, data, headers } if (this.timeout) { req.timeout = typeof this.timeout === 'string' ? ms(this.timeout) : this.timeout } axios(req) .then(() => done()) .catch(err => { if (err.response) { const error = new Error(err.response.statusText) return done(error) } done(err) }) } _isErrorRetryable (error: AxiosError) { // Retry Network Errors. if (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 } }