@leanbase-giangnd/js 0.1.5 → 0.2.2
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/LICENSE +37 -0
- package/dist/index.cjs +180 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.mjs +180 -10
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +5329 -80
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +46 -47
- package/src/extensions/replay/extension-shim.ts +102 -3
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +47 -6
- package/src/extensions/replay/session-recording.ts +52 -2
- package/src/utils/index.ts +3 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,48 +1,47 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
2
|
+
"name": "@leanbase-giangnd/js",
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Leanbase browser SDK - event tracking, autocapture, and session replay",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"directory": "packages/leanbase"
|
|
8
|
+
},
|
|
9
|
+
"author": "leanbase",
|
|
10
|
+
"license": "Copyrighted by Leanflag Limited",
|
|
11
|
+
"main": "dist/index.cjs",
|
|
12
|
+
"module": "dist/index.mjs",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"unpkg": "dist/leanbase.iife.js",
|
|
15
|
+
"jsdelivr": "dist/leanbase.iife.js",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@rrweb/record": "2.0.0-alpha.17",
|
|
26
|
+
"fflate": "^0.4.8",
|
|
27
|
+
"@posthog/core": "1.3.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"jest": "^29.7.0",
|
|
31
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
32
|
+
"rollup": "^4.44.1",
|
|
33
|
+
"rimraf": "^6.0.1",
|
|
34
|
+
"@posthog-tooling/tsconfig-base": "1.0.0",
|
|
35
|
+
"@posthog-tooling/rollup-utils": "1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"clean": "rimraf dist coverage",
|
|
39
|
+
"test:unit": "jest -c jest.config.js",
|
|
40
|
+
"lint": "eslint src test",
|
|
41
|
+
"lint:fix": "eslint src test --fix",
|
|
42
|
+
"prebuild": "node -p \"'export const version = \\'' + require('./package.json').version + '\\''\" > src/version.ts",
|
|
43
|
+
"build": "rollup -c",
|
|
44
|
+
"dev": "rollup -c -w",
|
|
45
|
+
"package": "mkdir -p ../../target && pnpm pack --pack-destination ../../target"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -1,5 +1,103 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
1
2
|
import { window as win } from '../../utils'
|
|
2
|
-
|
|
3
|
+
|
|
4
|
+
// We avoid importing '@rrweb/record' at module load time to prevent IIFE builds
|
|
5
|
+
// from requiring a top-level global. Instead, expose a lazy proxy that will
|
|
6
|
+
// dynamically import the module the first time it's used.
|
|
7
|
+
|
|
8
|
+
let _cachedRRWeb: any | null = null
|
|
9
|
+
|
|
10
|
+
async function _loadRRWebModule(): Promise<any> {
|
|
11
|
+
if (_cachedRRWeb) return _cachedRRWeb
|
|
12
|
+
try {
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
const mod: any = await import('@rrweb/record')
|
|
15
|
+
_cachedRRWeb = mod
|
|
16
|
+
return _cachedRRWeb
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// queue for method calls before rrweb loads
|
|
23
|
+
const _queuedCalls: Array<() => void> = []
|
|
24
|
+
|
|
25
|
+
// Create a proxy function that delegates to the real rrweb.record when called
|
|
26
|
+
const rrwebRecordProxy: any = function (...args: any[]) {
|
|
27
|
+
let realStop: (() => void) | undefined
|
|
28
|
+
let calledReal = false
|
|
29
|
+
|
|
30
|
+
// Start loading asynchronously and call the real record when available
|
|
31
|
+
void (async () => {
|
|
32
|
+
const mod = await _loadRRWebModule()
|
|
33
|
+
const real = mod && (mod.record ?? mod.default?.record)
|
|
34
|
+
if (real) {
|
|
35
|
+
try {
|
|
36
|
+
calledReal = true
|
|
37
|
+
realStop = real(...args)
|
|
38
|
+
// flush any queued calls that were waiting for rrweb
|
|
39
|
+
while (_queuedCalls.length) {
|
|
40
|
+
try {
|
|
41
|
+
const fn = _queuedCalls.shift()!
|
|
42
|
+
fn()
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})()
|
|
52
|
+
|
|
53
|
+
// return a stop function that will call the real stop when available
|
|
54
|
+
return () => {
|
|
55
|
+
if (realStop) {
|
|
56
|
+
try {
|
|
57
|
+
realStop()
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// ignore
|
|
60
|
+
}
|
|
61
|
+
} else if (!calledReal) {
|
|
62
|
+
// If rrweb hasn't been initialised yet, queue a stop request that will
|
|
63
|
+
// call the real stop once available.
|
|
64
|
+
_queuedCalls.push(() => {
|
|
65
|
+
try {
|
|
66
|
+
realStop?.()
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// ignore
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// methods that can be called on the rrweb.record object - queue until real module is available
|
|
76
|
+
rrwebRecordProxy.addCustomEvent = function (tag?: string, payload?: any) {
|
|
77
|
+
const call = () => {
|
|
78
|
+
try {
|
|
79
|
+
const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record)
|
|
80
|
+
real?.addCustomEvent?.(tag, payload)
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (_cachedRRWeb) call()
|
|
86
|
+
else _queuedCalls.push(call)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
rrwebRecordProxy.takeFullSnapshot = function () {
|
|
90
|
+
const call = () => {
|
|
91
|
+
try {
|
|
92
|
+
const real = _cachedRRWeb && (_cachedRRWeb.record ?? _cachedRRWeb.default?.record)
|
|
93
|
+
real?.takeFullSnapshot?.()
|
|
94
|
+
} catch (e) {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (_cachedRRWeb) call()
|
|
99
|
+
else _queuedCalls.push(call)
|
|
100
|
+
}
|
|
3
101
|
// Delay importing heavy modules to avoid circular dependencies at build time.
|
|
4
102
|
// They will be required lazily when used at runtime.
|
|
5
103
|
// We avoid requiring the lazy-loaded recorder here to prevent circular dependencies during bundling.
|
|
@@ -12,9 +110,10 @@ const _target: any = (win as any) ?? (globalThis as any)
|
|
|
12
110
|
|
|
13
111
|
_target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {}
|
|
14
112
|
|
|
15
|
-
// Expose rrweb.record under the same contract
|
|
113
|
+
// Expose rrweb.record under the same contract. We provide a lazy proxy so
|
|
114
|
+
// builds that execute this file don't require rrweb at module evaluation time.
|
|
16
115
|
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
|
|
17
|
-
record:
|
|
116
|
+
record: rrwebRecordProxy,
|
|
18
117
|
}
|
|
19
118
|
|
|
20
119
|
// Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint-disable posthog-js/no-direct-function-check */
|
|
2
|
-
import { record as rrwebRecord } from '@rrweb/record'
|
|
3
2
|
import '../extension-shim'
|
|
4
3
|
import { clampToRange, includes, isBoolean, isNullish, isNumber, isObject, isString, isUndefined } from '@posthog/core'
|
|
5
4
|
import type { recordOptions, rrwebRecord as rrwebRecordType } from '../types/rrweb'
|
|
@@ -145,7 +144,43 @@ function getRRWebRecord(): rrwebRecordType | undefined {
|
|
|
145
144
|
// ignore
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
|
|
147
|
+
// If we've previously loaded rrweb via dynamic import, return the cached reference
|
|
148
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
149
|
+
const cached = (getRRWebRecord as any)._cachedRRWebRecord as rrwebRecordType | undefined
|
|
150
|
+
return cached as unknown as rrwebRecordType | undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function loadRRWeb(): Promise<rrwebRecordType | null> {
|
|
154
|
+
try {
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
156
|
+
const ext = (globalThis as any).__PosthogExtensions__
|
|
157
|
+
if (ext && ext.rrweb && ext.rrweb.record) {
|
|
158
|
+
;(getRRWebRecord as any)._cachedRRWebRecord = ext.rrweb.record as unknown as rrwebRecordType
|
|
159
|
+
return ext.rrweb.record as unknown as rrwebRecordType
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If already cached, return it
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
164
|
+
const already = (getRRWebRecord as any)._cachedRRWebRecord as rrwebRecordType | undefined
|
|
165
|
+
if (already) {
|
|
166
|
+
return already
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Dynamic import - let the bundler (IIFE build) include rrweb in the bundle or allow lazy-load
|
|
170
|
+
// Note: we intentionally use a dynamic import so rrweb is not referenced at the module top-level
|
|
171
|
+
// which would cause IIFE builds to assume a global is present at script execution.
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
const mod: any = await import('@rrweb/record')
|
|
174
|
+
const rr = (mod && (mod.record ?? (mod.default && mod.default.record))) as rrwebRecordType
|
|
175
|
+
if (rr) {
|
|
176
|
+
;(getRRWebRecord as any)._cachedRRWebRecord = rr
|
|
177
|
+
return rr
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
logger.error('could not dynamically load rrweb', e)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null
|
|
149
184
|
}
|
|
150
185
|
|
|
151
186
|
export type compressedFullSnapshotEvent = {
|
|
@@ -681,7 +716,7 @@ export class LazyLoadedSessionRecording {
|
|
|
681
716
|
return parsedConfig as SessionRecordingPersistedConfig
|
|
682
717
|
}
|
|
683
718
|
|
|
684
|
-
start(startReason?: SessionStartReason) {
|
|
719
|
+
async start(startReason?: SessionStartReason) {
|
|
685
720
|
const config = this._remoteConfig
|
|
686
721
|
if (!config) {
|
|
687
722
|
logger.info('remote config must be stored in persistence before recording can start')
|
|
@@ -722,7 +757,7 @@ export class LazyLoadedSessionRecording {
|
|
|
722
757
|
})
|
|
723
758
|
|
|
724
759
|
this._makeSamplingDecision(this.sessionId)
|
|
725
|
-
this._startRecorder()
|
|
760
|
+
await this._startRecorder()
|
|
726
761
|
|
|
727
762
|
// calling addEventListener multiple times is safe and will not add duplicates
|
|
728
763
|
addEventListener(window, 'beforeunload', this._onBeforeUnload)
|
|
@@ -1302,7 +1337,7 @@ export class LazyLoadedSessionRecording {
|
|
|
1302
1337
|
}
|
|
1303
1338
|
}
|
|
1304
1339
|
|
|
1305
|
-
private _startRecorder() {
|
|
1340
|
+
private async _startRecorder() {
|
|
1306
1341
|
if (this._stopRrweb) {
|
|
1307
1342
|
return
|
|
1308
1343
|
}
|
|
@@ -1353,7 +1388,13 @@ export class LazyLoadedSessionRecording {
|
|
|
1353
1388
|
sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined
|
|
1354
1389
|
}
|
|
1355
1390
|
|
|
1356
|
-
|
|
1391
|
+
// Ensure rrweb is loaded (either via global extension or dynamic import)
|
|
1392
|
+
let rrwebRecord = getRRWebRecord()
|
|
1393
|
+
if (!rrwebRecord) {
|
|
1394
|
+
const loaded = await loadRRWeb()
|
|
1395
|
+
rrwebRecord = loaded ?? undefined
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1357
1398
|
if (!rrwebRecord) {
|
|
1358
1399
|
logger.error(
|
|
1359
1400
|
'_startRecorder was called but rrwebRecord is not available. This indicates something has gone wrong.'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable posthog-js/no-direct-function-check */
|
|
1
2
|
import { SESSION_RECORDING_IS_SAMPLED, SESSION_RECORDING_REMOTE_CONFIG } from '../../constants'
|
|
2
3
|
import { Leanbase } from '../../leanbase'
|
|
3
4
|
import { Properties, RemoteConfig, SessionRecordingPersistedConfig, SessionStartReason } from '../../types'
|
|
@@ -5,7 +6,7 @@ import { type eventWithTime } from './types/rrweb-types'
|
|
|
5
6
|
|
|
6
7
|
import { isNullish, isUndefined } from '@posthog/core'
|
|
7
8
|
import { logger } from '../../leanbase-logger'
|
|
8
|
-
import { window } from '../../utils'
|
|
9
|
+
import { assignableWindow, window } from '../../utils'
|
|
9
10
|
import { LazyLoadedSessionRecording } from './external/lazy-loaded-session-recorder'
|
|
10
11
|
import { DISABLED, LAZY_LOADING, SessionRecordingStatus, TriggerType } from './external/triggerMatching'
|
|
11
12
|
|
|
@@ -91,6 +92,19 @@ export class SessionRecording {
|
|
|
91
92
|
return
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// If extensions provide a loader, use it. Otherwise fallback to the local _onScriptLoaded which
|
|
96
|
+
// will create the local LazyLoadedSessionRecording (so tests that mock it work correctly).
|
|
97
|
+
const loader = assignableWindow.__PosthogExtensions__?.loadExternalDependency
|
|
98
|
+
if (typeof loader === 'function') {
|
|
99
|
+
loader(this._instance, this._scriptName as any, (err: any) => {
|
|
100
|
+
if (err) {
|
|
101
|
+
return log.error('could not load recorder', err)
|
|
102
|
+
}
|
|
103
|
+
this._onScriptLoaded(startReason)
|
|
104
|
+
})
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
94
108
|
this._onScriptLoaded(startReason)
|
|
95
109
|
}
|
|
96
110
|
|
|
@@ -188,14 +202,50 @@ export class SessionRecording {
|
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
private get _scriptName() {
|
|
206
|
+
const remoteConfig: SessionRecordingPersistedConfig | undefined = this._instance?.persistence?.get_property(
|
|
207
|
+
SESSION_RECORDING_REMOTE_CONFIG
|
|
208
|
+
)
|
|
209
|
+
return (remoteConfig?.scriptConfig?.script as any) || 'lazy-recorder'
|
|
210
|
+
}
|
|
211
|
+
|
|
191
212
|
private _onScriptLoaded(startReason?: SessionStartReason) {
|
|
213
|
+
// If extensions provide an init function, use it. Otherwise, fall back to the local LazyLoadedSessionRecording
|
|
214
|
+
if (assignableWindow.__PosthogExtensions__?.initSessionRecording) {
|
|
215
|
+
if (!this._lazyLoadedSessionRecording) {
|
|
216
|
+
this._lazyLoadedSessionRecording = assignableWindow.__PosthogExtensions__?.initSessionRecording(
|
|
217
|
+
this._instance
|
|
218
|
+
)
|
|
219
|
+
;(this._lazyLoadedSessionRecording as any)._forceAllowLocalhostNetworkCapture =
|
|
220
|
+
this._forceAllowLocalhostNetworkCapture
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const maybePromise: any = this._lazyLoadedSessionRecording!.start(startReason)
|
|
225
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
226
|
+
maybePromise.catch((e: any) => logger.error('error starting session recording', e))
|
|
227
|
+
}
|
|
228
|
+
} catch (e: any) {
|
|
229
|
+
logger.error('error starting session recording', e)
|
|
230
|
+
}
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
192
234
|
if (!this._lazyLoadedSessionRecording) {
|
|
193
235
|
this._lazyLoadedSessionRecording = new LazyLoadedSessionRecording(this._instance)
|
|
194
236
|
;(this._lazyLoadedSessionRecording as any)._forceAllowLocalhostNetworkCapture =
|
|
195
237
|
this._forceAllowLocalhostNetworkCapture
|
|
196
238
|
}
|
|
197
239
|
|
|
198
|
-
|
|
240
|
+
// start may perform a dynamic import; handle both sync and Promise returns
|
|
241
|
+
try {
|
|
242
|
+
const maybePromise: any = this._lazyLoadedSessionRecording!.start(startReason)
|
|
243
|
+
if (maybePromise && typeof maybePromise.catch === 'function') {
|
|
244
|
+
maybePromise.catch((e: any) => logger.error('error starting session recording', e))
|
|
245
|
+
}
|
|
246
|
+
} catch (e: any) {
|
|
247
|
+
logger.error('error starting session recording', e)
|
|
248
|
+
}
|
|
199
249
|
}
|
|
200
250
|
|
|
201
251
|
/**
|
package/src/utils/index.ts
CHANGED
|
@@ -22,6 +22,9 @@ export const AbortController = global?.AbortController
|
|
|
22
22
|
export const userAgent = navigator?.userAgent
|
|
23
23
|
export { win as window }
|
|
24
24
|
|
|
25
|
+
// assignableWindow mirrors browser package's assignableWindow for extension loading shims
|
|
26
|
+
export const assignableWindow: (Window & typeof globalThis) | any = win ?? ({} as any)
|
|
27
|
+
|
|
25
28
|
export function eachArray<E = any>(
|
|
26
29
|
obj: E[] | null | undefined,
|
|
27
30
|
iterator: (value: E, key: number) => void | Breaker,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.2.2'
|