@leanbase-giangnd/js 0.1.2 → 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.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
- "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,23 +1,132 @@
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
- import { LazyLoadedSessionRecording } from './external/lazy-loaded-session-recorder'
4
- import { getRecordNetworkPlugin } from './external/network-plugin'
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
+ }
101
+ // Delay importing heavy modules to avoid circular dependencies at build time.
102
+ // They will be required lazily when used at runtime.
103
+ // We avoid requiring the lazy-loaded recorder here to prevent circular dependencies during bundling.
104
+ // Instead, `LazyLoadedSessionRecording` will register a factory on the global under
105
+ // `__PosthogExtensions__._initSessionRecordingFactory` when it loads.
106
+ type InitSessionRecordingFactory = (instance: any) => any
5
107
 
6
108
  // Use a safe global target (prefer `win`, fallback to globalThis)
7
109
  const _target: any = (win as any) ?? (globalThis as any)
8
110
 
9
111
  _target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {}
10
112
 
11
- // 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.
12
115
  _target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
13
- record: rrwebRecord,
116
+ record: rrwebRecordProxy,
14
117
  }
15
118
 
16
119
  // Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
17
120
  _target.__PosthogExtensions__.initSessionRecording =
18
121
  _target.__PosthogExtensions__.initSessionRecording ||
19
122
  ((instance: any) => {
20
- return new LazyLoadedSessionRecording(instance)
123
+ const factory: InitSessionRecordingFactory | undefined =
124
+ _target.__PosthogExtensions__._initSessionRecordingFactory
125
+ if (factory) {
126
+ return factory(instance)
127
+ }
128
+ // If no factory is registered yet, return undefined — callers should handle lazy-loading.
129
+ return undefined
21
130
  })
22
131
 
23
132
  // Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
@@ -29,7 +138,8 @@ _target.__PosthogExtensions__.loadExternalDependency =
29
138
 
30
139
  // Provide rrwebPlugins object with network plugin factory if not present
31
140
  _target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {}
141
+ // Default to undefined; the lazy-loaded recorder will register the real factory when it initializes.
32
142
  _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin =
33
- _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => getRecordNetworkPlugin)
143
+ _target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => undefined)
34
144
 
35
145
  export {}
@@ -1,4 +1,4 @@
1
- import { record as rrwebRecord } from '@rrweb/record'
1
+ /* eslint-disable posthog-js/no-direct-function-check */
2
2
  import '../extension-shim'
3
3
  import { clampToRange, includes, isBoolean, isNullish, isNumber, isObject, isString, isUndefined } from '@posthog/core'
4
4
  import type { recordOptions, rrwebRecord as rrwebRecordType } from '../types/rrweb'
@@ -11,7 +11,7 @@ import {
11
11
  RecordPlugin,
12
12
  } from '../types/rrweb-types'
13
13
  import { buildNetworkRequestOptions } from './config'
14
- import { getRecordNetworkPlugin } from './network-plugin'
14
+ // network plugin factory will be provided via __PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin
15
15
  import {
16
16
  ACTIVE,
17
17
  allMatchSessionRecordingStatus,
@@ -72,6 +72,21 @@ const FIVE_MINUTES = ONE_MINUTE * 5
72
72
 
73
73
  export const RECORDING_IDLE_THRESHOLD_MS = FIVE_MINUTES
74
74
 
75
+ // Register a factory on the global extensions object so the extension shim can
76
+ // instantiate a LazyLoadedSessionRecording without importing this module directly.
77
+ try {
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ const ext = (globalThis as any).__PosthogExtensions__
80
+ if (ext) {
81
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
82
+ ext._initSessionRecordingFactory =
83
+ ext._initSessionRecordingFactory || ((instance: any) => new LazyLoadedSessionRecording(instance))
84
+ }
85
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
86
+ } catch (e) {
87
+ // ignore
88
+ }
89
+
75
90
  export const RECORDING_MAX_EVENT_SIZE = ONE_KB * ONE_KB * 0.9 // ~1mb (with some wiggle room)
76
91
  export const RECORDING_BUFFER_TIMEOUT = 2000 // 2 seconds
77
92
  export const SESSION_RECORDING_BATCH_KEY = 'recordings'
@@ -129,7 +144,43 @@ function getRRWebRecord(): rrwebRecordType | undefined {
129
144
  // ignore
130
145
  }
131
146
 
132
- 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
133
184
  }
134
185
 
135
186
  export type compressedFullSnapshotEvent = {
@@ -468,11 +519,15 @@ export class LazyLoadedSessionRecording {
468
519
  const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture
469
520
 
470
521
  if (canRecordNetwork) {
471
- plugins.push(
472
- getRecordNetworkPlugin(
473
- buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)
522
+ const assignableWindow: any = globalThis
523
+ const networkFactory = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin?.()
524
+ if (typeof networkFactory === 'function') {
525
+ plugins.push(
526
+ networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture))
474
527
  )
475
- )
528
+ } else {
529
+ logger.info('Network plugin factory not available yet; skipping network plugin')
530
+ }
476
531
  } else {
477
532
  logger.info('NetworkCapture not started because we are on localhost.')
478
533
  }
@@ -661,7 +716,7 @@ export class LazyLoadedSessionRecording {
661
716
  return parsedConfig as SessionRecordingPersistedConfig
662
717
  }
663
718
 
664
- start(startReason?: SessionStartReason) {
719
+ async start(startReason?: SessionStartReason) {
665
720
  const config = this._remoteConfig
666
721
  if (!config) {
667
722
  logger.info('remote config must be stored in persistence before recording can start')
@@ -702,7 +757,7 @@ export class LazyLoadedSessionRecording {
702
757
  })
703
758
 
704
759
  this._makeSamplingDecision(this.sessionId)
705
- this._startRecorder()
760
+ await this._startRecorder()
706
761
 
707
762
  // calling addEventListener multiple times is safe and will not add duplicates
708
763
  addEventListener(window, 'beforeunload', this._onBeforeUnload)
@@ -1282,7 +1337,7 @@ export class LazyLoadedSessionRecording {
1282
1337
  }
1283
1338
  }
1284
1339
 
1285
- private _startRecorder() {
1340
+ private async _startRecorder() {
1286
1341
  if (this._stopRrweb) {
1287
1342
  return
1288
1343
  }
@@ -1333,7 +1388,13 @@ export class LazyLoadedSessionRecording {
1333
1388
  sessionRecordingOptions.blockSelector = this._masking.blockSelector ?? undefined
1334
1389
  }
1335
1390
 
1336
- 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
+
1337
1398
  if (!rrwebRecord) {
1338
1399
  logger.error(
1339
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.2'
1
+ export const version = '0.2.2'