posthog-node 1.1.7 → 2.0.0-alpha1

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,84 +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
- }
21
-
22
- export default class PostHog {
23
- constructor(apiKey: string, options?: Option)
24
- /**
25
- * @description Capture allows you to capture anything a user does within your system,
26
- * which you can later use in PostHog to find patterns in usage,
27
- * work out which features to improve or where people are giving up.
28
- * A capture call requires:
29
- * @param distinctId which uniquely identifies your user
30
- * @param event We recommend using [verb] [noun], like movie played or movie updated to easily identify what your events mean later on.
31
- * @param properties OPTIONAL | which can be a dict with any information you'd like to add
32
- */
33
- capture({ distinctId, event, properties }: EventMessage): void
34
-
35
- /**
36
- * @description Identify lets you add metadata on your users so you can more easily identify who they are in PostHog,
37
- * and even do things like segment users by these properties.
38
- * An identify call requires:
39
- * @param distinctId which uniquely identifies your user
40
- * @param properties with a dict with any key: value pairs
41
- */
42
- identify({ distinctId, properties }: IdentifyMessage): void
43
-
44
- /**
45
- * @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.
46
- * This will allow you to answer questions like "Which marketing channels leads to users churning after a month?"
47
- * or "What do users do on our website before signing up?"
48
- * 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.
49
- * Then, when that users signs up, you want to do an alias call with the session ID and the newly created user ID.
50
- * The same concept applies for when a user logs in. If you're using PostHog in the front-end and back-end,
51
- * doing the identify call in the frontend will be enough.:
52
- * @param distinctId the current unique id
53
- * @param alias the unique ID of the user before
54
- */
55
- alias(data: { distinctId: string; alias: string }): void
56
-
57
-
58
- /**
59
- * @description PostHog feature flags (https://posthog.com/docs/features/feature-flags)
60
- * allow you to safely deploy and roll back new features. Once you've created a feature flag in PostHog,
61
- * you can use this method to check if the flag is on for a given user, allowing you to create logic to turn
62
- * features on and off for different user groups or individual users.
63
- * IMPORTANT: To use this method, you need to specify `personalApiKey` in your config! More info: https://posthog.com/docs/api/overview
64
- * @param key the unique key of your feature flag
65
- * @param distinctId the current unique id
66
- * @param defaultResult optional - default value to be returned if the feature flag is not on for the user
67
- */
68
- isFeatureEnabled(key: string, distinctId: string, defaultResult?: boolean): Promise<boolean>
69
-
70
-
71
- /**
72
- * @description Force an immediate reload of the polled feature flags. Please note that they are
73
- * already polled automatically at a regular interval.
74
- */
75
- reloadFeatureFlags(): Promise<void>
76
-
77
- /**
78
- * @description Flushes the events still in the queue and clears the feature flags poller to allow for
79
- * a clean shutdown.
80
- */
81
- shutdown(): void
82
- }
83
-
84
- }
package/index.js DELETED
@@ -1,331 +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 apiMessage = Object.assign({}, message, {
135
- properties: Object.assign({}, message.properties, {
136
- $lib: 'posthog-node',
137
- $lib_version: version,
138
- }),
139
- })
140
-
141
- this.enqueue('capture', apiMessage, callback)
142
- return this
143
- }
144
-
145
- /**
146
- * Send an alias `message`.
147
- *
148
- * @param {Object} message
149
- * @param {Function} [callback] (optional)
150
- * @return {PostHog}
151
- */
152
-
153
- alias(message, callback) {
154
- this._validate(message, 'alias')
155
-
156
- const apiMessage = Object.assign({}, message, {
157
- event: '$create_alias',
158
- properties: {
159
- distinct_id: message.distinctId || message.distinct_id,
160
- alias: message.alias,
161
- $lib: 'posthog-node',
162
- $lib_version: version,
163
- },
164
- })
165
- delete apiMessage.alias
166
- delete apiMessage.distinctId
167
- apiMessage.distinct_id = message.distinctId || message.distinct_id
168
-
169
- this.enqueue('alias', apiMessage, callback)
170
- return this
171
- }
172
-
173
- /**
174
- * Add a `message` of type `type` to the queue and
175
- * check whether it should be flushed.
176
- *
177
- * @param {String} type
178
- * @param {Object} message
179
- * @param {Function} [callback] (optional)
180
- * @api private
181
- */
182
-
183
- enqueue(type, message, callback) {
184
- callback = callback || noop
185
-
186
- if (!this.enable) {
187
- return setImmediate(callback)
188
- }
189
-
190
- message = Object.assign({}, message)
191
- message.type = type
192
- message.library = 'posthog-node'
193
- message.library_version = version
194
-
195
- if (!message.timestamp) {
196
- message.timestamp = new Date()
197
- }
198
-
199
- if (message.distinctId) {
200
- message.distinct_id = message.distinctId
201
- delete message.distinctId
202
- }
203
-
204
- this.queue.push({ message, callback })
205
-
206
- if (!this.flushed) {
207
- this.flushed = true
208
- this.flush()
209
- return
210
- }
211
-
212
- if (this.queue.length >= this.flushAt) {
213
- this.flush()
214
- }
215
-
216
- if (this.flushInterval && !this.timer) {
217
- this.timer = setTimeout(() => this.flush(), this.flushInterval)
218
- }
219
- }
220
-
221
- async isFeatureEnabled(key, distinctId, defaultResult) {
222
- assert(this.personalApiKey, 'You have to specify the option personalApiKey to use feature flags.')
223
- return await this.featureFlagsPoller.isFeatureEnabled(key, distinctId, defaultResult)
224
- }
225
-
226
- async reloadFeatureFlags() {
227
- await this.featureFlagsPoller.loadFeatureFlags(true)
228
- }
229
-
230
- /**
231
- * Flush the current queue
232
- *
233
- * @param {Function} [callback] (optional)
234
- * @return {PostHog}
235
- */
236
-
237
- flush(callback) {
238
- callback = callback || noop
239
-
240
- if (!this.enable) {
241
- return setImmediate(callback)
242
- }
243
-
244
- if (this.timer) {
245
- clearTimeout(this.timer)
246
- this.timer = null
247
- }
248
-
249
- if (!this.queue.length) {
250
- return setImmediate(callback)
251
- }
252
-
253
- const items = this.queue.splice(0, this.flushAt)
254
- const callbacks = items.map((item) => item.callback)
255
- const messages = items.map((item) => item.message)
256
-
257
- const data = {
258
- api_key: this.apiKey,
259
- batch: messages,
260
- }
261
-
262
- const done = (err) => {
263
- callbacks.forEach((callback) => callback(err))
264
- callback(err, data)
265
- }
266
-
267
- // Don't set the user agent if we're not on a browser. The latest spec allows
268
- // the User-Agent header (see https://fetch.spec.whatwg.org/#terminology-headers
269
- // and https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader),
270
- // but browsers such as Chrome and Safari have not caught up.
271
- const headers = {}
272
- if (typeof window === 'undefined') {
273
- headers['user-agent'] = `posthog-node/${version}`
274
- }
275
-
276
- const req = {
277
- method: 'POST',
278
- url: `${this.host}/batch/`,
279
- data,
280
- headers,
281
- }
282
-
283
- if (this.timeout) {
284
- req.timeout = typeof this.timeout === 'string' ? ms(this.timeout) : this.timeout
285
- }
286
-
287
- axios(req)
288
- .then(() => done())
289
- .catch((err) => {
290
- if (err.response) {
291
- const error = new Error(err.response.statusText)
292
- return done(error)
293
- }
294
-
295
- done(err)
296
- })
297
- }
298
-
299
- shutdown() {
300
- if (this.personalApiKey) {
301
- this.featureFlagsPoller.stopPoller()
302
- }
303
- this.flush()
304
- }
305
-
306
- _isErrorRetryable(error) {
307
- // Retry Network Errors.
308
- if (axiosRetry.isNetworkError(error)) {
309
- return true
310
- }
311
-
312
- if (!error.response) {
313
- // Cannot determine if the request can be retried
314
- return false
315
- }
316
-
317
- // Retry Server Errors (5xx).
318
- if (error.response.status >= 500 && error.response.status <= 599) {
319
- return true
320
- }
321
-
322
- // Retry if rate limited.
323
- if (error.response.status === 429) {
324
- return true
325
- }
326
-
327
- return false
328
- }
329
- }
330
-
331
- module.exports = PostHog