pulled dev
This commit is contained in:
commit
7894d10509
38 changed files with 835 additions and 501 deletions
|
|
@ -3,6 +3,7 @@ import withPageTitle from 'HOCs/withPageTitle';
|
||||||
import { PageTitle } from 'UI';
|
import { PageTitle } from 'UI';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
|
import LanguageSwitcher from "App/components/LanguageSwitcher";
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
import ChangePassword from './ChangePassword';
|
import ChangePassword from './ChangePassword';
|
||||||
import styles from './profileSettings.module.css';
|
import styles from './profileSettings.module.css';
|
||||||
|
|
@ -20,107 +21,90 @@ function ProfileSettings() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border shadow-sm p-5">
|
<div className="bg-white rounded-lg border shadow-sm p-5">
|
||||||
<PageTitle title={<div>{t('Account')}</div>} />
|
<PageTitle title={<div>{t('Account')}</div>} />
|
||||||
<div className="flex items-center">
|
<Section
|
||||||
<div className={styles.left}>
|
title={t('Profile')}
|
||||||
<h4 className="text-lg mb-4">{t('Profile')}</h4>
|
description={t('Your email address is your identity on OpenReplay and is used to login.')}
|
||||||
<div className={styles.info}>
|
children={<Settings />}
|
||||||
{t(
|
/>
|
||||||
'Your email address is your identity on OpenReplay and is used to login.',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Settings />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-b my-10" />
|
<div className="border-b my-10" />
|
||||||
|
|
||||||
{account.hasPassword && (
|
{account.hasPassword && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center">
|
<Section
|
||||||
<div className={styles.left}>
|
title={t('Change Password')}
|
||||||
<h4 className="text-lg mb-4">{t('Change Password')}</h4>
|
description={t('Updating your password from time to time enhaces your account’s security')}
|
||||||
<div className={styles.info}>
|
children={<ChangePassword />}
|
||||||
{t('Updating your password from time to time enhances your account’s security.')}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ChangePassword />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-b my-10" />
|
<div className="border-b my-10" />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center">
|
<Section
|
||||||
<div className={styles.left}>
|
title={t('Interface Language')}
|
||||||
<h4 className="text-lg mb-4">{t('Organization API Key')}</h4>
|
description={t('Select the language in which OpenReplay will appear.')}
|
||||||
<div className={styles.info}>
|
children={<LanguageSwitcher />}
|
||||||
{t('Your API key gives you access to an extra set of services.')}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
<Section
|
||||||
<div>
|
title={t('Organization API Key')}
|
||||||
<Api />
|
description={t('Your API key gives you access to an extra set of services.')}
|
||||||
</div>
|
children={<Api />}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{isEnterprise && (account.admin || account.superAdmin) && (
|
{isEnterprise && (account.admin || account.superAdmin) && (
|
||||||
<>
|
<>
|
||||||
<div className="border-b my-10" />
|
<div className="border-b my-10" />
|
||||||
<div className="flex items-center">
|
<Section
|
||||||
<div className={styles.left}>
|
title={t('Tenant Key')}
|
||||||
<h4 className="text-lg mb-4">{t('Tenant Key')}</h4>
|
description={t('For SSO (SAML) authentication.')}
|
||||||
<div className={styles.info}>
|
children={<TenantKey />}
|
||||||
{t('For SSO (SAML) authentication.')}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<TenantKey />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isEnterprise && (
|
{!isEnterprise && (
|
||||||
<>
|
<>
|
||||||
<div className="border-b my-10" />
|
<div className="border-b my-10" />
|
||||||
<div className="flex items-center">
|
<Section
|
||||||
<div className={styles.left}>
|
title={t('Data Collection')}
|
||||||
<h4 className="text-lg mb-4">{t('Data Collection')}</h4>
|
description={t('Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.')}
|
||||||
<div className={styles.info}>
|
children={<OptOut />}
|
||||||
{t('Enables you to control how OpenReplay captures data on your organization’s usage to improve our product.')}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<OptOut />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{account.license && (
|
{account.license && (
|
||||||
<>
|
<>
|
||||||
<div className="border-b my-10" />
|
<div className="border-b my-10" />
|
||||||
|
<Section title={t('License')} description={t('License key and expiration date.')} children={<Licenses />} />
|
||||||
<div className="flex items-center">
|
|
||||||
<div className={styles.left}>
|
|
||||||
<h4 className="text-lg mb-4">{t('License')}</h4>
|
|
||||||
<div className={styles.info}>
|
|
||||||
{t('License key and expiration date.')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Licenses />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Section({ title, description, children }: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={styles.left}>
|
||||||
|
<h4 className="text-lg mb-4">{title}</h4>
|
||||||
|
<div className={styles.info}>
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default withPageTitle('Account - OpenReplay Preferences')(
|
export default withPageTitle('Account - OpenReplay Preferences')(
|
||||||
observer(ProfileSettings),
|
observer(ProfileSettings),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { Button, Dropdown, MenuProps, Space, Typography } from 'antd';
|
import { Button, Dropdown, MenuProps, Typography } from 'antd';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CaretDownOutlined } from '@ant-design/icons';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import { Languages } from 'lucide-react';
|
|
||||||
import { Icon } from '../ui';
|
|
||||||
|
|
||||||
const langs = [
|
const langs = [
|
||||||
{ code: 'en', label: 'English' },
|
{ code: 'en', label: 'English' },
|
||||||
|
|
@ -12,14 +10,25 @@ const langs = [
|
||||||
{ code: 'ru', label: 'Русский' },
|
{ code: 'ru', label: 'Русский' },
|
||||||
{ code: 'zh', label: '中國人' },
|
{ code: 'zh', label: '中國人' },
|
||||||
];
|
];
|
||||||
|
const langLabels = {
|
||||||
|
en: 'English',
|
||||||
|
fr: 'Français',
|
||||||
|
es: 'Español',
|
||||||
|
ru: 'Русский',
|
||||||
|
zh: '中國人',
|
||||||
|
}
|
||||||
|
|
||||||
function LanguageSwitcher() {
|
function LanguageSwitcher() {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
const [selected, setSelected] = React.useState(i18n.language);
|
||||||
|
|
||||||
const handleChangeLanguage = useCallback((lang: string) => {
|
const onChange = (val: string) => {
|
||||||
i18n.changeLanguage(lang);
|
setSelected(val)
|
||||||
localStorage.setItem('i18nextLng', lang);
|
}
|
||||||
}, []);
|
const handleChangeLanguage = () => {
|
||||||
|
void i18n.changeLanguage(selected)
|
||||||
|
localStorage.setItem('i18nextLng', selected)
|
||||||
|
}
|
||||||
|
|
||||||
const menuItems: MenuProps['items'] = langs.map((lang) => ({
|
const menuItems: MenuProps['items'] = langs.map((lang) => ({
|
||||||
key: lang.code,
|
key: lang.code,
|
||||||
|
|
@ -31,21 +40,31 @@ function LanguageSwitcher() {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<div className={'flex flex-col gap-2 align-start'}>
|
||||||
menu={{
|
<div className={'font-semibold'}>{i18n.t('Language')}</div>
|
||||||
items: menuItems,
|
<Dropdown
|
||||||
selectable: true,
|
menu={{
|
||||||
defaultSelectedKeys: [i18n.language],
|
items: menuItems,
|
||||||
style: {
|
selectable: true,
|
||||||
maxHeight: 500,
|
defaultSelectedKeys: [i18n.language],
|
||||||
overflowY: 'auto',
|
style: {
|
||||||
},
|
maxHeight: 500,
|
||||||
onClick: (e) => handleChangeLanguage(e.key),
|
overflowY: 'auto',
|
||||||
}}
|
},
|
||||||
placement="bottomLeft"
|
onClick: (e) => onChange(e.key),
|
||||||
>
|
}}
|
||||||
<Button icon={<Languages size={12} />} />
|
>
|
||||||
</Dropdown>
|
<Button>
|
||||||
|
<div className={'flex justify-between items-center gap-8'}>
|
||||||
|
<span>{langLabels[selected]}</span>
|
||||||
|
<ChevronDown size={14} />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Button className={'w-fit'} onClick={handleChangeLanguage}>
|
||||||
|
{i18n.t('Update')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import MobileOnboardingTabs from '../OnboardingTabs/OnboardingMobileTabs';
|
||||||
import ProjectFormButton from '../ProjectFormButton';
|
import ProjectFormButton from '../ProjectFormButton';
|
||||||
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
import withOnboarding, { WithOnboardingProps } from '../withOnboarding';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CircleHelp } from 'lucide-react'
|
||||||
|
|
||||||
interface Props extends WithOnboardingProps {
|
interface Props extends WithOnboardingProps {
|
||||||
platforms: Array<{
|
platforms: Array<{
|
||||||
|
|
@ -45,8 +46,8 @@ function InstallOpenReplayTab(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<a href={"https://docs.openreplay.com/en/sdk/using-or/"} target="_blank">
|
<a href={"https://docs.openreplay.com/en/sdk/using-or/"} target="_blank">
|
||||||
<Button size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
|
<Button size={"small"} type={"text"} className="ml-2 flex items-center gap-2">
|
||||||
<Icon name={"question-circle"} />
|
<CircleHelp size={14} />
|
||||||
<div className={"text-main"}>{t('See Documentation')}</div>
|
<div>{t('See Documentation')}</div>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react'
|
||||||
|
import DocCard from "App/components/shared/DocCard";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Mail } from 'lucide-react'
|
||||||
|
import { CopyButton } from "UI";
|
||||||
|
|
||||||
|
export function CollabCard({ showUserModal }: { showUserModal: () => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocCard title={t('Need help from team member?')}>
|
||||||
|
<div className={'text-main cursor-pointer flex items-center gap-2'} onClick={showUserModal}>
|
||||||
|
<Mail size={14} />
|
||||||
|
<span>
|
||||||
|
{t('Invite and Collaborate')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</DocCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectKeyCard({ projectKey }: { projectKey: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<DocCard title={t('Project Key')}>
|
||||||
|
<div className="p-2 rounded bg-white flex justify-between items-center">
|
||||||
|
<div className={'font-mono'}>{projectKey}</div>
|
||||||
|
<CopyButton content={projectKey} className={'capitalize font-medium text-neutral-400'} />
|
||||||
|
</div>
|
||||||
|
</DocCard>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import DocCard from 'Shared/DocCard/DocCard';
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
||||||
import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs';
|
import AndroidInstallDocs from 'Components/Onboarding/components/OnboardingTabs/InstallDocs/AndroidInstallDocs';
|
||||||
|
import { CollabCard, ProjectKeyCard } from "./Callouts";
|
||||||
import MobileInstallDocs from './InstallDocs/MobileInstallDocs';
|
import MobileInstallDocs from './InstallDocs/MobileInstallDocs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
@ -39,18 +40,9 @@ function MobileTrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title={t('Need help from team member?')}>
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
|
|
||||||
<DocCard title={t('Project Key')}>
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
<div className="p-2 rounded bg-white flex justify-between items-center">
|
|
||||||
{site.projectKey}
|
|
||||||
<CopyButton content={site.projectKey} />
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -62,18 +54,9 @@ function MobileTrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title={t('Need help from team member?')}>
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
|
|
||||||
<DocCard title={t('Project Key')}>
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
<div className="p-2 rounded bg-white flex justify-between items-center">
|
|
||||||
{site.projectKey}
|
|
||||||
<CopyButton content={site.projectKey} />
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Tabs, Icon, CopyButton } from 'UI';
|
||||||
import DocCard from 'Shared/DocCard/DocCard';
|
import DocCard from 'Shared/DocCard/DocCard';
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
import UserForm from 'App/components/Client/Users/components/UserForm/UserForm';
|
||||||
|
import { CollabCard, ProjectKeyCard } from "./Callouts";
|
||||||
import InstallDocs from './InstallDocs';
|
import InstallDocs from './InstallDocs';
|
||||||
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
import ProjectCodeSnippet from './ProjectCodeSnippet';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -37,20 +38,9 @@ function TrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title="Need help from team member?">
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
<DocCard title="Project Key">
|
|
||||||
<div className="rounded bg-white px-2 py-1 flex items-center justify-between">
|
|
||||||
<span>{site.projectKey}</span>
|
|
||||||
<CopyButton
|
|
||||||
content={site.projectKey}
|
|
||||||
className="capitalize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
<DocCard title="Other ways to install">
|
<DocCard title="Other ways to install">
|
||||||
<a
|
<a
|
||||||
className="link flex items-center"
|
className="link flex items-center"
|
||||||
|
|
@ -77,18 +67,9 @@ function TrackingCodeModal(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<DocCard title="Need help from team member?">
|
<CollabCard showUserModal={showUserModal} />
|
||||||
<a className="link" onClick={showUserModal}>
|
|
||||||
{t('Invite and Collaborate')}
|
|
||||||
</a>
|
|
||||||
</DocCard>
|
|
||||||
|
|
||||||
<DocCard title="Project Key">
|
<ProjectKeyCard projectKey={site.projectKey} />
|
||||||
<div className="p-2 rounded bg-white flex justify-between items-center">
|
|
||||||
{site.projectKey}
|
|
||||||
<CopyButton content={site.projectKey} />
|
|
||||||
</div>
|
|
||||||
</DocCard>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ function SideMenu(props: Props) {
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
style={{ marginTop: '8px', border: 'none' }}
|
style={{ border: 'none' }}
|
||||||
selectedKeys={activeTab ? [activeTab] : []}
|
selectedKeys={activeTab ? [activeTab] : []}
|
||||||
>
|
>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ function GraphQL({ panelHeight }: { panelHeight: number }) {
|
||||||
|
|
||||||
const filterList = (list: any, value: string) => {
|
const filterList = (list: any, value: string) => {
|
||||||
const filterRE = getRE(value, 'i');
|
const filterRE = getRE(value, 'i');
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
? list.filter(
|
? list.filter(
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { ResourceType, Timed } from 'Player';
|
import { ResourceType, Timed } from 'Player';
|
||||||
|
import { WsChannel } from 'Player/web/messages';
|
||||||
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
import MobilePlayer from 'Player/mobile/IOSPlayer';
|
||||||
import WebPlayer from 'Player/web/WebPlayer';
|
import WebPlayer from 'Player/web/WebPlayer';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, {
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import i18n from 'App/i18n'
|
||||||
|
|
||||||
import { useModal } from 'App/components/Modal';
|
import { useModal } from 'App/components/Modal';
|
||||||
import {
|
import {
|
||||||
|
|
@ -12,25 +20,27 @@ import {
|
||||||
} from 'App/components/Session/playerContext';
|
} from 'App/components/Session/playerContext';
|
||||||
import { formatMs } from 'App/date';
|
import { formatMs } from 'App/date';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { formatBytes } from 'App/utils';
|
import { formatBytes, debounceCall } from 'App/utils';
|
||||||
import { Icon, NoContent, Tabs } from 'UI';
|
import { Icon, NoContent, Tabs } from 'UI';
|
||||||
import { Tooltip, Input, Switch, Form } from 'antd';
|
import { Tooltip, Input, Switch, Form } from 'antd';
|
||||||
import { SearchOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
SearchOutlined,
|
||||||
|
InfoCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
import FetchDetailsModal from 'Shared/FetchDetailsModal';
|
||||||
import { WsChannel } from 'App/player/web/messages';
|
|
||||||
|
|
||||||
import BottomBlock from '../BottomBlock';
|
import BottomBlock from '../BottomBlock';
|
||||||
import InfoLine from '../BottomBlock/InfoLine';
|
import InfoLine from '../BottomBlock/InfoLine';
|
||||||
import TabSelector from '../TabSelector';
|
import TabSelector from '../TabSelector';
|
||||||
import TimeTable from '../TimeTable';
|
import TimeTable from '../TimeTable';
|
||||||
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
import useAutoscroll, { getLastItemTime } from '../useAutoscroll';
|
||||||
import { useRegExListFilterMemo, useTabListFilterMemo } from '../useListFilter';
|
|
||||||
import WSPanel from './WSPanel';
|
import WSPanel from './WSPanel';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { mergeListsWithZoom, processInChunks } from './utils'
|
||||||
|
|
||||||
|
// Constants remain the same
|
||||||
const INDEX_KEY = 'network';
|
const INDEX_KEY = 'network';
|
||||||
|
|
||||||
const ALL = 'ALL';
|
const ALL = 'ALL';
|
||||||
const XHR = 'xhr';
|
const XHR = 'xhr';
|
||||||
const JS = 'js';
|
const JS = 'js';
|
||||||
|
|
@ -62,6 +72,9 @@ export const NETWORK_TABS = TAP_KEYS.map((tab) => ({
|
||||||
const DOM_LOADED_TIME_COLOR = 'teal';
|
const DOM_LOADED_TIME_COLOR = 'teal';
|
||||||
const LOAD_TIME_COLOR = 'red';
|
const LOAD_TIME_COLOR = 'red';
|
||||||
|
|
||||||
|
const BATCH_SIZE = 2500;
|
||||||
|
const INITIAL_LOAD_SIZE = 5000;
|
||||||
|
|
||||||
export function renderType(r: any) {
|
export function renderType(r: any) {
|
||||||
return (
|
return (
|
||||||
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
<Tooltip style={{ width: '100%' }} title={<div>{r.type}</div>}>
|
||||||
|
|
@ -79,13 +92,17 @@ export function renderName(r: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSize(r: any) {
|
function renderSize(r: any) {
|
||||||
const { t } = useTranslation();
|
const t = i18n.t;
|
||||||
if (r.responseBodySize) return formatBytes(r.responseBodySize);
|
const notCaptured = t('Not captured');
|
||||||
|
const resSizeStr = t('Resource size')
|
||||||
let triggerText;
|
let triggerText;
|
||||||
let content;
|
let content;
|
||||||
if (r.decodedBodySize == null || r.decodedBodySize === 0) {
|
if (r.responseBodySize) {
|
||||||
|
triggerText = formatBytes(r.responseBodySize);
|
||||||
|
content = undefined;
|
||||||
|
} else if (r.decodedBodySize == null || r.decodedBodySize === 0) {
|
||||||
triggerText = 'x';
|
triggerText = 'x';
|
||||||
content = t('Not captured');
|
content = notCaptured;
|
||||||
} else {
|
} else {
|
||||||
const headerSize = r.headerSize || 0;
|
const headerSize = r.headerSize || 0;
|
||||||
const showTransferred = r.headerSize != null;
|
const showTransferred = r.headerSize != null;
|
||||||
|
|
@ -100,7 +117,7 @@ function renderSize(r: any) {
|
||||||
)} transferred over network`}
|
)} transferred over network`}
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
<li>{`${t('Resource size')}: ${formatBytes(r.decodedBodySize)} `}</li>
|
<li>{`${resSizeStr}: ${formatBytes(r.decodedBodySize)} `}</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +185,8 @@ function renderStatus({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main component for Network Panel
|
||||||
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
const { player, store } = React.useContext(PlayerContext);
|
const { player, store } = React.useContext(PlayerContext);
|
||||||
const { sessionStore, uiPlayerStore } = useStore();
|
const { sessionStore, uiPlayerStore } = useStore();
|
||||||
|
|
@ -216,6 +235,7 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
|
|
||||||
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
|
const getTabNum = (tab: string) => tabsArr.findIndex((t) => t === tab) + 1;
|
||||||
const getTabName = (tabId: string) => tabNames[tabId];
|
const getTabName = (tabId: string) => tabNames[tabId];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NetworkPanelComp
|
<NetworkPanelComp
|
||||||
loadTime={loadTime}
|
loadTime={loadTime}
|
||||||
|
|
@ -228,8 +248,8 @@ function NetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
resourceListNow={resourceListNow}
|
resourceListNow={resourceListNow}
|
||||||
player={player}
|
player={player}
|
||||||
startedAt={startedAt}
|
startedAt={startedAt}
|
||||||
websocketList={websocketList as WSMessage[]}
|
websocketList={websocketList}
|
||||||
websocketListNow={websocketListNow as WSMessage[]}
|
websocketListNow={websocketListNow}
|
||||||
getTabNum={getTabNum}
|
getTabNum={getTabNum}
|
||||||
getTabName={getTabName}
|
getTabName={getTabName}
|
||||||
showSingleTab={showSingleTab}
|
showSingleTab={showSingleTab}
|
||||||
|
|
@ -269,9 +289,7 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
resourceListNow={resourceListNow}
|
resourceListNow={resourceListNow}
|
||||||
player={player}
|
player={player}
|
||||||
startedAt={startedAt}
|
startedAt={startedAt}
|
||||||
// @ts-ignore
|
|
||||||
websocketList={websocketList}
|
websocketList={websocketList}
|
||||||
// @ts-ignore
|
|
||||||
websocketListNow={websocketListNow}
|
websocketListNow={websocketListNow}
|
||||||
zoomEnabled={zoomEnabled}
|
zoomEnabled={zoomEnabled}
|
||||||
zoomStartTs={zoomStartTs}
|
zoomStartTs={zoomStartTs}
|
||||||
|
|
@ -280,12 +298,35 @@ function MobileNetworkPanelCont({ panelHeight }: { panelHeight: number }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type WSMessage = Timed & {
|
const useInfiniteScroll = (loadMoreCallback: () => void, hasMore: boolean) => {
|
||||||
channelName: string;
|
const observerRef = useRef<IntersectionObserver>(null);
|
||||||
data: string;
|
const loadingRef = useRef<HTMLDivElement>(null);
|
||||||
timestamp: number;
|
|
||||||
dir: 'up' | 'down';
|
useEffect(() => {
|
||||||
messageType: string;
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0]?.isIntersecting && hasMore) {
|
||||||
|
loadMoreCallback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loadingRef.current) {
|
||||||
|
observer.observe(loadingRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
observerRef.current = observer;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (observerRef.current) {
|
||||||
|
observerRef.current.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [loadMoreCallback, hasMore, loadingRef]);
|
||||||
|
|
||||||
|
return loadingRef;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -302,8 +343,8 @@ interface Props {
|
||||||
resourceList: Timed[];
|
resourceList: Timed[];
|
||||||
fetchListNow: Timed[];
|
fetchListNow: Timed[];
|
||||||
resourceListNow: Timed[];
|
resourceListNow: Timed[];
|
||||||
websocketList: Array<WSMessage>;
|
websocketList: Array<WsChannel>;
|
||||||
websocketListNow: Array<WSMessage>;
|
websocketListNow: Array<WsChannel>;
|
||||||
player: WebPlayer | MobilePlayer;
|
player: WebPlayer | MobilePlayer;
|
||||||
startedAt: number;
|
startedAt: number;
|
||||||
isMobile?: boolean;
|
isMobile?: boolean;
|
||||||
|
|
@ -349,107 +390,189 @@ export const NetworkPanelComp = observer(
|
||||||
>(null);
|
>(null);
|
||||||
const { showModal } = useModal();
|
const { showModal } = useModal();
|
||||||
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
const [showOnlyErrors, setShowOnlyErrors] = useState(false);
|
||||||
|
|
||||||
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
const [isDetailsModalActive, setIsDetailsModalActive] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [displayedItems, setDisplayedItems] = useState([]);
|
||||||
|
const [totalItems, setTotalItems] = useState(0);
|
||||||
|
const [summaryStats, setSummaryStats] = useState({
|
||||||
|
resourcesSize: 0,
|
||||||
|
transferredSize: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const originalListRef = useRef([]);
|
||||||
|
const socketListRef = useRef([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
sessionStore: { devTools },
|
sessionStore: { devTools },
|
||||||
} = useStore();
|
} = useStore();
|
||||||
const { filter } = devTools[INDEX_KEY];
|
const { filter } = devTools[INDEX_KEY];
|
||||||
const { activeTab } = devTools[INDEX_KEY];
|
const { activeTab } = devTools[INDEX_KEY];
|
||||||
const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index;
|
const activeIndex = activeOutsideIndex ?? devTools[INDEX_KEY].index;
|
||||||
|
const [inputFilterValue, setInputFilterValue] = useState(filter);
|
||||||
|
|
||||||
const socketList = useMemo(
|
const debouncedFilter = useCallback(
|
||||||
() =>
|
debounceCall((filterValue) => {
|
||||||
websocketList.filter(
|
devTools.update(INDEX_KEY, { filter: filterValue });
|
||||||
(ws, i, arr) =>
|
}, 300),
|
||||||
arr.findIndex((it) => it.channelName === ws.channelName) === i,
|
[],
|
||||||
),
|
|
||||||
[websocketList],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const list = useMemo(
|
// Process socket lists once
|
||||||
() =>
|
useEffect(() => {
|
||||||
// TODO: better merge (with body size info) - do it in player
|
const uniqueSocketList = websocketList.filter(
|
||||||
resourceList
|
(ws, i, arr) =>
|
||||||
.filter(
|
arr.findIndex((it) => it.channelName === ws.channelName) === i,
|
||||||
(res) =>
|
|
||||||
!fetchList.some((ft) => {
|
|
||||||
// res.url !== ft.url doesn't work on relative URLs appearing within fetchList (to-fix in player)
|
|
||||||
if (res.name === ft.name) {
|
|
||||||
if (res.time === ft.time) return true;
|
|
||||||
if (res.url.includes(ft.url)) {
|
|
||||||
return (
|
|
||||||
Math.abs(res.time - ft.time) < 350 ||
|
|
||||||
Math.abs(res.timestamp - ft.timestamp) < 350
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.name !== ft.name) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Math.abs(res.time - ft.time) > 250) {
|
|
||||||
return false;
|
|
||||||
} // TODO: find good epsilons
|
|
||||||
if (Math.abs(res.duration - ft.duration) > 200) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.concat(fetchList)
|
|
||||||
.concat(
|
|
||||||
socketList.map((ws) => ({
|
|
||||||
...ws,
|
|
||||||
type: 'websocket',
|
|
||||||
method: 'ws',
|
|
||||||
url: ws.channelName,
|
|
||||||
name: ws.channelName,
|
|
||||||
status: '101',
|
|
||||||
duration: 0,
|
|
||||||
transferredBodySize: 0,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.filter((req) =>
|
|
||||||
zoomEnabled
|
|
||||||
? req.time >= zoomStartTs! && req.time <= zoomEndTs!
|
|
||||||
: true,
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.time - b.time),
|
|
||||||
[resourceList.length, fetchList.length, socketList.length],
|
|
||||||
);
|
|
||||||
|
|
||||||
let filteredList = useMemo(() => {
|
|
||||||
if (!showOnlyErrors) {
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
return list.filter(
|
|
||||||
(it) => parseInt(it.status) >= 400 || !it.success || it.error,
|
|
||||||
);
|
);
|
||||||
}, [showOnlyErrors, list]);
|
socketListRef.current = uniqueSocketList;
|
||||||
filteredList = useRegExListFilterMemo(
|
}, [websocketList.length]);
|
||||||
filteredList,
|
|
||||||
(it) => [it.status, it.name, it.type, it.method],
|
|
||||||
filter,
|
|
||||||
);
|
|
||||||
filteredList = useTabListFilterMemo(
|
|
||||||
filteredList,
|
|
||||||
(it) => TYPE_TO_TAB[it.type],
|
|
||||||
ALL,
|
|
||||||
activeTab,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onTabClick = (activeTab: (typeof TAP_KEYS)[number]) =>
|
// Initial data processing - do this only once when data changes
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Heaviest operation here, will create a final merged network list
|
||||||
|
const processData = async () => {
|
||||||
|
const fetchUrls = new Set(
|
||||||
|
fetchList.map((ft) => {
|
||||||
|
return `${ft.name}-${Math.floor(ft.time / 100)}-${Math.floor(ft.duration / 100)}`;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// We want to get resources that aren't in fetch list
|
||||||
|
const filteredResources = await processInChunks(resourceList, (chunk) =>
|
||||||
|
chunk.filter((res: any) => {
|
||||||
|
const key = `${res.name}-${Math.floor(res.time / 100)}-${Math.floor(res.duration / 100)}`;
|
||||||
|
return !fetchUrls.has(key);
|
||||||
|
}),
|
||||||
|
BATCH_SIZE,
|
||||||
|
25,
|
||||||
|
);
|
||||||
|
|
||||||
|
const processedSockets = socketListRef.current.map((ws: any) => ({
|
||||||
|
...ws,
|
||||||
|
type: 'websocket',
|
||||||
|
method: 'ws',
|
||||||
|
url: ws.channelName,
|
||||||
|
name: ws.channelName,
|
||||||
|
status: '101',
|
||||||
|
duration: 0,
|
||||||
|
transferredBodySize: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mergedList: Timed[] = mergeListsWithZoom(
|
||||||
|
filteredResources as Timed[],
|
||||||
|
fetchList,
|
||||||
|
processedSockets as Timed[],
|
||||||
|
{ enabled: Boolean(zoomEnabled), start: zoomStartTs ?? 0, end: zoomEndTs ?? 0 }
|
||||||
|
)
|
||||||
|
|
||||||
|
originalListRef.current = mergedList;
|
||||||
|
setTotalItems(mergedList.length);
|
||||||
|
|
||||||
|
calculateResourceStats(resourceList);
|
||||||
|
|
||||||
|
// Only display initial chunk
|
||||||
|
setDisplayedItems(mergedList.slice(0, INITIAL_LOAD_SIZE));
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
void processData();
|
||||||
|
}, [
|
||||||
|
resourceList.length,
|
||||||
|
fetchList.length,
|
||||||
|
socketListRef.current.length,
|
||||||
|
zoomEnabled,
|
||||||
|
zoomStartTs,
|
||||||
|
zoomEndTs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const calculateResourceStats = (resourceList: Record<string, any>) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
let resourcesSize = 0
|
||||||
|
let transferredSize = 0
|
||||||
|
resourceList.forEach(({ decodedBodySize, headerSize, encodedBodySize }: any) => {
|
||||||
|
resourcesSize += decodedBodySize || 0
|
||||||
|
transferredSize += (headerSize || 0) + (encodedBodySize || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
setSummaryStats({
|
||||||
|
resourcesSize,
|
||||||
|
transferredSize,
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (originalListRef.current.length === 0) return;
|
||||||
|
setIsProcessing(true);
|
||||||
|
const applyFilters = async () => {
|
||||||
|
let filteredItems: any[] = originalListRef.current;
|
||||||
|
|
||||||
|
filteredItems = await processInChunks(filteredItems, (chunk) =>
|
||||||
|
chunk.filter(
|
||||||
|
(it) => {
|
||||||
|
let valid = true;
|
||||||
|
if (showOnlyErrors) {
|
||||||
|
valid = parseInt(it.status) >= 400 || !it.success || it.error
|
||||||
|
}
|
||||||
|
if (filter) {
|
||||||
|
try {
|
||||||
|
const regex = new RegExp(filter, 'i');
|
||||||
|
valid = valid && regex.test(it.status) || regex.test(it.name) || regex.test(it.type) || regex.test(it.method);
|
||||||
|
} catch (e) {
|
||||||
|
valid = valid && String(it.status).includes(filter) || it.name.includes(filter) || it.type.includes(filter) || (it.method && it.method.includes(filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activeTab !== ALL) {
|
||||||
|
valid = valid && TYPE_TO_TAB[it.type] === activeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update displayed items
|
||||||
|
setDisplayedItems(filteredItems.slice(0, INITIAL_LOAD_SIZE));
|
||||||
|
setTotalItems(filteredItems.length);
|
||||||
|
setIsProcessing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
void applyFilters();
|
||||||
|
}, [filter, activeTab, showOnlyErrors]);
|
||||||
|
|
||||||
|
const loadMoreItems = useCallback(() => {
|
||||||
|
if (isProcessing) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setDisplayedItems((prevItems) => {
|
||||||
|
const currentLength = prevItems.length;
|
||||||
|
const newItems = originalListRef.current.slice(
|
||||||
|
currentLength,
|
||||||
|
currentLength + BATCH_SIZE,
|
||||||
|
);
|
||||||
|
return [...prevItems, ...newItems];
|
||||||
|
});
|
||||||
|
setIsProcessing(false);
|
||||||
|
}, 10);
|
||||||
|
}, [isProcessing]);
|
||||||
|
|
||||||
|
const hasMoreItems = displayedItems.length < totalItems;
|
||||||
|
const loadingRef = useInfiniteScroll(loadMoreItems, hasMoreItems);
|
||||||
|
|
||||||
|
const onTabClick = (activeTab) => {
|
||||||
devTools.update(INDEX_KEY, { activeTab });
|
devTools.update(INDEX_KEY, { activeTab });
|
||||||
const onFilterChange = ({
|
};
|
||||||
target: { value },
|
|
||||||
}: React.ChangeEvent<HTMLInputElement>) =>
|
const onFilterChange = ({ target: { value } }) => {
|
||||||
devTools.update(INDEX_KEY, { filter: value });
|
setInputFilterValue(value)
|
||||||
|
debouncedFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
// AutoScroll
|
|
||||||
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
const [timeoutStartAutoscroll, stopAutoscroll] = useAutoscroll(
|
||||||
filteredList,
|
displayedItems,
|
||||||
getLastItemTime(fetchListNow, resourceListNow),
|
getLastItemTime(fetchListNow, resourceListNow),
|
||||||
activeIndex,
|
activeIndex,
|
||||||
(index) => devTools.update(INDEX_KEY, { index }),
|
(index) => devTools.update(INDEX_KEY, { index }),
|
||||||
|
|
@ -462,24 +585,6 @@ export const NetworkPanelComp = observer(
|
||||||
timeoutStartAutoscroll();
|
timeoutStartAutoscroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const resourcesSize = useMemo(
|
|
||||||
() =>
|
|
||||||
resourceList.reduce(
|
|
||||||
(sum, { decodedBodySize }) => sum + (decodedBodySize || 0),
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
[resourceList.length],
|
|
||||||
);
|
|
||||||
const transferredSize = useMemo(
|
|
||||||
() =>
|
|
||||||
resourceList.reduce(
|
|
||||||
(sum, { headerSize, encodedBodySize }) =>
|
|
||||||
sum + (headerSize || 0) + (encodedBodySize || 0),
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
[resourceList.length],
|
|
||||||
);
|
|
||||||
|
|
||||||
const referenceLines = useMemo(() => {
|
const referenceLines = useMemo(() => {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
|
|
||||||
|
|
@ -513,7 +618,7 @@ export const NetworkPanelComp = observer(
|
||||||
isSpot={isSpot}
|
isSpot={isSpot}
|
||||||
time={item.time + startedAt}
|
time={item.time + startedAt}
|
||||||
resource={item}
|
resource={item}
|
||||||
rows={filteredList}
|
rows={displayedItems}
|
||||||
fetchPresented={fetchList.length > 0}
|
fetchPresented={fetchList.length > 0}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
|
|
@ -525,12 +630,12 @@ export const NetworkPanelComp = observer(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
devTools.update(INDEX_KEY, { index: filteredList.indexOf(item) });
|
devTools.update(INDEX_KEY, { index: displayedItems.indexOf(item) });
|
||||||
stopAutoscroll();
|
stopAutoscroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableCols = React.useMemo(() => {
|
const tableCols = useMemo(() => {
|
||||||
const cols: any[] = [
|
const cols = [
|
||||||
{
|
{
|
||||||
label: t('Status'),
|
label: t('Status'),
|
||||||
dataKey: 'status',
|
dataKey: 'status',
|
||||||
|
|
@ -585,7 +690,7 @@ export const NetworkPanelComp = observer(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cols;
|
return cols;
|
||||||
}, [showSingleTab]);
|
}, [showSingleTab, activeTab, t, getTabName, getTabNum, isSpot]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomBlock
|
<BottomBlock
|
||||||
|
|
@ -617,7 +722,7 @@ export const NetworkPanelComp = observer(
|
||||||
name="filter"
|
name="filter"
|
||||||
onChange={onFilterChange}
|
onChange={onFilterChange}
|
||||||
width={280}
|
width={280}
|
||||||
value={filter}
|
value={inputFilterValue}
|
||||||
size="small"
|
size="small"
|
||||||
prefix={<SearchOutlined className="text-neutral-400" />}
|
prefix={<SearchOutlined className="text-neutral-400" />}
|
||||||
/>
|
/>
|
||||||
|
|
@ -625,7 +730,7 @@ export const NetworkPanelComp = observer(
|
||||||
</BottomBlock.Header>
|
</BottomBlock.Header>
|
||||||
<BottomBlock.Content>
|
<BottomBlock.Content>
|
||||||
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
<div className="flex items-center justify-between px-4 border-b bg-teal/5 h-8">
|
||||||
<div>
|
<div className="flex items-center">
|
||||||
<Form.Item name="show-errors-only" className="mb-0">
|
<Form.Item name="show-errors-only" className="mb-0">
|
||||||
<label
|
<label
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -642,21 +747,29 @@ export const NetworkPanelComp = observer(
|
||||||
<span className="text-sm ms-2">4xx-5xx Only</span>
|
<span className="text-sm ms-2">4xx-5xx Only</span>
|
||||||
</label>
|
</label>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
{isProcessing && (
|
||||||
|
<span className="text-xs text-gray-500 ml-4">
|
||||||
|
Processing data...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<InfoLine>
|
<InfoLine>
|
||||||
|
<InfoLine.Point label={`${totalItems}`} value="requests" />
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={`${filteredList.length}`}
|
label={`${displayedItems.length}/${totalItems}`}
|
||||||
value=" requests"
|
value="displayed"
|
||||||
|
display={displayedItems.length < totalItems}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={formatBytes(transferredSize)}
|
label={formatBytes(summaryStats.transferredSize)}
|
||||||
value="transferred"
|
value="transferred"
|
||||||
display={transferredSize > 0}
|
display={summaryStats.transferredSize > 0}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={formatBytes(resourcesSize)}
|
label={formatBytes(summaryStats.resourcesSize)}
|
||||||
value="resources"
|
value="resources"
|
||||||
display={resourcesSize > 0}
|
display={summaryStats.resourcesSize > 0}
|
||||||
/>
|
/>
|
||||||
<InfoLine.Point
|
<InfoLine.Point
|
||||||
label={formatMs(domBuildingTime)}
|
label={formatMs(domBuildingTime)}
|
||||||
|
|
@ -679,42 +792,67 @@ export const NetworkPanelComp = observer(
|
||||||
/>
|
/>
|
||||||
</InfoLine>
|
</InfoLine>
|
||||||
</div>
|
</div>
|
||||||
<NoContent
|
|
||||||
title={
|
{isLoading ? (
|
||||||
<div className="capitalize flex items-center gap-2">
|
<div className="flex items-center justify-center h-full">
|
||||||
<InfoCircleOutlined size={18} />
|
<div className="text-center">
|
||||||
{t('No Data')}
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-2"></div>
|
||||||
|
<p>Processing initial network data...</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
</div>
|
||||||
size="small"
|
) : (
|
||||||
show={filteredList.length === 0}
|
<NoContent
|
||||||
>
|
title={
|
||||||
{/* @ts-ignore */}
|
<div className="capitalize flex items-center gap-2">
|
||||||
<TimeTable
|
<InfoCircleOutlined size={18} />
|
||||||
rows={filteredList}
|
{t('No Data')}
|
||||||
tableHeight={panelHeight - 102}
|
</div>
|
||||||
referenceLines={referenceLines}
|
}
|
||||||
renderPopup
|
size="small"
|
||||||
onRowClick={showDetailsModal}
|
show={displayedItems.length === 0}
|
||||||
sortBy="time"
|
|
||||||
sortAscending
|
|
||||||
onJump={(row: any) => {
|
|
||||||
devTools.update(INDEX_KEY, {
|
|
||||||
index: filteredList.indexOf(row),
|
|
||||||
});
|
|
||||||
player.jump(row.time);
|
|
||||||
}}
|
|
||||||
activeIndex={activeIndex}
|
|
||||||
>
|
>
|
||||||
{tableCols}
|
<div>
|
||||||
</TimeTable>
|
<TimeTable
|
||||||
{selectedWsChannel ? (
|
rows={displayedItems}
|
||||||
<WSPanel
|
tableHeight={panelHeight - 102 - (hasMoreItems ? 30 : 0)}
|
||||||
socketMsgList={selectedWsChannel}
|
referenceLines={referenceLines}
|
||||||
onClose={() => setSelectedWsChannel(null)}
|
renderPopup
|
||||||
/>
|
onRowClick={showDetailsModal}
|
||||||
) : null}
|
sortBy="time"
|
||||||
</NoContent>
|
sortAscending
|
||||||
|
onJump={(row) => {
|
||||||
|
devTools.update(INDEX_KEY, {
|
||||||
|
index: displayedItems.indexOf(row),
|
||||||
|
});
|
||||||
|
player.jump(row.time);
|
||||||
|
}}
|
||||||
|
activeIndex={activeIndex}
|
||||||
|
>
|
||||||
|
{tableCols}
|
||||||
|
</TimeTable>
|
||||||
|
|
||||||
|
{hasMoreItems && (
|
||||||
|
<div
|
||||||
|
ref={loadingRef}
|
||||||
|
className="flex justify-center items-center text-xs text-gray-500"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-2"></div>
|
||||||
|
Loading more data ({totalItems - displayedItems.length}{' '}
|
||||||
|
remaining)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedWsChannel ? (
|
||||||
|
<WSPanel
|
||||||
|
socketMsgList={selectedWsChannel}
|
||||||
|
onClose={() => setSelectedWsChannel(null)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</NoContent>
|
||||||
|
)}
|
||||||
</BottomBlock.Content>
|
</BottomBlock.Content>
|
||||||
</BottomBlock>
|
</BottomBlock>
|
||||||
);
|
);
|
||||||
|
|
@ -722,7 +860,6 @@ export const NetworkPanelComp = observer(
|
||||||
);
|
);
|
||||||
|
|
||||||
const WebNetworkPanel = observer(NetworkPanelCont);
|
const WebNetworkPanel = observer(NetworkPanelCont);
|
||||||
|
|
||||||
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
|
const MobileNetworkPanel = observer(MobileNetworkPanelCont);
|
||||||
|
|
||||||
export { WebNetworkPanel, MobileNetworkPanel };
|
export { WebNetworkPanel, MobileNetworkPanel };
|
||||||
|
|
|
||||||
178
frontend/app/components/shared/DevTools/NetworkPanel/utils.ts
Normal file
178
frontend/app/components/shared/DevTools/NetworkPanel/utils.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
export function mergeListsWithZoom<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
Y extends Record<string, any>,
|
||||||
|
Z extends Record<string, any>,
|
||||||
|
>(
|
||||||
|
arr1: T[],
|
||||||
|
arr2: Y[],
|
||||||
|
arr3: Z[],
|
||||||
|
zoom?: { enabled: boolean; start: number; end: number },
|
||||||
|
): Array<T | Y | Z> {
|
||||||
|
// Early return for empty arrays
|
||||||
|
if (arr1.length === 0 && arr2.length === 0 && arr3.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized for common case - no zoom
|
||||||
|
if (!zoom?.enabled) {
|
||||||
|
return mergeThreeSortedArrays(arr1, arr2, arr3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search for start indexes (faster than linear search for large arrays)
|
||||||
|
const index1 = binarySearchStartIndex(arr1, zoom.start);
|
||||||
|
const index2 = binarySearchStartIndex(arr2, zoom.start);
|
||||||
|
const index3 = binarySearchStartIndex(arr3, zoom.start);
|
||||||
|
|
||||||
|
// Merge arrays within zoom range
|
||||||
|
return mergeThreeSortedArraysWithinRange(
|
||||||
|
arr1,
|
||||||
|
arr2,
|
||||||
|
arr3,
|
||||||
|
index1,
|
||||||
|
index2,
|
||||||
|
index3,
|
||||||
|
zoom.start,
|
||||||
|
zoom.end,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function binarySearchStartIndex<T extends Record<string, any>>(
|
||||||
|
arr: T[],
|
||||||
|
threshold: number,
|
||||||
|
): number {
|
||||||
|
if (arr.length === 0) return 0;
|
||||||
|
|
||||||
|
let low = 0;
|
||||||
|
let high = arr.length - 1;
|
||||||
|
|
||||||
|
// Handle edge cases first for better performance
|
||||||
|
if (arr[high].time < threshold) return arr.length;
|
||||||
|
if (arr[low].time >= threshold) return 0;
|
||||||
|
|
||||||
|
while (low <= high) {
|
||||||
|
const mid = Math.floor((low + high) / 2);
|
||||||
|
|
||||||
|
if (arr[mid].time < threshold) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else {
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeThreeSortedArrays<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
Y extends Record<string, any>,
|
||||||
|
Z extends Record<string, any>,
|
||||||
|
>(arr1: T[], arr2: Y[], arr3: Z[]): Array<T | Y | Z> {
|
||||||
|
const totalLength = arr1.length + arr2.length + arr3.length;
|
||||||
|
// prealloc array size
|
||||||
|
const result = new Array(totalLength);
|
||||||
|
|
||||||
|
let i = 0,
|
||||||
|
j = 0,
|
||||||
|
k = 0,
|
||||||
|
index = 0;
|
||||||
|
|
||||||
|
while (i < arr1.length || j < arr2.length || k < arr3.length) {
|
||||||
|
const val1 = i < arr1.length ? arr1[i].time : Infinity;
|
||||||
|
const val2 = j < arr2.length ? arr2[j].time : Infinity;
|
||||||
|
const val3 = k < arr3.length ? arr3[k].time : Infinity;
|
||||||
|
|
||||||
|
if (val1 <= val2 && val1 <= val3) {
|
||||||
|
result[index++] = arr1[i++];
|
||||||
|
} else if (val2 <= val1 && val2 <= val3) {
|
||||||
|
result[index++] = arr2[j++];
|
||||||
|
} else {
|
||||||
|
result[index++] = arr3[k++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// same as above, just with zoom stuff
|
||||||
|
function mergeThreeSortedArraysWithinRange<
|
||||||
|
T extends Record<string, any>,
|
||||||
|
Y extends Record<string, any>,
|
||||||
|
Z extends Record<string, any>,
|
||||||
|
>(
|
||||||
|
arr1: T[],
|
||||||
|
arr2: Y[],
|
||||||
|
arr3: Z[],
|
||||||
|
startIdx1: number,
|
||||||
|
startIdx2: number,
|
||||||
|
startIdx3: number,
|
||||||
|
start: number,
|
||||||
|
end: number,
|
||||||
|
): Array<T | Y | Z> {
|
||||||
|
// we don't know beforehand how many items will be there
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
let i = startIdx1;
|
||||||
|
let j = startIdx2;
|
||||||
|
let k = startIdx3;
|
||||||
|
|
||||||
|
while (i < arr1.length || j < arr2.length || k < arr3.length) {
|
||||||
|
const val1 = i < arr1.length ? arr1[i].time : Infinity;
|
||||||
|
const val2 = j < arr2.length ? arr2[j].time : Infinity;
|
||||||
|
const val3 = k < arr3.length ? arr3[k].time : Infinity;
|
||||||
|
|
||||||
|
// Early termination: if all remaining values exceed end time
|
||||||
|
if (Math.min(val1, val2, val3) > end) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val1 <= val2 && val1 <= val3) {
|
||||||
|
if (val1 <= end) {
|
||||||
|
result.push(arr1[i]);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (val2 <= val1 && val2 <= val3) {
|
||||||
|
if (val2 <= end) {
|
||||||
|
result.push(arr2[j]);
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
} else {
|
||||||
|
if (val3 <= end) {
|
||||||
|
result.push(arr3[k]);
|
||||||
|
}
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processInChunks(
|
||||||
|
items: any[],
|
||||||
|
processFn: (item: any) => any,
|
||||||
|
chunkSize = 1000,
|
||||||
|
overscan = 0,
|
||||||
|
) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (items.length === 0) {
|
||||||
|
resolve([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: any[] = [];
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const processNextChunk = () => {
|
||||||
|
const chunk = items.slice(index, index + chunkSize + overscan);
|
||||||
|
result = result.concat(processFn(chunk));
|
||||||
|
index += chunkSize;
|
||||||
|
|
||||||
|
if (index < items.length) {
|
||||||
|
setTimeout(processNextChunk, 0);
|
||||||
|
} else {
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
processNextChunk();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ function DocCard(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('p-5 bg-gray-lightest mb-4 rounded', className)}>
|
<div className={cn('p-5 bg-gray-lightest mb-4 rounded-lg', className)}>
|
||||||
<div className="font-medium mb-2 flex items-center">
|
<div className="font-medium mb-2 flex items-center">
|
||||||
{props.icon && (
|
{props.icon && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import { Input } from 'antd';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useStore } from 'App/mstore';
|
|
||||||
|
|
||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import NoSessionsMessage from 'Shared/NoSessionsMessage/NoSessionsMessage';
|
import NoSessionsMessage from 'Shared/NoSessionsMessage/NoSessionsMessage';
|
||||||
import MainSearchBar from 'Shared/MainSearchBar/MainSearchBar';
|
import MainSearchBar from 'Shared/MainSearchBar/MainSearchBar';
|
||||||
import usePageTitle from '@/hooks/usePageTitle';
|
import usePageTitle from '@/hooks/usePageTitle';
|
||||||
|
|
@ -13,22 +9,8 @@ import SessionHeader from './components/SessionHeader';
|
||||||
import LatestSessionsMessage from './components/LatestSessionsMessage';
|
import LatestSessionsMessage from './components/LatestSessionsMessage';
|
||||||
|
|
||||||
function SessionsTabOverview() {
|
function SessionsTabOverview() {
|
||||||
const [query, setQuery] = React.useState('');
|
|
||||||
const { aiFiltersStore, searchStore } = useStore();
|
|
||||||
const appliedFilter = searchStore.instance;
|
|
||||||
usePageTitle('Sessions - OpenReplay');
|
usePageTitle('Sessions - OpenReplay');
|
||||||
|
|
||||||
const handleKeyDown = (event: any) => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
fetchResults();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const fetchResults = () => {
|
|
||||||
void aiFiltersStore.omniSearch(query, appliedFilter.toData());
|
|
||||||
};
|
|
||||||
|
|
||||||
const testingKey =
|
|
||||||
localStorage.getItem('__mauricio_testing_access') === 'true';
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NoSessionsMessage />
|
<NoSessionsMessage />
|
||||||
|
|
@ -36,15 +18,6 @@ function SessionsTabOverview() {
|
||||||
<MainSearchBar />
|
<MainSearchBar />
|
||||||
<div className="my-4" />
|
<div className="my-4" />
|
||||||
<div className="widget-wrapper">
|
<div className="widget-wrapper">
|
||||||
{testingKey ? (
|
|
||||||
<Input
|
|
||||||
value={query}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
|
||||||
className="mb-2"
|
|
||||||
placeholder="ask session ai"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<SessionHeader />
|
<SessionHeader />
|
||||||
<div className="border-b" />
|
<div className="border-b" />
|
||||||
<LatestSessionsMessage />
|
<LatestSessionsMessage />
|
||||||
|
|
@ -59,4 +32,4 @@ export default withPermissions(
|
||||||
'',
|
'',
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
)(observer(SessionsTabOverview));
|
)(SessionsTabOverview);
|
||||||
|
|
|
||||||
|
|
@ -20,73 +20,13 @@ const tagIcons = {
|
||||||
function SessionTags() {
|
function SessionTags() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const screens = useBreakpoint();
|
const screens = useBreakpoint();
|
||||||
const { projectsStore, sessionStore, searchStore } = useStore();
|
const { projectsStore, searchStore } = useStore();
|
||||||
const total = sessionStore.total;
|
|
||||||
const platform = projectsStore.active?.platform || '';
|
const platform = projectsStore.active?.platform || '';
|
||||||
const activeTab = searchStore.activeTags;
|
const activeTab = searchStore.activeTags;
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const filteredOptions = issues_types
|
React.useEffect(() => {
|
||||||
.filter(
|
searchStore.toggleTag(types.ALL);
|
||||||
(tag) =>
|
}, [projectsStore.activeSiteId])
|
||||||
tag.type !== 'mouse_thrashing' &&
|
|
||||||
(platform === 'web'
|
|
||||||
? tag.type !== types.TAP_RAGE
|
|
||||||
: tag.type !== types.CLICK_RAGE),
|
|
||||||
)
|
|
||||||
.map((tag) => ({
|
|
||||||
value: tag.type,
|
|
||||||
icon: tagIcons[tag.type],
|
|
||||||
label: t(tag.name),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Find the currently active option
|
|
||||||
const activeOption =
|
|
||||||
filteredOptions.find((option) => option.value === activeTab[0]) ||
|
|
||||||
filteredOptions[0];
|
|
||||||
|
|
||||||
// Check if on mobile
|
|
||||||
useEffect(() => {
|
|
||||||
const checkIfMobile = () => {
|
|
||||||
setIsMobile(window.innerWidth < 768);
|
|
||||||
};
|
|
||||||
|
|
||||||
checkIfMobile();
|
|
||||||
window.addEventListener('resize', checkIfMobile);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', checkIfMobile);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Close dropdown when clicking outside
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
dropdownRef.current &&
|
|
||||||
!(dropdownRef.current as HTMLElement).contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handler for dropdown item selection
|
|
||||||
const handleSelectOption = (value: string) => {
|
|
||||||
searchStore.toggleTag(value as any);
|
|
||||||
setIsDropdownOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (total === 0 && (activeTab.length === 0 || activeTab[0] === 'all')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|
|
||||||
30
frontend/app/layout/LangBanner.tsx
Normal file
30
frontend/app/layout/LangBanner.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Languages, X, Info
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { client } from 'App/routes'
|
||||||
|
|
||||||
|
function LangBanner({ onClose }: { onClose: () => void }) {
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
|
const onClick = () => {
|
||||||
|
history.push(client('account'))
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={'px-4 py-2 bg-yellow flex items-center w-screen gap-2'}>
|
||||||
|
<Info size={16} />
|
||||||
|
<div>
|
||||||
|
OpenReplay now supports French, Russian, Chinese, and Spanish 🎉. Update your language in settings.
|
||||||
|
</div>
|
||||||
|
<div className={'ml-auto'} />
|
||||||
|
<Button icon={<Languages size={14} />} size={'small'} onClick={onClick}>
|
||||||
|
Change Language
|
||||||
|
</Button>
|
||||||
|
<Button icon={<X size={16} />} type={'text'} shape={'circle'} onClick={onClose} size={'small'} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LangBanner
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Layout, Space, Tooltip } from 'antd';
|
import { Layout, Space, Tooltip } from 'antd';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
import LangBanner from './LangBanner';
|
||||||
|
|
||||||
import { INDEXES } from 'App/constants/zindex';
|
import { INDEXES } from 'App/constants/zindex';
|
||||||
import Logo from 'App/layout/Logo';
|
import Logo from 'App/layout/Logo';
|
||||||
|
|
@ -11,14 +12,27 @@ import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
const { Header } = Layout;
|
const { Header } = Layout;
|
||||||
|
|
||||||
|
const langBannerClosedKey = '__or__langBannerClosed';
|
||||||
|
const getLangBannerClosed = () => localStorage.getItem(langBannerClosedKey) === '1'
|
||||||
function TopHeader() {
|
function TopHeader() {
|
||||||
const { userStore, notificationStore, projectsStore, settingsStore } =
|
const { userStore, notificationStore, projectsStore, settingsStore } =
|
||||||
useStore();
|
useStore();
|
||||||
const { account } = userStore;
|
const { account } = userStore;
|
||||||
const { siteId } = projectsStore;
|
const { siteId } = projectsStore;
|
||||||
const { initialDataFetched } = userStore;
|
const { initialDataFetched } = userStore;
|
||||||
|
const [langBannerClosed, setLangBannerClosed] = React.useState(getLangBannerClosed);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const langBannerVal = localStorage.getItem(langBannerClosedKey);
|
||||||
|
if (langBannerVal === null) {
|
||||||
|
localStorage.setItem(langBannerClosedKey, '0')
|
||||||
|
}
|
||||||
|
if (langBannerVal === '0') {
|
||||||
|
localStorage.setItem(langBannerClosedKey, '1')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!account.id || initialDataFetched) return;
|
if (!account.id || initialDataFetched) return;
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
|
@ -29,51 +43,58 @@ function TopHeader() {
|
||||||
});
|
});
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
|
const closeLangBanner = () => {
|
||||||
|
setLangBannerClosed(true);
|
||||||
|
localStorage.setItem(langBannerClosedKey, '1');
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Header
|
<>
|
||||||
style={{
|
{langBannerClosed ? null : <LangBanner onClose={closeLangBanner} />}
|
||||||
position: 'sticky',
|
<Header
|
||||||
top: 0,
|
style={{
|
||||||
zIndex: INDEXES.HEADER,
|
position: 'sticky',
|
||||||
padding: '0 20px',
|
top: 0,
|
||||||
display: 'flex',
|
zIndex: INDEXES.HEADER,
|
||||||
alignItems: 'center',
|
padding: '0 20px',
|
||||||
height: '60px',
|
display: 'flex',
|
||||||
}}
|
alignItems: 'center',
|
||||||
className="justify-between"
|
height: '60px',
|
||||||
>
|
}}
|
||||||
<Space>
|
className="justify-between"
|
||||||
<div
|
>
|
||||||
onClick={() => {
|
<Space>
|
||||||
settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed);
|
<div
|
||||||
}}
|
onClick={() => {
|
||||||
style={{ paddingTop: '4px' }}
|
settingsStore.updateMenuCollapsed(!settingsStore.menuCollapsed);
|
||||||
className="cursor-pointer xl:block hidden"
|
}}
|
||||||
>
|
style={{ paddingTop: '4px' }}
|
||||||
<Tooltip
|
className="cursor-pointer xl:block hidden"
|
||||||
title={
|
|
||||||
settingsStore.menuCollapsed ? t('Show Menu') : t('Hide Menu')
|
|
||||||
}
|
|
||||||
mouseEnterDelay={1}
|
|
||||||
>
|
>
|
||||||
<Icon
|
<Tooltip
|
||||||
name={
|
title={
|
||||||
settingsStore.menuCollapsed
|
settingsStore.menuCollapsed ? t('Show Menu') : t('Hide Menu')
|
||||||
? 'side_menu_closed'
|
|
||||||
: 'side_menu_open'
|
|
||||||
}
|
}
|
||||||
size={20}
|
mouseEnterDelay={1}
|
||||||
/>
|
>
|
||||||
</Tooltip>
|
<Icon
|
||||||
</div>
|
name={
|
||||||
|
settingsStore.menuCollapsed
|
||||||
|
? 'side_menu_closed'
|
||||||
|
: 'side_menu_open'
|
||||||
|
}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Logo siteId={siteId} />
|
<Logo siteId={siteId} />
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
<TopRight />
|
<TopRight />
|
||||||
</Header>
|
</Header>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,13 @@ import ProjectDropdown from 'Shared/ProjectDropdown';
|
||||||
import { useStore } from 'App/mstore';
|
import { useStore } from 'App/mstore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
interface Props {
|
function TopRight() {
|
||||||
account: any;
|
|
||||||
spotOnly?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TopRight(props: Props) {
|
|
||||||
const { userStore } = useStore();
|
const { userStore } = useStore();
|
||||||
const spotOnly = userStore.scopeState === 1;
|
const spotOnly = userStore.scopeState === 1;
|
||||||
const { account } = userStore;
|
const { account } = userStore;
|
||||||
return (
|
return (
|
||||||
<Space style={{ lineHeight: '0' }}>
|
<Space style={{ lineHeight: '0' }}>
|
||||||
{props.spotOnly ? null : (
|
{spotOnly ? null : (
|
||||||
<>
|
<>
|
||||||
<ProjectDropdown />
|
<ProjectDropdown />
|
||||||
<GettingStartedProgress />
|
<GettingStartedProgress />
|
||||||
|
|
@ -30,7 +25,6 @@ function TopRight(props: Props) {
|
||||||
<Notifications />
|
<Notifications />
|
||||||
|
|
||||||
{account.name ? <HealthStatus /> : null}
|
{account.name ? <HealthStatus /> : null}
|
||||||
<LanguageSwitcher />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1498,5 +1498,8 @@
|
||||||
"More attribute": "More attribute",
|
"More attribute": "More attribute",
|
||||||
"More attributes": "More attributes",
|
"More attributes": "More attributes",
|
||||||
"Account settings updated successfully": "Account settings updated successfully",
|
"Account settings updated successfully": "Account settings updated successfully",
|
||||||
"Include rage clicks": "Include rage clicks"
|
"Include rage clicks": "Include rage clicks",
|
||||||
|
"Interface Language": "Interface Language",
|
||||||
|
"Select the language in which OpenReplay will appear.": "Select the language in which OpenReplay will appear.",
|
||||||
|
"Language": "Language"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1498,5 +1498,8 @@
|
||||||
"More attribute": "Más atributos",
|
"More attribute": "Más atributos",
|
||||||
"More attributes": "Más atributos",
|
"More attributes": "Más atributos",
|
||||||
"Account settings updated successfully": "Configuración de la cuenta actualizada correctamente",
|
"Account settings updated successfully": "Configuración de la cuenta actualizada correctamente",
|
||||||
"Include rage clicks": "Incluir clics de ira"
|
"Include rage clicks": "Incluir clics de ira",
|
||||||
|
"Interface Language": "Idioma de la interfaz",
|
||||||
|
"Select the language in which OpenReplay will appear.": "Selecciona el idioma en el que aparecerá OpenReplay.",
|
||||||
|
"Language": "Idioma"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1498,5 +1498,8 @@
|
||||||
"More attribute": "Plus d'attributs",
|
"More attribute": "Plus d'attributs",
|
||||||
"More attributes": "Plus d'attributs",
|
"More attributes": "Plus d'attributs",
|
||||||
"Account settings updated successfully": "Paramètres du compte mis à jour avec succès",
|
"Account settings updated successfully": "Paramètres du compte mis à jour avec succès",
|
||||||
"Include rage clicks": "Inclure les clics de rage"
|
"Include rage clicks": "Inclure les clics de rage",
|
||||||
|
"Interface Language": "Langue de l'interface",
|
||||||
|
"Select the language in which OpenReplay will appear.": "Sélectionnez la langue dans laquelle OpenReplay apparaîtra.",
|
||||||
|
"Language": "Langue"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1498,5 +1498,8 @@
|
||||||
"More attribute": "Еще атрибут",
|
"More attribute": "Еще атрибут",
|
||||||
"More attributes": "Еще атрибуты",
|
"More attributes": "Еще атрибуты",
|
||||||
"Account settings updated successfully": "Настройки аккаунта успешно обновлены",
|
"Account settings updated successfully": "Настройки аккаунта успешно обновлены",
|
||||||
"Include rage clicks": "Включить невыносимые клики"
|
"Include rage clicks": "Включить невыносимые клики",
|
||||||
|
"Interface Language": "Язык интерфейса",
|
||||||
|
"Select the language in which OpenReplay will appear.": "Выберите язык, на котором будет отображаться OpenReplay.",
|
||||||
|
"Language": "Язык"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1498,5 +1498,8 @@
|
||||||
"More attributes": "更多属性",
|
"More attributes": "更多属性",
|
||||||
"More attribute": "更多属性",
|
"More attribute": "更多属性",
|
||||||
"Account settings updated successfully": "帐户设置已成功更新",
|
"Account settings updated successfully": "帐户设置已成功更新",
|
||||||
"Include rage clicks": "包括点击狂怒"
|
"Include rage clicks": "包括点击狂怒",
|
||||||
|
"Interface Language": "界面语言",
|
||||||
|
"Select the language in which OpenReplay will appear.": "选择 OpenReplay 将显示的语言。",
|
||||||
|
"Language": "语言"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,18 +240,7 @@ class UserStore {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
})
|
})
|
||||||
.catch(async (e) => {
|
.catch(async (e) => {
|
||||||
const err = await e.response?.json();
|
toast.error(e.message || this.t("Failed to save user's data."));
|
||||||
runInAction(() => {
|
|
||||||
this.saving = false;
|
|
||||||
});
|
|
||||||
const errStr = err.errors[0]
|
|
||||||
? err.errors[0].includes('already exists')
|
|
||||||
? this.t(
|
|
||||||
"This email is already linked to an account or team on OpenReplay and can't be used again.",
|
|
||||||
)
|
|
||||||
: err.errors[0]
|
|
||||||
: this.t('Error saving user');
|
|
||||||
toast.error(errStr);
|
|
||||||
reject(e);
|
reject(e);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,15 @@ export function debounce(callback, wait, context = this) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function debounceCall(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
const context = this;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function randomInt(a, b) {
|
export function randomInt(a, b) {
|
||||||
const min = (b ? a : 0) - 0.5;
|
const min = (b ? a : 0) - 0.5;
|
||||||
const max = b || a || Number.MAX_SAFE_INTEGER;
|
const max = b || a || Number.MAX_SAFE_INTEGER;
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ spec:
|
||||||
value: {{ .Values.global.jwtSecret }}
|
value: {{ .Values.global.jwtSecret }}
|
||||||
- name: JWT_SPOT_SECRET
|
- name: JWT_SPOT_SECRET
|
||||||
value: {{ .Values.global.jwtSpotSecret }}
|
value: {{ .Values.global.jwtSpotSecret }}
|
||||||
|
- name: TOKEN_SECRET
|
||||||
|
value: {{ .Values.global.tokenSecret }}
|
||||||
ports:
|
ports:
|
||||||
{{- range $key, $val := .Values.service.ports }}
|
{{- range $key, $val := .Values.service.ports }}
|
||||||
- name: {{ $key }}
|
- name: {{ $key }}
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ global:
|
||||||
assistJWTSecret: "{{ randAlphaNum 20}}"
|
assistJWTSecret: "{{ randAlphaNum 20}}"
|
||||||
jwtSecret: "{{ randAlphaNum 20}}"
|
jwtSecret: "{{ randAlphaNum 20}}"
|
||||||
jwtSpotSecret: "{{ randAlphaNum 20}}"
|
jwtSpotSecret: "{{ randAlphaNum 20}}"
|
||||||
|
tokenSecret: "{{randAlphaNum 20}}"
|
||||||
# In case of multiple nodes in the kubernetes cluster,
|
# In case of multiple nodes in the kubernetes cluster,
|
||||||
# we'll have to create an RWX PVC for shared components.
|
# we'll have to create an RWX PVC for shared components.
|
||||||
# If it's a single node, we'll use hostVolume, which is the default for the community/oss edition.
|
# If it's a single node, we'll use hostVolume, which is the default for the community/oss edition.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
## 16.1.0
|
||||||
|
|
||||||
|
- new `privateMode` option to hide all possible data from tracking
|
||||||
|
|
||||||
## 16.0.3
|
## 16.0.3
|
||||||
|
|
||||||
- better handling for local svg spritemaps
|
- better handling for local svg spritemaps
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@ const config = {
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'(.+)\\.js': '$1',
|
'(.+)\\.js': '$1',
|
||||||
},
|
},
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsConfig: {
|
||||||
|
target: 'es2020',
|
||||||
|
lib: ['DOM', 'ES2022'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default config
|
export default config
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@openreplay/tracker",
|
"name": "@openreplay/tracker",
|
||||||
"description": "The OpenReplay tracker main package",
|
"description": "The OpenReplay tracker main package",
|
||||||
"version": "16.0.3",
|
"version": "16.1.0",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"logging",
|
"logging",
|
||||||
"replay"
|
"replay"
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,9 @@ export default abstract class Observer {
|
||||||
if (name === 'href' || value.length > 1e5) {
|
if (name === 'href' || value.length > 1e5) {
|
||||||
value = ''
|
value = ''
|
||||||
}
|
}
|
||||||
|
if (['alt', 'placeholder'].includes(name) && this.app.sanitizer.privateMode) {
|
||||||
|
value = value.replaceAll(/./g, '*')
|
||||||
|
}
|
||||||
this.app.attributeSender.sendSetAttribute(id, name, value)
|
this.app.attributeSender.sendSetAttribute(id, name, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -389,7 +392,7 @@ export default abstract class Observer {
|
||||||
{
|
{
|
||||||
acceptNode: (node) => {
|
acceptNode: (node) => {
|
||||||
if (this.app.nodes.getID(node) !== undefined) {
|
if (this.app.nodes.getID(node) !== undefined) {
|
||||||
this.app.debug.warn('! Node is already bound', node)
|
this.app.debug.info('! Node is already bound', node)
|
||||||
}
|
}
|
||||||
return isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
return isIgnored(node) || this.app.nodes.getID(node) !== undefined
|
||||||
? NodeFilter.FILTER_REJECT
|
? NodeFilter.FILTER_REJECT
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type App from './index.js'
|
import type App from './index.js'
|
||||||
import { stars, hasOpenreplayAttribute } from '../utils.js'
|
import { stars, hasOpenreplayAttribute } from '../utils.js'
|
||||||
import { isElementNode } from './guards.js'
|
import { isElementNode, isTextNode } from './guards.js'
|
||||||
|
|
||||||
export enum SanitizeLevel {
|
export enum SanitizeLevel {
|
||||||
Plain,
|
Plain,
|
||||||
|
|
@ -32,31 +32,46 @@ export interface Options {
|
||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
domSanitizer?: (node: Element) => SanitizeLevel
|
domSanitizer?: (node: Element) => SanitizeLevel
|
||||||
|
/**
|
||||||
|
* private by default mode that will mask all elements not marked by data-openreplay-unmask
|
||||||
|
* */
|
||||||
|
privateMode?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stringWiper = (input: string) =>
|
export const stringWiper = (input: string) =>
|
||||||
input
|
input
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█')
|
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff\s]/g, '*')
|
||||||
|
|
||||||
export default class Sanitizer {
|
export default class Sanitizer {
|
||||||
private readonly obscured: Set<number> = new Set()
|
private readonly obscured: Set<number> = new Set()
|
||||||
private readonly hidden: Set<number> = new Set()
|
private readonly hidden: Set<number> = new Set()
|
||||||
private readonly options: Options
|
private readonly options: Options
|
||||||
|
public readonly privateMode: boolean
|
||||||
private readonly app: App
|
private readonly app: App
|
||||||
|
|
||||||
constructor(params: { app: App; options?: Partial<Options> }) {
|
constructor(params: { app: App; options?: Partial<Options> }) {
|
||||||
this.app = params.app
|
this.app = params.app
|
||||||
this.options = Object.assign(
|
const defaultOptions: Options = {
|
||||||
{
|
obscureTextEmails: true,
|
||||||
obscureTextEmails: true,
|
obscureTextNumbers: false,
|
||||||
obscureTextNumbers: false,
|
privateMode: false,
|
||||||
},
|
domSanitizer: undefined,
|
||||||
params.options,
|
}
|
||||||
)
|
this.privateMode = params.options?.privateMode ?? false
|
||||||
|
this.options = Object.assign(defaultOptions, params.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNode(id: number, parentID: number, node: Node) {
|
handleNode(id: number, parentID: number, node: Node) {
|
||||||
|
if (this.options.privateMode) {
|
||||||
|
if (isElementNode(node) && !hasOpenreplayAttribute(node, 'unmask')) {
|
||||||
|
return this.obscured.add(id)
|
||||||
|
}
|
||||||
|
if (isTextNode(node) && !hasOpenreplayAttribute(node.parentNode as Element, 'unmask')) {
|
||||||
|
return this.obscured.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.obscured.has(parentID) ||
|
this.obscured.has(parentID) ||
|
||||||
(isElementNode(node) &&
|
(isElementNode(node) &&
|
||||||
|
|
|
||||||
|
|
@ -108,9 +108,13 @@ export default function (app: App, opts: Partial<Options>): void {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendConsoleLog = app.safe((level: string, args: unknown[]): void =>
|
const sendConsoleLog = app.safe((level: string, args: unknown[]): void => {
|
||||||
app.send(ConsoleLog(level, printf(args))),
|
let logMsg = printf(args)
|
||||||
)
|
if (app.sanitizer.privateMode) {
|
||||||
|
logMsg = logMsg.replaceAll(/./g, '*')
|
||||||
|
}
|
||||||
|
app.send(ConsoleLog(level, logMsg))
|
||||||
|
})
|
||||||
|
|
||||||
let n = 0
|
let n = 0
|
||||||
const reset = (): void => {
|
const reset = (): void => {
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,10 @@ export default function (app: App, opts: Partial<Options>): void {
|
||||||
inputTime: number,
|
inputTime: number,
|
||||||
) {
|
) {
|
||||||
const { value, mask } = getInputValue(id, node)
|
const { value, mask } = getInputValue(id, node)
|
||||||
const label = getInputLabel(node)
|
let label = getInputLabel(node)
|
||||||
|
if (app.sanitizer.privateMode) {
|
||||||
|
label = label.replaceAll(/./g, '*')
|
||||||
|
}
|
||||||
|
|
||||||
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime))
|
app.send(InputChange(id, value, mask !== 0, label, hesitationTime, inputTime))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,11 +230,12 @@ export default function (app: App, options?: MouseHandlerOptions): void {
|
||||||
const normalizedY = roundNumber(clickY / contentHeight)
|
const normalizedY = roundNumber(clickY / contentHeight)
|
||||||
|
|
||||||
sendMouseMove()
|
sendMouseMove()
|
||||||
|
const label = getTargetLabel(target)
|
||||||
app.send(
|
app.send(
|
||||||
MouseClick(
|
MouseClick(
|
||||||
id,
|
id,
|
||||||
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
|
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
|
||||||
getTargetLabel(target),
|
app.sanitizer.privateMode ? label.replaceAll(/./g, '*') : label,
|
||||||
isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : '',
|
isClickable(target) && !disableClickmaps ? getSelector(id, target, options) : '',
|
||||||
normalizedX,
|
normalizedX,
|
||||||
normalizedY,
|
normalizedY,
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitize(reqResInfo: RequestResponseData) {
|
function sanitize(reqResInfo: RequestResponseData) {
|
||||||
if (!options.capturePayload) {
|
if (!options.capturePayload || app.sanitizer.privateMode) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
delete reqResInfo.request.body
|
delete reqResInfo.request.body
|
||||||
delete reqResInfo.response.body
|
delete reqResInfo.response.body
|
||||||
|
|
@ -136,18 +136,19 @@ export default function (app: App, opts: Partial<Options> = {}) {
|
||||||
if (options.useProxy) {
|
if (options.useProxy) {
|
||||||
return createNetworkProxy(
|
return createNetworkProxy(
|
||||||
context,
|
context,
|
||||||
options.ignoreHeaders,
|
app.sanitizer.privateMode ? true : options.ignoreHeaders,
|
||||||
setSessionTokenHeader,
|
setSessionTokenHeader,
|
||||||
sanitize,
|
sanitize,
|
||||||
(message) => {
|
(message) => {
|
||||||
if (options.failuresOnly && message.status < 400) {
|
if (options.failuresOnly && message.status < 400) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const url = app.sanitizer.privateMode ? '************' : message.url
|
||||||
app.send(
|
app.send(
|
||||||
NetworkRequest(
|
NetworkRequest(
|
||||||
message.requestType,
|
message.requestType,
|
||||||
message.method,
|
message.method,
|
||||||
message.url,
|
url,
|
||||||
message.request,
|
message.request,
|
||||||
message.response,
|
message.response,
|
||||||
message.status,
|
message.status,
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ export default function (app: App, opts: Partial<Options>): void {
|
||||||
entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
|
entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
|
||||||
entry.encodedBodySize || 0,
|
entry.encodedBodySize || 0,
|
||||||
entry.decodedBodySize || 0,
|
entry.decodedBodySize || 0,
|
||||||
entry.name,
|
app.sanitizer.privateMode ? entry.name.replaceAll(/./g, '*') : entry.name,
|
||||||
entry.initiatorType,
|
entry.initiatorType,
|
||||||
entry.transferSize,
|
entry.transferSize,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type App from '../app/index.js'
|
import type App from '../app/index.js'
|
||||||
import { getTimeOrigin } from '../utils.js'
|
import { getTimeOrigin } from '../utils.js'
|
||||||
import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.gen.js'
|
import { SetPageLocation, SetViewportSize, SetPageVisibility } from '../app/messages.gen.js'
|
||||||
|
import { stringWiper } from '../app/sanitizer.js'
|
||||||
|
|
||||||
export default function (app: App): void {
|
export default function (app: App): void {
|
||||||
let url: string | null, width: number, height: number
|
let url: string | null, width: number, height: number
|
||||||
|
|
@ -11,7 +12,10 @@ export default function (app: App): void {
|
||||||
const { URL } = document
|
const { URL } = document
|
||||||
if (URL !== url) {
|
if (URL !== url) {
|
||||||
url = URL
|
url = URL
|
||||||
app.send(SetPageLocation(url, referrer, navigationStart, document.title))
|
const safeTitle = app.sanitizer.privateMode ? stringWiper(document.title) : document.title
|
||||||
|
const safeUrl = app.sanitizer.privateMode ? stringWiper(url) : url
|
||||||
|
const safeReferrer = app.sanitizer.privateMode ? stringWiper(referrer) : referrer
|
||||||
|
app.send(SetPageLocation(safeUrl, safeReferrer, navigationStart, safeTitle))
|
||||||
navigationStart = 0
|
navigationStart = 0
|
||||||
referrer = url
|
referrer = url
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ describe('Console logging module', () => {
|
||||||
safe: jest.fn((callback) => callback),
|
safe: jest.fn((callback) => callback),
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
attachStartCallback: jest.fn(),
|
attachStartCallback: jest.fn(),
|
||||||
|
sanitizer: {
|
||||||
|
privateMode: false,
|
||||||
|
},
|
||||||
ticker: {
|
ticker: {
|
||||||
attach: jest.fn(),
|
attach: jest.fn(),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globa
|
||||||
import Sanitizer, { SanitizeLevel, Options, stringWiper } from '../main/app/sanitizer.js'
|
import Sanitizer, { SanitizeLevel, Options, stringWiper } from '../main/app/sanitizer.js'
|
||||||
|
|
||||||
describe('stringWiper', () => {
|
describe('stringWiper', () => {
|
||||||
test('should replace all characters with █', () => {
|
test('should replace all characters with *', () => {
|
||||||
expect(stringWiper('Sensitive Data')).toBe('██████████████')
|
expect(stringWiper('Sensitive Data')).toBe('**************')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ describe('Sanitizer', () => {
|
||||||
element.mockId = 1
|
element.mockId = 1
|
||||||
element.innerText = 'Sensitive Data'
|
element.innerText = 'Sensitive Data'
|
||||||
const sanitizedText = sanitizer.getInnerTextSecure(element)
|
const sanitizedText = sanitizer.getInnerTextSecure(element)
|
||||||
expect(sanitizedText).toEqual('██████████████')
|
expect(sanitizedText).toEqual('**************')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('should return empty string if node element does not exist', () => {
|
test('should return empty string if node element does not exist', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue