mirror of
https://github.com/grafana/grafana.git
synced 2026-08-18 09:05:04 -05:00
Logging: log frontend errors caught by ErrorBoundary, including component stack (#29345)
* log component stack on error boundary * test for error boundary * PR feedback fixes
This commit is contained in:
@@ -39,6 +39,7 @@
|
|||||||
"@types/react-color": "3.0.1",
|
"@types/react-color": "3.0.1",
|
||||||
"@types/react-select": "3.0.8",
|
"@types/react-select": "3.0.8",
|
||||||
"@types/react-table": "7.0.12",
|
"@types/react-table": "7.0.12",
|
||||||
|
"@sentry/browser": "5.25.0",
|
||||||
"@types/slate": "0.47.1",
|
"@types/slate": "0.47.1",
|
||||||
"@types/slate-react": "0.22.5",
|
"@types/slate-react": "0.22.5",
|
||||||
"classnames": "2.2.6",
|
"classnames": "2.2.6",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { ErrorBoundary } from './ErrorBoundary';
|
||||||
|
import { captureException } from '@sentry/browser';
|
||||||
|
|
||||||
|
jest.mock('@sentry/browser');
|
||||||
|
|
||||||
|
const ErrorThrower: FC<{ error: Error }> = ({ error }) => {
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ErrorBoundary', () => {
|
||||||
|
it('should catch error and report it to sentry, including react component stack in context', async () => {
|
||||||
|
const problem = new Error('things went terribly wrong');
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
{({ error }) => {
|
||||||
|
if (!error) {
|
||||||
|
return <ErrorThrower error={problem} />;
|
||||||
|
} else {
|
||||||
|
return <p>{error.message}</p>;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText(problem.message);
|
||||||
|
expect(captureException).toHaveBeenCalledTimes(1);
|
||||||
|
const [error, context] = (captureException as jest.Mock).mock.calls[0];
|
||||||
|
expect(error).toBe(problem);
|
||||||
|
expect(context).toHaveProperty('contexts');
|
||||||
|
expect(context.contexts).toHaveProperty('react');
|
||||||
|
expect(context.contexts.react).toHaveProperty('componentStack');
|
||||||
|
expect(context.contexts.react.componentStack).toMatch(/^\s+at ErrorThrower (.*)\s+at ErrorBoundary (.*)\s*$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { PureComponent, ReactNode } from 'react';
|
import React, { PureComponent, ReactNode } from 'react';
|
||||||
|
import { captureException } from '@sentry/browser';
|
||||||
import { Alert } from '../Alert/Alert';
|
import { Alert } from '../Alert/Alert';
|
||||||
import { ErrorWithStack } from './ErrorWithStack';
|
import { ErrorWithStack } from './ErrorWithStack';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export class ErrorBoundary extends PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
captureException(error, { contexts: { react: { componentStack: errorInfo.componentStack } } });
|
||||||
this.setState({
|
this.setState({
|
||||||
error: error,
|
error: error,
|
||||||
errorInfo: errorInfo,
|
errorInfo: errorInfo,
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ func (exception *frontendSentryException) FmtStacktraces() string {
|
|||||||
return strings.Join(stacktraces, "\n\n")
|
return strings.Join(stacktraces, "\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addEventContextToLogContext(rootPrefix string, logCtx log15.Ctx, eventCtx map[string]interface{}) {
|
||||||
|
for key, element := range eventCtx {
|
||||||
|
prefix := fmt.Sprintf("%s_%s", rootPrefix, key)
|
||||||
|
switch v := element.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
addEventContextToLogContext(prefix, logCtx, v)
|
||||||
|
default:
|
||||||
|
logCtx[prefix] = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (event *frontendSentryEvent) ToLogContext() log15.Ctx {
|
func (event *frontendSentryEvent) ToLogContext() log15.Ctx {
|
||||||
var ctx = make(log15.Ctx)
|
var ctx = make(log15.Ctx)
|
||||||
ctx["url"] = event.Request.URL
|
ctx["url"] = event.Request.URL
|
||||||
@@ -56,6 +68,7 @@ func (event *frontendSentryEvent) ToLogContext() log15.Ctx {
|
|||||||
if event.Exception != nil {
|
if event.Exception != nil {
|
||||||
ctx["stacktrace"] = event.Exception.FmtStacktraces()
|
ctx["stacktrace"] = event.Exception.FmtStacktraces()
|
||||||
}
|
}
|
||||||
|
addEventContextToLogContext("context", ctx, event.Contexts)
|
||||||
if len(event.User.Email) > 0 {
|
if len(event.User.Email) > 0 {
|
||||||
ctx["user_email"] = event.User.Email
|
ctx["user_email"] = event.User.Email
|
||||||
ctx["user_id"] = event.User.ID
|
ctx["user_id"] = event.User.ID
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
|
|||||||
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
|
assertContextContains(t, logs[0], "stacktrace", `UserError: Please replace user and try again
|
||||||
at foofn (foo.js:123:23)
|
at foofn (foo.js:123:23)
|
||||||
at barfn (bar.js:113:231)`)
|
at barfn (bar.js:113:231)`)
|
||||||
|
assert.NotContains(t, logs[0].Ctx, "context")
|
||||||
})
|
})
|
||||||
|
|
||||||
messageEvent := frontendSentryEvent{
|
messageEvent := frontendSentryEvent{
|
||||||
@@ -127,9 +128,37 @@ func TestFrontendLoggingEndpoint(t *testing.T) {
|
|||||||
assertContextContains(t, logs[0], "event_id", messageEvent.EventID)
|
assertContextContains(t, logs[0], "event_id", messageEvent.EventID)
|
||||||
assertContextContains(t, logs[0], "original_timestamp", messageEvent.Timestamp)
|
assertContextContains(t, logs[0], "original_timestamp", messageEvent.Timestamp)
|
||||||
assert.NotContains(t, logs[0].Ctx, "stacktrace")
|
assert.NotContains(t, logs[0].Ctx, "stacktrace")
|
||||||
|
assert.NotContains(t, logs[0].Ctx, "context")
|
||||||
assertContextContains(t, logs[0], "user_email", user.Email)
|
assertContextContains(t, logs[0], "user_email", user.Email)
|
||||||
assertContextContains(t, logs[0], "user_id", user.ID)
|
assertContextContains(t, logs[0], "user_id", user.ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
eventWithContext := frontendSentryEvent{
|
||||||
|
&sentry.Event{
|
||||||
|
EventID: "123",
|
||||||
|
Level: sentry.LevelInfo,
|
||||||
|
Request: &request,
|
||||||
|
Timestamp: ts,
|
||||||
|
Message: "hello world",
|
||||||
|
User: user,
|
||||||
|
Contexts: map[string]interface{}{
|
||||||
|
"foo": map[string]interface{}{
|
||||||
|
"one": "two",
|
||||||
|
"three": 4,
|
||||||
|
},
|
||||||
|
"bar": "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
logSentryEventScenario(t, "Should log event context", eventWithContext, func(sc *scenarioContext, logs []*log.Record) {
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assert.Len(t, logs, 1)
|
||||||
|
assertContextContains(t, logs[0], "context_foo_one", "two")
|
||||||
|
assertContextContains(t, logs[0], "context_foo_three", "4")
|
||||||
|
assertContextContains(t, logs[0], "context_bar", "baz")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user