@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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { window } from './utils'
|
|
2
|
+
import { Leanbase } from './leanbase'
|
|
3
|
+
import { addEventListener } from './utils'
|
|
4
|
+
import { isArray } from '@posthog/core'
|
|
5
|
+
|
|
6
|
+
export interface ScrollContext {
|
|
7
|
+
// scroll is how far down the page the user has scrolled,
|
|
8
|
+
// content is how far down the page the user can view content
|
|
9
|
+
// (e.g. if the page is 1000 tall, but the user's screen is only 500 tall,
|
|
10
|
+
// and they don't scroll at all, then scroll is 0 and content is 500)
|
|
11
|
+
maxScrollHeight?: number
|
|
12
|
+
maxScrollY?: number
|
|
13
|
+
lastScrollY?: number
|
|
14
|
+
maxContentHeight?: number
|
|
15
|
+
maxContentY?: number
|
|
16
|
+
lastContentY?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// This class is responsible for tracking scroll events and maintaining the scroll context
|
|
20
|
+
export class ScrollManager {
|
|
21
|
+
private _context: ScrollContext | undefined
|
|
22
|
+
|
|
23
|
+
constructor(private _instance: Leanbase) {}
|
|
24
|
+
|
|
25
|
+
getContext(): ScrollContext | undefined {
|
|
26
|
+
return this._context
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resetContext(): ScrollContext | undefined {
|
|
30
|
+
const ctx = this._context
|
|
31
|
+
|
|
32
|
+
// update the scroll properties for the new page, but wait until the next tick
|
|
33
|
+
// of the event loop
|
|
34
|
+
setTimeout(this._updateScrollData, 0)
|
|
35
|
+
|
|
36
|
+
return ctx
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private _updateScrollData = () => {
|
|
40
|
+
if (!this._context) {
|
|
41
|
+
this._context = {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const el = this.scrollElement()
|
|
45
|
+
|
|
46
|
+
const scrollY = this.scrollY()
|
|
47
|
+
const scrollHeight = el ? Math.max(0, el.scrollHeight - el.clientHeight) : 0
|
|
48
|
+
const contentY = scrollY + (el?.clientHeight || 0)
|
|
49
|
+
const contentHeight = el?.scrollHeight || 0
|
|
50
|
+
|
|
51
|
+
this._context.lastScrollY = Math.ceil(scrollY)
|
|
52
|
+
this._context.maxScrollY = Math.max(scrollY, this._context.maxScrollY ?? 0)
|
|
53
|
+
this._context.maxScrollHeight = Math.max(scrollHeight, this._context.maxScrollHeight ?? 0)
|
|
54
|
+
|
|
55
|
+
this._context.lastContentY = contentY
|
|
56
|
+
this._context.maxContentY = Math.max(contentY, this._context.maxContentY ?? 0)
|
|
57
|
+
this._context.maxContentHeight = Math.max(contentHeight, this._context.maxContentHeight ?? 0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// `capture: true` is required to get scroll events for other scrollable elements
|
|
61
|
+
// on the page, not just the window
|
|
62
|
+
// see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture
|
|
63
|
+
startMeasuringScrollPosition() {
|
|
64
|
+
addEventListener(window, 'scroll', this._updateScrollData, { capture: true })
|
|
65
|
+
addEventListener(window, 'scrollend', this._updateScrollData, { capture: true })
|
|
66
|
+
addEventListener(window, 'resize', this._updateScrollData)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public scrollElement(): Element | undefined {
|
|
70
|
+
if (this._instance.config.scroll_root_selector) {
|
|
71
|
+
const selectors = isArray(this._instance.config.scroll_root_selector)
|
|
72
|
+
? this._instance.config.scroll_root_selector
|
|
73
|
+
: [this._instance.config.scroll_root_selector]
|
|
74
|
+
for (const selector of selectors) {
|
|
75
|
+
const element = window?.document.querySelector(selector)
|
|
76
|
+
if (element) {
|
|
77
|
+
return element
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined
|
|
81
|
+
} else {
|
|
82
|
+
return window?.document.documentElement
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public scrollY(): number {
|
|
87
|
+
if (this._instance.config.scroll_root_selector) {
|
|
88
|
+
const element = this.scrollElement()
|
|
89
|
+
return (element && element.scrollTop) || 0
|
|
90
|
+
} else {
|
|
91
|
+
return window ? window.scrollY || window.pageYOffset || window.document.documentElement.scrollTop || 0 : 0
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public scrollX(): number {
|
|
96
|
+
if (this._instance.config.scroll_root_selector) {
|
|
97
|
+
const element = this.scrollElement()
|
|
98
|
+
return (element && element.scrollLeft) || 0
|
|
99
|
+
} else {
|
|
100
|
+
return window ? window.scrollX || window.pageXOffset || window.document.documentElement.scrollLeft || 0 : 0
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* Store some session-level attribution-related properties in the persistence layer
|
|
2
|
+
*
|
|
3
|
+
* These have the same lifespan as a session_id, meaning that if the session_id changes, these properties will be reset.
|
|
4
|
+
*
|
|
5
|
+
* We only store the entry URL and referrer, and derive many props (such as utm tags) from those.
|
|
6
|
+
*
|
|
7
|
+
* Given that the cookie is limited to 4K bytes, we don't want to store too much data, so we chose not to store device
|
|
8
|
+
* properties (such as browser, OS, etc) here, as usually getting the current value of those from event properties is
|
|
9
|
+
* sufficient.
|
|
10
|
+
*/
|
|
11
|
+
import { getPersonInfo, getPersonPropsFromInfo } from './utils/event-utils'
|
|
12
|
+
import type { SessionIdManager } from './sessionid'
|
|
13
|
+
import type { LeanbasePersistence } from './leanbase-persistence'
|
|
14
|
+
import { CLIENT_SESSION_PROPS } from './constants'
|
|
15
|
+
import type { Leanbase } from './leanbase'
|
|
16
|
+
import { each, stripEmptyProperties } from './utils'
|
|
17
|
+
import { stripLeadingDollar } from '@posthog/core'
|
|
18
|
+
|
|
19
|
+
interface LegacySessionSourceProps {
|
|
20
|
+
initialPathName: string
|
|
21
|
+
referringDomain: string // Is actually referring host, but named referring domain for internal consistency. Should contain a port if there is one.
|
|
22
|
+
utm_medium?: string
|
|
23
|
+
utm_source?: string
|
|
24
|
+
utm_campaign?: string
|
|
25
|
+
utm_content?: string
|
|
26
|
+
utm_term?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface CurrentSessionSourceProps {
|
|
30
|
+
r: string // Referring host
|
|
31
|
+
u: string | undefined // full URL
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface StoredSessionSourceProps {
|
|
35
|
+
sessionId: string
|
|
36
|
+
props: LegacySessionSourceProps | CurrentSessionSourceProps
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const generateSessionSourceParams = (posthog?: Leanbase): LegacySessionSourceProps | CurrentSessionSourceProps => {
|
|
40
|
+
return getPersonInfo(posthog?.config.mask_personal_data_properties, posthog?.config.custom_personal_data_properties)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class SessionPropsManager {
|
|
44
|
+
private readonly _instance: Leanbase
|
|
45
|
+
private readonly _sessionIdManager: SessionIdManager
|
|
46
|
+
private readonly _persistence: LeanbasePersistence
|
|
47
|
+
private readonly _sessionSourceParamGenerator: (
|
|
48
|
+
instance?: Leanbase
|
|
49
|
+
) => LegacySessionSourceProps | CurrentSessionSourceProps
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
instance: Leanbase,
|
|
53
|
+
sessionIdManager: SessionIdManager,
|
|
54
|
+
persistence: LeanbasePersistence,
|
|
55
|
+
sessionSourceParamGenerator?: (instance?: Leanbase) => LegacySessionSourceProps | CurrentSessionSourceProps
|
|
56
|
+
) {
|
|
57
|
+
this._instance = instance
|
|
58
|
+
this._sessionIdManager = sessionIdManager
|
|
59
|
+
this._persistence = persistence
|
|
60
|
+
this._sessionSourceParamGenerator = sessionSourceParamGenerator || generateSessionSourceParams
|
|
61
|
+
|
|
62
|
+
this._sessionIdManager.onSessionId(this._onSessionIdCallback)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_getStored(): StoredSessionSourceProps | undefined {
|
|
66
|
+
return this._persistence.props[CLIENT_SESSION_PROPS]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_onSessionIdCallback = (sessionId: string) => {
|
|
70
|
+
const stored = this._getStored()
|
|
71
|
+
if (stored && stored.sessionId === sessionId) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newProps: StoredSessionSourceProps = {
|
|
76
|
+
sessionId,
|
|
77
|
+
props: this._sessionSourceParamGenerator(this._instance),
|
|
78
|
+
}
|
|
79
|
+
this._persistence.register({ [CLIENT_SESSION_PROPS]: newProps })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
getSetOnceProps() {
|
|
83
|
+
const p = this._getStored()?.props
|
|
84
|
+
if (!p) {
|
|
85
|
+
return {}
|
|
86
|
+
}
|
|
87
|
+
if ('r' in p) {
|
|
88
|
+
return getPersonPropsFromInfo(p)
|
|
89
|
+
} else {
|
|
90
|
+
return {
|
|
91
|
+
$referring_domain: p.referringDomain,
|
|
92
|
+
$pathname: p.initialPathName,
|
|
93
|
+
utm_source: p.utm_source,
|
|
94
|
+
utm_campaign: p.utm_campaign,
|
|
95
|
+
utm_medium: p.utm_medium,
|
|
96
|
+
utm_content: p.utm_content,
|
|
97
|
+
utm_term: p.utm_term,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getSessionProps() {
|
|
103
|
+
// it's the same props, but don't include null for unset properties, and add a prefix
|
|
104
|
+
const p: Record<string, any> = {}
|
|
105
|
+
each(stripEmptyProperties(this.getSetOnceProps()), (v, k) => {
|
|
106
|
+
if (k === '$current_url') {
|
|
107
|
+
// $session_entry_current_url would be a weird name, call it $session_entry_url instead
|
|
108
|
+
k = 'url'
|
|
109
|
+
}
|
|
110
|
+
p[`$session_entry_${stripLeadingDollar(k)}`] = v
|
|
111
|
+
})
|
|
112
|
+
return p
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/sessionid.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { LeanbasePersistence } from './leanbase-persistence'
|
|
2
|
+
import { SESSION_ID } from './constants'
|
|
3
|
+
import { sessionStore } from './storage'
|
|
4
|
+
import { LeanbaseConfig, SessionIdChangedCallback } from './types'
|
|
5
|
+
import { uuid7ToTimestampMs, uuidv7 } from './uuidv7'
|
|
6
|
+
import { window } from './utils'
|
|
7
|
+
|
|
8
|
+
import { isArray, isNumber, isUndefined, clampToRange, Logger } from '@posthog/core'
|
|
9
|
+
import { Leanbase } from './leanbase'
|
|
10
|
+
import { addEventListener } from './utils'
|
|
11
|
+
import { SimpleEventEmitter } from './utils/simple-event-emitter'
|
|
12
|
+
import { logger } from './leanbase-logger'
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS = 30 * 60 // 30 minutes
|
|
15
|
+
export const MAX_SESSION_IDLE_TIMEOUT_SECONDS = 10 * 60 * 60 // 10 hours
|
|
16
|
+
const MIN_SESSION_IDLE_TIMEOUT_SECONDS = 60 // 1 minute
|
|
17
|
+
const SESSION_LENGTH_LIMIT_MILLISECONDS = 24 * 3600 * 1000 // 24 hours
|
|
18
|
+
|
|
19
|
+
export class SessionIdManager {
|
|
20
|
+
private readonly _sessionIdGenerator: () => string
|
|
21
|
+
private readonly _windowIdGenerator: () => string
|
|
22
|
+
private _config: Partial<LeanbaseConfig>
|
|
23
|
+
private _persistence: LeanbasePersistence
|
|
24
|
+
private _windowId: string | null | undefined
|
|
25
|
+
private _sessionId: string | null | undefined
|
|
26
|
+
private readonly _window_id_storage_key: string
|
|
27
|
+
private readonly _primary_window_exists_storage_key: string
|
|
28
|
+
private _sessionStartTimestamp: number | null
|
|
29
|
+
|
|
30
|
+
private _sessionActivityTimestamp: number | null
|
|
31
|
+
private _sessionIdChangedHandlers: SessionIdChangedCallback[] = []
|
|
32
|
+
private readonly _sessionTimeoutMs: number
|
|
33
|
+
|
|
34
|
+
// we track activity so we can end the session proactively when it has passed the idle timeout
|
|
35
|
+
private _enforceIdleTimeout: ReturnType<typeof setTimeout> | undefined
|
|
36
|
+
|
|
37
|
+
private _beforeUnloadListener: (() => void) | undefined = undefined
|
|
38
|
+
|
|
39
|
+
private _eventEmitter: SimpleEventEmitter = new SimpleEventEmitter()
|
|
40
|
+
public on(event: 'forcedIdleReset', handler: () => void): () => void {
|
|
41
|
+
return this._eventEmitter.on(event, handler)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
constructor(instance: Leanbase, sessionIdGenerator?: () => string, windowIdGenerator?: () => string) {
|
|
45
|
+
if (!instance.persistence) {
|
|
46
|
+
throw new Error('SessionIdManager requires a LeanbasePersistence instance')
|
|
47
|
+
}
|
|
48
|
+
if (instance.config.cookieless_mode === 'always') {
|
|
49
|
+
throw new Error('SessionIdManager cannot be used with cookieless_mode="always"')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this._config = instance.config
|
|
53
|
+
this._persistence = instance.persistence
|
|
54
|
+
this._windowId = undefined
|
|
55
|
+
this._sessionId = undefined
|
|
56
|
+
this._sessionStartTimestamp = null
|
|
57
|
+
this._sessionActivityTimestamp = null
|
|
58
|
+
this._sessionIdGenerator = sessionIdGenerator || uuidv7
|
|
59
|
+
this._windowIdGenerator = windowIdGenerator || uuidv7
|
|
60
|
+
|
|
61
|
+
const persistenceName = this._config['persistence_name'] || this._config['token']
|
|
62
|
+
|
|
63
|
+
const desiredTimeout = this._config['session_idle_timeout_seconds'] || DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS
|
|
64
|
+
this._sessionTimeoutMs =
|
|
65
|
+
clampToRange(
|
|
66
|
+
desiredTimeout,
|
|
67
|
+
MIN_SESSION_IDLE_TIMEOUT_SECONDS,
|
|
68
|
+
MAX_SESSION_IDLE_TIMEOUT_SECONDS,
|
|
69
|
+
logger as Logger,
|
|
70
|
+
DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS
|
|
71
|
+
) * 1000
|
|
72
|
+
|
|
73
|
+
instance.register({ $configured_session_timeout_ms: this._sessionTimeoutMs })
|
|
74
|
+
this._resetIdleTimer()
|
|
75
|
+
|
|
76
|
+
this._window_id_storage_key = 'ph_' + persistenceName + '_window_id'
|
|
77
|
+
this._primary_window_exists_storage_key = 'ph_' + persistenceName + '_primary_window_exists'
|
|
78
|
+
|
|
79
|
+
// primary_window_exists is set when the DOM has been loaded and is cleared on unload
|
|
80
|
+
// if it exists here it means there was no unload which suggests this window is opened as a tab duplication, window.open, etc.
|
|
81
|
+
if (this._canUseSessionStorage()) {
|
|
82
|
+
const lastWindowId = sessionStore._parse(this._window_id_storage_key)
|
|
83
|
+
|
|
84
|
+
const primaryWindowExists = sessionStore._parse(this._primary_window_exists_storage_key)
|
|
85
|
+
if (lastWindowId && !primaryWindowExists) {
|
|
86
|
+
// Persist window from previous storage state
|
|
87
|
+
this._windowId = lastWindowId
|
|
88
|
+
} else {
|
|
89
|
+
// Wipe any reference to previous window id
|
|
90
|
+
sessionStore._remove(this._window_id_storage_key)
|
|
91
|
+
}
|
|
92
|
+
// Flag this session as having a primary window
|
|
93
|
+
sessionStore._set(this._primary_window_exists_storage_key, true)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (this._config.bootstrap?.sessionID) {
|
|
97
|
+
try {
|
|
98
|
+
const sessionStartTimestamp = uuid7ToTimestampMs(this._config.bootstrap.sessionID)
|
|
99
|
+
this._setSessionId(this._config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp)
|
|
100
|
+
} catch (e) {
|
|
101
|
+
logger.error('Invalid sessionID in bootstrap', e)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this._listenToReloadWindow()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get sessionTimeoutMs(): number {
|
|
109
|
+
return this._sessionTimeoutMs
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onSessionId(callback: SessionIdChangedCallback): () => void {
|
|
113
|
+
// KLUDGE: when running in tests the handlers array was always undefined
|
|
114
|
+
// it's yucky but safe to set it here so that it's always definitely available
|
|
115
|
+
if (isUndefined(this._sessionIdChangedHandlers)) {
|
|
116
|
+
this._sessionIdChangedHandlers = []
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this._sessionIdChangedHandlers.push(callback)
|
|
120
|
+
if (this._sessionId) {
|
|
121
|
+
callback(this._sessionId, this._windowId)
|
|
122
|
+
}
|
|
123
|
+
return () => {
|
|
124
|
+
this._sessionIdChangedHandlers = this._sessionIdChangedHandlers.filter((h) => h !== callback)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private _canUseSessionStorage(): boolean {
|
|
129
|
+
// We only want to use sessionStorage if persistence is enabled and not memory storage
|
|
130
|
+
return this._config.persistence !== 'memory' && !this._persistence._disabled && sessionStore._is_supported()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Note: this tries to store the windowId in sessionStorage. SessionStorage is unique to the current window/tab,
|
|
134
|
+
// and persists page loads/reloads. So it's uniquely suited for storing the windowId. This function also respects
|
|
135
|
+
// when persistence is disabled (by user config) and when sessionStorage is not supported (it *should* be supported on all browsers),
|
|
136
|
+
// and in that case, it falls back to memory (which sadly, won't persist page loads)
|
|
137
|
+
private _setWindowId(windowId: string): void {
|
|
138
|
+
if (windowId !== this._windowId) {
|
|
139
|
+
this._windowId = windowId
|
|
140
|
+
if (this._canUseSessionStorage()) {
|
|
141
|
+
sessionStore._set(this._window_id_storage_key, windowId)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _getWindowId(): string | null {
|
|
147
|
+
if (this._windowId) {
|
|
148
|
+
return this._windowId
|
|
149
|
+
}
|
|
150
|
+
if (this._canUseSessionStorage()) {
|
|
151
|
+
return sessionStore._parse(this._window_id_storage_key)
|
|
152
|
+
}
|
|
153
|
+
// New window id will be generated
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Note: 'this.persistence.register' can be disabled in the config.
|
|
158
|
+
// In that case, this works by storing sessionId and the timestamp in memory.
|
|
159
|
+
private _setSessionId(
|
|
160
|
+
sessionId: string | null,
|
|
161
|
+
sessionActivityTimestamp: number | null,
|
|
162
|
+
sessionStartTimestamp: number | null
|
|
163
|
+
): void {
|
|
164
|
+
if (
|
|
165
|
+
sessionId !== this._sessionId ||
|
|
166
|
+
sessionActivityTimestamp !== this._sessionActivityTimestamp ||
|
|
167
|
+
sessionStartTimestamp !== this._sessionStartTimestamp
|
|
168
|
+
) {
|
|
169
|
+
this._sessionStartTimestamp = sessionStartTimestamp
|
|
170
|
+
this._sessionActivityTimestamp = sessionActivityTimestamp
|
|
171
|
+
this._sessionId = sessionId
|
|
172
|
+
|
|
173
|
+
this._persistence.register({
|
|
174
|
+
[SESSION_ID]: [sessionActivityTimestamp, sessionId, sessionStartTimestamp],
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private _getSessionId(): [number, string, number] {
|
|
180
|
+
if (this._sessionId && this._sessionActivityTimestamp && this._sessionStartTimestamp) {
|
|
181
|
+
return [this._sessionActivityTimestamp, this._sessionId, this._sessionStartTimestamp]
|
|
182
|
+
}
|
|
183
|
+
const sessionIdInfo = this._persistence.props[SESSION_ID]
|
|
184
|
+
|
|
185
|
+
if (isArray(sessionIdInfo) && sessionIdInfo.length === 2) {
|
|
186
|
+
// Storage does not yet have a session start time. Add the last activity timestamp as the start time
|
|
187
|
+
sessionIdInfo.push(sessionIdInfo[0])
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return sessionIdInfo || [0, null, 0]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Resets the session id by setting it to null. On the subsequent call to checkAndGetSessionAndWindowId,
|
|
194
|
+
// new ids will be generated.
|
|
195
|
+
resetSessionId(): void {
|
|
196
|
+
this._setSessionId(null, null, null)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Cleans up resources used by SessionIdManager.
|
|
201
|
+
* Should be called when the SessionIdManager is no longer needed to prevent memory leaks.
|
|
202
|
+
*/
|
|
203
|
+
destroy(): void {
|
|
204
|
+
// Clear the idle timeout timer
|
|
205
|
+
clearTimeout(this._enforceIdleTimeout)
|
|
206
|
+
this._enforceIdleTimeout = undefined
|
|
207
|
+
|
|
208
|
+
// Remove the beforeunload event listener
|
|
209
|
+
if (this._beforeUnloadListener && window) {
|
|
210
|
+
window.removeEventListener('beforeunload', this._beforeUnloadListener, { capture: false } as any)
|
|
211
|
+
this._beforeUnloadListener = undefined
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clear session id changed handlers
|
|
215
|
+
this._sessionIdChangedHandlers = []
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* Listens to window unloads and removes the primaryWindowExists key from sessionStorage.
|
|
220
|
+
* Reloaded or fresh tabs created after a DOM unloads (reloading the same tab) WILL NOT have this primaryWindowExists flag in session storage.
|
|
221
|
+
* Cloned sessions (new tab, tab duplication, window.open(), ...) WILL have this primaryWindowExists flag in their copied session storage.
|
|
222
|
+
* We conditionally check the primaryWindowExists value in the constructor to decide if the window id in the last session storage should be carried over.
|
|
223
|
+
*/
|
|
224
|
+
private _listenToReloadWindow(): void {
|
|
225
|
+
this._beforeUnloadListener = () => {
|
|
226
|
+
if (this._canUseSessionStorage()) {
|
|
227
|
+
sessionStore._remove(this._primary_window_exists_storage_key)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
addEventListener(window, 'beforeunload', this._beforeUnloadListener, { capture: false })
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private _sessionHasBeenIdleTooLong = (timestamp: number, lastActivityTimestamp: number) => {
|
|
234
|
+
return Math.abs(timestamp - lastActivityTimestamp) > this.sessionTimeoutMs
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/*
|
|
238
|
+
* This function returns the current sessionId and windowId. It should be used to
|
|
239
|
+
* access these values over directly calling `._sessionId` or `._windowId`.
|
|
240
|
+
* In addition to returning the sessionId and windowId, this function also manages cycling the
|
|
241
|
+
* sessionId and windowId when appropriate by doing the following:
|
|
242
|
+
*
|
|
243
|
+
* 1. If the sessionId or windowId is not set, it will generate a new one and store it.
|
|
244
|
+
* 2. If the readOnly param is set to false, it will:
|
|
245
|
+
* a. Check if it has been > SESSION_CHANGE_THRESHOLD since the last call with this flag set.
|
|
246
|
+
* If so, it will generate a new sessionId and store it.
|
|
247
|
+
* b. Update the timestamp stored with the sessionId to ensure the current session is extended
|
|
248
|
+
* for the appropriate amount of time.
|
|
249
|
+
*
|
|
250
|
+
* @param {boolean} readOnly (optional) Defaults to False. Should be set to True when the call to the function should not extend or cycle the session (e.g. being called for non-user generated events)
|
|
251
|
+
* @param {Number} timestamp (optional) Defaults to the current time. The timestamp to be stored with the sessionId (used when determining if a new sessionId should be generated)
|
|
252
|
+
*/
|
|
253
|
+
checkAndGetSessionAndWindowId(readOnly = false, _timestamp: number | null = null) {
|
|
254
|
+
if (this._config.cookieless_mode === 'always') {
|
|
255
|
+
throw new Error('checkAndGetSessionAndWindowId should not be called with cookieless_mode="always"')
|
|
256
|
+
}
|
|
257
|
+
const timestamp = _timestamp || new Date().getTime()
|
|
258
|
+
|
|
259
|
+
// eslint-disable-next-line prefer-const
|
|
260
|
+
let [lastActivityTimestamp, sessionId, startTimestamp] = this._getSessionId()
|
|
261
|
+
let windowId = this._getWindowId()
|
|
262
|
+
|
|
263
|
+
const sessionPastMaximumLength =
|
|
264
|
+
isNumber(startTimestamp) &&
|
|
265
|
+
startTimestamp > 0 &&
|
|
266
|
+
Math.abs(timestamp - startTimestamp) > SESSION_LENGTH_LIMIT_MILLISECONDS
|
|
267
|
+
|
|
268
|
+
let valuesChanged = false
|
|
269
|
+
const noSessionId = !sessionId
|
|
270
|
+
const activityTimeout = !readOnly && this._sessionHasBeenIdleTooLong(timestamp, lastActivityTimestamp)
|
|
271
|
+
if (noSessionId || activityTimeout || sessionPastMaximumLength) {
|
|
272
|
+
sessionId = this._sessionIdGenerator()
|
|
273
|
+
windowId = this._windowIdGenerator()
|
|
274
|
+
logger.info('new session ID generated', {
|
|
275
|
+
sessionId,
|
|
276
|
+
windowId,
|
|
277
|
+
changeReason: { noSessionId, activityTimeout, sessionPastMaximumLength },
|
|
278
|
+
})
|
|
279
|
+
startTimestamp = timestamp
|
|
280
|
+
valuesChanged = true
|
|
281
|
+
} else if (!windowId) {
|
|
282
|
+
windowId = this._windowIdGenerator()
|
|
283
|
+
valuesChanged = true
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const newActivityTimestamp =
|
|
287
|
+
lastActivityTimestamp === 0 || !readOnly || sessionPastMaximumLength ? timestamp : lastActivityTimestamp
|
|
288
|
+
const sessionStartTimestamp = startTimestamp === 0 ? new Date().getTime() : startTimestamp
|
|
289
|
+
|
|
290
|
+
this._setWindowId(windowId)
|
|
291
|
+
this._setSessionId(sessionId, newActivityTimestamp, sessionStartTimestamp)
|
|
292
|
+
|
|
293
|
+
if (!readOnly) {
|
|
294
|
+
this._resetIdleTimer()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (valuesChanged) {
|
|
298
|
+
this._sessionIdChangedHandlers.forEach((handler) =>
|
|
299
|
+
handler(
|
|
300
|
+
sessionId,
|
|
301
|
+
windowId,
|
|
302
|
+
valuesChanged ? { noSessionId, activityTimeout, sessionPastMaximumLength } : undefined
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
sessionId,
|
|
309
|
+
windowId,
|
|
310
|
+
sessionStartTimestamp,
|
|
311
|
+
changeReason: valuesChanged ? { noSessionId, activityTimeout, sessionPastMaximumLength } : undefined,
|
|
312
|
+
lastActivityTimestamp: lastActivityTimestamp,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private _resetIdleTimer() {
|
|
317
|
+
clearTimeout(this._enforceIdleTimeout)
|
|
318
|
+
this._enforceIdleTimeout = setTimeout(() => {
|
|
319
|
+
// enforce idle timeout a little after the session timeout to ensure the session is reset even without activity
|
|
320
|
+
// we need to check session activity first in case a different window has kept the session active
|
|
321
|
+
// while this window has been idle - and the timer has not progressed - e.g. window memory frozen while hidden
|
|
322
|
+
const [lastActivityTimestamp] = this._getSessionId()
|
|
323
|
+
if (this._sessionHasBeenIdleTooLong(new Date().getTime(), lastActivityTimestamp)) {
|
|
324
|
+
const idleSessionId = this._sessionId
|
|
325
|
+
this.resetSessionId()
|
|
326
|
+
this._eventEmitter.emit('forcedIdleReset', { idleSessionId })
|
|
327
|
+
}
|
|
328
|
+
}, this.sessionTimeoutMs * 1.1)
|
|
329
|
+
}
|
|
330
|
+
}
|