diff --git a/weave-js/src/components/ErrorBoundary.tsx b/weave-js/src/components/ErrorBoundary.tsx index 48b4fcf1a1e..053af08aa97 100644 --- a/weave-js/src/components/ErrorBoundary.tsx +++ b/weave-js/src/components/ErrorBoundary.tsx @@ -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'; @@ -10,24 +11,31 @@ type Props = { }; type State = { - hasError: boolean; + uuid: string | undefined; + timestamp: Date | undefined; + error: Error | undefined; }; export class ErrorBoundary extends Component { - 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', }, @@ -35,8 +43,9 @@ export class ErrorBoundary extends Component { } public render() { - if (this.state.hasError) { - return ; + const {uuid, timestamp, error} = this.state; + if (error != null) { + return ; } return this.props.children; diff --git a/weave-js/src/components/ErrorPanel.tsx b/weave-js/src/components/ErrorPanel.tsx index cbcfd244ac9..74707b90b5d 100644 --- a/weave-js/src/components/ErrorPanel.tsx +++ b/weave-js/src/components/ErrorPanel.tsx @@ -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'; @@ -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` @@ -85,11 +102,88 @@ export const ErrorPanelSmall = ({ ); }; +const getDateObject = (timestamp?: Date): Record | 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', // Full weekday name + 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', // Minute + second: 'numeric', // Second + timeZone: 'UTC', // Ensures it's in UTC + timeZoneName: 'long', // Full time zone name + }), + user: timestamp.toLocaleString('en-US', { + dateStyle: 'full', // Full date + timeStyle: 'full', // Full time + // timeZoneName: 'long', // Full time zone name + }), + }; +}; + +const getErrorObject = (error?: Error): Record | 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( - ({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 ( @@ -98,6 +192,14 @@ export const ErrorPanelLarge = forwardRef( {titleStr} {subtitleStr} {subtitle2Str} + ); } diff --git a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/TabUseCall.tsx b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/TabUseCall.tsx index 51f268011c0..d28522d9055 100644 --- a/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/TabUseCall.tsx +++ b/weave-js/src/components/PagePanelComponents/Home/Browse3/pages/TabUseCall.tsx @@ -48,6 +48,9 @@ os.environ["WF_TRACE_SERVER_URL"] = "http://127.0.0.1:6345" const codeFeedback = selectedTab === 'javascript' ? codeFeedbackJS : codeFeedbackPython; + if (1 + 1 === 2) { + throw new Error('Not implemented'); + } return ( { try { return { @@ -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.