Skip to content

Commit 885219c

Browse files
committed
catch exceptions globally and send to analytics
1 parent 832cd50 commit 885219c

8 files changed

Lines changed: 160 additions & 75 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"react-contexify": "^5.0.0",
1818
"react-device-detect": "^1.17.0",
1919
"react-dom": "^17.0.1",
20+
"react-error-boundary": "^3.1.4",
2021
"react-icons": "^4.1.0",
2122
"react-markdown": "^7.0.1",
2223
"react-modal": "^3.12.1",

src/App.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ body {
1919
height: 100vh;
2020
display: flex;
2121
}
22+
.appError {
23+
justify-content: center;
24+
align-items: center;
25+
height: 100vh;
26+
display: flex;
27+
flex-direction: column;
28+
}
2229

2330
a {
2431
color: var(--primary-text-color);

src/App.js

Lines changed: 19 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import React, { useState, useReducer, useEffect, useContext, useRef } from 'react'
22
import './App.css'
3-
import { PreferencesProvider } from './preferences/PreferencesContext'
4-
import AppReducer from './preferences/AppReducer'
53
import ConfigurationContext from './configuration/ConfigurationContext'
64
import { APP, LS_PREFERENCES_KEY, SUPPORTED_CARDS } from './Constants'
7-
import { getOSMode } from './services/os'
85
import AppStorage from './services/localStorage'
96
import TermsPage from './pages/TermsPage'
107
import PrivacyPage from './pages/PrivacyPage'
@@ -18,76 +15,37 @@ import ScrollCardsNavigator from './components/ScrollCardsNavigator'
1815
import BottomNavigation from './components/BottomNavigation'
1916
import AppContentLayout from './components/AppContentLayout'
2017
import 'react-contexify/dist/ReactContexify.css'
18+
import PreferencesContext from './preferences/PreferencesContext'
2119

2220
function App() {
23-
const {
24-
supportedTags,
25-
marketingBannerConfig = {},
26-
feedbackWidget,
27-
} = useContext(ConfigurationContext)
21+
const { marketingBannerConfig = {}, feedbackWidget } = useContext(ConfigurationContext)
2822
const [showSideBar, setShowSideBar] = useState(false)
2923
const [showSettings, setShowSettings] = useState(false)
3024
const [currentPage, setCurrentPage] = useState('home')
31-
const [state, dispatcher] = useReducer(
32-
AppReducer,
33-
{
34-
userSelectedTags: supportedTags.filter((t) => t.value === 'javascript'),
35-
userBookmarks: [],
36-
theme: getOSMode(),
37-
openLinksNewTab: true,
38-
listingMode: 'normal',
39-
searchEngine: 'Google',
40-
cards: [
41-
{ id: 0, name: 'github' },
42-
{ id: 1, name: 'hackernews' },
43-
{ id: 2, name: 'devto' },
44-
{ id: 3, name: 'producthunt' },
45-
],
46-
},
47-
(initialState) => {
48-
try {
49-
let preferences = AppStorage.getItem(LS_PREFERENCES_KEY)
50-
if (preferences) {
51-
preferences = JSON.parse(preferences)
52-
preferences = {
53-
...preferences,
54-
userSelectedTags: supportedTags.filter((tag) =>
55-
preferences.userSelectedTags.includes(tag.value)
56-
),
57-
}
58-
return {
59-
...initialState,
60-
...preferences,
61-
}
62-
}
63-
} catch (e) {}
64-
return initialState
65-
}
66-
)
25+
const { state, dispatcher } = useContext(PreferencesContext)
26+
6727
useEffect(() => {
6828
trackPageView(currentPage)
6929
}, [currentPage])
7030

7131
const renderHomePage = () => {
7232
return (
73-
<PreferencesProvider value={{ ...state, dispatcher: dispatcher }}>
74-
<div className="App">
75-
<Header
76-
setShowSideBar={setShowSideBar}
77-
state={state}
78-
dispatcher={dispatcher}
79-
showSideBar={showSideBar}
80-
showSettings={showSettings}
81-
setShowSettings={setShowSettings}
82-
/>
83-
<ScrollCardsNavigator />
84-
<MarketingBanner {...marketingBannerConfig} />
85-
<AppContentLayout setShowSettings={setShowSettings} />
86-
<BookmarksSidebar showSidebar={showSideBar} onClose={() => setShowSideBar(false)} />
33+
<div className="App">
34+
<Header
35+
setShowSideBar={setShowSideBar}
36+
state={state}
37+
dispatcher={dispatcher}
38+
showSideBar={showSideBar}
39+
showSettings={showSettings}
40+
setShowSettings={setShowSettings}
41+
/>
42+
<ScrollCardsNavigator />
43+
<MarketingBanner {...marketingBannerConfig} />
44+
<AppContentLayout setShowSettings={setShowSettings} />
45+
<BookmarksSidebar showSidebar={showSideBar} onClose={() => setShowSideBar(false)} />
8746

88-
<Footer setCurrentPage={setCurrentPage} feedbackWidget={feedbackWidget} />
89-
</div>
90-
</PreferencesProvider>
47+
<Footer setCurrentPage={setCurrentPage} feedbackWidget={feedbackWidget} />
48+
</div>
9149
)
9250
}
9351

src/configuration/AppWrapper.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { useReducer, useContext } from 'react'
2+
import { getOSMode } from '../services/os'
3+
import AppReducer from '../preferences/AppReducer'
4+
import { PreferencesProvider } from '../preferences/PreferencesContext'
5+
import ConfigurationContext from './ConfigurationContext'
6+
import { ErrorBoundary } from 'react-error-boundary'
7+
import { trackException } from '../utils/Analytics'
8+
import { AiFillBug } from 'react-icons/ai'
9+
import { WiRefresh } from 'react-icons/wi'
10+
import { APP } from '../Constants'
11+
12+
function ErrorFallback({ error, resetErrorBoundary }) {
13+
return (
14+
<div className="Page appError">
15+
<AiFillBug size={64} />
16+
<p>Sorry there was a problem loading this page.</p>
17+
<p>Please try again or contact the developer at {APP.contactEmail}</p>
18+
<button onClick={resetErrorBoundary}>
19+
<WiRefresh size={32} className={'buttonIcon'} /> Try again
20+
</button>
21+
</div>
22+
)
23+
}
24+
25+
export default function AppWrapper({ children }) {
26+
const configuration = useContext(ConfigurationContext)
27+
28+
const [state, dispatcher] = useReducer(
29+
AppReducer,
30+
{
31+
userSelectedTags: configuration.supportedTags.filter((t) => t.value === 'javascript'),
32+
userBookmarks: [],
33+
theme: getOSMode(),
34+
openLinksNewTab: true,
35+
listingMode: 'normal',
36+
searchEngine: 'Google',
37+
cards: [
38+
{ id: 0, name: 'github' },
39+
{ id: 1, name: 'hackernews' },
40+
{ id: 2, name: 'devto' },
41+
{ id: 3, name: 'producthunt' },
42+
],
43+
},
44+
(initialState) => {
45+
try {
46+
let preferences = AppStorage.getItem(LS_PREFERENCES_KEY)
47+
if (preferences) {
48+
preferences = JSON.parse(preferences)
49+
preferences = {
50+
...preferences,
51+
userSelectedTags: supportedTags.filter((tag) =>
52+
preferences.userSelectedTags.includes(tag.value)
53+
),
54+
}
55+
return {
56+
...initialState,
57+
...preferences,
58+
}
59+
}
60+
} catch (e) {}
61+
return initialState
62+
}
63+
)
64+
65+
const errorHandler = (error, info) => {
66+
trackException(error, true)
67+
}
68+
69+
return (
70+
<ErrorBoundary
71+
FallbackComponent={ErrorFallback}
72+
onError={errorHandler}
73+
onReset={() => {
74+
// reset the state of your app so the error doesn't happen again
75+
}}>
76+
<PreferencesProvider value={{ ...state, state, dispatcher: dispatcher }}>
77+
{children}
78+
</PreferencesProvider>
79+
</ErrorBoundary>
80+
)
81+
}

src/configuration/ConfigurationWrapper.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import useRemoteConfiguration from './useRemoteConfiguration';
22
import { ConfigurationProvider } from './ConfigurationContext';
33
import { LOCAL_CONFIGURATION } from '../Constants';
44
import BeatLoader from "react-spinners/BeatLoader";
5+
import AppWrapper from './AppWrapper'
56

67
export default function ConfigurationWrapper({ children }) {
7-
const [configuration, loadingConfiguration, errorConfiguration] = useRemoteConfiguration();
8+
const [configuration, loadingConfiguration, errorConfiguration] = useRemoteConfiguration()
89
if (loadingConfiguration) {
9-
return <div className="appLoading">
10-
<BeatLoader color={"#A9B2BD"} loading={true} size={15} />
11-
</div>
10+
return (
11+
<div className="appLoading">
12+
<BeatLoader color={'#A9B2BD'} loading={true} size={15} />
13+
</div>
14+
)
1215
}
1316

14-
const getConfiguration = () => errorConfiguration ? LOCAL_CONFIGURATION : configuration
17+
const getConfiguration = () => (errorConfiguration ? LOCAL_CONFIGURATION : configuration)
1518

1619
return (
1720
<ConfigurationProvider value={getConfiguration()}>
18-
{children}
21+
<AppWrapper>{children}</AppWrapper>
1922
</ConfigurationProvider>
2023
)
21-
2224
}

src/pages/Page.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@
1818

1919
.Page .buttonIcon {
2020
position: relative;
21-
top: 2px;
21+
vertical-align: middle;
2222
}

src/utils/Analytics.js

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ const trackReposDateRangeChange = (dateRange) => {
6969
trackEvent('Repos', 'ChangeDateRange', dateRange)
7070
}
7171

72+
const trackException = (exceptionMessage, fatal) => {
73+
if (!process.env.REACT_APP_ANALYTICS_ID) {
74+
console.log('Missing analytics ID')
75+
return
76+
}
77+
78+
let userId = getRandomUserId()
79+
80+
const payload = new URLSearchParams([
81+
['v', '1'],
82+
['type', 'exception'],
83+
['exd', exceptionMessage],
84+
['exf', fatal === true ? '1' : '0'],
85+
['tid', process.env.REACT_APP_ANALYTICS_ID],
86+
['cid', userId],
87+
])
88+
89+
if (process.env.NODE_ENV !== 'production') {
90+
console.log('Analytics debug payload', payload.toString())
91+
return
92+
}
93+
94+
navigator.sendBeacon('https://www.google-analytics.com/collect', payload.toString())
95+
}
7296
const getResolution = () => {
7397
const realWidth = window.screen.width
7498
const realHeight = window.screen.height
@@ -81,12 +105,7 @@ const trackEvent = (category, action, label) => {
81105
return
82106
}
83107

84-
let userId = AppStorage.getItem(LS_ANALYTICS_ID_KEY)
85-
if (!userId) {
86-
let newUserId = `${new Date().getTime()}${Math.random()}` // Random User Id
87-
AppStorage.setItem(LS_ANALYTICS_ID_KEY, newUserId)
88-
userId = newUserId
89-
}
108+
let userId = getRandomUserId()
90109

91110
const payload = new URLSearchParams([
92111
['v', '1'],
@@ -118,6 +137,15 @@ const trackEvent = (category, action, label) => {
118137
navigator.sendBeacon('https://www.google-analytics.com/collect', payload.toString())
119138
}
120139

140+
const getRandomUserId = () => {
141+
let userId = AppStorage.getItem(LS_ANALYTICS_ID_KEY)
142+
if (!userId) {
143+
let newUserId = `${new Date().getTime()}${Math.random()}` // Random User Id
144+
AppStorage.setItem(LS_ANALYTICS_ID_KEY, newUserId)
145+
userId = newUserId
146+
}
147+
return userId
148+
}
121149
Object.assign(String.prototype, {
122150
capitalize() {
123151
return this.charAt(0).toUpperCase() + this.slice(1)
@@ -141,4 +169,5 @@ export {
141169
trackPageScroll,
142170
trackSearch,
143171
trackSearchEngineChange,
172+
trackException,
144173
}

yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9734,6 +9734,13 @@ react-dom@^17.0.1:
97349734
object-assign "^4.1.1"
97359735
scheduler "^0.20.1"
97369736

9737+
react-error-boundary@^3.1.4:
9738+
version "3.1.4"
9739+
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.4.tgz#255db92b23197108757a888b01e5b729919abde0"
9740+
integrity sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==
9741+
dependencies:
9742+
"@babel/runtime" "^7.12.5"
9743+
97379744
react-error-overlay@^6.0.9:
97389745
version "6.0.9"
97399746
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"

0 commit comments

Comments
 (0)