posthog-node 3.2.1 → 3.4.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 +9 -0
- package/index.ts +1 -0
- package/lib/index.cjs.js +195 -14
- package/lib/index.cjs.js.map +1 -1
- package/lib/index.d.ts +42 -2
- package/lib/index.esm.js +195 -15
- 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 -1
- 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 +106 -13
- package/src/posthog-node.ts +1 -1
- package/test/extensions/sentry-integration.spec.ts +150 -0
- package/test/feature-flags.spec.ts +256 -3
|
@@ -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 {};
|
|
@@ -55,4 +55,5 @@ declare class FeatureFlagsPoller {
|
|
|
55
55
|
stopPoller(): void;
|
|
56
56
|
}
|
|
57
57
|
declare function matchProperty(property: FeatureFlagCondition['properties'][number], propertyValues: Record<string, any>): boolean;
|
|
58
|
-
|
|
58
|
+
declare function relativeDateParseForFeatureFlagMatching(value: string): Date | null;
|
|
59
|
+
export { FeatureFlagsPoller, matchProperty, relativeDateParseForFeatureFlagMatching, InconclusiveMatchError, ClientError, };
|
|
@@ -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.4.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
|
@@ -464,11 +464,32 @@ function matchProperty(
|
|
|
464
464
|
|
|
465
465
|
const overrideValue = propertyValues[key]
|
|
466
466
|
|
|
467
|
+
function computeExactMatch(value: any, overrideValue: any): boolean {
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
return value.map((val) => String(val).toLowerCase()).includes(String(overrideValue).toLowerCase())
|
|
470
|
+
}
|
|
471
|
+
return String(value).toLowerCase() === String(overrideValue).toLowerCase()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function compare(lhs: any, rhs: any, operator: string): boolean {
|
|
475
|
+
if (operator === 'gt') {
|
|
476
|
+
return lhs > rhs
|
|
477
|
+
} else if (operator === 'gte') {
|
|
478
|
+
return lhs >= rhs
|
|
479
|
+
} else if (operator === 'lt') {
|
|
480
|
+
return lhs < rhs
|
|
481
|
+
} else if (operator === 'lte') {
|
|
482
|
+
return lhs <= rhs
|
|
483
|
+
} else {
|
|
484
|
+
throw new Error(`Invalid operator: ${operator}`)
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
467
488
|
switch (operator) {
|
|
468
489
|
case 'exact':
|
|
469
|
-
return
|
|
490
|
+
return computeExactMatch(value, overrideValue)
|
|
470
491
|
case 'is_not':
|
|
471
|
-
return
|
|
492
|
+
return !computeExactMatch(value, overrideValue)
|
|
472
493
|
case 'is_set':
|
|
473
494
|
return key in propertyValues
|
|
474
495
|
case 'icontains':
|
|
@@ -480,25 +501,54 @@ function matchProperty(
|
|
|
480
501
|
case 'not_regex':
|
|
481
502
|
return isValidRegex(String(value)) && String(overrideValue).match(String(value)) === null
|
|
482
503
|
case 'gt':
|
|
483
|
-
return typeof overrideValue == typeof value && overrideValue > value
|
|
484
504
|
case 'gte':
|
|
485
|
-
return typeof overrideValue == typeof value && overrideValue >= value
|
|
486
505
|
case 'lt':
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
506
|
+
case 'lte': {
|
|
507
|
+
// :TRICKY: We adjust comparison based on the override value passed in,
|
|
508
|
+
// to make sure we handle both numeric and string comparisons appropriately.
|
|
509
|
+
let parsedValue = typeof value === 'number' ? value : null
|
|
510
|
+
|
|
511
|
+
if (typeof value === 'string') {
|
|
512
|
+
try {
|
|
513
|
+
parsedValue = parseFloat(value)
|
|
514
|
+
} catch (err) {
|
|
515
|
+
// pass
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (parsedValue != null && overrideValue != null) {
|
|
520
|
+
// check both null and undefined
|
|
521
|
+
if (typeof overrideValue === 'string') {
|
|
522
|
+
return compare(overrideValue, String(value), operator)
|
|
523
|
+
} else {
|
|
524
|
+
return compare(overrideValue, parsedValue, operator)
|
|
525
|
+
}
|
|
526
|
+
} else {
|
|
527
|
+
return compare(String(overrideValue), String(value), operator)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
490
530
|
case 'is_date_after':
|
|
491
|
-
case 'is_date_before':
|
|
492
|
-
|
|
531
|
+
case 'is_date_before':
|
|
532
|
+
case 'is_relative_date_before':
|
|
533
|
+
case 'is_relative_date_after': {
|
|
534
|
+
let parsedDate = null
|
|
535
|
+
if (['is_relative_date_before', 'is_relative_date_after'].includes(operator)) {
|
|
536
|
+
parsedDate = relativeDateParseForFeatureFlagMatching(String(value))
|
|
537
|
+
} else {
|
|
538
|
+
parsedDate = convertToDateTime(value)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (parsedDate == null) {
|
|
542
|
+
throw new InconclusiveMatchError(`Invalid date: ${value}`)
|
|
543
|
+
}
|
|
493
544
|
const overrideDate = convertToDateTime(overrideValue)
|
|
494
|
-
if (
|
|
545
|
+
if (['is_date_before', 'is_relative_date_before'].includes(operator)) {
|
|
495
546
|
return overrideDate < parsedDate
|
|
496
547
|
}
|
|
497
548
|
return overrideDate > parsedDate
|
|
498
549
|
}
|
|
499
550
|
default:
|
|
500
|
-
|
|
501
|
-
return false
|
|
551
|
+
throw new InconclusiveMatchError(`Unknown operator: ${operator}`)
|
|
502
552
|
}
|
|
503
553
|
}
|
|
504
554
|
|
|
@@ -636,4 +686,47 @@ function convertToDateTime(value: string | number | (string | number)[] | Date):
|
|
|
636
686
|
}
|
|
637
687
|
}
|
|
638
688
|
|
|
639
|
-
|
|
689
|
+
function relativeDateParseForFeatureFlagMatching(value: string): Date | null {
|
|
690
|
+
const regex = /^(?<number>[0-9]+)(?<interval>[a-z])$/
|
|
691
|
+
const match = value.match(regex)
|
|
692
|
+
const parsedDt = new Date(new Date().toISOString())
|
|
693
|
+
|
|
694
|
+
if (match) {
|
|
695
|
+
if (!match.groups) {
|
|
696
|
+
return null
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const number = parseInt(match.groups['number'])
|
|
700
|
+
|
|
701
|
+
if (number >= 10000) {
|
|
702
|
+
// Guard against overflow, disallow numbers greater than 10_000
|
|
703
|
+
return null
|
|
704
|
+
}
|
|
705
|
+
const interval = match.groups['interval']
|
|
706
|
+
if (interval == 'h') {
|
|
707
|
+
parsedDt.setUTCHours(parsedDt.getUTCHours() - number)
|
|
708
|
+
} else if (interval == 'd') {
|
|
709
|
+
parsedDt.setUTCDate(parsedDt.getUTCDate() - number)
|
|
710
|
+
} else if (interval == 'w') {
|
|
711
|
+
parsedDt.setUTCDate(parsedDt.getUTCDate() - number * 7)
|
|
712
|
+
} else if (interval == 'm') {
|
|
713
|
+
parsedDt.setUTCMonth(parsedDt.getUTCMonth() - number)
|
|
714
|
+
} else if (interval == 'y') {
|
|
715
|
+
parsedDt.setUTCFullYear(parsedDt.getUTCFullYear() - number)
|
|
716
|
+
} else {
|
|
717
|
+
return null
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return parsedDt
|
|
721
|
+
} else {
|
|
722
|
+
return null
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
export {
|
|
727
|
+
FeatureFlagsPoller,
|
|
728
|
+
matchProperty,
|
|
729
|
+
relativeDateParseForFeatureFlagMatching,
|
|
730
|
+
InconclusiveMatchError,
|
|
731
|
+
ClientError,
|
|
732
|
+
}
|
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
|
|
|
@@ -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
|
+
})
|