mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
Merge pull request #42 from prymitive/sentry-client
feat(sentry): add ErrorBoundary to capture exceptions
This commit is contained in:
10
ui/__mocks__/@sentry/browser.js
Normal file
10
ui/__mocks__/@sentry/browser.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const init = jest.fn();
|
||||
const MockScope = {
|
||||
setExtra: jest.fn()
|
||||
};
|
||||
const configureScope = jest.fn().mockImplementation(fn => {
|
||||
fn(MockScope);
|
||||
});
|
||||
const captureException = jest.fn();
|
||||
|
||||
export { init, configureScope, captureException, MockScope };
|
||||
106
ui/package-lock.json
generated
106
ui/package-lock.json
generated
@@ -133,6 +133,87 @@
|
||||
"prop-types": "15.6.2"
|
||||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-4.0.2.tgz",
|
||||
"integrity": "sha512-i5yHv5OSOtO9LfR7Ju26X27yIw2B4h8nI92uM1HA0NEMgKvayA2fTj+gC3e/1+/E84bAu/8ZWEPt/j3zPEoYXQ==",
|
||||
"requires": {
|
||||
"@sentry/core": "4.0.1",
|
||||
"@sentry/types": "4.0.1",
|
||||
"@sentry/utils": "4.0.1",
|
||||
"md5": "2.2.1"
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-4.0.1.tgz",
|
||||
"integrity": "sha512-ZCaLe+o3Ic1V0yUEQC7b9XvtJJsQ/N/UwWtDjIxn2ShszZNlYkZXxZp29r6GtZ1u318j3t6shcO9dw5eHN9kQg==",
|
||||
"requires": {
|
||||
"@sentry/hub": "4.0.1",
|
||||
"@sentry/minimal": "4.0.1",
|
||||
"@sentry/types": "4.0.0",
|
||||
"@sentry/utils": "4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/types": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-4.0.0.tgz",
|
||||
"integrity": "sha512-UaGfdu9DTcdQjyZ7nSQXAX4Nz1EzwDY6lFMfWxvb3hq7qDvr3bg0LOSFyjELbS6RcAkXwjB8BLpicABAXao3ag=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-4.0.1.tgz",
|
||||
"integrity": "sha512-XLpVrB8MJcquZpVstv7pXq1qjWH80mX7hln2CxWfeQTT5OmUMPGz0NbcaUyPA/QisvMYNo9eeVe2+lTnmSn5+Q==",
|
||||
"requires": {
|
||||
"@sentry/types": "4.0.0",
|
||||
"@sentry/utils": "4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/types": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-4.0.0.tgz",
|
||||
"integrity": "sha512-UaGfdu9DTcdQjyZ7nSQXAX4Nz1EzwDY6lFMfWxvb3hq7qDvr3bg0LOSFyjELbS6RcAkXwjB8BLpicABAXao3ag=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-4.0.1.tgz",
|
||||
"integrity": "sha512-KKrgVATezmf/Wt1c0yH8csRwvV4s6TFGY1R3lgBwql3pKl3YYfL7RMqBke5elG9/3zWl6W4G9cleKZGAPLSvTA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "4.0.1",
|
||||
"@sentry/types": "4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/types": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-4.0.0.tgz",
|
||||
"integrity": "sha512-UaGfdu9DTcdQjyZ7nSQXAX4Nz1EzwDY6lFMfWxvb3hq7qDvr3bg0LOSFyjELbS6RcAkXwjB8BLpicABAXao3ag=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-4.0.1.tgz",
|
||||
"integrity": "sha512-1Cq2gk/wZuBHB//HO830nykysHEsvZpjFcoIBHyqsJ7GjjcMxRAnO8ix0aw3hRfOsiPgD3mp8QomY9DqRHbjjA=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-4.0.1.tgz",
|
||||
"integrity": "sha512-SYbE1oe94TPnzcMlGZyCgIfo8e0NpkDo8sX1w43yHcE4HUbJ0NpK8z2FF4h52hShaog4ceLIXXSiYIKoKaJa2A==",
|
||||
"requires": {
|
||||
"@sentry/types": "4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/types": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-4.0.0.tgz",
|
||||
"integrity": "sha512-UaGfdu9DTcdQjyZ7nSQXAX4Nz1EzwDY6lFMfWxvb3hq7qDvr3bg0LOSFyjELbS6RcAkXwjB8BLpicABAXao3ag=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "10.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.0.tgz",
|
||||
@@ -2079,6 +2160,11 @@
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
|
||||
"integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
|
||||
},
|
||||
"charenc": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
|
||||
"integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
|
||||
},
|
||||
"cheerio": {
|
||||
"version": "1.0.0-rc.2",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz",
|
||||
@@ -2637,6 +2723,11 @@
|
||||
"which": "1.3.1"
|
||||
}
|
||||
},
|
||||
"crypt": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
|
||||
"integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
|
||||
},
|
||||
"cryptiles": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
|
||||
@@ -7568,6 +7659,16 @@
|
||||
"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz",
|
||||
"integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w="
|
||||
},
|
||||
"md5": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
|
||||
"integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
|
||||
"requires": {
|
||||
"charenc": "0.0.2",
|
||||
"crypt": "0.0.2",
|
||||
"is-buffer": "1.1.6"
|
||||
}
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
|
||||
@@ -10194,11 +10295,6 @@
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
|
||||
},
|
||||
"raven-js": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.27.0.tgz",
|
||||
"integrity": "sha512-vChdOL+yzecfnGA+B5EhEZkJ3kY3KlMzxEhShKh6Vdtooyl0yZfYNFQfYzgMf2v4pyQa+OTZ5esTxxgOOZDHqw=="
|
||||
},
|
||||
"raw-body": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.3.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.3",
|
||||
"@sentry/browser": "4.0.2",
|
||||
"body-scroll-lock": "2.5.7",
|
||||
"bootstrap": "4.1.3",
|
||||
"bootswatch": "4.1.3",
|
||||
@@ -25,7 +26,6 @@
|
||||
"object-hash": "1.3.0",
|
||||
"prop-types": "15.6.2",
|
||||
"qs": "6.5.2",
|
||||
"raven-js": "3.27.0",
|
||||
"react": "16.5.2",
|
||||
"react-autosuggest": "9.4.1",
|
||||
"react-datepicker": "1.6.0",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
early, before the UI app is started.
|
||||
-->
|
||||
<span id="settings"
|
||||
data-raven-dsn="{{ .SentryDSN }}"
|
||||
data-sentry-dsn="{{ .SentryDSN }}"
|
||||
data-version="{{ .Version }}"
|
||||
data-default-filters-base64="{{ .DefaultFilter }}">
|
||||
</span>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NavBar } from "Components/NavBar";
|
||||
import { Grid } from "Components/Grid";
|
||||
import { Fetcher } from "Components/Fetcher";
|
||||
import { FaviconBadge } from "Components/FaviconBadge";
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
@@ -49,7 +50,7 @@ class App extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ErrorBoundary>
|
||||
<FaviconBadge alertStore={this.alertStore} />
|
||||
<NavBar
|
||||
alertStore={this.alertStore}
|
||||
@@ -67,7 +68,7 @@ class App extends Component {
|
||||
alertStore={this.alertStore}
|
||||
settingsStore={this.settingsStore}
|
||||
/>
|
||||
</React.Fragment>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
// helpers used to bootstrap App instance and environment for it
|
||||
|
||||
import Raven from "raven-js";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
const SettingsElement = () => document.getElementById("settings");
|
||||
|
||||
const SetupRaven = settingsElement => {
|
||||
const SetupSentry = settingsElement => {
|
||||
if (
|
||||
settingsElement !== null &&
|
||||
settingsElement.dataset.ravenDsn &&
|
||||
settingsElement.dataset.ravenDsn !== "{{ .SentryDSN }}"
|
||||
settingsElement.dataset.sentryDsn &&
|
||||
settingsElement.dataset.sentryDsn !== "{{ .SentryDSN }}"
|
||||
) {
|
||||
let version = "unknown";
|
||||
if (
|
||||
@@ -18,17 +18,14 @@ const SetupRaven = settingsElement => {
|
||||
version = settingsElement.dataset.version;
|
||||
}
|
||||
|
||||
const ravenClient = new Raven.Client();
|
||||
try {
|
||||
ravenClient
|
||||
.config(settingsElement.dataset.ravenDsn, {
|
||||
release: version
|
||||
})
|
||||
.install();
|
||||
return Sentry.init({
|
||||
dsn: settingsElement.dataset.sentryDsn,
|
||||
release: version
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Raven config failed: " + err);
|
||||
console.error("Sentry config failed: " + err);
|
||||
}
|
||||
return ravenClient;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -55,4 +52,4 @@ const ParseDefaultFilters = settingsElement => {
|
||||
return defaultFilters;
|
||||
};
|
||||
|
||||
export { SettingsElement, SetupRaven, ParseDefaultFilters };
|
||||
export { SettingsElement, SetupSentry, ParseDefaultFilters };
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import { SettingsElement, SetupRaven, ParseDefaultFilters } from "./AppBoot";
|
||||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
const MockSettings = (version, ravenDsn, defaultFilters) => {
|
||||
import { SettingsElement, SetupSentry, ParseDefaultFilters } from "./AppBoot";
|
||||
|
||||
beforeEach(() => {
|
||||
Sentry.init.mockReset();
|
||||
});
|
||||
|
||||
const MockSettings = (version, SentryDsn, defaultFilters) => {
|
||||
return jest.spyOn(document, "getElementById").mockImplementation(() => {
|
||||
const filtersBase64 = btoa(JSON.stringify(defaultFilters));
|
||||
const settings = document.createElement("span");
|
||||
settings.id = "settings";
|
||||
settings.dataset = {
|
||||
version: version,
|
||||
ravenDsn: ravenDsn,
|
||||
SentryDsn: SentryDsn,
|
||||
defaultFiltersBase64: filtersBase64
|
||||
};
|
||||
return settings;
|
||||
});
|
||||
};
|
||||
|
||||
const RavenClient = (ravenDsn, version) => {
|
||||
const SentryClient = (SentryDsn, version) => {
|
||||
const settings = document.createElement("span");
|
||||
settings.dataset = { ravenDsn: ravenDsn, version: version };
|
||||
return SetupRaven(settings);
|
||||
settings.dataset = { sentryDsn: SentryDsn, version: version };
|
||||
SetupSentry(settings);
|
||||
};
|
||||
|
||||
const FiltersSetting = filters => {
|
||||
@@ -38,34 +44,40 @@ describe("SettingsElement()", () => {
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(settings.id).toBe("settings");
|
||||
expect(settings.dataset.version).toBe("ver1");
|
||||
expect(settings.dataset.ravenDsn).toBe("fakeDSN");
|
||||
expect(settings.dataset.SentryDsn).toBe("fakeDSN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SetupRaven()", () => {
|
||||
it("does nothing when raven DSN is missing", () => {
|
||||
const client = RavenClient("");
|
||||
expect(client).toBeNull();
|
||||
describe("SetupSentry()", () => {
|
||||
it("does nothing when Sentry DSN is missing", () => {
|
||||
SentryClient("");
|
||||
expect(Sentry.init).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("configures raven when DSN is present", () => {
|
||||
const client = RavenClient("https://key@example.com/mock");
|
||||
expect(client.isSetup()).toBeTruthy();
|
||||
expect(client._dsn).toBe("https://key@example.com/mock");
|
||||
it("configures Sentry when DSN is present", () => {
|
||||
SentryClient("https://key@example.com/mock");
|
||||
expect(Sentry.init).toHaveBeenCalledWith({
|
||||
dsn: "https://key@example.com/mock",
|
||||
release: "unknown" // default version
|
||||
});
|
||||
});
|
||||
|
||||
it("passes release option when version attr is present", () => {
|
||||
const client = RavenClient("https://key@example.com/mock", "ver1");
|
||||
expect(client.isSetup()).toBeTruthy();
|
||||
expect(client._globalOptions.release).toBe("ver1");
|
||||
SentryClient("https://key@example.com/mock", "ver1");
|
||||
expect(Sentry.init).toHaveBeenCalledWith({
|
||||
dsn: "https://key@example.com/mock",
|
||||
release: "ver1"
|
||||
});
|
||||
});
|
||||
|
||||
it("logs an error when invalid DSN is passed to raven", () => {
|
||||
it("logs an error when invalid DSN is passed to Sentry", () => {
|
||||
Sentry.init = jest.fn().mockImplementation(() => {
|
||||
throw new Error("Fake error");
|
||||
});
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
const client = RavenClient("invalidDSN");
|
||||
expect(client.isSetup()).toBeFalsy();
|
||||
SentryClient("invalidDSN");
|
||||
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
74
ui/src/ErrorBoundary.js
Normal file
74
ui/src/ErrorBoundary.js
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
|
||||
|
||||
const InternalError = ({ message, secondsLeft }) => (
|
||||
<div className="jumbotron text-center bg-primary my-4">
|
||||
<div className="container-fluid">
|
||||
<h1 className="display-1 my-5">
|
||||
<FontAwesomeIcon
|
||||
className="text-danger mr-2"
|
||||
icon={faExclamationCircle}
|
||||
/>
|
||||
<span className="text-muted">Internal error</span>
|
||||
</h1>
|
||||
<p className="lead text-muted">{message}</p>
|
||||
<p className="text-muted">
|
||||
This page will auto refresh in {secondsLeft}s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
InternalError.propTypes = {
|
||||
message: PropTypes.node.isRequired,
|
||||
secondsLeft: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { cachedError: null, reloadSeconds: 60 };
|
||||
}
|
||||
|
||||
reloadApp = () => {
|
||||
if (this.state.reloadSeconds <= 1) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
this.setState({ reloadSeconds: this.state.reloadSeconds - 1 });
|
||||
}
|
||||
};
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
this.setState({ cachedError: error });
|
||||
Sentry.configureScope(scope => {
|
||||
Object.keys(errorInfo).forEach(key => {
|
||||
scope.setExtra(key, errorInfo[key]);
|
||||
});
|
||||
});
|
||||
Sentry.captureException(error);
|
||||
// reload after 60s, this is to fix wall monitors automatically
|
||||
setInterval(this.reloadApp, 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.cachedError !== null) {
|
||||
return (
|
||||
<InternalError
|
||||
message={this.state.cachedError.toString()}
|
||||
secondsLeft={this.state.reloadSeconds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export { ErrorBoundary };
|
||||
70
ui/src/ErrorBoundary.test.js
Normal file
70
ui/src/ErrorBoundary.test.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
|
||||
import { mount } from "enzyme";
|
||||
|
||||
import toDiffableHtml from "diffable-html";
|
||||
|
||||
import * as Sentry from "@sentry/browser";
|
||||
|
||||
import { ErrorBoundary } from "./ErrorBoundary";
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllTimers();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const FailingComponent = () => {
|
||||
throw new Error("Error thrown from problem child");
|
||||
};
|
||||
|
||||
const MountedFailingComponent = () => {
|
||||
return mount(
|
||||
<ErrorBoundary>
|
||||
<FailingComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
describe("<ErrorBoundary />", () => {
|
||||
it("matches snapshot", () => {
|
||||
const tree = MountedFailingComponent();
|
||||
expect(toDiffableHtml(tree.html())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("componentDidCatch should catch an error from FailingComponent", () => {
|
||||
jest.spyOn(ErrorBoundary.prototype, "componentDidCatch");
|
||||
MountedFailingComponent();
|
||||
expect(ErrorBoundary.prototype.componentDidCatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("componentDidCatch should report to sentry", () => {
|
||||
MountedFailingComponent();
|
||||
expect(Sentry.captureException).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("componentDidCatch passes scope to sentry", () => {
|
||||
const tree = mount(
|
||||
<ErrorBoundary>
|
||||
<div />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
const instance = tree.instance();
|
||||
instance.componentDidCatch("foo", { foo: "bar" });
|
||||
expect(Sentry.MockScope.setExtra).toHaveBeenCalledWith("foo", "bar");
|
||||
});
|
||||
|
||||
it("calls window.location.reload after 60s", () => {
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
const reloadSpy = jest.spyOn(global.window.location, "reload");
|
||||
MountedFailingComponent();
|
||||
jest.runTimersToTime(1000 * 61);
|
||||
expect(reloadSpy).toHaveBeenCalled();
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
34
ui/src/__snapshots__/ErrorBoundary.test.js.snap
Normal file
34
ui/src/__snapshots__/ErrorBoundary.test.js.snap
Normal file
@@ -0,0 +1,34 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<ErrorBoundary /> matches snapshot 1`] = `
|
||||
"
|
||||
<div class=\\"jumbotron text-center bg-primary my-4\\">
|
||||
<div class=\\"container-fluid\\">
|
||||
<h1 class=\\"display-1 my-5\\">
|
||||
<svg aria-hidden=\\"true\\"
|
||||
data-prefix=\\"fas\\"
|
||||
data-icon=\\"exclamation-circle\\"
|
||||
class=\\"svg-inline--fa fa-exclamation-circle fa-w-16 text-danger mr-2\\"
|
||||
role=\\"img\\"
|
||||
xmlns=\\"http://www.w3.org/2000/svg\\"
|
||||
viewbox=\\"0 0 512 512\\"
|
||||
>
|
||||
<path fill=\\"currentColor\\"
|
||||
d=\\"M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z\\"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
<span class=\\"text-muted\\">
|
||||
Internal error
|
||||
</span>
|
||||
</h1>
|
||||
<p class=\\"lead text-muted\\">
|
||||
Error: Error thrown from problem child
|
||||
</p>
|
||||
<p class=\\"text-muted\\">
|
||||
This page will auto refresh in 60s
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
"
|
||||
`;
|
||||
@@ -3,12 +3,12 @@ import ReactDOM from "react-dom";
|
||||
|
||||
import Moment from "react-moment";
|
||||
|
||||
import { SettingsElement, SetupRaven, ParseDefaultFilters } from "./AppBoot";
|
||||
import { SettingsElement, SetupSentry, ParseDefaultFilters } from "./AppBoot";
|
||||
import { App } from "./App";
|
||||
|
||||
const settingsElement = SettingsElement();
|
||||
|
||||
SetupRaven(settingsElement);
|
||||
SetupSentry(settingsElement);
|
||||
|
||||
// global timer for updating timestamps to human readable offsets
|
||||
// this needs to be run before any <Moment/> instance
|
||||
|
||||
Reference in New Issue
Block a user