quala-node
Version:
client library for quala
344 lines (275 loc) • 7.81 kB
text/typescript
'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
userId: string
}
export class BatchMessage extends Message {
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 {
companyId: string
traits?: UserTraits & ObjectLiteral
companyTraits?: CompanyTraits & ObjectLiteral
}
class IndentifyCompanyMessage extends Message {
companyId: string
companyTraits: CompanyTraits & ObjectLiteral
}
class Properties {
value?: number
currency?: string
revenue?: number
}
export class TrackMessage extends Message {
signal: string
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
}
}