import { PostHogCore, getFetch, isEmptyObject, isEmptyString, isNumber, isObject, isString, isUndefined, } from '@posthog/core' import type { PostHogEventProperties, PostHogFetchOptions, PostHogFetchResponse, PostHogPersistedProperty, } from '@posthog/core' import { LeanbaseConfig, LeanbasegCaptureOptions as LeanbaseCaptureOptions, RemoteConfig, CaptureResult, Properties, } from './types' import { LeanbasePersistence } from './leanbase-persistence' import { addEventListener, copyAndTruncateStrings, document, extend, isCrossDomainCookie, navigator, userAgent, } from './utils' import Config from './config' import { Autocapture } from './autocapture' import { logger } from './leanbase-logger' import { COOKIELESS_MODE_FLAG_PROPERTY, USER_STATE } from './constants' import { getEventProperties } from './utils/event-utils' import { SessionIdManager } from './sessionid' import { SessionPropsManager } from './session-props' import { uuidv7 } from './uuidv7' import { PageViewManager } from './page-view' import { ScrollManager } from './scroll-manager' import { isLikelyBot } from './utils/blocked-uas' const defaultConfig = (): LeanbaseConfig => ({ host: 'https://i.leanbase.co', token: '', autocapture: true, rageclick: true, persistence: 'localStorage+cookie', capture_pageview: 'history_change', capture_pageleave: 'if_capture_pageview', persistence_name: '', mask_all_element_attributes: false, cookie_expiration: 365, cross_subdomain_cookie: isCrossDomainCookie(document?.location), custom_campaign_params: [], custom_personal_data_properties: [], disable_persistence: false, mask_personal_data_properties: false, secure_cookie: window?.location?.protocol === 'https:', mask_all_text: false, bootstrap: {}, session_idle_timeout_seconds: 30 * 60, save_campaign_params: true, save_referrer: true, opt_out_useragent_filter: false, properties_string_max_length: 65535, loaded: () => {}, }) export class Leanbase extends PostHogCore { config: LeanbaseConfig scrollManager: ScrollManager pageViewManager: PageViewManager replayAutocapture?: Autocapture persistence?: LeanbasePersistence sessionPersistence?: LeanbasePersistence sessionManager?: SessionIdManager sessionPropsManager?: SessionPropsManager isRemoteConfigLoaded?: boolean personProcessingSetOncePropertiesSent = false isLoaded: boolean = false initialPageviewCaptured: boolean visibilityStateListener: (() => void) | null constructor(token: string, config?: Partial) { const mergedConfig = extend(defaultConfig(), config || {}, { token, }) super(token, mergedConfig) this.config = mergedConfig as LeanbaseConfig this.visibilityStateListener = null this.initialPageviewCaptured = false this.scrollManager = new ScrollManager(this) this.pageViewManager = new PageViewManager(this) this.init(token, mergedConfig) } init(token: string, config: Partial) { this.setConfig( extend(defaultConfig(), config, { token, }) ) this.isLoaded = true this.persistence = new LeanbasePersistence(this.config) this.replayAutocapture = new Autocapture(this) this.replayAutocapture.startIfEnabled() if (this.config.preloadFeatureFlags !== false) { this.reloadFeatureFlags() } this.config.loaded?.(this) if (this.config.capture_pageview) { setTimeout(() => { if (this.config.cookieless_mode === 'always') { this.captureInitialPageview() } }, 1) } addEventListener(document, 'DOMContentLoaded', () => { this.loadRemoteConfig() }) addEventListener(window, 'onpagehide' in self ? 'pagehide' : 'unload', this.capturePageLeave.bind(this), { passive: false, }) } captureInitialPageview(): void { if (!document) { return } if (document.visibilityState !== 'visible') { if (!this.visibilityStateListener) { this.visibilityStateListener = this.captureInitialPageview.bind(this) addEventListener(document, 'visibilitychange', this.visibilityStateListener) } return } if (!this.initialPageviewCaptured) { this.initialPageviewCaptured = true this.capture('$pageview', { title: document.title }) if (this.visibilityStateListener) { document.removeEventListener('visibilitychange', this.visibilityStateListener) this.visibilityStateListener = null } } } capturePageLeave() { const { capture_pageleave, capture_pageview } = this.config if ( capture_pageleave === true || (capture_pageleave === 'if_capture_pageview' && (capture_pageview === true || capture_pageview === 'history_change')) ) { this.capture('$pageleave') } } async loadRemoteConfig() { if (!this.isRemoteConfigLoaded) { const remoteConfig = await this.reloadRemoteConfigAsync() if (remoteConfig) { this.onRemoteConfig(remoteConfig as RemoteConfig) } } } onRemoteConfig(config: RemoteConfig): void { if (!(document && document.body)) { setTimeout(() => { this.onRemoteConfig(config) }, 500) return } this.isRemoteConfigLoaded = true this.replayAutocapture?.onRemoteConfig(config) } fetch(url: string, options: PostHogFetchOptions): Promise { const fetchFn = getFetch() if (!fetchFn) { return Promise.reject(new Error('Fetch API is not available in this environment.')) } return fetchFn(url, options) } setConfig(config: Partial): void { const oldConfig = { ...this.config } if (isObject(config)) { extend(this.config, config) this.persistence?.update_config(this.config, oldConfig) this.replayAutocapture?.startIfEnabled() } const isTempStorage = this.config.persistence === 'sessionStorage' || this.config.persistence === 'memory' this.sessionPersistence = isTempStorage ? this.persistence : new LeanbasePersistence({ ...this.config, persistence: 'sessionStorage' }) } getLibraryId(): string { return 'leanbase' } getLibraryVersion(): string { return Config.LIB_VERSION } getCustomUserAgent(): void { return } getPersistedProperty(key: PostHogPersistedProperty): T | undefined { return this.persistence?.get_property(key) } setPersistedProperty(key: PostHogPersistedProperty, value: T | null): void { this.persistence?.set_property(key, value) } calculateEventProperties( eventName: string, eventProperties: PostHogEventProperties, timestamp: Date, uuid: string, readOnly?: boolean ): Properties { if (!this.persistence || !this.sessionPersistence) { return eventProperties } timestamp = timestamp || new Date() const startTimestamp = readOnly ? undefined : this.persistence?.remove_event_timer(eventName) let properties = { ...eventProperties } properties['token'] = this.config.token if (this.config.cookieless_mode == 'always' || this.config.cookieless_mode == 'on_reject') { properties[COOKIELESS_MODE_FLAG_PROPERTY] = true } if (eventName === '$snapshot') { const persistenceProps = { ...this.persistence.properties() } properties['distinct_id'] = persistenceProps.distinct_id if ( !(isString(properties['distinct_id']) || isNumber(properties['distinct_id'])) || isEmptyString(properties['distinct_id']) ) { logger.error('Invalid distinct_id for replay event. This indicates a bug in your implementation') } return properties } const infoProperties = getEventProperties( this.config.mask_personal_data_properties, this.config.custom_personal_data_properties ) if (this.sessionManager) { const { sessionId, windowId } = this.sessionManager.checkAndGetSessionAndWindowId( readOnly, timestamp.getTime() ) properties['$session_id'] = sessionId properties['$window_id'] = windowId } if (this.sessionPropsManager) { extend(properties, this.sessionPropsManager.getSessionProps()) } let pageviewProperties: Record = this.pageViewManager.doEvent() if (eventName === '$pageview' && !readOnly) { pageviewProperties = this.pageViewManager.doPageView(timestamp, uuid) } if (eventName === '$pageleave' && !readOnly) { pageviewProperties = this.pageViewManager.doPageLeave(timestamp) } properties = extend(properties, pageviewProperties) if (eventName === '$pageview' && document) { properties['title'] = document.title } if (!isUndefined(startTimestamp)) { const duration_in_ms = timestamp.getTime() - startTimestamp properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)) } if (userAgent && this.config.opt_out_useragent_filter) { properties['$browser_type'] = isLikelyBot(navigator, []) ? 'bot' : 'browser' } properties = extend( {}, infoProperties, this.persistence.properties(), this.sessionPersistence.properties(), properties ) properties['$is_identified'] = this.isIdentified() return properties } isIdentified(): boolean { return ( this.persistence?.get_property(USER_STATE) === 'identified' || this.sessionPersistence?.get_property(USER_STATE) === 'identified' ) } /** * Add additional set_once properties to the event when creating a person profile. This allows us to create the * profile with mostly-accurate properties, despite earlier events not setting them. We do this by storing them in * persistence. * @param dataSetOnce */ calculateSetOnceProperties(dataSetOnce?: Properties): Properties | undefined { if (!this.persistence) { return dataSetOnce } if (this.personProcessingSetOncePropertiesSent) { return dataSetOnce } const initialProps = this.persistence.get_initial_props() const sessionProps = this.sessionPropsManager?.getSetOnceProps() const setOnceProperties = extend({}, initialProps, sessionProps || {}, dataSetOnce || {}) this.personProcessingSetOncePropertiesSent = true if (isEmptyObject(setOnceProperties)) { return undefined } return setOnceProperties } capture(event: string, properties?: PostHogEventProperties, options?: LeanbaseCaptureOptions): void { if (!this.isLoaded || !this.sessionPersistence || !this.persistence) { return } if (isUndefined(event) || !isString(event)) { logger.error('No event name provided to posthog.capture') return } if (properties?.$current_url && !isString(properties?.$current_url)) { logger.error( 'Invalid `$current_url` property provided to `posthog.capture`. Input must be a string. Ignoring provided value.' ) delete properties?.$current_url } this.sessionPersistence.update_search_keyword() if (this.config.save_campaign_params) { this.sessionPersistence.update_campaign_params() } if (this.config.save_referrer) { this.sessionPersistence.update_referrer_info() } if (this.config.save_campaign_params || this.config.save_referrer) { this.persistence.set_initial_person_info() } const systemTime = new Date() const timestamp = options?.timestamp || systemTime const uuid = uuidv7() let data: CaptureResult = { uuid, event, properties: this.calculateEventProperties(event, properties || {}, timestamp, uuid), } const setProperties = options?.$set if (setProperties) { data.$set = options?.$set } const setOnceProperties = this.calculateSetOnceProperties(options?.$set_once) if (setOnceProperties) { data.$set_once = setOnceProperties } data = copyAndTruncateStrings(data, options?._noTruncate ? null : this.config.properties_string_max_length) data.timestamp = timestamp if (!isUndefined(options?.timestamp)) { data.properties['$event_time_override_provided'] = true data.properties['$event_time_override_system_time'] = systemTime } const finalSet = { ...data.properties['$set'], ...data['$set'] } if (!isEmptyObject(finalSet)) { this.setPersonPropertiesForFlags(finalSet) } super.capture(data.event, data.properties, options) } identify(distinctId?: string, properties?: PostHogEventProperties, options?: LeanbaseCaptureOptions): void { super.identify(distinctId, properties, options) } destroy(): void { this.persistence?.clear() } }