@leanbase-giangnd/js 0.0.7 → 0.1.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/dist/index.cjs +2791 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +5439 -172
- package/dist/index.mjs +2792 -266
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +3427 -276
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +6 -5
- package/src/extensions/replay/extension-shim.ts +35 -0
- package/src/extensions/replay/external/config.ts +33 -25
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +78 -71
- package/src/extensions/replay/external/mutation-throttler.ts +4 -1
- package/src/extensions/replay/external/network-plugin.ts +10 -3
- package/src/extensions/replay/external/triggerMatching.ts +13 -13
- package/src/extensions/replay/rrweb-plugins/patch.ts +0 -7
- package/src/extensions/replay/session-recording.ts +29 -58
- package/src/leanbase.ts +35 -48
- package/src/types/fflate.d.ts +5 -0
- package/src/types/rrweb-record.d.ts +8 -0
- package/src/types.ts +130 -117
- package/src/utils/logger.ts +13 -51
- package/src/version.ts +1 -1
- package/src/extensions/replay/external/README.md +0 -5
- package/src/extensions/utils/stylesheet-loader.ts +0 -27
- package/src/posthog-core.ts +0 -12
- package/src/utils/globals.ts +0 -239
- package/src/utils/request-router.ts +0 -77
- package/src/utils/type-utils.ts +0 -139
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leanbase-giangnd/js",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Leanbase
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Leanbase browser SDK - event tracking, autocapture, and session replay",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"directory": "packages/leanbase"
|
|
@@ -34,14 +34,15 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@posthog/core": "workspace:*",
|
|
37
|
+
"@rrweb/record": "2.0.0-alpha.17",
|
|
37
38
|
"fflate": "^0.4.8"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
|
-
"@posthog-tooling/rollup-utils": "workspace:*",
|
|
41
41
|
"@posthog-tooling/tsconfig-base": "workspace:*",
|
|
42
|
+
"@posthog-tooling/rollup-utils": "workspace:*",
|
|
42
43
|
"jest": "catalog:",
|
|
43
44
|
"jest-environment-jsdom": "catalog:",
|
|
44
|
-
"
|
|
45
|
-
"
|
|
45
|
+
"rollup": "catalog:",
|
|
46
|
+
"rimraf": "^6.0.1"
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
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'
|
|
5
|
+
|
|
6
|
+
// Use a safe global target (prefer `win`, fallback to globalThis)
|
|
7
|
+
const _target: any = (win as any) ?? (globalThis as any)
|
|
8
|
+
|
|
9
|
+
_target.__PosthogExtensions__ = _target.__PosthogExtensions__ || {}
|
|
10
|
+
|
|
11
|
+
// Expose rrweb.record under the same contract
|
|
12
|
+
_target.__PosthogExtensions__.rrweb = _target.__PosthogExtensions__.rrweb || {
|
|
13
|
+
record: rrwebRecord,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Provide initSessionRecording if not present — return a new LazyLoadedSessionRecording when called
|
|
17
|
+
_target.__PosthogExtensions__.initSessionRecording =
|
|
18
|
+
_target.__PosthogExtensions__.initSessionRecording ||
|
|
19
|
+
((instance: any) => {
|
|
20
|
+
return new LazyLoadedSessionRecording(instance)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Provide a no-op loadExternalDependency that calls the callback immediately (since rrweb is bundled)
|
|
24
|
+
_target.__PosthogExtensions__.loadExternalDependency =
|
|
25
|
+
_target.__PosthogExtensions__.loadExternalDependency ||
|
|
26
|
+
((instance: any, scriptName: string, cb?: (err?: any) => void) => {
|
|
27
|
+
if (cb) cb(undefined)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
// Provide rrwebPlugins object with network plugin factory if not present
|
|
31
|
+
_target.__PosthogExtensions__.rrwebPlugins = _target.__PosthogExtensions__.rrwebPlugins || {}
|
|
32
|
+
_target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin =
|
|
33
|
+
_target.__PosthogExtensions__.rrwebPlugins.getRecordNetworkPlugin || (() => getRecordNetworkPlugin)
|
|
34
|
+
|
|
35
|
+
export {}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CapturedNetworkRequest,
|
|
2
|
-
import { isFunction, isNullish, isString, isUndefined } from '@posthog/core'
|
|
1
|
+
import { CapturedNetworkRequest, LeanbaseConfig, NetworkRecordOptions } from '../../../types'
|
|
2
|
+
import { isArray, isBoolean, isFunction, isNullish, isString, isUndefined } from '@posthog/core'
|
|
3
3
|
import { convertToURL } from '../../../utils/request-utils'
|
|
4
4
|
import { logger } from '../../../utils/logger'
|
|
5
5
|
import { shouldCaptureValue } from '../../../autocapture-utils'
|
|
@@ -15,12 +15,11 @@ export const defaultNetworkOptions: Required<NetworkRecordOptions> = {
|
|
|
15
15
|
'beacon',
|
|
16
16
|
'body',
|
|
17
17
|
'css',
|
|
18
|
-
'early-
|
|
18
|
+
'early-hints',
|
|
19
19
|
'embed',
|
|
20
20
|
'fetch',
|
|
21
21
|
'frame',
|
|
22
22
|
'iframe',
|
|
23
|
-
'icon',
|
|
24
23
|
'image',
|
|
25
24
|
'img',
|
|
26
25
|
'input',
|
|
@@ -92,11 +91,17 @@ const PAYLOAD_CONTENT_DENY_LIST = [
|
|
|
92
91
|
const removeAuthorizationHeader = (data: CapturedNetworkRequest): CapturedNetworkRequest => {
|
|
93
92
|
const headers = data.requestHeaders
|
|
94
93
|
if (!isNullish(headers)) {
|
|
95
|
-
|
|
94
|
+
const mutableHeaders: Record<string, any> = isArray(headers)
|
|
95
|
+
? Object.fromEntries(headers as any)
|
|
96
|
+
: (headers as any)
|
|
97
|
+
|
|
98
|
+
each(Object.keys(mutableHeaders ?? {}), (header) => {
|
|
96
99
|
if (HEADER_DENY_LIST.includes(header.toLowerCase())) {
|
|
97
|
-
|
|
100
|
+
mutableHeaders[header] = REDACTED
|
|
98
101
|
}
|
|
99
102
|
})
|
|
103
|
+
|
|
104
|
+
data.requestHeaders = mutableHeaders as any
|
|
100
105
|
}
|
|
101
106
|
return data
|
|
102
107
|
}
|
|
@@ -106,12 +111,12 @@ const POSTHOG_PATHS_TO_IGNORE = ['/s/', '/e/', '/i/']
|
|
|
106
111
|
// because calls to PostHog would be reported using a call to PostHog which would be reported....
|
|
107
112
|
const ignorePostHogPaths = (
|
|
108
113
|
data: CapturedNetworkRequest,
|
|
109
|
-
apiHostConfig:
|
|
114
|
+
apiHostConfig: LeanbaseConfig['host']
|
|
110
115
|
): CapturedNetworkRequest | undefined => {
|
|
111
116
|
const url = convertToURL(data.name)
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
let replaceValue =
|
|
118
|
+
const host = apiHostConfig || ''
|
|
119
|
+
let replaceValue = host.indexOf('http') === 0 ? convertToURL(host)?.pathname : host
|
|
115
120
|
if (replaceValue === '/') {
|
|
116
121
|
replaceValue = ''
|
|
117
122
|
}
|
|
@@ -205,44 +210,47 @@ function scrubPayloads(capturedRequest: CapturedNetworkRequest | undefined): Cap
|
|
|
205
210
|
* if someone complains then we'll add an opt-in to let them override it
|
|
206
211
|
*/
|
|
207
212
|
export const buildNetworkRequestOptions = (
|
|
208
|
-
instanceConfig:
|
|
213
|
+
instanceConfig: LeanbaseConfig,
|
|
209
214
|
remoteNetworkOptions: Pick<
|
|
210
215
|
NetworkRecordOptions,
|
|
211
216
|
'recordHeaders' | 'recordBody' | 'recordPerformance' | 'payloadHostDenyList'
|
|
212
|
-
>
|
|
217
|
+
> = {}
|
|
213
218
|
): NetworkRecordOptions => {
|
|
219
|
+
const remoteOptions = remoteNetworkOptions || {}
|
|
214
220
|
const config: NetworkRecordOptions = {
|
|
215
221
|
payloadSizeLimitBytes: defaultNetworkOptions.payloadSizeLimitBytes,
|
|
216
222
|
performanceEntryTypeToObserve: [...defaultNetworkOptions.performanceEntryTypeToObserve],
|
|
217
223
|
payloadHostDenyList: [
|
|
218
|
-
...(
|
|
224
|
+
...(remoteOptions.payloadHostDenyList || []),
|
|
219
225
|
...defaultNetworkOptions.payloadHostDenyList,
|
|
220
226
|
],
|
|
221
227
|
}
|
|
222
228
|
// client can always disable despite remote options
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
const sessionRecordingConfig = instanceConfig.session_recording || {}
|
|
230
|
+
const capturePerformanceConfig = instanceConfig.capture_performance
|
|
231
|
+
const userPerformanceOptIn = isBoolean(capturePerformanceConfig)
|
|
232
|
+
? capturePerformanceConfig
|
|
233
|
+
: !!capturePerformanceConfig?.network_timing
|
|
234
|
+
const canRecordHeaders = sessionRecordingConfig.recordHeaders === true && !!remoteOptions.recordHeaders
|
|
235
|
+
const canRecordBody = sessionRecordingConfig.recordBody === true && !!remoteOptions.recordBody
|
|
236
|
+
const canRecordPerformance = userPerformanceOptIn && !!remoteOptions.recordPerformance
|
|
229
237
|
|
|
230
238
|
const payloadLimiter = limitPayloadSize(config)
|
|
231
239
|
|
|
232
240
|
const enforcedCleaningFn: NetworkRecordOptions['maskRequestFn'] = (d: CapturedNetworkRequest) =>
|
|
233
|
-
payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.
|
|
241
|
+
payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''))
|
|
234
242
|
|
|
235
|
-
const hasDeprecatedMaskFunction = isFunction(
|
|
243
|
+
const hasDeprecatedMaskFunction = isFunction(sessionRecordingConfig.maskNetworkRequestFn)
|
|
236
244
|
|
|
237
|
-
if (hasDeprecatedMaskFunction && isFunction(
|
|
245
|
+
if (hasDeprecatedMaskFunction && isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
|
|
238
246
|
logger.warn(
|
|
239
247
|
'Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.'
|
|
240
248
|
)
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
if (hasDeprecatedMaskFunction) {
|
|
244
|
-
|
|
245
|
-
const cleanedURL =
|
|
252
|
+
sessionRecordingConfig.maskCapturedNetworkRequestFn = (data: CapturedNetworkRequest) => {
|
|
253
|
+
const cleanedURL = sessionRecordingConfig.maskNetworkRequestFn!({ url: data.name })
|
|
246
254
|
return {
|
|
247
255
|
...data,
|
|
248
256
|
name: cleanedURL?.url,
|
|
@@ -250,11 +258,11 @@ export const buildNetworkRequestOptions = (
|
|
|
250
258
|
}
|
|
251
259
|
}
|
|
252
260
|
|
|
253
|
-
config.maskRequestFn = isFunction(
|
|
261
|
+
config.maskRequestFn = isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)
|
|
254
262
|
? (data) => {
|
|
255
263
|
const cleanedRequest = enforcedCleaningFn(data)
|
|
256
264
|
return cleanedRequest
|
|
257
|
-
? (
|
|
265
|
+
? (sessionRecordingConfig.maskCapturedNetworkRequestFn?.(cleanedRequest) ?? undefined)
|
|
258
266
|
: undefined
|
|
259
267
|
}
|
|
260
268
|
: (data) => scrubPayloads(enforcedCleaningFn(data))
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { record as rrwebRecord } from '@rrweb/record'
|
|
2
|
+
import '../extension-shim'
|
|
3
|
+
import { clampToRange, includes, isBoolean, isNullish, isNumber, isObject, isString, isUndefined } from '@posthog/core'
|
|
1
4
|
import type { recordOptions, rrwebRecord as rrwebRecordType } from '../types/rrweb'
|
|
2
5
|
import {
|
|
3
6
|
type customEvent,
|
|
@@ -8,6 +11,7 @@ import {
|
|
|
8
11
|
RecordPlugin,
|
|
9
12
|
} from '../types/rrweb-types'
|
|
10
13
|
import { buildNetworkRequestOptions } from './config'
|
|
14
|
+
import { getRecordNetworkPlugin } from './network-plugin'
|
|
11
15
|
import {
|
|
12
16
|
ACTIVE,
|
|
13
17
|
allMatchSessionRecordingStatus,
|
|
@@ -31,28 +35,16 @@ import {
|
|
|
31
35
|
} from './triggerMatching'
|
|
32
36
|
import { estimateSize, INCREMENTAL_SNAPSHOT_EVENT_TYPE, truncateLargeConsoleLogs } from './sessionrecording-utils'
|
|
33
37
|
import { gzipSync, strFromU8, strToU8 } from 'fflate'
|
|
34
|
-
import {
|
|
35
|
-
import { addEventListener } from '../../../utils'
|
|
38
|
+
import { window, document, addEventListener } from '../../../utils'
|
|
36
39
|
import { MutationThrottler } from './mutation-throttler'
|
|
37
40
|
import { createLogger } from '../../../utils/logger'
|
|
38
|
-
import {
|
|
39
|
-
clampToRange,
|
|
40
|
-
includes,
|
|
41
|
-
isBoolean,
|
|
42
|
-
isFunction,
|
|
43
|
-
isNullish,
|
|
44
|
-
isNumber,
|
|
45
|
-
isObject,
|
|
46
|
-
isString,
|
|
47
|
-
isUndefined,
|
|
48
|
-
} from '@posthog/core'
|
|
49
41
|
import {
|
|
50
42
|
SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION,
|
|
51
43
|
SESSION_RECORDING_IS_SAMPLED,
|
|
52
44
|
SESSION_RECORDING_REMOTE_CONFIG,
|
|
53
45
|
SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION,
|
|
54
46
|
} from '../../../constants'
|
|
55
|
-
import {
|
|
47
|
+
import { Leanbase } from '../../../leanbase'
|
|
56
48
|
import {
|
|
57
49
|
CaptureResult,
|
|
58
50
|
NetworkRecordOptions,
|
|
@@ -127,7 +119,17 @@ const newQueuedEvent = (rrwebMethod: () => void): QueuedRRWebEvent => ({
|
|
|
127
119
|
})
|
|
128
120
|
|
|
129
121
|
function getRRWebRecord(): rrwebRecordType | undefined {
|
|
130
|
-
|
|
122
|
+
try {
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
124
|
+
const ext = (globalThis as any).__PosthogExtensions__
|
|
125
|
+
if (ext && ext.rrweb && ext.rrweb.record) {
|
|
126
|
+
return ext.rrweb.record as unknown as rrwebRecordType
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return rrwebRecord as unknown as rrwebRecordType
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
export type compressedFullSnapshotEvent = {
|
|
@@ -258,7 +260,7 @@ export function splitBuffer(buffer: SnapshotBuffer, sizeLimit: number = SEVEN_ME
|
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
export class LazyLoadedSessionRecording
|
|
263
|
+
export class LazyLoadedSessionRecording {
|
|
262
264
|
private _endpoint: string = BASE_ENDPOINT
|
|
263
265
|
private _mutationThrottler?: MutationThrottler
|
|
264
266
|
/**
|
|
@@ -309,7 +311,7 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
private get _sessionIdleThresholdMilliseconds(): number {
|
|
312
|
-
return this._instance.config.session_recording
|
|
314
|
+
return this._instance.config.session_recording?.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS
|
|
313
315
|
}
|
|
314
316
|
|
|
315
317
|
private get _isSampled(): boolean | null {
|
|
@@ -337,7 +339,7 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
337
339
|
private _samplingSessionListener: (() => void) | undefined = undefined
|
|
338
340
|
private _forceIdleSessionIdListener: (() => void) | undefined = undefined
|
|
339
341
|
|
|
340
|
-
constructor(private readonly _instance:
|
|
342
|
+
constructor(private readonly _instance: Leanbase) {
|
|
341
343
|
// we know there's a sessionManager, so don't need to start without a session id
|
|
342
344
|
const { sessionId, windowId } = this._sessionManager.checkAndGetSessionAndWindowId()
|
|
343
345
|
this._sessionId = sessionId
|
|
@@ -380,7 +382,7 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
380
382
|
}
|
|
381
383
|
|
|
382
384
|
private get _canvasRecording(): { enabled: boolean; fps: number; quality: number } {
|
|
383
|
-
const canvasRecording_client_side = this._instance.config.session_recording
|
|
385
|
+
const canvasRecording_client_side = this._instance.config.session_recording?.captureCanvas
|
|
384
386
|
const canvasRecording_server_side = this._remoteConfig?.canvasRecording
|
|
385
387
|
|
|
386
388
|
const enabled: boolean =
|
|
@@ -416,44 +418,60 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
416
418
|
// network payload capture config has three parts
|
|
417
419
|
// each can be configured server side or client side
|
|
418
420
|
private get _networkPayloadCapture():
|
|
419
|
-
| Pick<NetworkRecordOptions, 'recordHeaders' | 'recordBody' | 'recordPerformance'>
|
|
421
|
+
| Pick<NetworkRecordOptions, 'recordHeaders' | 'recordBody' | 'recordPerformance' | 'payloadHostDenyList'>
|
|
420
422
|
| undefined {
|
|
421
423
|
const networkPayloadCapture_server_side = this._remoteConfig?.networkPayloadCapture
|
|
422
424
|
const networkPayloadCapture_client_side = {
|
|
423
425
|
recordHeaders: this._instance.config.session_recording?.recordHeaders,
|
|
424
426
|
recordBody: this._instance.config.session_recording?.recordBody,
|
|
425
427
|
}
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
428
|
+
const headersOptIn = networkPayloadCapture_client_side?.recordHeaders === true
|
|
429
|
+
const bodyOptIn = networkPayloadCapture_client_side?.recordBody === true
|
|
430
|
+
const clientPerformanceConfig = this._instance.config.capture_performance
|
|
431
|
+
const clientPerformanceOptIn = isObject(clientPerformanceConfig)
|
|
432
|
+
? !!clientPerformanceConfig.network_timing
|
|
433
|
+
: !!clientPerformanceConfig
|
|
434
|
+
const serverAllowsHeaders = networkPayloadCapture_server_side?.recordHeaders ?? true
|
|
435
|
+
const serverAllowsBody = networkPayloadCapture_server_side?.recordBody ?? true
|
|
436
|
+
const capturePerfResponse = networkPayloadCapture_server_side?.capturePerformance
|
|
437
|
+
const serverAllowsPerformance = (() => {
|
|
438
|
+
if (isObject(capturePerfResponse)) {
|
|
439
|
+
return !!capturePerfResponse.network_timing
|
|
440
|
+
}
|
|
441
|
+
return capturePerfResponse ?? true
|
|
442
|
+
})()
|
|
443
|
+
|
|
444
|
+
const headersEnabled = headersOptIn && serverAllowsHeaders
|
|
445
|
+
const bodyEnabled = bodyOptIn && serverAllowsBody
|
|
446
|
+
const networkTimingEnabled = clientPerformanceOptIn && serverAllowsPerformance
|
|
447
|
+
|
|
448
|
+
if (!headersEnabled && !bodyEnabled && !networkTimingEnabled) {
|
|
449
|
+
return undefined
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
recordHeaders: headersEnabled,
|
|
454
|
+
recordBody: bodyEnabled,
|
|
455
|
+
recordPerformance: networkTimingEnabled,
|
|
456
|
+
payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList,
|
|
457
|
+
}
|
|
440
458
|
}
|
|
441
459
|
|
|
442
460
|
private _gatherRRWebPlugins() {
|
|
443
461
|
const plugins: RecordPlugin[] = []
|
|
444
462
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
plugins.push(recordConsolePlugin())
|
|
463
|
+
if (this._isConsoleLogCaptureEnabled) {
|
|
464
|
+
logger.info('Console log capture requested but console plugin is not bundled in this build yet.')
|
|
448
465
|
}
|
|
449
466
|
|
|
450
|
-
|
|
451
|
-
if (!!this._networkPayloadCapture && isFunction(networkPlugin)) {
|
|
467
|
+
if (this._networkPayloadCapture) {
|
|
452
468
|
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture
|
|
453
469
|
|
|
454
470
|
if (canRecordNetwork) {
|
|
455
471
|
plugins.push(
|
|
456
|
-
|
|
472
|
+
getRecordNetworkPlugin(
|
|
473
|
+
buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)
|
|
474
|
+
)
|
|
457
475
|
)
|
|
458
476
|
} else {
|
|
459
477
|
logger.info('NetworkCapture not started because we are on localhost.')
|
|
@@ -464,7 +482,7 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
464
482
|
}
|
|
465
483
|
|
|
466
484
|
private _maskUrl(url: string): string | undefined {
|
|
467
|
-
const userSessionRecordingOptions = this._instance.config.session_recording
|
|
485
|
+
const userSessionRecordingOptions = this._instance.config.session_recording || {}
|
|
468
486
|
|
|
469
487
|
if (userSessionRecordingOptions.maskNetworkRequestFn) {
|
|
470
488
|
let networkRequest: NetworkRequest | null | undefined = {
|
|
@@ -666,8 +684,8 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
666
684
|
this._statusMatcher = allMatchSessionRecordingStatus
|
|
667
685
|
this._triggerMatching = new AndTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching])
|
|
668
686
|
}
|
|
669
|
-
this._instance.
|
|
670
|
-
$sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType,
|
|
687
|
+
this._instance.registerForSession({
|
|
688
|
+
$sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType ?? null,
|
|
671
689
|
})
|
|
672
690
|
|
|
673
691
|
this._urlTriggerMatching.onConfig(config)
|
|
@@ -870,7 +888,7 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
870
888
|
}
|
|
871
889
|
|
|
872
890
|
const eventToSend =
|
|
873
|
-
(this._instance.config.session_recording
|
|
891
|
+
(this._instance.config.session_recording?.compress_events ?? true) ? compressEvent(event) : event
|
|
874
892
|
const size = estimateSize(eventToSend)
|
|
875
893
|
|
|
876
894
|
const properties = {
|
|
@@ -1013,36 +1031,25 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
1013
1031
|
}
|
|
1014
1032
|
|
|
1015
1033
|
private _captureSnapshot(properties: Properties) {
|
|
1016
|
-
//
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
// capture if the immediate API isn't available at runtime.
|
|
1020
|
-
const opts = {
|
|
1034
|
+
// :TRICKY: Make sure we batch these requests, use a custom endpoint and don't truncate the strings.
|
|
1035
|
+
this._instance.capture('$snapshot', properties, {
|
|
1036
|
+
_url: this._instance.requestRouter.endpointFor('api', this._endpoint),
|
|
1021
1037
|
_noTruncate: true,
|
|
1022
1038
|
_batchKey: SESSION_RECORDING_BATCH_KEY,
|
|
1023
1039
|
skip_client_rate_limiting: true,
|
|
1024
|
-
}
|
|
1040
|
+
})
|
|
1041
|
+
}
|
|
1025
1042
|
|
|
1043
|
+
private _snapshotUrl(): string {
|
|
1044
|
+
const host = this._instance.config.host || ''
|
|
1026
1045
|
try {
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1034
|
-
} catch (e) {
|
|
1035
|
-
// if anything goes wrong, fall through to the safe capture path below
|
|
1036
|
-
logger.error('Failed to send snapshot via stateless immediate path, falling back to capture', e)
|
|
1046
|
+
// eslint-disable-next-line compat/compat
|
|
1047
|
+
return new URL(this._endpoint, host).href
|
|
1048
|
+
} catch {
|
|
1049
|
+
const normalizedHost = host.endsWith('/') ? host.slice(0, -1) : host
|
|
1050
|
+
const normalizedEndpoint = this._endpoint.startsWith('/') ? this._endpoint.slice(1) : this._endpoint
|
|
1051
|
+
return `${normalizedHost}/${normalizedEndpoint}`
|
|
1037
1052
|
}
|
|
1038
|
-
|
|
1039
|
-
// :TRICKY: Fallback - use the standard capture path. Keep the explicit
|
|
1040
|
-
// _url for compatibility with environments that expect a pre-computed
|
|
1041
|
-
// endpoint, even though the immediate path above is preferred.
|
|
1042
|
-
this._instance.capture('$snapshot', properties, {
|
|
1043
|
-
_url: this._instance.requestRouter.endpointFor('api', this._endpoint),
|
|
1044
|
-
...opts,
|
|
1045
|
-
})
|
|
1046
1053
|
}
|
|
1047
1054
|
|
|
1048
1055
|
private get _sessionDuration(): number | null {
|
|
@@ -1103,7 +1110,7 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
1103
1110
|
}
|
|
1104
1111
|
|
|
1105
1112
|
private _reportStarted(startReason: SessionStartReason, tagPayload?: Record<string, any>) {
|
|
1106
|
-
this._instance.
|
|
1113
|
+
this._instance.registerForSession({
|
|
1107
1114
|
$session_recording_start_reason: startReason,
|
|
1108
1115
|
})
|
|
1109
1116
|
logger.info(startReason.replace('_', ' '), tagPayload)
|
|
@@ -1337,8 +1344,8 @@ export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInt
|
|
|
1337
1344
|
this._mutationThrottler =
|
|
1338
1345
|
this._mutationThrottler ??
|
|
1339
1346
|
new MutationThrottler(rrwebRecord, {
|
|
1340
|
-
refillRate: this._instance.config.session_recording
|
|
1341
|
-
bucketSize: this._instance.config.session_recording
|
|
1347
|
+
refillRate: this._instance.config.session_recording?.__mutationThrottlerRefillRate,
|
|
1348
|
+
bucketSize: this._instance.config.session_recording?.__mutationThrottlerBucketSize,
|
|
1342
1349
|
onBlockedNode: (id, node) => {
|
|
1343
1350
|
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`
|
|
1344
1351
|
logger.info(message, {
|
|
@@ -16,8 +16,11 @@ export class MutationThrottler {
|
|
|
16
16
|
onBlockedNode?: (id: number, node: Node | null) => void
|
|
17
17
|
} = {}
|
|
18
18
|
) {
|
|
19
|
+
const configuredBucketSize = this._options.bucketSize ?? 100
|
|
20
|
+
const effectiveBucketSize = Math.max(configuredBucketSize - 1, 1)
|
|
21
|
+
|
|
19
22
|
this._rateLimiter = new BucketedRateLimiter({
|
|
20
|
-
bucketSize:
|
|
23
|
+
bucketSize: effectiveBucketSize,
|
|
21
24
|
refillRate: this._options.refillRate ?? 10,
|
|
22
25
|
refillInterval: 1000, // one second
|
|
23
26
|
_onBucketRateLimited: this._onNodeRateLimited,
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import type { IWindow, listenerHandler, RecordPlugin } from '../types/rrweb-types'
|
|
13
13
|
import { CapturedNetworkRequest, Headers, InitiatorType, NetworkRecordOptions } from '../../../types'
|
|
14
14
|
import { isArray, isBoolean, isFormData, isNull, isNullish, isString, isUndefined, isObject } from '@posthog/core'
|
|
15
|
-
import { isDocument } from '../../../utils/type-utils'
|
|
16
15
|
import { createLogger } from '../../../utils/logger'
|
|
17
16
|
import { formDataToQuery } from '../../../utils/request-utils'
|
|
18
17
|
import { patch } from '../rrweb-plugins/patch'
|
|
@@ -46,6 +45,10 @@ export function findLast<T>(array: Array<T>, predicate: (value: T) => boolean):
|
|
|
46
45
|
return undefined
|
|
47
46
|
}
|
|
48
47
|
|
|
48
|
+
function isDocument(value: any): value is Document {
|
|
49
|
+
return !!value && typeof value === 'object' && 'nodeType' in value && (value as any).nodeType === 9
|
|
50
|
+
}
|
|
51
|
+
|
|
49
52
|
function initPerformanceObserver(cb: networkCallback, win: IWindow, options: Required<NetworkRecordOptions>) {
|
|
50
53
|
// if we are only observing timings then we could have a single observer for all types, with buffer true,
|
|
51
54
|
// but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
|
|
@@ -668,12 +671,16 @@ function initNetworkObserver(
|
|
|
668
671
|
fetchObserver = initFetchObserver(cb, win, networkOptions)
|
|
669
672
|
}
|
|
670
673
|
|
|
671
|
-
|
|
674
|
+
const teardown: listenerHandler = () => {
|
|
672
675
|
performanceObserver()
|
|
673
676
|
xhrObserver()
|
|
674
677
|
fetchObserver()
|
|
678
|
+
// allow future observers to initialize after cleanup
|
|
679
|
+
initialisedHandler = null
|
|
675
680
|
}
|
|
676
|
-
|
|
681
|
+
|
|
682
|
+
initialisedHandler = teardown
|
|
683
|
+
return teardown
|
|
677
684
|
}
|
|
678
685
|
|
|
679
686
|
// use the plugin name so that when this functionality is adopted into rrweb
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION,
|
|
3
3
|
SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION,
|
|
4
4
|
} from '../../../constants'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { Leanbase } from '../../../leanbase'
|
|
6
|
+
import { RemoteConfig, SessionRecordingPersistedConfig, SessionRecordingUrlTrigger } from '../../../types'
|
|
7
7
|
import { isNullish, isBoolean, isString, isObject } from '@posthog/core'
|
|
8
|
-
import { window } from '../../../utils
|
|
8
|
+
import { window } from '../../../utils'
|
|
9
9
|
|
|
10
10
|
export const DISABLED = 'disabled'
|
|
11
11
|
export const SAMPLED = 'sampled'
|
|
@@ -133,7 +133,7 @@ export class URLTriggerMatching implements TriggerStatusMatching {
|
|
|
133
133
|
|
|
134
134
|
urlBlocked: boolean = false
|
|
135
135
|
|
|
136
|
-
constructor(private readonly _instance:
|
|
136
|
+
constructor(private readonly _instance: Leanbase) {}
|
|
137
137
|
|
|
138
138
|
onConfig(config: ReplayConfigType) {
|
|
139
139
|
this._urlTriggers =
|
|
@@ -172,7 +172,7 @@ export class URLTriggerMatching implements TriggerStatusMatching {
|
|
|
172
172
|
const eitherIsPending = urlTriggerStatus === TRIGGER_PENDING
|
|
173
173
|
|
|
174
174
|
const result = eitherIsActivated ? TRIGGER_ACTIVATED : eitherIsPending ? TRIGGER_PENDING : TRIGGER_DISABLED
|
|
175
|
-
this._instance.
|
|
175
|
+
this._instance.registerForSession({
|
|
176
176
|
$sdk_debug_replay_url_trigger_status: result,
|
|
177
177
|
})
|
|
178
178
|
return result
|
|
@@ -212,10 +212,10 @@ export class URLTriggerMatching implements TriggerStatusMatching {
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
export class LinkedFlagMatching implements TriggerStatusMatching {
|
|
215
|
-
linkedFlag: string |
|
|
215
|
+
linkedFlag: string | { flag: string; variant: string } | null = null
|
|
216
216
|
linkedFlagSeen: boolean = false
|
|
217
217
|
private _flagListenerCleanup: () => void = () => {}
|
|
218
|
-
constructor(private readonly _instance:
|
|
218
|
+
constructor(private readonly _instance: Leanbase) {}
|
|
219
219
|
|
|
220
220
|
triggerStatus(): TriggerStatus {
|
|
221
221
|
let result = TRIGGER_PENDING
|
|
@@ -225,7 +225,7 @@ export class LinkedFlagMatching implements TriggerStatusMatching {
|
|
|
225
225
|
if (this.linkedFlagSeen) {
|
|
226
226
|
result = TRIGGER_ACTIVATED
|
|
227
227
|
}
|
|
228
|
-
this._instance.
|
|
228
|
+
this._instance.registerForSession({
|
|
229
229
|
$sdk_debug_replay_linked_flag_trigger_status: result,
|
|
230
230
|
})
|
|
231
231
|
return result
|
|
@@ -242,11 +242,11 @@ export class LinkedFlagMatching implements TriggerStatusMatching {
|
|
|
242
242
|
if (!isNullish(this.linkedFlag) && !this.linkedFlagSeen) {
|
|
243
243
|
const linkedFlag = isString(this.linkedFlag) ? this.linkedFlag : this.linkedFlag.flag
|
|
244
244
|
const linkedVariant = isString(this.linkedFlag) ? null : this.linkedFlag.variant
|
|
245
|
-
this._flagListenerCleanup = this._instance.onFeatureFlags((
|
|
246
|
-
const flagIsPresent = isObject(
|
|
245
|
+
this._flagListenerCleanup = this._instance.onFeatureFlags((flags) => {
|
|
246
|
+
const flagIsPresent = isObject(flags) && linkedFlag in (flags as any)
|
|
247
247
|
let linkedFlagMatches = false
|
|
248
248
|
if (flagIsPresent) {
|
|
249
|
-
const variantForFlagKey =
|
|
249
|
+
const variantForFlagKey = (flags as any)[linkedFlag]
|
|
250
250
|
if (isBoolean(variantForFlagKey)) {
|
|
251
251
|
linkedFlagMatches = variantForFlagKey === true
|
|
252
252
|
} else if (linkedVariant) {
|
|
@@ -279,7 +279,7 @@ export class LinkedFlagMatching implements TriggerStatusMatching {
|
|
|
279
279
|
export class EventTriggerMatching implements TriggerStatusMatching {
|
|
280
280
|
_eventTriggers: string[] = []
|
|
281
281
|
|
|
282
|
-
constructor(private readonly _instance:
|
|
282
|
+
constructor(private readonly _instance: Leanbase) {}
|
|
283
283
|
|
|
284
284
|
onConfig(config: ReplayConfigType) {
|
|
285
285
|
this._eventTriggers =
|
|
@@ -314,7 +314,7 @@ export class EventTriggerMatching implements TriggerStatusMatching {
|
|
|
314
314
|
: eventTriggerStatus === TRIGGER_PENDING
|
|
315
315
|
? TRIGGER_PENDING
|
|
316
316
|
: TRIGGER_DISABLED
|
|
317
|
-
this._instance.
|
|
317
|
+
this._instance.registerForSession({
|
|
318
318
|
$sdk_debug_replay_event_trigger_status: result,
|
|
319
319
|
})
|
|
320
320
|
return result
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// import { patch } from 'rrweb/typings/utils'
|
|
2
|
-
// copied from https://github.com/rrweb-io/rrweb/blob/8aea5b00a4dfe5a6f59bd2ae72bb624f45e51e81/packages/rrweb/src/utils.ts#L129
|
|
3
|
-
// which was copied from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
|
|
4
1
|
import { isFunction } from '@posthog/core'
|
|
5
2
|
|
|
6
3
|
export function patch(
|
|
@@ -18,8 +15,6 @@ export function patch(
|
|
|
18
15
|
const original = source[name] as () => unknown
|
|
19
16
|
const wrapped = replacement(original)
|
|
20
17
|
|
|
21
|
-
// Make sure it's a function first, as we need to attach an empty prototype for `defineProperties` to work
|
|
22
|
-
// otherwise it'll throw "TypeError: Object.defineProperties called on non-object"
|
|
23
18
|
if (isFunction(wrapped)) {
|
|
24
19
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
25
20
|
wrapped.prototype = wrapped.prototype || {}
|
|
@@ -40,7 +35,5 @@ export function patch(
|
|
|
40
35
|
return () => {
|
|
41
36
|
//
|
|
42
37
|
}
|
|
43
|
-
// This can throw if multiple fill happens on a global object like XMLHttpRequest
|
|
44
|
-
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
|
|
45
38
|
}
|
|
46
39
|
}
|