posthog-js-lite 3.4.2 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +19 -0
- package/lib/{index.cjs.js → index.cjs} +950 -199
- package/lib/index.cjs.map +1 -0
- package/lib/index.d.ts +661 -350
- package/lib/{index.esm.js → index.mjs} +950 -199
- package/lib/index.mjs.map +1 -0
- package/package.json +3 -3
- package/src/context.ts +1 -1
- package/src/patch.ts +50 -0
- package/src/posthog-web.ts +57 -8
- package/src/types.ts +2 -1
- package/test/posthog-web.spec.ts +193 -2
- package/tsconfig.json +1 -0
- package/lib/index.cjs.js.map +0 -1
- package/lib/index.esm.js.map +0 -1
- package/lib/posthog-core/src/eventemitter.d.ts +0 -8
- package/lib/posthog-core/src/index.d.ts +0 -207
- package/lib/posthog-core/src/lz-string.d.ts +0 -8
- package/lib/posthog-core/src/types.d.ts +0 -133
- package/lib/posthog-core/src/utils.d.ts +0 -15
- package/lib/posthog-core/src/vendor/uuidv7.d.ts +0 -179
- package/lib/posthog-web/index.d.ts +0 -3
- package/lib/posthog-web/src/context.d.ts +0 -1
- package/lib/posthog-web/src/posthog-web.d.ts +0 -16
- package/lib/posthog-web/src/storage.d.ts +0 -10
- package/lib/posthog-web/src/types.d.ts +0 -6
package/src/context.ts
CHANGED
package/src/patch.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// import { patch } from 'rrweb/typings/utils'
|
|
2
|
+
// copied from: https://github.com/PostHog/posthog-js/blob/main/src/extensions/replay/rrweb-plugins/patch.ts
|
|
3
|
+
// which was copied from https://github.com/rrweb-io/rrweb/blob/8aea5b00a4dfe5a6f59bd2ae72bb624f45e51e81/packages/rrweb/src/utils.ts#L129
|
|
4
|
+
// which was copied from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
|
|
5
|
+
|
|
6
|
+
// copied from: https://github.com/PostHog/posthog-js/blob/main/react/src/utils/type-utils.ts#L4
|
|
7
|
+
export const isFunction = function (f: any): f is (...args: any[]) => any {
|
|
8
|
+
return typeof f === 'function'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function patch(
|
|
12
|
+
source: { [key: string]: any },
|
|
13
|
+
name: string,
|
|
14
|
+
replacement: (...args: unknown[]) => unknown
|
|
15
|
+
): () => void {
|
|
16
|
+
try {
|
|
17
|
+
if (!(name in source)) {
|
|
18
|
+
return () => {
|
|
19
|
+
//
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const original = source[name] as () => unknown
|
|
24
|
+
const wrapped = replacement(original)
|
|
25
|
+
|
|
26
|
+
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
|
|
27
|
+
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
|
|
28
|
+
if (isFunction(wrapped)) {
|
|
29
|
+
wrapped.prototype = wrapped.prototype || {}
|
|
30
|
+
Object.defineProperties(wrapped, {
|
|
31
|
+
__posthog_wrapped__: {
|
|
32
|
+
enumerable: false,
|
|
33
|
+
value: true,
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
source[name] = wrapped
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
source[name] = original
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
return () => {
|
|
45
|
+
//
|
|
46
|
+
}
|
|
47
|
+
// This can throw if multiple fill happens on a global object like XMLHttpRequest
|
|
48
|
+
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/posthog-web.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '../../posthog-core/src'
|
|
1
|
+
import { version } from '../package.json'
|
|
2
|
+
|
|
3
|
+
import { PostHogCore, getFetch } from 'posthog-core'
|
|
4
|
+
import type { PostHogFetchOptions, PostHogFetchResponse, PostHogPersistedProperty } from 'posthog-core'
|
|
5
|
+
|
|
7
6
|
import { getContext } from './context'
|
|
8
7
|
import { PostHogStorage, getStorage } from './storage'
|
|
9
|
-
import { version } from '../package.json'
|
|
10
8
|
import { PostHogOptions } from './types'
|
|
11
|
-
import {
|
|
9
|
+
import { patch } from './patch'
|
|
12
10
|
|
|
13
11
|
export class PostHog extends PostHogCore {
|
|
14
12
|
private _storage: PostHogStorage
|
|
15
13
|
private _storageCache: any
|
|
16
14
|
private _storageKey: string
|
|
15
|
+
private _lastPathname: string = ''
|
|
17
16
|
|
|
18
17
|
constructor(apiKey: string, options?: PostHogOptions) {
|
|
19
18
|
super(apiKey, options)
|
|
@@ -27,6 +26,11 @@ export class PostHog extends PostHogCore {
|
|
|
27
26
|
if (options?.preloadFeatureFlags !== false) {
|
|
28
27
|
this.reloadFeatureFlags()
|
|
29
28
|
}
|
|
29
|
+
|
|
30
|
+
if (options?.captureHistoryEvents && typeof window !== 'undefined') {
|
|
31
|
+
this._lastPathname = window?.location?.pathname || ''
|
|
32
|
+
this.setupHistoryEventTracking()
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
private getWindow(): Window | undefined {
|
|
@@ -84,4 +88,49 @@ export class PostHog extends PostHogCore {
|
|
|
84
88
|
...getContext(this.getWindow()),
|
|
85
89
|
}
|
|
86
90
|
}
|
|
91
|
+
|
|
92
|
+
private setupHistoryEventTracking(): void {
|
|
93
|
+
const window = this.getWindow()
|
|
94
|
+
if (!window) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Old fashioned, we could also use arrow functions but I think the closure for a patch is more reliable
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
100
|
+
const self = this
|
|
101
|
+
|
|
102
|
+
patch(window.history, 'pushState', (originalPushState) => {
|
|
103
|
+
return function patchedPushState(this: History, state: any, title: string, url?: string | URL | null): void {
|
|
104
|
+
;(originalPushState as History['pushState']).call(this, state, title, url)
|
|
105
|
+
self.captureNavigationEvent('pushState')
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
patch(window.history, 'replaceState', (originalReplaceState) => {
|
|
110
|
+
return function patchedReplaceState(this: History, state: any, title: string, url?: string | URL | null): void {
|
|
111
|
+
;(originalReplaceState as History['replaceState']).call(this, state, title, url)
|
|
112
|
+
self.captureNavigationEvent('replaceState')
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// For popstate we need to listen to the event instead of overriding a method
|
|
117
|
+
window.addEventListener('popstate', () => {
|
|
118
|
+
this.captureNavigationEvent('popstate')
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private captureNavigationEvent(navigationType: 'pushState' | 'replaceState' | 'popstate'): void {
|
|
123
|
+
const window = this.getWindow()
|
|
124
|
+
if (!window) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const currentPathname = window.location.pathname
|
|
129
|
+
|
|
130
|
+
// Only capture pageview if the pathname has changed
|
|
131
|
+
if (currentPathname !== this._lastPathname) {
|
|
132
|
+
this.capture('$pageview', { navigation_type: navigationType })
|
|
133
|
+
this._lastPathname = currentPathname
|
|
134
|
+
}
|
|
135
|
+
}
|
|
87
136
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { PostHogCoreOptions } from '
|
|
1
|
+
import type { PostHogCoreOptions } from 'posthog-core'
|
|
2
2
|
|
|
3
3
|
export type PostHogOptions = {
|
|
4
4
|
autocapture?: boolean
|
|
5
5
|
persistence?: 'localStorage' | 'sessionStorage' | 'cookie' | 'memory'
|
|
6
6
|
persistence_name?: string
|
|
7
|
+
captureHistoryEvents?: boolean
|
|
7
8
|
} & PostHogCoreOptions
|
package/test/posthog-web.spec.ts
CHANGED
|
@@ -12,7 +12,7 @@ describe('PostHogWeb', () => {
|
|
|
12
12
|
beforeEach(() => {
|
|
13
13
|
;(global as any).window.fetch = fetch = jest.fn(async (url) => {
|
|
14
14
|
let res: any = { status: 'ok' }
|
|
15
|
-
if (url.includes('
|
|
15
|
+
if (url.includes('flags')) {
|
|
16
16
|
res = {
|
|
17
17
|
featureFlags: {
|
|
18
18
|
'feature-1': true,
|
|
@@ -61,7 +61,7 @@ describe('PostHogWeb', () => {
|
|
|
61
61
|
|
|
62
62
|
await waitForPromises()
|
|
63
63
|
|
|
64
|
-
expect(fetch).toHaveBeenCalledWith('https://us.i.posthog.com/
|
|
64
|
+
expect(fetch).toHaveBeenCalledWith('https://us.i.posthog.com/flags/?v=2', {
|
|
65
65
|
body: JSON.stringify({
|
|
66
66
|
token: 'TEST_API_KEY',
|
|
67
67
|
distinct_id: posthog.getDistinctId(),
|
|
@@ -95,4 +95,195 @@ describe('PostHogWeb', () => {
|
|
|
95
95
|
})
|
|
96
96
|
})
|
|
97
97
|
})
|
|
98
|
+
|
|
99
|
+
describe('History API tracking', () => {
|
|
100
|
+
const originalPushState = window.history.pushState
|
|
101
|
+
const originalReplaceState = window.history.replaceState
|
|
102
|
+
let setPathname: (pathname: string) => void
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
const mockLocation = {
|
|
106
|
+
pathname: '/initial-path',
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Object.defineProperty(window, 'location', {
|
|
110
|
+
value: mockLocation,
|
|
111
|
+
writable: true,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
setPathname = (pathname: string) => {
|
|
115
|
+
mockLocation.pathname = pathname
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Reset history methods after each test
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
window.history.pushState = originalPushState
|
|
122
|
+
window.history.replaceState = originalReplaceState
|
|
123
|
+
|
|
124
|
+
const popstateHandler = (): void => {}
|
|
125
|
+
window.addEventListener('popstate', popstateHandler)
|
|
126
|
+
window.removeEventListener('popstate', popstateHandler)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should not patch history methods when captureHistoryEvents is disabled', () => {
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
131
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
132
|
+
captureHistoryEvents: false,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(window.history.pushState).toBe(originalPushState)
|
|
136
|
+
expect(window.history.replaceState).toBe(originalReplaceState)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should patch history methods when captureHistoryEvents is enabled', () => {
|
|
140
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
141
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
142
|
+
captureHistoryEvents: true,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(window.history.pushState).not.toBe(originalPushState)
|
|
146
|
+
expect(window.history.replaceState).not.toBe(originalReplaceState)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should capture pageview events on pushState when pathname changes', async () => {
|
|
150
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
151
|
+
captureHistoryEvents: true,
|
|
152
|
+
flushAt: 1,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const captureSpy = jest.spyOn(posthog, 'capture')
|
|
156
|
+
|
|
157
|
+
// Change pathname
|
|
158
|
+
setPathname('/test-page')
|
|
159
|
+
window.history.pushState({}, '', '/test-page')
|
|
160
|
+
|
|
161
|
+
expect(captureSpy).toHaveBeenCalledWith(
|
|
162
|
+
'$pageview',
|
|
163
|
+
expect.objectContaining({
|
|
164
|
+
navigation_type: 'pushState',
|
|
165
|
+
})
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should not capture pageview events on pushState when pathname does not change', async () => {
|
|
170
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
171
|
+
captureHistoryEvents: true,
|
|
172
|
+
flushAt: 1,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
const captureSpy = jest.spyOn(posthog, 'capture')
|
|
176
|
+
captureSpy.mockClear()
|
|
177
|
+
|
|
178
|
+
// Don't change pathname
|
|
179
|
+
window.history.pushState({}, '', '/initial-path')
|
|
180
|
+
|
|
181
|
+
expect(captureSpy).not.toHaveBeenCalled()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should capture pageview events on replaceState when pathname changes', async () => {
|
|
185
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
186
|
+
captureHistoryEvents: true,
|
|
187
|
+
flushAt: 1,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const captureSpy = jest.spyOn(posthog, 'capture')
|
|
191
|
+
|
|
192
|
+
// Change pathname
|
|
193
|
+
setPathname('/replaced-page')
|
|
194
|
+
window.history.replaceState({}, '', '/replaced-page')
|
|
195
|
+
|
|
196
|
+
expect(captureSpy).toHaveBeenCalledWith(
|
|
197
|
+
'$pageview',
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
navigation_type: 'replaceState',
|
|
200
|
+
})
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should not capture pageview events on replaceState when pathname does not change', async () => {
|
|
205
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
206
|
+
captureHistoryEvents: true,
|
|
207
|
+
flushAt: 1,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const captureSpy = jest.spyOn(posthog, 'capture')
|
|
211
|
+
captureSpy.mockClear()
|
|
212
|
+
|
|
213
|
+
// Don't change pathname
|
|
214
|
+
window.history.replaceState({}, '', '/initial-path')
|
|
215
|
+
|
|
216
|
+
expect(captureSpy).not.toHaveBeenCalled()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should capture pageview events on popstate when pathname changes', async () => {
|
|
220
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
221
|
+
captureHistoryEvents: true,
|
|
222
|
+
flushAt: 1,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const captureSpy = jest.spyOn(posthog, 'capture')
|
|
226
|
+
|
|
227
|
+
// Change pathname
|
|
228
|
+
setPathname('/popstate-page')
|
|
229
|
+
window.dispatchEvent(new Event('popstate'))
|
|
230
|
+
|
|
231
|
+
expect(captureSpy).toHaveBeenCalledWith(
|
|
232
|
+
'$pageview',
|
|
233
|
+
expect.objectContaining({
|
|
234
|
+
navigation_type: 'popstate',
|
|
235
|
+
})
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should not capture pageview events on popstate when pathname does not change', async () => {
|
|
240
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
241
|
+
captureHistoryEvents: true,
|
|
242
|
+
flushAt: 1,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const captureSpy = jest.spyOn(posthog, 'capture')
|
|
246
|
+
captureSpy.mockClear()
|
|
247
|
+
|
|
248
|
+
// Don't change pathname
|
|
249
|
+
window.dispatchEvent(new Event('popstate'))
|
|
250
|
+
|
|
251
|
+
expect(captureSpy).not.toHaveBeenCalled()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should include navigation properties in capture call and rely on getCommonEventProperties', async () => {
|
|
255
|
+
const posthog = new PostHog('TEST_API_KEY', {
|
|
256
|
+
captureHistoryEvents: true,
|
|
257
|
+
flushAt: 1,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const commonEventProps = { $lib: 'posthog-js-lite', $lib_version: '1.0.0' }
|
|
261
|
+
const getCommonEventPropertiesSpy = jest
|
|
262
|
+
.spyOn(posthog, 'getCommonEventProperties')
|
|
263
|
+
.mockImplementation(() => commonEventProps)
|
|
264
|
+
|
|
265
|
+
const coreCaptureMethod = jest.spyOn(PostHog.prototype, 'capture')
|
|
266
|
+
|
|
267
|
+
// Change pathname
|
|
268
|
+
setPathname('/captured-page')
|
|
269
|
+
window.history.pushState({}, '', '/captured-page')
|
|
270
|
+
|
|
271
|
+
// Will use a mock here for now and rely on the implementation since the tests setup is very simple at the moment
|
|
272
|
+
expect(getCommonEventPropertiesSpy).toHaveBeenCalled()
|
|
273
|
+
|
|
274
|
+
const captureCall = coreCaptureMethod.mock.calls.find(
|
|
275
|
+
(call) => call[0] === '$pageview' && call[1] && call[1].navigation_type === 'pushState'
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(captureCall).toBeDefined()
|
|
279
|
+
|
|
280
|
+
const navigationProperties = {
|
|
281
|
+
navigation_type: 'pushState',
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (captureCall) {
|
|
285
|
+
expect(captureCall[1]).toMatchObject(navigationProperties)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
})
|
|
98
289
|
})
|