@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.
Files changed (126) hide show
  1. package/dist/error-tracking/chunk-ids.js +1 -1
  2. package/dist/error-tracking/chunk-ids.mjs +1 -1
  3. package/dist/error-tracking/coercers/error-event-coercer.js +4 -5
  4. package/dist/error-tracking/coercers/error-event-coercer.mjs +4 -5
  5. package/dist/error-tracking/coercers/event-coercer.js +1 -2
  6. package/dist/error-tracking/coercers/event-coercer.mjs +1 -2
  7. package/dist/error-tracking/coercers/object-coercer.js +1 -2
  8. package/dist/error-tracking/coercers/object-coercer.mjs +1 -2
  9. package/dist/error-tracking/coercers/primitive-coercer.js +1 -2
  10. package/dist/error-tracking/coercers/primitive-coercer.mjs +1 -2
  11. package/dist/error-tracking/coercers/promise-rejection-event.js +4 -5
  12. package/dist/error-tracking/coercers/promise-rejection-event.mjs +4 -5
  13. package/dist/error-tracking/coercers/string-coercer.js +3 -4
  14. package/dist/error-tracking/coercers/string-coercer.mjs +3 -4
  15. package/dist/error-tracking/coercers/utils.js +2 -4
  16. package/dist/error-tracking/coercers/utils.mjs +2 -4
  17. package/dist/error-tracking/error-properties-builder.js +11 -15
  18. package/dist/error-tracking/error-properties-builder.mjs +11 -15
  19. package/dist/error-tracking/parsers/index.js +2 -4
  20. package/dist/error-tracking/parsers/index.mjs +2 -4
  21. package/dist/error-tracking/parsers/node.js +3 -5
  22. package/dist/error-tracking/parsers/node.mjs +3 -5
  23. package/dist/error-tracking/utils.js +4 -4
  24. package/dist/error-tracking/utils.mjs +4 -4
  25. package/dist/eventemitter.js +4 -4
  26. package/dist/eventemitter.mjs +4 -4
  27. package/dist/featureFlagUtils.js +20 -45
  28. package/dist/featureFlagUtils.mjs +20 -45
  29. package/dist/gzip.js +1 -2
  30. package/dist/gzip.mjs +1 -2
  31. package/dist/index.d.ts +4 -366
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +54 -1225
  34. package/dist/index.mjs +5 -1190
  35. package/dist/posthog-core-stateless.d.ts +204 -0
  36. package/dist/posthog-core-stateless.d.ts.map +1 -0
  37. package/dist/posthog-core-stateless.js +675 -0
  38. package/dist/posthog-core-stateless.mjs +632 -0
  39. package/dist/posthog-core.d.ts +171 -0
  40. package/dist/posthog-core.d.ts.map +1 -0
  41. package/dist/posthog-core.js +554 -0
  42. package/dist/posthog-core.mjs +520 -0
  43. package/dist/testing/PostHogCoreTestClient.d.ts +2 -1
  44. package/dist/testing/PostHogCoreTestClient.d.ts.map +1 -1
  45. package/dist/testing/PostHogCoreTestClient.js +9 -11
  46. package/dist/testing/PostHogCoreTestClient.mjs +8 -10
  47. package/dist/testing/test-utils.js +1 -1
  48. package/dist/testing/test-utils.mjs +1 -1
  49. package/dist/utils/bucketed-rate-limiter.js +8 -12
  50. package/dist/utils/bucketed-rate-limiter.mjs +8 -12
  51. package/dist/utils/index.js +3 -3
  52. package/dist/utils/index.mjs +3 -3
  53. package/dist/utils/type-utils.js +1 -1
  54. package/dist/utils/type-utils.mjs +1 -1
  55. package/dist/vendor/uuidv7.js +12 -16
  56. package/dist/vendor/uuidv7.mjs +12 -16
  57. package/package.json +3 -2
  58. package/src/__tests__/featureFlagUtils.spec.ts +427 -0
  59. package/src/__tests__/gzip.spec.ts +69 -0
  60. package/src/__tests__/posthog.ai.spec.ts +110 -0
  61. package/src/__tests__/posthog.capture.spec.ts +91 -0
  62. package/src/__tests__/posthog.core.spec.ts +135 -0
  63. package/src/__tests__/posthog.debug.spec.ts +36 -0
  64. package/src/__tests__/posthog.enqueue.spec.ts +93 -0
  65. package/src/__tests__/posthog.featureflags.spec.ts +1106 -0
  66. package/src/__tests__/posthog.featureflags.v1.spec.ts +922 -0
  67. package/src/__tests__/posthog.flush.spec.ts +237 -0
  68. package/src/__tests__/posthog.gdpr.spec.ts +50 -0
  69. package/src/__tests__/posthog.groups.spec.ts +96 -0
  70. package/src/__tests__/posthog.identify.spec.ts +194 -0
  71. package/src/__tests__/posthog.init.spec.ts +110 -0
  72. package/src/__tests__/posthog.listeners.spec.ts +51 -0
  73. package/src/__tests__/posthog.register.spec.ts +47 -0
  74. package/src/__tests__/posthog.reset.spec.ts +76 -0
  75. package/src/__tests__/posthog.sessions.spec.ts +63 -0
  76. package/src/__tests__/posthog.setProperties.spec.ts +102 -0
  77. package/src/__tests__/posthog.shutdown.spec.ts +88 -0
  78. package/src/__tests__/utils.spec.ts +36 -0
  79. package/src/error-tracking/chunk-ids.ts +58 -0
  80. package/src/error-tracking/coercers/dom-exception-coercer.ts +38 -0
  81. package/src/error-tracking/coercers/error-coercer.ts +36 -0
  82. package/src/error-tracking/coercers/error-event-coercer.ts +24 -0
  83. package/src/error-tracking/coercers/event-coercer.ts +19 -0
  84. package/src/error-tracking/coercers/index.ts +8 -0
  85. package/src/error-tracking/coercers/object-coercer.ts +76 -0
  86. package/src/error-tracking/coercers/primitive-coercer.ts +19 -0
  87. package/src/error-tracking/coercers/promise-rejection-event.spec.ts +77 -0
  88. package/src/error-tracking/coercers/promise-rejection-event.ts +53 -0
  89. package/src/error-tracking/coercers/string-coercer.spec.ts +26 -0
  90. package/src/error-tracking/coercers/string-coercer.ts +31 -0
  91. package/src/error-tracking/coercers/utils.ts +33 -0
  92. package/src/error-tracking/error-properties-builder.coerce.spec.ts +202 -0
  93. package/src/error-tracking/error-properties-builder.parse.spec.ts +30 -0
  94. package/src/error-tracking/error-properties-builder.ts +169 -0
  95. package/src/error-tracking/index.ts +5 -0
  96. package/src/error-tracking/parsers/base.ts +29 -0
  97. package/src/error-tracking/parsers/chrome.ts +53 -0
  98. package/src/error-tracking/parsers/gecko.ts +38 -0
  99. package/src/error-tracking/parsers/index.ts +104 -0
  100. package/src/error-tracking/parsers/node.ts +111 -0
  101. package/src/error-tracking/parsers/opera.ts +18 -0
  102. package/src/error-tracking/parsers/react-native.ts +0 -0
  103. package/src/error-tracking/parsers/safari.ts +33 -0
  104. package/src/error-tracking/parsers/winjs.ts +12 -0
  105. package/src/error-tracking/types.ts +107 -0
  106. package/src/error-tracking/utils.ts +39 -0
  107. package/src/eventemitter.ts +27 -0
  108. package/src/featureFlagUtils.ts +192 -0
  109. package/src/gzip.ts +29 -0
  110. package/src/index.ts +8 -0
  111. package/src/posthog-core-stateless.ts +1226 -0
  112. package/src/posthog-core.ts +958 -0
  113. package/src/testing/PostHogCoreTestClient.ts +91 -0
  114. package/src/testing/index.ts +2 -0
  115. package/src/testing/test-utils.ts +47 -0
  116. package/src/types.ts +544 -0
  117. package/src/utils/bucketed-rate-limiter.spec.ts +33 -0
  118. package/src/utils/bucketed-rate-limiter.ts +85 -0
  119. package/src/utils/index.ts +98 -0
  120. package/src/utils/number-utils.spec.ts +89 -0
  121. package/src/utils/number-utils.ts +30 -0
  122. package/src/utils/promise-queue.spec.ts +55 -0
  123. package/src/utils/promise-queue.ts +30 -0
  124. package/src/utils/string-utils.ts +23 -0
  125. package/src/utils/type-utils.ts +134 -0
  126. 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'