/** * External dependencies */ import { EventType, IncrementalSource, MediaInteractions, MouseInteractions, } from '@rrweb/types'; import type { Maybe } from '@neliosr/types'; import type { eventWithTime } from '@rrweb/types'; /** * Internal dependencies */ import { getCookie, setCookie } from '../../cookies'; import { SESSION_MAX_TIME_BETWEEN_EVENTS } from '../constants'; import type { StoredSession } from '../../../types'; const SESSION_COOKIE = 'neliosrSession'; const SESSION_INFO_FLUSH_INTERVAL = 1000; let cachedSessionInfo: StoredSession | undefined; let isSessionInfoDirty = false; let flushTimeoutId: number | undefined; // ======= // HELPERS // ======= export const getSessionInfo = (): Maybe< StoredSession > => { if ( cachedSessionInfo ) { return cachedSessionInfo; } //end if const value = getCookie( SESSION_COOKIE ); cachedSessionInfo = value ? ( JSON.parse( value ) as StoredSession ) : undefined; return cachedSessionInfo; }; export const updateSessionInfoWithEventData = ( event: eventWithTime ): void => { updateSessionInfoWithEventsData( [ event ] ); }; export const updateSessionInfoWithEventsData = ( events: ReadonlyArray< eventWithTime > ): void => { if ( ! events.length ) { return; } //end if const session = getSessionInfo(); if ( ! session ) { return; } //end if cachedSessionInfo = events.reduce< StoredSession >( ( current, event ) => ( { ...current, first: Math.min( current.first, event.timestamp ), last: Math.max( current.last, event.timestamp ), isMeaningful: !! current.isMeaningful || isMeaningful( event ), } ), session ); isSessionInfoDirty = true; scheduleSessionInfoFlush(); }; export const updatePageHistoryInStoredSession = (): Pick< StoredSession, | 'currentPage' | 'previousPage' | 'secondPreviousPage' | 'currentPageTimestamp' | 'previousPageTimestamp' | 'secondPreviousPageTimestamp' > => { const session = getSessionInfo(); if ( ! session ) { return { currentPage: window.location.pathname, }; } //end if const updatedSession = { ...session, ...( !! session.previousPage && { secondPreviousPage: session.previousPage, secondPreviousPageTimestamp: session.previousPageTimestamp, } ), ...( !! session.currentPage && { previousPage: session.currentPage, previousPageTimestamp: session.currentPageTimestamp, } ), currentPage: window.location.pathname, currentPageTimestamp: Date.now(), }; cachedSessionInfo = updatedSession; isSessionInfoDirty = true; writeSessionInfo(); return updatedSession; }; export const flushSessionInfo = (): void => { if ( flushTimeoutId ) { window.clearTimeout( flushTimeoutId ); flushTimeoutId = undefined; } //end if writeSessionInfo(); }; function isMeaningful( event: eventWithTime ): boolean { switch ( event.type ) { case EventType.DomContentLoaded: case EventType.Load: case EventType.FullSnapshot: case EventType.Custom: case EventType.Plugin: return false; case EventType.IncrementalSnapshot: switch ( event.data.source ) { case IncrementalSource.Mutation: case IncrementalSource.MouseMove: return false; case IncrementalSource.MouseInteraction: switch ( event.data.type ) { case MouseInteractions.MouseUp: case MouseInteractions.MouseDown: return false; case MouseInteractions.Click: return true; case MouseInteractions.ContextMenu: return false; case MouseInteractions.DblClick: return true; case MouseInteractions.Focus: case MouseInteractions.Blur: case MouseInteractions.TouchStart: case MouseInteractions.TouchMove_Departed: case MouseInteractions.TouchEnd: case MouseInteractions.TouchCancel: return false; } //end switch return false; case IncrementalSource.Scroll: case IncrementalSource.ViewportResize: return true; case IncrementalSource.Input: return !! event.data.userTriggered; case IncrementalSource.MediaInteraction: switch ( event.data.type ) { case MediaInteractions.Play: case MediaInteractions.Pause: return true; case MediaInteractions.RateChange: case MediaInteractions.Seeked: return false; case MediaInteractions.VolumeChange: return true; } //end switch return false; case IncrementalSource.StyleSheetRule: case IncrementalSource.CanvasMutation: case IncrementalSource.Font: return false; case IncrementalSource.Selection: return true; case IncrementalSource.StyleDeclaration: case IncrementalSource.AdoptedStyleSheet: return false; } //end switch return false; case EventType.Meta: return false; } //end switch } function scheduleSessionInfoFlush(): void { if ( flushTimeoutId ) { return; } //end if flushTimeoutId = window.setTimeout( () => { flushTimeoutId = undefined; writeSessionInfo(); }, SESSION_INFO_FLUSH_INTERVAL ); } function writeSessionInfo(): void { if ( ! cachedSessionInfo || ! isSessionInfoDirty ) { return; } //end if setCookie( SESSION_COOKIE, JSON.stringify( cachedSessionInfo ), { expires: SESSION_MAX_TIME_BETWEEN_EVENTS, } ); isSessionInfoDirty = false; }