finish removing errors reducer
This commit is contained in:
parent
29576a775c
commit
de35ef8822
14 changed files with 124 additions and 276 deletions
|
|
@ -1,67 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import ErrorFrame from './ErrorFrame'
|
||||
import { IconButton, Icon } from 'UI';
|
||||
|
||||
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
|
||||
|
||||
interface Props {
|
||||
error: any,
|
||||
errorStack: any,
|
||||
}
|
||||
function ErrorDetails({ className, name = "Error", message, errorStack, sourcemapUploaded }: any) {
|
||||
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: 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 ErrorDetails;
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { Icon } from 'UI';
|
||||
import cn from 'classnames';
|
||||
import stl from './errorFrame.module.css';
|
||||
|
||||
function ErrorFrame({ frame = {}, showRaw, isFirst }) {
|
||||
const [open, setOpen] = useState(isFirst)
|
||||
const hasContext = frame.context && frame.context.length > 0;
|
||||
return (
|
||||
<div>
|
||||
{ showRaw ?
|
||||
<div className={stl.rawLine}>at { frame.function ? frame.function : '?' } <span className="color-gray-medium">({`${frame.filename}:${frame.lineNo}:${frame.colNo}`})</span></div>
|
||||
:
|
||||
<div className={stl.formatted}>
|
||||
<div className={cn(stl.header, 'flex items-center cursor-pointer')} onClick={() => setOpen(!open)}>
|
||||
<div className="truncate">
|
||||
<span className="font-medium">{ frame.absPath }</span>
|
||||
{ frame.function &&
|
||||
<>
|
||||
<span>{' in '}</span>
|
||||
<span className="font-medium"> {frame.function} </span>
|
||||
</>
|
||||
}
|
||||
<span>{' at line '}</span>
|
||||
<span className="font-medium">
|
||||
{frame.lineNo}:{frame.colNo}
|
||||
</span>
|
||||
</div>
|
||||
{ hasContext &&
|
||||
<div className="ml-auto mr-3">
|
||||
<Icon name={ open ? 'minus' : 'plus'} size="14" color="gray-medium" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{ open && hasContext &&
|
||||
<ol start={ frame.context[0][0]} className={stl.content}>
|
||||
{ frame.context.map(i => (
|
||||
<li
|
||||
key={i[0]}
|
||||
className={ cn("leading-7 text-sm break-all h-auto pl-2", { [stl.errorLine] :i[0] == frame.lineNo }) }
|
||||
>
|
||||
<span>{ i[1].replace(/ /g, "\u00a0") }</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ErrorFrame;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
.rawLine {
|
||||
margin-left: 30px;
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.formatted {
|
||||
border: solid thin #EEE;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.header {
|
||||
background-color: $gray-lightest;
|
||||
padding: 8px;
|
||||
border-bottom: solid thin #EEE;
|
||||
}
|
||||
.content {
|
||||
font-family: 'Menlo', 'monaco', 'consolas', monospace;
|
||||
list-style-position: inside;
|
||||
list-style-type: decimal-leading-zero;
|
||||
}
|
||||
|
||||
.errorLine {
|
||||
background-color: $teal;
|
||||
color: white !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ErrorFrame';
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './ErrorDetails';
|
||||
|
|
@ -17,10 +17,7 @@ function ErrorListItem(props: Props) {
|
|||
const { error, className = '' } = props;
|
||||
// const { showModal } = useModal();
|
||||
|
||||
// const onClick = () => {
|
||||
// alert('test')
|
||||
// showModal(<ErrorDetailsModal />, { right: true });
|
||||
// }
|
||||
|
||||
return (
|
||||
<div
|
||||
className={ cn("p-3 grid grid-cols-12 gap-4 cursor-pointer py-4 hover:bg-active-blue", className) }
|
||||
|
|
@ -49,7 +46,7 @@ function ErrorListItem(props: Props) {
|
|||
<Bar name="Sessions" minPointSize={1} dataKey="count" fill="#A8E0DA" />
|
||||
</BarChart>
|
||||
</div>
|
||||
<ErrorLabel
|
||||
<ErrorLabel
|
||||
// className={stl.sessions}
|
||||
topValue={ error.sessions }
|
||||
bottomValue="Sessions"
|
||||
|
|
@ -84,6 +81,6 @@ const CustomTooltip = ({ active, payload, label }: any) => {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ import SideSection from './SideSection';
|
|||
|
||||
function ErrorInfo(props) {
|
||||
const { errorStore } = useStore();
|
||||
|
||||
const instance = errorStore.instance;
|
||||
const ensureInstance = () => {
|
||||
if (errorStore.isLoading) return;
|
||||
errorStore.fetch(props.errorId);
|
||||
errorStore.fetchTrace(props.errorId);
|
||||
errorStore.fetchError(props.errorId);
|
||||
errorStore.fetchErrorTrace(props.errorId);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
ensureInstance();
|
||||
}, [props.errorId]);
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ function ErrorInfo(props) {
|
|||
show={!loading && errorIdInStore == null}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
<Loader loading={loading} className="w-full">
|
||||
<Loader loading={loading || !instance} className="w-full">
|
||||
<MainSection className="w-9/12" />
|
||||
<SideSection className="w-3/12" />
|
||||
</Loader>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ function MainSection(props) {
|
|||
|
||||
const findSessions = () => {
|
||||
addFilterByKeyAndValue(FilterKey.ERROR, error.message);
|
||||
this.props.history.push(sessionsRoute());
|
||||
props.history.push(sessionsRoute());
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,123 +1,120 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import cn from 'classnames';
|
||||
import withRequest from 'HOCs/withRequest';
|
||||
import { Loader } from 'UI';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
|
||||
import { countries } from 'App/constants';
|
||||
import Trend from './Trend';
|
||||
import { useStore } from 'App/mstore';
|
||||
import { Loader } from 'UI';
|
||||
|
||||
import DateAgo from './DateAgo';
|
||||
import DistributionBar from './DistributionBar';
|
||||
import Trend from './Trend';
|
||||
import { errorService } from 'App/services';
|
||||
|
||||
const MAX_PERCENTAGE = 3;
|
||||
const MIN_COUNT = 4;
|
||||
const MAX_COUNT = 10;
|
||||
function hidePredicate(percentage, index) {
|
||||
if (index < MIN_COUNT) return false;
|
||||
if (index < MAX_COUNT && percentage < MAX_PERCENTAGE) return false;
|
||||
return true;
|
||||
if (index < MIN_COUNT) return false;
|
||||
if (index < MAX_COUNT && percentage < MAX_PERCENTAGE) return false;
|
||||
return true;
|
||||
}
|
||||
function partitionsWrapper(partitions = [], mapCountry = false) {
|
||||
const counts = partitions.map(({ count }) => count);
|
||||
const sum = counts.reduce((a,b)=>parseInt(a)+parseInt(b),0);
|
||||
const sum = counts.reduce((a, b) => parseInt(a) + parseInt(b), 0);
|
||||
if (sum === 0) {
|
||||
return [];
|
||||
return [];
|
||||
}
|
||||
const otherPrcs = counts
|
||||
.map(c => c/sum * 100)
|
||||
.filter(hidePredicate);
|
||||
const otherPrcsSum = otherPrcs.reduce((a,b)=>a+b,0);
|
||||
const showLength = partitions.length - otherPrcs.length;
|
||||
const show = partitions
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, showLength)
|
||||
.map(p => ({
|
||||
label: mapCountry
|
||||
? (countries[p.name] || "Unknown")
|
||||
: p.name,
|
||||
prc: p.count/sum * 100,
|
||||
}))
|
||||
const otherPrcs = counts.map((c) => (c / sum) * 100).filter(hidePredicate);
|
||||
const otherPrcsSum = otherPrcs.reduce((a, b) => a + b, 0);
|
||||
const showLength = partitions.length - otherPrcs.length;
|
||||
const show = partitions
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, showLength)
|
||||
.map((p) => ({
|
||||
label: mapCountry ? countries[p.name] || 'Unknown' : p.name,
|
||||
prc: (p.count / sum) * 100,
|
||||
}));
|
||||
|
||||
if (otherPrcsSum > 0) {
|
||||
show.push({
|
||||
label: "Other",
|
||||
label: 'Other',
|
||||
prc: otherPrcsSum,
|
||||
other: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
return show;
|
||||
}
|
||||
function tagsWrapper(tags = []) {
|
||||
return tags.map(({ name, partitions }) => ({
|
||||
name,
|
||||
partitions: partitionsWrapper(partitions, name === "country")
|
||||
}))
|
||||
return tags.map(({ name, partitions }) => ({
|
||||
name,
|
||||
partitions: partitionsWrapper(partitions, name === 'country'),
|
||||
}));
|
||||
}
|
||||
|
||||
function dataWrapper(data = {}) {
|
||||
return {
|
||||
chart24: data.chart24 || [],
|
||||
chart30: data.chart30 || [],
|
||||
tags: tagsWrapper(data.tags),
|
||||
};
|
||||
return {
|
||||
chart24: data.chart24 || [],
|
||||
chart30: data.chart30 || [],
|
||||
tags: tagsWrapper(data.tags),
|
||||
};
|
||||
}
|
||||
|
||||
@connect(state => ({
|
||||
error: state.getIn([ "errors", "instance" ])
|
||||
}))
|
||||
@withRequest({
|
||||
initialData: props => dataWrapper(props.error),
|
||||
endpoint: props => `/errors/${ props.error.errorId }/stats`,
|
||||
dataWrapper,
|
||||
})
|
||||
export default class SideSection extends React.PureComponent {
|
||||
onDateChange = ({ startDate, endDate }) => {
|
||||
this.props.request({ startDate, endDate });
|
||||
}
|
||||
function SideSection(props) {
|
||||
const [data, setData] = React.useState({
|
||||
chart24: [],
|
||||
chart30: [],
|
||||
tags: [],
|
||||
});
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const { className } = props;
|
||||
const { errorStore } = useStore();
|
||||
const error = errorStore.instance;
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
error,
|
||||
data,
|
||||
loading,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={ cn(className, "pl-5") }>
|
||||
<h3 className="text-xl mb-2">Overview</h3>
|
||||
<Trend
|
||||
chart={ data.chart24 }
|
||||
title="Past 24 hours"
|
||||
/>
|
||||
<div className="mb-6" />
|
||||
<Trend
|
||||
chart={ data.chart30 }
|
||||
title="Last 30 days"
|
||||
timeFormat={'l'}
|
||||
/>
|
||||
<div className="mb-6" />
|
||||
<DateAgo
|
||||
className="my-4"
|
||||
title="First Seen"
|
||||
timestamp={ error.firstOccurrence }
|
||||
/>
|
||||
<DateAgo
|
||||
className="my-4"
|
||||
title="Last Seen"
|
||||
timestamp={ error.lastOccurrence }
|
||||
/>
|
||||
{ data.tags.length > 0 && <h4 className="text-xl mt-6 mb-3">Summary</h4> }
|
||||
<Loader loading={loading}>
|
||||
{ data.tags.map(({ name, partitions }) =>
|
||||
<DistributionBar
|
||||
key={ name }
|
||||
title={name}
|
||||
partitions={partitions}
|
||||
className="mb-6"
|
||||
/>
|
||||
)}
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const grabData = async () => {
|
||||
setLoading(true);
|
||||
errorService.fetchErrorStats(error.errorId)
|
||||
.then(data => {
|
||||
setData(dataWrapper(data))
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
setData(dataWrapper(error))
|
||||
}, [error.errorId])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'pl-5')}>
|
||||
<h3 className="text-xl mb-2">Overview</h3>
|
||||
<Trend chart={data.chart24} title="Past 24 hours" />
|
||||
<div className="mb-6" />
|
||||
<Trend chart={data.chart30} title="Last 30 days" timeFormat={'l'} />
|
||||
<div className="mb-6" />
|
||||
<DateAgo
|
||||
className="my-4"
|
||||
title="First Seen"
|
||||
timestamp={error.firstOccurrence}
|
||||
/>
|
||||
<DateAgo
|
||||
className="my-4"
|
||||
title="Last Seen"
|
||||
timestamp={error.lastOccurrence}
|
||||
/>
|
||||
{data.tags.length > 0 && <h4 className="text-xl mt-6 mb-3">Summary</h4>}
|
||||
<Loader loading={loading}>
|
||||
{data.tags.map(({ name, partitions }) => (
|
||||
<DistributionBar
|
||||
key={name}
|
||||
title={name}
|
||||
partitions={partitions}
|
||||
className="mb-6"
|
||||
/>
|
||||
))}
|
||||
</Loader>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(SideSection);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
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';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'App/mstore';
|
||||
|
||||
const docLink = 'https://docs.openreplay.com/installation/upload-sourcemaps';
|
||||
|
||||
|
|
@ -15,21 +16,16 @@ interface Props {
|
|||
error: any;
|
||||
}
|
||||
function ErrorDetails(props: Props) {
|
||||
const { error, sessionId, message = '', errorStack = [], sourcemapUploaded = false } = props;
|
||||
const { errorStore } = useStore();
|
||||
const errorStack = errorStore.instanceTrace;
|
||||
const { error, sessionId, message = '', sourcemapUploaded = false } = props;
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
const firstFunc = errorStack.first() && errorStack.first().function;
|
||||
|
||||
const firstFunc = errorStack[0] && errorStack[0].function;
|
||||
|
||||
const openDocs = () => {
|
||||
window.open(docLink, '_blank');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionId) {
|
||||
props.fetchErrorStackList(sessionId, error.errorId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-white p-5 h-screen">
|
||||
{!sourcemapUploaded && (
|
||||
|
|
@ -78,9 +74,6 @@ function ErrorDetails(props: Props) {
|
|||
ErrorDetails.displayName = 'ErrorDetails';
|
||||
export default connect(
|
||||
(state: any) => ({
|
||||
// errorStack: state.getIn(['sessions', 'errorStack']),
|
||||
errorStack: state.getIn(['errors', 'instanceTrace']),
|
||||
sessionId: state.getIn(['sessions', 'current']).sessionId,
|
||||
}),
|
||||
{ fetchErrorStackList }
|
||||
)(ErrorDetails);
|
||||
})
|
||||
)(observer(ErrorDetails));
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
import apiClient from 'App/api_client';
|
||||
import { errorService } from 'App/services';
|
||||
import { errorService, sessionService } from 'App/services';
|
||||
|
||||
import { ErrorInfo } from './types/error';
|
||||
|
||||
export default class ErrorStore {
|
||||
instance: ErrorInfo | null = null;
|
||||
instanceTrace: Record<string, any> = [];
|
||||
instanceTrace: Record<string, any>[] = [];
|
||||
stats: Record<string, any> = {};
|
||||
sourcemapUploaded = false;
|
||||
isLoading = false;
|
||||
|
|
@ -65,8 +65,8 @@ export default class ErrorStore {
|
|||
|
||||
try {
|
||||
const response = await errorService.fetchErrorTrace(id);
|
||||
this.setInstanceTrace(response.data.trace);
|
||||
this.setSourcemapUploaded(response.data.sourcemapUploaded);
|
||||
this.setInstanceTrace(response.trace);
|
||||
this.setSourcemapUploaded(response.sourcemapUploaded);
|
||||
} catch (error) {
|
||||
this.setErrorState(actionKey, error);
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { makeAutoObservable } from 'mobx';
|
|||
import Widget from './types/widget';
|
||||
import { metricService, errorService } from 'App/services';
|
||||
import { toast } from 'react-toastify';
|
||||
import Error from './types/error';
|
||||
import { ErrorInfo } from './types/error';
|
||||
import {
|
||||
TIMESERIES,
|
||||
TABLE,
|
||||
|
|
@ -318,7 +318,7 @@ export default class MetricStore {
|
|||
errorService
|
||||
.one(errorId)
|
||||
.then((error: any) => {
|
||||
resolve(new Error().fromJSON(error));
|
||||
resolve(new ErrorInfo(error));
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error('Failed to fetch error details.');
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Period, {LAST_24_HOURS} from 'Types/app/period';
|
|||
import Funnel from '../types/funnel';
|
||||
import {metricService} from 'App/services';
|
||||
import { FUNNEL, HEATMAP, INSIGHTS, TABLE, USER_PATH, WEB_VITALS } from "App/constants/card";
|
||||
import Error from '../types/error';
|
||||
import { ErrorInfo } from '../types/error';
|
||||
import {getChartFormatter} from 'Types/dashboard/helper';
|
||||
import FilterItem from './filterItem';
|
||||
import {filtersMap} from 'Types/filter/newFilter';
|
||||
|
|
@ -299,7 +299,7 @@ export default class Widget {
|
|||
}
|
||||
|
||||
if (this.metricOf === FilterKey.ERRORS) {
|
||||
_data['errors'] = data.errors.map((s: any) => new Error().fromJSON(s));
|
||||
_data['errors'] = data.errors.map((s: any) => new ErrorInfo(s));
|
||||
} else if (this.metricType === INSIGHTS) {
|
||||
_data['issues'] = data
|
||||
.filter((i: any) => i.change > 0 || i.change < 0)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import BaseService from './BaseService';
|
||||
import { IErrorStack } from 'Types/session/errorStack';
|
||||
|
||||
export default class ErrorService extends BaseService {
|
||||
fetchError = async (id: string) => {
|
||||
|
|
@ -13,10 +14,11 @@ export default class ErrorService extends BaseService {
|
|||
return await r.json();
|
||||
};
|
||||
|
||||
fetchErrorTrace = async (id: string) => {
|
||||
fetchErrorTrace = async (id: string): Promise<{ trace: IErrorStack[], sourcemapUploaded: boolean }> => {
|
||||
const r = await this.client.get(`/errors/${id}/sourcemaps`);
|
||||
const { data } = await r.json()
|
||||
|
||||
return await r.json();
|
||||
return data;
|
||||
};
|
||||
|
||||
fetchNewErrorsCount = async (params: any) => {
|
||||
|
|
@ -24,4 +26,10 @@ export default class ErrorService extends BaseService {
|
|||
|
||||
return await r.json();
|
||||
};
|
||||
|
||||
fetchErrorStats = async (errorId: string) => {
|
||||
const r = await this.client.get(`/errors/${errorId}/stats`);
|
||||
|
||||
return await r.json();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue