mirror of https://github.com/Budibase/budibase.git
nocodelowcodelow-codedockerdocker-composeinternal-projectinternal-toolinternal-toolslow-code-developmentlow-code-development-platformopensourceselfhostedweb-devweb-developmentweb-development-toolswebdevwebdevelopmentworkflow-automationautomationdeveloper-tools
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
10 KiB
362 lines
10 KiB
'use strict'
|
|
|
|
const assert = require('assert')
|
|
const removeSlash = require('remove-trailing-slash')
|
|
const axios = require('axios')
|
|
const axiosRetry = require('axios-retry')
|
|
const ms = require('ms')
|
|
const version = require('./package.json').version
|
|
const looselyValidate = require('./event-validation')
|
|
const { FeatureFlagsPoller } = require('./feature-flags')
|
|
|
|
const setImmediate = global.setImmediate || process.nextTick.bind(process)
|
|
const noop = () => {}
|
|
|
|
const FIVE_MINUTES = 5 * 60 * 1000
|
|
class PostHog {
|
|
/**
|
|
* Initialize a new `PostHog` with your PostHog project's `apiKey` 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://app.posthog.com')
|
|
* @property {Boolean} enable (default: true)
|
|
* @property {String} featureFlagsPollingInterval (default: 300000)
|
|
* @property {String} personalApiKey
|
|
*/
|
|
|
|
constructor(apiKey, options) {
|
|
options = options || {}
|
|
|
|
assert(apiKey, "You must pass your PostHog project's api key.")
|
|
|
|
this.queue = []
|
|
this.apiKey = apiKey
|
|
this.host = removeSlash(options.host || 'https://app.posthog.com')
|
|
this.timeout = options.timeout || false
|
|
this.flushAt = Math.max(options.flushAt, 1) || 20
|
|
this.flushInterval = typeof options.flushInterval === 'number' ? options.flushInterval : 10000
|
|
this.flushed = false
|
|
this.personalApiKey = options.personalApiKey
|
|
|
|
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: axiosRetry.exponentialDelay,
|
|
})
|
|
|
|
if (this.personalApiKey) {
|
|
const featureFlagCalledCallback = (key, distinctId, isFlagEnabledResponse) => {
|
|
this.capture({
|
|
distinctId,
|
|
event: '$feature_flag_called',
|
|
properties: {
|
|
$feature_flag: key,
|
|
$feature_flag_response: isFlagEnabledResponse,
|
|
},
|
|
})
|
|
}
|
|
|
|
this.featureFlagsPoller = new FeatureFlagsPoller({
|
|
pollingInterval:
|
|
typeof options.featureFlagsPollingInterval === 'number'
|
|
? options.featureFlagsPollingInterval
|
|
: FIVE_MINUTES,
|
|
personalApiKey: options.personalApiKey,
|
|
projectApiKey: apiKey,
|
|
timeout: options.timeout || false,
|
|
host: this.host,
|
|
featureFlagCalledCallback,
|
|
})
|
|
}
|
|
}
|
|
|
|
_validate(message, type) {
|
|
try {
|
|
looselyValidate(message, type)
|
|
} catch (e) {
|
|
if (e.message === 'Your message must be < 32 kB.') {
|
|
console.log(
|
|
'Your message must be < 32 kB.',
|
|
JSON.stringify(message)
|
|
)
|
|
return
|
|
}
|
|
throw e
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send an identify `message`.
|
|
*
|
|
* @param {Object} message
|
|
* @param {Function} [callback] (optional)
|
|
* @return {PostHog}
|
|
*/
|
|
|
|
identify(message, callback) {
|
|
this._validate(message, 'identify')
|
|
|
|
const apiMessage = Object.assign({}, message, {
|
|
$set: message.properties || {},
|
|
event: '$identify',
|
|
properties: {
|
|
$lib: 'posthog-node',
|
|
$lib_version: version,
|
|
},
|
|
})
|
|
|
|
this.enqueue('identify', apiMessage, callback)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Send a capture `message`.
|
|
*
|
|
* @param {Object} message
|
|
* @param {Function} [callback] (optional)
|
|
* @return {PostHog}
|
|
*/
|
|
|
|
capture(message, callback) {
|
|
this._validate(message, 'capture')
|
|
|
|
const properties = Object.assign({}, message.properties, {
|
|
$lib: 'posthog-node',
|
|
$lib_version: version,
|
|
})
|
|
|
|
if ('groups' in message) {
|
|
properties.$groups = message.groups
|
|
delete message.groups
|
|
}
|
|
|
|
const apiMessage = Object.assign({}, message, { properties })
|
|
|
|
this.enqueue('capture', apiMessage, callback)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* Send an alias `message`.
|
|
*
|
|
* @param {Object} message
|
|
* @param {Function} [callback] (optional)
|
|
* @return {PostHog}
|
|
*/
|
|
|
|
alias(message, callback) {
|
|
this._validate(message, 'alias')
|
|
|
|
const apiMessage = Object.assign({}, message, {
|
|
event: '$create_alias',
|
|
properties: {
|
|
distinct_id: message.distinctId || message.distinct_id,
|
|
alias: message.alias,
|
|
$lib: 'posthog-node',
|
|
$lib_version: version,
|
|
},
|
|
})
|
|
delete apiMessage.alias
|
|
delete apiMessage.distinctId
|
|
apiMessage.distinct_id = message.distinctId || message.distinct_id
|
|
|
|
this.enqueue('alias', apiMessage, callback)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
|
|
* using my product in PostHog.
|
|
*
|
|
* @param groupType Type of group (ex: 'company'). Limited to 5 per project
|
|
* @param groupKey Unique identifier for that type of group (ex: 'id:5')
|
|
* @param properties OPTIONAL | which can be a object with any information you'd like to add
|
|
*/
|
|
groupIdentify(message, callback) {
|
|
this._validate(message, 'groupIdentify')
|
|
|
|
const captureMessage = {
|
|
event: '$groupidentify',
|
|
distinctId: `\$${message.groupType}_${message.groupKey}`,
|
|
properties: {
|
|
$group_type: message.groupType,
|
|
$group_key: message.groupKey,
|
|
$group_set: message.properties || {}
|
|
}
|
|
}
|
|
|
|
return this.capture(captureMessage, callback)
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
}
|
|
|
|
message = Object.assign({}, message)
|
|
message.type = type
|
|
message.library = 'posthog-node'
|
|
message.library_version = version
|
|
|
|
if (!message.timestamp) {
|
|
message.timestamp = new Date()
|
|
}
|
|
|
|
if (message.distinctId) {
|
|
message.distinct_id = message.distinctId
|
|
delete message.distinctId
|
|
}
|
|
|
|
this.queue.push({ message, 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(), this.flushInterval)
|
|
}
|
|
}
|
|
|
|
async isFeatureEnabled(key, distinctId, defaultResult = false, groups = {}) {
|
|
this._validate({ key, distinctId, defaultResult, groups }, 'isFeatureEnabled')
|
|
assert(this.personalApiKey, 'You have to specify the option personalApiKey to use feature flags.')
|
|
|
|
return await this.featureFlagsPoller.isFeatureEnabled(key, distinctId, defaultResult, groups)
|
|
}
|
|
|
|
async reloadFeatureFlags() {
|
|
await this.featureFlagsPoller.loadFeatureFlags(true)
|
|
}
|
|
|
|
/**
|
|
* Flush the current queue
|
|
*
|
|
* @param {Function} [callback] (optional)
|
|
* @return {PostHog}
|
|
*/
|
|
|
|
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) => item.message)
|
|
|
|
const data = {
|
|
api_key: this.apiKey,
|
|
batch: messages,
|
|
}
|
|
|
|
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'] = `posthog-node/${version}`
|
|
}
|
|
|
|
const req = {
|
|
method: 'POST',
|
|
url: `${this.host}/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)
|
|
})
|
|
}
|
|
|
|
shutdown() {
|
|
if (this.personalApiKey) {
|
|
this.featureFlagsPoller.stopPoller()
|
|
}
|
|
this.flush()
|
|
}
|
|
|
|
_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 = PostHog
|
|
|