Skip to content

Commit

Permalink
feat(ui): Add error details button (#3215)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamie-rasmussen authored Dec 12, 2024
1 parent 9bda890 commit 968a99d
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 11 deletions.
25 changes: 17 additions & 8 deletions weave-js/src/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {datadogRum} from '@datadog/browser-rum';
import * as Sentry from '@sentry/react';
import React, {Component, ErrorInfo, ReactNode} from 'react';
import {v7 as uuidv7} from 'uuid';

import {weaveErrorToDDPayload} from '../errors';
import {ErrorPanel} from './ErrorPanel';
Expand All @@ -10,33 +11,41 @@ type Props = {
};

type State = {
hasError: boolean;
uuid: string | undefined;
timestamp: Date | undefined;
error: Error | undefined;
};

export class ErrorBoundary extends Component<Props, State> {
public static getDerivedStateFromError(_: Error): State {
return {hasError: true};
public static getDerivedStateFromError(error: Error): State {
return {uuid: uuidv7(), timestamp: new Date(), error};
}
public state: State = {
hasError: false,
uuid: undefined,
timestamp: undefined,
error: undefined,
};

public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
const {uuid} = this.state;
datadogRum.addAction(
'weave_panel_error_boundary',
weaveErrorToDDPayload(error)
weaveErrorToDDPayload(error, undefined, uuid)
);

Sentry.captureException(error, {
extra: {
uuid,
},
tags: {
weaveErrorBoundary: 'true',
},
});
}

public render() {
if (this.state.hasError) {
return <ErrorPanel />;
const {uuid, timestamp, error} = this.state;
if (error != null) {
return <ErrorPanel uuid={uuid} timestamp={timestamp} error={error} />;
}

return this.props.children;
Expand Down
105 changes: 103 additions & 2 deletions weave-js/src/components/ErrorPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import React, {forwardRef, useEffect, useRef, useState} from 'react';
import copyToClipboard from 'copy-to-clipboard';
import _ from 'lodash';
import React, {
forwardRef,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import styled from 'styled-components';

import {toast} from '../common/components/elements/Toast';
import {hexToRGB, MOON_300, MOON_600} from '../common/css/globals.styles';
import {useViewerInfo} from '../common/hooks/useViewerInfo';
import {getCookieBool, getFirebaseCookie} from '../common/util/cookie';
import {Button} from './Button';
import {Icon} from './Icon';
import {Tooltip} from './Tooltip';

Expand All @@ -14,6 +26,11 @@ type ErrorPanelProps = {
title?: string;
subtitle?: string;
subtitle2?: string;

// These props are for error details object
uuid?: string;
timestamp?: Date;
error?: Error;
};

export const Centered = styled.div`
Expand Down Expand Up @@ -85,11 +102,87 @@ export const ErrorPanelSmall = ({
);
};

const getDateObject = (timestamp?: Date): Record<string, any> | null => {
if (!timestamp) {
return null;
}
return {
// e.g. "2024-12-12T06:10:19.475Z",
iso: timestamp.toISOString(),
// e.g. "Thursday, December 12, 2024 at 6:10:19 AM Coordinated Universal Time"
long: timestamp.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric', // Full year
month: 'long', // Full month name
day: 'numeric', // Day of the month
hour: 'numeric', // Hour (12-hour or 24-hour depending on locale)
minute: 'numeric',
second: 'numeric',
timeZone: 'UTC', // Ensures it's in UTC
timeZoneName: 'long', // Full time zone name
}),
user: timestamp.toLocaleString('en-US', {
dateStyle: 'full',
timeStyle: 'full',
}),
};
};

const getErrorObject = (error?: Error): Record<string, any> | null => {
if (!error) {
return null;
}

// Error object properties are not enumerable so we have to copy them manually
const stack = (error.stack ?? '').split('\n');
return {
message: error.message,
stack,
};
};

export const ErrorPanelLarge = forwardRef<HTMLDivElement, ErrorPanelProps>(
({title, subtitle, subtitle2}, ref) => {
({title, subtitle, subtitle2, uuid, timestamp, error}, ref) => {
const titleStr = title ?? DEFAULT_TITLE;
const subtitleStr = subtitle ?? DEFAULT_SUBTITLE;
const subtitle2Str = subtitle2 ?? DEFAULT_SUBTITLE2;

const {userInfo} = useViewerInfo();

const onClick = useCallback(() => {
const betaVersion = getFirebaseCookie('betaVersion');
const isUsingAdminPrivileges = getCookieBool('use_admin_privileges');
const {location, navigator, screen} = window;
const {userAgent, language} = navigator;
const details = {
uuid,
url: location.href,
error: getErrorObject(error),
timestamp_err: getDateObject(timestamp),
timestamp_copied: getDateObject(new Date()),
user: _.pick(userInfo, ['id', 'username']), // Skipping teams and admin
cookies: {
...(betaVersion && {betaVersion}),
...(isUsingAdminPrivileges && {use_admin_privileges: true}),
},
browser: {
userAgent,
language,
screenSize: {
width: screen.width,
height: screen.height,
},
viewportSize: {
width: window.innerWidth,
height: window.innerHeight,
},
},
};
const detailsText = JSON.stringify(details, null, 2);
copyToClipboard(detailsText);
toast('Copied to clipboard');
}, [uuid, timestamp, error, userInfo]);

return (
<Large ref={ref}>
<Circle $size={40} $hoverHighlight={false}>
Expand All @@ -98,6 +191,14 @@ export const ErrorPanelLarge = forwardRef<HTMLDivElement, ErrorPanelProps>(
<Title>{titleStr}</Title>
<Subtitle>{subtitleStr}</Subtitle>
<Subtitle>{subtitle2Str}</Subtitle>
<Button
style={{marginTop: 16}}
size="small"
variant="secondary"
icon="copy"
onClick={onClick}>
Copy error details
</Button>
</Large>
);
}
Expand Down
4 changes: 3 additions & 1 deletion weave-js/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ type DDErrorPayload = {

export const weaveErrorToDDPayload = (
error: Error,
weave?: WeaveApp
weave?: WeaveApp,
uuid?: string
): DDErrorPayload => {
try {
return {
Expand All @@ -49,6 +50,7 @@ export const weaveErrorToDDPayload = (
windowLocationURL: trimString(window.location.href),
weaveContext: weave?.client.debugMeta(),
isServerError: error instanceof UseNodeValueServerExecutionError,
...(uuid != null && {uuid}),
};
} catch (e) {
// If we fail to serialize the error, just return an empty object.
Expand Down

0 comments on commit 968a99d

Please sign in to comment.