@leanbase-giangnd/js 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +143 -0
  2. package/dist/index.cjs +6012 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1484 -0
  5. package/dist/index.mjs +6010 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/leanbase.iife.js +13431 -0
  8. package/dist/leanbase.iife.js.map +1 -0
  9. package/package.json +48 -0
  10. package/src/autocapture-utils.ts +550 -0
  11. package/src/autocapture.ts +415 -0
  12. package/src/config.ts +8 -0
  13. package/src/constants.ts +108 -0
  14. package/src/extensions/rageclick.ts +34 -0
  15. package/src/extensions/replay/external/config.ts +278 -0
  16. package/src/extensions/replay/external/denylist.ts +32 -0
  17. package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +1376 -0
  18. package/src/extensions/replay/external/mutation-throttler.ts +109 -0
  19. package/src/extensions/replay/external/network-plugin.ts +701 -0
  20. package/src/extensions/replay/external/sessionrecording-utils.ts +141 -0
  21. package/src/extensions/replay/external/triggerMatching.ts +422 -0
  22. package/src/extensions/replay/rrweb-plugins/patch.ts +39 -0
  23. package/src/extensions/replay/session-recording.ts +285 -0
  24. package/src/extensions/replay/types/rrweb-types.ts +575 -0
  25. package/src/extensions/replay/types/rrweb.ts +114 -0
  26. package/src/extensions/sampling.ts +26 -0
  27. package/src/iife.ts +87 -0
  28. package/src/index.ts +2 -0
  29. package/src/leanbase-logger.ts +26 -0
  30. package/src/leanbase-persistence.ts +374 -0
  31. package/src/leanbase.ts +457 -0
  32. package/src/page-view.ts +124 -0
  33. package/src/scroll-manager.ts +103 -0
  34. package/src/session-props.ts +114 -0
  35. package/src/sessionid.ts +330 -0
  36. package/src/storage.ts +410 -0
  37. package/src/types/fflate.d.ts +5 -0
  38. package/src/types/rrweb-record.d.ts +8 -0
  39. package/src/types.ts +807 -0
  40. package/src/utils/blocked-uas.ts +162 -0
  41. package/src/utils/element-utils.ts +50 -0
  42. package/src/utils/event-utils.ts +304 -0
  43. package/src/utils/index.ts +222 -0
  44. package/src/utils/logger.ts +26 -0
  45. package/src/utils/request-utils.ts +128 -0
  46. package/src/utils/simple-event-emitter.ts +27 -0
  47. package/src/utils/user-agent-utils.ts +357 -0
  48. package/src/uuidv7.ts +268 -0
  49. package/src/version.ts +1 -0
@@ -0,0 +1,278 @@
1
+ import { CapturedNetworkRequest, LeanbaseConfig, NetworkRecordOptions } from '../../../types'
2
+ import { isArray, isBoolean, isFunction, isNullish, isString, isUndefined } from '@posthog/core'
3
+ import { convertToURL } from '../../../utils/request-utils'
4
+ import { logger } from '../../../utils/logger'
5
+ import { shouldCaptureValue } from '../../../autocapture-utils'
6
+ import { each } from '../../../utils'
7
+
8
+ const LOGGER_PREFIX = '[SessionRecording]'
9
+
10
+ const REDACTED = 'redacted'
11
+
12
+ export const defaultNetworkOptions: Required<NetworkRecordOptions> = {
13
+ initiatorTypes: [
14
+ 'audio',
15
+ 'beacon',
16
+ 'body',
17
+ 'css',
18
+ 'early-hints',
19
+ 'embed',
20
+ 'fetch',
21
+ 'frame',
22
+ 'iframe',
23
+ 'image',
24
+ 'img',
25
+ 'input',
26
+ 'link',
27
+ 'navigation',
28
+ 'object',
29
+ 'ping',
30
+ 'script',
31
+ 'track',
32
+ 'video',
33
+ 'xmlhttprequest',
34
+ ],
35
+ maskRequestFn: (data: CapturedNetworkRequest) => data,
36
+ recordHeaders: false,
37
+ recordBody: false,
38
+ recordInitialRequests: false,
39
+ recordPerformance: false,
40
+ performanceEntryTypeToObserve: [
41
+ // 'event', // This is too noisy as it covers all browser events
42
+ 'first-input',
43
+ // 'mark', // Mark is used too liberally. We would need to filter for specific marks
44
+ // 'measure', // Measure is used too liberally. We would need to filter for specific measures
45
+ 'navigation',
46
+ 'paint',
47
+ 'resource',
48
+ ],
49
+ payloadSizeLimitBytes: 1000000,
50
+ payloadHostDenyList: [
51
+ '.lr-ingest.io',
52
+ '.ingest.sentry.io',
53
+ '.clarity.ms',
54
+ // NB no leading dot here
55
+ 'analytics.google.com',
56
+ 'bam.nr-data.net',
57
+ ],
58
+ }
59
+
60
+ const HEADER_DENY_LIST = [
61
+ 'authorization',
62
+ 'x-forwarded-for',
63
+ 'authorization',
64
+ 'cookie',
65
+ 'set-cookie',
66
+ 'x-api-key',
67
+ 'x-real-ip',
68
+ 'remote-addr',
69
+ 'forwarded',
70
+ 'proxy-authorization',
71
+ 'x-csrf-token',
72
+ 'x-csrftoken',
73
+ 'x-xsrf-token',
74
+ ]
75
+
76
+ const PAYLOAD_CONTENT_DENY_LIST = [
77
+ 'password',
78
+ 'secret',
79
+ 'passwd',
80
+ 'api_key',
81
+ 'apikey',
82
+ 'auth',
83
+ 'credentials',
84
+ 'mysql_pwd',
85
+ 'privatekey',
86
+ 'private_key',
87
+ 'token',
88
+ ]
89
+
90
+ // we always remove headers on the deny list because we never want to capture this sensitive data
91
+ const removeAuthorizationHeader = (data: CapturedNetworkRequest): CapturedNetworkRequest => {
92
+ const headers = data.requestHeaders
93
+ if (!isNullish(headers)) {
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) => {
99
+ if (HEADER_DENY_LIST.includes(header.toLowerCase())) {
100
+ mutableHeaders[header] = REDACTED
101
+ }
102
+ })
103
+
104
+ data.requestHeaders = mutableHeaders as any
105
+ }
106
+ return data
107
+ }
108
+
109
+ const POSTHOG_PATHS_TO_IGNORE = ['/s/', '/e/', '/i/']
110
+ // want to ignore posthog paths when capturing requests, or we can get trapped in a loop
111
+ // because calls to PostHog would be reported using a call to PostHog which would be reported....
112
+ const ignorePostHogPaths = (
113
+ data: CapturedNetworkRequest,
114
+ apiHostConfig: LeanbaseConfig['host']
115
+ ): CapturedNetworkRequest | undefined => {
116
+ const url = convertToURL(data.name)
117
+
118
+ const host = apiHostConfig || ''
119
+ let replaceValue = host.indexOf('http') === 0 ? convertToURL(host)?.pathname : host
120
+ if (replaceValue === '/') {
121
+ replaceValue = ''
122
+ }
123
+ const pathname = url?.pathname.replace(replaceValue || '', '')
124
+
125
+ if (url && pathname && POSTHOG_PATHS_TO_IGNORE.some((path) => pathname.indexOf(path) === 0)) {
126
+ return undefined
127
+ }
128
+ return data
129
+ }
130
+
131
+ function estimateBytes(payload: string): number {
132
+ return new Blob([payload]).size
133
+ }
134
+
135
+ function enforcePayloadSizeLimit(
136
+ payload: string | null | undefined,
137
+ headers: Record<string, any> | undefined,
138
+ limit: number,
139
+ description: string
140
+ ): string | null | undefined {
141
+ if (isNullish(payload)) {
142
+ return payload
143
+ }
144
+
145
+ let requestContentLength: string | number = headers?.['content-length'] || estimateBytes(payload)
146
+ if (isString(requestContentLength)) {
147
+ requestContentLength = parseInt(requestContentLength)
148
+ }
149
+
150
+ if (requestContentLength > limit) {
151
+ return LOGGER_PREFIX + ` ${description} body too large to record (${requestContentLength} bytes)`
152
+ }
153
+
154
+ return payload
155
+ }
156
+
157
+ // people can have arbitrarily large payloads on their site, but we don't want to ingest them
158
+ const limitPayloadSize = (
159
+ options: NetworkRecordOptions
160
+ ): ((data: CapturedNetworkRequest | undefined) => CapturedNetworkRequest | undefined) => {
161
+ // the smallest of 1MB or the specified limit if there is one
162
+ const limit = Math.min(1000000, options.payloadSizeLimitBytes ?? 1000000)
163
+
164
+ return (data) => {
165
+ if (data?.requestBody) {
166
+ data.requestBody = enforcePayloadSizeLimit(data.requestBody, data.requestHeaders, limit, 'Request')
167
+ }
168
+
169
+ if (data?.responseBody) {
170
+ data.responseBody = enforcePayloadSizeLimit(data.responseBody, data.responseHeaders, limit, 'Response')
171
+ }
172
+
173
+ return data
174
+ }
175
+ }
176
+
177
+ function scrubPayload(payload: string | null | undefined, label: 'Request' | 'Response'): string | null | undefined {
178
+ if (isNullish(payload)) {
179
+ return payload
180
+ }
181
+ let scrubbed = payload
182
+
183
+ if (!shouldCaptureValue(scrubbed, false)) {
184
+ scrubbed = LOGGER_PREFIX + ' ' + label + ' body ' + REDACTED
185
+ }
186
+ each(PAYLOAD_CONTENT_DENY_LIST, (text) => {
187
+ if (scrubbed?.length && scrubbed?.indexOf(text) !== -1) {
188
+ scrubbed = LOGGER_PREFIX + ' ' + label + ' body ' + REDACTED + ' as might contain: ' + text
189
+ }
190
+ })
191
+
192
+ return scrubbed
193
+ }
194
+
195
+ function scrubPayloads(capturedRequest: CapturedNetworkRequest | undefined): CapturedNetworkRequest | undefined {
196
+ if (isUndefined(capturedRequest)) {
197
+ return undefined
198
+ }
199
+
200
+ capturedRequest.requestBody = scrubPayload(capturedRequest.requestBody, 'Request')
201
+ capturedRequest.responseBody = scrubPayload(capturedRequest.responseBody, 'Response')
202
+
203
+ return capturedRequest
204
+ }
205
+
206
+ /**
207
+ * whether a maskRequestFn is provided or not,
208
+ * we ensure that we remove the denied header from requests
209
+ * we _never_ want to record that header by accident
210
+ * if someone complains then we'll add an opt-in to let them override it
211
+ */
212
+ export const buildNetworkRequestOptions = (
213
+ instanceConfig: LeanbaseConfig,
214
+ remoteNetworkOptions: Pick<
215
+ NetworkRecordOptions,
216
+ 'recordHeaders' | 'recordBody' | 'recordPerformance' | 'payloadHostDenyList'
217
+ > = {}
218
+ ): NetworkRecordOptions => {
219
+ const remoteOptions = remoteNetworkOptions || {}
220
+ const config: NetworkRecordOptions = {
221
+ payloadSizeLimitBytes: defaultNetworkOptions.payloadSizeLimitBytes,
222
+ performanceEntryTypeToObserve: [...defaultNetworkOptions.performanceEntryTypeToObserve],
223
+ payloadHostDenyList: [
224
+ ...(remoteOptions.payloadHostDenyList || []),
225
+ ...defaultNetworkOptions.payloadHostDenyList,
226
+ ],
227
+ }
228
+ // client can always disable despite remote options
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
237
+
238
+ const payloadLimiter = limitPayloadSize(config)
239
+
240
+ const enforcedCleaningFn: NetworkRecordOptions['maskRequestFn'] = (d: CapturedNetworkRequest) =>
241
+ payloadLimiter(ignorePostHogPaths(removeAuthorizationHeader(d), instanceConfig.host || ''))
242
+
243
+ const hasDeprecatedMaskFunction = isFunction(sessionRecordingConfig.maskNetworkRequestFn)
244
+
245
+ if (hasDeprecatedMaskFunction && isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)) {
246
+ logger.warn(
247
+ 'Both `maskNetworkRequestFn` and `maskCapturedNetworkRequestFn` are defined. `maskNetworkRequestFn` will be ignored.'
248
+ )
249
+ }
250
+
251
+ if (hasDeprecatedMaskFunction) {
252
+ sessionRecordingConfig.maskCapturedNetworkRequestFn = (data: CapturedNetworkRequest) => {
253
+ const cleanedURL = sessionRecordingConfig.maskNetworkRequestFn!({ url: data.name })
254
+ return {
255
+ ...data,
256
+ name: cleanedURL?.url,
257
+ } as CapturedNetworkRequest
258
+ }
259
+ }
260
+
261
+ config.maskRequestFn = isFunction(sessionRecordingConfig.maskCapturedNetworkRequestFn)
262
+ ? (data) => {
263
+ const cleanedRequest = enforcedCleaningFn(data)
264
+ return cleanedRequest
265
+ ? (sessionRecordingConfig.maskCapturedNetworkRequestFn?.(cleanedRequest) ?? undefined)
266
+ : undefined
267
+ }
268
+ : (data) => scrubPayloads(enforcedCleaningFn(data))
269
+
270
+ return {
271
+ ...defaultNetworkOptions,
272
+ ...config,
273
+ recordHeaders: canRecordHeaders,
274
+ recordBody: canRecordBody,
275
+ recordPerformance: canRecordPerformance,
276
+ recordInitialRequests: canRecordPerformance,
277
+ }
278
+ }
@@ -0,0 +1,32 @@
1
+ import { NetworkRecordOptions } from '../../../types'
2
+
3
+ function hostnameFromURL(url: string | URL | RequestInfo): string | null {
4
+ try {
5
+ if (typeof url === 'string') {
6
+ return new URL(url).hostname
7
+ }
8
+ if ('url' in url) {
9
+ return new URL(url.url).hostname
10
+ }
11
+ return url.hostname
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ export function isHostOnDenyList(url: string | URL | Request, options: NetworkRecordOptions) {
18
+ const hostname = hostnameFromURL(url)
19
+ const defaultNotDenied = { hostname, isHostDenied: false }
20
+
21
+ if (!options.payloadHostDenyList?.length || !hostname?.trim().length) {
22
+ return defaultNotDenied
23
+ }
24
+
25
+ for (const deny of options.payloadHostDenyList) {
26
+ if (hostname.endsWith(deny)) {
27
+ return { hostname, isHostDenied: true }
28
+ }
29
+ }
30
+
31
+ return defaultNotDenied
32
+ }