@leanbase.com/js 0.1.1 → 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/leanbase.ts CHANGED
@@ -1,72 +1,195 @@
1
- import { PostHogCore, getFetch } from '@posthog/core'
1
+ import {
2
+ PostHogCore,
3
+ getFetch,
4
+ isEmptyObject,
5
+ isEmptyString,
6
+ isNumber,
7
+ isObject,
8
+ isString,
9
+ isUndefined,
10
+ } from '@posthog/core'
2
11
  import type {
3
- PostHogCoreOptions,
4
12
  PostHogEventProperties,
5
13
  PostHogFetchOptions,
6
14
  PostHogFetchResponse,
7
15
  PostHogPersistedProperty,
8
- PostHogCaptureOptions,
9
16
  } from '@posthog/core'
10
- import { version } from './version'
17
+ import {
18
+ LeanbaseConfig,
19
+ LeanbasegCaptureOptions as LeanbaseCaptureOptions,
20
+ RemoteConfig,
21
+ CaptureResult,
22
+ Properties,
23
+ } from './types'
24
+ import { LeanbasePersistence } from './leanbase-persistence'
25
+ import {
26
+ addEventListener,
27
+ copyAndTruncateStrings,
28
+ document,
29
+ extend,
30
+ isCrossDomainCookie,
31
+ navigator,
32
+ userAgent,
33
+ } from './utils'
34
+ import Config from './config'
35
+ import { Autocapture } from './autocapture'
11
36
  import { logger } from './leanbase-logger'
12
- import { isNull } from '@posthog/core'
13
-
14
- export interface LeanbaseOptions extends Partial<PostHogCoreOptions> {
15
- /**
16
- * Enable autocapture of clicks and form interactions
17
- * @default true
18
- */
19
- autocapture?: boolean
37
+ import { COOKIELESS_MODE_FLAG_PROPERTY, USER_STATE } from './constants'
38
+ import { getEventProperties } from './utils/event-utils'
39
+ import { SessionIdManager } from './sessionid'
40
+ import { SessionPropsManager } from './session-props'
41
+ import { uuidv7 } from './uuidv7'
42
+ import { PageViewManager } from './page-view'
43
+ import { ScrollManager } from './scroll-manager'
44
+ import { isLikelyBot } from './utils/blocked-uas'
20
45
 
21
- /**
22
- * API host for Leanbase
23
- * @default 'https://i.leanbase.co'
24
- */
25
- host?: string
26
- }
46
+ const defaultConfig = (): LeanbaseConfig => ({
47
+ host: 'https://i.leanbase.co',
48
+ token: '',
49
+ autocapture: true,
50
+ rageclick: true,
51
+ persistence: 'localStorage+cookie',
52
+ capture_pageview: 'history_change',
53
+ capture_pageleave: 'if_capture_pageview',
54
+ persistence_name: '',
55
+ mask_all_element_attributes: false,
56
+ cookie_expiration: 365,
57
+ cross_subdomain_cookie: isCrossDomainCookie(document?.location),
58
+ custom_campaign_params: [],
59
+ custom_personal_data_properties: [],
60
+ disable_persistence: false,
61
+ mask_personal_data_properties: false,
62
+ secure_cookie: window?.location?.protocol === 'https:',
63
+ mask_all_text: false,
64
+ bootstrap: {},
65
+ session_idle_timeout_seconds: 30 * 60,
66
+ save_campaign_params: true,
67
+ save_referrer: true,
68
+ opt_out_useragent_filter: false,
69
+ properties_string_max_length: 65535,
70
+ loaded: () => {},
71
+ })
27
72
 
28
73
  export class Leanbase extends PostHogCore {
29
- private _storage: Map<string, any> = new Map()
30
- private _storageKey: string
74
+ config: LeanbaseConfig
75
+ scrollManager: ScrollManager
76
+ pageViewManager: PageViewManager
31
77
 
32
- constructor(apiKey: string, options?: LeanbaseOptions) {
33
- // Leanbase defaults
34
- const leanbaseOptions: PostHogCoreOptions = {
35
- host: 'https://i.leanbase.co',
36
- ...options,
37
- }
78
+ replayAutocapture?: Autocapture
79
+ persistence?: LeanbasePersistence
80
+ sessionPersistence?: LeanbasePersistence
81
+ sessionManager?: SessionIdManager
82
+ sessionPropsManager?: SessionPropsManager
83
+ isRemoteConfigLoaded?: boolean
84
+ personProcessingSetOncePropertiesSent = false
85
+ isLoaded: boolean = false
86
+ initialPageviewCaptured: boolean
87
+ visibilityStateListener: (() => void) | null
88
+
89
+ constructor(token: string, config?: Partial<LeanbaseConfig>) {
90
+ const mergedConfig = extend(defaultConfig(), config || {}, {
91
+ token,
92
+ })
93
+ super(token, mergedConfig)
94
+ this.config = mergedConfig as LeanbaseConfig
95
+ this.visibilityStateListener = null
96
+ this.initialPageviewCaptured = false
97
+ this.scrollManager = new ScrollManager(this)
98
+ this.pageViewManager = new PageViewManager(this)
99
+ this.init(token, mergedConfig)
100
+ }
38
101
 
39
- super(apiKey, leanbaseOptions)
102
+ init(token: string, config: Partial<LeanbaseConfig>) {
103
+ this.setConfig(
104
+ extend(defaultConfig(), config, {
105
+ token,
106
+ })
107
+ )
108
+ this.isLoaded = true
109
+ this.persistence = new LeanbasePersistence(this.config)
110
+ this.replayAutocapture = new Autocapture(this)
111
+ this.replayAutocapture.startIfEnabled()
40
112
 
41
- this._storageKey = `leanbase_${apiKey}`
113
+ if (this.config.preloadFeatureFlags !== false) {
114
+ this.reloadFeatureFlags()
115
+ }
42
116
 
43
- // Load from localStorage if available
44
- if (typeof window !== 'undefined' && window.localStorage) {
45
- try {
46
- const stored = window.localStorage.getItem(this._storageKey)
47
- if (stored) {
48
- const parsed = JSON.parse(stored)
49
- Object.entries(parsed).forEach(([key, value]) => {
50
- this._storage.set(key, value)
51
- })
117
+ this.config.loaded?.(this)
118
+ if (this.config.capture_pageview) {
119
+ setTimeout(() => {
120
+ if (this.config.cookieless_mode === 'always') {
121
+ this.captureInitialPageview()
52
122
  }
53
- } catch (err) {
54
- logger.warn('Failed to load persisted data', err)
123
+ }, 1)
124
+ }
125
+
126
+ addEventListener(document, 'DOMContentLoaded', () => {
127
+ this.loadRemoteConfig()
128
+ })
129
+ addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), {
130
+ passive: false,
131
+ })
132
+ }
133
+
134
+ captureInitialPageview(): void {
135
+ if (!document) {
136
+ return
137
+ }
138
+
139
+ if (document.visibilityState !== 'visible') {
140
+ if (!this.visibilityStateListener) {
141
+ this.visibilityStateListener = this.captureInitialPageview.bind(this)
142
+ addEventListener(document, 'visibilitychange', this.visibilityStateListener)
55
143
  }
144
+
145
+ return
56
146
  }
57
147
 
58
- logger.info('Leanbase initialized', { apiKey, host: leanbaseOptions.host })
148
+ if (!this.initialPageviewCaptured) {
149
+ this.initialPageviewCaptured = true
150
+ this.capture('$pageview', { title: document.title })
59
151
 
60
- // Preload feature flags if not explicitly disabled
61
- if (options?.preloadFeatureFlags !== false) {
62
- this.reloadFeatureFlags()
152
+ if (this.visibilityStateListener) {
153
+ document.removeEventListener('visibilitychange', this.visibilityStateListener)
154
+ this.visibilityStateListener = null
155
+ }
156
+ }
157
+ }
158
+
159
+ capturePageLeave() {
160
+ const { capture_pageleave, capture_pageview } = this.config
161
+ if (
162
+ capture_pageleave === true ||
163
+ (capture_pageleave === 'if_capture_pageview' &&
164
+ (capture_pageview === true || capture_pageview === 'history_change'))
165
+ ) {
166
+ this.capture('$pageleave')
63
167
  }
64
168
  }
65
169
 
66
- // PostHogCore abstract methods
170
+ async loadRemoteConfig() {
171
+ if (!this.isRemoteConfigLoaded) {
172
+ const remoteConfig = await this.reloadRemoteConfigAsync()
173
+ if (remoteConfig) {
174
+ this.onRemoteConfig(remoteConfig as RemoteConfig)
175
+ }
176
+ }
177
+ }
178
+
179
+ onRemoteConfig(config: RemoteConfig): void {
180
+ if (!(document && document.body)) {
181
+ setTimeout(() => {
182
+ this.onRemoteConfig(config)
183
+ }, 500)
184
+ return
185
+ }
186
+
187
+ this.isRemoteConfigLoaded = true
188
+ this.replayAutocapture?.onRemoteConfig(config)
189
+ }
190
+
67
191
  fetch(url: string, options: PostHogFetchOptions): Promise<PostHogFetchResponse> {
68
192
  const fetchFn = getFetch()
69
-
70
193
  if (!fetchFn) {
71
194
  return Promise.reject(new Error('Fetch API is not available in this environment.'))
72
195
  }
@@ -74,12 +197,26 @@ export class Leanbase extends PostHogCore {
74
197
  return fetchFn(url, options)
75
198
  }
76
199
 
200
+ setConfig(config: Partial<LeanbaseConfig>): void {
201
+ const oldConfig = { ...this.config }
202
+ if (isObject(config)) {
203
+ extend(this.config, config)
204
+ this.persistence?.update_config(this.config, oldConfig)
205
+ this.replayAutocapture?.startIfEnabled()
206
+ }
207
+
208
+ const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory'
209
+ this.sessionPersistence = isTempStorage
210
+ ? this.persistence
211
+ : new LeanbasePersistence({ ...this.config, persistence: 'sessionStorage' })
212
+ }
213
+
77
214
  getLibraryId(): string {
78
215
  return 'leanbase'
79
216
  }
80
217
 
81
218
  getLibraryVersion(): string {
82
- return version
219
+ return Config.LIB_VERSION
83
220
  }
84
221
 
85
222
  getCustomUserAgent(): void {
@@ -87,43 +224,201 @@ export class Leanbase extends PostHogCore {
87
224
  }
88
225
 
89
226
  getPersistedProperty<T>(key: PostHogPersistedProperty): T | undefined {
90
- return this._storage.get(key)
227
+ return this.persistence?.get_property(key)
91
228
  }
92
229
 
93
230
  setPersistedProperty<T>(key: PostHogPersistedProperty, value: T | null): void {
94
- if (isNull(value)) {
95
- this._storage.delete(key)
96
- } else {
97
- this._storage.set(key, value)
98
- }
99
-
100
- // Persist to localStorage if available
101
- if (typeof window !== 'undefined' && window.localStorage) {
102
- try {
103
- const obj: Record<string, any> = {}
104
- this._storage.forEach((v, k) => {
105
- obj[k] = v
106
- })
107
- window.localStorage.setItem(this._storageKey, JSON.stringify(obj))
108
- } catch (err) {
109
- logger.warn('Failed to persist data', err)
231
+ this.persistence?.set_property(key, value)
232
+ }
233
+
234
+ calculateEventProperties(
235
+ eventName: string,
236
+ eventProperties: PostHogEventProperties,
237
+ timestamp: Date,
238
+ uuid: string,
239
+ readOnly?: boolean
240
+ ): Properties {
241
+ if (!this.persistence || !this.sessionPersistence) {
242
+ return eventProperties
243
+ }
244
+
245
+ timestamp = timestamp || new Date()
246
+ const startTimestamp = readOnly ? undefined : this.persistence?.remove_event_timer(eventName)
247
+ let properties = { ...eventProperties }
248
+ properties['token'] = this.config.token
249
+ if (this.config.cookieless_mode == 'always' || this.config.cookieless_mode == 'on_reject') {
250
+ properties[COOKIELESS_MODE_FLAG_PROPERTY] = true
251
+ }
252
+
253
+ if (eventName === '$snapshot') {
254
+ const persistenceProps = { ...this.persistence.properties() }
255
+ properties['distinct_id'] = persistenceProps.distinct_id
256
+ if (
257
+ !(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) ||
258
+ isEmptyString(properties['distinct_id'])
259
+ ) {
260
+ logger.error('Invalid distinct_id for replay event. This indicates a bug in your implementation')
110
261
  }
262
+ return properties
263
+ }
264
+
265
+ const infoProperties = getEventProperties(
266
+ this.config.mask_personal_data_properties,
267
+ this.config.custom_personal_data_properties
268
+ )
269
+
270
+ if (this.sessionManager) {
271
+ const { sessionId, windowId } = this.sessionManager.checkAndGetSessionAndWindowId(
272
+ readOnly,
273
+ timestamp.getTime()
274
+ )
275
+ properties['$session_id'] = sessionId
276
+ properties['$window_id'] = windowId
277
+ }
278
+
279
+ if (this.sessionPropsManager) {
280
+ extend(properties, this.sessionPropsManager.getSessionProps())
111
281
  }
282
+
283
+ let pageviewProperties: Record<string, any> = this.pageViewManager.doEvent()
284
+ if (eventName === '$pageview' && !readOnly) {
285
+ pageviewProperties = this.pageViewManager.doPageView(timestamp, uuid)
286
+ }
287
+
288
+ if (eventName === '$pageleave' && !readOnly) {
289
+ pageviewProperties = this.pageViewManager.doPageLeave(timestamp)
290
+ }
291
+
292
+ properties = extend(properties, pageviewProperties)
293
+
294
+ if (eventName === '$pageview' && document) {
295
+ properties['title'] = document.title
296
+ }
297
+
298
+ if (!isUndefined(startTimestamp)) {
299
+ const duration_in_ms = timestamp.getTime() - startTimestamp
300
+ properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3))
301
+ }
302
+
303
+ if (userAgent && this.config.opt_out_useragent_filter) {
304
+ properties['$browser_type'] = isLikelyBot(navigator, []) ? 'bot' : 'browser'
305
+ }
306
+
307
+ properties = extend(
308
+ {},
309
+ infoProperties,
310
+ this.persistence.properties(),
311
+ this.sessionPersistence.properties(),
312
+ properties
313
+ )
314
+
315
+ properties['$is_identified'] = this.isIdentified()
316
+ return properties
317
+ }
318
+
319
+ isIdentified(): boolean {
320
+ return (
321
+ this.persistence?.get_property(USER_STATE) === 'identified' ||
322
+ this.sessionPersistence?.get_property(USER_STATE) === 'identified'
323
+ )
324
+ }
325
+
326
+ /**
327
+ * Add additional set_once properties to the event when creating a person profile. This allows us to create the
328
+ * profile with mostly-accurate properties, despite earlier events not setting them. We do this by storing them in
329
+ * persistence.
330
+ * @param dataSetOnce
331
+ */
332
+ calculateSetOnceProperties(dataSetOnce?: Properties): Properties | undefined {
333
+ if (!this.persistence) {
334
+ return dataSetOnce
335
+ }
336
+
337
+ if (this.personProcessingSetOncePropertiesSent) {
338
+ return dataSetOnce
339
+ }
340
+
341
+ const initialProps = this.persistence.get_initial_props()
342
+ const sessionProps = this.sessionPropsManager?.getSetOnceProps()
343
+ const setOnceProperties = extend({}, initialProps, sessionProps || {}, dataSetOnce || {})
344
+ this.personProcessingSetOncePropertiesSent = true
345
+ if (isEmptyObject(setOnceProperties)) {
346
+ return undefined
347
+ }
348
+
349
+ return setOnceProperties
112
350
  }
113
351
 
114
- // Public API: leanbase.capture()
115
- capture(event: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): void {
116
- super.capture(event, properties, options)
352
+ capture(event: string, properties?: PostHogEventProperties, options?: LeanbaseCaptureOptions): void {
353
+ if (!this.isLoaded || !this.sessionPersistence || !this.persistence) {
354
+ return
355
+ }
356
+
357
+ if (isUndefined(event) || !isString(event)) {
358
+ logger.error('No event name provided to posthog.capture')
359
+ return
360
+ }
361
+
362
+ if (properties?.$current_url && !isString(properties?.$current_url)) {
363
+ logger.error(
364
+ 'Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.'
365
+ )
366
+ delete properties?.$current_url
367
+ }
368
+
369
+ this.sessionPersistence.update_search_keyword()
370
+
371
+ if (this.config.save_campaign_params) {
372
+ this.sessionPersistence.update_campaign_params()
373
+ }
374
+
375
+ if (this.config.save_referrer) {
376
+ this.sessionPersistence.update_referrer_info()
377
+ }
378
+
379
+ if (this.config.save_campaign_params || this.config.save_referrer) {
380
+ this.persistence.set_initial_person_info()
381
+ }
382
+
383
+ const systemTime = new Date()
384
+ const timestamp = options?.timestamp || systemTime
385
+ const uuid = uuidv7()
386
+ let data: CaptureResult = {
387
+ uuid,
388
+ event,
389
+ properties: this.calculateEventProperties(event, properties || {}, timestamp, uuid),
390
+ }
391
+
392
+ const setProperties = options?.$set
393
+ if (setProperties) {
394
+ data.$set = options?.$set
395
+ }
396
+
397
+ const setOnceProperties = this.calculateSetOnceProperties(options?.$set_once)
398
+ if (setOnceProperties) {
399
+ data.$set_once = setOnceProperties
400
+ }
401
+
402
+ data = copyAndTruncateStrings(data, options?._noTruncate ? null : this.config.properties_string_max_length)
403
+ data.timestamp = timestamp
404
+ if (!isUndefined(options?.timestamp)) {
405
+ data.properties['$event_time_override_provided'] = true
406
+ data.properties['$event_time_override_system_time'] = systemTime
407
+ }
408
+
409
+ const finalSet = { ...data.properties['$set'], ...data['$set'] }
410
+ if (!isEmptyObject(finalSet)) {
411
+ this.setPersonPropertiesForFlags(finalSet)
412
+ }
413
+
414
+ super.capture(data.event, data.properties, options)
117
415
  }
118
416
 
119
- // Public API: leanbase.identify()
120
- identify(distinctId?: string, properties?: PostHogEventProperties, options?: PostHogCaptureOptions): void {
417
+ identify(distinctId?: string, properties?: PostHogEventProperties, options?: LeanbaseCaptureOptions): void {
121
418
  super.identify(distinctId, properties, options)
122
419
  }
123
420
 
124
- // Cleanup
125
421
  destroy(): void {
126
- // Future: cleanup autocapture and session recording
127
- this._storage.clear()
422
+ this.persistence?.clear()
128
423
  }
129
424
  }
@@ -0,0 +1,124 @@
1
+ import { window } from './utils'
2
+ import { Leanbase } from './leanbase'
3
+ import { clampToRange, isUndefined, Logger } from '@posthog/core'
4
+ import { extend } from './utils'
5
+ import { logger } from './leanbase-logger'
6
+
7
+ interface PageViewEventProperties {
8
+ $pageview_id?: string
9
+ $prev_pageview_id?: string
10
+ $prev_pageview_pathname?: string
11
+ $prev_pageview_duration?: number // seconds
12
+ $prev_pageview_last_scroll?: number
13
+ $prev_pageview_last_scroll_percentage?: number
14
+ $prev_pageview_max_scroll?: number
15
+ $prev_pageview_max_scroll_percentage?: number
16
+ $prev_pageview_last_content?: number
17
+ $prev_pageview_last_content_percentage?: number
18
+ $prev_pageview_max_content?: number
19
+ $prev_pageview_max_content_percentage?: number
20
+ }
21
+
22
+ // This keeps track of the PageView state (such as the previous PageView's path, timestamp, id, and scroll properties).
23
+ // We store the state in memory, which means that for non-SPA sites, the state will be lost on page reload. This means
24
+ // that non-SPA sites should always send a $pageleave event on any navigation, before the page unloads. For SPA sites,
25
+ // they only need to send a $pageleave event when the user navigates away from the site, as the information is not lost
26
+ // on an internal navigation, and is included as the $prev_pageview_ properties in the next $pageview event.
27
+
28
+ // Practically, this means that to find the scroll properties for a given pageview, you need to find the event where
29
+ // event name is $pageview or $pageleave and where $prev_pageview_id matches the original pageview event's id.
30
+
31
+ export class PageViewManager {
32
+ _currentPageview?: { timestamp: Date; pageViewId: string | undefined; pathname: string | undefined }
33
+ _instance: Leanbase
34
+
35
+ constructor(instance: Leanbase) {
36
+ this._instance = instance
37
+ }
38
+
39
+ doPageView(timestamp: Date, pageViewId?: string): PageViewEventProperties {
40
+ const response = this._previousPageViewProperties(timestamp, pageViewId)
41
+
42
+ // On a pageview we reset the contexts
43
+ this._currentPageview = { pathname: window?.location.pathname ?? '', pageViewId, timestamp }
44
+ this._instance.scrollManager.resetContext()
45
+
46
+ return response
47
+ }
48
+
49
+ doPageLeave(timestamp: Date): PageViewEventProperties {
50
+ return this._previousPageViewProperties(timestamp, this._currentPageview?.pageViewId)
51
+ }
52
+
53
+ doEvent(): PageViewEventProperties {
54
+ return { $pageview_id: this._currentPageview?.pageViewId }
55
+ }
56
+
57
+ private _previousPageViewProperties(timestamp: Date, pageviewId: string | undefined): PageViewEventProperties {
58
+ const previousPageView = this._currentPageview
59
+
60
+ if (!previousPageView) {
61
+ return { $pageview_id: pageviewId }
62
+ }
63
+
64
+ let properties: PageViewEventProperties = {
65
+ $pageview_id: pageviewId,
66
+ $prev_pageview_id: previousPageView.pageViewId,
67
+ }
68
+
69
+ const scrollContext = this._instance.scrollManager.getContext()
70
+
71
+ if (scrollContext && !this._instance.config.disable_scroll_properties) {
72
+ let { maxScrollHeight, lastScrollY, maxScrollY, maxContentHeight, lastContentY, maxContentY } =
73
+ scrollContext
74
+
75
+ if (
76
+ !isUndefined(maxScrollHeight) &&
77
+ !isUndefined(lastScrollY) &&
78
+ !isUndefined(maxScrollY) &&
79
+ !isUndefined(maxContentHeight) &&
80
+ !isUndefined(lastContentY) &&
81
+ !isUndefined(maxContentY)
82
+ ) {
83
+ // Use ceil, so that e.g. scrolling 999.5px of a 1000px page is considered 100% scrolled
84
+ maxScrollHeight = Math.ceil(maxScrollHeight)
85
+ lastScrollY = Math.ceil(lastScrollY)
86
+ maxScrollY = Math.ceil(maxScrollY)
87
+ maxContentHeight = Math.ceil(maxContentHeight)
88
+ lastContentY = Math.ceil(lastContentY)
89
+ maxContentY = Math.ceil(maxContentY)
90
+
91
+ // if the maximum scroll height is near 0, then the percentage is 1
92
+ const lastScrollPercentage =
93
+ maxScrollHeight <= 1 ? 1 : clampToRange(lastScrollY / maxScrollHeight, 0, 1, logger as Logger)
94
+ const maxScrollPercentage =
95
+ maxScrollHeight <= 1 ? 1 : clampToRange(maxScrollY / maxScrollHeight, 0, 1, logger as Logger)
96
+ const lastContentPercentage =
97
+ maxContentHeight <= 1 ? 1 : clampToRange(lastContentY / maxContentHeight, 0, 1, logger as Logger)
98
+ const maxContentPercentage =
99
+ maxContentHeight <= 1 ? 1 : clampToRange(maxContentY / maxContentHeight, 0, 1, logger as Logger)
100
+
101
+ properties = extend(properties, {
102
+ $prev_pageview_last_scroll: lastScrollY,
103
+ $prev_pageview_last_scroll_percentage: lastScrollPercentage,
104
+ $prev_pageview_max_scroll: maxScrollY,
105
+ $prev_pageview_max_scroll_percentage: maxScrollPercentage,
106
+ $prev_pageview_last_content: lastContentY,
107
+ $prev_pageview_last_content_percentage: lastContentPercentage,
108
+ $prev_pageview_max_content: maxContentY,
109
+ $prev_pageview_max_content_percentage: maxContentPercentage,
110
+ })
111
+ }
112
+ }
113
+
114
+ if (previousPageView.pathname) {
115
+ properties.$prev_pageview_pathname = previousPageView.pathname
116
+ }
117
+ if (previousPageView.timestamp) {
118
+ // Use seconds, for consistency with our other duration-related properties like $duration
119
+ properties.$prev_pageview_duration = (timestamp.getTime() - previousPageView.timestamp.getTime()) / 1000
120
+ }
121
+
122
+ return properties
123
+ }
124
+ }