@leanbase-giangnd/js 0.3.1 → 0.4.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.
- package/dist/index.cjs +836 -85
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +59 -43
- package/dist/index.mjs +837 -86
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +2577 -510
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +2 -1
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +104 -17
- package/src/extensions/replay/session-recording.ts +9 -6
- package/src/leanbase.ts +4 -1
- package/src/utils/request-router.ts +39 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leanbase-giangnd/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Leanbase browser SDK - event tracking, autocapture, and session replay",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@posthog/core": "workspace:*",
|
|
37
37
|
"@rrweb/record": "2.0.0-alpha.17",
|
|
38
|
+
"@rrweb/rrweb-plugin-console-record": "2.0.0-alpha.17",
|
|
38
39
|
"fflate": "^0.4.8"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
@@ -318,12 +318,18 @@ export class LazyLoadedSessionRecording {
|
|
|
318
318
|
* Util to help developers working on this feature manually override
|
|
319
319
|
*/
|
|
320
320
|
private _forceAllowLocalhostNetworkCapture = false
|
|
321
|
+
private _debug(...args: any[]) {
|
|
322
|
+
if (this._instance?.config?.debug) {
|
|
323
|
+
logger.info(...args)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
321
326
|
// "Started" is only true once we have a valid rrweb stop handler AND we have marked recording enabled.
|
|
322
327
|
// If rrweb fails to initialize, we permanently disable replay for this page load.
|
|
323
328
|
private _recording: { stop: listenerHandler } | undefined
|
|
324
329
|
private _stopRrweb: listenerHandler | undefined = undefined
|
|
325
330
|
private _permanentlyDisabled: boolean = false
|
|
326
331
|
private _loggedPermanentlyDisabled: boolean = false
|
|
332
|
+
private _hasReportedRecordingInitialized: boolean = false
|
|
327
333
|
private _lastActivityTimestamp: number = Date.now()
|
|
328
334
|
/**
|
|
329
335
|
* if pageview capture is disabled,
|
|
@@ -536,31 +542,84 @@ export class LazyLoadedSessionRecording {
|
|
|
536
542
|
}
|
|
537
543
|
}
|
|
538
544
|
|
|
539
|
-
private
|
|
545
|
+
private async _loadConsolePlugin(): Promise<RecordPlugin | null> {
|
|
546
|
+
try {
|
|
547
|
+
const mod: any = await import('@rrweb/rrweb-plugin-console-record')
|
|
548
|
+
const factory = mod?.getRecordConsolePlugin ?? mod?.default?.getRecordConsolePlugin
|
|
549
|
+
|
|
550
|
+
if (typeof factory === 'function') {
|
|
551
|
+
const plugin = factory()
|
|
552
|
+
this._debug('Console plugin loaded')
|
|
553
|
+
return plugin
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
logger.warn('console plugin factory unavailable after import')
|
|
557
|
+
} catch (e) {
|
|
558
|
+
logger.warn('could not load console plugin', e)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return null
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private async _loadNetworkPlugin(
|
|
565
|
+
networkPayloadCapture: Pick<
|
|
566
|
+
NetworkRecordOptions,
|
|
567
|
+
'recordHeaders' | 'recordBody' | 'recordPerformance' | 'payloadHostDenyList'
|
|
568
|
+
>
|
|
569
|
+
): Promise<RecordPlugin | null> {
|
|
570
|
+
try {
|
|
571
|
+
const mod: any = await import('./network-plugin')
|
|
572
|
+
const factory = mod?.getRecordNetworkPlugin ?? mod?.default?.getRecordNetworkPlugin
|
|
573
|
+
|
|
574
|
+
if (typeof factory === 'function') {
|
|
575
|
+
const options = buildNetworkRequestOptions(this._instance.config, networkPayloadCapture)
|
|
576
|
+
const plugin = factory(options)
|
|
577
|
+
this._debug('Network plugin loaded')
|
|
578
|
+
return plugin
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
logger.warn('network plugin factory unavailable after import')
|
|
582
|
+
} catch (e) {
|
|
583
|
+
logger.warn('could not load network plugin', e)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return null
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private async _gatherRRWebPlugins(): Promise<RecordPlugin[]> {
|
|
540
590
|
const plugins: RecordPlugin[] = []
|
|
541
591
|
|
|
592
|
+
if (!window) {
|
|
593
|
+
return plugins
|
|
594
|
+
}
|
|
595
|
+
|
|
542
596
|
if (this._isConsoleLogCaptureEnabled) {
|
|
543
|
-
|
|
597
|
+
const consolePlugin = await this._loadConsolePlugin()
|
|
598
|
+
if (consolePlugin) {
|
|
599
|
+
plugins.push(consolePlugin)
|
|
600
|
+
}
|
|
544
601
|
}
|
|
545
602
|
|
|
546
603
|
if (this._networkPayloadCapture) {
|
|
547
604
|
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture
|
|
548
605
|
|
|
549
606
|
if (canRecordNetwork) {
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
plugins.push(
|
|
554
|
-
networkFactory(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture))
|
|
555
|
-
)
|
|
556
|
-
} else {
|
|
557
|
-
logger.info('Network plugin factory not available yet; skipping network plugin')
|
|
607
|
+
const networkPlugin = await this._loadNetworkPlugin(this._networkPayloadCapture)
|
|
608
|
+
if (networkPlugin) {
|
|
609
|
+
plugins.push(networkPlugin)
|
|
558
610
|
}
|
|
559
611
|
} else {
|
|
560
|
-
|
|
612
|
+
this._debug('NetworkCapture not started because we are on localhost.')
|
|
561
613
|
}
|
|
562
614
|
}
|
|
563
615
|
|
|
616
|
+
if (plugins.length > 0) {
|
|
617
|
+
this._debug(
|
|
618
|
+
'Replay plugins loaded',
|
|
619
|
+
plugins.map((p) => p.name)
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
|
|
564
623
|
return plugins
|
|
565
624
|
}
|
|
566
625
|
|
|
@@ -886,7 +945,13 @@ export class LazyLoadedSessionRecording {
|
|
|
886
945
|
}
|
|
887
946
|
|
|
888
947
|
if (this.status === ACTIVE) {
|
|
889
|
-
|
|
948
|
+
const reason = startReason || 'recording_initialized'
|
|
949
|
+
if (reason !== 'recording_initialized' || !this._hasReportedRecordingInitialized) {
|
|
950
|
+
if (reason === 'recording_initialized') {
|
|
951
|
+
this._hasReportedRecordingInitialized = true
|
|
952
|
+
}
|
|
953
|
+
this._reportStarted(reason)
|
|
954
|
+
}
|
|
890
955
|
}
|
|
891
956
|
}
|
|
892
957
|
|
|
@@ -942,19 +1007,36 @@ export class LazyLoadedSessionRecording {
|
|
|
942
1007
|
this._recording = undefined
|
|
943
1008
|
this._stopRrweb = undefined
|
|
944
1009
|
this._isFullyReady = false
|
|
1010
|
+
this._hasReportedRecordingInitialized = false
|
|
945
1011
|
|
|
946
1012
|
logger.info('stopped')
|
|
947
1013
|
}
|
|
948
1014
|
|
|
949
1015
|
private _snapshotIngestionUrl(): string | null {
|
|
950
1016
|
const endpointFor = (this._instance as any)?.requestRouter?.endpointFor
|
|
951
|
-
|
|
1017
|
+
|
|
1018
|
+
// Prefer requestRouter (parity with Browser SDK)
|
|
1019
|
+
if (typeof endpointFor === 'function') {
|
|
1020
|
+
try {
|
|
1021
|
+
return endpointFor('api', this._endpoint)
|
|
1022
|
+
} catch {
|
|
1023
|
+
return null
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Fallback: construct from host/api_host if requestRouter is unavailable (older IIFE builds)
|
|
1028
|
+
const host = (this._instance.config.api_host || this._instance.config.host || '').trim()
|
|
1029
|
+
if (!host) {
|
|
952
1030
|
return null
|
|
953
1031
|
}
|
|
1032
|
+
|
|
954
1033
|
try {
|
|
955
|
-
|
|
1034
|
+
// eslint-disable-next-line compat/compat
|
|
1035
|
+
return new URL(this._endpoint, host).href
|
|
956
1036
|
} catch {
|
|
957
|
-
|
|
1037
|
+
const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host
|
|
1038
|
+
const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint : `/${this._endpoint}`
|
|
1039
|
+
return `${normalizedHost}${normalizedEndpoint}`
|
|
958
1040
|
}
|
|
959
1041
|
}
|
|
960
1042
|
|
|
@@ -1315,7 +1397,12 @@ export class LazyLoadedSessionRecording {
|
|
|
1315
1397
|
this._instance.registerForSession({
|
|
1316
1398
|
$session_recording_start_reason: startReason,
|
|
1317
1399
|
})
|
|
1318
|
-
|
|
1400
|
+
const message = startReason.replace('_', ' ')
|
|
1401
|
+
if (typeof tagPayload === 'undefined') {
|
|
1402
|
+
logger.info(message)
|
|
1403
|
+
} else {
|
|
1404
|
+
logger.info(message, tagPayload)
|
|
1405
|
+
}
|
|
1319
1406
|
if (!includes(['recording_initialized', 'session_id_changed'], startReason)) {
|
|
1320
1407
|
this._tryAddCustomEvent(startReason, tagPayload)
|
|
1321
1408
|
}
|
|
@@ -1547,7 +1634,7 @@ export class LazyLoadedSessionRecording {
|
|
|
1547
1634
|
return
|
|
1548
1635
|
}
|
|
1549
1636
|
|
|
1550
|
-
const activePlugins = this._gatherRRWebPlugins()
|
|
1637
|
+
const activePlugins = await this._gatherRRWebPlugins()
|
|
1551
1638
|
|
|
1552
1639
|
let stopHandler: listenerHandler | undefined
|
|
1553
1640
|
try {
|
|
@@ -25,6 +25,12 @@ export class SessionRecording {
|
|
|
25
25
|
private _persistFlagsOnSessionListener: (() => void) | undefined = undefined
|
|
26
26
|
private _lazyLoadedSessionRecording: LazyLoadedSessionRecording | undefined
|
|
27
27
|
|
|
28
|
+
private _debug(...args: any[]) {
|
|
29
|
+
if (this._instance?.config?.debug) {
|
|
30
|
+
log.info(...args)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
public get started(): boolean {
|
|
29
35
|
return !!this._lazyLoadedSessionRecording?.isStarted
|
|
30
36
|
}
|
|
@@ -66,8 +72,10 @@ export class SessionRecording {
|
|
|
66
72
|
|
|
67
73
|
const canRunReplay = !isUndefined(Object.assign) && !isUndefined(Array.from)
|
|
68
74
|
if (this._isRecordingEnabled && canRunReplay) {
|
|
75
|
+
this._debug('Session replay enabled; starting recorder')
|
|
69
76
|
this._lazyLoadAndStart(startReason)
|
|
70
77
|
} else {
|
|
78
|
+
this._debug('Session replay disabled; stopping recorder')
|
|
71
79
|
this.stopRecording()
|
|
72
80
|
}
|
|
73
81
|
}
|
|
@@ -156,13 +164,8 @@ export class SessionRecording {
|
|
|
156
164
|
log.info('skipping remote config with no sessionRecording', response)
|
|
157
165
|
return
|
|
158
166
|
}
|
|
159
|
-
this._receivedFlags = true
|
|
160
|
-
|
|
161
|
-
if (response.sessionRecording === false) {
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
167
|
this._persistRemoteConfig(response)
|
|
168
|
+
this._receivedFlags = true
|
|
166
169
|
this.startIfEnabledOrStop()
|
|
167
170
|
}
|
|
168
171
|
|
package/src/leanbase.ts
CHANGED
|
@@ -43,7 +43,7 @@ import { PageViewManager } from './page-view'
|
|
|
43
43
|
import { ScrollManager } from './scroll-manager'
|
|
44
44
|
import { isLikelyBot } from './utils/blocked-uas'
|
|
45
45
|
import { SessionRecording } from './extensions/replay/session-recording'
|
|
46
|
-
import
|
|
46
|
+
import { RequestRouter } from './utils/request-router'
|
|
47
47
|
import type { ConsentManager } from '../../browser/lib/src/consent'
|
|
48
48
|
|
|
49
49
|
const defaultConfig = (): LeanbaseConfig => ({
|
|
@@ -124,6 +124,9 @@ export class Leanbase extends PostHogCore {
|
|
|
124
124
|
this.isLoaded = true
|
|
125
125
|
this.persistence = new LeanbasePersistence(this.config)
|
|
126
126
|
|
|
127
|
+
// Browser SDK always has a requestRouter; session replay relies on it for $snapshot ingestion URLs.
|
|
128
|
+
this.requestRouter = new RequestRouter(this)
|
|
129
|
+
|
|
127
130
|
if (this.config.cookieless_mode !== 'always') {
|
|
128
131
|
this.sessionManager = new SessionIdManager(this)
|
|
129
132
|
this.sessionPropsManager = new SessionPropsManager(this, this.sessionManager, this.persistence)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Leanbase } from '../leanbase'
|
|
2
|
+
|
|
3
|
+
export type RequestRouterTarget = 'api' | 'ui' | 'assets'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Leanbase-local version of PostHog's RequestRouter.
|
|
7
|
+
*
|
|
8
|
+
* Browser SDK always has a requestRouter instance; Leanbase IIFE needs this too so
|
|
9
|
+
* features like Session Replay can construct ingestion URLs.
|
|
10
|
+
*/
|
|
11
|
+
export class RequestRouter {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
13
|
+
constructor(private readonly instance: Leanbase) {}
|
|
14
|
+
|
|
15
|
+
get apiHost(): string {
|
|
16
|
+
const configured = (this.instance.config.api_host || this.instance.config.host || '').trim()
|
|
17
|
+
return configured.replace(/\/$/, '')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get uiHost(): string | undefined {
|
|
21
|
+
const configured = this.instance.config.ui_host?.trim().replace(/\/$/, '')
|
|
22
|
+
return configured || undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
endpointFor(target: RequestRouterTarget, path: string = ''): string {
|
|
26
|
+
if (path) {
|
|
27
|
+
path = path[0] === '/' ? path : `/${path}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (target === 'ui') {
|
|
31
|
+
const host = this.uiHost || this.apiHost
|
|
32
|
+
return host + path
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Leanbase doesn't currently do region-based routing; default to apiHost.
|
|
36
|
+
// Browser's router has special handling for assets; we keep parity in interface, not domains.
|
|
37
|
+
return this.apiHost + path
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.4.0'
|