Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Add error details button #3215

Merged
merged 2 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I need to get this into prod before I can figure out if passing the UUID to datadog and Sentry is working.

);

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
Loading