@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/package.json CHANGED
@@ -1,48 +1,47 @@
1
1
  {
2
- "name": "@leanbase-giangnd/js",
3
- "version": "0.1.5",
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
- "scripts": {
17
- "clean": "rimraf dist coverage",
18
- "test:unit": "jest -c jest.config.js",
19
- "lint": "eslint src test",
20
- "lint:fix": "eslint src test --fix",
21
- "prebuild": "node -p \"'export const version = \\'' + require('./package.json').version + '\\''\" > src/version.ts",
22
- "build": "rollup -c",
23
- "dev": "rollup -c -w",
24
- "prepublishOnly": "pnpm lint && pnpm test:unit && pnpm build",
25
- "package": "mkdir -p ../../target && pnpm pack --pack-destination ../../target"
26
- },
27
- "files": [
28
- "dist",
29
- "src",
30
- "README.md"
31
- ],
32
- "publishConfig": {
33
- "access": "public"
34
- },
35
- "dependencies": {
36
- "@posthog/core": "workspace:*",
37
- "@rrweb/record": "2.0.0-alpha.17",
38
- "fflate": "^0.4.8"
39
- },
40
- "devDependencies": {
41
- "@posthog-tooling/tsconfig-base": "workspace:*",
42
- "@posthog-tooling/rollup-utils": "workspace:*",
43
- "jest": "catalog:",
44
- "jest-environment-jsdom": "catalog:",
45
- "rollup": "catalog:",
46
- "rimraf": "^6.0.1"
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
- import { record as rrwebRecord } from '@rrweb/record'
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: rrwebRecord,
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
- return rrwebRecord as unknown as rrwebRecordType
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
- const rrwebRecord = getRRWebRecord()
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
- this._lazyLoadedSessionRecording.start(startReason)
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
  /**
@@ -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.5'
1
+ export const version = '0.2.2'