@leanbase-giangnd/js 0.0.4 → 0.0.7
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 +261 -2875
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +95 -685
- package/dist/index.mjs +262 -2876
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +236 -8815
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +5 -6
- package/src/extensions/replay/external/README.md +5 -0
- package/src/extensions/replay/external/config.ts +25 -33
- package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +71 -67
- package/src/extensions/replay/external/mutation-throttler.ts +1 -4
- package/src/extensions/replay/external/network-plugin.ts +3 -10
- package/src/extensions/replay/external/triggerMatching.ts +13 -13
- package/src/extensions/replay/rrweb-plugins/patch.ts +7 -0
- package/src/extensions/replay/session-recording.ts +61 -50
- package/src/extensions/utils/stylesheet-loader.ts +27 -0
- package/src/leanbase.ts +51 -154
- package/src/posthog-core.ts +12 -0
- package/src/types.ts +119 -123
- package/src/utils/globals.ts +239 -0
- package/src/utils/logger.ts +51 -13
- package/src/utils/request-router.ts +77 -0
- package/src/utils/type-utils.ts +139 -0
- package/src/version.ts +1 -1
- package/src/types/fflate.d.ts +0 -6
- package/src/types/rrweb-record.d.ts +0 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leanbase-giangnd/js",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Leanbase
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"description": "Leanbase bgiangndrowser SDK - event tracking, autocapture, and session replay",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"directory": "packages/leanbase"
|
|
@@ -34,15 +34,14 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@posthog/core": "workspace:*",
|
|
37
|
-
"@rrweb/record": "2.0.0-alpha.17",
|
|
38
37
|
"fflate": "^0.4.8"
|
|
39
38
|
},
|
|
40
39
|
"devDependencies": {
|
|
41
|
-
"@posthog-tooling/tsconfig-base": "workspace:*",
|
|
42
40
|
"@posthog-tooling/rollup-utils": "workspace:*",
|
|
41
|
+
"@posthog-tooling/tsconfig-base": "workspace:*",
|
|
43
42
|
"jest": "catalog:",
|
|
44
43
|
"jest-environment-jsdom": "catalog:",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
44
|
+
"rimraf": "^6.0.1",
|
|
45
|
+
"rollup": "catalog:"
|
|
47
46
|
}
|
|
48
47
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CapturedNetworkRequest,
|
|
2
|
-
import {
|
|
1
|
+
import { CapturedNetworkRequest, NetworkRecordOptions, PostHogConfig } from '../../../types'
|
|
2
|
+
import { 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,11 +15,12 @@ export const defaultNetworkOptions: Required<NetworkRecordOptions> = {
|
|
|
15
15
|
'beacon',
|
|
16
16
|
'body',
|
|
17
17
|
'css',
|
|
18
|
-
'early-
|
|
18
|
+
'early-hint',
|
|
19
19
|
'embed',
|
|
20
20
|
'fetch',
|
|
21
21
|
'frame',
|
|
22
22
|
'iframe',
|
|
23
|
+
'icon',
|
|
23
24
|
'image',
|
|
24
25
|
'img',
|
|
25
26
|
'input',
|
|
@@ -91,17 +92,11 @@ const PAYLOAD_CONTENT_DENY_LIST = [
|
|
|
91
92
|
const removeAuthorizationHeader = (data: CapturedNetworkRequest): CapturedNetworkRequest => {
|
|
92
93
|
const headers = data.requestHeaders
|
|
93
94
|
if (!isNullish(headers)) {
|
|
94
|
-
|
|
95
|
-
? Object.fromEntries(headers as any)
|
|
96
|
-
: (headers as any)
|
|
97
|
-
|
|
98
|
-
each(Object.keys(mutableHeaders ?? {}), (header) => {
|
|
95
|
+
each(Object.keys(headers ?? {}), (header) => {
|
|
99
96
|
if (HEADER_DENY_LIST.includes(header.toLowerCase())) {
|
|
100
|
-
|
|
97
|
+
headers[header] = REDACTED
|
|
101
98
|
}
|
|
102
99
|
})
|
|
103
|
-
|
|
104
|
-
data.requestHeaders = mutableHeaders as any
|
|
105
100
|
}
|
|
106
101
|
return data
|
|
107
102
|
}
|
|
@@ -111,12 +106,12 @@ const POSTHOG_PATHS_TO_IGNORE = ['/s/', '/e/', '/i/']
|
|
|
111
106
|
// because calls to PostHog would be reported using a call to PostHog which would be reported....
|
|
112
107
|
const ignorePostHogPaths = (
|
|
113
108
|
data: CapturedNetworkRequest,
|
|
114
|
-
apiHostConfig:
|
|
109
|
+
apiHostConfig: PostHogConfig['api_host']
|
|
115
110
|
): CapturedNetworkRequest | undefined => {
|
|
116
111
|
const url = convertToURL(data.name)
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
let replaceValue =
|
|
113
|
+
// we need to account for api host config as e.g. pathname could be /ingest/s/ and we want to ignore that
|
|
114
|
+
let replaceValue = apiHostConfig.indexOf('http') === 0 ? convertToURL(apiHostConfig)?.pathname : apiHostConfig
|
|
120
115
|
if (replaceValue === '/') {
|
|
121
116
|
replaceValue = ''
|
|
122
117
|
}
|
|
@@ -210,47 +205,44 @@ function scrubPayloads(capturedRequest: CapturedNetworkRequest | undefined): Cap
|
|
|
210
205
|
* if someone complains then we'll add an opt-in to let them override it
|
|
211
206
|
*/
|
|
212
207
|
export const buildNetworkRequestOptions = (
|
|
213
|
-
instanceConfig:
|
|
208
|
+
instanceConfig: PostHogConfig,
|
|
214
209
|
remoteNetworkOptions: Pick<
|
|
215
210
|
NetworkRecordOptions,
|
|
216
211
|
'recordHeaders' | 'recordBody' | 'recordPerformance' | 'payloadHostDenyList'
|
|
217
|
-
>
|
|
212
|
+
>
|
|
218
213
|
): NetworkRecordOptions => {
|
|
219
|
-
const remoteOptions = remoteNetworkOptions || {}
|
|
220
214
|
const config: NetworkRecordOptions = {
|
|
221
215
|
payloadSizeLimitBytes: defaultNetworkOptions.payloadSizeLimitBytes,
|
|
222
216
|
performanceEntryTypeToObserve: [...defaultNetworkOptions.performanceEntryTypeToObserve],
|
|
223
217
|
payloadHostDenyList: [
|
|
224
|
-
...(
|
|
218
|
+
...(remoteNetworkOptions.payloadHostDenyList || []),
|
|
225
219
|
...defaultNetworkOptions.payloadHostDenyList,
|
|
226
220
|
],
|
|
227
221
|
}
|
|
228
222
|
// client can always disable despite remote options
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
?
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const canRecordBody = sessionRecordingConfig.recordBody === true && !!remoteOptions.recordBody
|
|
236
|
-
const canRecordPerformance = userPerformanceOptIn && !!remoteOptions.recordPerformance
|
|
223
|
+
const canRecordHeaders =
|
|
224
|
+
instanceConfig.session_recording.recordHeaders === false ? false : remoteNetworkOptions.recordHeaders
|
|
225
|
+
const canRecordBody =
|
|
226
|
+
instanceConfig.session_recording.recordBody === false ? false : remoteNetworkOptions.recordBody
|
|
227
|
+
const canRecordPerformance =
|
|
228
|
+
instanceConfig.capture_performance === false ? false : remoteNetworkOptions.recordPerformance
|
|
237
229
|
|
|
238
230
|
const payloadLimiter = limitPayloadSize(config)
|
|
239
231
|
|
|
240
232
|
const enforcedCleaningFn: NetworkRecordOptions['maskRequestFn'] = (d: CapturedNetworkRequest) =>
|
|
241
|
-
payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.
|
|
233
|
+
payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.api_host))
|
|
242
234
|
|
|
243
|
-
const hasDeprecatedMaskFunction = isFunction(
|
|
235
|
+
const hasDeprecatedMaskFunction = isFunction(instanceConfig.session_recording.maskNetworkRequestFn)
|
|
244
236
|
|
|
245
|
-
if (hasDeprecatedMaskFunction && isFunction(
|
|
237
|
+
if (hasDeprecatedMaskFunction && isFunction(instanceConfig.session_recording.maskCapturedNetworkRequestFn)) {
|
|
246
238
|
logger.warn(
|
|
247
239
|
'Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.'
|
|
248
240
|
)
|
|
249
241
|
}
|
|
250
242
|
|
|
251
243
|
if (hasDeprecatedMaskFunction) {
|
|
252
|
-
|
|
253
|
-
const cleanedURL =
|
|
244
|
+
instanceConfig.session_recording.maskCapturedNetworkRequestFn = (data: CapturedNetworkRequest) => {
|
|
245
|
+
const cleanedURL = instanceConfig.session_recording.maskNetworkRequestFn!({ url: data.name })
|
|
254
246
|
return {
|
|
255
247
|
...data,
|
|
256
248
|
name: cleanedURL?.url,
|
|
@@ -258,11 +250,11 @@ export const buildNetworkRequestOptions = (
|
|
|
258
250
|
}
|
|
259
251
|
}
|
|
260
252
|
|
|
261
|
-
config.maskRequestFn = isFunction(
|
|
253
|
+
config.maskRequestFn = isFunction(instanceConfig.session_recording.maskCapturedNetworkRequestFn)
|
|
262
254
|
? (data) => {
|
|
263
255
|
const cleanedRequest = enforcedCleaningFn(data)
|
|
264
256
|
return cleanedRequest
|
|
265
|
-
? (
|
|
257
|
+
? (instanceConfig.session_recording.maskCapturedNetworkRequestFn?.(cleanedRequest) ?? undefined)
|
|
266
258
|
: undefined
|
|
267
259
|
}
|
|
268
260
|
: (data) => scrubPayloads(enforcedCleaningFn(data))
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { record as rrwebRecord } from '@rrweb/record'
|
|
2
|
-
import { clampToRange, includes, isBoolean, isNullish, isNumber, isObject, isString, isUndefined } from '@posthog/core'
|
|
3
1
|
import type { recordOptions, rrwebRecord as rrwebRecordType } from '../types/rrweb'
|
|
4
2
|
import {
|
|
5
3
|
type customEvent,
|
|
@@ -10,7 +8,6 @@ import {
|
|
|
10
8
|
RecordPlugin,
|
|
11
9
|
} from '../types/rrweb-types'
|
|
12
10
|
import { buildNetworkRequestOptions } from './config'
|
|
13
|
-
import { getRecordNetworkPlugin } from './network-plugin'
|
|
14
11
|
import {
|
|
15
12
|
ACTIVE,
|
|
16
13
|
allMatchSessionRecordingStatus,
|
|
@@ -34,16 +31,28 @@ import {
|
|
|
34
31
|
} from './triggerMatching'
|
|
35
32
|
import { estimateSize, INCREMENTAL_SNAPSHOT_EVENT_TYPE, truncateLargeConsoleLogs } from './sessionrecording-utils'
|
|
36
33
|
import { gzipSync, strFromU8, strToU8 } from 'fflate'
|
|
37
|
-
import { window, document
|
|
34
|
+
import { assignableWindow, LazyLoadedSessionRecordingInterface, window, document } from '../../../utils/globals'
|
|
35
|
+
import { addEventListener } from '../../../utils'
|
|
38
36
|
import { MutationThrottler } from './mutation-throttler'
|
|
39
37
|
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'
|
|
40
49
|
import {
|
|
41
50
|
SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION,
|
|
42
51
|
SESSION_RECORDING_IS_SAMPLED,
|
|
43
52
|
SESSION_RECORDING_REMOTE_CONFIG,
|
|
44
53
|
SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION,
|
|
45
54
|
} from '../../../constants'
|
|
46
|
-
import {
|
|
55
|
+
import { PostHog } from '../../../posthog-core'
|
|
47
56
|
import {
|
|
48
57
|
CaptureResult,
|
|
49
58
|
NetworkRecordOptions,
|
|
@@ -118,7 +127,7 @@ const newQueuedEvent = (rrwebMethod: () => void): QueuedRRWebEvent => ({
|
|
|
118
127
|
})
|
|
119
128
|
|
|
120
129
|
function getRRWebRecord(): rrwebRecordType | undefined {
|
|
121
|
-
return
|
|
130
|
+
return assignableWindow?.__PosthogExtensions__?.rrweb?.record
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
export type compressedFullSnapshotEvent = {
|
|
@@ -249,7 +258,7 @@ export function splitBuffer(buffer: SnapshotBuffer, sizeLimit: number = SEVEN_ME
|
|
|
249
258
|
}
|
|
250
259
|
}
|
|
251
260
|
|
|
252
|
-
export class LazyLoadedSessionRecording {
|
|
261
|
+
export class LazyLoadedSessionRecording implements LazyLoadedSessionRecordingInterface {
|
|
253
262
|
private _endpoint: string = BASE_ENDPOINT
|
|
254
263
|
private _mutationThrottler?: MutationThrottler
|
|
255
264
|
/**
|
|
@@ -300,7 +309,7 @@ export class LazyLoadedSessionRecording {
|
|
|
300
309
|
}
|
|
301
310
|
|
|
302
311
|
private get _sessionIdleThresholdMilliseconds(): number {
|
|
303
|
-
return this._instance.config.session_recording
|
|
312
|
+
return this._instance.config.session_recording.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS
|
|
304
313
|
}
|
|
305
314
|
|
|
306
315
|
private get _isSampled(): boolean | null {
|
|
@@ -328,7 +337,7 @@ export class LazyLoadedSessionRecording {
|
|
|
328
337
|
private _samplingSessionListener: (() => void) | undefined = undefined
|
|
329
338
|
private _forceIdleSessionIdListener: (() => void) | undefined = undefined
|
|
330
339
|
|
|
331
|
-
constructor(private readonly _instance:
|
|
340
|
+
constructor(private readonly _instance: PostHog) {
|
|
332
341
|
// we know there's a sessionManager, so don't need to start without a session id
|
|
333
342
|
const { sessionId, windowId } = this._sessionManager.checkAndGetSessionAndWindowId()
|
|
334
343
|
this._sessionId = sessionId
|
|
@@ -371,7 +380,7 @@ export class LazyLoadedSessionRecording {
|
|
|
371
380
|
}
|
|
372
381
|
|
|
373
382
|
private get _canvasRecording(): { enabled: boolean; fps: number; quality: number } {
|
|
374
|
-
const canvasRecording_client_side = this._instance.config.session_recording
|
|
383
|
+
const canvasRecording_client_side = this._instance.config.session_recording.captureCanvas
|
|
375
384
|
const canvasRecording_server_side = this._remoteConfig?.canvasRecording
|
|
376
385
|
|
|
377
386
|
const enabled: boolean =
|
|
@@ -407,60 +416,44 @@ export class LazyLoadedSessionRecording {
|
|
|
407
416
|
// network payload capture config has three parts
|
|
408
417
|
// each can be configured server side or client side
|
|
409
418
|
private get _networkPayloadCapture():
|
|
410
|
-
| Pick<NetworkRecordOptions, 'recordHeaders' | 'recordBody' | 'recordPerformance'
|
|
419
|
+
| Pick<NetworkRecordOptions, 'recordHeaders' | 'recordBody' | 'recordPerformance'>
|
|
411
420
|
| undefined {
|
|
412
421
|
const networkPayloadCapture_server_side = this._remoteConfig?.networkPayloadCapture
|
|
413
422
|
const networkPayloadCapture_client_side = {
|
|
414
423
|
recordHeaders: this._instance.config.session_recording?.recordHeaders,
|
|
415
424
|
recordBody: this._instance.config.session_recording?.recordBody,
|
|
416
425
|
}
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
})()
|
|
432
|
-
|
|
433
|
-
const headersEnabled = headersOptIn && serverAllowsHeaders
|
|
434
|
-
const bodyEnabled = bodyOptIn && serverAllowsBody
|
|
435
|
-
const networkTimingEnabled = clientPerformanceOptIn && serverAllowsPerformance
|
|
436
|
-
|
|
437
|
-
if (!headersEnabled && !bodyEnabled && !networkTimingEnabled) {
|
|
438
|
-
return undefined
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
recordHeaders: headersEnabled,
|
|
443
|
-
recordBody: bodyEnabled,
|
|
444
|
-
recordPerformance: networkTimingEnabled,
|
|
445
|
-
payloadHostDenyList: networkPayloadCapture_server_side?.payloadHostDenyList,
|
|
446
|
-
}
|
|
426
|
+
const headersEnabled =
|
|
427
|
+
networkPayloadCapture_client_side?.recordHeaders || networkPayloadCapture_server_side?.recordHeaders
|
|
428
|
+
const bodyEnabled =
|
|
429
|
+
networkPayloadCapture_client_side?.recordBody || networkPayloadCapture_server_side?.recordBody
|
|
430
|
+
const clientConfigForPerformanceCapture = isObject(this._instance.config.capture_performance)
|
|
431
|
+
? this._instance.config.capture_performance.network_timing
|
|
432
|
+
: this._instance.config.capture_performance
|
|
433
|
+
const networkTimingEnabled = !!(isBoolean(clientConfigForPerformanceCapture)
|
|
434
|
+
? clientConfigForPerformanceCapture
|
|
435
|
+
: networkPayloadCapture_server_side?.capturePerformance)
|
|
436
|
+
|
|
437
|
+
return headersEnabled || bodyEnabled || networkTimingEnabled
|
|
438
|
+
? { recordHeaders: headersEnabled, recordBody: bodyEnabled, recordPerformance: networkTimingEnabled }
|
|
439
|
+
: undefined
|
|
447
440
|
}
|
|
448
441
|
|
|
449
442
|
private _gatherRRWebPlugins() {
|
|
450
443
|
const plugins: RecordPlugin[] = []
|
|
451
444
|
|
|
452
|
-
|
|
453
|
-
|
|
445
|
+
const recordConsolePlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordConsolePlugin
|
|
446
|
+
if (recordConsolePlugin && this._isConsoleLogCaptureEnabled) {
|
|
447
|
+
plugins.push(recordConsolePlugin())
|
|
454
448
|
}
|
|
455
449
|
|
|
456
|
-
|
|
450
|
+
const networkPlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin
|
|
451
|
+
if (!!this._networkPayloadCapture && isFunction(networkPlugin)) {
|
|
457
452
|
const canRecordNetwork = !isLocalhost() || this._forceAllowLocalhostNetworkCapture
|
|
458
453
|
|
|
459
454
|
if (canRecordNetwork) {
|
|
460
455
|
plugins.push(
|
|
461
|
-
|
|
462
|
-
buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture)
|
|
463
|
-
)
|
|
456
|
+
networkPlugin(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture))
|
|
464
457
|
)
|
|
465
458
|
} else {
|
|
466
459
|
logger.info('NetworkCapture not started because we are on localhost.')
|
|
@@ -471,7 +464,7 @@ export class LazyLoadedSessionRecording {
|
|
|
471
464
|
}
|
|
472
465
|
|
|
473
466
|
private _maskUrl(url: string): string | undefined {
|
|
474
|
-
const userSessionRecordingOptions = this._instance.config.session_recording
|
|
467
|
+
const userSessionRecordingOptions = this._instance.config.session_recording
|
|
475
468
|
|
|
476
469
|
if (userSessionRecordingOptions.maskNetworkRequestFn) {
|
|
477
470
|
let networkRequest: NetworkRequest | null | undefined = {
|
|
@@ -673,8 +666,8 @@ export class LazyLoadedSessionRecording {
|
|
|
673
666
|
this._statusMatcher = allMatchSessionRecordingStatus
|
|
674
667
|
this._triggerMatching = new AndTriggerMatching([this._eventTriggerMatching, this._urlTriggerMatching])
|
|
675
668
|
}
|
|
676
|
-
this._instance.
|
|
677
|
-
$sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType
|
|
669
|
+
this._instance.register_for_session({
|
|
670
|
+
$sdk_debug_replay_remote_trigger_matching_config: config?.triggerMatchType,
|
|
678
671
|
})
|
|
679
672
|
|
|
680
673
|
this._urlTriggerMatching.onConfig(config)
|
|
@@ -877,7 +870,7 @@ export class LazyLoadedSessionRecording {
|
|
|
877
870
|
}
|
|
878
871
|
|
|
879
872
|
const eventToSend =
|
|
880
|
-
(this._instance.config.session_recording
|
|
873
|
+
(this._instance.config.session_recording.compress_events ?? true) ? compressEvent(event) : event
|
|
881
874
|
const size = estimateSize(eventToSend)
|
|
882
875
|
|
|
883
876
|
const properties = {
|
|
@@ -1020,25 +1013,36 @@ export class LazyLoadedSessionRecording {
|
|
|
1020
1013
|
}
|
|
1021
1014
|
|
|
1022
1015
|
private _captureSnapshot(properties: Properties) {
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1016
|
+
// Send snapshots immediately via the stateless immediate path so they are
|
|
1017
|
+
// not mixed into general event batches. This ensures the client will
|
|
1018
|
+
// choose the snapshot-only endpoint (`/s/`). Fall back to normal
|
|
1019
|
+
// capture if the immediate API isn't available at runtime.
|
|
1020
|
+
const opts = {
|
|
1026
1021
|
_noTruncate: true,
|
|
1027
1022
|
_batchKey: SESSION_RECORDING_BATCH_KEY,
|
|
1028
1023
|
skip_client_rate_limiting: true,
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1024
|
+
}
|
|
1031
1025
|
|
|
1032
|
-
private _snapshotUrl(): string {
|
|
1033
|
-
const host = this._instance.config.host || ''
|
|
1034
1026
|
try {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1027
|
+
const maybeCaptureStatelessImmediate = (this._instance as any).captureStatelessImmediate
|
|
1028
|
+
if (isFunction(maybeCaptureStatelessImmediate)) {
|
|
1029
|
+
const distinctId = (this._instance as any).getDistinctId?.() || undefined
|
|
1030
|
+
// captureStatelessImmediate expects (distinctId, event, properties, options)
|
|
1031
|
+
;(this._instance as any).captureStatelessImmediate(distinctId, '$snapshot', properties, opts)
|
|
1032
|
+
return
|
|
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)
|
|
1041
1037
|
}
|
|
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
|
+
})
|
|
1042
1046
|
}
|
|
1043
1047
|
|
|
1044
1048
|
private get _sessionDuration(): number | null {
|
|
@@ -1099,7 +1103,7 @@ export class LazyLoadedSessionRecording {
|
|
|
1099
1103
|
}
|
|
1100
1104
|
|
|
1101
1105
|
private _reportStarted(startReason: SessionStartReason, tagPayload?: Record<string, any>) {
|
|
1102
|
-
this._instance.
|
|
1106
|
+
this._instance.register_for_session({
|
|
1103
1107
|
$session_recording_start_reason: startReason,
|
|
1104
1108
|
})
|
|
1105
1109
|
logger.info(startReason.replace('_', ' '), tagPayload)
|
|
@@ -1333,8 +1337,8 @@ export class LazyLoadedSessionRecording {
|
|
|
1333
1337
|
this._mutationThrottler =
|
|
1334
1338
|
this._mutationThrottler ??
|
|
1335
1339
|
new MutationThrottler(rrwebRecord, {
|
|
1336
|
-
refillRate: this._instance.config.session_recording
|
|
1337
|
-
bucketSize: this._instance.config.session_recording
|
|
1340
|
+
refillRate: this._instance.config.session_recording.__mutationThrottlerRefillRate,
|
|
1341
|
+
bucketSize: this._instance.config.session_recording.__mutationThrottlerBucketSize,
|
|
1338
1342
|
onBlockedNode: (id, node) => {
|
|
1339
1343
|
const message = `Too many mutations on node '${id}'. Rate limiting. This could be due to SVG animations or something similar`
|
|
1340
1344
|
logger.info(message, {
|
|
@@ -16,11 +16,8 @@ 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
|
-
|
|
22
19
|
this._rateLimiter = new BucketedRateLimiter({
|
|
23
|
-
bucketSize:
|
|
20
|
+
bucketSize: this._options.bucketSize ?? 100,
|
|
24
21
|
refillRate: this._options.refillRate ?? 10,
|
|
25
22
|
refillInterval: 1000, // one second
|
|
26
23
|
_onBucketRateLimited: this._onNodeRateLimited,
|
|
@@ -12,6 +12,7 @@
|
|
|
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'
|
|
15
16
|
import { createLogger } from '../../../utils/logger'
|
|
16
17
|
import { formDataToQuery } from '../../../utils/request-utils'
|
|
17
18
|
import { patch } from '../rrweb-plugins/patch'
|
|
@@ -45,10 +46,6 @@ export function findLast<T>(array: Array<T>, predicate: (value: T) => boolean):
|
|
|
45
46
|
return undefined
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function isDocument(value: any): value is Document {
|
|
49
|
-
return !!value && typeof value === 'object' && 'nodeType' in value && (value as any).nodeType === 9
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
function initPerformanceObserver(cb: networkCallback, win: IWindow, options: Required<NetworkRecordOptions>) {
|
|
53
50
|
// if we are only observing timings then we could have a single observer for all types, with buffer true,
|
|
54
51
|
// but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
|
|
@@ -671,16 +668,12 @@ function initNetworkObserver(
|
|
|
671
668
|
fetchObserver = initFetchObserver(cb, win, networkOptions)
|
|
672
669
|
}
|
|
673
670
|
|
|
674
|
-
|
|
671
|
+
initialisedHandler = () => {
|
|
675
672
|
performanceObserver()
|
|
676
673
|
xhrObserver()
|
|
677
674
|
fetchObserver()
|
|
678
|
-
// allow future observers to initialize after cleanup
|
|
679
|
-
initialisedHandler = null
|
|
680
675
|
}
|
|
681
|
-
|
|
682
|
-
initialisedHandler = teardown
|
|
683
|
-
return teardown
|
|
676
|
+
return initialisedHandler
|
|
684
677
|
}
|
|
685
678
|
|
|
686
679
|
// 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 { RemoteConfig, SessionRecordingPersistedConfig, SessionRecordingUrlTrigger } from '../../../types'
|
|
5
|
+
import { PostHog } from '../../../posthog-core'
|
|
6
|
+
import { FlagVariant, 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/globals'
|
|
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: PostHog) {}
|
|
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.register_for_session({
|
|
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 | FlagVariant | null = null
|
|
216
216
|
linkedFlagSeen: boolean = false
|
|
217
217
|
private _flagListenerCleanup: () => void = () => {}
|
|
218
|
-
constructor(private readonly _instance:
|
|
218
|
+
constructor(private readonly _instance: PostHog) {}
|
|
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.register_for_session({
|
|
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: any, variants: any) => {
|
|
246
|
+
const flagIsPresent = isObject(variants) && linkedFlag in variants
|
|
247
247
|
let linkedFlagMatches = false
|
|
248
248
|
if (flagIsPresent) {
|
|
249
|
-
const variantForFlagKey =
|
|
249
|
+
const variantForFlagKey = variants[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: PostHog) {}
|
|
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.register_for_session({
|
|
318
318
|
$sdk_debug_replay_event_trigger_status: result,
|
|
319
319
|
})
|
|
320
320
|
return result
|
|
@@ -1,3 +1,6 @@
|
|
|
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
|
|
1
4
|
import { isFunction } from '@posthog/core'
|
|
2
5
|
|
|
3
6
|
export function patch(
|
|
@@ -15,6 +18,8 @@ export function patch(
|
|
|
15
18
|
const original = source[name] as () => unknown
|
|
16
19
|
const wrapped = replacement(original)
|
|
17
20
|
|
|
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"
|
|
18
23
|
if (isFunction(wrapped)) {
|
|
19
24
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
20
25
|
wrapped.prototype = wrapped.prototype || {}
|
|
@@ -35,5 +40,7 @@ export function patch(
|
|
|
35
40
|
return () => {
|
|
36
41
|
//
|
|
37
42
|
}
|
|
43
|
+
// This can throw if multiple fill happens on a global object like XMLHttpRequest
|
|
44
|
+
// Fixes https://github.com/getsentry/sentry-javascript/issues/2043
|
|
38
45
|
}
|
|
39
46
|
}
|