@leanbase.com/js 0.1.1 → 0.1.3
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/README.md +0 -12
- package/dist/index.cjs +2976 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +760 -11
- package/dist/index.mjs +2977 -61
- package/dist/index.mjs.map +1 -1
- package/dist/leanbase.iife.js +3093 -134
- package/dist/leanbase.iife.js.map +1 -1
- package/package.json +45 -45
- package/src/autocapture-utils.ts +550 -0
- package/src/autocapture.ts +415 -0
- package/src/config.ts +8 -0
- package/src/constants.ts +108 -0
- package/src/extensions/rageclick.ts +34 -0
- package/src/iife.ts +31 -27
- package/src/index.ts +1 -1
- package/src/leanbase-logger.ts +7 -4
- package/src/leanbase-persistence.ts +374 -0
- package/src/leanbase.ts +366 -71
- package/src/page-view.ts +124 -0
- package/src/scroll-manager.ts +103 -0
- package/src/session-props.ts +114 -0
- package/src/sessionid.ts +330 -0
- package/src/storage.ts +410 -0
- package/src/types.ts +634 -0
- package/src/utils/blocked-uas.ts +162 -0
- package/src/utils/element-utils.ts +50 -0
- package/src/utils/event-utils.ts +304 -0
- package/src/utils/index.ts +222 -0
- package/src/utils/request-utils.ts +128 -0
- package/src/utils/simple-event-emitter.ts +27 -0
- package/src/utils/user-agent-utils.ts +357 -0
- package/src/uuidv7.ts +268 -0
- package/src/version.ts +1 -1
- package/LICENSE +0 -37
package/src/storage.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { extend } from './utils'
|
|
2
|
+
import { PersistentStore, Properties } from './types'
|
|
3
|
+
import {
|
|
4
|
+
DISTINCT_ID,
|
|
5
|
+
ENABLE_PERSON_PROCESSING,
|
|
6
|
+
INITIAL_PERSON_INFO,
|
|
7
|
+
SESSION_ID,
|
|
8
|
+
SESSION_RECORDING_IS_SAMPLED,
|
|
9
|
+
} from './constants'
|
|
10
|
+
|
|
11
|
+
import { isNull, isUndefined } from '@posthog/core'
|
|
12
|
+
import { window, document } from './utils'
|
|
13
|
+
import { logger } from './leanbase-logger'
|
|
14
|
+
import { uuidv7 } from './uuidv7'
|
|
15
|
+
|
|
16
|
+
// we store the discovered subdomain in memory because it might be read multiple times
|
|
17
|
+
let firstNonPublicSubDomain = ''
|
|
18
|
+
|
|
19
|
+
// helper to allow tests to clear this "cache"
|
|
20
|
+
export const resetSubDomainCache = () => {
|
|
21
|
+
firstNonPublicSubDomain = ''
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Browsers don't offer a way to check if something is a public suffix
|
|
26
|
+
* e.g. `.com.au`, `.io`, `.org.uk`
|
|
27
|
+
*
|
|
28
|
+
* But they do reject cookies set on public suffixes
|
|
29
|
+
* Setting a cookie on `.co.uk` would mean it was sent for every `.co.uk` site visited
|
|
30
|
+
*
|
|
31
|
+
* So, we can use this to check if a domain is a public suffix
|
|
32
|
+
* by trying to set a cookie on a subdomain of the provided hostname
|
|
33
|
+
* until the browser accepts it
|
|
34
|
+
*
|
|
35
|
+
* inspired by https://github.com/AngusFu/browser-root-domain
|
|
36
|
+
*/
|
|
37
|
+
export function seekFirstNonPublicSubDomain(hostname: string, cookieJar = document): string {
|
|
38
|
+
if (firstNonPublicSubDomain) {
|
|
39
|
+
return firstNonPublicSubDomain
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!cookieJar) {
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
if (['localhost', '127.0.0.1'].includes(hostname)) return ''
|
|
46
|
+
|
|
47
|
+
const list = hostname.split('.')
|
|
48
|
+
let len = Math.min(list.length, 8) // paranoia - we know this number should be small
|
|
49
|
+
const key = 'dmn_chk_' + uuidv7()
|
|
50
|
+
|
|
51
|
+
while (!firstNonPublicSubDomain && len--) {
|
|
52
|
+
const candidate = list.slice(len).join('.')
|
|
53
|
+
const candidateCookieValue = key + '=1;domain=.' + candidate + ';path=/'
|
|
54
|
+
|
|
55
|
+
// try to set cookie, include a short expiry in seconds since we'll check immediately
|
|
56
|
+
cookieJar.cookie = candidateCookieValue + ';max-age=3'
|
|
57
|
+
|
|
58
|
+
if (cookieJar.cookie.includes(key)) {
|
|
59
|
+
// the cookie was accepted by the browser, remove the test cookie
|
|
60
|
+
cookieJar.cookie = candidateCookieValue + ';max-age=0'
|
|
61
|
+
firstNonPublicSubDomain = candidate
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return firstNonPublicSubDomain
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z]{2,}$/i
|
|
69
|
+
const originalCookieDomainFn = (hostname: string): string => {
|
|
70
|
+
const matches = hostname.match(DOMAIN_MATCH_REGEX)
|
|
71
|
+
return matches ? matches[0] : ''
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function chooseCookieDomain(hostname: string, cross_subdomain: boolean | undefined): string {
|
|
75
|
+
if (cross_subdomain) {
|
|
76
|
+
// NOTE: Could we use this for cross domain tracking?
|
|
77
|
+
let matchedSubDomain = seekFirstNonPublicSubDomain(hostname)
|
|
78
|
+
|
|
79
|
+
if (!matchedSubDomain) {
|
|
80
|
+
const originalMatch = originalCookieDomainFn(hostname)
|
|
81
|
+
if (originalMatch !== matchedSubDomain) {
|
|
82
|
+
logger.info('Warning: cookie subdomain discovery mismatch', originalMatch, matchedSubDomain)
|
|
83
|
+
}
|
|
84
|
+
matchedSubDomain = originalMatch
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return matchedSubDomain ? '; domain=.' + matchedSubDomain : ''
|
|
88
|
+
}
|
|
89
|
+
return ''
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Methods partially borrowed from quirksmode.org/js/cookies.html
|
|
93
|
+
export const cookieStore: PersistentStore = {
|
|
94
|
+
_is_supported: () => !!document,
|
|
95
|
+
|
|
96
|
+
_error: function (msg) {
|
|
97
|
+
logger.error('cookieStore error: ' + msg)
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
_get: function (name) {
|
|
101
|
+
if (!document) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const nameEQ = name + '='
|
|
107
|
+
const ca = document.cookie.split(';').filter((x) => x.length)
|
|
108
|
+
for (let i = 0; i < ca.length; i++) {
|
|
109
|
+
let c = ca[i]
|
|
110
|
+
while (c.charAt(0) == ' ') {
|
|
111
|
+
c = c.substring(1, c.length)
|
|
112
|
+
}
|
|
113
|
+
if (c.indexOf(nameEQ) === 0) {
|
|
114
|
+
return decodeURIComponent(c.substring(nameEQ.length, c.length))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} catch {}
|
|
118
|
+
return null
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
_parse: function (name) {
|
|
122
|
+
let cookie
|
|
123
|
+
try {
|
|
124
|
+
cookie = JSON.parse(cookieStore._get(name)) || {}
|
|
125
|
+
} catch {
|
|
126
|
+
// noop
|
|
127
|
+
}
|
|
128
|
+
return cookie
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
_set: function (name, value, days, cross_subdomain, is_secure) {
|
|
132
|
+
if (!document) {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
let expires = '',
|
|
137
|
+
secure = ''
|
|
138
|
+
|
|
139
|
+
const cdomain = chooseCookieDomain(document.location.hostname, cross_subdomain)
|
|
140
|
+
|
|
141
|
+
if (days) {
|
|
142
|
+
const date = new Date()
|
|
143
|
+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
|
|
144
|
+
expires = '; expires=' + date.toUTCString()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (is_secure) {
|
|
148
|
+
secure = '; secure'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const new_cookie_val =
|
|
152
|
+
name +
|
|
153
|
+
'=' +
|
|
154
|
+
encodeURIComponent(JSON.stringify(value)) +
|
|
155
|
+
expires +
|
|
156
|
+
'; SameSite=Lax; path=/' +
|
|
157
|
+
cdomain +
|
|
158
|
+
secure
|
|
159
|
+
|
|
160
|
+
// 4096 bytes is the size at which some browsers (e.g. firefox) will not store a cookie, warn slightly before that
|
|
161
|
+
if (new_cookie_val.length > 4096 * 0.9) {
|
|
162
|
+
logger.warn('cookieStore warning: large cookie, len=' + new_cookie_val.length)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
document.cookie = new_cookie_val
|
|
166
|
+
return new_cookie_val
|
|
167
|
+
} catch {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
_remove: function (name, cross_subdomain) {
|
|
173
|
+
if (!document?.cookie) {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
cookieStore._set(name, '', -1, cross_subdomain)
|
|
178
|
+
} catch {
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let _localStorage_supported: boolean | null = null
|
|
185
|
+
export const resetLocalStorageSupported = () => {
|
|
186
|
+
_localStorage_supported = null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const localStore: PersistentStore = {
|
|
190
|
+
_is_supported: function () {
|
|
191
|
+
if (!isNull(_localStorage_supported)) {
|
|
192
|
+
return _localStorage_supported
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let supported = true
|
|
196
|
+
if (!isUndefined(window)) {
|
|
197
|
+
try {
|
|
198
|
+
const key = '__mplssupport__',
|
|
199
|
+
val = 'xyz'
|
|
200
|
+
localStore._set(key, val)
|
|
201
|
+
if (localStore._get(key) !== '"xyz"') {
|
|
202
|
+
supported = false
|
|
203
|
+
}
|
|
204
|
+
localStore._remove(key)
|
|
205
|
+
} catch {
|
|
206
|
+
supported = false
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
supported = false
|
|
210
|
+
}
|
|
211
|
+
if (!supported) {
|
|
212
|
+
logger.error('localStorage unsupported; falling back to cookie store')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_localStorage_supported = supported
|
|
216
|
+
return supported
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
_error: function (msg) {
|
|
220
|
+
logger.error('localStorage error: ' + msg)
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
_get: function (name) {
|
|
224
|
+
try {
|
|
225
|
+
return window?.localStorage.getItem(name)
|
|
226
|
+
} catch (err) {
|
|
227
|
+
localStore._error(err)
|
|
228
|
+
}
|
|
229
|
+
return null
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
_parse: function (name) {
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(localStore._get(name)) || {}
|
|
235
|
+
} catch {
|
|
236
|
+
// noop
|
|
237
|
+
}
|
|
238
|
+
return null
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
_set: function (name, value) {
|
|
242
|
+
try {
|
|
243
|
+
window?.localStorage.setItem(name, JSON.stringify(value))
|
|
244
|
+
} catch (err) {
|
|
245
|
+
localStore._error(err)
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
_remove: function (name) {
|
|
250
|
+
try {
|
|
251
|
+
window?.localStorage.removeItem(name)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
localStore._error(err)
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Use localstorage for most data but still use cookie for COOKIE_PERSISTED_PROPERTIES
|
|
259
|
+
// This solves issues with cookies having too much data in them causing headers too large
|
|
260
|
+
// Also makes sure we don't have to send a ton of data to the server
|
|
261
|
+
const COOKIE_PERSISTED_PROPERTIES = [
|
|
262
|
+
DISTINCT_ID,
|
|
263
|
+
SESSION_ID,
|
|
264
|
+
SESSION_RECORDING_IS_SAMPLED,
|
|
265
|
+
ENABLE_PERSON_PROCESSING,
|
|
266
|
+
INITIAL_PERSON_INFO,
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
export const localPlusCookieStore: PersistentStore = {
|
|
270
|
+
...localStore,
|
|
271
|
+
_parse: function (name) {
|
|
272
|
+
try {
|
|
273
|
+
let cookieProperties: Properties = {}
|
|
274
|
+
try {
|
|
275
|
+
// See if there's a cookie stored with data.
|
|
276
|
+
cookieProperties = cookieStore._parse(name) || {}
|
|
277
|
+
} catch {}
|
|
278
|
+
const value = extend(cookieProperties, JSON.parse(localStore._get(name) || '{}'))
|
|
279
|
+
localStore._set(name, value)
|
|
280
|
+
return value
|
|
281
|
+
} catch {
|
|
282
|
+
// noop
|
|
283
|
+
}
|
|
284
|
+
return null
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
_set: function (name, value, days, cross_subdomain, is_secure, debug) {
|
|
288
|
+
try {
|
|
289
|
+
localStore._set(name, value, undefined, undefined, debug)
|
|
290
|
+
const cookiePersistedProperties: Record<string, any> = {}
|
|
291
|
+
COOKIE_PERSISTED_PROPERTIES.forEach((key) => {
|
|
292
|
+
if (value[key]) {
|
|
293
|
+
cookiePersistedProperties[key] = value[key]
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
if (Object.keys(cookiePersistedProperties).length) {
|
|
298
|
+
cookieStore._set(name, cookiePersistedProperties, days, cross_subdomain, is_secure, debug)
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
localStore._error(err)
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
_remove: function (name, cross_subdomain) {
|
|
306
|
+
try {
|
|
307
|
+
window?.localStorage.removeItem(name)
|
|
308
|
+
cookieStore._remove(name, cross_subdomain)
|
|
309
|
+
} catch (err) {
|
|
310
|
+
localStore._error(err)
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const memoryStorage: Properties = {}
|
|
316
|
+
|
|
317
|
+
// Storage that only lasts the length of the pageview if we don't want to use cookies
|
|
318
|
+
export const memoryStore: PersistentStore = {
|
|
319
|
+
_is_supported: function () {
|
|
320
|
+
return true
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
_error: function (msg) {
|
|
324
|
+
logger.error('memoryStorage error: ' + msg)
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
_get: function (name) {
|
|
328
|
+
return memoryStorage[name] || null
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
_parse: function (name) {
|
|
332
|
+
return memoryStorage[name] || null
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
_set: function (name, value) {
|
|
336
|
+
memoryStorage[name] = value
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
_remove: function (name) {
|
|
340
|
+
delete memoryStorage[name]
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let sessionStorageSupported: boolean | null = null
|
|
345
|
+
export const resetSessionStorageSupported = () => {
|
|
346
|
+
sessionStorageSupported = null
|
|
347
|
+
}
|
|
348
|
+
// Storage that only lasts the length of a tab/window. Survives page refreshes
|
|
349
|
+
export const sessionStore: PersistentStore = {
|
|
350
|
+
_is_supported: function () {
|
|
351
|
+
if (!isNull(sessionStorageSupported)) {
|
|
352
|
+
return sessionStorageSupported
|
|
353
|
+
}
|
|
354
|
+
sessionStorageSupported = true
|
|
355
|
+
if (!isUndefined(window)) {
|
|
356
|
+
try {
|
|
357
|
+
const key = '__support__',
|
|
358
|
+
val = 'xyz'
|
|
359
|
+
sessionStore._set(key, val)
|
|
360
|
+
if (sessionStore._get(key) !== '"xyz"') {
|
|
361
|
+
sessionStorageSupported = false
|
|
362
|
+
}
|
|
363
|
+
sessionStore._remove(key)
|
|
364
|
+
} catch {
|
|
365
|
+
sessionStorageSupported = false
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
sessionStorageSupported = false
|
|
369
|
+
}
|
|
370
|
+
return sessionStorageSupported
|
|
371
|
+
},
|
|
372
|
+
|
|
373
|
+
_error: function (msg) {
|
|
374
|
+
logger.error('sessionStorage error: ', msg)
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
_get: function (name) {
|
|
378
|
+
try {
|
|
379
|
+
return window?.sessionStorage.getItem(name)
|
|
380
|
+
} catch (err) {
|
|
381
|
+
sessionStore._error(err)
|
|
382
|
+
}
|
|
383
|
+
return null
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
_parse: function (name) {
|
|
387
|
+
try {
|
|
388
|
+
return JSON.parse(sessionStore._get(name)) || null
|
|
389
|
+
} catch {
|
|
390
|
+
// noop
|
|
391
|
+
}
|
|
392
|
+
return null
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
_set: function (name, value) {
|
|
396
|
+
try {
|
|
397
|
+
window?.sessionStorage.setItem(name, JSON.stringify(value))
|
|
398
|
+
} catch (err) {
|
|
399
|
+
sessionStore._error(err)
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
_remove: function (name) {
|
|
404
|
+
try {
|
|
405
|
+
window?.sessionStorage.removeItem(name)
|
|
406
|
+
} catch (err) {
|
|
407
|
+
sessionStore._error(err)
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
}
|