@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@leanbase-giangnd/js",
3
- "version": "0.0.7",
4
- "description": "Leanbase bgiangndrowser SDK - event tracking, autocapture, and session replay",
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
- "rimraf": "^6.0.1",
45
- "rollup": "catalog:"
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, NetworkRecordOptions, PostHogConfig } from '../../../types'
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-hint',
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
- each(Object.keys(headers ?? {}), (header) => {
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
- headers[header] = REDACTED
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: PostHogConfig['api_host']
114
+ apiHostConfig: LeanbaseConfig['host']
110
115
  ): CapturedNetworkRequest | undefined => {
111
116
  const url = convertToURL(data.name)
112
117
 
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
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: PostHogConfig,
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
- ...(remoteNetworkOptions.payloadHostDenyList || []),
224
+ ...(remoteOptions.payloadHostDenyList || []),
219
225
  ...defaultNetworkOptions.payloadHostDenyList,
220
226
  ],
221
227
  }
222
228
  // client can always disable despite remote options
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
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.api_host))
241
+ payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''))
234
242
 
235
- const hasDeprecatedMaskFunction = isFunction(instanceConfig.session_recording.maskNetworkRequestFn)
243
+ const hasDeprecatedMaskFunction = isFunction(sessionRecordingConfig.maskNetworkRequestFn)
236
244
 
237
- if (hasDeprecatedMaskFunction && isFunction(instanceConfig.session_recording.maskCapturedNetworkRequestFn)) {
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
- instanceConfig.session_recording.maskCapturedNetworkRequestFn = (data: CapturedNetworkRequest) => {
245
- const cleanedURL = instanceConfig.session_recording.maskNetworkRequestFn!({ url: data.name })
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(instanceConfig.session_recording.maskCapturedNetworkRequestFn)
261
+ config.maskRequestFn = isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)
254
262
  ? (data) => {
255
263
  const cleanedRequest = enforcedCleaningFn(data)
256
264
  return cleanedRequest
257
- ? (instanceConfig.session_recording.maskCapturedNetworkRequestFn?.(cleanedRequest) ?? undefined)
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 { assignableWindow, LazyLoadedSessionRecordingInterface, window, document } from '../../../utils/globals'
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 { PostHog } from '../../../posthog-core'
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
- return assignableWindow?.__PosthogExtensions__?.rrweb?.record
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 implements LazyLoadedSessionRecordingInterface {
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.session_idle_threshold_ms || RECORDING_IDLE_THRESHOLD_MS
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: PostHog) {
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.captureCanvas
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 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
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
- const recordConsolePlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordConsolePlugin
446
- if (recordConsolePlugin && this._isConsoleLogCaptureEnabled) {
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
- const networkPlugin = assignableWindow.__PosthogExtensions__?.rrwebPlugins?.getRecordNetworkPlugin
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
- networkPlugin(buildNetworkRequestOptions(this._instance.config, this._networkPayloadCapture))
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.register_for_session({
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.compress_events ?? true) ? compressEvent(event) : event
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
- // 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 = {
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
- 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)
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.register_for_session({
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.__mutationThrottlerRefillRate,
1341
- bucketSize: this._instance.config.session_recording.__mutationThrottlerBucketSize,
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: this._options.bucketSize ?? 100,
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
- initialisedHandler = () => {
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
- return initialisedHandler
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 { PostHog } from '../../../posthog-core'
6
- import { FlagVariant, RemoteConfig, SessionRecordingPersistedConfig, SessionRecordingUrlTrigger } from '../../../types'
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/globals'
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: PostHog) {}
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.register_for_session({
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 | FlagVariant | null = null
215
+ linkedFlag: string | { flag: string; variant: string } | null = null
216
216
  linkedFlagSeen: boolean = false
217
217
  private _flagListenerCleanup: () => void = () => {}
218
- constructor(private readonly _instance: PostHog) {}
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.register_for_session({
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((_flags: any, variants: any) => {
246
- const flagIsPresent = isObject(variants) && linkedFlag in variants
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 = variants[linkedFlag]
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: PostHog) {}
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.register_for_session({
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
  }