posthog-node 3.2.0 → 3.3.0
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/CHANGELOG.md +8 -0
- package/index.ts +1 -0
- package/lib/index.cjs.js +116 -17
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +42 -2
- package/lib/index.esm.js +116 -18
- package/lib/index.esm.js.map +1 -1
- package/lib/posthog-node/index.d.ts +1 -0
- package/lib/posthog-node/src/extensions/sentry-integration.d.ts +40 -0
- package/lib/posthog-node/src/feature-flags.d.ts +2 -0
- package/lib/posthog-node/src/posthog-node.d.ts +1 -1
- package/package.json +6 -2
- package/src/extensions/sentry-integration.ts +125 -0
- package/src/feature-flags.ts +5 -2
- package/src/posthog-node.ts +4 -1
- package/test/extensions/sentry-integration.spec.ts +150 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
4
|
+
import { type PostHog } from '../posthog-node';
|
|
5
|
+
declare type _SentryEventProcessor = any;
|
|
6
|
+
declare type _SentryHub = any;
|
|
7
|
+
interface _SentryIntegration {
|
|
8
|
+
name: string;
|
|
9
|
+
setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Integrate Sentry with PostHog. This will add a direct link to the person in Sentry, and an $exception event in PostHog.
|
|
13
|
+
*
|
|
14
|
+
* ### Usage
|
|
15
|
+
*
|
|
16
|
+
* Sentry.init({
|
|
17
|
+
* dsn: 'https://example',
|
|
18
|
+
* integrations: [
|
|
19
|
+
* new PostHogSentryIntegration(posthog)
|
|
20
|
+
* ]
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* Sentry.setTag(PostHogSentryIntegration.POSTHOG_ID_TAG, 'some distinct id');
|
|
24
|
+
*
|
|
25
|
+
* @param {Object} [posthog] The posthog object
|
|
26
|
+
* @param {string} [organization] Optional: The Sentry organization, used to send a direct link from PostHog to Sentry
|
|
27
|
+
* @param {Number} [projectId] Optional: The Sentry project id, used to send a direct link from PostHog to Sentry
|
|
28
|
+
* @param {string} [prefix] Optional: Url of a self-hosted sentry instance (default: https://sentry.io/organizations/)
|
|
29
|
+
*/
|
|
30
|
+
export declare class PostHogSentryIntegration implements _SentryIntegration {
|
|
31
|
+
private readonly posthog;
|
|
32
|
+
private readonly posthogHost?;
|
|
33
|
+
private readonly organization?;
|
|
34
|
+
private readonly prefix?;
|
|
35
|
+
readonly name = "posthog-node";
|
|
36
|
+
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;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -14,6 +14,7 @@ declare type FeatureFlagsPollerOptions = {
|
|
|
14
14
|
pollingInterval: number;
|
|
15
15
|
timeout?: number;
|
|
16
16
|
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
17
|
+
onError?: (error: Error) => void;
|
|
17
18
|
};
|
|
18
19
|
declare class FeatureFlagsPoller {
|
|
19
20
|
pollingInterval: number;
|
|
@@ -29,6 +30,7 @@ declare class FeatureFlagsPoller {
|
|
|
29
30
|
poller?: NodeJS.Timeout;
|
|
30
31
|
fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>;
|
|
31
32
|
debugMode: boolean;
|
|
33
|
+
onError?: (error: Error) => void;
|
|
32
34
|
constructor({ pollingInterval, personalApiKey, projectApiKey, timeout, host, ...options }: FeatureFlagsPollerOptions);
|
|
33
35
|
debug(enabled?: boolean): void;
|
|
34
36
|
getFeatureFlag(key: string, distinctId: string, groups?: Record<string, string>, personProperties?: Record<string, string>, groupProperties?: Record<string, Record<string, string>>): Promise<string | boolean | undefined>;
|
|
@@ -12,7 +12,7 @@ export declare class PostHog extends PostHogCoreStateless implements PostHogNode
|
|
|
12
12
|
private _memoryStorage;
|
|
13
13
|
private featureFlagsPoller?;
|
|
14
14
|
private maxCacheSize;
|
|
15
|
-
|
|
15
|
+
readonly options: PostHogOptions;
|
|
16
16
|
distinctIdHasSentFlagCalls: Record<string, string[]>;
|
|
17
17
|
constructor(apiKey: string, options?: PostHogOptions);
|
|
18
18
|
getPersistedProperty(key: PostHogPersistedProperty): any | undefined;
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "posthog-node",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "PostHog Node.js integration",
|
|
5
|
-
"repository":
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/PostHog/posthog-js-lite.git",
|
|
8
|
+
"directory": "posthog-node"
|
|
9
|
+
},
|
|
6
10
|
"scripts": {
|
|
7
11
|
"prepublishOnly": "cd .. && yarn build"
|
|
8
12
|
},
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*/
|
|
4
|
+
import { type PostHog } from '../posthog-node'
|
|
5
|
+
|
|
6
|
+
// NOTE - we can't import from @sentry/types because it changes frequently and causes clashes
|
|
7
|
+
// We only use a small subset of the types, so we can just define the integration overall and use any for the rest
|
|
8
|
+
|
|
9
|
+
// import {
|
|
10
|
+
// Event as _SentryEvent,
|
|
11
|
+
// EventProcessor as _SentryEventProcessor,
|
|
12
|
+
// Exception as _SentryException,
|
|
13
|
+
// Hub as _SentryHub,
|
|
14
|
+
// Integration as _SentryIntegration,
|
|
15
|
+
// Primitive as _SentryPrimitive,
|
|
16
|
+
// } from '@sentry/types'
|
|
17
|
+
|
|
18
|
+
// Uncomment the above and comment the below to get type checking for development
|
|
19
|
+
|
|
20
|
+
type _SentryEvent = any
|
|
21
|
+
type _SentryEventProcessor = any
|
|
22
|
+
type _SentryHub = any
|
|
23
|
+
type _SentryException = any
|
|
24
|
+
type _SentryPrimitive = any
|
|
25
|
+
|
|
26
|
+
interface _SentryIntegration {
|
|
27
|
+
name: string
|
|
28
|
+
setupOnce(addGlobalEventProcessor: (callback: _SentryEventProcessor) => void, getCurrentHub: () => _SentryHub): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PostHogSentryExceptionProperties {
|
|
32
|
+
$sentry_event_id?: string
|
|
33
|
+
$sentry_exception?: { values?: _SentryException[] }
|
|
34
|
+
$sentry_exception_message?: string
|
|
35
|
+
$sentry_exception_type?: string
|
|
36
|
+
$sentry_tags: { [key: string]: _SentryPrimitive }
|
|
37
|
+
$sentry_url?: string
|
|
38
|
+
$exception_type?: string
|
|
39
|
+
$exception_message?: string
|
|
40
|
+
$exception_personURL?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
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'
|
|
64
|
+
|
|
65
|
+
public static readonly POSTHOG_ID_TAG = 'posthog_distinct_id'
|
|
66
|
+
|
|
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://app.posthog.com'
|
|
74
|
+
}
|
|
75
|
+
|
|
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
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!event.tags) {
|
|
86
|
+
event.tags = {}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const sentry = getCurrentHub()
|
|
90
|
+
|
|
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
|
|
96
|
+
}
|
|
97
|
+
|
|
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
|
+
|
|
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
|
+
}
|
|
119
|
+
|
|
120
|
+
this.posthog.capture({ event: '$exception', distinctId: userId, properties })
|
|
121
|
+
|
|
122
|
+
return event
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/feature-flags.ts
CHANGED
|
@@ -37,6 +37,7 @@ type FeatureFlagsPollerOptions = {
|
|
|
37
37
|
pollingInterval: number
|
|
38
38
|
timeout?: number
|
|
39
39
|
fetch?: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
40
|
+
onError?: (error: Error) => void
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
class FeatureFlagsPoller {
|
|
@@ -53,6 +54,7 @@ class FeatureFlagsPoller {
|
|
|
53
54
|
poller?: NodeJS.Timeout
|
|
54
55
|
fetch: (url: string, options: PostHogFetchOptions) => Promise<PostHogFetchResponse>
|
|
55
56
|
debugMode: boolean = false
|
|
57
|
+
onError?: (error: Error) => void
|
|
56
58
|
|
|
57
59
|
constructor({
|
|
58
60
|
pollingInterval,
|
|
@@ -75,6 +77,7 @@ class FeatureFlagsPoller {
|
|
|
75
77
|
this.poller = undefined
|
|
76
78
|
// NOTE: as any is required here as the AbortSignal typing is slightly misaligned but works just fine
|
|
77
79
|
this.fetch = options.fetch || fetch
|
|
80
|
+
this.onError = options.onError
|
|
78
81
|
|
|
79
82
|
void this.loadFeatureFlags()
|
|
80
83
|
}
|
|
@@ -177,7 +180,7 @@ class FeatureFlagsPoller {
|
|
|
177
180
|
if (e instanceof InconclusiveMatchError) {
|
|
178
181
|
// do nothing
|
|
179
182
|
} else if (e instanceof Error) {
|
|
180
|
-
|
|
183
|
+
this.onError?.(new Error(`Error computing flag locally: ${flag.key}: ${e}`))
|
|
181
184
|
}
|
|
182
185
|
fallbackToDecide = true
|
|
183
186
|
}
|
|
@@ -395,7 +398,7 @@ class FeatureFlagsPoller {
|
|
|
395
398
|
// if an error that is not an instance of ClientError is thrown
|
|
396
399
|
// we silently ignore the error when reloading feature flags
|
|
397
400
|
if (err instanceof ClientError) {
|
|
398
|
-
|
|
401
|
+
this.onError?.(err)
|
|
399
402
|
}
|
|
400
403
|
}
|
|
401
404
|
}
|
package/src/posthog-node.ts
CHANGED
|
@@ -35,7 +35,7 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
|
|
|
35
35
|
|
|
36
36
|
private featureFlagsPoller?: FeatureFlagsPoller
|
|
37
37
|
private maxCacheSize: number
|
|
38
|
-
|
|
38
|
+
public readonly options: PostHogOptions
|
|
39
39
|
|
|
40
40
|
distinctIdHasSentFlagCalls: Record<string, string[]>
|
|
41
41
|
|
|
@@ -56,6 +56,9 @@ export class PostHog extends PostHogCoreStateless implements PostHogNodeV1 {
|
|
|
56
56
|
timeout: options.requestTimeout ?? 10000, // 10 seconds
|
|
57
57
|
host: this.host,
|
|
58
58
|
fetch: options.fetch,
|
|
59
|
+
onError: (err: Error) => {
|
|
60
|
+
this._events.emit('error', err)
|
|
61
|
+
},
|
|
59
62
|
})
|
|
60
63
|
}
|
|
61
64
|
this.distinctIdHasSentFlagCalls = {}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// import { PostHog } from '../'
|
|
2
|
+
import { PostHog as PostHog } from '../../src/posthog-node'
|
|
3
|
+
import { PostHogSentryIntegration } from '../../src/extensions/sentry-integration'
|
|
4
|
+
jest.mock('../../src/fetch')
|
|
5
|
+
import fetch from '../../src/fetch'
|
|
6
|
+
|
|
7
|
+
jest.mock('../../package.json', () => ({ version: '1.2.3' }))
|
|
8
|
+
|
|
9
|
+
const mockedFetch = jest.mocked(fetch, true)
|
|
10
|
+
|
|
11
|
+
const getLastBatchEvents = (): any[] | undefined => {
|
|
12
|
+
expect(mockedFetch).toHaveBeenCalledWith('http://example.com/batch/', expect.objectContaining({ method: 'POST' }))
|
|
13
|
+
|
|
14
|
+
// reverse mock calls array to get the last call
|
|
15
|
+
const call = mockedFetch.mock.calls.reverse().find((x) => (x[0] as string).includes('/batch/'))
|
|
16
|
+
if (!call) {
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
return JSON.parse((call[1] as any).body as any).batch
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const createMockSentryException = (): any => ({
|
|
23
|
+
exception: {
|
|
24
|
+
values: [
|
|
25
|
+
{
|
|
26
|
+
type: 'Error',
|
|
27
|
+
value: 'example error',
|
|
28
|
+
stacktrace: {
|
|
29
|
+
frames: [],
|
|
30
|
+
},
|
|
31
|
+
mechanism: { type: 'generic', handled: true },
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
event_id: '80a7023ac32c47f7acb0adaed600d149',
|
|
36
|
+
platform: 'node',
|
|
37
|
+
contexts: {},
|
|
38
|
+
server_name: 'localhost',
|
|
39
|
+
timestamp: 1704203482.356,
|
|
40
|
+
environment: 'production',
|
|
41
|
+
tags: { posthog_distinct_id: 'EXAMPLE_APP_GLOBAL' },
|
|
42
|
+
breadcrumbs: [
|
|
43
|
+
{
|
|
44
|
+
timestamp: 1704203481.422,
|
|
45
|
+
category: 'console',
|
|
46
|
+
level: 'log',
|
|
47
|
+
message: '⚡: Server is running at http://localhost:8010',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
timestamp: 1704203481.658,
|
|
51
|
+
category: 'console',
|
|
52
|
+
level: 'log',
|
|
53
|
+
message:
|
|
54
|
+
"PostHog Debug error [ClientError: Your personalApiKey is invalid. Are you sure you're not using your Project API key? More information: https://posthog.com/docs/api/overview]",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
sdkProcessingMetadata: {
|
|
58
|
+
propagationContext: { traceId: 'ea26146e5a354cb0b3b1daebb3f90e33', spanId: '8d642089c3daa272' },
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('PostHogSentryIntegration', () => {
|
|
63
|
+
let posthog: PostHog
|
|
64
|
+
let posthogSentry: PostHogSentryIntegration
|
|
65
|
+
|
|
66
|
+
jest.useFakeTimers()
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
posthog = new PostHog('TEST_API_KEY', {
|
|
70
|
+
host: 'http://example.com',
|
|
71
|
+
fetchRetryCount: 0,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
posthogSentry = new PostHogSentryIntegration(posthog)
|
|
75
|
+
|
|
76
|
+
mockedFetch.mockResolvedValue({
|
|
77
|
+
status: 200,
|
|
78
|
+
text: () => Promise.resolve('ok'),
|
|
79
|
+
json: () =>
|
|
80
|
+
Promise.resolve({
|
|
81
|
+
status: 'ok',
|
|
82
|
+
}),
|
|
83
|
+
} as any)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
// ensure clean shutdown & no test interdependencies
|
|
88
|
+
await posthog.shutdownAsync()
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('should forward sentry exceptions to posthog', async () => {
|
|
92
|
+
expect(mockedFetch).toHaveBeenCalledTimes(0)
|
|
93
|
+
|
|
94
|
+
const mockSentry = {
|
|
95
|
+
getClient: () => ({
|
|
96
|
+
getDsn: () => ({
|
|
97
|
+
projectId: 123,
|
|
98
|
+
}),
|
|
99
|
+
}),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let processorFunction: any
|
|
103
|
+
|
|
104
|
+
posthogSentry.setupOnce(
|
|
105
|
+
(fn) => (processorFunction = fn),
|
|
106
|
+
() => mockSentry
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
processorFunction(createMockSentryException())
|
|
110
|
+
|
|
111
|
+
jest.runOnlyPendingTimers()
|
|
112
|
+
const batchEvents = getLastBatchEvents()
|
|
113
|
+
|
|
114
|
+
expect(batchEvents).toEqual([
|
|
115
|
+
{
|
|
116
|
+
distinct_id: 'EXAMPLE_APP_GLOBAL',
|
|
117
|
+
event: '$exception',
|
|
118
|
+
properties: {
|
|
119
|
+
$exception_message: 'example error',
|
|
120
|
+
$exception_type: 'Error',
|
|
121
|
+
$exception_personURL: 'http://example.com/person/EXAMPLE_APP_GLOBAL',
|
|
122
|
+
$sentry_event_id: '80a7023ac32c47f7acb0adaed600d149',
|
|
123
|
+
$sentry_exception: {
|
|
124
|
+
values: [
|
|
125
|
+
{
|
|
126
|
+
type: 'Error',
|
|
127
|
+
value: 'example error',
|
|
128
|
+
stacktrace: { frames: [] },
|
|
129
|
+
mechanism: { type: 'generic', handled: true },
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
$sentry_exception_message: 'example error',
|
|
134
|
+
$sentry_exception_type: 'Error',
|
|
135
|
+
$sentry_tags: {
|
|
136
|
+
posthog_distinct_id: 'EXAMPLE_APP_GLOBAL',
|
|
137
|
+
'PostHog Person URL': 'http://example.com/person/EXAMPLE_APP_GLOBAL',
|
|
138
|
+
},
|
|
139
|
+
$lib: 'posthog-node',
|
|
140
|
+
$lib_version: '1.2.3',
|
|
141
|
+
$geoip_disable: true,
|
|
142
|
+
},
|
|
143
|
+
type: 'capture',
|
|
144
|
+
library: 'posthog-node',
|
|
145
|
+
library_version: '1.2.3',
|
|
146
|
+
timestamp: expect.any(String),
|
|
147
|
+
},
|
|
148
|
+
])
|
|
149
|
+
})
|
|
150
|
+
})
|