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/Dashboard/components/WidgetChart/WidgetChart.tsx b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx index 577670e44..76b1afa5c 100644 --- a/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx +++ b/frontend/app/components/Dashboard/components/WidgetChart/WidgetChart.tsx @@ -181,9 +181,10 @@ function WidgetChart(props: Props) { } prevMetricRef.current = _metric; const timestmaps = drillDownPeriod.toTimestamps(); + const density = props.isPreview ? metric.density : dashboardStore.selectedDensity const payload = isSaved - ? { ...metricParams } - : { ...params, ...timestmaps, ..._metric.toJson() }; + ? { ...metricParams, density } + : { ...params, ...timestmaps, ..._metric.toJson(), density }; debounceRequest( _metric, payload, diff --git a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx index 02d9a970c..18a2b7815 100644 --- a/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx +++ b/frontend/app/components/Dashboard/components/WidgetDateRange/RangeGranularity.tsx @@ -55,7 +55,7 @@ function RangeGranularity({ } const PAST_24_HR_MS = 24 * 60 * 60 * 1000; -function calculateGranularities(periodDurationMs: number) { +export function calculateGranularities(periodDurationMs: number) { const granularities = [ { label: 'Hourly', durationMs: 60 * 60 * 1000 }, { label: 'Daily', durationMs: 24 * 60 * 60 * 1000 }, 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/Filters/FilterAutoComplete/AutocompleteModal.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx index e8d86beb4..88042887c 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/AutocompleteModal.tsx @@ -125,7 +125,7 @@ export function AutocompleteModal({ if (index === blocksAmount - 1 && blocksAmount > 1) { str += ' and '; } - str += `"${block.trim()}"`; + str += block.trim(); if (index < blocksAmount - 2) { str += ', '; } @@ -188,10 +188,10 @@ export function AutocompleteModal({ {query.length ? (
- {t('Apply')} {queryStr} + {t('Apply')} {queryStr}
) : null} diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index 2b6fa260a..41fc685b1 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -128,8 +128,10 @@ const FilterAutoComplete = observer( }; const handleFocus = () => { + if (!initialFocus) { + setOptions(topValues.map((i) => ({ value: i.value, label: i.value }))); + } setInitialFocus(true); - setOptions(topValues.map((i) => ({ value: i.value, label: i.value }))); }; return ( 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/dashboardStore.ts b/frontend/app/mstore/dashboardStore.ts index 723b97c7d..f985b97ca 100644 --- a/frontend/app/mstore/dashboardStore.ts +++ b/frontend/app/mstore/dashboardStore.ts @@ -1,4 +1,4 @@ -import { makeAutoObservable, runInAction } from 'mobx'; +import { makeAutoObservable, runInAction, reaction } from 'mobx'; import { dashboardService, metricService } from 'App/services'; import { toast } from 'react-toastify'; import Period, { LAST_24_HOURS, LAST_7_DAYS } from 'Types/app/period'; @@ -6,6 +6,7 @@ import { getRE } from 'App/utils'; import Filter from './types/filter'; import Widget from './types/widget'; import Dashboard from './types/dashboard'; +import { calculateGranularities } from '@/components/Dashboard/components/WidgetDateRange/RangeGranularity'; interface DashboardFilter { query?: string; @@ -36,7 +37,7 @@ export default class DashboardStore { drillDownPeriod: Record = Period({ rangeName: LAST_24_HOURS }); - selectedDensity: number = 7; // depends on default drilldown, 7 points here!!!; + selectedDensity: number = 7; comparisonPeriods: Record = {}; @@ -83,10 +84,25 @@ export default class DashboardStore { makeAutoObservable(this); this.resetDrillDownFilter(); + + this.createDensity(this.period.getDuration()); + reaction( + () => this.period, + (period) => { + this.createDensity(period.getDuration()); + } + ) } - setDensity = (density: any) => { - this.selectedDensity = parseInt(density, 10); + createDensity = (duration: number) => { + const densityOpts = calculateGranularities(duration); + const defaultOption = densityOpts[densityOpts.length - 2]; + + this.setDensity(defaultOption.key) + } + + setDensity = (density: number) => { + this.selectedDensity = density; }; get sortedDashboards() { @@ -529,7 +545,7 @@ export default class DashboardStore { const data = await metricService.getMetricChartData( metric, params, - isSaved, + isSaved ); resolve(metric.setData(data, period, isComparison, density)); } catch (error) { 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/mstore/types/widget.ts b/frontend/app/mstore/types/widget.ts index 2809ef63f..5ce757500 100644 --- a/frontend/app/mstore/types/widget.ts +++ b/frontend/app/mstore/types/widget.ts @@ -163,6 +163,7 @@ export default class Widget { fromJson(json: any, period?: any) { json.config = json.config || {}; runInAction(() => { + this.dashboardId = json.dashboardId; this.metricId = json.metricId; this.widgetId = json.widgetId; this.metricValue = this.metricValueFromArray( diff --git a/frontend/app/player/web/MessageLoader.ts b/frontend/app/player/web/MessageLoader.ts index e77f82822..c43355363 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 ee92c7891..119ef56f9 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( @@ -104,6 +99,10 @@ export default class WebPlayer extends Player { window.__OPENREPLAY_DEV_TOOLS__.player = this; } + 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 d6c666548..b518395b0 100644 --- a/frontend/app/player/web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -44,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