Budibase is an open-source low-code platform for creating internal apps in minutes. Supports PostgreSQL, MySQL, MSSQL, MongoDB, Rest API, Docker, K8s 🚀
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

'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