Merge pull request #680 from openreplay/timeline-overview

feat(ui) - xray (timeline overview)
This commit is contained in:
Shekar Siri 2022-08-16 15:10:07 +02:00 committed by GitHub
commit a7743b426e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1927 additions and 1103 deletions

View file

@ -2,82 +2,77 @@ import React from 'react';
import { connect } from 'react-redux';
import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { errors as errorsRoute, error as errorRoute } from 'App/routes';
import { NoContent , Loader, IconButton, Icon, Popup, BackLink, } from 'UI';
import { NoContent, Loader, IconButton, Icon, Popup, BackLink } from 'UI';
import { fetch, fetchTrace } from 'Duck/errors';
import MainSection from './MainSection';
import SideSection from './SideSection';
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
@connect(state =>({
errorIdInStore: state.getIn(["errors", "instance"]).errorId,
loading: state.getIn([ "errors", "fetch", "loading" ]) || state.getIn([ "errors", "fetchTrace", "loading" ]),
errorOnFetch: state.getIn(["errors", "fetch", "errors"]) || state.getIn([ "errors", "fetchTrace", "errors" ]),
}), {
fetch,
fetchTrace,
})
@connect(
(state) => ({
errorIdInStore: state.getIn(['errors', 'instance']).errorId,
loading: state.getIn(['errors', 'fetch', 'loading']) || state.getIn(['errors', 'fetchTrace', 'loading']),
errorOnFetch: state.getIn(['errors', 'fetch', 'errors']) || state.getIn(['errors', 'fetchTrace', 'errors']),
}),
{
fetch,
fetchTrace,
}
)
@withSiteIdRouter
export default class ErrorInfo extends React.PureComponent {
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading &&
this.props.errorIdInStore !== errorId &&
errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId)
}
}
componentDidMount() {
this.ensureInstance();
}
componentDidUpdate() {
this.ensureInstance();
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex(e => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId))
}
}
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex(e => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId))
}
}
render() {
const {
loading,
errorIdInStore,
list,
errorId,
} = this.props;
ensureInstance() {
const { errorId, loading, errorOnFetch } = this.props;
if (!loading && this.props.errorIdInStore !== errorId && errorId != null) {
this.props.fetch(errorId);
this.props.fetchTrace(errorId);
}
}
componentDidMount() {
this.ensureInstance();
}
componentDidUpdate() {
this.ensureInstance();
}
next = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const next = list.get(curIndex + 1);
if (next != null) {
this.props.history.push(errorRoute(next.errorId));
}
};
prev = () => {
const { list, errorId } = this.props;
const curIndex = list.findIndex((e) => e.errorId === errorId);
const prev = list.get(curIndex - 1);
if (prev != null) {
this.props.history.push(errorRoute(prev.errorId));
}
};
render() {
const { loading, errorIdInStore, list, errorId } = this.props;
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
let nextDisabled = true,
prevDisabled = true;
if (list.size > 0) {
nextDisabled = loading || list.last().errorId === errorId;
prevDisabled = loading || list.first().errorId === errorId;
}
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
// animatedIcon="no-results"
show={ !loading && errorIdInStore == null }
>
{/* <div className="w-9/12 mb-4 flex justify-between">
return (
<NoContent
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.EMPTY_STATE} size="170" />
<div className="mt-6 text-2xl">No Error Found!</div>
</div>
}
subtext="Please try to find existing one."
// animatedIcon="no-results"
show={!loading && errorIdInStore == null}
>
{/* <div className="w-9/12 mb-4 flex justify-between">
<BackLink to={ errorsRoute() } label="Back" />
<div />
<div className="flex items-center">
@ -111,13 +106,13 @@ export default class ErrorInfo extends React.PureComponent {
</Popup>
</div>
</div> */}
<div className="flex" >
<Loader loading={ loading } className="w-9/12">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
}
}
<div className="flex">
<Loader loading={loading} className="w-9/12">
<MainSection className="w-9/12" />
<SideSection className="w-3/12" />
</Loader>
</div>
</NoContent>
);
}
}

View file

@ -5,105 +5,89 @@ import withSiteIdRouter from 'HOCs/withSiteIdRouter';
import { ErrorDetails, IconButton, Icon, Loader, Button } from 'UI';
import { sessions as sessionsRoute } from 'App/routes';
import { TYPES as EV_FILER_TYPES } from 'Types/filter/event';
import { UNRESOLVED, RESOLVED, IGNORED } from "Types/errorInfo";
import { UNRESOLVED, RESOLVED, IGNORED } from 'Types/errorInfo';
import { addFilterByKeyAndValue } from 'Duck/search';
import { resolve,unresolve,ignore, toggleFavorite } from "Duck/errors";
import { resolve, unresolve, ignore, toggleFavorite } from 'Duck/errors';
import { resentOrDate } from 'App/date';
import Divider from 'Components/Errors/ui/Divider';
import ErrorName from 'Components/Errors/ui/ErrorName';
import Label from 'Components/Errors/ui/Label';
import SharePopup from 'Shared/SharePopup'
import SharePopup from 'Shared/SharePopup';
import { FilterKey } from 'Types/filter/filterType';
import SessionBar from './SessionBar';
@withSiteIdRouter
@connect(state => ({
error: state.getIn([ "errors", "instance" ]),
trace: state.getIn([ "errors", "instanceTrace" ]),
sourcemapUploaded: state.getIn([ "errors", "sourcemapUploaded" ]),
resolveToggleLoading: state.getIn(["errors", "resolve", "loading"]) ||
state.getIn(["errors", "unresolve", "loading"]),
ignoreLoading: state.getIn([ "errors", "ignore", "loading" ]),
toggleFavoriteLoading: state.getIn([ "errors", "toggleFavorite", "loading" ]),
traceLoading: state.getIn([ "errors", "fetchTrace", "loading"]),
}),{
resolve,
unresolve,
ignore,
toggleFavorite,
addFilterByKeyAndValue,
})
@connect(
(state) => ({
error: state.getIn(['errors', 'instance']),
trace: state.getIn(['errors', 'instanceTrace']),
sourcemapUploaded: state.getIn(['errors', 'sourcemapUploaded']),
resolveToggleLoading: state.getIn(['errors', 'resolve', 'loading']) || state.getIn(['errors', 'unresolve', 'loading']),
ignoreLoading: state.getIn(['errors', 'ignore', 'loading']),
toggleFavoriteLoading: state.getIn(['errors', 'toggleFavorite', 'loading']),
traceLoading: state.getIn(['errors', 'fetchTrace', 'loading']),
}),
{
resolve,
unresolve,
ignore,
toggleFavorite,
addFilterByKeyAndValue,
}
)
export default class MainSection extends React.PureComponent {
resolve = () => {
const { error } = this.props;
this.props.resolve(error.errorId)
}
resolve = () => {
const { error } = this.props;
this.props.resolve(error.errorId);
};
unresolve = () => {
const { error } = this.props;
this.props.unresolve(error.errorId)
}
unresolve = () => {
const { error } = this.props;
this.props.unresolve(error.errorId);
};
ignore = () => {
const { error } = this.props;
this.props.ignore(error.errorId)
}
bookmark = () => {
const { error } = this.props;
this.props.toggleFavorite(error.errorId);
}
ignore = () => {
const { error } = this.props;
this.props.ignore(error.errorId);
};
bookmark = () => {
const { error } = this.props;
this.props.toggleFavorite(error.errorId);
};
findSessions = () => {
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
this.props.history.push(sessionsRoute());
}
findSessions = () => {
this.props.addFilterByKeyAndValue(FilterKey.ERROR, this.props.error.message);
this.props.history.push(sessionsRoute());
};
render() {
const {
error,
trace,
sourcemapUploaded,
ignoreLoading,
resolveToggleLoading,
toggleFavoriteLoading,
className,
traceLoading,
} = this.props;
render() {
const { error, trace, sourcemapUploaded, ignoreLoading, resolveToggleLoading, toggleFavoriteLoading, className, traceLoading } = this.props;
return (
<div className={cn(className, "bg-white border-radius-3 thin-gray-border mb-6")} >
<div className="m-4">
<ErrorName
className="text-lg leading-relaxed"
name={ error.name }
message={ error.stack0InfoString }
lineThrough={ error.status === RESOLVED }
/>
<div className="flex justify-between items-center">
<div className="flex items-center color-gray-dark" style={{ wordBreak: 'break-all'}}>
{ error.message }
</div>
<div className="text-center">
<div className="flex">
<Label
topValue={ error.sessions }
topValueSize="text-lg"
bottomValue="Sessions"
/>
<Label
topValue={ error.users }
topValueSize="text-lg"
bottomValue="Users"
/>
</div>
<div className="text-xs color-gray-medium">Over the past 30 days</div>
</div>
</div>
</div>
return (
<div className={cn(className, 'bg-white border-radius-3 thin-gray-border mb-6')}>
<div className="m-4">
<ErrorName
className="text-lg leading-relaxed"
name={error.name}
message={error.stack0InfoString}
lineThrough={error.status === RESOLVED}
/>
<div className="flex justify-between items-center">
<div className="flex items-center color-gray-dark" style={{ wordBreak: 'break-all' }}>
{error.message}
</div>
<div className="text-center">
<div className="flex">
<Label topValue={error.sessions} topValueSize="text-lg" bottomValue="Sessions" />
<Label topValue={error.users} topValueSize="text-lg" bottomValue="Users" />
</div>
<div className="text-xs color-gray-medium">Over the past 30 days</div>
</div>
</div>
</div>
{/* <Divider />
{/* <Divider />
<div className="flex m-4">
{ error.status === UNRESOLVED
? <IconButton
@ -158,35 +142,29 @@ export default class MainSection extends React.PureComponent {
}
/>
</div> */}
<Divider />
<div className="m-4">
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
<span className="font-thin text-sm">{ resentOrDate(error.lastOccurrence) }</span>
<SessionBar
className="my-4"
session={ error.lastHydratedSession }
/>
<Button
variant="text-primary"
onClick={ this.findSessions }
>
Find all sessions with this error
<Icon className="ml-1" name="next1" color="teal" />
</Button>
</div>
<Divider />
<div className="m-4">
<Loader loading={ traceLoading }>
<ErrorDetails
name={error.name}
message={error.message}
errorStack={trace}
sourcemapUploaded={sourcemapUploaded}
/>
</Loader>
</div>
</div>
);
}
}
<Divider />
<div className="m-4">
<h3 className="text-xl inline-block mr-2">Last session with this error</h3>
<span className="font-thin text-sm">{resentOrDate(error.lastOccurrence)}</span>
<SessionBar className="my-4" session={error.lastHydratedSession} />
<Button variant="text-primary" onClick={this.findSessions}>
Find all sessions with this error
<Icon className="ml-1" name="next1" color="teal" />
</Button>
</div>
<Divider />
<div className="m-4">
<Loader loading={traceLoading}>
<ErrorDetails
name={error.name}
message={error.message}
errorStack={trace}
error={error}
sourcemapUploaded={sourcemapUploaded}
/>
</Loader>
</div>
</div>
);
}
}

View file

@ -127,6 +127,7 @@ const Header = (props) => {
</div>
<ul>
<li><button onClick={ onLogoutClick }>{ 'Account' }</button></li>
<li><button onClick={ onLogoutClick }>{ 'Logout' }</button></li>
</ul>
</div>

View file

@ -49,7 +49,7 @@ function LivePlayer ({
}, [])
const TABS = {
EVENTS: 'Events',
EVENTS: 'User Actions',
HEATMAPS: 'Click Map',
}
const [activeTab, setActiveTab] = useState('');

View file

@ -14,7 +14,7 @@ import styles from '../Session_/session.module.css';
import { countDaysFrom } from 'App/date';
const TABS = {
EVENTS: 'Events',
EVENTS: 'User Actions',
HEATMAPS: 'Click Map',
};

View file

@ -3,9 +3,9 @@ import cn from 'classnames';
import stl from './bottomBlock.module.css';
const BottomBlock = ({
children,
className,
additionalHeight,
children = null,
className = '',
additionalHeight = 0,
...props
}) => (
<div className={ cn(stl.wrapper, "flex flex-col mb-2") } { ...props } >

View file

@ -197,7 +197,7 @@ export default class EventsBlock extends React.PureComponent {
setActiveTab={setActiveTab}
value={query}
header={
<div className="text-xl">User Events <span className="color-gray-medium">{ events.size }</span></div>
<div className="text-xl">User Actions <span className="color-gray-medium">{ events.size }</span></div>
}
/>
</div>

View file

@ -64,7 +64,7 @@ export default class Exceptions extends React.PureComponent {
show={ !loading && errorStack.size === 0 }
title="Nothing found!"
>
<ErrorDetails error={ currentError.name } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
<ErrorDetails error={ currentError } errorStack={errorStack} sourcemapUploaded={sourcemapUploaded} />
</NoContent>
</Loader>
</div>
@ -73,24 +73,27 @@ export default class Exceptions extends React.PureComponent {
/>
<BottomBlock>
<BottomBlock.Header>
<Input
// className="input-small"
placeholder="Filter by name or message"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
<div className="mr-8">
<QuestionMarkHint
onHover={true}
content={
<>
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/installation/upload-sourcemaps">Upload Source Maps </a>
and see source code context obtained from stack traces in their original form.
</>
}
/>
<div></div>
<div className="flex items-center">
<Input
// className="input-small"
placeholder="Filter by name or message"
icon="search"
iconPosition="left"
name="filter"
onChange={ this.onFilterChange }
/>
<div className="mx-4">
<QuestionMarkHint
onHover={true}
content={
<>
<a className="color-teal underline" target="_blank" href="https://docs.openreplay.com/installation/upload-sourcemaps">Upload Source Maps </a>
and see source code context obtained from stack traces in their original form.
</>
}
/>
</div>
</div>
</BottomBlock.Header>
<BottomBlock.Content>

View file

@ -116,7 +116,7 @@ export default class Fetch extends React.PureComponent {
/>
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">Fetch</h4>
<span className="font-semibold color-gray-medium mr-4">Fetch</span>
<div className="flex items-center">
<Input
className="input-small"

View file

@ -85,7 +85,7 @@ export default class GraphQL extends React.PureComponent {
/>
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">GraphQL</h4>
<span className="font-semibold color-gray-medium mr-4">GraphQL</span>
<div className="flex items-center">
<Input
// className="input-small"

View file

@ -50,7 +50,7 @@ export default class GraphQL extends React.PureComponent {
return (
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">Long Tasks</h4>
<span className="font-semibold color-gray-medium mr-4">Long Tasks</span>
<div className="flex items-center">
<Input
className="input-small mr-3"

View file

@ -0,0 +1,108 @@
import { connectPlayer } from 'App/player';
import { toggleBottomBlock } from 'Duck/components/player';
import React, { useEffect } from 'react';
import BottomBlock from '../BottomBlock';
import EventRow from './components/EventRow';
import { TYPES } from 'Types/session/event';
import { connect } from 'react-redux';
import TimelineScale from './components/TimelineScale';
import FeatureSelection from './components/FeatureSelection/FeatureSelection';
import TimelinePointer from './components/TimelinePointer';
import VerticalPointerLine from './components/VerticalPointerLine';
import cn from 'classnames';
// import VerticalLine from './components/VerticalLine';
import OverviewPanelContainer from './components/OverviewPanelContainer';
interface Props {
resourceList: any[];
exceptionsList: any[];
eventsList: any[];
toggleBottomBlock: any;
stackEventList: any[];
issuesList: any[];
performanceChartData: any;
endTime: number;
}
function OverviewPanel(props: Props) {
const [dataLoaded, setDataLoaded] = React.useState(false);
const [selectedFeatures, setSelectedFeatures] = React.useState(['PERFORMANCE', 'ERRORS', 'EVENTS']);
const resources: any = React.useMemo(() => {
const { resourceList, exceptionsList, eventsList, stackEventList, issuesList, performanceChartData } = props;
return {
NETWORK: resourceList,
ERRORS: exceptionsList,
EVENTS: stackEventList,
CLICKRAGE: eventsList.filter((item: any) => item.type === TYPES.CLICKRAGE),
PERFORMANCE: performanceChartData,
};
}, [dataLoaded]);
useEffect(() => {
if (dataLoaded) {
return;
}
if (props.resourceList.length > 0) {
setDataLoaded(true);
}
}, [props.resourceList]);
return (
dataLoaded && (
<Wrapper {...props}>
<BottomBlock style={{ height: '250px' }}>
<BottomBlock.Header>
<span className="font-semibold color-gray-medium mr-4">X-RAY</span>
<div className="flex items-center h-20">
<FeatureSelection list={selectedFeatures} updateList={setSelectedFeatures} />
</div>
</BottomBlock.Header>
<BottomBlock.Content>
<OverviewPanelContainer endTime={props.endTime}>
<TimelineScale endTime={props.endTime} />
<div style={{ width: '100%', height: '200px' }} className="transition relative">
<VerticalPointerLine />
{selectedFeatures.map((feature: any, index: number) => (
<div className={cn('border-b', { 'bg-white': index % 2 })}>
<EventRow
isGraph={feature === 'PERFORMANCE'}
key={feature}
title={feature}
list={resources[feature]}
renderElement={(pointer: any) => <TimelinePointer pointer={pointer} type={feature} />}
endTime={props.endTime}
/>
</div>
))}
</div>
</OverviewPanelContainer>
</BottomBlock.Content>
</BottomBlock>
</Wrapper>
)
);
}
export default connect(
(state: any) => ({
issuesList: state.getIn(['sessions', 'current', 'issues']),
}),
{
toggleBottomBlock,
}
)(
connectPlayer((state: any) => ({
resourceList: state.resourceList.filter((r: any) => r.isRed() || r.isYellow()),
exceptionsList: state.exceptionsList,
eventsList: state.eventList,
stackEventList: state.stackList,
performanceChartData: state.performanceChartData,
endTime: state.endTime,
// endTime: 30000000,
}))(OverviewPanel)
);
const Wrapper = React.memo((props: any) => {
return <div>{props.children}</div>;
});

View file

@ -0,0 +1,48 @@
import React from 'react';
import cn from 'classnames';
import { getTimelinePosition } from 'App/utils';
import { connectPlayer } from 'App/player';
import PerformanceGraph from '../PerformanceGraph';
interface Props {
list?: any[];
title: string;
className?: string;
endTime?: number;
renderElement?: (item: any) => React.ReactNode;
isGraph?: boolean;
}
const EventRow = React.memo((props: Props) => {
const { title, className, list = [], endTime = 0, isGraph = false } = props;
const scale = 100 / endTime;
const _list =
!isGraph &&
React.useMemo(() => {
return list.map((item: any, _index: number) => {
return {
...item.toJS(),
left: getTimelinePosition(item.time, scale),
};
});
}, [list]);
return (
<div className={cn('w-full flex flex-col py-2', className)} style={{ height: '60px' }}>
<div className="uppercase color-gray-medium ml-4 text-sm">{title}</div>
<div className="relative w-full">
{isGraph ? (
<PerformanceGraph list={list} />
) : (
_list.map((item: any, index: number) => {
return (
<div key={index} className="absolute" style={{ left: item.left + '%' }}>
{props.renderElement ? props.renderElement(item) : null}
</div>
);
})
)}
</div>
</div>
);
});
export default EventRow;

View file

@ -0,0 +1 @@
export { default } from './EventRow';

View file

@ -0,0 +1,45 @@
import React from 'react';
import { Checkbox } from 'UI';
const NETWORK = 'NETWORK';
const ERRORS = 'ERRORS';
const EVENTS = 'EVENTS';
const CLICKRAGE = 'CLICKRAGE';
const PERFORMANCE = 'PERFORMANCE';
interface Props {
list: any[];
updateList: any;
}
function FeatureSelection(props: Props) {
const { list } = props;
const features = [NETWORK, ERRORS, EVENTS, CLICKRAGE, PERFORMANCE];
const disabled = list.length >= 3;
return (
<React.Fragment>
{features.map((feature, index) => {
const checked = list.includes(feature);
const _disabled = disabled && !checked;
return (
<Checkbox
key={index}
label={feature}
checked={checked}
className="mx-4"
disabled={_disabled}
onClick={() => {
if (checked) {
props.updateList(list.filter((item: any) => item !== feature));
} else {
props.updateList([...list, feature]);
}
}}
/>
);
})}
</React.Fragment>
);
}
export default FeatureSelection;

View file

@ -0,0 +1,48 @@
import React from 'react';
import VerticalLine from '../VerticalLine';
import { connectPlayer, Controls } from 'App/player';
interface Props {
children: React.ReactNode;
endTime: number;
}
const OverviewPanelContainer = React.memo((props: Props) => {
const { endTime } = props;
const [mouseX, setMouseX] = React.useState(0);
const [mouseIn, setMouseIn] = React.useState(false);
const onClickTrack = (e: any) => {
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
if (time) {
Controls.jump(time);
}
};
// const onMouseMoveCapture = (e: any) => {
// if (!mouseIn) {
// return;
// }
// const p = e.nativeEvent.offsetX / e.target.offsetWidth;
// setMouseX(p * 100);
// };
return (
<div
className="overflow-x-auto overflow-y-hidden bg-gray-lightest"
onClick={onClickTrack}
// onMouseMoveCapture={onMouseMoveCapture}
// onMouseOver={() => setMouseIn(true)}
// onMouseOut={() => setMouseIn(false)}
>
{mouseIn && <VerticalLine left={mouseX} className="border-gray-medium" />}
<div className="">{props.children}</div>
</div>
);
});
export default OverviewPanelContainer;
// export default connectPlayer((state: any) => ({
// endTime: state.endTime,
// }))(OverviewPanelContainer);

View file

@ -0,0 +1 @@
export { default } from './OverviewPanelContainer';

View file

@ -0,0 +1,83 @@
import React from 'react';
import { connectPlayer } from 'App/player';
import { AreaChart, Area, Tooltip, ResponsiveContainer } from 'recharts';
interface Props {
list: any;
}
const PerformanceGraph = React.memo((props: Props) => {
const { list } = props;
const finalValues = React.useMemo(() => {
const cpuMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.cpu);
}, 0);
const cpuMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.cpu);
}, Infinity);
const memoryMin = list.reduce((acc: number, item: any) => {
return Math.min(acc, item.usedHeap);
}, Infinity);
const memoryMax = list.reduce((acc: number, item: any) => {
return Math.max(acc, item.usedHeap);
}, 0);
const convertToPercentage = (val: number, max: number, min: number) => {
return ((val - min) / (max - min)) * 100;
};
const cpuValues = list.map((item: any) => convertToPercentage(item.cpu, cpuMax, cpuMin));
const memoryValues = list.map((item: any) => convertToPercentage(item.usedHeap, memoryMax, memoryMin));
const mergeArraysWithMaxNumber = (arr1: any[], arr2: any[]) => {
const maxLength = Math.max(arr1.length, arr2.length);
const result = [];
for (let i = 0; i < maxLength; i++) {
const num = Math.round(Math.max(arr1[i] || 0, arr2[i] || 0));
result.push(num > 60 ? num : 1);
}
return result;
};
const finalValues = mergeArraysWithMaxNumber(cpuValues, memoryValues);
return finalValues;
}, []);
const data = list.map((item: any, index: number) => {
return {
time: item.time,
cpu: finalValues[index],
};
});
return (
<ResponsiveContainer height={35}>
<AreaChart
data={data}
margin={{
top: 0,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient id="cpuGradientTimeline" x1="0" y1="0" x2="0" y2="1">
<stop offset="30%" stopColor="#CC0000" stopOpacity={0.5} />
<stop offset="95%" stopColor="#3EAAAF" stopOpacity={0.8} />
</linearGradient>
</defs>
{/* <Tooltip filterNull={false} /> */}
<Area
dataKey="cpu"
baseValue={5}
type="monotone"
stroke="none"
activeDot={false}
fill="url(#cpuGradientTimeline)"
isAnimationActive={false}
/>
</AreaChart>
</ResponsiveContainer>
);
});
export default PerformanceGraph;

View file

@ -0,0 +1 @@
export { default } from './PerformanceGraph';

View file

@ -0,0 +1,32 @@
import React from 'react';
import JsonViewer from './components/JsonViewer';
import Sentry from './components/Sentry';
import { OPENREPLAY, SENTRY, DATADOG, STACKDRIVER } from 'Types/session/stackEvent';
interface Props {
event: any;
}
function StackEventModal(props: Props) {
const renderPopupContent = () => {
const {
event: { source, payload, name },
} = props;
switch (source) {
case SENTRY:
return <Sentry event={payload} />;
case DATADOG:
return <JsonViewer title={name} data={payload} icon="integrations/datadog" />;
case STACKDRIVER:
return <JsonViewer title={name} data={payload} icon="integrations/stackdriver" />;
default:
return <JsonViewer title={name} data={payload} icon={`integrations/${source}`} />;
}
};
return (
<div className="bg-white h-screen overflow-y-auto" style={{ width: '450px' }}>
{renderPopupContent()}
</div>
);
}
export default StackEventModal;

View file

@ -0,0 +1,15 @@
import React from 'react';
import { Icon, JSONTree } from 'UI';
export default class JsonViewer extends React.PureComponent {
render() {
const { data, title, icon } = this.props;
return (
<div className="p-5">
<Icon name={icon} size="30" />
<h4 className="my-5 capitalize"> {title}</h4>
<JSONTree src={data} collapsed={false} />
</div>
);
}
}

View file

@ -0,0 +1 @@
export { default } from './JsonViewer';

View file

@ -0,0 +1,67 @@
import React from 'react';
import { getIn, get } from 'immutable';
import cn from 'classnames';
import { withRequest } from 'HOCs';
import { Loader, Icon, JSONTree } from 'UI';
import { Accordion } from 'semantic-ui-react';
import stl from './sentry.module.css';
@withRequest({
endpoint: (props) => `/integrations/sentry/events/${props.event.id}`,
dataName: 'detailedEvent',
loadOnInitialize: true,
})
export default class SentryEventInfo extends React.PureComponent {
makePanelsFromStackTrace(stacktrace) {
return get(stacktrace, 'frames', []).map(({ filename, function: method, lineNo, context = [] }) => ({
key: `${filename}_${method}_${lineNo}`,
title: {
content: (
<span className={stl.accordionTitle}>
<b>{filename}</b>
{' in '}
<b>{method}</b>
{' at line '}
<b>{lineNo}</b>
</span>
),
},
content: {
content: (
<ol start={getIn(context, [0, 0], 0)} className={stl.lineList}>
{context.map(([ctxLineNo, codeText]) => (
<li className={cn(stl.codeLine, { [stl.highlighted]: ctxLineNo === lineNo })}>{codeText}</li>
))}
</ol>
),
},
}));
}
renderBody() {
const { detailedEvent, requestError, event } = this.props;
const exceptionEntry = get(detailedEvent, ['entries'], []).find(({ type }) => type === 'exception');
const stacktraces = getIn(exceptionEntry, ['data', 'values']);
if (!stacktraces) {
return <JSONTree src={requestError ? event : detailedEvent} sortKeys={false} enableClipboard />;
}
return stacktraces.map(({ type, value, stacktrace }) => (
<div key={type} className={stl.stacktrace}>
<h6>{type}</h6>
<p>{value}</p>
<Accordion styled panels={this.makePanelsFromStackTrace(stacktrace)} />
</div>
));
}
render() {
const { open, toggleOpen, loading } = this.props;
return (
<div className={stl.wrapper}>
<Icon name="integrations/sentry-text" size="30" color="gray-medium" />
<Loader loading={loading}>{this.renderBody()}</Loader>
</div>
);
}
}

View file

@ -0,0 +1 @@
export { default } from './Sentry';

View file

@ -0,0 +1,47 @@
.wrapper {
padding: 20px 40px 30px;
}
.icon {
margin-left: -5px;
}
.stacktrace {
& h6 {
display: flex;
align-items: center;
font-size: 17px;
padding-top: 7px;
margin-bottom: 10px;
}
& p {
font-family: 'Menlo', 'monaco', 'consolas', monospace;
}
}
.accordionTitle {
font-weight: 100;
& > b {
font-weight: 700;
}
}
.lineList {
list-style-position: inside;
list-style-type: decimal-leading-zero;
background: $gray-lightest;
}
.codeLine {
font-family: 'Menlo', 'monaco', 'consolas', monospace;
line-height: 24px;
font-size: 12px;
white-space: pre-wrap;
word-wrap: break-word;
min-height: 24px;
padding: 0 25px;
&.highlighted {
background: $red;
color: $white;
}
}

View file

@ -0,0 +1 @@
export { default } from './StackEventModal';

View file

@ -0,0 +1,150 @@
import React from 'react';
import { connectPlayer, Controls } from 'App/player';
import { toggleBottomBlock, NETWORK, EXCEPTIONS, PERFORMANCE } from 'Duck/components/player';
import { useModal } from 'App/components/Modal';
import { Icon, ErrorDetails, Popup } from 'UI';
import { Tooltip } from 'react-tippy';
import { TYPES as EVENT_TYPES } from 'Types/session/event';
import StackEventModal from '../StackEventModal';
import ErrorDetailsModal from 'App/components/Dashboard/components/Errors/ErrorDetailsModal';
interface Props {
pointer: any;
type: any;
}
const TimelinePointer = React.memo((props: Props) => {
const { showModal, hideModal } = useModal();
const createEventClickHandler = (pointer: any, type: any) => (e: any) => {
e.stopPropagation();
Controls.jump(pointer.time);
if (!type) {
return;
}
if (type === 'ERRORS') {
showModal(<ErrorDetailsModal errorId={pointer.errorId} />, { right: true });
}
if (type === 'EVENT') {
showModal(<StackEventModal event={pointer} />, { right: true });
}
// props.toggleBottomBlock(type);
};
const renderNetworkElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{item.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
{item.name}
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, NETWORK)} className="cursor-pointer">
<div className="h-3 w-3 rounded-full bg-red" />
</div>
</Popup>
);
};
const renderClickRageElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Click Rage'}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, null)} className="cursor-pointer">
<Icon className="bg-white" name="funnel/emoji-angry" color="red" size="16" />
</div>
</Popup>
);
};
const renderStackEventElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Stack Event'}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, 'EVENT')} className="cursor-pointer w-1 h-4 bg-red">
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
);
};
const renderPerformanceElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{item.type}</b>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, EXCEPTIONS)} className="cursor-pointer w-1 h-4 bg-red">
{/* <Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" /> */}
</div>
</Popup>
);
};
const renderExceptionElement = (item: any) => {
return (
<Popup
content={
<div className="">
<b>{'Exception'}</b>
<br />
<span>{item.message}</span>
</div>
}
delay={0}
position="top"
>
<div onClick={createEventClickHandler(item, 'ERRORS')} className="cursor-pointer">
<Icon className="rounded-full bg-white" name="funnel/exclamation-circle-fill" color="red" size="16" />
</div>
</Popup>
);
};
const render = () => {
const { pointer, type } = props;
if (type === 'NETWORK') {
return renderNetworkElement(pointer);
}
if (type === 'CLICKRAGE') {
return renderClickRageElement(pointer);
}
if (type === 'ERRORS') {
return renderExceptionElement(pointer);
}
if (type === 'EVENTS') {
return renderStackEventElement(pointer);
}
if (type === 'PERFORMANCE') {
return renderPerformanceElement(pointer);
}
};
return <div>{render()}</div>;
});
export default TimelinePointer;

View file

@ -0,0 +1 @@
export { default } from './TimelinePointer'

View file

@ -0,0 +1,60 @@
import React from 'react';
import { connectPlayer } from 'App/player';
import { millisToMinutesAndSeconds } from 'App/utils';
interface Props {
endTime: number;
}
function TimelineScale(props: Props) {
const { endTime } = props;
const scaleRef = React.useRef<HTMLDivElement>(null);
const gap = 60;
const drawScale = (container: any) => {
const width = container.offsetWidth;
const part = Math.round(width / gap);
container.replaceChildren();
for (var i = 0; i < part; i++) {
const txt = millisToMinutesAndSeconds(i * (endTime / part));
const el = document.createElement('div');
// el.style.height = '10px';
// el.style.width = '1px';
// el.style.backgroundColor = '#ccc';
el.style.position = 'absolute';
el.style.left = `${i * gap}px`;
el.style.paddingTop = '1px';
el.style.opacity = '0.8';
el.innerHTML = txt + '';
el.style.fontSize = '12px';
el.style.color = 'white';
container.appendChild(el);
}
};
React.useEffect(() => {
if (!scaleRef.current) {
return;
}
drawScale(scaleRef.current);
// const resize = () => drawScale(scaleRef.current);
// window.addEventListener('resize', resize);
// return () => {
// window.removeEventListener('resize', resize);
// };
}, [scaleRef]);
return (
<div className="h-6 bg-gray-darkest w-full" ref={scaleRef}>
{/* <div ref={scaleRef} className="w-full h-10 bg-gray-300 relative"></div> */}
</div>
);
}
export default TimelineScale;
// export default connectPlayer((state: any) => ({
// endTime: state.endTime,
// }))(TimelineScale);

View file

@ -0,0 +1 @@
export { default } from './TimelineScale';

View file

@ -0,0 +1,15 @@
import React from 'react';
import cn from 'classnames';
interface Props {
left: number;
className?: string;
height?: string;
width?: string;
}
function VerticalLine(props: Props) {
const { left, className = 'border-gray-dark', height = '221px', width = '1px' } = props;
return <div className={cn('absolute border-r border-dashed z-10', className)} style={{ left: `${left}%`, height, width }} />;
}
export default VerticalLine;

View file

@ -0,0 +1 @@
export { default } from './VerticalLine'

View file

@ -0,0 +1,18 @@
import React from 'react';
import { connectPlayer } from 'App/player';
import VerticalLine from '../VerticalLine';
interface Props {
time: number;
scale: number;
}
function VerticalPointerLine(props: Props) {
const { time, scale } = props;
const left = time * scale;
return <VerticalLine left={left} className="border-teal" />;
}
export default connectPlayer((state: any) => ({
time: state.time,
scale: 100 / state.endTime,
}))(VerticalPointerLine);

View file

@ -0,0 +1 @@
export { default } from './VerticalPointerLine'

View file

@ -0,0 +1 @@
export { default } from './OverviewPanel';

View file

@ -0,0 +1,13 @@
.popup {
max-width: 300px !important;
/* max-height: 300px !important; */
overflow: hidden;
text-overflow: ellipsis;
& span {
display: block;
max-height: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}

View file

@ -1,58 +1,51 @@
import React from 'react';
import cn from 'classnames';
import { connect } from 'react-redux';
import {
connectPlayer,
STORAGE_TYPES,
selectStorageType,
selectStorageListNow,
} from 'Player/store';
import { connectPlayer, STORAGE_TYPES, selectStorageType, selectStorageListNow } from 'Player/store';
import LiveTag from 'Shared/LiveTag';
import {
toggleTimetravel,
jumpToLive,
} from 'Player';
import { toggleTimetravel, jumpToLive } from 'Player';
import { Icon } from 'UI';
import { Icon, Button } from 'UI';
import { toggleInspectorMode } from 'Player';
import {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
OVERVIEW,
CONSOLE,
NETWORK,
STACKEVENTS,
STORAGE,
PROFILER,
PERFORMANCE,
GRAPHQL,
FETCH,
EXCEPTIONS,
INSPECTOR,
} from 'Duck/components/player';
import { AssistDuration } from './Time';
import Timeline from './Timeline';
import ControlButton from './ControlButton';
import PlayerControls from './components/PlayerControls'
import PlayerControls from './components/PlayerControls';
import styles from './controls.module.css';
import { Tooltip } from 'react-tippy';
import XRayButton from 'Shared/XRayButton';
function getStorageIconName(type) {
switch(type) {
case STORAGE_TYPES.REDUX:
return "vendors/redux";
case STORAGE_TYPES.MOBX:
return "vendors/mobx"
case STORAGE_TYPES.VUEX:
return "vendors/vuex";
case STORAGE_TYPES.NGRX:
return "vendors/ngrx";
case STORAGE_TYPES.NONE:
return "store"
}
switch (type) {
case STORAGE_TYPES.REDUX:
return 'vendors/redux';
case STORAGE_TYPES.MOBX:
return 'vendors/mobx';
case STORAGE_TYPES.VUEX:
return 'vendors/vuex';
case STORAGE_TYPES.NGRX:
return 'vendors/ngrx';
case STORAGE_TYPES.NONE:
return 'store';
}
}
const SKIP_INTERVALS = {
@ -66,296 +59,301 @@ const SKIP_INTERVALS = {
};
function getStorageName(type) {
switch(type) {
case STORAGE_TYPES.REDUX:
return "REDUX";
case STORAGE_TYPES.MOBX:
return "MOBX";
case STORAGE_TYPES.VUEX:
return "VUEX";
case STORAGE_TYPES.NGRX:
return "NGRX";
case STORAGE_TYPES.NONE:
return "STATE";
}
switch (type) {
case STORAGE_TYPES.REDUX:
return 'REDUX';
case STORAGE_TYPES.MOBX:
return 'MOBX';
case STORAGE_TYPES.VUEX:
return 'VUEX';
case STORAGE_TYPES.NGRX:
return 'NGRX';
case STORAGE_TYPES.NONE:
return 'STATE';
}
}
@connectPlayer(state => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
@connectPlayer((state) => ({
time: state.time,
endTime: state.endTime,
live: state.live,
livePlay: state.livePlay,
playing: state.playing,
completed: state.completed,
skip: state.skip,
skipToIssue: state.skipToIssue,
speed: state.speed,
disabled: state.cssLoading || state.messagesLoading || state.inspectorMode || state.markedTargets,
inspectorMode: state.inspectorMode,
fullscreenDisabled: state.messagesLoading,
logCount: state.logListNow.length,
logRedCount: state.logRedCountNow,
resourceRedCount: state.resourceRedCountNow,
fetchRedCount: state.fetchRedCountNow,
showStack: state.stackList.length > 0,
stackCount: state.stackListNow.length,
stackRedCount: state.stackRedCountNow,
profilesCount: state.profilesListNow.length,
storageCount: selectStorageListNow(state).length,
storageType: selectStorageType(state),
showStorage: selectStorageType(state) !== STORAGE_TYPES.NONE,
showProfiler: state.profilesList.length > 0,
showGraphql: state.graphqlList.length > 0,
showFetch: state.fetchCount > 0,
fetchCount: state.fetchCountNow,
graphqlCount: state.graphqlListNow.length,
exceptionsCount: state.exceptionsListNow.length,
showExceptions: state.exceptionsList.length > 0,
showLongtasks: state.longtasksList.length > 0,
liveTimeTravel: state.liveTimeTravel,
}))
@connect((state, props) => {
const permissions = state.getIn([ 'user', 'account', 'permissions' ]) || [];
const isEnterprise = state.getIn([ 'user', 'account', 'edition' ]) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn([ 'components', 'player', 'fullscreen' ]),
bottomBlock: state.getIn([ 'components', 'player', 'bottomBlock' ]),
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive: !!state.getIn([ 'sessions', 'errors' ]) || !state.getIn([ 'sessions', 'current', 'live' ]),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
}
}, {
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,
})
@connect(
(state, props) => {
const permissions = state.getIn(['user', 'account', 'permissions']) || [];
const isEnterprise = state.getIn(['user', 'account', 'edition']) === 'ee';
return {
disabled: props.disabled || (isEnterprise && !permissions.includes('DEV_TOOLS')),
fullscreen: state.getIn(['components', 'player', 'fullscreen']),
bottomBlock: state.getIn(['components', 'player', 'bottomBlock']),
showStorage: props.showStorage || !state.getIn(['components', 'player', 'hiddenHints', 'storage']),
showStack: props.showStack || !state.getIn(['components', 'player', 'hiddenHints', 'stack']),
closedLive: !!state.getIn(['sessions', 'errors']) || !state.getIn(['sessions', 'current', 'live']),
skipInterval: state.getIn(['components', 'player', 'skipInterval']),
};
},
{
fullscreenOn,
fullscreenOff,
toggleBottomBlock,
changeSkipInterval,}
)
export default class Controls extends React.Component {
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel ||
nextProps.skipInterval !== this.props.skipInterval
) return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
componentDidMount() {
document.addEventListener('keydown', this.onKeyDown);
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onKeyDown);
//this.props.toggleInspectorMode(false);
}
shouldComponentUpdate(nextProps) {
if (
nextProps.fullscreen !== this.props.fullscreen ||
nextProps.bottomBlock !== this.props.bottomBlock ||
nextProps.live !== this.props.live ||
nextProps.livePlay !== this.props.livePlay ||
nextProps.playing !== this.props.playing ||
nextProps.completed !== this.props.completed ||
nextProps.skip !== this.props.skip ||
nextProps.skipToIssue !== this.props.skipToIssue ||
nextProps.speed !== this.props.speed ||
nextProps.disabled !== this.props.disabled ||
nextProps.fullscreenDisabled !== this.props.fullscreenDisabled ||
// nextProps.inspectorMode !== this.props.inspectorMode ||
nextProps.logCount !== this.props.logCount ||
nextProps.logRedCount !== this.props.logRedCount ||
nextProps.resourceRedCount !== this.props.resourceRedCount ||
nextProps.fetchRedCount !== this.props.fetchRedCount ||
nextProps.showStack !== this.props.showStack ||
nextProps.stackCount !== this.props.stackCount ||
nextProps.stackRedCount !== this.props.stackRedCount ||
nextProps.profilesCount !== this.props.profilesCount ||
nextProps.storageCount !== this.props.storageCount ||
nextProps.storageType !== this.props.storageType ||
nextProps.showStorage !== this.props.showStorage ||
nextProps.showProfiler !== this.props.showProfiler ||
nextProps.showGraphql !== this.props.showGraphql ||
nextProps.showFetch !== this.props.showFetch ||
nextProps.fetchCount !== this.props.fetchCount ||
nextProps.graphqlCount !== this.props.graphqlCount ||
nextProps.showExceptions !== this.props.showExceptions ||
nextProps.exceptionsCount !== this.props.exceptionsCount ||
nextProps.showLongtasks !== this.props.showLongtasks ||
nextProps.liveTimeTravel !== this.props.liveTimeTravel||
nextProps.skipInterval !== this.props.skipInterval)
return true;
return false;
}
onKeyDown = (e) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
return;
}
if (this.props.inspectorMode) {
if (e.key === 'Esc' || e.key === 'Escape') {
toggleInspectorMode(false);
}
}
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === 'ArrowRight') {
this.forthTenSeconds();
}
if (e.key === 'ArrowLeft') {
this.backTenSeconds();
}
if (e.key === 'ArrowDown') {
this.props.speedDown();
}
if (e.key === 'ArrowUp') {
this.props.speedUp();
}
};
// if (e.key === ' ') {
// document.activeElement.blur();
// this.props.togglePlay();
// }
if (e.key === 'Esc' || e.key === 'Escape') {
this.props.fullscreenOff();
}
if (e.key === "ArrowRight") {
this.forthTenSeconds();
}
if (e.key === "ArrowLeft") {
this.backTenSeconds();
}
if (e.key === "ArrowDown") {
this.props.speedDown();
}
if (e.key === "ArrowUp") {
this.props.speedUp();
}
}
forthTenSeconds = () => {
const { time, endTime, jump, skipInterval } = this.props;
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]))
}
forthTenSeconds = () => {
const { time, endTime, jump, skipInterval } = this.props;
jump(Math.min(endTime, time + SKIP_INTERVALS[skipInterval]));
};
backTenSeconds = () => { //shouldComponentUpdate
const { time, jump, skipInterval } = this.props;
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
}
backTenSeconds = () => {
//shouldComponentUpdate
const { time, jump, skipInterval } = this.props;
jump(Math.max(0, time - SKIP_INTERVALS[skipInterval]));
};
goLive =() => this.props.jump(this.props.endTime)
goLive = () => this.props.jump(this.props.endTime);
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session'
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play'
}
renderPlayBtn = () => {
const { completed, playing } = this.props;
let label;
let icon;
if (completed) {
icon = 'arrow-clockwise';
label = 'Replay this session';
} else if (playing) {
icon = 'pause-fill';
label = 'Pause';
} else {
icon = 'play-fill-new';
label = 'Pause';
label = 'Play';
}
return (
<Tooltip
delay={0}
position="top"
title={label}
interactive
hideOnClick="persistent"
className="mr-4"
>
return (
<Tooltip delay={0} position="top" title={label} interactive hideOnClick="persistent" className="mr-4">
<div onClick={this.props.togglePlay} className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade">
<Icon name={icon} size="36" color="inherit" />
</div>
</Tooltip>
);
};
controlIcon = (icon, size, action, isBackwards, additionalClasses) => (
<div
onClick={this.props.togglePlay}
className="hover-main color-main cursor-pointer rounded hover:bg-gray-light-shade"
onClick={action}
className={cn('py-1 px-2 hover-main cursor-pointer', additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size="36" color="inherit" />
<Icon name={icon} size={size} color="inherit" />
</div>
</Tooltip>
)
}
);
controlIcon = (icon, size, action, isBackwards, additionalClasses) =>
<div
onClick={ action }
className={cn("py-1 px-2 hover-main cursor-pointer", additionalClasses)}
style={{ transform: isBackwards ? 'rotate(180deg)' : '' }}
>
<Icon name={icon} size={size} color="inherit" />
</div>
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
changeSkipInterval,
render() {
const {
bottomBlock,
toggleBottomBlock,
live,
livePlay,
skip,
speed,
disabled,
logCount,
logRedCount,
resourceRedCount,
fetchRedCount,
showStack,
stackCount,
stackRedCount,
profilesCount,
storageCount,
showStorage,
storageType,
showProfiler,
showGraphql,
showFetch,
fetchCount,
graphqlCount,
exceptionsCount,
showExceptions,
fullscreen,
inspectorMode,
closedLive,
toggleSpeed,
toggleSkip,
liveTimeTravel,
changeSkipInterval,
skipInterval,
} = this.props;
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
}
const toggleBottomTools = (blockName) => {
if (blockName === INSPECTOR) {
toggleInspectorMode();
bottomBlock && toggleBottomBlock();
} else {
toggleInspectorMode(false);
toggleBottomBlock(blockName);
}
};
return (
<div className={ styles.controls }>
{ !live || liveTimeTravel ? <Timeline jump={ this.props.jump } liveTimeTravel={liveTimeTravel} pause={this.props.pause} togglePlay={this.props.togglePlay} /> : null}
{ !fullscreen &&
<div className={ cn(styles.buttons, {'!px-5 !pt-0' : live}) } data-is-live={ live }>
<div>
{!live && (
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
ref={this.speedRef}
return (
<div className={styles.controls}>
{!live || liveTimeTravel ? (
<Timeline jump={this.props.jump} liveTimeTravel={liveTimeTravel} pause={this.props.pause} togglePlay={this.props.togglePlay} />
) : null}
{!fullscreen && (
<div className={cn(styles.buttons, { '!px-5 !pt-0': live })} data-is-live={live}>
<div className="flex items-center">
{!live && (
<>
<PlayerControls
live={live}
skip={skip}
speed={speed}
disabled={disabled}
backTenSeconds={this.backTenSeconds}
forthTenSeconds={this.forthTenSeconds}
toggleSpeed={toggleSpeed}
toggleSkip={toggleSkip}
playButton={this.renderPlayBtn()}
controlIcon={this.controlIcon}
ref={this.speedRef}
skipIntervals={SKIP_INTERVALS}
setSkipInterval={changeSkipInterval}
currentInterval={skipInterval}
/>
)}
currentInterval={skipInterval}/>
{/* <Button variant="text" onClick={() => toggleBottomTools(OVERVIEW)}>X-RAY</Button> */}
<div className={cn('h-14 border-r bg-gray-light mx-6')} />
<XRayButton isActive={bottomBlock === OVERVIEW && !inspectorMode} onClick={() => toggleBottomTools(OVERVIEW)} />
</>
)}
{ live && !closedLive && (
<div className={ styles.buttonsLeft }>
<LiveTag isLive={livePlay} onClick={() => livePlay ? null : jumpToLive()} />
<div className="font-semibold px-2"><AssistDuration isLivePlay={livePlay} /></div>
{live && !closedLive && (
<div className={styles.buttonsLeft}>
<LiveTag isLive={livePlay} onClick={() => (livePlay ? null : jumpToLive())} />
<div className="font-semibold px-2">
<AssistDuration isLivePlay={livePlay} />
</div>
{!liveTimeTravel && (
<div onClick={toggleTimetravel} className="p-2 ml-2 rounded hover:bg-teal-light bg-gray-lightest cursor-pointer">
See Past Activity
</div>
)}
</div>
)}
</div>
{!liveTimeTravel && (
<div
onClick={toggleTimetravel}
className="p-2 ml-2 rounded hover:bg-teal-light bg-gray-lightest cursor-pointer"
>
See Past Activity
</div>
)}
</div>
)}
</div>
<div className="flex items-center h-full">
{ !live && <div className={cn(styles.divider, 'h-full')} /> }
{/* ! TEMP DISABLED !
<div className="flex items-center h-full">
{/* { !live && <div className={cn(styles.divider, 'h-full')} /> } */}
{/* ! TEMP DISABLED !
{!live && (
<ControlButton
disabled={ disabled && !inspectorMode }
@ -367,131 +365,142 @@ export default class Controls extends React.Component {
containerClassName="mx-2"
/>
)} */}
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(CONSOLE) }
active={ bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
{/* <ControlButton
// disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(OVERVIEW) }
active={ bottomBlock === OVERVIEW && !inspectorMode}
label="OVERVIEW"
noIcon
labelClassName="!text-base font-semibold"
count={ logCount }
hasErrors={ logRedCount > 0 }
// count={ logCount }
// hasErrors={ logRedCount > 0 }
containerClassName="mx-2"
/>
{ !live &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(NETWORK) }
active={ bottomBlock === NETWORK && !inspectorMode }
label="NETWORK"
hasErrors={ resourceRedCount > 0 }
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{!live &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(PERFORMANCE) }
active={ bottomBlock === PERFORMANCE && !inspectorMode }
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{showFetch &&
<ControlButton
disabled={disabled && !inspectorMode}
onClick={ ()=> toggleBottomTools(FETCH) }
active={ bottomBlock === FETCH && !inspectorMode }
hasErrors={ fetchRedCount > 0 }
count={ fetchCount }
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ !live && showGraphql &&
<ControlButton
disabled={disabled && !inspectorMode}
onClick={ ()=> toggleBottomTools(GRAPHQL) }
active={ bottomBlock === GRAPHQL && !inspectorMode }
count={ graphqlCount }
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ !live && showStorage &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(STORAGE) }
active={ bottomBlock === STORAGE && !inspectorMode }
count={ storageCount }
label={ getStorageName(storageType) }
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ showExceptions &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(EXCEPTIONS) }
active={ bottomBlock === EXCEPTIONS && !inspectorMode }
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={ exceptionsCount }
hasErrors={ exceptionsCount > 0 }
/>
}
{ !live && showStack &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(STACKEVENTS) }
active={ bottomBlock === STACKEVENTS && !inspectorMode }
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={ stackCount }
hasErrors={ stackRedCount > 0 }
/>
}
{ !live && showProfiler &&
<ControlButton
disabled={ disabled && !inspectorMode }
onClick={ () => toggleBottomTools(PROFILER) }
active={ bottomBlock === PROFILER && !inspectorMode }
count={ profilesCount }
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
}
{ !live && <div className={cn(styles.divider, 'h-full')} /> }
{ !live && (
<Tooltip
title="Fullscreen"
delay={0}
position="top-end"
className="mx-4"
>
{this.controlIcon("arrows-angle-extend", 18, this.props.fullscreenOn, false, "rounded hover:bg-gray-light-shade color-gray-medium")}
</Tooltip>
)
}
/> */}
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(CONSOLE)}
active={bottomBlock === CONSOLE && !inspectorMode}
label="CONSOLE"
noIcon
labelClassName="!text-base font-semibold"
count={logCount}
hasErrors={logRedCount > 0}
containerClassName="mx-2"
/>
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(NETWORK)}
active={bottomBlock === NETWORK && !inspectorMode}
label="NETWORK"
hasErrors={resourceRedCount > 0}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PERFORMANCE)}
active={bottomBlock === PERFORMANCE && !inspectorMode}
label="PERFORMANCE"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showFetch && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(FETCH)}
active={bottomBlock === FETCH && !inspectorMode}
hasErrors={fetchRedCount > 0}
count={fetchCount}
label="FETCH"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showGraphql && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(GRAPHQL)}
active={bottomBlock === GRAPHQL && !inspectorMode}
count={graphqlCount}
label="GRAPHQL"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && showStorage && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STORAGE)}
active={bottomBlock === STORAGE && !inspectorMode}
count={storageCount}
label={getStorageName(storageType)}
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{showExceptions && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(EXCEPTIONS)}
active={bottomBlock === EXCEPTIONS && !inspectorMode}
label="EXCEPTIONS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={exceptionsCount}
hasErrors={exceptionsCount > 0}
/>
)}
{!live && showStack && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(STACKEVENTS)}
active={bottomBlock === STACKEVENTS && !inspectorMode}
label="EVENTS"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
count={stackCount}
hasErrors={stackRedCount > 0}
/>
)}
{!live && showProfiler && (
<ControlButton
disabled={disabled && !inspectorMode}
onClick={() => toggleBottomTools(PROFILER)}
active={bottomBlock === PROFILER && !inspectorMode}
count={profilesCount}
label="PROFILER"
noIcon
labelClassName="!text-base font-semibold"
containerClassName="mx-2"
/>
)}
{!live && <div className={cn(styles.divider, 'h-full')} />}
{!live && (
<Tooltip title="Fullscreen" delay={0} position="top-end" className="mx-4">
{this.controlIcon(
'arrows-angle-extend',
18,
this.props.fullscreenOn,
false,
'rounded hover:bg-gray-light-shade color-gray-medium'
)}
</Tooltip>
)}
</div>
</div>
)}
</div>
</div>
}
</div>
);
}
);
}
}

View file

@ -13,393 +13,392 @@ import { debounce } from 'App/utils';
import { Tooltip } from 'react-tippy';
import TooltipContainer from './components/TooltipContainer';
const BOUNDRY = 15
const BOUNDRY = 0;
function getTimelinePosition(value, scale) {
const pos = value * scale;
const pos = value * scale;
return pos > 100 ? 100 : pos;
return pos > 100 ? 100 : pos;
}
const getPointerIcon = (type) => {
// exception,
switch(type) {
case 'fetch':
return 'funnel/file-earmark-minus-fill';
case 'exception':
return 'funnel/exclamation-circle-fill';
case 'log':
return 'funnel/exclamation-circle-fill';
case 'stack':
return 'funnel/patch-exclamation-fill';
case 'resource':
return 'funnel/file-earmark-minus-fill';
// exception,
switch (type) {
case 'fetch':
return 'funnel/file-earmark-minus-fill';
case 'exception':
return 'funnel/exclamation-circle-fill';
case 'log':
return 'funnel/exclamation-circle-fill';
case 'stack':
return 'funnel/patch-exclamation-fill';
case 'resource':
return 'funnel/file-earmark-minus-fill';
case 'dead_click':
return 'funnel/dizzy';
case 'click_rage':
return 'funnel/dizzy';
case 'excessive_scrolling':
return 'funnel/mouse';
case 'bad_request':
return 'funnel/file-medical-alt';
case 'missing_resource':
return 'funnel/file-earmark-minus-fill';
case 'memory':
return 'funnel/sd-card';
case 'cpu':
return 'funnel/microchip';
case 'slow_resource':
return 'funnel/hourglass-top';
case 'slow_page_load':
return 'funnel/hourglass-top';
case 'crash':
return 'funnel/file-exclamation';
case 'js_exception':
return 'funnel/exclamation-circle-fill';
}
return 'info';
}
case 'dead_click':
return 'funnel/dizzy';
case 'click_rage':
return 'funnel/dizzy';
case 'excessive_scrolling':
return 'funnel/mouse';
case 'bad_request':
return 'funnel/file-medical-alt';
case 'missing_resource':
return 'funnel/file-earmark-minus-fill';
case 'memory':
return 'funnel/sd-card';
case 'cpu':
return 'funnel/microchip';
case 'slow_resource':
return 'funnel/hourglass-top';
case 'slow_page_load':
return 'funnel/hourglass-top';
case 'crash':
return 'funnel/file-exclamation';
case 'js_exception':
return 'funnel/exclamation-circle-fill';
}
return 'info';
};
let deboucneJump = () => null;
let debounceTooltipChange = () => null;
@connectPlayer(state => ({
playing: state.playing,
time: state.time,
skipIntervals: state.skipIntervals,
events: state.eventList,
skip: state.skip,
// not updating properly rn
// skipToIssue: state.skipToIssue,
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
endTime: state.endTime,
live: state.live,
logList: state.logList,
exceptionsList: state.exceptionsList,
resourceList: state.resourceList,
stackList: state.stackList,
fetchList: state.fetchList,
@connectPlayer((state) => ({
playing: state.playing,
time: state.time,
skipIntervals: state.skipIntervals,
events: state.eventList,
skip: state.skip,
// not updating properly rn
// skipToIssue: state.skipToIssue,
disabled: state.cssLoading || state.messagesLoading || state.markedTargets,
endTime: state.endTime,
live: state.live,
logList: state.logList,
exceptionsList: state.exceptionsList,
resourceList: state.resourceList,
stackList: state.stackList,
fetchList: state.fetchList,
}))
@connect(state => ({
issues: state.getIn([ 'sessions', 'current', 'issues' ]),
clickRageTime: state.getIn([ 'sessions', 'current', 'clickRage' ]) &&
state.getIn([ 'sessions', 'current', 'clickRageTime' ]),
returningLocationTime: state.getIn([ 'sessions', 'current', 'returningLocation' ]) &&
state.getIn([ 'sessions', 'current', 'returningLocationTime' ]),
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible'])
}), { setTimelinePointer, setTimelineHoverTime })
@connect(
(state) => ({
issues: state.getIn(['sessions', 'current', 'issues']),
clickRageTime: state.getIn(['sessions', 'current', 'clickRage']) && state.getIn(['sessions', 'current', 'clickRageTime']),
returningLocationTime:
state.getIn(['sessions', 'current', 'returningLocation']) && state.getIn(['sessions', 'current', 'returningLocationTime']),
tooltipVisible: state.getIn(['sessions', 'timeLineTooltip', 'isVisible']),
}),
{ setTimelinePointer, setTimelineHoverTime }
)
export default class Timeline extends React.PureComponent {
progressRef = React.createRef()
timelineRef = React.createRef()
wasPlaying = false
progressRef = React.createRef();
timelineRef = React.createRef();
wasPlaying = false;
seekProgress = (e) => {
const time = this.getTime(e)
this.props.jump(time);
this.hideTimeTooltip()
}
seekProgress = (e) => {
const time = this.getTime(e);
this.props.jump(time);
this.hideTimeTooltip();
};
getTime = (e) => {
const { endTime } = this.props;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
getTime = (e) => {
const { endTime } = this.props;
const p = e.nativeEvent.offsetX / e.target.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
return time
}
return time;
};
createEventClickHandler = pointer => (e) => {
e.stopPropagation();
this.props.jump(pointer.time);
this.props.setTimelinePointer(pointer);
}
createEventClickHandler = (pointer) => (e) => {
e.stopPropagation();
this.props.jump(pointer.time);
this.props.setTimelinePointer(pointer);
};
componentDidMount() {
const { issues } = this.props;
const skipToIssue = Controls.updateSkipToIssue();
const firstIssue = issues.get(0);
deboucneJump = debounce(this.props.jump, 500);
debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50);
componentDidMount() {
const { issues } = this.props;
const skipToIssue = Controls.updateSkipToIssue();
const firstIssue = issues.get(0);
deboucneJump = debounce(this.props.jump, 500);
debounceTooltipChange = debounce(this.props.setTimelineHoverTime, 50);
if (firstIssue && skipToIssue) {
this.props.jump(firstIssue.time);
if (firstIssue && skipToIssue) {
this.props.jump(firstIssue.time);
}
}
}
onDragEnd = () => {
if (this.wasPlaying) {
this.props.togglePlay();
onDragEnd = () => {
if (this.wasPlaying) {
this.props.togglePlay();
}
};
onDrag = (offset) => {
const { endTime } = this.props;
const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
deboucneJump(time);
this.hideTimeTooltip();
if (this.props.playing) {
this.wasPlaying = true;
this.props.pause();
}
};
showTimeTooltip = (e) => {
if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) {
return this.props.tooltipVisible && this.hideTimeTooltip();
}
const time = this.getTime(e);
const { endTime, liveTimeTravel } = this.props;
const timeLineTooltip = {
time: liveTimeTravel ? endTime - time : time,
offset: e.nativeEvent.offsetX,
isVisible: true,
};
debounceTooltipChange(timeLineTooltip);
};
hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false };
debounceTooltipChange(timeLineTooltip);
};
render() {
const {
events,
skip,
skipIntervals,
disabled,
endTime,
exceptionsList,
resourceList,
clickRageTime,
stackList,
fetchList,
issues,
liveTimeTravel,
} = this.props;
const scale = 100 / endTime;
return (
<div className="flex items-center absolute w-full" style={{ top: '-4px', zIndex: 100, padding: `0 ${BOUNDRY}px`, maxWidth: '100%' }}>
<div
className={stl.progress}
onClick={disabled ? null : this.seekProgress}
ref={this.progressRef}
role="button"
onMouseMoveCapture={this.showTimeTooltip}
onMouseEnter={this.showTimeTooltip}
onMouseLeave={this.hideTimeTooltip}
>
<TooltipContainer liveTimeTravel={liveTimeTravel} />
{/* custo color is live */}
<DraggableCircle left={this.props.time * scale} onDrop={this.onDragEnd} />
<CustomDragLayer
onDrag={this.onDrag}
minX={BOUNDRY}
maxX={this.progressRef.current && this.progressRef.current.offsetWidth + BOUNDRY}
/>
<TimeTracker scale={scale} />
{skip &&
skipIntervals.map((interval) => (
<div
key={interval.start}
className={stl.skipInterval}
style={{
left: `${getTimelinePosition(interval.start, scale)}%`,
width: `${(interval.end - interval.start) * scale}%`,
}}
/>
))}
<div className={stl.timeline} ref={this.timelineRef} />
{events.map((e) => (
<div key={e.key} className={stl.event} style={{ left: `${getTimelinePosition(e.time, scale)}%` }} />
))}
{/* {issues.map((iss) => (
<div
style={{
left: `${getTimelinePosition(iss.time, scale)}%`,
top: '0px',
zIndex: 11,
width: 16,
height: 16,
}}
key={iss.key}
className={stl.clickRage}
onClick={this.createEventClickHandler(iss)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{iss.name}</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={iss.icon} size="16" />
</Tooltip>
</div>
))}
{events
.filter((e) => e.type === TYPES.CLICKRAGE)
.map((e) => (
<div
style={{
left: `${getTimelinePosition(e.time, scale)}%`,
top: '0px',
zIndex: 11,
width: 16,
height: 16,
}}
key={e.key}
className={stl.clickRage}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{'Click Rage'}</b>
</div>
}
>
<Icon className="bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
))}
{typeof clickRageTime === 'number' && (
<div
style={{
left: `${getTimelinePosition(clickRageTime, scale)}%`,
top: '-0px',
zIndex: 11,
width: 16,
height: 16,
}}
className={stl.clickRage}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{'Click Rage'}</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
)}
{exceptionsList.map((e) => (
<div
key={e.key}
className={cn(stl.markup, stl.error)}
style={{ left: `${getTimelinePosition(e.time, scale)}%`, top: '0px', zIndex: 10, width: 16, height: 16 }}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{'Exception'}</b>
<br />
<span>{e.message}</span>
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('exception')} color="red" size="16" />
</Tooltip>
</div>
))}
{resourceList
.filter((r) => r.isRed() || r.isYellow())
.map((r) => (
<div
key={r.key}
className={cn(stl.markup, {
[stl.error]: r.isRed(),
[stl.warning]: r.isYellow(),
})}
style={{ left: `${getTimelinePosition(r.time, scale)}%`, top: '0px', zIndex: 10, width: 16, height: 16 }}
onClick={this.createEventClickHandler(r)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>{r.success ? 'Slow resource: ' : 'Missing resource:'}</b>
<br />
{r.name}
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('resource')} size="16" />
</Tooltip>
</div>
))}
{fetchList
.filter((e) => e.isRed())
.map((e) => (
<div
key={e.key}
className={cn(stl.markup, stl.error)}
style={{ left: `${getTimelinePosition(e.time, scale)}%`, top: '0px' }}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>Failed Fetch</b>
<br />
{e.name}
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('fetch')} color="red" size="16" />
</Tooltip>
</div>
))}
{stackList
.filter((e) => e.isRed())
.map((e) => (
<div
key={e.key}
className={cn(stl.markup, stl.error)}
style={{ left: `${getTimelinePosition(e.time, scale)}%`, top: '0px' }}
onClick={this.createEventClickHandler(e)}
>
<Tooltip
delay={0}
position="top"
html={
<div className={stl.popup}>
<b>Stack Event</b>
<br />
{e.name}
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('stack')} size="16" />
</Tooltip>
</div>
))} */}
</div>
</div>
);
}
}
onDrag = (offset) => {
const { endTime } = this.props;
const p = (offset.x - BOUNDRY) / this.progressRef.current.offsetWidth;
const time = Math.max(Math.round(p * endTime), 0);
deboucneJump(time);
this.hideTimeTooltip();
if (this.props.playing) {
this.wasPlaying = true;
this.props.pause();
}
}
showTimeTooltip = (e) => {
if (e.target !== this.progressRef.current && e.target !== this.timelineRef.current) {
return this.props.tooltipVisible && this.hideTimeTooltip()
}
const time = this.getTime(e);
const { endTime, liveTimeTravel } = this.props;
const timeLineTooltip = {
time: liveTimeTravel ? endTime - time : time,
offset: e.nativeEvent.offsetX,
isVisible: true
}
debounceTooltipChange(timeLineTooltip)
}
hideTimeTooltip = () => {
const timeLineTooltip = { isVisible: false }
debounceTooltipChange(timeLineTooltip)
}
render() {
const {
events,
skip,
skipIntervals,
disabled,
endTime,
exceptionsList,
resourceList,
clickRageTime,
stackList,
fetchList,
issues,
liveTimeTravel,
} = this.props;
const scale = 100 / endTime;
return (
<div
className="flex items-center absolute w-full"
style={{ top: '-4px', zIndex: 100, padding: `0 ${BOUNDRY}px`, maxWidth: '100%' }}
>
<div
className={ stl.progress }
onClick={ disabled ? null : this.seekProgress }
ref={ this.progressRef }
role="button"
onMouseMoveCapture={this.showTimeTooltip}
onMouseEnter={ this.showTimeTooltip}
onMouseLeave={this.hideTimeTooltip}
>
<TooltipContainer liveTimeTravel={liveTimeTravel} />
{/* custo color is live */}
<DraggableCircle left={this.props.time * scale} onDrop={this.onDragEnd} />
<CustomDragLayer onDrag={this.onDrag} minX={BOUNDRY} maxX={this.progressRef.current && this.progressRef.current.offsetWidth + BOUNDRY} />
<TimeTracker scale={ scale } />
{ skip && skipIntervals.map(interval =>
(<div
key={ interval.start }
className={ stl.skipInterval }
style={ {
left: `${getTimelinePosition(interval.start, scale)}%`,
width: `${ (interval.end - interval.start) * scale }%`,
} }
/>))
}
<div className={ stl.timeline } ref={this.timelineRef} />
{ events.map(e => (
<div
key={ e.key }
className={ stl.event }
style={ { left: `${ getTimelinePosition(e.time,scale)}%` } }
/>
))
}
{
issues.map(iss => (
<div
style={ {
left: `${ getTimelinePosition(iss.time, scale) }%`,
top: '0px',
zIndex: 11, width: 16, height: 16
} }
key={iss.key}
className={ stl.clickRage }
onClick={ this.createEventClickHandler(iss) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ iss.name }</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={iss.icon} size="16" />
</Tooltip>
</div>
))
}
{ events.filter(e => e.type === TYPES.CLICKRAGE).map(e => (
<div
style={ {
left: `${ getTimelinePosition(e.time, scale) }%`,
top: '0px',
zIndex: 11, width: 16, height: 16
} }
key={e.key}
className={ stl.clickRage }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ "Click Rage" }</b>
</div>
}
>
<Icon className="bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
))}
{typeof clickRageTime === 'number' &&
<div
style={{
left: `${ getTimelinePosition(clickRageTime, scale) }%`,
top: '-0px',
zIndex: 11, width: 16, height: 16
}}
className={stl.clickRage}
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ "Click Rage" }</b>
</div>
}
>
<Icon className="rounded-full bg-white" name={getPointerIcon('click_rage')} color="red" size="16" />
</Tooltip>
</div>
}
{ exceptionsList
.map(e => (
<div
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ getTimelinePosition(e.time, scale) }%`, top: '0px', zIndex: 10, width: 16, height: 16 } }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup } >
<b>{ "Exception" }</b>
<br/>
<span>{ e.message }</span>
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('exception')} color="red" size="16" />
</Tooltip>
</div>
))
}
{ resourceList
.filter(r => r.isRed() || r.isYellow())
.map(r => (
<div
key={ r.key }
className={ cn(stl.markup, {
[ stl.error ]: r.isRed(),
[ stl.warning ]: r.isYellow(),
}) }
style={ { left: `${ getTimelinePosition(r.time, scale) }%`, top: '0px', zIndex: 10, width: 16, height: 16 } }
onClick={ this.createEventClickHandler(r) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>{ r.success ? "Slow resource: " : "Missing resource:" }</b>
<br/>
{ r.name }
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('resource')} size="16" />
</Tooltip>
</div>
))
}
{ fetchList
.filter(e => e.isRed())
.map(e => (
<div
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ getTimelinePosition(e.time, scale) }%`, top: '0px' } }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>Failed Fetch</b>
<br/>
{ e.name }
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('fetch')} color="red" size="16" />
</Tooltip>
</div>
))
}
{ stackList
.filter(e => e.isRed())
.map(e => (
<div
key={ e.key }
className={ cn(stl.markup, stl.error) }
style={ { left: `${ getTimelinePosition(e.time, scale) }%`, top: '0px' } }
onClick={ this.createEventClickHandler(e) }
>
<Tooltip
delay={0}
position="top"
html={
<div className={ stl.popup }>
<b>Stack Event</b>
<br/>
{ e.name }
</div>
}
>
<Icon className=" rounded-full bg-white" name={getPointerIcon('stack')} size="16" />
</Tooltip>
</div>
))
}
</div>
</div>
);
}
}

View file

@ -83,7 +83,10 @@ function PlayerControls(props: Props) {
<div className="rounded ml-4 bg-active-blue border border-active-blue-border flex items-stretch">
{/* @ts-ignore */}
<Tooltip title="Rewind 10s" delay={0} position="top">
<button ref={arrowBackRef} className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent">
<button
ref={arrowBackRef}
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
>
{controlIcon(
'skip-forward-fill',
18,
@ -123,7 +126,10 @@ function PlayerControls(props: Props) {
</div>
{/* @ts-ignore */}
<Tooltip title="Forward 10s" delay={0} position="top">
<button ref={arrowForwardRef} className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent">
<button
ref={arrowForwardRef}
className="h-full hover:border-active-blue-border focus:border focus:border-blue border-borderColor-transparent"
>
{controlIcon(
'skip-forward-fill',
18,
@ -136,7 +142,7 @@ function PlayerControls(props: Props) {
</div>
{!live && (
<div className="flex items-center mx-4">
<div className="flex items-center ml-4">
{/* @ts-ignore */}
<Tooltip title="Control play back speed (↑↓)" delay={0} position="top">
<button

View file

@ -18,6 +18,7 @@ import {
EXCEPTIONS,
LONGTASKS,
INSPECTOR,
OVERVIEW,
} from 'Duck/components/player';
import Network from '../Network';
import Console from '../Console/Console';
@ -40,6 +41,7 @@ import Controls from './Controls';
import Overlay from './Overlay';
import stl from './player.module.css';
import { updateLastPlayedSession } from 'Duck/sessions';
import OverviewPanel from '../OverviewPanel';
@connectPlayer(state => ({
live: state.live,
@ -104,6 +106,9 @@ export default class Player extends React.PureComponent {
</div>
{ !fullscreen && !!bottomBlock &&
<div style={{ maxWidth, width: '100%' }}>
{ bottomBlock === OVERVIEW &&
<OverviewPanel />
}
{ bottomBlock === CONSOLE &&
<Console />
}

View file

@ -3,7 +3,7 @@ import cn from "classnames";
import { connect } from 'react-redux';
import { } from 'Player';
import {
NONE,
NONE, OVERVIEW,
} from 'Duck/components/player';
import Player from './Player';
import SubHeader from './Subheader';
@ -38,6 +38,7 @@ export default class PlayerBlock extends React.PureComponent {
<Player
className="flex-1"
bottomBlockIsActive={ !fullscreen && bottomBlock !== NONE }
// bottomBlockIsActive={ true }
bottomBlock={bottomBlock}
fullscreen={fullscreen}
activeTab={activeTab}

View file

@ -42,7 +42,9 @@ export default class Profiler extends React.PureComponent {
/>
<BottomBlock>
<BottomBlock.Header>
<h4 className="text-lg">Profiler</h4>
<div className="flex items-center">
<span className="font-semibold color-gray-medium mr-4">Profiler</span>
</div>
<Input
// className="input-small"
placeholder="Filter by Name"

View file

@ -0,0 +1,18 @@
import React from 'react';
import stl from './xrayButton.module.css';
import cn from 'classnames';
interface Props {
onClick?: () => void;
isActive?: boolean;
}
function XRayButton(props: Props) {
const { isActive } = props;
return (
<button className={cn(stl.wrapper, { [stl.default] : !isActive, [stl.active] : isActive})} onClick={props.onClick}>
X-RAY
</button>
);
}
export default XRayButton;

View file

@ -0,0 +1 @@
export { default } from './XRayButton';

View file

@ -0,0 +1,17 @@
.wrapper {
text-align: center;
padding: 4px 14px;
border: none;
border-radius: 6px;
font-weight: 500;
&.default {
color: white;
background: linear-gradient(90deg, rgba(57, 78, 255, 0.87) 0%, rgba(62, 170, 175, 0.87) 100%);
}
&.active {
background: rgba(63, 81, 181, 0.08);
color: $gray-darkest;
}
}

View file

@ -2,19 +2,16 @@ import React from 'react';
import cn from 'classnames';
interface Props {
classNam?: string;
label?: string;
[x: string]: any;
classNam?: string;
label?: string;
[x: string]: any;
}
export default (props: Props) => {
const { className = '', label = '', ...rest } = props;
return (
<label className={ cn("flex items-center cursor-pointer", className)}>
<input
type="checkbox"
{ ...rest }
/>
{label && <span className="ml-2 select-none mb-0">{label}</span>}
</label>
)
};
const { className = '', label = '', ...rest } = props;
return (
<label className={cn('flex items-center cursor-pointer', className)}>
<input type="checkbox" {...rest} />
{label && <span className="ml-2 select-none mb-0">{label}</span>}
</label>
);
};

View file

@ -1,65 +0,0 @@
import React, { useState } from 'react'
import ErrorFrame from '../ErrorFrame/ErrorFrame'
import cn from 'classnames';
import { IconButton, Icon } from 'UI';
import { connect } from 'react-redux';
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }) {
const [showRaw, setShowRaw] = useState(false)
const firstFunc = errorStack.first() && errorStack.first().function
const openDocs = () => {
window.open(docLink, '_blank');
}
return (
<div className={className} >
{ !sourcemapUploaded && (
<div
style={{ backgroundColor: 'rgba(204, 0, 0, 0.1)' }}
className="font-normal flex items-center text-sm font-regular color-red border p-2 rounded"
>
<Icon name="info" size="16" color="red" />
<div className="ml-2">Source maps must be uploaded to OpenReplay to be able to see stack traces. <a href="#" className="color-red font-medium underline" style={{ textDecoration: 'underline' }} onClick={openDocs}>Learn more.</a></div>
</div>
) }
<div className="flex items-center my-3">
<h3 className="text-xl mr-auto">
Stacktrace
</h3>
<div className="flex justify-end mr-2">
<IconButton
onClick={() => setShowRaw(false) }
label="FULL"
plain={!showRaw}
primaryText={!showRaw}
/>
<IconButton
primaryText={showRaw}
onClick={() => setShowRaw(true) }
plain={showRaw}
label="RAW"
/>
</div>
</div>
<div className="mb-6 code-font" data-hidden={showRaw}>
<div className="leading-relaxed font-weight-bold">{ name }</div>
<div style={{ wordBreak: 'break-all'}}>{message}</div>
</div>
{ showRaw &&
<div className="mb-3 code-font">{name} : {firstFunc ? firstFunc : '?' }</div>
}
{ errorStack.map((frame, i) => (
<div className="mb-3" key={frame.key}>
<ErrorFrame frame={frame} showRaw={showRaw} isFirst={i == 0} />
</div>
))
}
</div>
)
}
ErrorDetails.displayName = "ErrorDetails";
export default ErrorDetails;

View file

@ -0,0 +1,82 @@
import React, { useEffect, useState } from 'react';
import ErrorFrame from '../ErrorFrame/ErrorFrame';
import { fetchErrorStackList } from 'Duck/sessions';
import { Button, Icon } from 'UI';
import { connect } from 'react-redux';
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
interface Props {
fetchErrorStackList: any;
sourcemapUploaded?: boolean;
errorStack?: any;
message?: string;
sessionId: string;
error: any;
}
function ErrorDetails(props: Props) {
const { error, sessionId, message = '', errorStack = [], sourcemapUploaded = false } = props;
const [showRaw, setShowRaw] = useState(false);
const firstFunc = errorStack.first() && errorStack.first().function;
const openDocs = () => {
window.open(docLink, '_blank');
};
useEffect(() => {
props.fetchErrorStackList(sessionId, error.errorId);
}, []);
return (
<div className="bg-white p-5 h-screen">
{!sourcemapUploaded && (
<div
style={{ backgroundColor: 'rgba(204, 0, 0, 0.1)' }}
className="font-normal flex items-center text-sm font-regular color-red border p-2 rounded"
>
<Icon name="info" size="16" color="red" />
<div className="ml-2">
Source maps must be uploaded to OpenReplay to be able to see stack traces.{' '}
<a href="#" className="color-red font-medium underline" style={{ textDecoration: 'underline' }} onClick={openDocs}>
Learn more.
</a>
</div>
</div>
)}
<div className="flex items-center my-3">
<h3 className="text-xl mr-auto">Stacktrace</h3>
<div className="flex justify-end mr-2">
<Button variant={!showRaw ? 'text-primary' : 'text'} onClick={() => setShowRaw(false)}>
FULL
</Button>
<Button variant={showRaw ? 'text-primary' : 'text'} onClick={() => setShowRaw(true)}>
RAW
</Button>
</div>
</div>
<div className="mb-6 code-font" data-hidden={showRaw}>
<div className="leading-relaxed font-weight-bold">{error.name}</div>
<div style={{ wordBreak: 'break-all' }}>{message}</div>
</div>
{showRaw && (
<div className="mb-3 code-font">
{error.name} : {firstFunc ? firstFunc : '?'}
</div>
)}
{errorStack.map((frame: any, i: any) => (
<div className="mb-3" key={frame.key}>
<ErrorFrame frame={frame} showRaw={showRaw} isFirst={i == 0} />
</div>
))}
</div>
);
}
ErrorDetails.displayName = 'ErrorDetails';
export default connect(
(state: any) => ({
errorStack: state.getIn(['sessions', 'errorStack']),
sessionId: state.getIn(['sessions', 'current', 'sessionId']),
}),
{ fetchErrorStackList }
)(ErrorDetails);

View file

@ -12,6 +12,7 @@ export const FETCH = 8;
export const EXCEPTIONS = 9;
export const LONGTASKS = 10;
export const INSPECTOR = 11;
export const OVERVIEW = 12;
const TOGGLE_FULLSCREEN = 'player/TOGGLE_FS';
const TOGGLE_BOTTOM_BLOCK = 'player/SET_BOTTOM_BLOCK';

View file

@ -64,9 +64,9 @@ export function getRE(string: string, options: string) {
}
export const filterList = <T extends Record<string, any>>(
list: T[],
searchQuery: string,
testKeys: string[],
list: T[],
searchQuery: string,
testKeys: string[],
searchCb?: (listItem: T, query: string | RegExp
) => boolean): T[] => {
const filterRE = getRE(searchQuery, 'i');
@ -337,8 +337,12 @@ export const fetchErrorCheck = async (response: any) => {
export const cleanSessionFilters = (data: any) => {
const { filters, ...rest } = data;
const _fitlers = filters.filter((f: any) => {
if (f.operator === 'isAny' || f.operator === 'onAny') { return true } // ignore filter with isAny/onAny operator
if (Array.isArray(f.filters) && f.filters.length > 0) { return true } // ignore subfilters
if (f.operator === 'isAny' || f.operator === 'onAny') {
return true;
} // ignore filter with isAny/onAny operator
if (Array.isArray(f.filters) && f.filters.length > 0) {
return true;
} // ignore subfilters
return f.value !== '' && Array.isArray(f.value) && f.value.length > 0;
});
@ -361,3 +365,13 @@ export const getInitials = (name: any) => {
const names = name.split(' ');
return names.slice(0, 2).map((n: any) => n[0]).join('');
}
export function getTimelinePosition(value: any, scale: any) {
const pos = value * scale;
return pos > 100 ? 100 : pos;
}
export function millisToMinutesAndSeconds(millis: any) {
const minutes = Math.floor(millis / 60000);
const seconds: any = ((millis % 60000) / 1000).toFixed(0);
return minutes + 'm' + (seconds < 10 ? '0' : '') + seconds + 's';
}