@leanbase.com/js 0.1.0 → 0.1.2

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/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
+ }