@posthog/core 1.1.0 → 1.2.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/dist/error-tracking/chunk-ids.js +1 -1
- package/dist/error-tracking/chunk-ids.mjs +1 -1
- package/dist/error-tracking/coercers/error-event-coercer.js +4 -5
- package/dist/error-tracking/coercers/error-event-coercer.mjs +4 -5
- package/dist/error-tracking/coercers/event-coercer.js +1 -2
- package/dist/error-tracking/coercers/event-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/object-coercer.js +1 -2
- package/dist/error-tracking/coercers/object-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.js +1 -2
- package/dist/error-tracking/coercers/primitive-coercer.mjs +1 -2
- package/dist/error-tracking/coercers/promise-rejection-event.js +4 -5
- package/dist/error-tracking/coercers/promise-rejection-event.mjs +4 -5
- package/dist/error-tracking/coercers/string-coercer.js +3 -4
- package/dist/error-tracking/coercers/string-coercer.mjs +3 -4
- package/dist/error-tracking/coercers/utils.js +2 -4
- package/dist/error-tracking/coercers/utils.mjs +2 -4
- package/dist/error-tracking/error-properties-builder.js +11 -15
- package/dist/error-tracking/error-properties-builder.mjs +11 -15
- package/dist/error-tracking/parsers/index.js +2 -4
- package/dist/error-tracking/parsers/index.mjs +2 -4
- package/dist/error-tracking/parsers/node.js +3 -5
- package/dist/error-tracking/parsers/node.mjs +3 -5
- package/dist/error-tracking/utils.js +4 -4
- package/dist/error-tracking/utils.mjs +4 -4
- package/dist/eventemitter.js +4 -4
- package/dist/eventemitter.mjs +4 -4
- package/dist/featureFlagUtils.js +20 -45
- package/dist/featureFlagUtils.mjs +20 -45
- package/dist/gzip.js +1 -2
- package/dist/gzip.mjs +1 -2
- package/dist/index.d.ts +4 -366
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -1225
- package/dist/index.mjs +5 -1190
- package/dist/posthog-core-stateless.d.ts +204 -0
- package/dist/posthog-core-stateless.d.ts.map +1 -0
- package/dist/posthog-core-stateless.js +675 -0
- package/dist/posthog-core-stateless.mjs +632 -0
- package/dist/posthog-core.d.ts +171 -0
- package/dist/posthog-core.d.ts.map +1 -0
- package/dist/posthog-core.js +554 -0
- package/dist/posthog-core.mjs +520 -0
- package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
- package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
- package/dist/testing/PostHogCoreTestClient.js +9 -11
- package/dist/testing/PostHogCoreTestClient.mjs +8 -10
- package/dist/testing/test-utils.js +1 -1
- package/dist/testing/test-utils.mjs +1 -1
- package/dist/utils/bucketed-rate-limiter.js +8 -12
- package/dist/utils/bucketed-rate-limiter.mjs +8 -12
- package/dist/utils/index.js +3 -3
- package/dist/utils/index.mjs +3 -3
- package/dist/utils/type-utils.js +1 -1
- package/dist/utils/type-utils.mjs +1 -1
- package/dist/vendor/uuidv7.js +12 -16
- package/dist/vendor/uuidv7.mjs +12 -16
- package/package.json +3 -2
- package/src/__tests__/featureFlagUtils.spec.ts +427 -0
- package/src/__tests__/gzip.spec.ts +69 -0
- package/src/__tests__/posthog.ai.spec.ts +110 -0
- package/src/__tests__/posthog.capture.spec.ts +91 -0
- package/src/__tests__/posthog.core.spec.ts +135 -0
- package/src/__tests__/posthog.debug.spec.ts +36 -0
- package/src/__tests__/posthog.enqueue.spec.ts +93 -0
- package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
- package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
- package/src/__tests__/posthog.flush.spec.ts +237 -0
- package/src/__tests__/posthog.gdpr.spec.ts +50 -0
- package/src/__tests__/posthog.groups.spec.ts +96 -0
- package/src/__tests__/posthog.identify.spec.ts +194 -0
- package/src/__tests__/posthog.init.spec.ts +110 -0
- package/src/__tests__/posthog.listeners.spec.ts +51 -0
- package/src/__tests__/posthog.register.spec.ts +47 -0
- package/src/__tests__/posthog.reset.spec.ts +76 -0
- package/src/__tests__/posthog.sessions.spec.ts +63 -0
- package/src/__tests__/posthog.setProperties.spec.ts +102 -0
- package/src/__tests__/posthog.shutdown.spec.ts +88 -0
- package/src/__tests__/utils.spec.ts +36 -0
- package/src/error-tracking/chunk-ids.ts +58 -0
- package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
- package/src/error-tracking/coercers/error-coercer.ts +36 -0
- package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
- package/src/error-tracking/coercers/event-coercer.ts +19 -0
- package/src/error-tracking/coercers/index.ts +8 -0
- package/src/error-tracking/coercers/object-coercer.ts +76 -0
- package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
- package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
- package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
- package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
- package/src/error-tracking/coercers/string-coercer.ts +31 -0
- package/src/error-tracking/coercers/utils.ts +33 -0
- package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
- package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
- package/src/error-tracking/error-properties-builder.ts +169 -0
- package/src/error-tracking/index.ts +5 -0
- package/src/error-tracking/parsers/base.ts +29 -0
- package/src/error-tracking/parsers/chrome.ts +53 -0
- package/src/error-tracking/parsers/gecko.ts +38 -0
- package/src/error-tracking/parsers/index.ts +104 -0
- package/src/error-tracking/parsers/node.ts +111 -0
- package/src/error-tracking/parsers/opera.ts +18 -0
- package/src/error-tracking/parsers/react-native.ts +0 -0
- package/src/error-tracking/parsers/safari.ts +33 -0
- package/src/error-tracking/parsers/winjs.ts +12 -0
- package/src/error-tracking/types.ts +107 -0
- package/src/error-tracking/utils.ts +39 -0
- package/src/eventemitter.ts +27 -0
- package/src/featureFlagUtils.ts +192 -0
- package/src/gzip.ts +29 -0
- package/src/index.ts +8 -0
- package/src/posthog-core-stateless.ts +1226 -0
- package/src/posthog-core.ts +958 -0
- package/src/testing/PostHogCoreTestClient.ts +91 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-utils.ts +47 -0
- package/src/types.ts +544 -0
- package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
- package/src/utils/bucketed-rate-limiter.ts +85 -0
- package/src/utils/index.ts +98 -0
- package/src/utils/number-utils.spec.ts +89 -0
- package/src/utils/number-utils.ts +30 -0
- package/src/utils/promise-queue.spec.ts +55 -0
- package/src/utils/promise-queue.ts +30 -0
- package/src/utils/string-utils.ts +23 -0
- package/src/utils/type-utils.ts +134 -0
- package/src/vendor/uuidv7.ts +479 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { waitForPromises, createTestClient, PostHogCoreTestClient, PostHogCoreTestClientMocks } from '@/testing'
|
|
2
|
+
|
|
3
|
+
describe('PostHog Core', () => {
|
|
4
|
+
let posthog: PostHogCoreTestClient
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
6
|
+
let mocks: PostHogCoreTestClientMocks
|
|
7
|
+
|
|
8
|
+
jest.useFakeTimers()
|
|
9
|
+
jest.setSystemTime(new Date('2022-01-01'))
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 10 })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('on', () => {
|
|
16
|
+
it('should listen to various events', () => {
|
|
17
|
+
const mock = jest.fn()
|
|
18
|
+
const mockOther = jest.fn()
|
|
19
|
+
posthog.on('identify', mock)
|
|
20
|
+
posthog.on('identify', mockOther)
|
|
21
|
+
|
|
22
|
+
posthog.identify('user-1')
|
|
23
|
+
expect(mock).toHaveBeenCalledTimes(1)
|
|
24
|
+
expect(mockOther).toHaveBeenCalledTimes(1)
|
|
25
|
+
expect(mock.mock.lastCall[0]).toMatchObject({ type: 'identify' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should unsubscribe when called', () => {
|
|
29
|
+
const mock = jest.fn()
|
|
30
|
+
const unsubscribe = posthog.on('identify', mock)
|
|
31
|
+
|
|
32
|
+
posthog.identify('user-1')
|
|
33
|
+
expect(mock).toHaveBeenCalledTimes(1)
|
|
34
|
+
posthog.identify('user-1')
|
|
35
|
+
expect(mock).toHaveBeenCalledTimes(2)
|
|
36
|
+
unsubscribe()
|
|
37
|
+
posthog.identify('user-1')
|
|
38
|
+
expect(mock).toHaveBeenCalledTimes(2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should subscribe to flush events', async () => {
|
|
42
|
+
const mock = jest.fn()
|
|
43
|
+
posthog.on('flush', mock)
|
|
44
|
+
posthog.capture('event')
|
|
45
|
+
expect(mock).toHaveBeenCalledTimes(0)
|
|
46
|
+
jest.runOnlyPendingTimers()
|
|
47
|
+
await waitForPromises()
|
|
48
|
+
expect(mock).toHaveBeenCalledTimes(1)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PostHogPersistedProperty } from '@/types'
|
|
2
|
+
import { createTestClient, PostHogCoreTestClient, PostHogCoreTestClientMocks } from '@/testing'
|
|
3
|
+
|
|
4
|
+
describe('PostHog Core', () => {
|
|
5
|
+
let posthog: PostHogCoreTestClient
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
let mocks: PostHogCoreTestClientMocks
|
|
8
|
+
|
|
9
|
+
const getEnrichedProperties = (): any => {
|
|
10
|
+
// NOTE: Hacky override so we can just test the props functionality
|
|
11
|
+
return (posthog as any).enrichProperties()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
;[posthog, mocks] = createTestClient('TEST_API_KEY', {})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('register', () => {
|
|
19
|
+
it('should register properties to storage', () => {
|
|
20
|
+
posthog.register({ foo: 'bar' })
|
|
21
|
+
expect(getEnrichedProperties()).toMatchObject({ foo: 'bar' })
|
|
22
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual({ foo: 'bar' })
|
|
23
|
+
posthog.register({ foo2: 'bar2' })
|
|
24
|
+
expect(getEnrichedProperties()).toMatchObject({ foo: 'bar', foo2: 'bar2' })
|
|
25
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual({ foo: 'bar', foo2: 'bar2' })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should unregister properties from storage', () => {
|
|
29
|
+
posthog.register({ foo: 'bar', foo2: 'bar2' })
|
|
30
|
+
posthog.unregister('foo')
|
|
31
|
+
expect(getEnrichedProperties().foo).toBeUndefined()
|
|
32
|
+
expect(getEnrichedProperties().foo2).toEqual('bar2')
|
|
33
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual({ foo2: 'bar2' })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should register properties only for the session', () => {
|
|
37
|
+
posthog.registerForSession({ foo: 'bar' })
|
|
38
|
+
expect(getEnrichedProperties()).toMatchObject({ foo: 'bar' })
|
|
39
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual(undefined)
|
|
40
|
+
|
|
41
|
+
posthog.register({ foo: 'bar2' })
|
|
42
|
+
expect(getEnrichedProperties()).toMatchObject({ foo: 'bar' })
|
|
43
|
+
posthog.unregisterForSession('foo')
|
|
44
|
+
expect(getEnrichedProperties()).toMatchObject({ foo: 'bar2' })
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { PostHogPersistedProperty } from '@/types'
|
|
2
|
+
import { createTestClient, PostHogCoreTestClient, PostHogCoreTestClientMocks } from '@/testing'
|
|
3
|
+
|
|
4
|
+
describe('PostHog Core', () => {
|
|
5
|
+
let posthog: PostHogCoreTestClient
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
7
|
+
let mocks: PostHogCoreTestClientMocks
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
;[posthog, mocks] = createTestClient('TEST_API_KEY', {})
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
describe('reset', () => {
|
|
14
|
+
it('should reset the storage when called', () => {
|
|
15
|
+
const distinctId = posthog.getDistinctId()
|
|
16
|
+
posthog.overrideFeatureFlag({
|
|
17
|
+
foo: 'bar',
|
|
18
|
+
})
|
|
19
|
+
posthog.register({
|
|
20
|
+
prop: 1,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.AnonymousId)).toEqual(distinctId)
|
|
24
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags)).toEqual({ foo: 'bar' })
|
|
25
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual({ prop: 1 })
|
|
26
|
+
|
|
27
|
+
posthog.reset()
|
|
28
|
+
|
|
29
|
+
expect(posthog.getDistinctId()).not.toEqual(distinctId)
|
|
30
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags)).toEqual(undefined)
|
|
31
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual(undefined)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("shouldn't reset the events capture queue", async () => {
|
|
35
|
+
posthog.getDistinctId()
|
|
36
|
+
posthog.capture('custom-event')
|
|
37
|
+
|
|
38
|
+
const expectedQueue = [
|
|
39
|
+
{
|
|
40
|
+
message: expect.objectContaining({
|
|
41
|
+
event: 'custom-event',
|
|
42
|
+
library: 'posthog-core-tests',
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Queue)).toEqual(expectedQueue)
|
|
48
|
+
posthog.reset()
|
|
49
|
+
|
|
50
|
+
const newDistinctId = posthog.getDistinctId()
|
|
51
|
+
|
|
52
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Queue)).toEqual(expectedQueue)
|
|
53
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.AnonymousId)).toEqual(newDistinctId)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should not reset specific props when set', () => {
|
|
57
|
+
const distinctId = posthog.getDistinctId()
|
|
58
|
+
posthog.overrideFeatureFlag({
|
|
59
|
+
foo: 'bar',
|
|
60
|
+
})
|
|
61
|
+
posthog.register({
|
|
62
|
+
prop: 1,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.AnonymousId)).toEqual(distinctId)
|
|
66
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags)).toEqual({ foo: 'bar' })
|
|
67
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual({ prop: 1 })
|
|
68
|
+
|
|
69
|
+
posthog.reset([PostHogPersistedProperty.OverrideFeatureFlags])
|
|
70
|
+
|
|
71
|
+
expect(posthog.getDistinctId()).not.toEqual(distinctId)
|
|
72
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.OverrideFeatureFlags)).toEqual({ foo: 'bar' })
|
|
73
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.Props)).toEqual(undefined)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PostHogPersistedProperty } from '@/types'
|
|
2
|
+
import { createTestClient, PostHogCoreTestClient, PostHogCoreTestClientMocks } from '@/testing'
|
|
3
|
+
|
|
4
|
+
describe('PostHog Core', () => {
|
|
5
|
+
let posthog: PostHogCoreTestClient
|
|
6
|
+
let mocks: PostHogCoreTestClientMocks
|
|
7
|
+
|
|
8
|
+
jest.useFakeTimers()
|
|
9
|
+
jest.setSystemTime(new Date('2022-01-01T12:00:00'))
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('sessions', () => {
|
|
16
|
+
it('should create a sessionId if not set', () => {
|
|
17
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).toEqual(undefined)
|
|
18
|
+
posthog.capture('test')
|
|
19
|
+
expect(mocks.storage.setItem).toHaveBeenCalledWith(PostHogPersistedProperty.SessionId, expect.any(String))
|
|
20
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).toEqual(expect.any(String))
|
|
21
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionLastTimestamp)).toEqual(Date.now())
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should re-use existing sessionId', () => {
|
|
25
|
+
posthog.setPersistedProperty(PostHogPersistedProperty.SessionId, 'test-session-id')
|
|
26
|
+
const now = Date.now()
|
|
27
|
+
posthog.setPersistedProperty(PostHogPersistedProperty.SessionLastTimestamp, now)
|
|
28
|
+
posthog.setPersistedProperty(PostHogPersistedProperty.SessionStartTimestamp, now)
|
|
29
|
+
posthog.capture('test')
|
|
30
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).toEqual('test-session-id')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should generate new sessionId if expired', () => {
|
|
34
|
+
jest.setSystemTime(new Date('2022-01-01T12:00:00'))
|
|
35
|
+
posthog.capture('test')
|
|
36
|
+
const sessionId = posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)
|
|
37
|
+
|
|
38
|
+
// Check 29 minutes later
|
|
39
|
+
jest.setSystemTime(new Date('2022-01-01T12:29:00'))
|
|
40
|
+
posthog.capture('test')
|
|
41
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).toEqual(sessionId)
|
|
42
|
+
|
|
43
|
+
// Check another 29 minutes later
|
|
44
|
+
jest.setSystemTime(new Date('2022-01-01T12:58:00'))
|
|
45
|
+
posthog.capture('test')
|
|
46
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).toEqual(sessionId)
|
|
47
|
+
|
|
48
|
+
// Check more than 30 minutes later
|
|
49
|
+
jest.setSystemTime(new Date('2022-01-01T13:30:00'))
|
|
50
|
+
posthog.capture('test')
|
|
51
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).not.toEqual(sessionId)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should reset sessionId if called', () => {
|
|
55
|
+
posthog.capture('test')
|
|
56
|
+
const sessionId = posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)
|
|
57
|
+
|
|
58
|
+
posthog.resetSessionId()
|
|
59
|
+
posthog.capture('test2')
|
|
60
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.SessionId)).not.toEqual(sessionId)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { PostHogPersistedProperty } from '@/types'
|
|
2
|
+
import { createTestClient, PostHogCoreTestClient, PostHogCoreTestClientMocks } from '@/testing'
|
|
3
|
+
|
|
4
|
+
describe('PostHog Core', () => {
|
|
5
|
+
let posthog: PostHogCoreTestClient
|
|
6
|
+
let mocks: PostHogCoreTestClientMocks
|
|
7
|
+
|
|
8
|
+
jest.useFakeTimers()
|
|
9
|
+
jest.setSystemTime(new Date('2022-01-01'))
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
;[posthog, mocks] = createTestClient('TEST_API_KEY', { flushAt: 1 })
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('setGroupPropertiesForFlags', () => {
|
|
16
|
+
it('should store setGroupPropertiesForFlags as persisted with group_properties key', () => {
|
|
17
|
+
const props = { organisation: { name: 'bar' }, project: { name: 'baz' } }
|
|
18
|
+
posthog.setGroupPropertiesForFlags(props)
|
|
19
|
+
|
|
20
|
+
expect(mocks.storage.setItem).toHaveBeenCalledWith('group_properties', props)
|
|
21
|
+
|
|
22
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual(props)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should update setGroupPropertiesForFlags appropriately', () => {
|
|
26
|
+
const props = { organisation: { name: 'bar' }, project: { name: 'baz' } }
|
|
27
|
+
posthog.setGroupPropertiesForFlags(props)
|
|
28
|
+
|
|
29
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual(props)
|
|
30
|
+
|
|
31
|
+
posthog.setGroupPropertiesForFlags({ organisation: { name: 'bar2' }, project: { name2: 'baz' } })
|
|
32
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual({
|
|
33
|
+
organisation: { name: 'bar2' },
|
|
34
|
+
project: { name: 'baz', name2: 'baz' },
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
posthog.setGroupPropertiesForFlags({ organisation2: { name: 'bar' } })
|
|
38
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual({
|
|
39
|
+
organisation: { name: 'bar2' },
|
|
40
|
+
project: { name: 'baz', name2: 'baz' },
|
|
41
|
+
organisation2: { name: 'bar' },
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should clear setGroupPropertiesForFlags on reset', () => {
|
|
46
|
+
const props = { organisation: { name: 'bar' }, project: { name: 'baz' } }
|
|
47
|
+
posthog.setGroupPropertiesForFlags(props)
|
|
48
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual(props)
|
|
49
|
+
|
|
50
|
+
posthog.reset()
|
|
51
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual(undefined)
|
|
52
|
+
|
|
53
|
+
posthog.setGroupPropertiesForFlags(props)
|
|
54
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.GroupProperties)).toEqual(props)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('setPersonPropertiesForFlags', () => {
|
|
59
|
+
it('should store setPersonPropertiesForFlags as persisted with person_properties key', () => {
|
|
60
|
+
const props = { organisation: 'bar', project: 'baz' }
|
|
61
|
+
posthog.setPersonPropertiesForFlags(props)
|
|
62
|
+
|
|
63
|
+
expect(mocks.storage.setItem).toHaveBeenCalledWith('person_properties', props)
|
|
64
|
+
|
|
65
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual(props)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should update setPersonPropertiesForFlags appropriately', () => {
|
|
69
|
+
const props = { organisation: 'bar', project: 'baz' }
|
|
70
|
+
posthog.setPersonPropertiesForFlags(props)
|
|
71
|
+
|
|
72
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual(props)
|
|
73
|
+
|
|
74
|
+
posthog.setPersonPropertiesForFlags({ organisation: 'bar2', project2: 'baz' })
|
|
75
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual({
|
|
76
|
+
organisation: 'bar2',
|
|
77
|
+
project: 'baz',
|
|
78
|
+
project2: 'baz',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
posthog.setPersonPropertiesForFlags({ organisation2: 'bar' })
|
|
82
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual({
|
|
83
|
+
organisation: 'bar2',
|
|
84
|
+
project: 'baz',
|
|
85
|
+
project2: 'baz',
|
|
86
|
+
organisation2: 'bar',
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should clear setPersonPropertiesForFlags on reset', () => {
|
|
91
|
+
const props = { organisation: 'bar', project: 'baz' }
|
|
92
|
+
posthog.setPersonPropertiesForFlags(props)
|
|
93
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual(props)
|
|
94
|
+
|
|
95
|
+
posthog.reset()
|
|
96
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual(undefined)
|
|
97
|
+
|
|
98
|
+
posthog.setPersonPropertiesForFlags(props)
|
|
99
|
+
expect(posthog.getPersistedProperty(PostHogPersistedProperty.PersonProperties)).toEqual(props)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createTestClient, PostHogCoreTestClient, PostHogCoreTestClientMocks } from '@/testing'
|
|
2
|
+
|
|
3
|
+
describe('PostHog Core', () => {
|
|
4
|
+
let posthog: PostHogCoreTestClient
|
|
5
|
+
let mocks: PostHogCoreTestClientMocks
|
|
6
|
+
|
|
7
|
+
describe('shutdown', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.useRealTimers()
|
|
10
|
+
;[posthog, mocks] = createTestClient('TEST_API_KEY', {
|
|
11
|
+
flushAt: 10,
|
|
12
|
+
preloadFeatureFlags: false,
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('flush messsages once called', async () => {
|
|
17
|
+
for (let i = 0; i < 5; i++) {
|
|
18
|
+
posthog.capture('test-event')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await posthog.shutdown()
|
|
22
|
+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('respects timeout', async () => {
|
|
26
|
+
mocks.fetch.mockImplementation(async () => {
|
|
27
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
28
|
+
console.log('FETCH RETURNED')
|
|
29
|
+
return {
|
|
30
|
+
status: 200,
|
|
31
|
+
text: () => Promise.resolve('ok'),
|
|
32
|
+
json: () => Promise.resolve({ status: 'ok' }),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
posthog.capture('test-event')
|
|
37
|
+
|
|
38
|
+
await posthog
|
|
39
|
+
.shutdown(100)
|
|
40
|
+
.then(() => {
|
|
41
|
+
throw new Error('Should not resolve')
|
|
42
|
+
})
|
|
43
|
+
.catch((e) => {
|
|
44
|
+
expect(e).toEqual('Timeout while shutting down PostHog. Some events may not have been sent.')
|
|
45
|
+
})
|
|
46
|
+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('return the same promise if called multiple times in parallel', async () => {
|
|
50
|
+
mocks.fetch.mockImplementation(async () => {
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
52
|
+
return {
|
|
53
|
+
status: 200,
|
|
54
|
+
text: () => Promise.resolve('ok'),
|
|
55
|
+
json: () => Promise.resolve({ status: 'ok' }),
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
posthog.capture('test-event')
|
|
60
|
+
|
|
61
|
+
const p1 = posthog.shutdown(100)
|
|
62
|
+
const p2 = posthog.shutdown(100)
|
|
63
|
+
expect(p1).toEqual(p2)
|
|
64
|
+
await Promise.allSettled([p1, p2])
|
|
65
|
+
expect(mocks.fetch).toHaveBeenCalledTimes(1)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('can handle being called multiple times in series (discouraged but some users will do this)', async () => {
|
|
69
|
+
mocks.fetch.mockImplementation(async () => {
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
71
|
+
console.log('FETCH RETURNED')
|
|
72
|
+
return {
|
|
73
|
+
status: 200,
|
|
74
|
+
text: () => Promise.resolve('ok'),
|
|
75
|
+
json: () => Promise.resolve({ status: 'ok' }),
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
posthog.capture('test-event')
|
|
80
|
+
await posthog.shutdown()
|
|
81
|
+
|
|
82
|
+
posthog.capture('test-event')
|
|
83
|
+
await posthog.shutdown()
|
|
84
|
+
|
|
85
|
+
expect(mocks.fetch).toHaveBeenCalledTimes(2)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assert, removeTrailingSlash, currentISOTime, currentTimestamp } from '@/utils'
|
|
2
|
+
|
|
3
|
+
describe('utils', () => {
|
|
4
|
+
describe('assert', () => {
|
|
5
|
+
it('should throw on falsey values', () => {
|
|
6
|
+
;[false, '', null, undefined, 0, {}, []].forEach((x) => {
|
|
7
|
+
expect(() => assert(x, 'error')).toThrow('error')
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
it('should not throw on truthy value', () => {
|
|
11
|
+
expect(() => assert('string', 'error')).not.toThrow('error')
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
describe('removeTrailingSlash', () => {
|
|
15
|
+
it('should removeSlashes', () => {
|
|
16
|
+
expect(removeTrailingSlash('me////')).toEqual('me')
|
|
17
|
+
expect(removeTrailingSlash('me/wat///')).toEqual('me/wat')
|
|
18
|
+
expect(removeTrailingSlash('me/')).toEqual('me')
|
|
19
|
+
expect(removeTrailingSlash('/me')).toEqual('/me')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
describe.skip('retriable', () => {
|
|
23
|
+
it('should do something', () => {})
|
|
24
|
+
})
|
|
25
|
+
describe('currentTimestamp', () => {
|
|
26
|
+
it('should get the timestamp', () => {
|
|
27
|
+
expect(currentTimestamp()).toEqual(Date.now())
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
describe('currentISOTime', () => {
|
|
31
|
+
it('should get the iso time', () => {
|
|
32
|
+
jest.setSystemTime(new Date('2022-01-01'))
|
|
33
|
+
expect(currentISOTime()).toEqual('2022-01-01T00:00:00.000Z')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Portions of this file are derived from getsentry/sentry-javascript by Software, Inc. dba Sentry
|
|
2
|
+
// Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
import type { StackParser } from './types'
|
|
5
|
+
|
|
6
|
+
type StackString = string
|
|
7
|
+
type CachedResult = [string, string]
|
|
8
|
+
|
|
9
|
+
type ChunkIdMapType = Record<string, string>
|
|
10
|
+
|
|
11
|
+
let parsedStackResults: Record<StackString, CachedResult> | undefined
|
|
12
|
+
let lastKeysCount: number | undefined
|
|
13
|
+
let cachedFilenameChunkIds: ChunkIdMapType | undefined
|
|
14
|
+
|
|
15
|
+
export function getFilenameToChunkIdMap(stackParser: StackParser): ChunkIdMapType | undefined {
|
|
16
|
+
const chunkIdMap = (globalThis as any)._posthogChunkIds as ChunkIdMapType | undefined
|
|
17
|
+
if (!chunkIdMap) {
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const chunkIdKeys = Object.keys(chunkIdMap)
|
|
22
|
+
|
|
23
|
+
if (cachedFilenameChunkIds && chunkIdKeys.length === lastKeysCount) {
|
|
24
|
+
return cachedFilenameChunkIds
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
lastKeysCount = chunkIdKeys.length
|
|
28
|
+
|
|
29
|
+
cachedFilenameChunkIds = chunkIdKeys.reduce<Record<string, string>>((acc, stackKey) => {
|
|
30
|
+
if (!parsedStackResults) {
|
|
31
|
+
parsedStackResults = {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = parsedStackResults[stackKey]
|
|
35
|
+
|
|
36
|
+
if (result) {
|
|
37
|
+
acc[result[0]] = result[1]
|
|
38
|
+
} else {
|
|
39
|
+
const parsedStack = stackParser(stackKey)
|
|
40
|
+
|
|
41
|
+
for (let i = parsedStack.length - 1; i >= 0; i--) {
|
|
42
|
+
const stackFrame = parsedStack[i]
|
|
43
|
+
const filename = stackFrame?.filename
|
|
44
|
+
const chunkId = chunkIdMap[stackKey]
|
|
45
|
+
|
|
46
|
+
if (filename && chunkId) {
|
|
47
|
+
acc[filename] = chunkId
|
|
48
|
+
parsedStackResults[stackKey] = [filename, chunkId]
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return acc
|
|
55
|
+
}, {})
|
|
56
|
+
|
|
57
|
+
return cachedFilenameChunkIds
|
|
58
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '@/error-tracking/types'
|
|
2
|
+
import { isBuiltin, isString } from '@/utils'
|
|
3
|
+
|
|
4
|
+
export class DOMExceptionCoercer implements ErrorTrackingCoercer<DOMException> {
|
|
5
|
+
match(err: unknown): err is DOMException {
|
|
6
|
+
return this.isDOMException(err) || this.isDOMError(err)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
coerce(err: DOMException, ctx: CoercingContext): ExceptionLike {
|
|
10
|
+
const hasStack = isString(err.stack)
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
type: this.getType(err),
|
|
14
|
+
value: this.getValue(err),
|
|
15
|
+
stack: hasStack ? err.stack : undefined,
|
|
16
|
+
cause: err.cause ? ctx.next(err.cause) : undefined,
|
|
17
|
+
synthetic: false,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private getType(candidate: DOMException) {
|
|
22
|
+
return this.isDOMError(candidate) ? 'DOMError' : 'DOMException'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private getValue(err: DOMException) {
|
|
26
|
+
const name = err.name || (this.isDOMError(err) ? 'DOMError' : 'DOMException')
|
|
27
|
+
const message = err.message ? `${name}: ${err.message}` : name
|
|
28
|
+
return message
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private isDOMException(err: unknown): err is DOMException {
|
|
32
|
+
return isBuiltin(err, 'DOMException')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private isDOMError(err: unknown): err is DOMException {
|
|
36
|
+
return isBuiltin(err, 'DOMError')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isPlainError } from '@/utils'
|
|
2
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
|
|
3
|
+
|
|
4
|
+
export class ErrorCoercer implements ErrorTrackingCoercer<Error> {
|
|
5
|
+
match(err: unknown): err is Error {
|
|
6
|
+
return isPlainError(err)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
coerce(err: Error, ctx: CoercingContext): ExceptionLike {
|
|
10
|
+
return {
|
|
11
|
+
type: this.getType(err),
|
|
12
|
+
value: this.getMessage(err, ctx),
|
|
13
|
+
stack: this.getStack(err),
|
|
14
|
+
cause: err.cause ? ctx.next(err.cause) : undefined,
|
|
15
|
+
synthetic: false,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private getType(err: Error): string {
|
|
20
|
+
return err.name || err.constructor.name
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private getMessage(err: Error & { message: { error?: Error } }, _ctx: CoercingContext): string {
|
|
24
|
+
const message = err.message
|
|
25
|
+
|
|
26
|
+
if (message.error && typeof message.error.message === 'string') {
|
|
27
|
+
return String(message.error.message)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return String(message)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private getStack(err: Error & { stacktrace?: string }): string | undefined {
|
|
34
|
+
return err.stacktrace || err.stack || undefined
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
|
|
2
|
+
import { isErrorEvent } from '@/utils'
|
|
3
|
+
|
|
4
|
+
export class ErrorEventCoercer implements ErrorTrackingCoercer<ErrorEvent> {
|
|
5
|
+
constructor() {}
|
|
6
|
+
|
|
7
|
+
match(err: unknown): err is ErrorEvent {
|
|
8
|
+
return isErrorEvent(err) && (err as ErrorEvent).error != undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
coerce(err: ErrorEvent, ctx: CoercingContext): ExceptionLike {
|
|
12
|
+
const exceptionLike = ctx.apply(err.error)
|
|
13
|
+
if (!exceptionLike) {
|
|
14
|
+
return {
|
|
15
|
+
type: 'ErrorEvent',
|
|
16
|
+
value: err.message,
|
|
17
|
+
stack: ctx.syntheticException?.stack,
|
|
18
|
+
synthetic: true,
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
return exceptionLike
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isEvent } from '@/utils'
|
|
2
|
+
import { CoercingContext, ErrorTrackingCoercer, ExceptionLike } from '../types'
|
|
3
|
+
import { extractExceptionKeysForMessage } from './utils'
|
|
4
|
+
|
|
5
|
+
export class EventCoercer implements ErrorTrackingCoercer<Event> {
|
|
6
|
+
match(err: unknown): err is Event {
|
|
7
|
+
return isEvent(err)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
coerce(evt: Event, ctx: CoercingContext): ExceptionLike {
|
|
11
|
+
const constructorName = evt.constructor.name
|
|
12
|
+
return {
|
|
13
|
+
type: constructorName,
|
|
14
|
+
value: `${constructorName} captured as exception with keys: ${extractExceptionKeysForMessage(evt)}`,
|
|
15
|
+
stack: ctx.syntheticException?.stack,
|
|
16
|
+
synthetic: true,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './dom-exception-coercer'
|
|
2
|
+
export * from './error-coercer'
|
|
3
|
+
export * from './error-event-coercer'
|
|
4
|
+
export * from './string-coercer'
|
|
5
|
+
export * from './object-coercer'
|
|
6
|
+
export * from './event-coercer'
|
|
7
|
+
export * from './primitive-coercer'
|
|
8
|
+
export * from './promise-rejection-event'
|