posthog-node 5.8.8 → 5.9.1
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/dist/{index.d.ts → client.d.ts} +7 -378
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +480 -0
- package/dist/client.mjs +436 -0
- package/dist/entrypoints/index.edge.d.ts +6 -0
- package/dist/entrypoints/index.edge.d.ts.map +1 -0
- package/dist/entrypoints/index.edge.js +96 -0
- package/dist/entrypoints/index.edge.mjs +19 -0
- package/dist/entrypoints/index.node.d.ts +6 -0
- package/dist/entrypoints/index.node.d.ts.map +1 -0
- package/dist/entrypoints/index.node.js +107 -0
- package/dist/entrypoints/index.node.mjs +24 -0
- package/dist/exports.d.ts +4 -0
- package/dist/exports.d.ts.map +1 -0
- package/dist/exports.js +78 -0
- package/dist/exports.mjs +3 -0
- package/dist/extensions/error-tracking/autocapture.d.ts +4 -0
- package/dist/extensions/error-tracking/autocapture.d.ts.map +1 -0
- package/dist/extensions/error-tracking/autocapture.js +68 -0
- package/dist/extensions/error-tracking/autocapture.mjs +31 -0
- package/dist/extensions/error-tracking/index.d.ts +19 -0
- package/dist/extensions/error-tracking/index.d.ts.map +1 -0
- package/dist/extensions/error-tracking/index.js +97 -0
- package/dist/extensions/error-tracking/index.mjs +63 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.d.ts +5 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.d.ts.map +1 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.js +227 -0
- package/dist/extensions/error-tracking/modifiers/context-lines.node.mjs +187 -0
- package/dist/extensions/error-tracking/modifiers/module.node.d.ts +3 -0
- package/dist/extensions/error-tracking/modifiers/module.node.d.ts.map +1 -0
- package/dist/extensions/error-tracking/modifiers/module.node.js +64 -0
- package/dist/extensions/error-tracking/modifiers/module.node.mjs +30 -0
- package/dist/extensions/express.d.ts +17 -0
- package/dist/extensions/express.d.ts.map +1 -0
- package/dist/extensions/express.js +61 -0
- package/dist/extensions/express.mjs +17 -0
- package/dist/extensions/feature-flags/crypto-helpers.d.ts +3 -0
- package/dist/extensions/feature-flags/crypto-helpers.d.ts.map +1 -0
- package/dist/extensions/feature-flags/crypto-helpers.js +77 -0
- package/dist/extensions/feature-flags/crypto-helpers.mjs +22 -0
- package/dist/extensions/feature-flags/crypto.d.ts +2 -0
- package/dist/extensions/feature-flags/crypto.d.ts.map +1 -0
- package/dist/extensions/feature-flags/crypto.js +47 -0
- package/dist/extensions/feature-flags/crypto.mjs +13 -0
- package/dist/extensions/feature-flags/feature-flags.d.ts +89 -0
- package/dist/extensions/feature-flags/feature-flags.d.ts.map +1 -0
- package/dist/extensions/feature-flags/feature-flags.js +529 -0
- package/dist/extensions/feature-flags/feature-flags.mjs +483 -0
- package/dist/extensions/feature-flags/lazy.d.ts +24 -0
- package/dist/extensions/feature-flags/lazy.d.ts.map +1 -0
- package/dist/extensions/feature-flags/lazy.js +60 -0
- package/dist/extensions/feature-flags/lazy.mjs +26 -0
- package/dist/extensions/sentry-integration.d.ts +54 -0
- package/dist/extensions/sentry-integration.d.ts.map +1 -0
- package/dist/extensions/sentry-integration.js +113 -0
- package/dist/extensions/sentry-integration.mjs +73 -0
- package/dist/storage-memory.d.ts +7 -0
- package/dist/storage-memory.d.ts.map +1 -0
- package/dist/storage-memory.js +46 -0
- package/dist/storage-memory.mjs +12 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.mjs +0 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +63 -0
- package/dist/utils/logger.mjs +29 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +36 -0
- package/dist/version.mjs +2 -0
- package/package.json +32 -31
- package/src/client.ts +1532 -0
- package/src/entrypoints/index.edge.ts +22 -0
- package/src/entrypoints/index.node.ts +26 -0
- package/src/exports.ts +3 -0
- package/src/extensions/error-tracking/autocapture.ts +67 -0
- package/src/extensions/error-tracking/index.ts +104 -0
- package/src/extensions/error-tracking/modifiers/context-lines.node.ts +404 -0
- package/src/extensions/error-tracking/modifiers/module.node.ts +68 -0
- package/src/extensions/express.ts +40 -0
- package/src/extensions/feature-flags/crypto-helpers.ts +36 -0
- package/src/extensions/feature-flags/crypto.ts +22 -0
- package/src/extensions/feature-flags/feature-flags.ts +1003 -0
- package/src/extensions/feature-flags/lazy.ts +55 -0
- package/src/extensions/sentry-integration.ts +216 -0
- package/src/storage-memory.ts +13 -0
- package/src/types.ts +294 -0
- package/src/utils/logger.ts +39 -0
- package/src/version.ts +1 -0
- package/dist/edge/index.cjs +0 -3150
- package/dist/edge/index.cjs.map +0 -1
- package/dist/edge/index.mjs +0 -3144
- package/dist/edge/index.mjs.map +0 -1
- package/dist/node/index.cjs +0 -3556
- package/dist/node/index.cjs.map +0 -1
- package/dist/node/index.mjs +0 -3550
- package/dist/node/index.mjs.map +0 -1
package/dist/edge/index.cjs
DELETED
|
@@ -1,3150 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var core = require('@posthog/core');
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
|
|
7
|
-
*/
|
|
8
|
-
/**
|
|
9
|
-
* Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
|
|
10
|
-
*
|
|
11
|
-
* ### Usage
|
|
12
|
-
*
|
|
13
|
-
* Sentry.init({
|
|
14
|
-
* dsn: 'https://example',
|
|
15
|
-
* integrations: [
|
|
16
|
-
* new PostHogSentryIntegration(posthog)
|
|
17
|
-
* ]
|
|
18
|
-
* })
|
|
19
|
-
*
|
|
20
|
-
* Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
|
|
21
|
-
*
|
|
22
|
-
* @param {Object} [posthog] The posthog object
|
|
23
|
-
* @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
|
|
24
|
-
* @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
|
|
25
|
-
* @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
|
|
26
|
-
* @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
|
|
27
|
-
* @param {boolean} [sendExceptionsToPostHog] Optional: capture exceptions as events in PostHog (default: true)
|
|
28
|
-
*/
|
|
29
|
-
const NAME = 'posthog-node';
|
|
30
|
-
function createEventProcessor(_posthog, {
|
|
31
|
-
organization,
|
|
32
|
-
projectId,
|
|
33
|
-
prefix,
|
|
34
|
-
severityAllowList = ['error'],
|
|
35
|
-
sendExceptionsToPostHog = true
|
|
36
|
-
} = {}) {
|
|
37
|
-
return event => {
|
|
38
|
-
const shouldProcessLevel = severityAllowList === '*' || severityAllowList.includes(event.level);
|
|
39
|
-
if (!shouldProcessLevel) {
|
|
40
|
-
return event;
|
|
41
|
-
}
|
|
42
|
-
if (!event.tags) {
|
|
43
|
-
event.tags = {};
|
|
44
|
-
}
|
|
45
|
-
// Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
|
|
46
|
-
const userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG];
|
|
47
|
-
if (userId === undefined) {
|
|
48
|
-
// If we can't find a user ID, don't bother linking the event. We won't be able to send anything meaningful to PostHog without it.
|
|
49
|
-
return event;
|
|
50
|
-
}
|
|
51
|
-
const uiHost = _posthog.options.host ?? 'https://us.i.posthog.com';
|
|
52
|
-
const personUrl = new URL(`/project/${_posthog.apiKey}/person/${userId}`, uiHost).toString();
|
|
53
|
-
event.tags['PostHog Person URL'] = personUrl;
|
|
54
|
-
const exceptions = event.exception?.values || [];
|
|
55
|
-
const exceptionList = exceptions.map(exception => ({
|
|
56
|
-
...exception,
|
|
57
|
-
stacktrace: exception.stacktrace ? {
|
|
58
|
-
...exception.stacktrace,
|
|
59
|
-
type: 'raw',
|
|
60
|
-
frames: (exception.stacktrace.frames || []).map(frame => {
|
|
61
|
-
return {
|
|
62
|
-
...frame,
|
|
63
|
-
platform: 'node:javascript'
|
|
64
|
-
};
|
|
65
|
-
})
|
|
66
|
-
} : undefined
|
|
67
|
-
}));
|
|
68
|
-
const properties = {
|
|
69
|
-
// PostHog Exception Properties,
|
|
70
|
-
$exception_message: exceptions[0]?.value || event.message,
|
|
71
|
-
$exception_type: exceptions[0]?.type,
|
|
72
|
-
$exception_personURL: personUrl,
|
|
73
|
-
$exception_level: event.level,
|
|
74
|
-
$exception_list: exceptionList,
|
|
75
|
-
// Sentry Exception Properties
|
|
76
|
-
$sentry_event_id: event.event_id,
|
|
77
|
-
$sentry_exception: event.exception,
|
|
78
|
-
$sentry_exception_message: exceptions[0]?.value || event.message,
|
|
79
|
-
$sentry_exception_type: exceptions[0]?.type,
|
|
80
|
-
$sentry_tags: event.tags
|
|
81
|
-
};
|
|
82
|
-
if (organization && projectId) {
|
|
83
|
-
properties['$sentry_url'] = (prefix || 'https://sentry.io/organizations/') + organization + '/issues/?project=' + projectId + '&query=' + event.event_id;
|
|
84
|
-
}
|
|
85
|
-
if (sendExceptionsToPostHog) {
|
|
86
|
-
_posthog.capture({
|
|
87
|
-
event: '$exception',
|
|
88
|
-
distinctId: userId,
|
|
89
|
-
properties
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
return event;
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
// V8 integration - function based
|
|
96
|
-
function sentryIntegration(_posthog, options) {
|
|
97
|
-
const processor = createEventProcessor(_posthog, options);
|
|
98
|
-
return {
|
|
99
|
-
name: NAME,
|
|
100
|
-
processEvent(event) {
|
|
101
|
-
return processor(event);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
// V7 integration - class based
|
|
106
|
-
class PostHogSentryIntegration {
|
|
107
|
-
constructor(_posthog, organization, prefix, severityAllowList, sendExceptionsToPostHog) {
|
|
108
|
-
this.name = NAME;
|
|
109
|
-
// setupOnce gets called by Sentry when it intializes the plugin
|
|
110
|
-
this.name = NAME;
|
|
111
|
-
this.setupOnce = function (addGlobalEventProcessor, getCurrentHub) {
|
|
112
|
-
const projectId = getCurrentHub()?.getClient()?.getDsn()?.projectId;
|
|
113
|
-
addGlobalEventProcessor(createEventProcessor(_posthog, {
|
|
114
|
-
organization,
|
|
115
|
-
projectId,
|
|
116
|
-
prefix,
|
|
117
|
-
severityAllowList,
|
|
118
|
-
sendExceptionsToPostHog: sendExceptionsToPostHog ?? true
|
|
119
|
-
}));
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
PostHogSentryIntegration.POSTHOG_ID_TAG = 'posthog_distinct_id';
|
|
124
|
-
|
|
125
|
-
/*! For license information please see uuidv7.mjs.LICENSE.txt */
|
|
126
|
-
/**
|
|
127
|
-
* uuidv7: An experimental implementation of the proposed UUID Version 7
|
|
128
|
-
*
|
|
129
|
-
* @license Apache-2.0
|
|
130
|
-
* @copyright 2021-2023 LiosK
|
|
131
|
-
* @packageDocumentation
|
|
132
|
-
*/ const DIGITS = "0123456789abcdef";
|
|
133
|
-
class UUID {
|
|
134
|
-
static ofInner(bytes) {
|
|
135
|
-
if (16 === bytes.length) return new UUID(bytes);
|
|
136
|
-
throw new TypeError("not 128-bit length");
|
|
137
|
-
}
|
|
138
|
-
static fromFieldsV7(unixTsMs, randA, randBHi, randBLo) {
|
|
139
|
-
if (!Number.isInteger(unixTsMs) || !Number.isInteger(randA) || !Number.isInteger(randBHi) || !Number.isInteger(randBLo) || unixTsMs < 0 || randA < 0 || randBHi < 0 || randBLo < 0 || unixTsMs > 0xffffffffffff || randA > 0xfff || randBHi > 0x3fffffff || randBLo > 0xffffffff) throw new RangeError("invalid field value");
|
|
140
|
-
const bytes = new Uint8Array(16);
|
|
141
|
-
bytes[0] = unixTsMs / 2 ** 40;
|
|
142
|
-
bytes[1] = unixTsMs / 2 ** 32;
|
|
143
|
-
bytes[2] = unixTsMs / 2 ** 24;
|
|
144
|
-
bytes[3] = unixTsMs / 2 ** 16;
|
|
145
|
-
bytes[4] = unixTsMs / 256;
|
|
146
|
-
bytes[5] = unixTsMs;
|
|
147
|
-
bytes[6] = 0x70 | randA >>> 8;
|
|
148
|
-
bytes[7] = randA;
|
|
149
|
-
bytes[8] = 0x80 | randBHi >>> 24;
|
|
150
|
-
bytes[9] = randBHi >>> 16;
|
|
151
|
-
bytes[10] = randBHi >>> 8;
|
|
152
|
-
bytes[11] = randBHi;
|
|
153
|
-
bytes[12] = randBLo >>> 24;
|
|
154
|
-
bytes[13] = randBLo >>> 16;
|
|
155
|
-
bytes[14] = randBLo >>> 8;
|
|
156
|
-
bytes[15] = randBLo;
|
|
157
|
-
return new UUID(bytes);
|
|
158
|
-
}
|
|
159
|
-
static parse(uuid) {
|
|
160
|
-
let hex;
|
|
161
|
-
switch(uuid.length){
|
|
162
|
-
case 32:
|
|
163
|
-
var _exec;
|
|
164
|
-
hex = null == (_exec = /^[0-9a-f]{32}$/i.exec(uuid)) ? void 0 : _exec[0];
|
|
165
|
-
break;
|
|
166
|
-
case 36:
|
|
167
|
-
var _exec1;
|
|
168
|
-
hex = null == (_exec1 = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(uuid)) ? void 0 : _exec1.slice(1, 6).join("");
|
|
169
|
-
break;
|
|
170
|
-
case 38:
|
|
171
|
-
var _exec2;
|
|
172
|
-
hex = null == (_exec2 = /^\{([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})\}$/i.exec(uuid)) ? void 0 : _exec2.slice(1, 6).join("");
|
|
173
|
-
break;
|
|
174
|
-
case 45:
|
|
175
|
-
var _exec3;
|
|
176
|
-
hex = null == (_exec3 = /^urn:uuid:([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/i.exec(uuid)) ? void 0 : _exec3.slice(1, 6).join("");
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
if (hex) {
|
|
180
|
-
const inner = new Uint8Array(16);
|
|
181
|
-
for(let i = 0; i < 16; i += 4){
|
|
182
|
-
const n = parseInt(hex.substring(2 * i, 2 * i + 8), 16);
|
|
183
|
-
inner[i + 0] = n >>> 24;
|
|
184
|
-
inner[i + 1] = n >>> 16;
|
|
185
|
-
inner[i + 2] = n >>> 8;
|
|
186
|
-
inner[i + 3] = n;
|
|
187
|
-
}
|
|
188
|
-
return new UUID(inner);
|
|
189
|
-
}
|
|
190
|
-
throw new SyntaxError("could not parse UUID string");
|
|
191
|
-
}
|
|
192
|
-
toString() {
|
|
193
|
-
let text = "";
|
|
194
|
-
for(let i = 0; i < this.bytes.length; i++){
|
|
195
|
-
text += DIGITS.charAt(this.bytes[i] >>> 4);
|
|
196
|
-
text += DIGITS.charAt(0xf & this.bytes[i]);
|
|
197
|
-
if (3 === i || 5 === i || 7 === i || 9 === i) text += "-";
|
|
198
|
-
}
|
|
199
|
-
return text;
|
|
200
|
-
}
|
|
201
|
-
toHex() {
|
|
202
|
-
let text = "";
|
|
203
|
-
for(let i = 0; i < this.bytes.length; i++){
|
|
204
|
-
text += DIGITS.charAt(this.bytes[i] >>> 4);
|
|
205
|
-
text += DIGITS.charAt(0xf & this.bytes[i]);
|
|
206
|
-
}
|
|
207
|
-
return text;
|
|
208
|
-
}
|
|
209
|
-
toJSON() {
|
|
210
|
-
return this.toString();
|
|
211
|
-
}
|
|
212
|
-
getVariant() {
|
|
213
|
-
const n = this.bytes[8] >>> 4;
|
|
214
|
-
if (n < 0) throw new Error("unreachable");
|
|
215
|
-
if (n <= 7) return this.bytes.every((e)=>0 === e) ? "NIL" : "VAR_0";
|
|
216
|
-
if (n <= 11) return "VAR_10";
|
|
217
|
-
if (n <= 13) return "VAR_110";
|
|
218
|
-
if (n <= 15) return this.bytes.every((e)=>0xff === e) ? "MAX" : "VAR_RESERVED";
|
|
219
|
-
else throw new Error("unreachable");
|
|
220
|
-
}
|
|
221
|
-
getVersion() {
|
|
222
|
-
return "VAR_10" === this.getVariant() ? this.bytes[6] >>> 4 : void 0;
|
|
223
|
-
}
|
|
224
|
-
clone() {
|
|
225
|
-
return new UUID(this.bytes.slice(0));
|
|
226
|
-
}
|
|
227
|
-
equals(other) {
|
|
228
|
-
return 0 === this.compareTo(other);
|
|
229
|
-
}
|
|
230
|
-
compareTo(other) {
|
|
231
|
-
for(let i = 0; i < 16; i++){
|
|
232
|
-
const diff = this.bytes[i] - other.bytes[i];
|
|
233
|
-
if (0 !== diff) return Math.sign(diff);
|
|
234
|
-
}
|
|
235
|
-
return 0;
|
|
236
|
-
}
|
|
237
|
-
constructor(bytes){
|
|
238
|
-
this.bytes = bytes;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
class V7Generator {
|
|
242
|
-
generate() {
|
|
243
|
-
return this.generateOrResetCore(Date.now(), 10000);
|
|
244
|
-
}
|
|
245
|
-
generateOrAbort() {
|
|
246
|
-
return this.generateOrAbortCore(Date.now(), 10000);
|
|
247
|
-
}
|
|
248
|
-
generateOrResetCore(unixTsMs, rollbackAllowance) {
|
|
249
|
-
let value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
|
|
250
|
-
if (void 0 === value) {
|
|
251
|
-
this.timestamp = 0;
|
|
252
|
-
value = this.generateOrAbortCore(unixTsMs, rollbackAllowance);
|
|
253
|
-
}
|
|
254
|
-
return value;
|
|
255
|
-
}
|
|
256
|
-
generateOrAbortCore(unixTsMs, rollbackAllowance) {
|
|
257
|
-
const MAX_COUNTER = 0x3ffffffffff;
|
|
258
|
-
if (!Number.isInteger(unixTsMs) || unixTsMs < 1 || unixTsMs > 0xffffffffffff) throw new RangeError("`unixTsMs` must be a 48-bit positive integer");
|
|
259
|
-
if (rollbackAllowance < 0 || rollbackAllowance > 0xffffffffffff) throw new RangeError("`rollbackAllowance` out of reasonable range");
|
|
260
|
-
if (unixTsMs > this.timestamp) {
|
|
261
|
-
this.timestamp = unixTsMs;
|
|
262
|
-
this.resetCounter();
|
|
263
|
-
} else {
|
|
264
|
-
if (!(unixTsMs + rollbackAllowance >= this.timestamp)) return;
|
|
265
|
-
this.counter++;
|
|
266
|
-
if (this.counter > MAX_COUNTER) {
|
|
267
|
-
this.timestamp++;
|
|
268
|
-
this.resetCounter();
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return UUID.fromFieldsV7(this.timestamp, Math.trunc(this.counter / 2 ** 30), this.counter & 2 ** 30 - 1, this.random.nextUint32());
|
|
272
|
-
}
|
|
273
|
-
resetCounter() {
|
|
274
|
-
this.counter = 0x400 * this.random.nextUint32() + (0x3ff & this.random.nextUint32());
|
|
275
|
-
}
|
|
276
|
-
generateV4() {
|
|
277
|
-
const bytes = new Uint8Array(Uint32Array.of(this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32(), this.random.nextUint32()).buffer);
|
|
278
|
-
bytes[6] = 0x40 | bytes[6] >>> 4;
|
|
279
|
-
bytes[8] = 0x80 | bytes[8] >>> 2;
|
|
280
|
-
return UUID.ofInner(bytes);
|
|
281
|
-
}
|
|
282
|
-
constructor(randomNumberGenerator){
|
|
283
|
-
this.timestamp = 0;
|
|
284
|
-
this.counter = 0;
|
|
285
|
-
this.random = null != randomNumberGenerator ? randomNumberGenerator : getDefaultRandom();
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const getDefaultRandom = ()=>({
|
|
289
|
-
nextUint32: ()=>0x10000 * Math.trunc(0x10000 * Math.random()) + Math.trunc(0x10000 * Math.random())
|
|
290
|
-
});
|
|
291
|
-
let defaultGenerator;
|
|
292
|
-
const uuidv7 = ()=>uuidv7obj().toString();
|
|
293
|
-
const uuidv7obj = ()=>(defaultGenerator || (defaultGenerator = new V7Generator())).generate();
|
|
294
|
-
|
|
295
|
-
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
296
|
-
// Licensed under the MIT License
|
|
297
|
-
function makeUncaughtExceptionHandler(captureFn, onFatalFn) {
|
|
298
|
-
let calledFatalError = false;
|
|
299
|
-
return Object.assign(error => {
|
|
300
|
-
// Attaching a listener to `uncaughtException` will prevent the node process from exiting. We generally do not
|
|
301
|
-
// want to alter this behaviour so we check for other listeners that users may have attached themselves and adjust
|
|
302
|
-
// exit behaviour of the SDK accordingly:
|
|
303
|
-
// - If other listeners are attached, do not exit.
|
|
304
|
-
// - If the only listener attached is ours, exit.
|
|
305
|
-
const userProvidedListenersCount = global.process.listeners('uncaughtException').filter(listener => {
|
|
306
|
-
// There are 2 listeners we ignore:
|
|
307
|
-
return (
|
|
308
|
-
// as soon as we're using domains this listener is attached by node itself
|
|
309
|
-
listener.name !== 'domainUncaughtExceptionClear' &&
|
|
310
|
-
// the handler we register in this integration
|
|
311
|
-
listener._posthogErrorHandler !== true
|
|
312
|
-
);
|
|
313
|
-
}).length;
|
|
314
|
-
const processWouldExit = userProvidedListenersCount === 0;
|
|
315
|
-
captureFn(error, {
|
|
316
|
-
mechanism: {
|
|
317
|
-
type: 'onuncaughtexception',
|
|
318
|
-
handled: false
|
|
319
|
-
}
|
|
320
|
-
});
|
|
321
|
-
if (!calledFatalError && processWouldExit) {
|
|
322
|
-
calledFatalError = true;
|
|
323
|
-
onFatalFn(error);
|
|
324
|
-
}
|
|
325
|
-
}, {
|
|
326
|
-
_posthogErrorHandler: true
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
function addUncaughtExceptionListener(captureFn, onFatalFn) {
|
|
330
|
-
global.process.on('uncaughtException', makeUncaughtExceptionHandler(captureFn, onFatalFn));
|
|
331
|
-
}
|
|
332
|
-
function addUnhandledRejectionListener(captureFn) {
|
|
333
|
-
global.process.on('unhandledRejection', reason => {
|
|
334
|
-
return captureFn(reason, {
|
|
335
|
-
mechanism: {
|
|
336
|
-
type: 'onunhandledrejection',
|
|
337
|
-
handled: false
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
344
|
-
// Licensed under the MIT License
|
|
345
|
-
let parsedStackResults;
|
|
346
|
-
let lastKeysCount;
|
|
347
|
-
let cachedFilenameChunkIds;
|
|
348
|
-
function getFilenameToChunkIdMap(stackParser) {
|
|
349
|
-
const chunkIdMap = globalThis._posthogChunkIds;
|
|
350
|
-
if (!chunkIdMap) {
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
const chunkIdKeys = Object.keys(chunkIdMap);
|
|
354
|
-
if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
|
|
355
|
-
return cachedFilenameChunkIds;
|
|
356
|
-
}
|
|
357
|
-
lastKeysCount = chunkIdKeys.length;
|
|
358
|
-
cachedFilenameChunkIds = chunkIdKeys.reduce((acc, stackKey) => {
|
|
359
|
-
if (!parsedStackResults) {
|
|
360
|
-
parsedStackResults = {};
|
|
361
|
-
}
|
|
362
|
-
const result = parsedStackResults[stackKey];
|
|
363
|
-
if (result) {
|
|
364
|
-
acc[result[0]] = result[1];
|
|
365
|
-
} else {
|
|
366
|
-
const parsedStack = stackParser(stackKey);
|
|
367
|
-
for (let i = parsedStack.length - 1; i >= 0; i--) {
|
|
368
|
-
const stackFrame = parsedStack[i];
|
|
369
|
-
const filename = stackFrame?.filename;
|
|
370
|
-
const chunkId = chunkIdMap[stackKey];
|
|
371
|
-
if (filename && chunkId) {
|
|
372
|
-
acc[filename] = chunkId;
|
|
373
|
-
parsedStackResults[stackKey] = [filename, chunkId];
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return acc;
|
|
379
|
-
}, {});
|
|
380
|
-
return cachedFilenameChunkIds;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
384
|
-
// Licensed under the MIT License
|
|
385
|
-
function isEvent(candidate) {
|
|
386
|
-
return typeof Event !== 'undefined' && isInstanceOf(candidate, Event);
|
|
387
|
-
}
|
|
388
|
-
function isPlainObject(candidate) {
|
|
389
|
-
return isBuiltin(candidate, 'Object');
|
|
390
|
-
}
|
|
391
|
-
function isError(candidate) {
|
|
392
|
-
switch (Object.prototype.toString.call(candidate)) {
|
|
393
|
-
case '[object Error]':
|
|
394
|
-
case '[object Exception]':
|
|
395
|
-
case '[object DOMException]':
|
|
396
|
-
case '[object WebAssembly.Exception]':
|
|
397
|
-
return true;
|
|
398
|
-
default:
|
|
399
|
-
return isInstanceOf(candidate, Error);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
function isInstanceOf(candidate, base) {
|
|
403
|
-
try {
|
|
404
|
-
return candidate instanceof base;
|
|
405
|
-
} catch {
|
|
406
|
-
return false;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function isErrorEvent(event) {
|
|
410
|
-
return isBuiltin(event, 'ErrorEvent');
|
|
411
|
-
}
|
|
412
|
-
function isBuiltin(candidate, className) {
|
|
413
|
-
return Object.prototype.toString.call(candidate) === `[object ${className}]`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
417
|
-
// Licensed under the MIT License
|
|
418
|
-
async function propertiesFromUnknownInput(stackParser, frameModifiers, input, hint) {
|
|
419
|
-
const providedMechanism = hint && hint.mechanism;
|
|
420
|
-
const mechanism = providedMechanism || {
|
|
421
|
-
handled: true,
|
|
422
|
-
type: 'generic'
|
|
423
|
-
};
|
|
424
|
-
const errorList = getErrorList(mechanism, input, hint);
|
|
425
|
-
const exceptionList = await Promise.all(errorList.map(async error => {
|
|
426
|
-
const exception = await exceptionFromError(stackParser, frameModifiers, error);
|
|
427
|
-
exception.value = exception.value || '';
|
|
428
|
-
exception.type = exception.type || 'Error';
|
|
429
|
-
exception.mechanism = mechanism;
|
|
430
|
-
return exception;
|
|
431
|
-
}));
|
|
432
|
-
const properties = {
|
|
433
|
-
$exception_list: exceptionList
|
|
434
|
-
};
|
|
435
|
-
return properties;
|
|
436
|
-
}
|
|
437
|
-
// Flatten error causes into a list of errors
|
|
438
|
-
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
439
|
-
function getErrorList(mechanism, input, hint) {
|
|
440
|
-
const error = getError(mechanism, input, hint);
|
|
441
|
-
if (error.cause) {
|
|
442
|
-
return [error, ...getErrorList(mechanism, error.cause, hint)];
|
|
443
|
-
}
|
|
444
|
-
return [error];
|
|
445
|
-
}
|
|
446
|
-
function getError(mechanism, exception, hint) {
|
|
447
|
-
if (isError(exception)) {
|
|
448
|
-
return exception;
|
|
449
|
-
}
|
|
450
|
-
mechanism.synthetic = true;
|
|
451
|
-
if (isPlainObject(exception)) {
|
|
452
|
-
const errorFromProp = getErrorPropertyFromObject(exception);
|
|
453
|
-
if (errorFromProp) {
|
|
454
|
-
return errorFromProp;
|
|
455
|
-
}
|
|
456
|
-
const message = getMessageForObject(exception);
|
|
457
|
-
const ex = hint?.syntheticException || new Error(message);
|
|
458
|
-
ex.message = message;
|
|
459
|
-
return ex;
|
|
460
|
-
}
|
|
461
|
-
// This handles when someone does: `throw "something awesome";`
|
|
462
|
-
// We use synthesized Error here so we can extract a (rough) stack trace.
|
|
463
|
-
const ex = hint?.syntheticException || new Error(exception);
|
|
464
|
-
ex.message = `${exception}`;
|
|
465
|
-
return ex;
|
|
466
|
-
}
|
|
467
|
-
/** If a plain object has a property that is an `Error`, return this error. */
|
|
468
|
-
function getErrorPropertyFromObject(obj) {
|
|
469
|
-
for (const prop in obj) {
|
|
470
|
-
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
471
|
-
const value = obj[prop];
|
|
472
|
-
if (isError(value)) {
|
|
473
|
-
return value;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
return undefined;
|
|
478
|
-
}
|
|
479
|
-
function getMessageForObject(exception) {
|
|
480
|
-
if ('name' in exception && typeof exception.name === 'string') {
|
|
481
|
-
let message = `'${exception.name}' captured as exception`;
|
|
482
|
-
if ('message' in exception && typeof exception.message === 'string') {
|
|
483
|
-
message += ` with message '${exception.message}'`;
|
|
484
|
-
}
|
|
485
|
-
return message;
|
|
486
|
-
} else if ('message' in exception && typeof exception.message === 'string') {
|
|
487
|
-
return exception.message;
|
|
488
|
-
}
|
|
489
|
-
const keys = extractExceptionKeysForMessage(exception);
|
|
490
|
-
// Some ErrorEvent instances do not have an `error` property, which is why they are not handled before
|
|
491
|
-
// We still want to try to get a decent message for these cases
|
|
492
|
-
if (isErrorEvent(exception)) {
|
|
493
|
-
return `Event \`ErrorEvent\` captured as exception with message \`${exception.message}\``;
|
|
494
|
-
}
|
|
495
|
-
const className = getObjectClassName(exception);
|
|
496
|
-
return `${className && className !== 'Object' ? `'${className}'` : 'Object'} captured as exception with keys: ${keys}`;
|
|
497
|
-
}
|
|
498
|
-
function getObjectClassName(obj) {
|
|
499
|
-
try {
|
|
500
|
-
const prototype = Object.getPrototypeOf(obj);
|
|
501
|
-
return prototype ? prototype.constructor.name : undefined;
|
|
502
|
-
} catch (e) {
|
|
503
|
-
// ignore errors here
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Given any captured exception, extract its keys and create a sorted
|
|
508
|
-
* and truncated list that will be used inside the event message.
|
|
509
|
-
* eg. `Non-error exception captured with keys: foo, bar, baz`
|
|
510
|
-
*/
|
|
511
|
-
function extractExceptionKeysForMessage(exception, maxLength = 40) {
|
|
512
|
-
const keys = Object.keys(convertToPlainObject(exception));
|
|
513
|
-
keys.sort();
|
|
514
|
-
const firstKey = keys[0];
|
|
515
|
-
if (!firstKey) {
|
|
516
|
-
return '[object has no keys]';
|
|
517
|
-
}
|
|
518
|
-
if (firstKey.length >= maxLength) {
|
|
519
|
-
return truncate(firstKey, maxLength);
|
|
520
|
-
}
|
|
521
|
-
for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) {
|
|
522
|
-
const serialized = keys.slice(0, includedKeys).join(', ');
|
|
523
|
-
if (serialized.length > maxLength) {
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (includedKeys === keys.length) {
|
|
527
|
-
return serialized;
|
|
528
|
-
}
|
|
529
|
-
return truncate(serialized, maxLength);
|
|
530
|
-
}
|
|
531
|
-
return '';
|
|
532
|
-
}
|
|
533
|
-
function truncate(str, max = 0) {
|
|
534
|
-
if (typeof str !== 'string' || max === 0) {
|
|
535
|
-
return str;
|
|
536
|
-
}
|
|
537
|
-
return str.length <= max ? str : `${str.slice(0, max)}...`;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Transforms any `Error` or `Event` into a plain object with all of their enumerable properties, and some of their
|
|
541
|
-
* non-enumerable properties attached.
|
|
542
|
-
*
|
|
543
|
-
* @param value Initial source that we have to transform in order for it to be usable by the serializer
|
|
544
|
-
* @returns An Event or Error turned into an object - or the value argument itself, when value is neither an Event nor
|
|
545
|
-
* an Error.
|
|
546
|
-
*/
|
|
547
|
-
function convertToPlainObject(value) {
|
|
548
|
-
if (isError(value)) {
|
|
549
|
-
return {
|
|
550
|
-
message: value.message,
|
|
551
|
-
name: value.name,
|
|
552
|
-
stack: value.stack,
|
|
553
|
-
...getOwnProperties(value)
|
|
554
|
-
};
|
|
555
|
-
} else if (isEvent(value)) {
|
|
556
|
-
const newObj = {
|
|
557
|
-
type: value.type,
|
|
558
|
-
target: serializeEventTarget(value.target),
|
|
559
|
-
currentTarget: serializeEventTarget(value.currentTarget),
|
|
560
|
-
...getOwnProperties(value)
|
|
561
|
-
};
|
|
562
|
-
// TODO: figure out why this fails typing (I think CustomEvent is only supported in Node 19 onwards)
|
|
563
|
-
// if (typeof CustomEvent !== 'undefined' && isInstanceOf(value, CustomEvent)) {
|
|
564
|
-
// newObj.detail = (value as unknown as CustomEvent).detail
|
|
565
|
-
// }
|
|
566
|
-
return newObj;
|
|
567
|
-
} else {
|
|
568
|
-
return value;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
/** Filters out all but an object's own properties */
|
|
572
|
-
function getOwnProperties(obj) {
|
|
573
|
-
if (typeof obj === 'object' && obj !== null) {
|
|
574
|
-
const extractedProps = {};
|
|
575
|
-
for (const property in obj) {
|
|
576
|
-
if (Object.prototype.hasOwnProperty.call(obj, property)) {
|
|
577
|
-
extractedProps[property] = obj[property];
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return extractedProps;
|
|
581
|
-
} else {
|
|
582
|
-
return {};
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
/** Creates a string representation of the target of an `Event` object */
|
|
586
|
-
function serializeEventTarget(target) {
|
|
587
|
-
try {
|
|
588
|
-
return Object.prototype.toString.call(target);
|
|
589
|
-
} catch (_oO) {
|
|
590
|
-
return '<unknown>';
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Extracts stack frames from the error and builds an Exception
|
|
595
|
-
*/
|
|
596
|
-
async function exceptionFromError(stackParser, frameModifiers, error) {
|
|
597
|
-
const exception = {
|
|
598
|
-
type: error.name || error.constructor.name,
|
|
599
|
-
value: error.message
|
|
600
|
-
};
|
|
601
|
-
let frames = parseStackFrames(stackParser, error);
|
|
602
|
-
for (const modifier of frameModifiers) {
|
|
603
|
-
frames = await modifier(frames);
|
|
604
|
-
}
|
|
605
|
-
if (frames.length) {
|
|
606
|
-
exception.stacktrace = {
|
|
607
|
-
frames,
|
|
608
|
-
type: 'raw'
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
return exception;
|
|
612
|
-
}
|
|
613
|
-
/**
|
|
614
|
-
* Extracts stack frames from the error.stack string
|
|
615
|
-
*/
|
|
616
|
-
function parseStackFrames(stackParser, error) {
|
|
617
|
-
return applyChunkIds(stackParser(error.stack || '', 1), stackParser);
|
|
618
|
-
}
|
|
619
|
-
function applyChunkIds(frames, parser) {
|
|
620
|
-
const filenameChunkIdMap = getFilenameToChunkIdMap(parser);
|
|
621
|
-
frames.forEach(frame => {
|
|
622
|
-
if (frame.filename && filenameChunkIdMap) {
|
|
623
|
-
frame.chunk_id = filenameChunkIdMap[frame.filename];
|
|
624
|
-
}
|
|
625
|
-
});
|
|
626
|
-
return frames;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const ObjProto = Object.prototype;
|
|
630
|
-
const type_utils_toString = ObjProto.toString;
|
|
631
|
-
const isNumber = (x)=>'[object Number]' == type_utils_toString.call(x);
|
|
632
|
-
|
|
633
|
-
function clampToRange(value, min, max, logger, fallbackValue) {
|
|
634
|
-
if (min > max) {
|
|
635
|
-
logger.warn('min cannot be greater than max.');
|
|
636
|
-
min = max;
|
|
637
|
-
}
|
|
638
|
-
if (isNumber(value)) if (value > max) {
|
|
639
|
-
logger.warn(' cannot be greater than max: ' + max + '. Using max value instead.');
|
|
640
|
-
return max;
|
|
641
|
-
} else {
|
|
642
|
-
if (!(value < min)) return value;
|
|
643
|
-
logger.warn(' cannot be less than min: ' + min + '. Using min value instead.');
|
|
644
|
-
return min;
|
|
645
|
-
}
|
|
646
|
-
logger.warn(' must be a number. using max or fallback. max: ' + max + ', fallback: ' + fallbackValue);
|
|
647
|
-
return clampToRange(max, min, max, logger);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
class BucketedRateLimiter {
|
|
651
|
-
stop() {
|
|
652
|
-
if (this._removeInterval) {
|
|
653
|
-
clearInterval(this._removeInterval);
|
|
654
|
-
this._removeInterval = void 0;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
constructor(_options){
|
|
658
|
-
this._options = _options;
|
|
659
|
-
this._buckets = {};
|
|
660
|
-
this._refillBuckets = ()=>{
|
|
661
|
-
Object.keys(this._buckets).forEach((key)=>{
|
|
662
|
-
const newTokens = this._getBucket(key) + this._refillRate;
|
|
663
|
-
if (newTokens >= this._bucketSize) delete this._buckets[key];
|
|
664
|
-
else this._setBucket(key, newTokens);
|
|
665
|
-
});
|
|
666
|
-
};
|
|
667
|
-
this._getBucket = (key)=>this._buckets[String(key)];
|
|
668
|
-
this._setBucket = (key, value)=>{
|
|
669
|
-
this._buckets[String(key)] = value;
|
|
670
|
-
};
|
|
671
|
-
this.consumeRateLimit = (key)=>{
|
|
672
|
-
var _this__getBucket;
|
|
673
|
-
let tokens = null != (_this__getBucket = this._getBucket(key)) ? _this__getBucket : this._bucketSize;
|
|
674
|
-
tokens = Math.max(tokens - 1, 0);
|
|
675
|
-
if (0 === tokens) return true;
|
|
676
|
-
this._setBucket(key, tokens);
|
|
677
|
-
const hasReachedZero = 0 === tokens;
|
|
678
|
-
if (hasReachedZero) {
|
|
679
|
-
var _this__onBucketRateLimited, _this;
|
|
680
|
-
null == (_this__onBucketRateLimited = (_this = this)._onBucketRateLimited) || _this__onBucketRateLimited.call(_this, key);
|
|
681
|
-
}
|
|
682
|
-
return hasReachedZero;
|
|
683
|
-
};
|
|
684
|
-
this._onBucketRateLimited = this._options._onBucketRateLimited;
|
|
685
|
-
this._bucketSize = clampToRange(this._options.bucketSize, 0, 100, this._options._logger);
|
|
686
|
-
this._refillRate = clampToRange(this._options.refillRate, 0, this._bucketSize, this._options._logger);
|
|
687
|
-
this._refillInterval = clampToRange(this._options.refillInterval, 0, 86400000, this._options._logger);
|
|
688
|
-
this._removeInterval = setInterval(()=>{
|
|
689
|
-
this._refillBuckets();
|
|
690
|
-
}, this._refillInterval);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function safeSetTimeout(fn, timeout) {
|
|
695
|
-
const t = setTimeout(fn, timeout);
|
|
696
|
-
(null == t ? void 0 : t.unref) && (null == t || t.unref());
|
|
697
|
-
return t;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
const SHUTDOWN_TIMEOUT = 2000;
|
|
701
|
-
class ErrorTracking {
|
|
702
|
-
constructor(client, options, _logger) {
|
|
703
|
-
this.client = client;
|
|
704
|
-
this._exceptionAutocaptureEnabled = options.enableExceptionAutocapture || false;
|
|
705
|
-
this._logger = _logger;
|
|
706
|
-
// by default captures ten exceptions before rate limiting by exception type
|
|
707
|
-
// refills at a rate of one token / 10 second period
|
|
708
|
-
// e.g. will capture 1 exception rate limited exception every 10 seconds until burst ends
|
|
709
|
-
this._rateLimiter = new BucketedRateLimiter({
|
|
710
|
-
refillRate: 1,
|
|
711
|
-
bucketSize: 10,
|
|
712
|
-
refillInterval: 10000,
|
|
713
|
-
// ten seconds in milliseconds
|
|
714
|
-
_logger: this._logger
|
|
715
|
-
});
|
|
716
|
-
this.startAutocaptureIfEnabled();
|
|
717
|
-
}
|
|
718
|
-
static async buildEventMessage(error, hint, distinctId, additionalProperties) {
|
|
719
|
-
const properties = {
|
|
720
|
-
...additionalProperties
|
|
721
|
-
};
|
|
722
|
-
// Given stateless nature of Node SDK we capture exceptions using personless processing when no
|
|
723
|
-
// user can be determined because a distinct_id is not provided e.g. exception autocapture
|
|
724
|
-
if (!distinctId) {
|
|
725
|
-
properties.$process_person_profile = false;
|
|
726
|
-
}
|
|
727
|
-
const exceptionProperties = await propertiesFromUnknownInput(this.stackParser, this.frameModifiers, error, hint);
|
|
728
|
-
return {
|
|
729
|
-
event: '$exception',
|
|
730
|
-
distinctId: distinctId || uuidv7(),
|
|
731
|
-
properties: {
|
|
732
|
-
...exceptionProperties,
|
|
733
|
-
...properties
|
|
734
|
-
}
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
startAutocaptureIfEnabled() {
|
|
738
|
-
if (this.isEnabled()) {
|
|
739
|
-
addUncaughtExceptionListener(this.onException.bind(this), this.onFatalError.bind(this));
|
|
740
|
-
addUnhandledRejectionListener(this.onException.bind(this));
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
async onException(exception, hint) {
|
|
744
|
-
this.client.addPendingPromise((async () => {
|
|
745
|
-
const eventMessage = await ErrorTracking.buildEventMessage(exception, hint);
|
|
746
|
-
const exceptionProperties = eventMessage.properties;
|
|
747
|
-
const exceptionType = exceptionProperties?.$exception_list[0]?.type ?? 'Exception';
|
|
748
|
-
const isRateLimited = this._rateLimiter.consumeRateLimit(exceptionType);
|
|
749
|
-
if (isRateLimited) {
|
|
750
|
-
this._logger.info('Skipping exception capture because of client rate limiting.', {
|
|
751
|
-
exception: exceptionType
|
|
752
|
-
});
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
return this.client.capture(eventMessage);
|
|
756
|
-
})());
|
|
757
|
-
}
|
|
758
|
-
async onFatalError(exception) {
|
|
759
|
-
console.error(exception);
|
|
760
|
-
await this.client.shutdown(SHUTDOWN_TIMEOUT);
|
|
761
|
-
process.exit(1);
|
|
762
|
-
}
|
|
763
|
-
isEnabled() {
|
|
764
|
-
return !this.client.isDisabled && this._exceptionAutocaptureEnabled;
|
|
765
|
-
}
|
|
766
|
-
shutdown() {
|
|
767
|
-
this._rateLimiter.stop();
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function setupExpressErrorHandler(_posthog, app) {
|
|
772
|
-
app.use((error, _, __, next) => {
|
|
773
|
-
const hint = {
|
|
774
|
-
mechanism: {
|
|
775
|
-
type: 'middleware',
|
|
776
|
-
handled: false
|
|
777
|
-
}
|
|
778
|
-
};
|
|
779
|
-
// Given stateless nature of Node SDK we capture exceptions using personless processing
|
|
780
|
-
// when no user can be determined e.g. in the case of exception autocapture
|
|
781
|
-
ErrorTracking.buildEventMessage(error, hint, uuidv7(), {
|
|
782
|
-
$process_person_profile: false
|
|
783
|
-
}).then(msg => _posthog.capture(msg));
|
|
784
|
-
next(error);
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
var version = "5.8.8";
|
|
789
|
-
|
|
790
|
-
/**
|
|
791
|
-
* A lazy value that is only computed when needed. Inspired by C#'s Lazy<T> class.
|
|
792
|
-
*/
|
|
793
|
-
class Lazy {
|
|
794
|
-
constructor(factory) {
|
|
795
|
-
this.factory = factory;
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* Gets the value, initializing it if necessary.
|
|
799
|
-
* Multiple concurrent calls will share the same initialization promise.
|
|
800
|
-
*/
|
|
801
|
-
async getValue() {
|
|
802
|
-
if (this.value !== undefined) {
|
|
803
|
-
return this.value;
|
|
804
|
-
}
|
|
805
|
-
if (this.initializationPromise === undefined) {
|
|
806
|
-
this.initializationPromise = (async () => {
|
|
807
|
-
try {
|
|
808
|
-
const result = await this.factory();
|
|
809
|
-
this.value = result;
|
|
810
|
-
return result;
|
|
811
|
-
} finally {
|
|
812
|
-
// Clear the promise so we can retry if needed
|
|
813
|
-
this.initializationPromise = undefined;
|
|
814
|
-
}
|
|
815
|
-
})();
|
|
816
|
-
}
|
|
817
|
-
return this.initializationPromise;
|
|
818
|
-
}
|
|
819
|
-
/**
|
|
820
|
-
* Returns true if the value has been initialized.
|
|
821
|
-
*/
|
|
822
|
-
isInitialized() {
|
|
823
|
-
return this.value !== undefined;
|
|
824
|
-
}
|
|
825
|
-
/**
|
|
826
|
-
* Returns a promise that resolves when the value is initialized.
|
|
827
|
-
* If already initialized, resolves immediately.
|
|
828
|
-
*/
|
|
829
|
-
async waitForInitialization() {
|
|
830
|
-
if (this.isInitialized()) {
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
await this.getValue();
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
/// <reference lib="dom" />
|
|
838
|
-
const nodeCrypto = new Lazy(async () => {
|
|
839
|
-
try {
|
|
840
|
-
return await import('crypto');
|
|
841
|
-
} catch {
|
|
842
|
-
return undefined;
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
async function getNodeCrypto() {
|
|
846
|
-
return await nodeCrypto.getValue();
|
|
847
|
-
}
|
|
848
|
-
const webCrypto = new Lazy(async () => {
|
|
849
|
-
if (typeof globalThis.crypto?.subtle !== 'undefined') {
|
|
850
|
-
return globalThis.crypto.subtle;
|
|
851
|
-
}
|
|
852
|
-
try {
|
|
853
|
-
// Node.js: use built-in webcrypto and assign it if needed
|
|
854
|
-
const crypto = await nodeCrypto.getValue();
|
|
855
|
-
if (crypto?.webcrypto?.subtle) {
|
|
856
|
-
return crypto.webcrypto.subtle;
|
|
857
|
-
}
|
|
858
|
-
} catch {
|
|
859
|
-
// Ignore if not available
|
|
860
|
-
}
|
|
861
|
-
return undefined;
|
|
862
|
-
});
|
|
863
|
-
async function getWebCrypto() {
|
|
864
|
-
return await webCrypto.getValue();
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/// <reference lib="dom" />
|
|
868
|
-
async function hashSHA1(text) {
|
|
869
|
-
// Try Node.js crypto first
|
|
870
|
-
const nodeCrypto = await getNodeCrypto();
|
|
871
|
-
if (nodeCrypto) {
|
|
872
|
-
return nodeCrypto.createHash('sha1').update(text).digest('hex');
|
|
873
|
-
}
|
|
874
|
-
const webCrypto = await getWebCrypto();
|
|
875
|
-
// Fall back to Web Crypto API
|
|
876
|
-
if (webCrypto) {
|
|
877
|
-
const hashBuffer = await webCrypto.digest('SHA-1', new TextEncoder().encode(text));
|
|
878
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
879
|
-
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
880
|
-
}
|
|
881
|
-
throw new Error('No crypto implementation available. Tried Node Crypto API and Web SubtleCrypto API');
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
const SIXTY_SECONDS = 60 * 1000;
|
|
885
|
-
// eslint-disable-next-line
|
|
886
|
-
const LONG_SCALE = 0xfffffffffffffff;
|
|
887
|
-
const NULL_VALUES_ALLOWED_OPERATORS = ['is_not'];
|
|
888
|
-
class ClientError extends Error {
|
|
889
|
-
constructor(message) {
|
|
890
|
-
super();
|
|
891
|
-
Error.captureStackTrace(this, this.constructor);
|
|
892
|
-
this.name = 'ClientError';
|
|
893
|
-
this.message = message;
|
|
894
|
-
Object.setPrototypeOf(this, ClientError.prototype);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
class InconclusiveMatchError extends Error {
|
|
898
|
-
constructor(message) {
|
|
899
|
-
super(message);
|
|
900
|
-
this.name = this.constructor.name;
|
|
901
|
-
Error.captureStackTrace(this, this.constructor);
|
|
902
|
-
// instanceof doesn't work in ES3 or ES5
|
|
903
|
-
// https://www.dannyguo.com/blog/how-to-fix-instanceof-not-working-for-custom-errors-in-typescript/
|
|
904
|
-
// this is the workaround
|
|
905
|
-
Object.setPrototypeOf(this, InconclusiveMatchError.prototype);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
class FeatureFlagsPoller {
|
|
909
|
-
constructor({
|
|
910
|
-
pollingInterval,
|
|
911
|
-
personalApiKey,
|
|
912
|
-
projectApiKey,
|
|
913
|
-
timeout,
|
|
914
|
-
host,
|
|
915
|
-
customHeaders,
|
|
916
|
-
...options
|
|
917
|
-
}) {
|
|
918
|
-
this.debugMode = false;
|
|
919
|
-
this.shouldBeginExponentialBackoff = false;
|
|
920
|
-
this.backOffCount = 0;
|
|
921
|
-
this.pollingInterval = pollingInterval;
|
|
922
|
-
this.personalApiKey = personalApiKey;
|
|
923
|
-
this.featureFlags = [];
|
|
924
|
-
this.featureFlagsByKey = {};
|
|
925
|
-
this.groupTypeMapping = {};
|
|
926
|
-
this.cohorts = {};
|
|
927
|
-
this.loadedSuccessfullyOnce = false;
|
|
928
|
-
this.timeout = timeout;
|
|
929
|
-
this.projectApiKey = projectApiKey;
|
|
930
|
-
this.host = host;
|
|
931
|
-
this.poller = undefined;
|
|
932
|
-
this.fetch = options.fetch || fetch;
|
|
933
|
-
this.onError = options.onError;
|
|
934
|
-
this.customHeaders = customHeaders;
|
|
935
|
-
this.onLoad = options.onLoad;
|
|
936
|
-
void this.loadFeatureFlags();
|
|
937
|
-
}
|
|
938
|
-
debug(enabled = true) {
|
|
939
|
-
this.debugMode = enabled;
|
|
940
|
-
}
|
|
941
|
-
logMsgIfDebug(fn) {
|
|
942
|
-
if (this.debugMode) {
|
|
943
|
-
fn();
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
async getFeatureFlag(key, distinctId, groups = {}, personProperties = {}, groupProperties = {}) {
|
|
947
|
-
await this.loadFeatureFlags();
|
|
948
|
-
let response = undefined;
|
|
949
|
-
let featureFlag = undefined;
|
|
950
|
-
if (!this.loadedSuccessfullyOnce) {
|
|
951
|
-
return response;
|
|
952
|
-
}
|
|
953
|
-
featureFlag = this.featureFlagsByKey[key];
|
|
954
|
-
if (featureFlag !== undefined) {
|
|
955
|
-
try {
|
|
956
|
-
const result = await this.computeFlagAndPayloadLocally(featureFlag, distinctId, groups, personProperties, groupProperties);
|
|
957
|
-
response = result.value;
|
|
958
|
-
this.logMsgIfDebug(() => console.debug(`Successfully computed flag locally: ${key} -> ${response}`));
|
|
959
|
-
} catch (e) {
|
|
960
|
-
if (e instanceof InconclusiveMatchError) {
|
|
961
|
-
this.logMsgIfDebug(() => console.debug(`InconclusiveMatchError when computing flag locally: ${key}: ${e}`));
|
|
962
|
-
} else if (e instanceof Error) {
|
|
963
|
-
this.onError?.(new Error(`Error computing flag locally: ${key}: ${e}`));
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
return response;
|
|
968
|
-
}
|
|
969
|
-
async getAllFlagsAndPayloads(distinctId, groups = {}, personProperties = {}, groupProperties = {}, flagKeysToExplicitlyEvaluate) {
|
|
970
|
-
await this.loadFeatureFlags();
|
|
971
|
-
const response = {};
|
|
972
|
-
const payloads = {};
|
|
973
|
-
let fallbackToFlags = this.featureFlags.length == 0;
|
|
974
|
-
const flagsToEvaluate = flagKeysToExplicitlyEvaluate ? flagKeysToExplicitlyEvaluate.map(key => this.featureFlagsByKey[key]).filter(Boolean) : this.featureFlags;
|
|
975
|
-
// Create a shared evaluation cache to prevent memory leaks when processing many flags
|
|
976
|
-
const sharedEvaluationCache = {};
|
|
977
|
-
await Promise.all(flagsToEvaluate.map(async flag => {
|
|
978
|
-
try {
|
|
979
|
-
const {
|
|
980
|
-
value: matchValue,
|
|
981
|
-
payload: matchPayload
|
|
982
|
-
} = await this.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties, undefined /* matchValue */, sharedEvaluationCache);
|
|
983
|
-
response[flag.key] = matchValue;
|
|
984
|
-
if (matchPayload) {
|
|
985
|
-
payloads[flag.key] = matchPayload;
|
|
986
|
-
}
|
|
987
|
-
} catch (e) {
|
|
988
|
-
if (e instanceof InconclusiveMatchError) {
|
|
989
|
-
this.logMsgIfDebug(() => console.debug(`InconclusiveMatchError when computing flag locally: ${flag.key}: ${e}`));
|
|
990
|
-
} else if (e instanceof Error) {
|
|
991
|
-
this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`));
|
|
992
|
-
}
|
|
993
|
-
fallbackToFlags = true;
|
|
994
|
-
}
|
|
995
|
-
}));
|
|
996
|
-
return {
|
|
997
|
-
response,
|
|
998
|
-
payloads,
|
|
999
|
-
fallbackToFlags
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
async computeFlagAndPayloadLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}, matchValue, evaluationCache, skipLoadCheck = false) {
|
|
1003
|
-
// Only load flags if not already loaded and not skipping the check
|
|
1004
|
-
if (!skipLoadCheck) {
|
|
1005
|
-
await this.loadFeatureFlags();
|
|
1006
|
-
}
|
|
1007
|
-
if (!this.loadedSuccessfullyOnce) {
|
|
1008
|
-
return {
|
|
1009
|
-
value: false,
|
|
1010
|
-
payload: null
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
let flagValue;
|
|
1014
|
-
// If matchValue is provided, use it directly; otherwise evaluate the flag
|
|
1015
|
-
if (matchValue !== undefined) {
|
|
1016
|
-
flagValue = matchValue;
|
|
1017
|
-
} else {
|
|
1018
|
-
flagValue = await this.computeFlagValueLocally(flag, distinctId, groups, personProperties, groupProperties, evaluationCache);
|
|
1019
|
-
}
|
|
1020
|
-
// Always compute payload based on the final flagValue (whether provided or computed)
|
|
1021
|
-
const payload = this.getFeatureFlagPayload(flag.key, flagValue);
|
|
1022
|
-
return {
|
|
1023
|
-
value: flagValue,
|
|
1024
|
-
payload
|
|
1025
|
-
};
|
|
1026
|
-
}
|
|
1027
|
-
async computeFlagValueLocally(flag, distinctId, groups = {}, personProperties = {}, groupProperties = {}, evaluationCache = {}) {
|
|
1028
|
-
if (flag.ensure_experience_continuity) {
|
|
1029
|
-
throw new InconclusiveMatchError('Flag has experience continuity enabled');
|
|
1030
|
-
}
|
|
1031
|
-
if (!flag.active) {
|
|
1032
|
-
return false;
|
|
1033
|
-
}
|
|
1034
|
-
const flagFilters = flag.filters || {};
|
|
1035
|
-
const aggregation_group_type_index = flagFilters.aggregation_group_type_index;
|
|
1036
|
-
if (aggregation_group_type_index != undefined) {
|
|
1037
|
-
const groupName = this.groupTypeMapping[String(aggregation_group_type_index)];
|
|
1038
|
-
if (!groupName) {
|
|
1039
|
-
this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Unknown group type index ${aggregation_group_type_index} for feature flag ${flag.key}`));
|
|
1040
|
-
throw new InconclusiveMatchError('Flag has unknown group type index');
|
|
1041
|
-
}
|
|
1042
|
-
if (!(groupName in groups)) {
|
|
1043
|
-
this.logMsgIfDebug(() => console.warn(`[FEATURE FLAGS] Can't compute group feature flag: ${flag.key} without group names passed in`));
|
|
1044
|
-
return false;
|
|
1045
|
-
}
|
|
1046
|
-
const focusedGroupProperties = groupProperties[groupName];
|
|
1047
|
-
return await this.matchFeatureFlagProperties(flag, groups[groupName], focusedGroupProperties, evaluationCache);
|
|
1048
|
-
} else {
|
|
1049
|
-
return await this.matchFeatureFlagProperties(flag, distinctId, personProperties, evaluationCache);
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
getFeatureFlagPayload(key, flagValue) {
|
|
1053
|
-
let payload = null;
|
|
1054
|
-
if (flagValue !== false && flagValue !== null && flagValue !== undefined) {
|
|
1055
|
-
if (typeof flagValue == 'boolean') {
|
|
1056
|
-
payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue.toString()] || null;
|
|
1057
|
-
} else if (typeof flagValue == 'string') {
|
|
1058
|
-
payload = this.featureFlagsByKey?.[key]?.filters?.payloads?.[flagValue] || null;
|
|
1059
|
-
}
|
|
1060
|
-
if (payload !== null && payload !== undefined) {
|
|
1061
|
-
// If payload is already an object, return it directly
|
|
1062
|
-
if (typeof payload === 'object') {
|
|
1063
|
-
return payload;
|
|
1064
|
-
}
|
|
1065
|
-
// If payload is a string, try to parse it as JSON
|
|
1066
|
-
if (typeof payload === 'string') {
|
|
1067
|
-
try {
|
|
1068
|
-
return JSON.parse(payload);
|
|
1069
|
-
} catch {
|
|
1070
|
-
// If parsing fails, return the string as is
|
|
1071
|
-
return payload;
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
// For other types, return as is
|
|
1075
|
-
return payload;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1080
|
-
async evaluateFlagDependency(property, distinctId, properties, evaluationCache) {
|
|
1081
|
-
const targetFlagKey = property.key;
|
|
1082
|
-
if (!this.featureFlagsByKey) {
|
|
1083
|
-
throw new InconclusiveMatchError('Feature flags not available for dependency evaluation');
|
|
1084
|
-
}
|
|
1085
|
-
// Check if dependency_chain is present - it should always be provided for flag dependencies
|
|
1086
|
-
if (!('dependency_chain' in property)) {
|
|
1087
|
-
throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' is missing required 'dependency_chain' field`);
|
|
1088
|
-
}
|
|
1089
|
-
const dependencyChain = property.dependency_chain;
|
|
1090
|
-
// Check for missing or invalid dependency chain (This should never happen, but being defensive)
|
|
1091
|
-
if (!Array.isArray(dependencyChain)) {
|
|
1092
|
-
throw new InconclusiveMatchError(`Flag dependency property for '${targetFlagKey}' has an invalid 'dependency_chain' (expected array, got ${typeof dependencyChain})`);
|
|
1093
|
-
}
|
|
1094
|
-
// Handle circular dependency (empty chain means circular) (This should never happen, but being defensive)
|
|
1095
|
-
if (dependencyChain.length === 0) {
|
|
1096
|
-
throw new InconclusiveMatchError(`Circular dependency detected for flag '${targetFlagKey}' (empty dependency chain)`);
|
|
1097
|
-
}
|
|
1098
|
-
// Evaluate all dependencies in the chain order
|
|
1099
|
-
for (const depFlagKey of dependencyChain) {
|
|
1100
|
-
if (!(depFlagKey in evaluationCache)) {
|
|
1101
|
-
// Need to evaluate this dependency first
|
|
1102
|
-
const depFlag = this.featureFlagsByKey[depFlagKey];
|
|
1103
|
-
if (!depFlag) {
|
|
1104
|
-
// Missing flag dependency - cannot evaluate locally
|
|
1105
|
-
throw new InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`);
|
|
1106
|
-
} else if (!depFlag.active) {
|
|
1107
|
-
// Inactive flag evaluates to false
|
|
1108
|
-
evaluationCache[depFlagKey] = false;
|
|
1109
|
-
} else {
|
|
1110
|
-
// Recursively evaluate the dependency
|
|
1111
|
-
try {
|
|
1112
|
-
const depResult = await this.matchFeatureFlagProperties(depFlag, distinctId, properties, evaluationCache);
|
|
1113
|
-
evaluationCache[depFlagKey] = depResult;
|
|
1114
|
-
} catch (error) {
|
|
1115
|
-
// If we can't evaluate a dependency, store throw InconclusiveMatchError(`Missing flag dependency '${depFlagKey}' for flag '${targetFlagKey}'`)
|
|
1116
|
-
throw new InconclusiveMatchError(`Error evaluating flag dependency '${depFlagKey}' for flag '${targetFlagKey}': ${error}`);
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
// Check if dependency evaluation was inconclusive
|
|
1121
|
-
const cachedResult = evaluationCache[depFlagKey];
|
|
1122
|
-
if (cachedResult === null || cachedResult === undefined) {
|
|
1123
|
-
throw new InconclusiveMatchError(`Dependency '${depFlagKey}' could not be evaluated`);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
// The target flag is specified in property.key (This should match the last element in the dependency chain)
|
|
1127
|
-
const targetFlagValue = evaluationCache[targetFlagKey];
|
|
1128
|
-
return this.flagEvaluatesToExpectedValue(property.value, targetFlagValue);
|
|
1129
|
-
}
|
|
1130
|
-
flagEvaluatesToExpectedValue(expectedValue, flagValue) {
|
|
1131
|
-
// If the expected value is a boolean, then return true if the flag evaluated to true (or any string variant)
|
|
1132
|
-
// If the expected value is false, then only return true if the flag evaluated to false.
|
|
1133
|
-
if (typeof expectedValue === 'boolean') {
|
|
1134
|
-
return expectedValue === flagValue || typeof flagValue === 'string' && flagValue !== '' && expectedValue === true;
|
|
1135
|
-
}
|
|
1136
|
-
// If the expected value is a string, then return true if and only if the flag evaluated to the expected value.
|
|
1137
|
-
if (typeof expectedValue === 'string') {
|
|
1138
|
-
return flagValue === expectedValue;
|
|
1139
|
-
}
|
|
1140
|
-
// The `flag_evaluates_to` operator is not supported for numbers and arrays.
|
|
1141
|
-
return false;
|
|
1142
|
-
}
|
|
1143
|
-
async matchFeatureFlagProperties(flag, distinctId, properties, evaluationCache = {}) {
|
|
1144
|
-
const flagFilters = flag.filters || {};
|
|
1145
|
-
const flagConditions = flagFilters.groups || [];
|
|
1146
|
-
let isInconclusive = false;
|
|
1147
|
-
let result = undefined;
|
|
1148
|
-
for (const condition of flagConditions) {
|
|
1149
|
-
try {
|
|
1150
|
-
if (await this.isConditionMatch(flag, distinctId, condition, properties, evaluationCache)) {
|
|
1151
|
-
const variantOverride = condition.variant;
|
|
1152
|
-
const flagVariants = flagFilters.multivariate?.variants || [];
|
|
1153
|
-
if (variantOverride && flagVariants.some(variant => variant.key === variantOverride)) {
|
|
1154
|
-
result = variantOverride;
|
|
1155
|
-
} else {
|
|
1156
|
-
result = (await this.getMatchingVariant(flag, distinctId)) || true;
|
|
1157
|
-
}
|
|
1158
|
-
break;
|
|
1159
|
-
}
|
|
1160
|
-
} catch (e) {
|
|
1161
|
-
if (e instanceof InconclusiveMatchError) {
|
|
1162
|
-
isInconclusive = true;
|
|
1163
|
-
} else {
|
|
1164
|
-
throw e;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if (result !== undefined) {
|
|
1169
|
-
return result;
|
|
1170
|
-
} else if (isInconclusive) {
|
|
1171
|
-
throw new InconclusiveMatchError("Can't determine if feature flag is enabled or not with given properties");
|
|
1172
|
-
}
|
|
1173
|
-
// We can only return False when all conditions are False
|
|
1174
|
-
return false;
|
|
1175
|
-
}
|
|
1176
|
-
async isConditionMatch(flag, distinctId, condition, properties, evaluationCache = {}) {
|
|
1177
|
-
const rolloutPercentage = condition.rollout_percentage;
|
|
1178
|
-
const warnFunction = msg => {
|
|
1179
|
-
this.logMsgIfDebug(() => console.warn(msg));
|
|
1180
|
-
};
|
|
1181
|
-
if ((condition.properties || []).length > 0) {
|
|
1182
|
-
for (const prop of condition.properties) {
|
|
1183
|
-
const propertyType = prop.type;
|
|
1184
|
-
let matches = false;
|
|
1185
|
-
if (propertyType === 'cohort') {
|
|
1186
|
-
matches = matchCohort(prop, properties, this.cohorts, this.debugMode);
|
|
1187
|
-
} else if (propertyType === 'flag') {
|
|
1188
|
-
matches = await this.evaluateFlagDependency(prop, distinctId, properties, evaluationCache);
|
|
1189
|
-
} else {
|
|
1190
|
-
matches = matchProperty(prop, properties, warnFunction);
|
|
1191
|
-
}
|
|
1192
|
-
if (!matches) {
|
|
1193
|
-
return false;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
if (rolloutPercentage == undefined) {
|
|
1197
|
-
return true;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
if (rolloutPercentage != undefined && (await _hash(flag.key, distinctId)) > rolloutPercentage / 100.0) {
|
|
1201
|
-
return false;
|
|
1202
|
-
}
|
|
1203
|
-
return true;
|
|
1204
|
-
}
|
|
1205
|
-
async getMatchingVariant(flag, distinctId) {
|
|
1206
|
-
const hashValue = await _hash(flag.key, distinctId, 'variant');
|
|
1207
|
-
const matchingVariant = this.variantLookupTable(flag).find(variant => {
|
|
1208
|
-
return hashValue >= variant.valueMin && hashValue < variant.valueMax;
|
|
1209
|
-
});
|
|
1210
|
-
if (matchingVariant) {
|
|
1211
|
-
return matchingVariant.key;
|
|
1212
|
-
}
|
|
1213
|
-
return undefined;
|
|
1214
|
-
}
|
|
1215
|
-
variantLookupTable(flag) {
|
|
1216
|
-
const lookupTable = [];
|
|
1217
|
-
let valueMin = 0;
|
|
1218
|
-
let valueMax = 0;
|
|
1219
|
-
const flagFilters = flag.filters || {};
|
|
1220
|
-
const multivariates = flagFilters.multivariate?.variants || [];
|
|
1221
|
-
multivariates.forEach(variant => {
|
|
1222
|
-
valueMax = valueMin + variant.rollout_percentage / 100.0;
|
|
1223
|
-
lookupTable.push({
|
|
1224
|
-
valueMin,
|
|
1225
|
-
valueMax,
|
|
1226
|
-
key: variant.key
|
|
1227
|
-
});
|
|
1228
|
-
valueMin = valueMax;
|
|
1229
|
-
});
|
|
1230
|
-
return lookupTable;
|
|
1231
|
-
}
|
|
1232
|
-
async loadFeatureFlags(forceReload = false) {
|
|
1233
|
-
if (!this.loadedSuccessfullyOnce || forceReload) {
|
|
1234
|
-
await this._loadFeatureFlags();
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
/**
|
|
1238
|
-
* Returns true if the feature flags poller has loaded successfully at least once and has more than 0 feature flags.
|
|
1239
|
-
* This is useful to check if local evaluation is ready before calling getFeatureFlag.
|
|
1240
|
-
*/
|
|
1241
|
-
isLocalEvaluationReady() {
|
|
1242
|
-
return (this.loadedSuccessfullyOnce ?? false) && (this.featureFlags?.length ?? 0) > 0;
|
|
1243
|
-
}
|
|
1244
|
-
/**
|
|
1245
|
-
* If a client is misconfigured with an invalid or improper API key, the polling interval is doubled each time
|
|
1246
|
-
* until a successful request is made, up to a maximum of 60 seconds.
|
|
1247
|
-
*
|
|
1248
|
-
* @returns The polling interval to use for the next request.
|
|
1249
|
-
*/
|
|
1250
|
-
getPollingInterval() {
|
|
1251
|
-
if (!this.shouldBeginExponentialBackoff) {
|
|
1252
|
-
return this.pollingInterval;
|
|
1253
|
-
}
|
|
1254
|
-
return Math.min(SIXTY_SECONDS, this.pollingInterval * 2 ** this.backOffCount);
|
|
1255
|
-
}
|
|
1256
|
-
async _loadFeatureFlags() {
|
|
1257
|
-
if (this.poller) {
|
|
1258
|
-
clearTimeout(this.poller);
|
|
1259
|
-
this.poller = undefined;
|
|
1260
|
-
}
|
|
1261
|
-
this.poller = setTimeout(() => this._loadFeatureFlags(), this.getPollingInterval());
|
|
1262
|
-
try {
|
|
1263
|
-
const res = await this._requestFeatureFlagDefinitions();
|
|
1264
|
-
// Handle undefined res case, this shouldn't happen, but it doesn't hurt to handle it anyway
|
|
1265
|
-
if (!res) {
|
|
1266
|
-
// Don't override existing flags when something goes wrong
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
// NB ON ERROR HANDLING & `loadedSuccessfullyOnce`:
|
|
1270
|
-
//
|
|
1271
|
-
// `loadedSuccessfullyOnce` indicates we've successfully loaded a valid set of flags at least once.
|
|
1272
|
-
// If we set it to `true` in an error scenario (e.g. 402 Over Quota, 401 Invalid Key, etc.),
|
|
1273
|
-
// any manual call to `loadFeatureFlags()` (without forceReload) will skip refetching entirely,
|
|
1274
|
-
// leaving us stuck with zero or outdated flags. The poller does keep running, but we also want
|
|
1275
|
-
// manual reloads to be possible as soon as the error condition is resolved.
|
|
1276
|
-
//
|
|
1277
|
-
// Therefore, on error statuses, we do *not* set `loadedSuccessfullyOnce = true`, ensuring that
|
|
1278
|
-
// both the background poller and any subsequent manual calls can keep trying to load flags
|
|
1279
|
-
// once the issue (quota, permission, rate limit, etc.) is resolved.
|
|
1280
|
-
switch (res.status) {
|
|
1281
|
-
case 401:
|
|
1282
|
-
// Invalid API key
|
|
1283
|
-
this.shouldBeginExponentialBackoff = true;
|
|
1284
|
-
this.backOffCount += 1;
|
|
1285
|
-
throw new ClientError(`Your project key or personal API key is invalid. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
|
|
1286
|
-
case 402:
|
|
1287
|
-
// Quota exceeded - clear all flags
|
|
1288
|
-
console.warn('[FEATURE FLAGS] Feature flags quota limit exceeded - unsetting all local flags. Learn more about billing limits at https://posthog.com/docs/billing/limits-alerts');
|
|
1289
|
-
this.featureFlags = [];
|
|
1290
|
-
this.featureFlagsByKey = {};
|
|
1291
|
-
this.groupTypeMapping = {};
|
|
1292
|
-
this.cohorts = {};
|
|
1293
|
-
return;
|
|
1294
|
-
case 403:
|
|
1295
|
-
// Permissions issue
|
|
1296
|
-
this.shouldBeginExponentialBackoff = true;
|
|
1297
|
-
this.backOffCount += 1;
|
|
1298
|
-
throw new ClientError(`Your personal API key does not have permission to fetch feature flag definitions for local evaluation. Setting next polling interval to ${this.getPollingInterval()}ms. Are you sure you're using the correct personal and Project API key pair? More information: https://posthog.com/docs/api/overview`);
|
|
1299
|
-
case 429:
|
|
1300
|
-
// Rate limited
|
|
1301
|
-
this.shouldBeginExponentialBackoff = true;
|
|
1302
|
-
this.backOffCount += 1;
|
|
1303
|
-
throw new ClientError(`You are being rate limited. Setting next polling interval to ${this.getPollingInterval()}ms. More information: https://posthog.com/docs/api#rate-limiting`);
|
|
1304
|
-
case 200:
|
|
1305
|
-
{
|
|
1306
|
-
// Process successful response
|
|
1307
|
-
const responseJson = (await res.json()) ?? {};
|
|
1308
|
-
if (!('flags' in responseJson)) {
|
|
1309
|
-
this.onError?.(new Error(`Invalid response when getting feature flags: ${JSON.stringify(responseJson)}`));
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
this.featureFlags = responseJson.flags ?? [];
|
|
1313
|
-
this.featureFlagsByKey = this.featureFlags.reduce((acc, curr) => (acc[curr.key] = curr, acc), {});
|
|
1314
|
-
this.groupTypeMapping = responseJson.group_type_mapping || {};
|
|
1315
|
-
this.cohorts = responseJson.cohorts || {};
|
|
1316
|
-
this.loadedSuccessfullyOnce = true;
|
|
1317
|
-
this.shouldBeginExponentialBackoff = false;
|
|
1318
|
-
this.backOffCount = 0;
|
|
1319
|
-
this.onLoad?.(this.featureFlags.length);
|
|
1320
|
-
break;
|
|
1321
|
-
}
|
|
1322
|
-
default:
|
|
1323
|
-
// Something else went wrong, or the server is down.
|
|
1324
|
-
// In this case, don't override existing flags
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
} catch (err) {
|
|
1328
|
-
if (err instanceof ClientError) {
|
|
1329
|
-
this.onError?.(err);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
getPersonalApiKeyRequestOptions(method = 'GET') {
|
|
1334
|
-
return {
|
|
1335
|
-
method,
|
|
1336
|
-
headers: {
|
|
1337
|
-
...this.customHeaders,
|
|
1338
|
-
'Content-Type': 'application/json',
|
|
1339
|
-
Authorization: `Bearer ${this.personalApiKey}`
|
|
1340
|
-
}
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1343
|
-
async _requestFeatureFlagDefinitions() {
|
|
1344
|
-
const url = `${this.host}/api/feature_flag/local_evaluation?token=${this.projectApiKey}&send_cohorts`;
|
|
1345
|
-
const options = this.getPersonalApiKeyRequestOptions();
|
|
1346
|
-
let abortTimeout = null;
|
|
1347
|
-
if (this.timeout && typeof this.timeout === 'number') {
|
|
1348
|
-
const controller = new AbortController();
|
|
1349
|
-
abortTimeout = safeSetTimeout(() => {
|
|
1350
|
-
controller.abort();
|
|
1351
|
-
}, this.timeout);
|
|
1352
|
-
options.signal = controller.signal;
|
|
1353
|
-
}
|
|
1354
|
-
try {
|
|
1355
|
-
return await this.fetch(url, options);
|
|
1356
|
-
} finally {
|
|
1357
|
-
clearTimeout(abortTimeout);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
stopPoller() {
|
|
1361
|
-
clearTimeout(this.poller);
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
// # This function takes a distinct_id and a feature flag key and returns a float between 0 and 1.
|
|
1365
|
-
// # Given the same distinct_id and key, it'll always return the same float. These floats are
|
|
1366
|
-
// # uniformly distributed between 0 and 1, so if we want to show this feature to 20% of traffic
|
|
1367
|
-
// # we can do _hash(key, distinct_id) < 0.2
|
|
1368
|
-
async function _hash(key, distinctId, salt = '') {
|
|
1369
|
-
const hashString = await hashSHA1(`${key}.${distinctId}${salt}`);
|
|
1370
|
-
return parseInt(hashString.slice(0, 15), 16) / LONG_SCALE;
|
|
1371
|
-
}
|
|
1372
|
-
function matchProperty(property, propertyValues, warnFunction) {
|
|
1373
|
-
const key = property.key;
|
|
1374
|
-
const value = property.value;
|
|
1375
|
-
const operator = property.operator || 'exact';
|
|
1376
|
-
if (!(key in propertyValues)) {
|
|
1377
|
-
throw new InconclusiveMatchError(`Property ${key} not found in propertyValues`);
|
|
1378
|
-
} else if (operator === 'is_not_set') {
|
|
1379
|
-
throw new InconclusiveMatchError(`Operator is_not_set is not supported`);
|
|
1380
|
-
}
|
|
1381
|
-
const overrideValue = propertyValues[key];
|
|
1382
|
-
if (overrideValue == null && !NULL_VALUES_ALLOWED_OPERATORS.includes(operator)) {
|
|
1383
|
-
// if the value is null, just fail the feature flag comparison
|
|
1384
|
-
// this isn't an InconclusiveMatchError because the property value was provided.
|
|
1385
|
-
if (warnFunction) {
|
|
1386
|
-
warnFunction(`Property ${key} cannot have a value of null/undefined with the ${operator} operator`);
|
|
1387
|
-
}
|
|
1388
|
-
return false;
|
|
1389
|
-
}
|
|
1390
|
-
function computeExactMatch(value, overrideValue) {
|
|
1391
|
-
if (Array.isArray(value)) {
|
|
1392
|
-
return value.map(val => String(val).toLowerCase()).includes(String(overrideValue).toLowerCase());
|
|
1393
|
-
}
|
|
1394
|
-
return String(value).toLowerCase() === String(overrideValue).toLowerCase();
|
|
1395
|
-
}
|
|
1396
|
-
function compare(lhs, rhs, operator) {
|
|
1397
|
-
if (operator === 'gt') {
|
|
1398
|
-
return lhs > rhs;
|
|
1399
|
-
} else if (operator === 'gte') {
|
|
1400
|
-
return lhs >= rhs;
|
|
1401
|
-
} else if (operator === 'lt') {
|
|
1402
|
-
return lhs < rhs;
|
|
1403
|
-
} else if (operator === 'lte') {
|
|
1404
|
-
return lhs <= rhs;
|
|
1405
|
-
} else {
|
|
1406
|
-
throw new Error(`Invalid operator: ${operator}`);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
switch (operator) {
|
|
1410
|
-
case 'exact':
|
|
1411
|
-
return computeExactMatch(value, overrideValue);
|
|
1412
|
-
case 'is_not':
|
|
1413
|
-
return !computeExactMatch(value, overrideValue);
|
|
1414
|
-
case 'is_set':
|
|
1415
|
-
return key in propertyValues;
|
|
1416
|
-
case 'icontains':
|
|
1417
|
-
return String(overrideValue).toLowerCase().includes(String(value).toLowerCase());
|
|
1418
|
-
case 'not_icontains':
|
|
1419
|
-
return !String(overrideValue).toLowerCase().includes(String(value).toLowerCase());
|
|
1420
|
-
case 'regex':
|
|
1421
|
-
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) !== null;
|
|
1422
|
-
case 'not_regex':
|
|
1423
|
-
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null;
|
|
1424
|
-
case 'gt':
|
|
1425
|
-
case 'gte':
|
|
1426
|
-
case 'lt':
|
|
1427
|
-
case 'lte':
|
|
1428
|
-
{
|
|
1429
|
-
// :TRICKY: We adjust comparison based on the override value passed in,
|
|
1430
|
-
// to make sure we handle both numeric and string comparisons appropriately.
|
|
1431
|
-
let parsedValue = typeof value === 'number' ? value : null;
|
|
1432
|
-
if (typeof value === 'string') {
|
|
1433
|
-
try {
|
|
1434
|
-
parsedValue = parseFloat(value);
|
|
1435
|
-
} catch (err) {
|
|
1436
|
-
// pass
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
if (parsedValue != null && overrideValue != null) {
|
|
1440
|
-
// check both null and undefined
|
|
1441
|
-
if (typeof overrideValue === 'string') {
|
|
1442
|
-
return compare(overrideValue, String(value), operator);
|
|
1443
|
-
} else {
|
|
1444
|
-
return compare(overrideValue, parsedValue, operator);
|
|
1445
|
-
}
|
|
1446
|
-
} else {
|
|
1447
|
-
return compare(String(overrideValue), String(value), operator);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
case 'is_date_after':
|
|
1451
|
-
case 'is_date_before':
|
|
1452
|
-
{
|
|
1453
|
-
// Boolean values should never be used with date operations
|
|
1454
|
-
if (typeof value === 'boolean') {
|
|
1455
|
-
throw new InconclusiveMatchError(`Date operations cannot be performed on boolean values`);
|
|
1456
|
-
}
|
|
1457
|
-
let parsedDate = relativeDateParseForFeatureFlagMatching(String(value));
|
|
1458
|
-
if (parsedDate == null) {
|
|
1459
|
-
parsedDate = convertToDateTime(value);
|
|
1460
|
-
}
|
|
1461
|
-
if (parsedDate == null) {
|
|
1462
|
-
throw new InconclusiveMatchError(`Invalid date: ${value}`);
|
|
1463
|
-
}
|
|
1464
|
-
const overrideDate = convertToDateTime(overrideValue);
|
|
1465
|
-
if (['is_date_before'].includes(operator)) {
|
|
1466
|
-
return overrideDate < parsedDate;
|
|
1467
|
-
}
|
|
1468
|
-
return overrideDate > parsedDate;
|
|
1469
|
-
}
|
|
1470
|
-
default:
|
|
1471
|
-
throw new InconclusiveMatchError(`Unknown operator: ${operator}`);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
function matchCohort(property, propertyValues, cohortProperties, debugMode = false) {
|
|
1475
|
-
const cohortId = String(property.value);
|
|
1476
|
-
if (!(cohortId in cohortProperties)) {
|
|
1477
|
-
throw new InconclusiveMatchError("can't match cohort without a given cohort property value");
|
|
1478
|
-
}
|
|
1479
|
-
const propertyGroup = cohortProperties[cohortId];
|
|
1480
|
-
return matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode);
|
|
1481
|
-
}
|
|
1482
|
-
function matchPropertyGroup(propertyGroup, propertyValues, cohortProperties, debugMode = false) {
|
|
1483
|
-
if (!propertyGroup) {
|
|
1484
|
-
return true;
|
|
1485
|
-
}
|
|
1486
|
-
const propertyGroupType = propertyGroup.type;
|
|
1487
|
-
const properties = propertyGroup.values;
|
|
1488
|
-
if (!properties || properties.length === 0) {
|
|
1489
|
-
// empty groups are no-ops, always match
|
|
1490
|
-
return true;
|
|
1491
|
-
}
|
|
1492
|
-
let errorMatchingLocally = false;
|
|
1493
|
-
if ('values' in properties[0]) {
|
|
1494
|
-
// a nested property group
|
|
1495
|
-
for (const prop of properties) {
|
|
1496
|
-
try {
|
|
1497
|
-
const matches = matchPropertyGroup(prop, propertyValues, cohortProperties, debugMode);
|
|
1498
|
-
if (propertyGroupType === 'AND') {
|
|
1499
|
-
if (!matches) {
|
|
1500
|
-
return false;
|
|
1501
|
-
}
|
|
1502
|
-
} else {
|
|
1503
|
-
// OR group
|
|
1504
|
-
if (matches) {
|
|
1505
|
-
return true;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
} catch (err) {
|
|
1509
|
-
if (err instanceof InconclusiveMatchError) {
|
|
1510
|
-
if (debugMode) {
|
|
1511
|
-
console.debug(`Failed to compute property ${prop} locally: ${err}`);
|
|
1512
|
-
}
|
|
1513
|
-
errorMatchingLocally = true;
|
|
1514
|
-
} else {
|
|
1515
|
-
throw err;
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
if (errorMatchingLocally) {
|
|
1520
|
-
throw new InconclusiveMatchError("Can't match cohort without a given cohort property value");
|
|
1521
|
-
}
|
|
1522
|
-
// if we get here, all matched in AND case, or none matched in OR case
|
|
1523
|
-
return propertyGroupType === 'AND';
|
|
1524
|
-
} else {
|
|
1525
|
-
for (const prop of properties) {
|
|
1526
|
-
try {
|
|
1527
|
-
let matches;
|
|
1528
|
-
if (prop.type === 'cohort') {
|
|
1529
|
-
matches = matchCohort(prop, propertyValues, cohortProperties, debugMode);
|
|
1530
|
-
} else if (prop.type === 'flag') {
|
|
1531
|
-
if (debugMode) {
|
|
1532
|
-
console.warn(`[FEATURE FLAGS] Flag dependency filters are not supported in local evaluation. ` + `Skipping condition with dependency on flag '${prop.key || 'unknown'}'`);
|
|
1533
|
-
}
|
|
1534
|
-
continue;
|
|
1535
|
-
} else {
|
|
1536
|
-
matches = matchProperty(prop, propertyValues);
|
|
1537
|
-
}
|
|
1538
|
-
const negation = prop.negation || false;
|
|
1539
|
-
if (propertyGroupType === 'AND') {
|
|
1540
|
-
// if negated property, do the inverse
|
|
1541
|
-
if (!matches && !negation) {
|
|
1542
|
-
return false;
|
|
1543
|
-
}
|
|
1544
|
-
if (matches && negation) {
|
|
1545
|
-
return false;
|
|
1546
|
-
}
|
|
1547
|
-
} else {
|
|
1548
|
-
// OR group
|
|
1549
|
-
if (matches && !negation) {
|
|
1550
|
-
return true;
|
|
1551
|
-
}
|
|
1552
|
-
if (!matches && negation) {
|
|
1553
|
-
return true;
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
} catch (err) {
|
|
1557
|
-
if (err instanceof InconclusiveMatchError) {
|
|
1558
|
-
if (debugMode) {
|
|
1559
|
-
console.debug(`Failed to compute property ${prop} locally: ${err}`);
|
|
1560
|
-
}
|
|
1561
|
-
errorMatchingLocally = true;
|
|
1562
|
-
} else {
|
|
1563
|
-
throw err;
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
if (errorMatchingLocally) {
|
|
1568
|
-
throw new InconclusiveMatchError("can't match cohort without a given cohort property value");
|
|
1569
|
-
}
|
|
1570
|
-
// if we get here, all matched in AND case, or none matched in OR case
|
|
1571
|
-
return propertyGroupType === 'AND';
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
function isValidRegex(regex) {
|
|
1575
|
-
try {
|
|
1576
|
-
new RegExp(regex);
|
|
1577
|
-
return true;
|
|
1578
|
-
} catch (err) {
|
|
1579
|
-
return false;
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
function convertToDateTime(value) {
|
|
1583
|
-
if (value instanceof Date) {
|
|
1584
|
-
return value;
|
|
1585
|
-
} else if (typeof value === 'string' || typeof value === 'number') {
|
|
1586
|
-
const date = new Date(value);
|
|
1587
|
-
if (!isNaN(date.valueOf())) {
|
|
1588
|
-
return date;
|
|
1589
|
-
}
|
|
1590
|
-
throw new InconclusiveMatchError(`${value} is in an invalid date format`);
|
|
1591
|
-
} else {
|
|
1592
|
-
throw new InconclusiveMatchError(`The date provided ${value} must be a string, number, or date object`);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
function relativeDateParseForFeatureFlagMatching(value) {
|
|
1596
|
-
const regex = /^-?(?<number>[0-9]+)(?<interval>[a-z])$/;
|
|
1597
|
-
const match = value.match(regex);
|
|
1598
|
-
const parsedDt = new Date(new Date().toISOString());
|
|
1599
|
-
if (match) {
|
|
1600
|
-
if (!match.groups) {
|
|
1601
|
-
return null;
|
|
1602
|
-
}
|
|
1603
|
-
const number = parseInt(match.groups['number']);
|
|
1604
|
-
if (number >= 10000) {
|
|
1605
|
-
// Guard against overflow, disallow numbers greater than 10_000
|
|
1606
|
-
return null;
|
|
1607
|
-
}
|
|
1608
|
-
const interval = match.groups['interval'];
|
|
1609
|
-
if (interval == 'h') {
|
|
1610
|
-
parsedDt.setUTCHours(parsedDt.getUTCHours() - number);
|
|
1611
|
-
} else if (interval == 'd') {
|
|
1612
|
-
parsedDt.setUTCDate(parsedDt.getUTCDate() - number);
|
|
1613
|
-
} else if (interval == 'w') {
|
|
1614
|
-
parsedDt.setUTCDate(parsedDt.getUTCDate() - number * 7);
|
|
1615
|
-
} else if (interval == 'm') {
|
|
1616
|
-
parsedDt.setUTCMonth(parsedDt.getUTCMonth() - number);
|
|
1617
|
-
} else if (interval == 'y') {
|
|
1618
|
-
parsedDt.setUTCFullYear(parsedDt.getUTCFullYear() - number);
|
|
1619
|
-
} else {
|
|
1620
|
-
return null;
|
|
1621
|
-
}
|
|
1622
|
-
return parsedDt;
|
|
1623
|
-
} else {
|
|
1624
|
-
return null;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
class PostHogMemoryStorage {
|
|
1629
|
-
constructor() {
|
|
1630
|
-
this._memoryStorage = {};
|
|
1631
|
-
}
|
|
1632
|
-
getProperty(key) {
|
|
1633
|
-
return this._memoryStorage[key];
|
|
1634
|
-
}
|
|
1635
|
-
setProperty(key, value) {
|
|
1636
|
-
this._memoryStorage[key] = value !== null ? value : undefined;
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
const _createLogger = (prefix, logMsgIfDebug) => {
|
|
1641
|
-
const logger = {
|
|
1642
|
-
_log: (level, ...args) => {
|
|
1643
|
-
logMsgIfDebug(() => {
|
|
1644
|
-
const consoleLog = console[level];
|
|
1645
|
-
consoleLog(prefix, ...args);
|
|
1646
|
-
});
|
|
1647
|
-
},
|
|
1648
|
-
info: (...args) => {
|
|
1649
|
-
logger._log('log', ...args);
|
|
1650
|
-
},
|
|
1651
|
-
warn: (...args) => {
|
|
1652
|
-
logger._log('warn', ...args);
|
|
1653
|
-
},
|
|
1654
|
-
error: (...args) => {
|
|
1655
|
-
logger._log('error', ...args);
|
|
1656
|
-
},
|
|
1657
|
-
critical: (...args) => {
|
|
1658
|
-
// Critical errors are always logged to the console
|
|
1659
|
-
// eslint-disable-next-line no-console
|
|
1660
|
-
console.error(prefix, ...args);
|
|
1661
|
-
},
|
|
1662
|
-
uninitializedWarning: methodName => {
|
|
1663
|
-
logger.error(`You must initialize PostHog before calling ${methodName}`);
|
|
1664
|
-
},
|
|
1665
|
-
createLogger: additionalPrefix => _createLogger(`${prefix} ${additionalPrefix}`, logMsgIfDebug)
|
|
1666
|
-
};
|
|
1667
|
-
return logger;
|
|
1668
|
-
};
|
|
1669
|
-
const createLogger = logMsgIfDebug => _createLogger('[PostHog.js]', logMsgIfDebug);
|
|
1670
|
-
|
|
1671
|
-
// Standard local evaluation rate limit is 600 per minute (10 per second),
|
|
1672
|
-
// so the fastest a poller should ever be set is 100ms.
|
|
1673
|
-
const MINIMUM_POLLING_INTERVAL = 100;
|
|
1674
|
-
const THIRTY_SECONDS = 30 * 1000;
|
|
1675
|
-
const MAX_CACHE_SIZE = 50 * 1000;
|
|
1676
|
-
// The actual exported Nodejs API.
|
|
1677
|
-
class PostHogBackendClient extends core.PostHogCoreStateless {
|
|
1678
|
-
/**
|
|
1679
|
-
* Initialize a new PostHog client instance.
|
|
1680
|
-
*
|
|
1681
|
-
* @example
|
|
1682
|
-
* ```ts
|
|
1683
|
-
* // Basic initialization
|
|
1684
|
-
* const client = new PostHogBackendClient(
|
|
1685
|
-
* 'your-api-key',
|
|
1686
|
-
* { host: 'https://app.posthog.com' }
|
|
1687
|
-
* )
|
|
1688
|
-
* ```
|
|
1689
|
-
*
|
|
1690
|
-
* @example
|
|
1691
|
-
* ```ts
|
|
1692
|
-
* // With personal API key
|
|
1693
|
-
* const client = new PostHogBackendClient(
|
|
1694
|
-
* 'your-api-key',
|
|
1695
|
-
* {
|
|
1696
|
-
* host: 'https://app.posthog.com',
|
|
1697
|
-
* personalApiKey: 'your-personal-api-key'
|
|
1698
|
-
* }
|
|
1699
|
-
* )
|
|
1700
|
-
* ```
|
|
1701
|
-
*
|
|
1702
|
-
* {@label Initialization}
|
|
1703
|
-
*
|
|
1704
|
-
* @param apiKey - Your PostHog project API key
|
|
1705
|
-
* @param options - Configuration options for the client
|
|
1706
|
-
*/
|
|
1707
|
-
constructor(apiKey, options = {}) {
|
|
1708
|
-
super(apiKey, options);
|
|
1709
|
-
this._memoryStorage = new PostHogMemoryStorage();
|
|
1710
|
-
this.options = options;
|
|
1711
|
-
this.logger = createLogger(this.logMsgIfDebug.bind(this));
|
|
1712
|
-
this.options.featureFlagsPollingInterval = typeof options.featureFlagsPollingInterval === 'number' ? Math.max(options.featureFlagsPollingInterval, MINIMUM_POLLING_INTERVAL) : THIRTY_SECONDS;
|
|
1713
|
-
if (options.personalApiKey) {
|
|
1714
|
-
if (options.personalApiKey.includes('phc_')) {
|
|
1715
|
-
throw new Error('Your Personal API key is invalid. These keys are prefixed with "phx_" and can be created in PostHog project settings.');
|
|
1716
|
-
}
|
|
1717
|
-
// Only start the poller if local evaluation is enabled (defaults to true for backward compatibility)
|
|
1718
|
-
const shouldEnableLocalEvaluation = options.enableLocalEvaluation !== false;
|
|
1719
|
-
if (shouldEnableLocalEvaluation) {
|
|
1720
|
-
this.featureFlagsPoller = new FeatureFlagsPoller({
|
|
1721
|
-
pollingInterval: this.options.featureFlagsPollingInterval,
|
|
1722
|
-
personalApiKey: options.personalApiKey,
|
|
1723
|
-
projectApiKey: apiKey,
|
|
1724
|
-
timeout: options.requestTimeout ?? 10000,
|
|
1725
|
-
// 10 seconds
|
|
1726
|
-
host: this.host,
|
|
1727
|
-
fetch: options.fetch,
|
|
1728
|
-
onError: err => {
|
|
1729
|
-
this._events.emit('error', err);
|
|
1730
|
-
},
|
|
1731
|
-
onLoad: count => {
|
|
1732
|
-
this._events.emit('localEvaluationFlagsLoaded', count);
|
|
1733
|
-
},
|
|
1734
|
-
customHeaders: this.getCustomHeaders()
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
this.errorTracking = new ErrorTracking(this, options, this.logger);
|
|
1739
|
-
this.distinctIdHasSentFlagCalls = {};
|
|
1740
|
-
this.maxCacheSize = options.maxCacheSize || MAX_CACHE_SIZE;
|
|
1741
|
-
}
|
|
1742
|
-
/**
|
|
1743
|
-
* Get a persisted property value from memory storage.
|
|
1744
|
-
*
|
|
1745
|
-
* @example
|
|
1746
|
-
* ```ts
|
|
1747
|
-
* // Get user ID
|
|
1748
|
-
* const userId = client.getPersistedProperty('userId')
|
|
1749
|
-
* ```
|
|
1750
|
-
*
|
|
1751
|
-
* @example
|
|
1752
|
-
* ```ts
|
|
1753
|
-
* // Get session ID
|
|
1754
|
-
* const sessionId = client.getPersistedProperty('sessionId')
|
|
1755
|
-
* ```
|
|
1756
|
-
*
|
|
1757
|
-
* {@label Initialization}
|
|
1758
|
-
*
|
|
1759
|
-
* @param key - The property key to retrieve
|
|
1760
|
-
* @returns The stored property value or undefined if not found
|
|
1761
|
-
*/
|
|
1762
|
-
getPersistedProperty(key) {
|
|
1763
|
-
return this._memoryStorage.getProperty(key);
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Set a persisted property value in memory storage.
|
|
1767
|
-
*
|
|
1768
|
-
* @example
|
|
1769
|
-
* ```ts
|
|
1770
|
-
* // Set user ID
|
|
1771
|
-
* client.setPersistedProperty('userId', 'user_123')
|
|
1772
|
-
* ```
|
|
1773
|
-
*
|
|
1774
|
-
* @example
|
|
1775
|
-
* ```ts
|
|
1776
|
-
* // Set session ID
|
|
1777
|
-
* client.setPersistedProperty('sessionId', 'session_456')
|
|
1778
|
-
* ```
|
|
1779
|
-
*
|
|
1780
|
-
* {@label Initialization}
|
|
1781
|
-
*
|
|
1782
|
-
* @param key - The property key to set
|
|
1783
|
-
* @param value - The value to store (null to remove)
|
|
1784
|
-
*/
|
|
1785
|
-
setPersistedProperty(key, value) {
|
|
1786
|
-
return this._memoryStorage.setProperty(key, value);
|
|
1787
|
-
}
|
|
1788
|
-
/**
|
|
1789
|
-
* Make an HTTP request using the configured fetch function or default fetch.
|
|
1790
|
-
*
|
|
1791
|
-
* @example
|
|
1792
|
-
* ```ts
|
|
1793
|
-
* // POST request
|
|
1794
|
-
* const response = await client.fetch('/api/endpoint', {
|
|
1795
|
-
* method: 'POST',
|
|
1796
|
-
* headers: { 'Content-Type': 'application/json' },
|
|
1797
|
-
* body: JSON.stringify(data)
|
|
1798
|
-
* })
|
|
1799
|
-
* ```
|
|
1800
|
-
*
|
|
1801
|
-
* @internal
|
|
1802
|
-
*
|
|
1803
|
-
* {@label Initialization}
|
|
1804
|
-
*
|
|
1805
|
-
* @param url - The URL to fetch
|
|
1806
|
-
* @param options - Fetch options
|
|
1807
|
-
* @returns Promise resolving to the fetch response
|
|
1808
|
-
*/
|
|
1809
|
-
fetch(url, options) {
|
|
1810
|
-
return this.options.fetch ? this.options.fetch(url, options) : fetch(url, options);
|
|
1811
|
-
}
|
|
1812
|
-
/**
|
|
1813
|
-
* Get the library version from package.json.
|
|
1814
|
-
*
|
|
1815
|
-
* @example
|
|
1816
|
-
* ```ts
|
|
1817
|
-
* // Get version
|
|
1818
|
-
* const version = client.getLibraryVersion()
|
|
1819
|
-
* console.log(`Using PostHog SDK version: ${version}`)
|
|
1820
|
-
* ```
|
|
1821
|
-
*
|
|
1822
|
-
* {@label Initialization}
|
|
1823
|
-
*
|
|
1824
|
-
* @returns The current library version string
|
|
1825
|
-
*/
|
|
1826
|
-
getLibraryVersion() {
|
|
1827
|
-
return version;
|
|
1828
|
-
}
|
|
1829
|
-
/**
|
|
1830
|
-
* Get the custom user agent string for this client.
|
|
1831
|
-
*
|
|
1832
|
-
* @example
|
|
1833
|
-
* ```ts
|
|
1834
|
-
* // Get user agent
|
|
1835
|
-
* const userAgent = client.getCustomUserAgent()
|
|
1836
|
-
* // Returns: "posthog-node/5.7.0"
|
|
1837
|
-
* ```
|
|
1838
|
-
*
|
|
1839
|
-
* {@label Identification}
|
|
1840
|
-
*
|
|
1841
|
-
* @returns The formatted user agent string
|
|
1842
|
-
*/
|
|
1843
|
-
getCustomUserAgent() {
|
|
1844
|
-
return `${this.getLibraryId()}/${this.getLibraryVersion()}`;
|
|
1845
|
-
}
|
|
1846
|
-
/**
|
|
1847
|
-
* Enable the PostHog client (opt-in).
|
|
1848
|
-
*
|
|
1849
|
-
* @example
|
|
1850
|
-
* ```ts
|
|
1851
|
-
* // Enable client
|
|
1852
|
-
* await client.enable()
|
|
1853
|
-
* // Client is now enabled and will capture events
|
|
1854
|
-
* ```
|
|
1855
|
-
*
|
|
1856
|
-
* {@label Privacy}
|
|
1857
|
-
*
|
|
1858
|
-
* @returns Promise that resolves when the client is enabled
|
|
1859
|
-
*/
|
|
1860
|
-
enable() {
|
|
1861
|
-
return super.optIn();
|
|
1862
|
-
}
|
|
1863
|
-
/**
|
|
1864
|
-
* Disable the PostHog client (opt-out).
|
|
1865
|
-
*
|
|
1866
|
-
* @example
|
|
1867
|
-
* ```ts
|
|
1868
|
-
* // Disable client
|
|
1869
|
-
* await client.disable()
|
|
1870
|
-
* // Client is now disabled and will not capture events
|
|
1871
|
-
* ```
|
|
1872
|
-
*
|
|
1873
|
-
* {@label Privacy}
|
|
1874
|
-
*
|
|
1875
|
-
* @returns Promise that resolves when the client is disabled
|
|
1876
|
-
*/
|
|
1877
|
-
disable() {
|
|
1878
|
-
return super.optOut();
|
|
1879
|
-
}
|
|
1880
|
-
/**
|
|
1881
|
-
* Enable or disable debug logging.
|
|
1882
|
-
*
|
|
1883
|
-
* @example
|
|
1884
|
-
* ```ts
|
|
1885
|
-
* // Enable debug logging
|
|
1886
|
-
* client.debug(true)
|
|
1887
|
-
* ```
|
|
1888
|
-
*
|
|
1889
|
-
* @example
|
|
1890
|
-
* ```ts
|
|
1891
|
-
* // Disable debug logging
|
|
1892
|
-
* client.debug(false)
|
|
1893
|
-
* ```
|
|
1894
|
-
*
|
|
1895
|
-
* {@label Initialization}
|
|
1896
|
-
*
|
|
1897
|
-
* @param enabled - Whether to enable debug logging
|
|
1898
|
-
*/
|
|
1899
|
-
debug(enabled = true) {
|
|
1900
|
-
super.debug(enabled);
|
|
1901
|
-
this.featureFlagsPoller?.debug(enabled);
|
|
1902
|
-
}
|
|
1903
|
-
/**
|
|
1904
|
-
* Capture an event manually.
|
|
1905
|
-
*
|
|
1906
|
-
* @example
|
|
1907
|
-
* ```ts
|
|
1908
|
-
* // Basic capture
|
|
1909
|
-
* client.capture({
|
|
1910
|
-
* distinctId: 'user_123',
|
|
1911
|
-
* event: 'button_clicked',
|
|
1912
|
-
* properties: { button_color: 'red' }
|
|
1913
|
-
* })
|
|
1914
|
-
* ```
|
|
1915
|
-
*
|
|
1916
|
-
* {@label Capture}
|
|
1917
|
-
*
|
|
1918
|
-
* @param props - The event properties
|
|
1919
|
-
* @returns void
|
|
1920
|
-
*/
|
|
1921
|
-
capture(props) {
|
|
1922
|
-
if (typeof props === 'string') {
|
|
1923
|
-
this.logMsgIfDebug(() => console.warn('Called capture() with a string as the first argument when an object was expected.'));
|
|
1924
|
-
}
|
|
1925
|
-
this.addPendingPromise(this.prepareEventMessage(props).then(({
|
|
1926
|
-
distinctId,
|
|
1927
|
-
event,
|
|
1928
|
-
properties,
|
|
1929
|
-
options
|
|
1930
|
-
}) => {
|
|
1931
|
-
return super.captureStateless(distinctId, event, properties, {
|
|
1932
|
-
timestamp: options.timestamp,
|
|
1933
|
-
disableGeoip: options.disableGeoip,
|
|
1934
|
-
uuid: options.uuid
|
|
1935
|
-
});
|
|
1936
|
-
}).catch(err => {
|
|
1937
|
-
if (err) {
|
|
1938
|
-
console.error(err);
|
|
1939
|
-
}
|
|
1940
|
-
}));
|
|
1941
|
-
}
|
|
1942
|
-
/**
|
|
1943
|
-
* Capture an event immediately (synchronously).
|
|
1944
|
-
*
|
|
1945
|
-
* @example
|
|
1946
|
-
* ```ts
|
|
1947
|
-
* // Basic immediate capture
|
|
1948
|
-
* await client.captureImmediate({
|
|
1949
|
-
* distinctId: 'user_123',
|
|
1950
|
-
* event: 'button_clicked',
|
|
1951
|
-
* properties: { button_color: 'red' }
|
|
1952
|
-
* })
|
|
1953
|
-
* ```
|
|
1954
|
-
*
|
|
1955
|
-
* @example
|
|
1956
|
-
* ```ts
|
|
1957
|
-
* // With feature flags
|
|
1958
|
-
* await client.captureImmediate({
|
|
1959
|
-
* distinctId: 'user_123',
|
|
1960
|
-
* event: 'user_action',
|
|
1961
|
-
* sendFeatureFlags: true
|
|
1962
|
-
* })
|
|
1963
|
-
* ```
|
|
1964
|
-
*
|
|
1965
|
-
* @example
|
|
1966
|
-
* ```ts
|
|
1967
|
-
* // With custom feature flags options
|
|
1968
|
-
* await client.captureImmediate({
|
|
1969
|
-
* distinctId: 'user_123',
|
|
1970
|
-
* event: 'user_action',
|
|
1971
|
-
* sendFeatureFlags: {
|
|
1972
|
-
* onlyEvaluateLocally: true,
|
|
1973
|
-
* personProperties: { plan: 'premium' },
|
|
1974
|
-
* groupProperties: { org: { tier: 'enterprise' } }
|
|
1975
|
-
* flagKeys: ['flag1', 'flag2']
|
|
1976
|
-
* }
|
|
1977
|
-
* })
|
|
1978
|
-
* ```
|
|
1979
|
-
*
|
|
1980
|
-
* {@label Capture}
|
|
1981
|
-
*
|
|
1982
|
-
* @param props - The event properties
|
|
1983
|
-
* @returns Promise that resolves when the event is captured
|
|
1984
|
-
*/
|
|
1985
|
-
async captureImmediate(props) {
|
|
1986
|
-
if (typeof props === 'string') {
|
|
1987
|
-
this.logMsgIfDebug(() => console.warn('Called captureImmediate() with a string as the first argument when an object was expected.'));
|
|
1988
|
-
}
|
|
1989
|
-
return this.addPendingPromise(this.prepareEventMessage(props).then(({
|
|
1990
|
-
distinctId,
|
|
1991
|
-
event,
|
|
1992
|
-
properties,
|
|
1993
|
-
options
|
|
1994
|
-
}) => {
|
|
1995
|
-
return super.captureStatelessImmediate(distinctId, event, properties, {
|
|
1996
|
-
timestamp: options.timestamp,
|
|
1997
|
-
disableGeoip: options.disableGeoip,
|
|
1998
|
-
uuid: options.uuid
|
|
1999
|
-
});
|
|
2000
|
-
}).catch(err => {
|
|
2001
|
-
if (err) {
|
|
2002
|
-
console.error(err);
|
|
2003
|
-
}
|
|
2004
|
-
}));
|
|
2005
|
-
}
|
|
2006
|
-
/**
|
|
2007
|
-
* Identify a user and set their properties.
|
|
2008
|
-
*
|
|
2009
|
-
* @example
|
|
2010
|
-
* ```ts
|
|
2011
|
-
* // Basic identify with properties
|
|
2012
|
-
* client.identify({
|
|
2013
|
-
* distinctId: 'user_123',
|
|
2014
|
-
* properties: {
|
|
2015
|
-
* name: 'John Doe',
|
|
2016
|
-
* email: 'john@example.com',
|
|
2017
|
-
* plan: 'premium'
|
|
2018
|
-
* }
|
|
2019
|
-
* })
|
|
2020
|
-
* ```
|
|
2021
|
-
*
|
|
2022
|
-
* @example
|
|
2023
|
-
* ```ts
|
|
2024
|
-
* // Using $set and $set_once
|
|
2025
|
-
* client.identify({
|
|
2026
|
-
* distinctId: 'user_123',
|
|
2027
|
-
* properties: {
|
|
2028
|
-
* $set: { name: 'John Doe', email: 'john@example.com' },
|
|
2029
|
-
* $set_once: { first_login: new Date().toISOString() }
|
|
2030
|
-
* }
|
|
2031
|
-
* })
|
|
2032
|
-
* ```
|
|
2033
|
-
*
|
|
2034
|
-
* {@label Identification}
|
|
2035
|
-
*
|
|
2036
|
-
* @param data - The identify data containing distinctId and properties
|
|
2037
|
-
*/
|
|
2038
|
-
identify({
|
|
2039
|
-
distinctId,
|
|
2040
|
-
properties,
|
|
2041
|
-
disableGeoip
|
|
2042
|
-
}) {
|
|
2043
|
-
// Catch properties passed as $set and move them to the top level
|
|
2044
|
-
// promote $set and $set_once to top level
|
|
2045
|
-
const userPropsOnce = properties?.$set_once;
|
|
2046
|
-
delete properties?.$set_once;
|
|
2047
|
-
// if no $set is provided we assume all properties are $set
|
|
2048
|
-
const userProps = properties?.$set || properties;
|
|
2049
|
-
super.identifyStateless(distinctId, {
|
|
2050
|
-
$set: userProps,
|
|
2051
|
-
$set_once: userPropsOnce
|
|
2052
|
-
}, {
|
|
2053
|
-
disableGeoip
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
/**
|
|
2057
|
-
* Identify a user and set their properties immediately (synchronously).
|
|
2058
|
-
*
|
|
2059
|
-
* @example
|
|
2060
|
-
* ```ts
|
|
2061
|
-
* // Basic immediate identify
|
|
2062
|
-
* await client.identifyImmediate({
|
|
2063
|
-
* distinctId: 'user_123',
|
|
2064
|
-
* properties: {
|
|
2065
|
-
* name: 'John Doe',
|
|
2066
|
-
* email: 'john@example.com'
|
|
2067
|
-
* }
|
|
2068
|
-
* })
|
|
2069
|
-
* ```
|
|
2070
|
-
*
|
|
2071
|
-
* {@label Identification}
|
|
2072
|
-
*
|
|
2073
|
-
* @param data - The identify data containing distinctId and properties
|
|
2074
|
-
* @returns Promise that resolves when the identify is processed
|
|
2075
|
-
*/
|
|
2076
|
-
async identifyImmediate({
|
|
2077
|
-
distinctId,
|
|
2078
|
-
properties,
|
|
2079
|
-
disableGeoip
|
|
2080
|
-
}) {
|
|
2081
|
-
// promote $set and $set_once to top level
|
|
2082
|
-
const userPropsOnce = properties?.$set_once;
|
|
2083
|
-
delete properties?.$set_once;
|
|
2084
|
-
// if no $set is provided we assume all properties are $set
|
|
2085
|
-
const userProps = properties?.$set || properties;
|
|
2086
|
-
await super.identifyStatelessImmediate(distinctId, {
|
|
2087
|
-
$set: userProps,
|
|
2088
|
-
$set_once: userPropsOnce
|
|
2089
|
-
}, {
|
|
2090
|
-
disableGeoip
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2093
|
-
/**
|
|
2094
|
-
* Create an alias to link two distinct IDs together.
|
|
2095
|
-
*
|
|
2096
|
-
* @example
|
|
2097
|
-
* ```ts
|
|
2098
|
-
* // Link an anonymous user to an identified user
|
|
2099
|
-
* client.alias({
|
|
2100
|
-
* distinctId: 'anonymous_123',
|
|
2101
|
-
* alias: 'user_456'
|
|
2102
|
-
* })
|
|
2103
|
-
* ```
|
|
2104
|
-
*
|
|
2105
|
-
* {@label Identification}
|
|
2106
|
-
*
|
|
2107
|
-
* @param data - The alias data containing distinctId and alias
|
|
2108
|
-
*/
|
|
2109
|
-
alias(data) {
|
|
2110
|
-
super.aliasStateless(data.alias, data.distinctId, undefined, {
|
|
2111
|
-
disableGeoip: data.disableGeoip
|
|
2112
|
-
});
|
|
2113
|
-
}
|
|
2114
|
-
/**
|
|
2115
|
-
* Create an alias to link two distinct IDs together immediately (synchronously).
|
|
2116
|
-
*
|
|
2117
|
-
* @example
|
|
2118
|
-
* ```ts
|
|
2119
|
-
* // Link an anonymous user to an identified user immediately
|
|
2120
|
-
* await client.aliasImmediate({
|
|
2121
|
-
* distinctId: 'anonymous_123',
|
|
2122
|
-
* alias: 'user_456'
|
|
2123
|
-
* })
|
|
2124
|
-
* ```
|
|
2125
|
-
*
|
|
2126
|
-
* {@label Identification}
|
|
2127
|
-
*
|
|
2128
|
-
* @param data - The alias data containing distinctId and alias
|
|
2129
|
-
* @returns Promise that resolves when the alias is processed
|
|
2130
|
-
*/
|
|
2131
|
-
async aliasImmediate(data) {
|
|
2132
|
-
await super.aliasStatelessImmediate(data.alias, data.distinctId, undefined, {
|
|
2133
|
-
disableGeoip: data.disableGeoip
|
|
2134
|
-
});
|
|
2135
|
-
}
|
|
2136
|
-
/**
|
|
2137
|
-
* Check if local evaluation of feature flags is ready.
|
|
2138
|
-
*
|
|
2139
|
-
* @example
|
|
2140
|
-
* ```ts
|
|
2141
|
-
* // Check if ready
|
|
2142
|
-
* if (client.isLocalEvaluationReady()) {
|
|
2143
|
-
* // Local evaluation is ready, can evaluate flags locally
|
|
2144
|
-
* const flag = await client.getFeatureFlag('flag-key', 'user_123')
|
|
2145
|
-
* } else {
|
|
2146
|
-
* // Local evaluation not ready, will use remote evaluation
|
|
2147
|
-
* const flag = await client.getFeatureFlag('flag-key', 'user_123')
|
|
2148
|
-
* }
|
|
2149
|
-
* ```
|
|
2150
|
-
*
|
|
2151
|
-
* {@label Feature flags}
|
|
2152
|
-
*
|
|
2153
|
-
* @returns true if local evaluation is ready, false otherwise
|
|
2154
|
-
*/
|
|
2155
|
-
isLocalEvaluationReady() {
|
|
2156
|
-
return this.featureFlagsPoller?.isLocalEvaluationReady() ?? false;
|
|
2157
|
-
}
|
|
2158
|
-
/**
|
|
2159
|
-
* Wait for local evaluation of feature flags to be ready.
|
|
2160
|
-
*
|
|
2161
|
-
* @example
|
|
2162
|
-
* ```ts
|
|
2163
|
-
* // Wait for local evaluation
|
|
2164
|
-
* const isReady = await client.waitForLocalEvaluationReady()
|
|
2165
|
-
* if (isReady) {
|
|
2166
|
-
* console.log('Local evaluation is ready')
|
|
2167
|
-
* } else {
|
|
2168
|
-
* console.log('Local evaluation timed out')
|
|
2169
|
-
* }
|
|
2170
|
-
* ```
|
|
2171
|
-
*
|
|
2172
|
-
* @example
|
|
2173
|
-
* ```ts
|
|
2174
|
-
* // Wait with custom timeout
|
|
2175
|
-
* const isReady = await client.waitForLocalEvaluationReady(10000) // 10 seconds
|
|
2176
|
-
* ```
|
|
2177
|
-
*
|
|
2178
|
-
* {@label Feature flags}
|
|
2179
|
-
*
|
|
2180
|
-
* @param timeoutMs - Timeout in milliseconds (default: 30000)
|
|
2181
|
-
* @returns Promise that resolves to true if ready, false if timed out
|
|
2182
|
-
*/
|
|
2183
|
-
async waitForLocalEvaluationReady(timeoutMs = THIRTY_SECONDS) {
|
|
2184
|
-
if (this.isLocalEvaluationReady()) {
|
|
2185
|
-
return true;
|
|
2186
|
-
}
|
|
2187
|
-
if (this.featureFlagsPoller === undefined) {
|
|
2188
|
-
return false;
|
|
2189
|
-
}
|
|
2190
|
-
return new Promise(resolve => {
|
|
2191
|
-
const timeout = setTimeout(() => {
|
|
2192
|
-
cleanup();
|
|
2193
|
-
resolve(false);
|
|
2194
|
-
}, timeoutMs);
|
|
2195
|
-
const cleanup = this._events.on('localEvaluationFlagsLoaded', count => {
|
|
2196
|
-
clearTimeout(timeout);
|
|
2197
|
-
cleanup();
|
|
2198
|
-
resolve(count > 0);
|
|
2199
|
-
});
|
|
2200
|
-
});
|
|
2201
|
-
}
|
|
2202
|
-
/**
|
|
2203
|
-
* Get the value of a feature flag for a specific user.
|
|
2204
|
-
*
|
|
2205
|
-
* @example
|
|
2206
|
-
* ```ts
|
|
2207
|
-
* // Basic feature flag check
|
|
2208
|
-
* const flagValue = await client.getFeatureFlag('new-feature', 'user_123')
|
|
2209
|
-
* if (flagValue === 'variant-a') {
|
|
2210
|
-
* // Show variant A
|
|
2211
|
-
* } else if (flagValue === 'variant-b') {
|
|
2212
|
-
* // Show variant B
|
|
2213
|
-
* } else {
|
|
2214
|
-
* // Flag is disabled or not found
|
|
2215
|
-
* }
|
|
2216
|
-
* ```
|
|
2217
|
-
*
|
|
2218
|
-
* @example
|
|
2219
|
-
* ```ts
|
|
2220
|
-
* // With groups and properties
|
|
2221
|
-
* const flagValue = await client.getFeatureFlag('org-feature', 'user_123', {
|
|
2222
|
-
* groups: { organization: 'acme-corp' },
|
|
2223
|
-
* personProperties: { plan: 'enterprise' },
|
|
2224
|
-
* groupProperties: { organization: { tier: 'premium' } }
|
|
2225
|
-
* })
|
|
2226
|
-
* ```
|
|
2227
|
-
*
|
|
2228
|
-
* @example
|
|
2229
|
-
* ```ts
|
|
2230
|
-
* // Only evaluate locally
|
|
2231
|
-
* const flagValue = await client.getFeatureFlag('local-flag', 'user_123', {
|
|
2232
|
-
* onlyEvaluateLocally: true
|
|
2233
|
-
* })
|
|
2234
|
-
* ```
|
|
2235
|
-
*
|
|
2236
|
-
* {@label Feature flags}
|
|
2237
|
-
*
|
|
2238
|
-
* @param key - The feature flag key
|
|
2239
|
-
* @param distinctId - The user's distinct ID
|
|
2240
|
-
* @param options - Optional configuration for flag evaluation
|
|
2241
|
-
* @returns Promise that resolves to the flag value or undefined
|
|
2242
|
-
*/
|
|
2243
|
-
async getFeatureFlag(key, distinctId, options) {
|
|
2244
|
-
const {
|
|
2245
|
-
groups,
|
|
2246
|
-
disableGeoip
|
|
2247
|
-
} = options || {};
|
|
2248
|
-
let {
|
|
2249
|
-
onlyEvaluateLocally,
|
|
2250
|
-
sendFeatureFlagEvents,
|
|
2251
|
-
personProperties,
|
|
2252
|
-
groupProperties
|
|
2253
|
-
} = options || {};
|
|
2254
|
-
const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
|
|
2255
|
-
personProperties = adjustedProperties.allPersonProperties;
|
|
2256
|
-
groupProperties = adjustedProperties.allGroupProperties;
|
|
2257
|
-
// set defaults
|
|
2258
|
-
if (onlyEvaluateLocally == undefined) {
|
|
2259
|
-
onlyEvaluateLocally = false;
|
|
2260
|
-
}
|
|
2261
|
-
if (sendFeatureFlagEvents == undefined) {
|
|
2262
|
-
sendFeatureFlagEvents = this.options.sendFeatureFlagEvent ?? true;
|
|
2263
|
-
}
|
|
2264
|
-
let response = await this.featureFlagsPoller?.getFeatureFlag(key, distinctId, groups, personProperties, groupProperties);
|
|
2265
|
-
const flagWasLocallyEvaluated = response !== undefined;
|
|
2266
|
-
let requestId = undefined;
|
|
2267
|
-
let flagDetail = undefined;
|
|
2268
|
-
if (!flagWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
2269
|
-
const remoteResponse = await super.getFeatureFlagDetailStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
2270
|
-
if (remoteResponse === undefined) {
|
|
2271
|
-
return undefined;
|
|
2272
|
-
}
|
|
2273
|
-
flagDetail = remoteResponse.response;
|
|
2274
|
-
response = core.getFeatureFlagValue(flagDetail);
|
|
2275
|
-
requestId = remoteResponse?.requestId;
|
|
2276
|
-
}
|
|
2277
|
-
const featureFlagReportedKey = `${key}_${response}`;
|
|
2278
|
-
if (sendFeatureFlagEvents && (!(distinctId in this.distinctIdHasSentFlagCalls) || !this.distinctIdHasSentFlagCalls[distinctId].includes(featureFlagReportedKey))) {
|
|
2279
|
-
if (Object.keys(this.distinctIdHasSentFlagCalls).length >= this.maxCacheSize) {
|
|
2280
|
-
this.distinctIdHasSentFlagCalls = {};
|
|
2281
|
-
}
|
|
2282
|
-
if (Array.isArray(this.distinctIdHasSentFlagCalls[distinctId])) {
|
|
2283
|
-
this.distinctIdHasSentFlagCalls[distinctId].push(featureFlagReportedKey);
|
|
2284
|
-
} else {
|
|
2285
|
-
this.distinctIdHasSentFlagCalls[distinctId] = [featureFlagReportedKey];
|
|
2286
|
-
}
|
|
2287
|
-
this.capture({
|
|
2288
|
-
distinctId,
|
|
2289
|
-
event: '$feature_flag_called',
|
|
2290
|
-
properties: {
|
|
2291
|
-
$feature_flag: key,
|
|
2292
|
-
$feature_flag_response: response,
|
|
2293
|
-
$feature_flag_id: flagDetail?.metadata?.id,
|
|
2294
|
-
$feature_flag_version: flagDetail?.metadata?.version,
|
|
2295
|
-
$feature_flag_reason: flagDetail?.reason?.description ?? flagDetail?.reason?.code,
|
|
2296
|
-
locally_evaluated: flagWasLocallyEvaluated,
|
|
2297
|
-
[`$feature/${key}`]: response,
|
|
2298
|
-
$feature_flag_request_id: requestId
|
|
2299
|
-
},
|
|
2300
|
-
groups,
|
|
2301
|
-
disableGeoip
|
|
2302
|
-
});
|
|
2303
|
-
}
|
|
2304
|
-
return response;
|
|
2305
|
-
}
|
|
2306
|
-
/**
|
|
2307
|
-
* Get the payload for a feature flag.
|
|
2308
|
-
*
|
|
2309
|
-
* @example
|
|
2310
|
-
* ```ts
|
|
2311
|
-
* // Get payload for a feature flag
|
|
2312
|
-
* const payload = await client.getFeatureFlagPayload('flag-key', 'user_123')
|
|
2313
|
-
* if (payload) {
|
|
2314
|
-
* console.log('Flag payload:', payload)
|
|
2315
|
-
* }
|
|
2316
|
-
* ```
|
|
2317
|
-
*
|
|
2318
|
-
* @example
|
|
2319
|
-
* ```ts
|
|
2320
|
-
* // Get payload with specific match value
|
|
2321
|
-
* const payload = await client.getFeatureFlagPayload('flag-key', 'user_123', 'variant-a')
|
|
2322
|
-
* ```
|
|
2323
|
-
*
|
|
2324
|
-
* @example
|
|
2325
|
-
* ```ts
|
|
2326
|
-
* // With groups and properties
|
|
2327
|
-
* const payload = await client.getFeatureFlagPayload('org-flag', 'user_123', undefined, {
|
|
2328
|
-
* groups: { organization: 'acme-corp' },
|
|
2329
|
-
* personProperties: { plan: 'enterprise' }
|
|
2330
|
-
* })
|
|
2331
|
-
* ```
|
|
2332
|
-
*
|
|
2333
|
-
* {@label Feature flags}
|
|
2334
|
-
*
|
|
2335
|
-
* @param key - The feature flag key
|
|
2336
|
-
* @param distinctId - The user's distinct ID
|
|
2337
|
-
* @param matchValue - Optional match value to get payload for
|
|
2338
|
-
* @param options - Optional configuration for flag evaluation
|
|
2339
|
-
* @returns Promise that resolves to the flag payload or undefined
|
|
2340
|
-
*/
|
|
2341
|
-
async getFeatureFlagPayload(key, distinctId, matchValue, options) {
|
|
2342
|
-
const {
|
|
2343
|
-
groups,
|
|
2344
|
-
disableGeoip
|
|
2345
|
-
} = options || {};
|
|
2346
|
-
let {
|
|
2347
|
-
onlyEvaluateLocally,
|
|
2348
|
-
personProperties,
|
|
2349
|
-
groupProperties
|
|
2350
|
-
} = options || {};
|
|
2351
|
-
const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
|
|
2352
|
-
personProperties = adjustedProperties.allPersonProperties;
|
|
2353
|
-
groupProperties = adjustedProperties.allGroupProperties;
|
|
2354
|
-
let response = undefined;
|
|
2355
|
-
const localEvaluationEnabled = this.featureFlagsPoller !== undefined;
|
|
2356
|
-
if (localEvaluationEnabled) {
|
|
2357
|
-
// Ensure flags are loaded before checking for the specific flag
|
|
2358
|
-
await this.featureFlagsPoller?.loadFeatureFlags();
|
|
2359
|
-
const flag = this.featureFlagsPoller?.featureFlagsByKey[key];
|
|
2360
|
-
if (flag) {
|
|
2361
|
-
const result = await this.featureFlagsPoller?.computeFlagAndPayloadLocally(flag, distinctId, groups, personProperties, groupProperties, matchValue);
|
|
2362
|
-
if (result) {
|
|
2363
|
-
matchValue = result.value;
|
|
2364
|
-
response = result.payload;
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
// set defaults
|
|
2369
|
-
if (onlyEvaluateLocally == undefined) {
|
|
2370
|
-
onlyEvaluateLocally = false;
|
|
2371
|
-
}
|
|
2372
|
-
const payloadWasLocallyEvaluated = response !== undefined;
|
|
2373
|
-
if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
|
|
2374
|
-
response = await super.getFeatureFlagPayloadStateless(key, distinctId, groups, personProperties, groupProperties, disableGeoip);
|
|
2375
|
-
}
|
|
2376
|
-
return response;
|
|
2377
|
-
}
|
|
2378
|
-
/**
|
|
2379
|
-
* Get the remote config payload for a feature flag.
|
|
2380
|
-
*
|
|
2381
|
-
* @example
|
|
2382
|
-
* ```ts
|
|
2383
|
-
* // Get remote config payload
|
|
2384
|
-
* const payload = await client.getRemoteConfigPayload('flag-key')
|
|
2385
|
-
* if (payload) {
|
|
2386
|
-
* console.log('Remote config payload:', payload)
|
|
2387
|
-
* }
|
|
2388
|
-
* ```
|
|
2389
|
-
*
|
|
2390
|
-
* {@label Feature flags}
|
|
2391
|
-
*
|
|
2392
|
-
* @param flagKey - The feature flag key
|
|
2393
|
-
* @returns Promise that resolves to the remote config payload or undefined
|
|
2394
|
-
* @throws Error if personal API key is not provided
|
|
2395
|
-
*/
|
|
2396
|
-
async getRemoteConfigPayload(flagKey) {
|
|
2397
|
-
if (!this.options.personalApiKey) {
|
|
2398
|
-
throw new Error('Personal API key is required for remote config payload decryption');
|
|
2399
|
-
}
|
|
2400
|
-
const response = await this._requestRemoteConfigPayload(flagKey);
|
|
2401
|
-
if (!response) {
|
|
2402
|
-
return undefined;
|
|
2403
|
-
}
|
|
2404
|
-
const parsed = await response.json();
|
|
2405
|
-
// The payload from the endpoint is stored as a JSON encoded string. So when we return
|
|
2406
|
-
// it, it's effectively double encoded. As far as we know, we should never get single-encoded
|
|
2407
|
-
// JSON, but we'll be defensive here just in case.
|
|
2408
|
-
if (typeof parsed === 'string') {
|
|
2409
|
-
try {
|
|
2410
|
-
// If the parsed value is a string, try parsing it again to handle double-encoded JSON
|
|
2411
|
-
return JSON.parse(parsed);
|
|
2412
|
-
} catch (e) {
|
|
2413
|
-
// If second parse fails, return the string as is
|
|
2414
|
-
return parsed;
|
|
2415
|
-
}
|
|
2416
|
-
}
|
|
2417
|
-
return parsed;
|
|
2418
|
-
}
|
|
2419
|
-
/**
|
|
2420
|
-
* Check if a feature flag is enabled for a specific user.
|
|
2421
|
-
*
|
|
2422
|
-
* @example
|
|
2423
|
-
* ```ts
|
|
2424
|
-
* // Basic feature flag check
|
|
2425
|
-
* const isEnabled = await client.isFeatureEnabled('new-feature', 'user_123')
|
|
2426
|
-
* if (isEnabled) {
|
|
2427
|
-
* // Feature is enabled
|
|
2428
|
-
* console.log('New feature is active')
|
|
2429
|
-
* } else {
|
|
2430
|
-
* // Feature is disabled
|
|
2431
|
-
* console.log('New feature is not active')
|
|
2432
|
-
* }
|
|
2433
|
-
* ```
|
|
2434
|
-
*
|
|
2435
|
-
* @example
|
|
2436
|
-
* ```ts
|
|
2437
|
-
* // With groups and properties
|
|
2438
|
-
* const isEnabled = await client.isFeatureEnabled('org-feature', 'user_123', {
|
|
2439
|
-
* groups: { organization: 'acme-corp' },
|
|
2440
|
-
* personProperties: { plan: 'enterprise' }
|
|
2441
|
-
* })
|
|
2442
|
-
* ```
|
|
2443
|
-
*
|
|
2444
|
-
* {@label Feature flags}
|
|
2445
|
-
*
|
|
2446
|
-
* @param key - The feature flag key
|
|
2447
|
-
* @param distinctId - The user's distinct ID
|
|
2448
|
-
* @param options - Optional configuration for flag evaluation
|
|
2449
|
-
* @returns Promise that resolves to true if enabled, false if disabled, undefined if not found
|
|
2450
|
-
*/
|
|
2451
|
-
async isFeatureEnabled(key, distinctId, options) {
|
|
2452
|
-
const feat = await this.getFeatureFlag(key, distinctId, options);
|
|
2453
|
-
if (feat === undefined) {
|
|
2454
|
-
return undefined;
|
|
2455
|
-
}
|
|
2456
|
-
return !!feat || false;
|
|
2457
|
-
}
|
|
2458
|
-
/**
|
|
2459
|
-
* Get all feature flag values for a specific user.
|
|
2460
|
-
*
|
|
2461
|
-
* @example
|
|
2462
|
-
* ```ts
|
|
2463
|
-
* // Get all flags for a user
|
|
2464
|
-
* const allFlags = await client.getAllFlags('user_123')
|
|
2465
|
-
* console.log('User flags:', allFlags)
|
|
2466
|
-
* // Output: { 'flag-1': 'variant-a', 'flag-2': false, 'flag-3': 'variant-b' }
|
|
2467
|
-
* ```
|
|
2468
|
-
*
|
|
2469
|
-
* @example
|
|
2470
|
-
* ```ts
|
|
2471
|
-
* // With specific flag keys
|
|
2472
|
-
* const specificFlags = await client.getAllFlags('user_123', {
|
|
2473
|
-
* flagKeys: ['flag-1', 'flag-2']
|
|
2474
|
-
* })
|
|
2475
|
-
* ```
|
|
2476
|
-
*
|
|
2477
|
-
* @example
|
|
2478
|
-
* ```ts
|
|
2479
|
-
* // With groups and properties
|
|
2480
|
-
* const orgFlags = await client.getAllFlags('user_123', {
|
|
2481
|
-
* groups: { organization: 'acme-corp' },
|
|
2482
|
-
* personProperties: { plan: 'enterprise' }
|
|
2483
|
-
* })
|
|
2484
|
-
* ```
|
|
2485
|
-
*
|
|
2486
|
-
* {@label Feature flags}
|
|
2487
|
-
*
|
|
2488
|
-
* @param distinctId - The user's distinct ID
|
|
2489
|
-
* @param options - Optional configuration for flag evaluation
|
|
2490
|
-
* @returns Promise that resolves to a record of flag keys and their values
|
|
2491
|
-
*/
|
|
2492
|
-
async getAllFlags(distinctId, options) {
|
|
2493
|
-
const response = await this.getAllFlagsAndPayloads(distinctId, options);
|
|
2494
|
-
return response.featureFlags || {};
|
|
2495
|
-
}
|
|
2496
|
-
/**
|
|
2497
|
-
* Get all feature flag values and payloads for a specific user.
|
|
2498
|
-
*
|
|
2499
|
-
* @example
|
|
2500
|
-
* ```ts
|
|
2501
|
-
* // Get all flags and payloads for a user
|
|
2502
|
-
* const result = await client.getAllFlagsAndPayloads('user_123')
|
|
2503
|
-
* console.log('Flags:', result.featureFlags)
|
|
2504
|
-
* console.log('Payloads:', result.featureFlagPayloads)
|
|
2505
|
-
* ```
|
|
2506
|
-
*
|
|
2507
|
-
* @example
|
|
2508
|
-
* ```ts
|
|
2509
|
-
* // With specific flag keys
|
|
2510
|
-
* const result = await client.getAllFlagsAndPayloads('user_123', {
|
|
2511
|
-
* flagKeys: ['flag-1', 'flag-2']
|
|
2512
|
-
* })
|
|
2513
|
-
* ```
|
|
2514
|
-
*
|
|
2515
|
-
* @example
|
|
2516
|
-
* ```ts
|
|
2517
|
-
* // Only evaluate locally
|
|
2518
|
-
* const result = await client.getAllFlagsAndPayloads('user_123', {
|
|
2519
|
-
* onlyEvaluateLocally: true
|
|
2520
|
-
* })
|
|
2521
|
-
* ```
|
|
2522
|
-
*
|
|
2523
|
-
* {@label Feature flags}
|
|
2524
|
-
*
|
|
2525
|
-
* @param distinctId - The user's distinct ID
|
|
2526
|
-
* @param options - Optional configuration for flag evaluation
|
|
2527
|
-
* @returns Promise that resolves to flags and payloads
|
|
2528
|
-
*/
|
|
2529
|
-
async getAllFlagsAndPayloads(distinctId, options) {
|
|
2530
|
-
const {
|
|
2531
|
-
groups,
|
|
2532
|
-
disableGeoip,
|
|
2533
|
-
flagKeys
|
|
2534
|
-
} = options || {};
|
|
2535
|
-
let {
|
|
2536
|
-
onlyEvaluateLocally,
|
|
2537
|
-
personProperties,
|
|
2538
|
-
groupProperties
|
|
2539
|
-
} = options || {};
|
|
2540
|
-
const adjustedProperties = this.addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties);
|
|
2541
|
-
personProperties = adjustedProperties.allPersonProperties;
|
|
2542
|
-
groupProperties = adjustedProperties.allGroupProperties;
|
|
2543
|
-
// set defaults
|
|
2544
|
-
if (onlyEvaluateLocally == undefined) {
|
|
2545
|
-
onlyEvaluateLocally = false;
|
|
2546
|
-
}
|
|
2547
|
-
const localEvaluationResult = await this.featureFlagsPoller?.getAllFlagsAndPayloads(distinctId, groups, personProperties, groupProperties, flagKeys);
|
|
2548
|
-
let featureFlags = {};
|
|
2549
|
-
let featureFlagPayloads = {};
|
|
2550
|
-
let fallbackToFlags = true;
|
|
2551
|
-
if (localEvaluationResult) {
|
|
2552
|
-
featureFlags = localEvaluationResult.response;
|
|
2553
|
-
featureFlagPayloads = localEvaluationResult.payloads;
|
|
2554
|
-
fallbackToFlags = localEvaluationResult.fallbackToFlags;
|
|
2555
|
-
}
|
|
2556
|
-
if (fallbackToFlags && !onlyEvaluateLocally) {
|
|
2557
|
-
const remoteEvaluationResult = await super.getFeatureFlagsAndPayloadsStateless(distinctId, groups, personProperties, groupProperties, disableGeoip, flagKeys);
|
|
2558
|
-
featureFlags = {
|
|
2559
|
-
...featureFlags,
|
|
2560
|
-
...(remoteEvaluationResult.flags || {})
|
|
2561
|
-
};
|
|
2562
|
-
featureFlagPayloads = {
|
|
2563
|
-
...featureFlagPayloads,
|
|
2564
|
-
...(remoteEvaluationResult.payloads || {})
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
return {
|
|
2568
|
-
featureFlags,
|
|
2569
|
-
featureFlagPayloads
|
|
2570
|
-
};
|
|
2571
|
-
}
|
|
2572
|
-
/**
|
|
2573
|
-
* Create or update a group and its properties.
|
|
2574
|
-
*
|
|
2575
|
-
* @example
|
|
2576
|
-
* ```ts
|
|
2577
|
-
* // Create a company group
|
|
2578
|
-
* client.groupIdentify({
|
|
2579
|
-
* groupType: 'company',
|
|
2580
|
-
* groupKey: 'acme-corp',
|
|
2581
|
-
* properties: {
|
|
2582
|
-
* name: 'Acme Corporation',
|
|
2583
|
-
* industry: 'Technology',
|
|
2584
|
-
* employee_count: 500
|
|
2585
|
-
* },
|
|
2586
|
-
* distinctId: 'user_123'
|
|
2587
|
-
* })
|
|
2588
|
-
* ```
|
|
2589
|
-
*
|
|
2590
|
-
* @example
|
|
2591
|
-
* ```ts
|
|
2592
|
-
* // Update organization properties
|
|
2593
|
-
* client.groupIdentify({
|
|
2594
|
-
* groupType: 'organization',
|
|
2595
|
-
* groupKey: 'org-456',
|
|
2596
|
-
* properties: {
|
|
2597
|
-
* plan: 'enterprise',
|
|
2598
|
-
* region: 'US-West'
|
|
2599
|
-
* }
|
|
2600
|
-
* })
|
|
2601
|
-
* ```
|
|
2602
|
-
*
|
|
2603
|
-
* {@label Identification}
|
|
2604
|
-
*
|
|
2605
|
-
* @param data - The group identify data
|
|
2606
|
-
*/
|
|
2607
|
-
groupIdentify({
|
|
2608
|
-
groupType,
|
|
2609
|
-
groupKey,
|
|
2610
|
-
properties,
|
|
2611
|
-
distinctId,
|
|
2612
|
-
disableGeoip
|
|
2613
|
-
}) {
|
|
2614
|
-
super.groupIdentifyStateless(groupType, groupKey, properties, {
|
|
2615
|
-
disableGeoip
|
|
2616
|
-
}, distinctId);
|
|
2617
|
-
}
|
|
2618
|
-
/**
|
|
2619
|
-
* Reload feature flag definitions from the server for local evaluation.
|
|
2620
|
-
*
|
|
2621
|
-
* @example
|
|
2622
|
-
* ```ts
|
|
2623
|
-
* // Force reload of feature flags
|
|
2624
|
-
* await client.reloadFeatureFlags()
|
|
2625
|
-
* console.log('Feature flags reloaded')
|
|
2626
|
-
* ```
|
|
2627
|
-
*
|
|
2628
|
-
* @example
|
|
2629
|
-
* ```ts
|
|
2630
|
-
* // Reload before checking a specific flag
|
|
2631
|
-
* await client.reloadFeatureFlags()
|
|
2632
|
-
* const flag = await client.getFeatureFlag('flag-key', 'user_123')
|
|
2633
|
-
* ```
|
|
2634
|
-
*
|
|
2635
|
-
* {@label Feature flags}
|
|
2636
|
-
*
|
|
2637
|
-
* @returns Promise that resolves when flags are reloaded
|
|
2638
|
-
*/
|
|
2639
|
-
async reloadFeatureFlags() {
|
|
2640
|
-
await this.featureFlagsPoller?.loadFeatureFlags(true);
|
|
2641
|
-
}
|
|
2642
|
-
/**
|
|
2643
|
-
* Shutdown the PostHog client gracefully.
|
|
2644
|
-
*
|
|
2645
|
-
* @example
|
|
2646
|
-
* ```ts
|
|
2647
|
-
* // Shutdown with default timeout
|
|
2648
|
-
* await client._shutdown()
|
|
2649
|
-
* ```
|
|
2650
|
-
*
|
|
2651
|
-
* @example
|
|
2652
|
-
* ```ts
|
|
2653
|
-
* // Shutdown with custom timeout
|
|
2654
|
-
* await client._shutdown(5000) // 5 seconds
|
|
2655
|
-
* ```
|
|
2656
|
-
*
|
|
2657
|
-
* {@label Shutdown}
|
|
2658
|
-
*
|
|
2659
|
-
* @param shutdownTimeoutMs - Timeout in milliseconds for shutdown
|
|
2660
|
-
* @returns Promise that resolves when shutdown is complete
|
|
2661
|
-
*/
|
|
2662
|
-
async _shutdown(shutdownTimeoutMs) {
|
|
2663
|
-
this.featureFlagsPoller?.stopPoller();
|
|
2664
|
-
this.errorTracking.shutdown();
|
|
2665
|
-
return super._shutdown(shutdownTimeoutMs);
|
|
2666
|
-
}
|
|
2667
|
-
async _requestRemoteConfigPayload(flagKey) {
|
|
2668
|
-
if (!this.options.personalApiKey) {
|
|
2669
|
-
return undefined;
|
|
2670
|
-
}
|
|
2671
|
-
const url = `${this.host}/api/projects/@current/feature_flags/${flagKey}/remote_config?token=${encodeURIComponent(this.apiKey)}`;
|
|
2672
|
-
const options = {
|
|
2673
|
-
method: 'GET',
|
|
2674
|
-
headers: {
|
|
2675
|
-
...this.getCustomHeaders(),
|
|
2676
|
-
'Content-Type': 'application/json',
|
|
2677
|
-
Authorization: `Bearer ${this.options.personalApiKey}`
|
|
2678
|
-
}
|
|
2679
|
-
};
|
|
2680
|
-
let abortTimeout = null;
|
|
2681
|
-
if (this.options.requestTimeout && typeof this.options.requestTimeout === 'number') {
|
|
2682
|
-
const controller = new AbortController();
|
|
2683
|
-
abortTimeout = core.safeSetTimeout(() => {
|
|
2684
|
-
controller.abort();
|
|
2685
|
-
}, this.options.requestTimeout);
|
|
2686
|
-
options.signal = controller.signal;
|
|
2687
|
-
}
|
|
2688
|
-
try {
|
|
2689
|
-
return await this.fetch(url, options);
|
|
2690
|
-
} catch (error) {
|
|
2691
|
-
this._events.emit('error', error);
|
|
2692
|
-
return undefined;
|
|
2693
|
-
} finally {
|
|
2694
|
-
if (abortTimeout) {
|
|
2695
|
-
clearTimeout(abortTimeout);
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
}
|
|
2699
|
-
extractPropertiesFromEvent(eventProperties, groups) {
|
|
2700
|
-
if (!eventProperties) {
|
|
2701
|
-
return {
|
|
2702
|
-
personProperties: {},
|
|
2703
|
-
groupProperties: {}
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
const personProperties = {};
|
|
2707
|
-
const groupProperties = {};
|
|
2708
|
-
for (const [key, value] of Object.entries(eventProperties)) {
|
|
2709
|
-
// If the value is a plain object and the key exists in groups, treat it as group properties
|
|
2710
|
-
if (isPlainObject(value) && groups && key in groups) {
|
|
2711
|
-
const groupProps = {};
|
|
2712
|
-
for (const [groupKey, groupValue] of Object.entries(value)) {
|
|
2713
|
-
groupProps[String(groupKey)] = String(groupValue);
|
|
2714
|
-
}
|
|
2715
|
-
groupProperties[String(key)] = groupProps;
|
|
2716
|
-
} else {
|
|
2717
|
-
// Otherwise treat as person property
|
|
2718
|
-
personProperties[String(key)] = String(value);
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2721
|
-
return {
|
|
2722
|
-
personProperties,
|
|
2723
|
-
groupProperties
|
|
2724
|
-
};
|
|
2725
|
-
}
|
|
2726
|
-
async getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions) {
|
|
2727
|
-
// Use properties directly from options if they exist
|
|
2728
|
-
const finalPersonProperties = sendFeatureFlagsOptions?.personProperties || {};
|
|
2729
|
-
const finalGroupProperties = sendFeatureFlagsOptions?.groupProperties || {};
|
|
2730
|
-
const flagKeys = sendFeatureFlagsOptions?.flagKeys;
|
|
2731
|
-
// Check if we should only evaluate locally
|
|
2732
|
-
const onlyEvaluateLocally = sendFeatureFlagsOptions?.onlyEvaluateLocally ?? false;
|
|
2733
|
-
// If onlyEvaluateLocally is true, only use local evaluation
|
|
2734
|
-
if (onlyEvaluateLocally) {
|
|
2735
|
-
if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
|
|
2736
|
-
const groupsWithStringValues = {};
|
|
2737
|
-
for (const [key, value] of Object.entries(groups || {})) {
|
|
2738
|
-
groupsWithStringValues[key] = String(value);
|
|
2739
|
-
}
|
|
2740
|
-
return await this.getAllFlags(distinctId, {
|
|
2741
|
-
groups: groupsWithStringValues,
|
|
2742
|
-
personProperties: finalPersonProperties,
|
|
2743
|
-
groupProperties: finalGroupProperties,
|
|
2744
|
-
disableGeoip,
|
|
2745
|
-
onlyEvaluateLocally: true,
|
|
2746
|
-
flagKeys
|
|
2747
|
-
});
|
|
2748
|
-
} else {
|
|
2749
|
-
// If onlyEvaluateLocally is true but we don't have local flags, return empty
|
|
2750
|
-
return {};
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
// Prefer local evaluation if available (default behavior; I'd rather not penalize users who haven't updated to the new API but still want to use local evaluation)
|
|
2754
|
-
if ((this.featureFlagsPoller?.featureFlags?.length || 0) > 0) {
|
|
2755
|
-
const groupsWithStringValues = {};
|
|
2756
|
-
for (const [key, value] of Object.entries(groups || {})) {
|
|
2757
|
-
groupsWithStringValues[key] = String(value);
|
|
2758
|
-
}
|
|
2759
|
-
return await this.getAllFlags(distinctId, {
|
|
2760
|
-
groups: groupsWithStringValues,
|
|
2761
|
-
personProperties: finalPersonProperties,
|
|
2762
|
-
groupProperties: finalGroupProperties,
|
|
2763
|
-
disableGeoip,
|
|
2764
|
-
onlyEvaluateLocally: true,
|
|
2765
|
-
flagKeys
|
|
2766
|
-
});
|
|
2767
|
-
}
|
|
2768
|
-
// Fall back to remote evaluation if local evaluation is not available
|
|
2769
|
-
return (await super.getFeatureFlagsStateless(distinctId, groups, finalPersonProperties, finalGroupProperties, disableGeoip)).flags;
|
|
2770
|
-
}
|
|
2771
|
-
addLocalPersonAndGroupProperties(distinctId, groups, personProperties, groupProperties) {
|
|
2772
|
-
const allPersonProperties = {
|
|
2773
|
-
distinct_id: distinctId,
|
|
2774
|
-
...(personProperties || {})
|
|
2775
|
-
};
|
|
2776
|
-
const allGroupProperties = {};
|
|
2777
|
-
if (groups) {
|
|
2778
|
-
for (const groupName of Object.keys(groups)) {
|
|
2779
|
-
allGroupProperties[groupName] = {
|
|
2780
|
-
$group_key: groups[groupName],
|
|
2781
|
-
...(groupProperties?.[groupName] || {})
|
|
2782
|
-
};
|
|
2783
|
-
}
|
|
2784
|
-
}
|
|
2785
|
-
return {
|
|
2786
|
-
allPersonProperties,
|
|
2787
|
-
allGroupProperties
|
|
2788
|
-
};
|
|
2789
|
-
}
|
|
2790
|
-
/**
|
|
2791
|
-
* Capture an error exception as an event.
|
|
2792
|
-
*
|
|
2793
|
-
* @example
|
|
2794
|
-
* ```ts
|
|
2795
|
-
* // Capture an error with user ID
|
|
2796
|
-
* try {
|
|
2797
|
-
* // Some risky operation
|
|
2798
|
-
* riskyOperation()
|
|
2799
|
-
* } catch (error) {
|
|
2800
|
-
* client.captureException(error, 'user_123')
|
|
2801
|
-
* }
|
|
2802
|
-
* ```
|
|
2803
|
-
*
|
|
2804
|
-
* @example
|
|
2805
|
-
* ```ts
|
|
2806
|
-
* // Capture with additional properties
|
|
2807
|
-
* try {
|
|
2808
|
-
* apiCall()
|
|
2809
|
-
* } catch (error) {
|
|
2810
|
-
* client.captureException(error, 'user_123', {
|
|
2811
|
-
* endpoint: '/api/users',
|
|
2812
|
-
* method: 'POST',
|
|
2813
|
-
* status_code: 500
|
|
2814
|
-
* })
|
|
2815
|
-
* }
|
|
2816
|
-
* ```
|
|
2817
|
-
*
|
|
2818
|
-
* {@label Error tracking}
|
|
2819
|
-
*
|
|
2820
|
-
* @param error - The error to capture
|
|
2821
|
-
* @param distinctId - Optional user distinct ID
|
|
2822
|
-
* @param additionalProperties - Optional additional properties to include
|
|
2823
|
-
*/
|
|
2824
|
-
captureException(error, distinctId, additionalProperties) {
|
|
2825
|
-
const syntheticException = new Error('PostHog syntheticException');
|
|
2826
|
-
this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
|
|
2827
|
-
syntheticException
|
|
2828
|
-
}, distinctId, additionalProperties).then(msg => this.capture(msg)));
|
|
2829
|
-
}
|
|
2830
|
-
/**
|
|
2831
|
-
* Capture an error exception as an event immediately (synchronously).
|
|
2832
|
-
*
|
|
2833
|
-
* @example
|
|
2834
|
-
* ```ts
|
|
2835
|
-
* // Capture an error immediately with user ID
|
|
2836
|
-
* try {
|
|
2837
|
-
* // Some risky operation
|
|
2838
|
-
* riskyOperation()
|
|
2839
|
-
* } catch (error) {
|
|
2840
|
-
* await client.captureExceptionImmediate(error, 'user_123')
|
|
2841
|
-
* }
|
|
2842
|
-
* ```
|
|
2843
|
-
*
|
|
2844
|
-
* @example
|
|
2845
|
-
* ```ts
|
|
2846
|
-
* // Capture with additional properties
|
|
2847
|
-
* try {
|
|
2848
|
-
* apiCall()
|
|
2849
|
-
* } catch (error) {
|
|
2850
|
-
* await client.captureExceptionImmediate(error, 'user_123', {
|
|
2851
|
-
* endpoint: '/api/users',
|
|
2852
|
-
* method: 'POST',
|
|
2853
|
-
* status_code: 500
|
|
2854
|
-
* })
|
|
2855
|
-
* }
|
|
2856
|
-
* ```
|
|
2857
|
-
*
|
|
2858
|
-
* {@label Error tracking}
|
|
2859
|
-
*
|
|
2860
|
-
* @param error - The error to capture
|
|
2861
|
-
* @param distinctId - Optional user distinct ID
|
|
2862
|
-
* @param additionalProperties - Optional additional properties to include
|
|
2863
|
-
* @returns Promise that resolves when the error is captured
|
|
2864
|
-
*/
|
|
2865
|
-
async captureExceptionImmediate(error, distinctId, additionalProperties) {
|
|
2866
|
-
const syntheticException = new Error('PostHog syntheticException');
|
|
2867
|
-
this.addPendingPromise(ErrorTracking.buildEventMessage(error, {
|
|
2868
|
-
syntheticException
|
|
2869
|
-
}, distinctId, additionalProperties).then(msg => this.captureImmediate(msg)));
|
|
2870
|
-
}
|
|
2871
|
-
async prepareEventMessage(props) {
|
|
2872
|
-
const {
|
|
2873
|
-
distinctId,
|
|
2874
|
-
event,
|
|
2875
|
-
properties,
|
|
2876
|
-
groups,
|
|
2877
|
-
sendFeatureFlags,
|
|
2878
|
-
timestamp,
|
|
2879
|
-
disableGeoip,
|
|
2880
|
-
uuid
|
|
2881
|
-
} = props;
|
|
2882
|
-
// Run before_send if configured
|
|
2883
|
-
const eventMessage = this._runBeforeSend({
|
|
2884
|
-
distinctId,
|
|
2885
|
-
event,
|
|
2886
|
-
properties,
|
|
2887
|
-
groups,
|
|
2888
|
-
sendFeatureFlags,
|
|
2889
|
-
timestamp,
|
|
2890
|
-
disableGeoip,
|
|
2891
|
-
uuid
|
|
2892
|
-
});
|
|
2893
|
-
if (!eventMessage) {
|
|
2894
|
-
return Promise.reject(null);
|
|
2895
|
-
}
|
|
2896
|
-
// :TRICKY: If we flush, or need to shut down, to not lose events we want this promise to resolve before we flush
|
|
2897
|
-
const eventProperties = await Promise.resolve().then(async () => {
|
|
2898
|
-
if (sendFeatureFlags) {
|
|
2899
|
-
// If we are sending feature flags, we evaluate them locally if the user prefers it, otherwise we fall back to remote evaluation
|
|
2900
|
-
const sendFeatureFlagsOptions = typeof sendFeatureFlags === 'object' ? sendFeatureFlags : undefined;
|
|
2901
|
-
return await this.getFeatureFlagsForEvent(distinctId, groups, disableGeoip, sendFeatureFlagsOptions);
|
|
2902
|
-
}
|
|
2903
|
-
if (event === '$feature_flag_called') {
|
|
2904
|
-
// If we're capturing a $feature_flag_called event, we don't want to enrich the event with cached flags that may be out of date.
|
|
2905
|
-
return {};
|
|
2906
|
-
}
|
|
2907
|
-
return {};
|
|
2908
|
-
}).then(flags => {
|
|
2909
|
-
// Derive the relevant flag properties to add
|
|
2910
|
-
const additionalProperties = {};
|
|
2911
|
-
if (flags) {
|
|
2912
|
-
for (const [feature, variant] of Object.entries(flags)) {
|
|
2913
|
-
additionalProperties[`$feature/${feature}`] = variant;
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
const activeFlags = Object.keys(flags || {}).filter(flag => flags?.[flag] !== false).sort();
|
|
2917
|
-
if (activeFlags.length > 0) {
|
|
2918
|
-
additionalProperties['$active_feature_flags'] = activeFlags;
|
|
2919
|
-
}
|
|
2920
|
-
return additionalProperties;
|
|
2921
|
-
}).catch(() => {
|
|
2922
|
-
// Something went wrong getting the flag info - we should capture the event anyways
|
|
2923
|
-
return {};
|
|
2924
|
-
}).then(additionalProperties => {
|
|
2925
|
-
// No matter what - capture the event
|
|
2926
|
-
const props = {
|
|
2927
|
-
...additionalProperties,
|
|
2928
|
-
...(eventMessage.properties || {}),
|
|
2929
|
-
$groups: eventMessage.groups || groups
|
|
2930
|
-
};
|
|
2931
|
-
return props;
|
|
2932
|
-
});
|
|
2933
|
-
return {
|
|
2934
|
-
distinctId: eventMessage.distinctId,
|
|
2935
|
-
event: eventMessage.event,
|
|
2936
|
-
properties: eventProperties,
|
|
2937
|
-
options: {
|
|
2938
|
-
timestamp: eventMessage.timestamp,
|
|
2939
|
-
disableGeoip: eventMessage.disableGeoip,
|
|
2940
|
-
uuid: eventMessage.uuid
|
|
2941
|
-
}
|
|
2942
|
-
};
|
|
2943
|
-
}
|
|
2944
|
-
_runBeforeSend(eventMessage) {
|
|
2945
|
-
const beforeSend = this.options.before_send;
|
|
2946
|
-
if (!beforeSend) {
|
|
2947
|
-
return eventMessage;
|
|
2948
|
-
}
|
|
2949
|
-
const fns = Array.isArray(beforeSend) ? beforeSend : [beforeSend];
|
|
2950
|
-
let result = eventMessage;
|
|
2951
|
-
for (const fn of fns) {
|
|
2952
|
-
result = fn(result);
|
|
2953
|
-
if (!result) {
|
|
2954
|
-
this.logMsgIfDebug(() => console.info(`Event '${eventMessage.event}' was rejected in beforeSend function`));
|
|
2955
|
-
return null;
|
|
2956
|
-
}
|
|
2957
|
-
if (!result.properties || Object.keys(result.properties).length === 0) {
|
|
2958
|
-
const message = `Event '${result.event}' has no properties after beforeSend function, this is likely an error.`;
|
|
2959
|
-
this.logMsgIfDebug(() => console.warn(message));
|
|
2960
|
-
}
|
|
2961
|
-
}
|
|
2962
|
-
return result;
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2967
|
-
// Licensed under the MIT License
|
|
2968
|
-
// This was originally forked from https://github.com/csnover/TraceKit, and was largely
|
|
2969
|
-
// re-written as part of raven - js.
|
|
2970
|
-
//
|
|
2971
|
-
// This code was later copied to the JavaScript mono - repo and further modified and
|
|
2972
|
-
// refactored over the years.
|
|
2973
|
-
// Copyright (c) 2013 Onur Can Cakmak onur.cakmak@gmail.com and all TraceKit contributors.
|
|
2974
|
-
//
|
|
2975
|
-
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
2976
|
-
// software and associated documentation files(the 'Software'), to deal in the Software
|
|
2977
|
-
// without restriction, including without limitation the rights to use, copy, modify,
|
|
2978
|
-
// merge, publish, distribute, sublicense, and / or sell copies of the Software, and to
|
|
2979
|
-
// permit persons to whom the Software is furnished to do so, subject to the following
|
|
2980
|
-
// conditions:
|
|
2981
|
-
//
|
|
2982
|
-
// The above copyright notice and this permission notice shall be included in all copies
|
|
2983
|
-
// or substantial portions of the Software.
|
|
2984
|
-
//
|
|
2985
|
-
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
2986
|
-
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
2987
|
-
// PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
2988
|
-
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
2989
|
-
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
2990
|
-
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
2991
|
-
const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/;
|
|
2992
|
-
const STACKTRACE_FRAME_LIMIT = 50;
|
|
2993
|
-
const UNKNOWN_FUNCTION = '?';
|
|
2994
|
-
/** Node Stack line parser */
|
|
2995
|
-
function node(getModule) {
|
|
2996
|
-
const FILENAME_MATCH = /^\s*[-]{4,}$/;
|
|
2997
|
-
const FULL_MATCH = /at (?:async )?(?:(.+?)\s+\()?(?:(.+):(\d+):(\d+)?|([^)]+))\)?/;
|
|
2998
|
-
return line => {
|
|
2999
|
-
const lineMatch = line.match(FULL_MATCH);
|
|
3000
|
-
if (lineMatch) {
|
|
3001
|
-
let object;
|
|
3002
|
-
let method;
|
|
3003
|
-
let functionName;
|
|
3004
|
-
let typeName;
|
|
3005
|
-
let methodName;
|
|
3006
|
-
if (lineMatch[1]) {
|
|
3007
|
-
functionName = lineMatch[1];
|
|
3008
|
-
let methodStart = functionName.lastIndexOf('.');
|
|
3009
|
-
if (functionName[methodStart - 1] === '.') {
|
|
3010
|
-
methodStart--;
|
|
3011
|
-
}
|
|
3012
|
-
if (methodStart > 0) {
|
|
3013
|
-
object = functionName.slice(0, methodStart);
|
|
3014
|
-
method = functionName.slice(methodStart + 1);
|
|
3015
|
-
const objectEnd = object.indexOf('.Module');
|
|
3016
|
-
if (objectEnd > 0) {
|
|
3017
|
-
functionName = functionName.slice(objectEnd + 1);
|
|
3018
|
-
object = object.slice(0, objectEnd);
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
typeName = undefined;
|
|
3022
|
-
}
|
|
3023
|
-
if (method) {
|
|
3024
|
-
typeName = object;
|
|
3025
|
-
methodName = method;
|
|
3026
|
-
}
|
|
3027
|
-
if (method === '<anonymous>') {
|
|
3028
|
-
methodName = undefined;
|
|
3029
|
-
functionName = undefined;
|
|
3030
|
-
}
|
|
3031
|
-
if (functionName === undefined) {
|
|
3032
|
-
methodName = methodName || UNKNOWN_FUNCTION;
|
|
3033
|
-
functionName = typeName ? `${typeName}.${methodName}` : methodName;
|
|
3034
|
-
}
|
|
3035
|
-
let filename = lineMatch[2]?.startsWith('file://') ? lineMatch[2].slice(7) : lineMatch[2];
|
|
3036
|
-
const isNative = lineMatch[5] === 'native';
|
|
3037
|
-
// If it's a Windows path, trim the leading slash so that `/C:/foo` becomes `C:/foo`
|
|
3038
|
-
if (filename?.match(/\/[A-Z]:/)) {
|
|
3039
|
-
filename = filename.slice(1);
|
|
3040
|
-
}
|
|
3041
|
-
if (!filename && lineMatch[5] && !isNative) {
|
|
3042
|
-
filename = lineMatch[5];
|
|
3043
|
-
}
|
|
3044
|
-
return {
|
|
3045
|
-
filename: filename ? decodeURI(filename) : undefined,
|
|
3046
|
-
module: undefined,
|
|
3047
|
-
function: functionName,
|
|
3048
|
-
lineno: _parseIntOrUndefined(lineMatch[3]),
|
|
3049
|
-
colno: _parseIntOrUndefined(lineMatch[4]),
|
|
3050
|
-
in_app: filenameIsInApp(filename || '', isNative),
|
|
3051
|
-
platform: 'node:javascript'
|
|
3052
|
-
};
|
|
3053
|
-
}
|
|
3054
|
-
if (line.match(FILENAME_MATCH)) {
|
|
3055
|
-
return {
|
|
3056
|
-
filename: line,
|
|
3057
|
-
platform: 'node:javascript'
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
3060
|
-
return undefined;
|
|
3061
|
-
};
|
|
3062
|
-
}
|
|
3063
|
-
/**
|
|
3064
|
-
* Does this filename look like it's part of the app code?
|
|
3065
|
-
*/
|
|
3066
|
-
function filenameIsInApp(filename, isNative = false) {
|
|
3067
|
-
const isInternal = isNative || filename &&
|
|
3068
|
-
// It's not internal if it's an absolute linux path
|
|
3069
|
-
!filename.startsWith('/') &&
|
|
3070
|
-
// It's not internal if it's an absolute windows path
|
|
3071
|
-
!filename.match(/^[A-Z]:/) &&
|
|
3072
|
-
// It's not internal if the path is starting with a dot
|
|
3073
|
-
!filename.startsWith('.') &&
|
|
3074
|
-
// It's not internal if the frame has a protocol. In node, this is usually the case if the file got pre-processed with a bundler like webpack
|
|
3075
|
-
!filename.match(/^[a-zA-Z]([a-zA-Z0-9.\-+])*:\/\//); // Schema from: https://stackoverflow.com/a/3641782
|
|
3076
|
-
// in_app is all that's not an internal Node function or a module within node_modules
|
|
3077
|
-
// note that isNative appears to return true even for node core libraries
|
|
3078
|
-
// see https://github.com/getsentry/raven-node/issues/176
|
|
3079
|
-
return !isInternal && filename !== undefined && !filename.includes('node_modules/');
|
|
3080
|
-
}
|
|
3081
|
-
function _parseIntOrUndefined(input) {
|
|
3082
|
-
return parseInt(input || '', 10) || undefined;
|
|
3083
|
-
}
|
|
3084
|
-
function nodeStackLineParser(getModule) {
|
|
3085
|
-
return [90, node()];
|
|
3086
|
-
}
|
|
3087
|
-
function createStackParser(getModule) {
|
|
3088
|
-
const parsers = [nodeStackLineParser()];
|
|
3089
|
-
const sortedParsers = parsers.sort((a, b) => a[0] - b[0]).map(p => p[1]);
|
|
3090
|
-
return (stack, skipFirstLines = 0) => {
|
|
3091
|
-
const frames = [];
|
|
3092
|
-
const lines = stack.split('\n');
|
|
3093
|
-
for (let i = skipFirstLines; i < lines.length; i++) {
|
|
3094
|
-
const line = lines[i];
|
|
3095
|
-
// Ignore lines over 1kb as they are unlikely to be stack frames.
|
|
3096
|
-
if (line.length > 1024) {
|
|
3097
|
-
continue;
|
|
3098
|
-
}
|
|
3099
|
-
// https://github.com/getsentry/sentry-javascript/issues/5459
|
|
3100
|
-
// Remove webpack (error: *) wrappers
|
|
3101
|
-
const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, '$1') : line;
|
|
3102
|
-
// https://github.com/getsentry/sentry-javascript/issues/7813
|
|
3103
|
-
// Skip Error: lines
|
|
3104
|
-
if (cleanedLine.match(/\S*Error: /)) {
|
|
3105
|
-
continue;
|
|
3106
|
-
}
|
|
3107
|
-
for (const parser of sortedParsers) {
|
|
3108
|
-
const frame = parser(cleanedLine);
|
|
3109
|
-
if (frame) {
|
|
3110
|
-
frames.push(frame);
|
|
3111
|
-
break;
|
|
3112
|
-
}
|
|
3113
|
-
}
|
|
3114
|
-
if (frames.length >= STACKTRACE_FRAME_LIMIT) {
|
|
3115
|
-
break;
|
|
3116
|
-
}
|
|
3117
|
-
}
|
|
3118
|
-
return reverseAndStripFrames(frames);
|
|
3119
|
-
};
|
|
3120
|
-
}
|
|
3121
|
-
function reverseAndStripFrames(stack) {
|
|
3122
|
-
if (!stack.length) {
|
|
3123
|
-
return [];
|
|
3124
|
-
}
|
|
3125
|
-
const localStack = Array.from(stack);
|
|
3126
|
-
localStack.reverse();
|
|
3127
|
-
return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({
|
|
3128
|
-
...frame,
|
|
3129
|
-
filename: frame.filename || getLastStackFrame(localStack).filename,
|
|
3130
|
-
function: frame.function || UNKNOWN_FUNCTION
|
|
3131
|
-
}));
|
|
3132
|
-
}
|
|
3133
|
-
function getLastStackFrame(arr) {
|
|
3134
|
-
return arr[arr.length - 1] || {};
|
|
3135
|
-
}
|
|
3136
|
-
|
|
3137
|
-
ErrorTracking.stackParser = createStackParser();
|
|
3138
|
-
ErrorTracking.frameModifiers = [];
|
|
3139
|
-
class PostHog extends PostHogBackendClient {
|
|
3140
|
-
getLibraryId() {
|
|
3141
|
-
return 'posthog-edge';
|
|
3142
|
-
}
|
|
3143
|
-
}
|
|
3144
|
-
|
|
3145
|
-
exports.PostHog = PostHog;
|
|
3146
|
-
exports.PostHogSentryIntegration = PostHogSentryIntegration;
|
|
3147
|
-
exports.createEventProcessor = createEventProcessor;
|
|
3148
|
-
exports.sentryIntegration = sentryIntegration;
|
|
3149
|
-
exports.setupExpressErrorHandler = setupExpressErrorHandler;
|
|
3150
|
-
//# sourceMappingURL=index.cjs.map
|