posthog-node 1.2.0 → 2.0.0-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/feature-flags.js DELETED
@@ -1,165 +0,0 @@
1
- const axios = require('axios')
2
- const crypto = require('crypto')
3
- const ms = require('ms')
4
- const version = require('./package.json').version
5
-
6
- const LONG_SCALE = 0xfffffffffffffff
7
-
8
- class ClientError extends Error {
9
- constructor(message, extra) {
10
- super()
11
- Error.captureStackTrace(this, this.constructor)
12
- this.name = 'ClientError'
13
- this.message = message
14
- if (extra) {
15
- this.extra = extra
16
- }
17
- }
18
- }
19
-
20
- class FeatureFlagsPoller {
21
- constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, featureFlagCalledCallback }) {
22
- this.pollingInterval = pollingInterval
23
- this.personalApiKey = personalApiKey
24
- this.featureFlags = []
25
- this.loadedSuccessfullyOnce = false
26
- this.timeout = timeout
27
- this.projectApiKey = projectApiKey
28
- this.featureFlagCalledCallback = featureFlagCalledCallback
29
- this.host = host
30
- this.poller = null
31
-
32
- void this.loadFeatureFlags()
33
- }
34
-
35
- async isFeatureEnabled(key, distinctId, defaultResult = false) {
36
- await this.loadFeatureFlags()
37
-
38
- if (!this.loadedSuccessfullyOnce) {
39
- return defaultResult
40
- }
41
-
42
- let featureFlag = null
43
-
44
- for (const flag of this.featureFlags) {
45
- if (key === flag.key) {
46
- featureFlag = flag
47
- break
48
- }
49
- }
50
-
51
- if (!featureFlag) {
52
- return defaultResult
53
- }
54
-
55
- let isFlagEnabledResponse
56
-
57
- if (featureFlag.is_simple_flag) {
58
- isFlagEnabledResponse = this._isSimpleFlagEnabled({
59
- key,
60
- distinctId,
61
- rolloutPercentage: featureFlag.rolloutPercentage,
62
- })
63
- } else {
64
- const res = await this._request({ path: 'decide', method: 'POST', data: { distinct_id: distinctId } })
65
- isFlagEnabledResponse = res.data.featureFlags.indexOf(key) >= 0
66
- }
67
-
68
- this.featureFlagCalledCallback(key, distinctId, isFlagEnabledResponse)
69
- return isFlagEnabledResponse
70
- }
71
-
72
- async loadFeatureFlags(forceReload = false) {
73
- if (!this.loadedSuccessfullyOnce || forceReload) {
74
- await this._loadFeatureFlags()
75
- }
76
- }
77
-
78
- /* istanbul ignore next */
79
- async _loadFeatureFlags() {
80
- if (this.poller) {
81
- clearTimeout(this.poller)
82
- this.poller = null
83
- }
84
- this.poller = setTimeout(() => this._loadFeatureFlags(), this.pollingInterval)
85
-
86
- try {
87
- const res = await this._request({ path: 'api/feature_flag', usePersonalApiKey: true })
88
- if (res && res.status === 401) {
89
- throw new ClientError(
90
- `Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview`
91
- )
92
- }
93
-
94
- this.featureFlags = res.data.results.filter(flag => flag.active)
95
-
96
- this.loadedSuccessfullyOnce = true
97
- } catch (err) {
98
- // if an error that is not an instance of ClientError is thrown
99
- // we silently ignore the error when reloading feature flags
100
- if (err instanceof ClientError) {
101
- throw err
102
- }
103
- }
104
- }
105
-
106
- // sha1('a.b') should equal '69f6642c9d71b463485b4faf4e989dc3fe77a8c6'
107
- // integerRepresentationOfHashSubset / LONG_SCALE for sha1('a.b') should equal 0.4139158829615955
108
- _isSimpleFlagEnabled({ key, distinctId, rolloutPercentage }) {
109
- if (!rolloutPercentage) {
110
- return true
111
- }
112
- const sha1Hash = crypto.createHash('sha1')
113
- sha1Hash.update(`${key}.${distinctId}`)
114
- const integerRepresentationOfHashSubset = parseInt(sha1Hash.digest('hex').slice(0, 15), 16)
115
- return integerRepresentationOfHashSubset / LONG_SCALE <= rolloutPercentage / 100
116
- }
117
-
118
- /* istanbul ignore next */
119
- async _request({ path, method = 'GET', usePersonalApiKey = false, data = {} }) {
120
- let url = `${this.host}/${path}/`
121
- let headers = {
122
- 'Content-Type': 'application/json',
123
- }
124
-
125
- if (usePersonalApiKey) {
126
- headers = { ...headers, Authorization: `Bearer ${this.personalApiKey}` }
127
- url = url + `?token=${this.projectApiKey}`
128
- } else {
129
- data = { ...data, token: this.projectApiKey }
130
- }
131
-
132
- if (typeof window === 'undefined') {
133
- headers['user-agent'] = `posthog-node/${version}`
134
- }
135
-
136
- const req = {
137
- method: method,
138
- url: url,
139
- headers: headers,
140
- data: JSON.stringify(data),
141
- }
142
-
143
- if (this.timeout) {
144
- req.timeout = typeof this.timeout === 'string' ? ms(this.timeout) : this.timeout
145
- }
146
-
147
-
148
- let res
149
- try {
150
- res = await axios(req)
151
- } catch (err) {
152
- throw new Error(`Request to ${path} failed with error: ${err.message}`)
153
- }
154
-
155
- return res
156
- }
157
-
158
- stopPoller() {
159
- clearTimeout(this.poller)
160
- }
161
- }
162
-
163
- module.exports = {
164
- FeatureFlagsPoller,
165
- }
package/index.d.ts DELETED
@@ -1,102 +0,0 @@
1
- // Type definitions for posthog-node
2
- // Project: Posthog
3
-
4
- declare module 'posthog-node' {
5
- interface Option {
6
- flushAt?: number
7
- flushInterval?: number
8
- host?: string
9
- enable?: boolean
10
- personalApiKey?: string
11
- featureFlagsPollingInterval?: number
12
- }
13
- interface IdentifyMessage {
14
- distinctId: string
15
- properties?: Record<string | number, any>
16
- }
17
-
18
- interface EventMessage extends IdentifyMessage {
19
- event: string
20
- groups?: Record<string, string | number> // Mapping of group type to group id
21
- }
22
-
23
- interface GroupIdentifyMessage {
24
- groupType: string
25
- groupKey: string // Unique identifier for the group
26
- properties?: Record<string | number, any>
27
- }
28
-
29
- export default class PostHog {
30
- constructor(apiKey: string, options?: Option)
31
- /**
32
- * @description Capture allows you to capture anything a user does within your system,
33
- * which you can later use in PostHog to find patterns in usage,
34
- * work out which features to improve or where people are giving up.
35
- * A capture call requires:
36
- * @param distinctId which uniquely identifies your user
37
- * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
38
- * @param properties OPTIONAL | which can be a object with any information you'd like to add
39
- * @param groups OPTIONAL | object of what groups are related to this event, example: { company: 'id:5' }. Can be used to analyze companies instead of users.
40
- */
41
- capture({ distinctId, event, properties, groups }: EventMessage): void
42
-
43
- /**
44
- * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog,
45
- * and even do things like segment users by these properties.
46
- * An identify call requires:
47
- * @param distinctId which uniquely identifies your user
48
- * @param properties with a dict with any key: value pairs
49
- */
50
- identify({ distinctId, properties }: IdentifyMessage): void
51
-
52
- /**
53
- * @description To marry up whatever a user does before they sign up or log in with what they do after you need to make an alias call.
54
- * This will allow you to answer questions like "Which marketing channels leads to users churning after a month?"
55
- * or "What do users do on our website before signing up?"
56
- * In a purely back-end implementation, this means whenever an anonymous user does something, you'll want to send a session ID with the capture call.
57
- * Then, when that users signs up, you want to do an alias call with the session ID and the newly created user ID.
58
- * The same concept applies for when a user logs in. If you're using PostHog in the front-end and back-end,
59
- * doing the identify call in the frontend will be enough.:
60
- * @param distinctId the current unique id
61
- * @param alias the unique ID of the user before
62
- */
63
- alias(data: { distinctId: string; alias: string }): void
64
-
65
-
66
- /**
67
- * @description PostHog feature flags (https://posthog.com/docs/features/feature-flags)
68
- * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
69
- * you can use this method to check if the flag is on for a given user, allowing you to create logic to turn
70
- * features on and off for different user groups or individual users.
71
- * IMPORTANT: To use this method, you need to specify `personalApiKey` in your config! More info: https://posthog.com/docs/api/overview
72
- * @param key the unique key of your feature flag
73
- * @param distinctId the current unique id
74
- * @param defaultResult optional - default value to be returned if the feature flag is not on for the user
75
- */
76
- isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean): Promise<boolean>
77
-
78
-
79
- /**
80
- * @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
81
- * using my product in PostHog.
82
- *
83
- * @param groupType Type of group (ex: 'company'). Limited to 5 per project
84
- * @param groupKey Unique identifier for that type of group (ex: 'id:5')
85
- * @param properties OPTIONAL | which can be a object with any information you'd like to add
86
- */
87
- groupIdentify({ groupType, groupKey, properties }: GroupIdentifyMessage): void
88
-
89
- /**
90
- * @description Force an immediate reload of the polled feature flags. Please note that they are
91
- * already polled automatically at a regular interval.
92
- */
93
- reloadFeatureFlags(): Promise<void>
94
-
95
- /**
96
- * @description Flushes the events still in the queue and clears the feature flags poller to allow for
97
- * a clean shutdown.
98
- */
99
- shutdown(): void
100
- }
101
-
102
- }
package/index.js DELETED
@@ -1,360 +0,0 @@
1
- 'use strict'
2
-
3
- const assert = require('assert')
4
- const removeSlash = require('remove-trailing-slash')
5
- const axios = require('axios')
6
- const axiosRetry = require('axios-retry')
7
- const ms = require('ms')
8
- const version = require('./package.json').version
9
- const looselyValidate = require('./event-validation')
10
- const { FeatureFlagsPoller } = require('./feature-flags')
11
-
12
- const setImmediate = global.setImmediate || process.nextTick.bind(process)
13
- const noop = () => {}
14
-
15
- const FIVE_MINUTES = 5 * 60 * 1000
16
- class PostHog {
17
- /**
18
- * Initialize a new `PostHog` with your PostHog project's `apiKey` and an
19
- * optional dictionary of `options`.
20
- *
21
- * @param {String} apiKey
22
- * @param {Object} [options] (optional)
23
- * @property {Number} flushAt (default: 20)
24
- * @property {Number} flushInterval (default: 10000)
25
- * @property {String} host (default: 'https://app.posthog.com')
26
- * @property {Boolean} enable (default: true)
27
- * @property {String} featureFlagsPollingInterval (default: 300000)
28
- * @property {String} personalApiKey
29
- */
30
-
31
- constructor(apiKey, options) {
32
- options = options || {}
33
-
34
- assert(apiKey, "You must pass your PostHog project's api key.")
35
-
36
- this.queue = []
37
- this.apiKey = apiKey
38
- this.host = removeSlash(options.host || 'https://app.posthog.com')
39
- this.timeout = options.timeout || false
40
- this.flushAt = Math.max(options.flushAt, 1) || 20
41
- this.flushInterval = typeof options.flushInterval === 'number' ? options.flushInterval : 10000
42
- this.flushed = false
43
- this.personalApiKey = options.personalApiKey
44
-
45
- Object.defineProperty(this, 'enable', {
46
- configurable: false,
47
- writable: false,
48
- enumerable: true,
49
- value: typeof options.enable === 'boolean' ? options.enable : true,
50
- })
51
-
52
- axiosRetry(axios, {
53
- retries: options.retryCount || 3,
54
- retryCondition: this._isErrorRetryable,
55
- retryDelay: axiosRetry.exponentialDelay,
56
- })
57
-
58
- if (this.personalApiKey) {
59
- const featureFlagCalledCallback = (key, distinctId, isFlagEnabledResponse) => {
60
- this.capture({
61
- distinctId,
62
- event: '$feature_flag_called',
63
- properties: {
64
- $feature_flag: key,
65
- $feature_flag_response: isFlagEnabledResponse,
66
- },
67
- })
68
- }
69
-
70
- this.featureFlagsPoller = new FeatureFlagsPoller({
71
- pollingInterval:
72
- typeof options.featureFlagsPollingInterval === 'number'
73
- ? options.featureFlagsPollingInterval
74
- : FIVE_MINUTES,
75
- personalApiKey: options.personalApiKey,
76
- projectApiKey: apiKey,
77
- timeout: options.timeout || false,
78
- host: this.host,
79
- featureFlagCalledCallback,
80
- })
81
- }
82
- }
83
-
84
- _validate(message, type) {
85
- try {
86
- looselyValidate(message, type)
87
- } catch (e) {
88
- if (e.message === 'Your message must be < 32 kB.') {
89
- console.log(
90
- 'Your message must be < 32 kB.',
91
- JSON.stringify(message)
92
- )
93
- return
94
- }
95
- throw e
96
- }
97
- }
98
-
99
- /**
100
- * Send an identify `message`.
101
- *
102
- * @param {Object} message
103
- * @param {Function} [callback] (optional)
104
- * @return {PostHog}
105
- */
106
-
107
- identify(message, callback) {
108
- this._validate(message, 'identify')
109
-
110
- const apiMessage = Object.assign({}, message, {
111
- $set: message.properties || {},
112
- event: '$identify',
113
- properties: {
114
- $lib: 'posthog-node',
115
- $lib_version: version,
116
- },
117
- })
118
-
119
- this.enqueue('identify', apiMessage, callback)
120
- return this
121
- }
122
-
123
- /**
124
- * Send a capture `message`.
125
- *
126
- * @param {Object} message
127
- * @param {Function} [callback] (optional)
128
- * @return {PostHog}
129
- */
130
-
131
- capture(message, callback) {
132
- this._validate(message, 'capture')
133
-
134
- const properties = Object.assign({}, message.properties, {
135
- $lib: 'posthog-node',
136
- $lib_version: version,
137
- })
138
-
139
- if ('groups' in message) {
140
- properties.$groups = message.groups
141
- delete message.groups
142
- }
143
-
144
- const apiMessage = Object.assign({}, message, { properties })
145
-
146
- this.enqueue('capture', apiMessage, callback)
147
- return this
148
- }
149
-
150
- /**
151
- * Send an alias `message`.
152
- *
153
- * @param {Object} message
154
- * @param {Function} [callback] (optional)
155
- * @return {PostHog}
156
- */
157
-
158
- alias(message, callback) {
159
- this._validate(message, 'alias')
160
-
161
- const apiMessage = Object.assign({}, message, {
162
- event: '$create_alias',
163
- properties: {
164
- distinct_id: message.distinctId || message.distinct_id,
165
- alias: message.alias,
166
- $lib: 'posthog-node',
167
- $lib_version: version,
168
- },
169
- })
170
- delete apiMessage.alias
171
- delete apiMessage.distinctId
172
- apiMessage.distinct_id = message.distinctId || message.distinct_id
173
-
174
- this.enqueue('alias', apiMessage, callback)
175
- return this
176
- }
177
-
178
- /**
179
- * @description Sets a groups properties, which allows asking questions like "Who are the most active companies"
180
- * using my product in PostHog.
181
- *
182
- * @param groupType Type of group (ex: 'company'). Limited to 5 per project
183
- * @param groupKey Unique identifier for that type of group (ex: 'id:5')
184
- * @param properties OPTIONAL | which can be a object with any information you'd like to add
185
- */
186
- groupIdentify(message, callback) {
187
- this._validate(message, 'groupIdentify')
188
-
189
- const captureMessage = {
190
- event: '$groupidentify',
191
- distinctId: `\$${message.groupType}_${message.groupKey}`,
192
- properties: {
193
- $group_type: message.groupType,
194
- $group_key: message.groupKey,
195
- $group_set: message.properties || {}
196
- }
197
- }
198
-
199
- return this.capture(captureMessage, callback)
200
- }
201
-
202
- /**
203
- * Add a `message` of type `type` to the queue and
204
- * check whether it should be flushed.
205
- *
206
- * @param {String} type
207
- * @param {Object} message
208
- * @param {Function} [callback] (optional)
209
- * @api private
210
- */
211
-
212
- enqueue(type, message, callback) {
213
- callback = callback || noop
214
-
215
- if (!this.enable) {
216
- return setImmediate(callback)
217
- }
218
-
219
- message = Object.assign({}, message)
220
- message.type = type
221
- message.library = 'posthog-node'
222
- message.library_version = version
223
-
224
- if (!message.timestamp) {
225
- message.timestamp = new Date()
226
- }
227
-
228
- if (message.distinctId) {
229
- message.distinct_id = message.distinctId
230
- delete message.distinctId
231
- }
232
-
233
- this.queue.push({ message, callback })
234
-
235
- if (!this.flushed) {
236
- this.flushed = true
237
- this.flush()
238
- return
239
- }
240
-
241
- if (this.queue.length >= this.flushAt) {
242
- this.flush()
243
- }
244
-
245
- if (this.flushInterval && !this.timer) {
246
- this.timer = setTimeout(() => this.flush(), this.flushInterval)
247
- }
248
- }
249
-
250
- async isFeatureEnabled(key, distinctId, defaultResult) {
251
- assert(this.personalApiKey, 'You have to specify the option personalApiKey to use feature flags.')
252
- return await this.featureFlagsPoller.isFeatureEnabled(key, distinctId, defaultResult)
253
- }
254
-
255
- async reloadFeatureFlags() {
256
- await this.featureFlagsPoller.loadFeatureFlags(true)
257
- }
258
-
259
- /**
260
- * Flush the current queue
261
- *
262
- * @param {Function} [callback] (optional)
263
- * @return {PostHog}
264
- */
265
-
266
- flush(callback) {
267
- callback = callback || noop
268
-
269
- if (!this.enable) {
270
- return setImmediate(callback)
271
- }
272
-
273
- if (this.timer) {
274
- clearTimeout(this.timer)
275
- this.timer = null
276
- }
277
-
278
- if (!this.queue.length) {
279
- return setImmediate(callback)
280
- }
281
-
282
- const items = this.queue.splice(0, this.flushAt)
283
- const callbacks = items.map((item) => item.callback)
284
- const messages = items.map((item) => item.message)
285
-
286
- const data = {
287
- api_key: this.apiKey,
288
- batch: messages,
289
- }
290
-
291
- const done = (err) => {
292
- callbacks.forEach((callback) => callback(err))
293
- callback(err, data)
294
- }
295
-
296
- // Don't set the user agent if we're not on a browser. The latest spec allows
297
- // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
298
- // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
299
- // but browsers such as Chrome and Safari have not caught up.
300
- const headers = {}
301
- if (typeof window === 'undefined') {
302
- headers['user-agent'] = `posthog-node/${version}`
303
- }
304
-
305
- const req = {
306
- method: 'POST',
307
- url: `${this.host}/batch/`,
308
- data,
309
- headers,
310
- }
311
-
312
- if (this.timeout) {
313
- req.timeout = typeof this.timeout === 'string' ? ms(this.timeout) : this.timeout
314
- }
315
-
316
- axios(req)
317
- .then(() => done())
318
- .catch((err) => {
319
- if (err.response) {
320
- const error = new Error(err.response.statusText)
321
- return done(error)
322
- }
323
-
324
- done(err)
325
- })
326
- }
327
-
328
- shutdown() {
329
- if (this.personalApiKey) {
330
- this.featureFlagsPoller.stopPoller()
331
- }
332
- this.flush()
333
- }
334
-
335
- _isErrorRetryable(error) {
336
- // Retry Network Errors.
337
- if (axiosRetry.isNetworkError(error)) {
338
- return true
339
- }
340
-
341
- if (!error.response) {
342
- // Cannot determine if the request can be retried
343
- return false
344
- }
345
-
346
- // Retry Server Errors (5xx).
347
- if (error.response.status >= 500 && error.response.status <= 599) {
348
- return true
349
- }
350
-
351
- // Retry if rate limited.
352
- if (error.response.status === 429) {
353
- return true
354
- }
355
-
356
- return false
357
- }
358
- }
359
-
360
- module.exports = PostHog