finish removing errors reducer

This commit is contained in:
nick-delirium 2024-09-16 15:39:07 +02:00
parent 29576a775c
commit de35ef8822
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
14 changed files with 124 additions and 276 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

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

View file

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

View file

@ -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;
};

View file

@ -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>

View file

@ -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

View file

@ -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);

View file

@ -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));

View file

@ -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 {

View file

@ -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.');

View file

@ -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)

View file

@ -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();
}
}