posthog-node 4.2.3 → 4.3.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.
@@ -1,13 +1,6 @@
1
1
  /**
2
2
  * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
3
3
  */
4
- import { type PostHog } from '../posthog-node';
5
- type _SentryEventProcessor = any;
6
- type _SentryHub = any;
7
- interface _SentryIntegration {
8
- name: string;
9
- setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void;
10
- }
11
4
  /**
12
5
  * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
13
6
  *
@@ -26,15 +19,34 @@ interface _SentryIntegration {
26
19
  * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
27
20
  * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
28
21
  * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
22
+ * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
29
23
  */
30
- export declare class PostHogSentryIntegration implements _SentryIntegration {
31
- private readonly posthog;
32
- private readonly posthogHost?;
33
- private readonly organization?;
34
- private readonly prefix?;
24
+ import { type PostHog } from '../posthog-node';
25
+ export declare const severityLevels: readonly ["fatal", "error", "warning", "log", "info", "debug"];
26
+ export declare type SeverityLevel = (typeof severityLevels)[number];
27
+ type _SentryEvent = any;
28
+ type _SentryEventProcessor = any;
29
+ type _SentryHub = any;
30
+ interface _SentryIntegration {
31
+ name: string;
32
+ processEvent(event: _SentryEvent): _SentryEvent;
33
+ }
34
+ interface _SentryIntegrationClass {
35
+ name: string;
36
+ setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void;
37
+ }
38
+ export type SentryIntegrationOptions = {
39
+ organization?: string;
40
+ projectId?: number;
41
+ prefix?: string;
42
+ severityAllowList?: SeverityLevel[] | '*';
43
+ };
44
+ export declare function createEventProcessor(_posthog: PostHog, { organization, projectId, prefix, severityAllowList }?: SentryIntegrationOptions): (event: _SentryEvent) => _SentryEvent;
45
+ export declare function sentryIntegration(_posthog: PostHog, options?: SentryIntegrationOptions): _SentryIntegration;
46
+ export declare class PostHogSentryIntegration implements _SentryIntegrationClass {
35
47
  readonly name = "posthog-node";
36
48
  static readonly POSTHOG_ID_TAG = "posthog_distinct_id";
37
- constructor(posthog: PostHog, posthogHost?: string | undefined, organization?: string | undefined, prefix?: string | undefined);
38
- setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void;
49
+ setupOnce: (addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub) => void;
50
+ constructor(_posthog: PostHog, organization?: string, prefix?: string, severityAllowList?: SeverityLevel[] | '*');
39
51
  }
40
52
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "posthog-node",
3
- "version": "4.2.3",
3
+ "version": "4.3.1",
4
4
  "description": "PostHog Node.js integration",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,8 +1,32 @@
1
1
  /**
2
2
  * @file Adapted from [posthog-js](https://github.com/PostHog/posthog-js/blob/8157df935a4d0e71d2fefef7127aa85ee51c82d1/src/extensions/sentry-integration.ts) with modifications for the Node SDK.
3
3
  */
4
+ /**
5
+ * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
6
+ *
7
+ * ### Usage
8
+ *
9
+ * Sentry.init({
10
+ * dsn: 'https://example',
11
+ * integrations: [
12
+ * new PostHogSentryIntegration(posthog)
13
+ * ]
14
+ * })
15
+ *
16
+ * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
17
+ *
18
+ * @param {Object} [posthog] The posthog object
19
+ * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
20
+ * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
21
+ * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
22
+ * @param {SeverityLevel[] | '*'} [severityAllowList] Optional: send events matching the provided levels. Use '*' to send all events (default: ['error'])
23
+ */
24
+
4
25
  import { type PostHog } from '../posthog-node'
5
26
 
27
+ export const severityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug'] as const
28
+ export declare type SeverityLevel = (typeof severityLevels)[number]
29
+
6
30
  // NOTE - we can't import from @sentry/types because it changes frequently and causes clashes
7
31
  // We only use a small subset of the types, so we can just define the integration overall and use any for the rest
8
32
 
@@ -11,115 +35,157 @@ import { type PostHog } from '../posthog-node'
11
35
  // EventProcessor as _SentryEventProcessor,
12
36
  // Exception as _SentryException,
13
37
  // Hub as _SentryHub,
14
- // Integration as _SentryIntegration,
15
38
  // Primitive as _SentryPrimitive,
39
+ // Integration as _SentryIntegration,
40
+ // IntegrationClass as _SentryIntegrationClass,
16
41
  // } from '@sentry/types'
17
42
 
18
43
  // Uncomment the above and comment the below to get type checking for development
19
44
 
20
45
  type _SentryEvent = any
21
46
  type _SentryEventProcessor = any
22
- type _SentryHub = any
23
47
  type _SentryException = any
48
+ type _SentryHub = any
24
49
  type _SentryPrimitive = any
25
50
 
26
51
  interface _SentryIntegration {
52
+ name: string
53
+ processEvent(event: _SentryEvent): _SentryEvent
54
+ }
55
+
56
+ interface _SentryIntegrationClass {
27
57
  name: string
28
58
  setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void
29
59
  }
30
60
 
31
- interface PostHogSentryExceptionProperties {
61
+ interface SentryExceptionProperties {
32
62
  $sentry_event_id?: string
33
63
  $sentry_exception?: { values?: _SentryException[] }
34
64
  $sentry_exception_message?: string
35
65
  $sentry_exception_type?: string
36
66
  $sentry_tags: { [key: string]: _SentryPrimitive }
37
67
  $sentry_url?: string
38
- $exception_type?: string
39
- $exception_message?: string
40
- $exception_personURL?: string
41
68
  }
42
69
 
43
- /**
44
- * Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
45
- *
46
- * ### Usage
47
- *
48
- * Sentry.init({
49
- * dsn: 'https://example',
50
- * integrations: [
51
- * new PostHogSentryIntegration(posthog)
52
- * ]
53
- * })
54
- *
55
- * Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
56
- *
57
- * @param {Object} [posthog] The posthog object
58
- * @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
59
- * @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
60
- * @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
61
- */
62
- export class PostHogSentryIntegration implements _SentryIntegration {
63
- public readonly name = 'posthog-node'
70
+ export type SentryIntegrationOptions = {
71
+ organization?: string
72
+ projectId?: number
73
+ prefix?: string
74
+ severityAllowList?: SeverityLevel[] | '*'
75
+ }
64
76
 
65
- public static readonly POSTHOG_ID_TAG = 'posthog_distinct_id'
77
+ const NAME = 'posthog-node'
66
78
 
67
- public constructor(
68
- private readonly posthog: PostHog,
69
- private readonly posthogHost?: string,
70
- private readonly organization?: string,
71
- private readonly prefix?: string
72
- ) {
73
- this.posthogHost = posthog.options.host ?? 'https://us.i.posthog.com'
74
- }
79
+ export function createEventProcessor(
80
+ _posthog: PostHog,
81
+ { organization, projectId, prefix, severityAllowList = ['error'] }: SentryIntegrationOptions = {}
82
+ ): (event: _SentryEvent) => _SentryEvent {
83
+ return (event) => {
84
+ const shouldProcessLevel = severityAllowList === '*' || severityAllowList.includes(event.level)
85
+ if (!shouldProcessLevel) {
86
+ return event
87
+ }
88
+ if (!event.tags) {
89
+ event.tags = {}
90
+ }
91
+
92
+ // Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
93
+ const userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG]
94
+ if (userId === undefined) {
95
+ // 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.
96
+ return event
97
+ }
75
98
 
76
- public setupOnce(
77
- addGlobalEventProcessor: (callback: _SentryEventProcessor) => void,
78
- getCurrentHub: () => _SentryHub
79
- ): void {
80
- addGlobalEventProcessor((event: _SentryEvent): _SentryEvent => {
81
- if (event.exception?.values === undefined || event.exception.values.length === 0) {
82
- return event
83
- }
99
+ const uiHost = _posthog.options.host ?? 'https://us.i.posthog.com'
100
+ const personUrl = new URL(`/project/${_posthog.apiKey}/person/${userId}`, uiHost).toString()
84
101
 
85
- if (!event.tags) {
86
- event.tags = {}
87
- }
102
+ event.tags['PostHog Person URL'] = personUrl
88
103
 
89
- const sentry = getCurrentHub()
104
+ const exceptions: _SentryException[] = event.exception?.values || []
90
105
 
91
- // Get the PostHog user ID from a specific tag, which users can set on their Sentry scope as they need.
92
- const userId = event.tags[PostHogSentryIntegration.POSTHOG_ID_TAG]
93
- if (userId === undefined) {
94
- // 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.
95
- return event
106
+ exceptions.map((exception) => {
107
+ if (exception.stacktrace) {
108
+ exception.stacktrace.type = 'raw'
96
109
  }
110
+ })
97
111
 
98
- event.tags['PostHog Person URL'] = new URL(`/person/${userId}`, this.posthogHost).toString()
99
-
100
- const properties: PostHogSentryExceptionProperties = {
101
- // PostHog Exception Properties
102
- $exception_message: event.exception.values[0]?.value,
103
- $exception_type: event.exception.values[0]?.type,
104
- $exception_personURL: event.tags['PostHog Person URL'],
105
- // Sentry Exception Properties
106
- $sentry_event_id: event.event_id,
107
- $sentry_exception: event.exception,
108
- $sentry_exception_message: event.exception.values[0]?.value,
109
- $sentry_exception_type: event.exception.values[0]?.type,
110
- $sentry_tags: event.tags,
111
- }
112
+ const properties: SentryExceptionProperties & {
113
+ // two properties added to match any exception auto-capture
114
+ // added manually to avoid any dependency on the lazily loaded content
115
+ $exception_message: any
116
+ $exception_type: any
117
+ $exception_list: any
118
+ $exception_personURL: string
119
+ $exception_level: SeverityLevel
120
+ } = {
121
+ // PostHog Exception Properties,
122
+ $exception_message: exceptions[0]?.value || event.message,
123
+ $exception_type: exceptions[0]?.type,
124
+ $exception_personURL: personUrl,
125
+ $exception_level: event.level,
126
+ $exception_list: exceptions,
127
+ // Sentry Exception Properties
128
+ $sentry_event_id: event.event_id,
129
+ $sentry_exception: event.exception,
130
+ $sentry_exception_message: exceptions[0]?.value || event.message,
131
+ $sentry_exception_type: exceptions[0]?.type,
132
+ $sentry_tags: event.tags,
133
+ }
134
+
135
+ if (organization && projectId) {
136
+ properties['$sentry_url'] =
137
+ (prefix || 'https://sentry.io/organizations/') +
138
+ organization +
139
+ '/issues/?project=' +
140
+ projectId +
141
+ '&query=' +
142
+ event.event_id
143
+ }
144
+
145
+ _posthog.capture({ event: '$exception', distinctId: userId, properties })
146
+
147
+ return event
148
+ }
149
+ }
112
150
 
113
- const projectId = sentry.getClient()?.getDsn()?.projectId
114
- if (this.organization !== undefined && projectId !== undefined && event.event_id !== undefined) {
115
- properties.$sentry_url = `${this.prefix ?? 'https://sentry.io/organizations'}/${
116
- this.organization
117
- }/issues/?project=${projectId}&query=${event.event_id}`
118
- }
151
+ // V8 integration - function based
152
+ export function sentryIntegration(_posthog: PostHog, options?: SentryIntegrationOptions): _SentryIntegration {
153
+ const processor = createEventProcessor(_posthog, options)
154
+ return {
155
+ name: NAME,
156
+ processEvent(event) {
157
+ return processor(event)
158
+ },
159
+ }
160
+ }
119
161
 
120
- this.posthog.capture({ event: '$exception', distinctId: userId, properties })
162
+ // V7 integration - class based
163
+ export class PostHogSentryIntegration implements _SentryIntegrationClass {
164
+ public readonly name = NAME
121
165
 
122
- return event
123
- })
166
+ public static readonly POSTHOG_ID_TAG = 'posthog_distinct_id'
167
+
168
+ public setupOnce: (
169
+ addGlobalEventProcessor: (callback: _SentryEventProcessor) => void,
170
+ getCurrentHub: () => _SentryHub
171
+ ) => void
172
+
173
+ constructor(_posthog: PostHog, organization?: string, prefix?: string, severityAllowList?: SeverityLevel[] | '*') {
174
+ // setupOnce gets called by Sentry when it intializes the plugin
175
+ this.name = NAME
176
+ this.setupOnce = function (
177
+ addGlobalEventProcessor: (callback: _SentryEventProcessor) => void,
178
+ getCurrentHub: () => _SentryHub
179
+ ) {
180
+ const projectId = getCurrentHub()?.getClient()?.getDsn()?.projectId
181
+ addGlobalEventProcessor(
182
+ createEventProcessor(_posthog, {
183
+ organization,
184
+ projectId,
185
+ prefix,
186
+ severityAllowList,
187
+ })
188
+ )
189
+ }
124
190
  }
125
191
  }
@@ -294,59 +294,80 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
294
294
  disableGeoip?: boolean
295
295
  }
296
296
  ): Promise<JsonType | undefined> {
297
- const { groups, disableGeoip } = options || {}
298
- let { onlyEvaluateLocally, sendFeatureFlagEvents, personProperties, groupProperties } = options || {}
297
+ const { groups, disableGeoip, onlyEvaluateLocally = false, personProperties, groupProperties } = options || {}
299
298
 
300
- const adjustedProperties = this.addLocalPersonAndGroupProperties(
299
+ const { allPersonProperties, allGroupProperties } = this.addLocalPersonAndGroupProperties(
301
300
  distinctId,
302
301
  groups,
303
302
  personProperties,
304
303
  groupProperties
305
304
  )
306
305
 
307
- personProperties = adjustedProperties.allPersonProperties
308
- groupProperties = adjustedProperties.allGroupProperties
309
-
310
- let response = undefined
311
-
312
- // Try to get match value locally if not provided
313
- if (!matchValue) {
306
+ if (matchValue === undefined) {
314
307
  matchValue = await this.getFeatureFlag(key, distinctId, {
315
308
  ...options,
316
309
  onlyEvaluateLocally: true,
310
+ sendFeatureFlagEvents: false,
317
311
  })
318
312
  }
319
313
 
320
- if (matchValue) {
321
- response = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue)
322
- }
314
+ let response: string | boolean | undefined
315
+ let payload: JsonType | undefined
323
316
 
324
- // set defaults
325
- if (onlyEvaluateLocally == undefined) {
326
- onlyEvaluateLocally = false
327
- }
328
- if (sendFeatureFlagEvents == undefined) {
329
- sendFeatureFlagEvents = true
317
+ if (matchValue) {
318
+ response = matchValue
319
+ payload = await this.featureFlagsPoller?.computeFeatureFlagPayloadLocally(key, matchValue)
320
+ } else {
321
+ response = undefined
322
+ payload = undefined
330
323
  }
331
324
 
332
- // set defaults
333
- if (onlyEvaluateLocally == undefined) {
334
- onlyEvaluateLocally = false
335
- }
325
+ // Determine if the payload was evaluated locally
326
+ const payloadWasLocallyEvaluated = payload !== undefined
336
327
 
337
- const payloadWasLocallyEvaluated = response !== undefined
328
+ // Fetch final flags and payloads either locally or from the remote server
329
+ let fetchedOrLocalFlags: Record<string, string | boolean> | undefined
330
+ let fetchedOrLocalPayloads: Record<string, JsonType | undefined> | undefined
338
331
 
339
- if (!payloadWasLocallyEvaluated && !onlyEvaluateLocally) {
340
- response = await super.getFeatureFlagPayloadStateless(
341
- key,
332
+ if (payloadWasLocallyEvaluated || onlyEvaluateLocally) {
333
+ if (response !== undefined) {
334
+ fetchedOrLocalFlags = { [key]: response }
335
+ fetchedOrLocalPayloads = { [key]: payload }
336
+ } else {
337
+ fetchedOrLocalFlags = {}
338
+ fetchedOrLocalPayloads = {}
339
+ }
340
+ } else {
341
+ const fetchedData = await super.getFeatureFlagsAndPayloadsStateless(
342
342
  distinctId,
343
343
  groups,
344
- personProperties,
345
- groupProperties,
344
+ allPersonProperties,
345
+ allGroupProperties,
346
346
  disableGeoip
347
347
  )
348
+ fetchedOrLocalFlags = fetchedData.flags || {}
349
+ fetchedOrLocalPayloads = fetchedData.payloads || {}
348
350
  }
349
- return response
351
+
352
+ const finalResponse = fetchedOrLocalFlags[key]
353
+ const finalPayload = fetchedOrLocalPayloads[key]
354
+ const finalLocallyEvaluated = payloadWasLocallyEvaluated
355
+
356
+ this.capture({
357
+ distinctId,
358
+ event: '$feature_flag_called',
359
+ properties: {
360
+ $feature_flag: key,
361
+ $feature_flag_response: finalResponse,
362
+ $feature_flag_payload: finalPayload,
363
+ locally_evaluated: finalLocallyEvaluated,
364
+ [`$feature/${key}`]: finalResponse,
365
+ },
366
+ groups,
367
+ disableGeoip,
368
+ })
369
+
370
+ return finalPayload
350
371
  }
351
372
 
352
373
  async isFeatureEnabled(
@@ -39,6 +39,7 @@ const createMockSentryException = (): any => ({
39
39
  server_name: 'localhost',
40
40
  timestamp: 1704203482.356,
41
41
  environment: 'production',
42
+ level: 'error',
42
43
  tags: { posthog_distinct_id: 'EXAMPLE_APP_GLOBAL' },
43
44
  breadcrumbs: [
44
45
  {
@@ -119,16 +120,25 @@ describe('PostHogSentryIntegration', () => {
119
120
  distinct_id: 'EXAMPLE_APP_GLOBAL',
120
121
  event: '$exception',
121
122
  properties: {
123
+ $exception_level: 'error',
124
+ $exception_list: [
125
+ {
126
+ mechanism: { handled: true, type: 'generic' },
127
+ stacktrace: { frames: [], type: 'raw' },
128
+ type: 'Error',
129
+ value: 'example error',
130
+ },
131
+ ],
122
132
  $exception_message: 'example error',
123
133
  $exception_type: 'Error',
124
- $exception_personURL: 'http://example.com/person/EXAMPLE_APP_GLOBAL',
134
+ $exception_personURL: 'http://example.com/project/TEST_API_KEY/person/EXAMPLE_APP_GLOBAL',
125
135
  $sentry_event_id: '80a7023ac32c47f7acb0adaed600d149',
126
136
  $sentry_exception: {
127
137
  values: [
128
138
  {
129
139
  type: 'Error',
130
140
  value: 'example error',
131
- stacktrace: { frames: [] },
141
+ stacktrace: { frames: [], type: 'raw' },
132
142
  mechanism: { type: 'generic', handled: true },
133
143
  },
134
144
  ],
@@ -137,7 +147,7 @@ describe('PostHogSentryIntegration', () => {
137
147
  $sentry_exception_type: 'Error',
138
148
  $sentry_tags: {
139
149
  posthog_distinct_id: 'EXAMPLE_APP_GLOBAL',
140
- 'PostHog Person URL': 'http://example.com/person/EXAMPLE_APP_GLOBAL',
150
+ 'PostHog Person URL': 'http://example.com/project/TEST_API_KEY/person/EXAMPLE_APP_GLOBAL',
141
151
  },
142
152
  $lib: 'posthog-node',
143
153
  $lib_version: '1.2.3',
@@ -897,13 +897,18 @@ describe('PostHog Node.js', () => {
897
897
  rollout_percentage: 100,
898
898
  },
899
899
  ],
900
+ payloads: { true: { variant: 'A' } },
900
901
  },
901
902
  },
902
903
  ],
903
904
  }
904
905
 
905
906
  mockedFetch.mockImplementation(
906
- apiImplementation({ localFlags: flags, decideFlags: { 'decide-flag': 'decide-value' } })
907
+ apiImplementation({
908
+ localFlags: flags,
909
+ decideFlags: { 'decide-flag': 'decide-value' },
910
+ decideFlagPayloads: { 'beta-feature': { variant: 'A' } },
911
+ })
907
912
  )
908
913
 
909
914
  posthog = new PostHog('TEST_API_KEY', {
@@ -945,6 +950,34 @@ describe('PostHog Node.js', () => {
945
950
  )
946
951
  mockedFetch.mockClear()
947
952
 
953
+ expect(
954
+ await posthog.getFeatureFlagPayload('beta-feature', 'some-distinct-id', undefined, {
955
+ personProperties: { region: 'USA', name: 'Aloha' },
956
+ })
957
+ ).toEqual({ variant: 'A' })
958
+
959
+ // TRICKY: There's now an extra step before events are queued, so need to wait for that to resolve
960
+ jest.runOnlyPendingTimers()
961
+ await waitForPromises()
962
+ await posthog.flush()
963
+
964
+ expect(mockedFetch).toHaveBeenCalledWith('http://example.com/batch/', expect.any(Object))
965
+
966
+ expect(getLastBatchEvents()?.[0]).toEqual(
967
+ expect.objectContaining({
968
+ distinct_id: 'some-distinct-id',
969
+ event: '$feature_flag_called',
970
+ properties: expect.objectContaining({
971
+ $feature_flag: 'beta-feature',
972
+ $feature_flag_response: true,
973
+ $feature_flag_payload: { variant: 'A' },
974
+ locally_evaluated: true,
975
+ [`$feature/${'beta-feature'}`]: true,
976
+ }),
977
+ })
978
+ )
979
+ mockedFetch.mockClear()
980
+
948
981
  // # called again for same user, shouldn't call capture again
949
982
  expect(
950
983
  await posthog.getFeatureFlag('beta-feature', 'some-distinct-id', {
@@ -1081,7 +1114,7 @@ describe('PostHog Node.js', () => {
1081
1114
  expect(mockedFetch).toHaveBeenCalledTimes(0)
1082
1115
 
1083
1116
  await expect(posthog.getFeatureFlagPayload('false-flag', '123', true)).resolves.toEqual(300)
1084
- expect(mockedFetch).toHaveBeenCalledTimes(0)
1117
+ expect(mockedFetch).toHaveBeenCalledTimes(1) // this now calls the server, because in this case the flag is not locally evaluated but we have a payload that we need to calculate
1085
1118
  })
1086
1119
 
1087
1120
  it('should not double parse json with getFeatureFlagPayloads and server eval', async () => {