From c9b29c5c3dca2eb1640774d4c3bd0fd0f92012e8 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 12 May 2025 14:57:22 +0200 Subject: [PATCH 01/40] feat(custom): parse custom event's payload and use the predefined timestamp as a correct ts for our messages --- backend/internal/db/datasaver/web.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/internal/db/datasaver/web.go b/backend/internal/db/datasaver/web.go index 439bcec32..108e6a01b 100644 --- a/backend/internal/db/datasaver/web.go +++ b/backend/internal/db/datasaver/web.go @@ -2,6 +2,7 @@ package datasaver import ( "context" + "encoding/json" "openreplay/backend/pkg/db/postgres" "openreplay/backend/pkg/db/types" @@ -63,6 +64,14 @@ func (s *saverImpl) handleWebMessage(sessCtx context.Context, session *sessions. s.pg.InsertAutocompleteValue(session.SessionID, session.ProjectID, "USERANONYMOUSID", m.ID) return nil case *messages.CustomEvent: + // Try to parse custom event payload to JSON and extract or_payload field + type CustomEventPayload struct { + CustomTimestamp uint64 `json:"or_timestamp"` + } + customPayload := &CustomEventPayload{} + if err := json.Unmarshal([]byte(m.Payload), customPayload); err == nil { + m.Timestamp = customPayload.CustomTimestamp + } if err := s.pg.InsertWebCustomEvent(session, m); err != nil { return err } From 2a483b62f06c5c9723d352d6b49aa77914522e5b Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 12 May 2025 15:08:18 +0200 Subject: [PATCH 02/40] feat(http): removed tracker version validation --- backend/pkg/sessions/api/web/handlers.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/pkg/sessions/api/web/handlers.go b/backend/pkg/sessions/api/web/handlers.go index a8538ba3f..f3530015a 100644 --- a/backend/pkg/sessions/api/web/handlers.go +++ b/backend/pkg/sessions/api/web/handlers.go @@ -154,13 +154,6 @@ func (e *handlersImpl) startSessionHandlerWeb(w http.ResponseWriter, r *http.Req // Add projectID to context r = r.WithContext(context.WithValue(r.Context(), "projectID", fmt.Sprintf("%d", p.ProjectID))) - // Validate tracker version - if err := validateTrackerVersion(req.TrackerVersion); err != nil { - e.log.Error(r.Context(), "unsupported tracker version: %s, err: %s", req.TrackerVersion, err) - e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusUpgradeRequired, errors.New("please upgrade the tracker version"), startTime, r.URL.Path, bodySize) - return - } - // Check if the project supports mobile sessions if !p.IsWeb() { e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusForbidden, errors.New("project doesn't support web sessions"), startTime, r.URL.Path, bodySize) From 500fb4485640213fe7320b9bc9b600a47c793596 Mon Sep 17 00:00:00 2001 From: Delirium Date: Mon, 12 May 2025 15:19:56 +0200 Subject: [PATCH 03/40] Litjs fixes2 (#3381) * ui: fixes for litjs capture * ui: introduce vmode for lwc light dom * ui: fixup the mode toggle and remover --- .../Client/SessionsListingSettings.tsx | 2 + .../app/components/Session_/Subheader.tsx | 12 ++++- .../app/components/Session_/WarnBadge.tsx | 49 ++++++++++++----- .../SessionSettings/SessionSettings.tsx | 1 + .../components/VirtualMode.tsx | 30 +++++++++++ frontend/app/constants/storageKeys.ts | 1 + frontend/app/mstore/types/sessionSettings.ts | 11 +--- frontend/app/player/web/MessageLoader.ts | 1 - frontend/app/player/web/MessageManager.ts | 19 ++++++- frontend/app/player/web/TabManager.ts | 13 +++++ frontend/app/player/web/WebPlayer.ts | 9 ++-- .../app/player/web/managers/DOM/DOMManager.ts | 52 +++++++++---------- .../app/player/web/managers/PagesManager.ts | 7 +++ 13 files changed, 148 insertions(+), 59 deletions(-) create mode 100644 frontend/app/components/shared/SessionSettings/components/VirtualMode.tsx diff --git a/frontend/app/components/Client/SessionsListingSettings.tsx b/frontend/app/components/Client/SessionsListingSettings.tsx index 02875ed0c..8fc159911 100644 --- a/frontend/app/components/Client/SessionsListingSettings.tsx +++ b/frontend/app/components/Client/SessionsListingSettings.tsx @@ -6,6 +6,7 @@ import DefaultPlaying from 'Shared/SessionSettings/components/DefaultPlaying'; import DefaultTimezone from 'Shared/SessionSettings/components/DefaultTimezone'; import ListingVisibility from 'Shared/SessionSettings/components/ListingVisibility'; import MouseTrailSettings from 'Shared/SessionSettings/components/MouseTrailSettings'; +import VirtualModeSettings from '../shared/SessionSettings/components/VirtualMode'; import DebugLog from './DebugLog'; import { useTranslation } from 'react-i18next'; @@ -35,6 +36,7 @@ function SessionsListingSettings() {
+
diff --git a/frontend/app/components/Session_/Subheader.tsx b/frontend/app/components/Session_/Subheader.tsx index addea8840..77de34cc1 100644 --- a/frontend/app/components/Session_/Subheader.tsx +++ b/frontend/app/components/Session_/Subheader.tsx @@ -38,6 +38,7 @@ function SubHeader(props) { projectsStore, userStore, issueReportingStore, + settingsStore } = useStore(); const { t } = useTranslation(); const { favorite } = sessionStore.current; @@ -45,7 +46,7 @@ function SubHeader(props) { const currentSession = sessionStore.current; const projectId = projectsStore.siteId; const integrations = integrationsStore.issues.list; - const { store } = React.useContext(PlayerContext); + const { player, store } = React.useContext(PlayerContext); const { location: currentLocation = 'loading...' } = store.get(); const hasIframe = localStorage.getItem(IFRAME) === 'true'; const [hideTools, setHideTools] = React.useState(false); @@ -127,6 +128,13 @@ function SubHeader(props) { }); }; + const showVModeBadge = store.get().vModeBadge; + const onVMode = () => { + settingsStore.sessionSettings.updateKey('virtualMode', true); + player.enableVMode?.(); + location.reload(); + } + return ( <>
diff --git a/frontend/app/components/Session_/WarnBadge.tsx b/frontend/app/components/Session_/WarnBadge.tsx index 1f4bc275b..90ac23076 100644 --- a/frontend/app/components/Session_/WarnBadge.tsx +++ b/frontend/app/components/Session_/WarnBadge.tsx @@ -34,38 +34,46 @@ const WarnBadge = React.memo( currentLocation, version, siteId, + virtualElsFailed, + onVMode, }: { currentLocation: string; version: string; siteId: string; + virtualElsFailed: boolean; + onVMode: () => void; }) => { const { t } = useTranslation(); const localhostWarnSiteKey = localhostWarn(siteId); const defaultLocalhostWarn = localStorage.getItem(localhostWarnSiteKey) !== '1'; - const localhostWarnActive = + const localhostWarnActive = Boolean( currentLocation && defaultLocalhostWarn && - /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation); + /(localhost)|(127.0.0.1)|(0.0.0.0)/.test(currentLocation) + ) const trackerVersion = window.env.TRACKER_VERSION ?? undefined; const trackerVerDiff = compareVersions(version, trackerVersion); const trackerWarnActive = trackerVerDiff !== VersionComparison.Same; - const [showLocalhostWarn, setLocalhostWarn] = - React.useState(localhostWarnActive); - const [showTrackerWarn, setTrackerWarn] = React.useState(trackerWarnActive); + const [warnings, setWarnings] = React.useState<[localhostWarn: boolean, trackerWarn: boolean, virtualElsFailWarn: boolean]>([localhostWarnActive, trackerWarnActive, virtualElsFailed]) - const closeWarning = (type: 1 | 2) => { + React.useEffect(() => { + setWarnings([localhostWarnActive, trackerWarnActive, virtualElsFailed]) + }, [localhostWarnActive, trackerWarnActive, virtualElsFailed]) + + const closeWarning = (type: 0 | 1 | 2) => { if (type === 1) { localStorage.setItem(localhostWarnSiteKey, '1'); - setLocalhostWarn(false); - } - if (type === 2) { - setTrackerWarn(false); } + setWarnings((prev) => { + const newWarnings = [...prev]; + newWarnings[type] = false; + return newWarnings; + }); }; - if (!showLocalhostWarn && !showTrackerWarn) return null; + if (!warnings.some(el => el === true)) return null; return (
- {showLocalhostWarn ? ( + {warnings[0] ? (
{t('Some assets may load incorrectly on localhost.')} @@ -101,7 +109,7 @@ const WarnBadge = React.memo(
) : null} - {showTrackerWarn ? ( + {warnings[1] ? (
@@ -125,6 +133,21 @@ const WarnBadge = React.memo(
+
closeWarning(2)} + > + +
+
+ ) : null} + {warnings[2] ? ( +
+
+
{t('If you have issues displaying custom HTML elements (i.e when using LWC), consider turning on Virtual Mode.')}
+
{t('Enable')}
+
+
closeWarning(2)} diff --git a/frontend/app/components/shared/SessionSettings/SessionSettings.tsx b/frontend/app/components/shared/SessionSettings/SessionSettings.tsx index 4556a0cb8..60da59598 100644 --- a/frontend/app/components/shared/SessionSettings/SessionSettings.tsx +++ b/frontend/app/components/shared/SessionSettings/SessionSettings.tsx @@ -5,6 +5,7 @@ import ListingVisibility from './components/ListingVisibility'; import DefaultPlaying from './components/DefaultPlaying'; import DefaultTimezone from './components/DefaultTimezone'; import CaptureRate from './components/CaptureRate'; + import { useTranslation } from 'react-i18next'; function SessionSettings() { diff --git a/frontend/app/components/shared/SessionSettings/components/VirtualMode.tsx b/frontend/app/components/shared/SessionSettings/components/VirtualMode.tsx new file mode 100644 index 000000000..690d64ac2 --- /dev/null +++ b/frontend/app/components/shared/SessionSettings/components/VirtualMode.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useStore } from 'App/mstore'; +import { observer } from 'mobx-react-lite'; +import { Switch } from 'UI'; +import { useTranslation } from 'react-i18next'; + +function VirtualModeSettings() { + const { settingsStore } = useStore(); + const { sessionSettings } = settingsStore; + const { virtualMode } = sessionSettings; + const { t } = useTranslation(); + + const updateSettings = (checked: boolean) => { + settingsStore.sessionSettings.updateKey('virtualMode', !virtualMode); + }; + + return ( +
+

{t('Virtual Mode')}

+
+ {t('Change this setting if you have issues with recordings containing Lightning Web Components (or similar custom HTML Element libraries).')} +
+
+ +
+
+ ); +} + +export default observer(VirtualModeSettings); diff --git a/frontend/app/constants/storageKeys.ts b/frontend/app/constants/storageKeys.ts index 948f47bf7..02eab3e48 100644 --- a/frontend/app/constants/storageKeys.ts +++ b/frontend/app/constants/storageKeys.ts @@ -9,6 +9,7 @@ export const GLOBAL_HAS_NO_RECORDINGS = '__$global-hasNoRecordings$__'; export const SITE_ID_STORAGE_KEY = '__$user-siteId$__'; export const GETTING_STARTED = '__$user-gettingStarted$__'; export const MOUSE_TRAIL = '__$session-mouseTrail$__'; +export const VIRTUAL_MODE_KEY = '__$session-virtualMode$__' export const IFRAME = '__$session-iframe$__'; export const JWT_PARAM = '__$session-jwt-param$__'; export const MENU_COLLAPSED = '__$global-menuCollapsed$__'; diff --git a/frontend/app/mstore/types/sessionSettings.ts b/frontend/app/mstore/types/sessionSettings.ts index c128c44ec..827dc767f 100644 --- a/frontend/app/mstore/types/sessionSettings.ts +++ b/frontend/app/mstore/types/sessionSettings.ts @@ -6,6 +6,7 @@ import { SHOWN_TIMEZONE, DURATION_FILTER, MOUSE_TRAIL, + VIRTUAL_MODE_KEY, } from 'App/constants/storageKeys'; import { DateTime, Settings } from 'luxon'; @@ -71,27 +72,19 @@ export const generateGMTZones = (): Timezone[] => { export default class SessionSettings { defaultTimezones = [...generateGMTZones()]; - skipToIssue: boolean = localStorage.getItem(SKIP_TO_ISSUE) === 'true'; - timezone: Timezone; - durationFilter: any = JSON.parse( localStorage.getItem(DURATION_FILTER) || JSON.stringify(defaultDurationFilter), ); - captureRate: string = '0'; - conditionalCapture: boolean = false; - captureConditions: { name: string; captureRate: number; filters: any[] }[] = []; - mouseTrail: boolean = localStorage.getItem(MOUSE_TRAIL) !== 'false'; - shownTimezone: 'user' | 'local'; - + virtualMode: boolean = localStorage.getItem(VIRTUAL_MODE_KEY) === 'true'; usingLocal: boolean = false; constructor() { diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts index cf9e2a6f6..f56947cf0 100644 --- a/frontend/app/player/web/MessageLoader.ts +++ b/frontend/app/player/web/MessageLoader.ts @@ -80,7 +80,6 @@ export default class MessageLoader { let artificialStartTime = Infinity; let startTimeSet = false; - msgs.forEach((msg, i) => { if (msg.tp === MType.Redux || msg.tp === MType.ReduxDeprecated) { if ('actionTime' in msg && msg.actionTime) { diff --git a/frontend/app/player/web/MessageManager.ts b/frontend/app/player/web/MessageManager.ts index 785b1d21e..ab3e0ae78 100644 --- a/frontend/app/player/web/MessageManager.ts +++ b/frontend/app/player/web/MessageManager.ts @@ -1,7 +1,7 @@ // @ts-ignore import { Decoder } from 'syncod'; import logger from 'App/logger'; - +import { VIRTUAL_MODE_KEY } from '@/constants/storageKeys'; import type { Store, ILog, SessionFilesInfo } from 'Player'; import TabSessionManager, { TabState } from 'Player/web/TabManager'; import ActiveTabManager from 'Player/web/managers/ActiveTabManager'; @@ -69,6 +69,7 @@ export interface State extends ScreenState { tabChangeEvents: TabChangeEvent[]; closedTabs: string[]; sessionStart: number; + vModeBadge: boolean; } export const visualChanges = [ @@ -99,6 +100,7 @@ export default class MessageManager { closedTabs: [], sessionStart: 0, tabNames: {}, + vModeBadge: false, }; private clickManager: ListWalker = new ListWalker(); @@ -126,7 +128,6 @@ export default class MessageManager { private tabsAmount = 0; private tabChangeEvents: TabChangeEvent[] = []; - private activeTab = ''; constructor( @@ -142,8 +143,19 @@ export default class MessageManager { this.activityManager = new ActivityManager( this.session.duration.milliseconds, ); // only if not-live + + const vMode = localStorage.getItem(VIRTUAL_MODE_KEY); + if (vMode === 'true') { + this.setVirtualMode(true); + } } + private virtualMode = false; + public setVirtualMode = (virtualMode: boolean) => { + this.virtualMode = virtualMode; + Object.values(this.tabs).forEach((tab) => tab.setVirtualMode(virtualMode)); + }; + public getListsFullState = () => { const fullState: Record = {}; for (const tab in Object.keys(this.tabs)) { @@ -394,6 +406,9 @@ export default class MessageManager { this.sessionStart, this.initialLists, ); + if (this.virtualMode) { + this.tabs[msg.tabId].setVirtualMode(this.virtualMode); + } } const lastMessageTime = Math.max(msg.time, this.lastMessageTime); diff --git a/frontend/app/player/web/TabManager.ts b/frontend/app/player/web/TabManager.ts index 574f7490e..71dd9ed53 100644 --- a/frontend/app/player/web/TabManager.ts +++ b/frontend/app/player/web/TabManager.ts @@ -99,6 +99,7 @@ export default class TabSessionManager { tabStates: { [tabId: string]: TabState }; tabNames: { [tabId: string]: string }; location?: string; + vModeBadge?: boolean; }>, private readonly screen: Screen, private readonly id: string, @@ -116,6 +117,14 @@ export default class TabSessionManager { screen, this.session.isMobile, this.setCSSLoading, + () => { + setTimeout(() => { + this.state.update({ + vModeBadge: true, + }) + // easier to spot the warning appearing plus more time to go over all messages + }, 1000) + } ); this.lists = new Lists(initialLists); initialLists?.event?.forEach((e: Record) => { @@ -126,6 +135,10 @@ export default class TabSessionManager { }); } + public setVirtualMode = (virtualMode: boolean) => { + this.pagesManager.setVirtualMode(virtualMode); + }; + setSession = (session: any) => { this.session = session; }; diff --git a/frontend/app/player/web/WebPlayer.ts b/frontend/app/player/web/WebPlayer.ts index ca1c29d60..50b77ee39 100644 --- a/frontend/app/player/web/WebPlayer.ts +++ b/frontend/app/player/web/WebPlayer.ts @@ -21,15 +21,10 @@ export default class WebPlayer extends Player { inspectorMode: false, mobsFetched: false, }; - private inspectorController: InspectorController; - protected screen: Screen; - protected readonly messageManager: MessageManager; - protected readonly messageLoader: MessageLoader; - private targetMarker: TargetMarker; constructor( @@ -124,6 +119,10 @@ export default class WebPlayer extends Player { }) } + enableVMode = () => { + this.messageManager.setVirtualMode(true); + } + preloadFirstFile(data: Uint8Array, fileKey?: string) { void this.messageLoader.preloadFirstFile(data, fileKey); } diff --git a/frontend/app/player/web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts index 6e2a10582..b518395b0 100644 --- a/frontend/app/player/web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -1,5 +1,4 @@ import logger from 'App/logger'; -import { resolveCSS } from '../../messages/rewriter/urlResolve'; import type Screen from '../../Screen/Screen'; import type { Message, SetNodeScroll } from '../../messages'; @@ -45,47 +44,33 @@ const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; export default class DOMManager extends ListWalker { private readonly vTexts: Map = new Map(); // map vs object here? - private readonly vElements: Map = new Map(); - private readonly olVRoots: Map = new Map(); - /** required to keep track of iframes, frameId : vnodeId */ private readonly iframeRoots: Record = {}; - private shadowRootParentMap: Map = new Map(); - /** Constructed StyleSheets https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets * as well as