* * add error boundary immediately above page component * send error logs to server via LoggerContext * display error stack and component stack in copyable text area * wrap every context provider in an error boundary * memoize context values to fix error boundary propagation issue * Wrap page component in an error boundary so we can use our context utilities for a better experience, like toast and logger Still have a top-level error boundary that catches things from the context providers, just in case. Don't display the whole error stack, just because it's ugly, but make it copyable
110 lines
3.2 KiB
JavaScript
110 lines
3.2 KiB
JavaScript
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
|
import { useMe } from './me'
|
|
import fancyNames from '../lib/fancy-names.json'
|
|
|
|
const generateFancyName = () => {
|
|
// 100 adjectives * 100 nouns * 10000 = 100M possible names
|
|
const pickRandom = (array) => array[Math.floor(Math.random() * array.length)]
|
|
const adj = pickRandom(fancyNames.adjectives)
|
|
const noun = pickRandom(fancyNames.nouns)
|
|
const id = Math.floor(Math.random() * fancyNames.maxSuffix)
|
|
return `${adj}-${noun}-${id}`
|
|
}
|
|
|
|
function detectOS () {
|
|
if (!window.navigator) return ''
|
|
|
|
const userAgent = window.navigator.userAgent
|
|
const platform = window.navigator.userAgentData?.platform || window.navigator.platform
|
|
const macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K']
|
|
const windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE']
|
|
const iosPlatforms = ['iPhone', 'iPad', 'iPod']
|
|
let os = null
|
|
|
|
if (macosPlatforms.indexOf(platform) !== -1) {
|
|
os = 'Mac OS'
|
|
} else if (iosPlatforms.indexOf(platform) !== -1) {
|
|
os = 'iOS'
|
|
} else if (windowsPlatforms.indexOf(platform) !== -1) {
|
|
os = 'Windows'
|
|
} else if (/Android/.test(userAgent)) {
|
|
os = 'Android'
|
|
} else if (/Linux/.test(platform)) {
|
|
os = 'Linux'
|
|
}
|
|
|
|
return os
|
|
}
|
|
|
|
export const LoggerContext = createContext()
|
|
|
|
export function LoggerProvider ({ children }) {
|
|
const me = useMe()
|
|
const [name, setName] = useState()
|
|
const [os, setOS] = useState()
|
|
|
|
useEffect(() => {
|
|
let name = window.localStorage.getItem('fancy-name')
|
|
if (!name) {
|
|
name = generateFancyName()
|
|
window.localStorage.setItem('fancy-name', name)
|
|
}
|
|
setName(name)
|
|
setOS(detectOS())
|
|
}, [])
|
|
|
|
const log = useCallback(level => {
|
|
return async (message, context) => {
|
|
if (!me || !me.diagnostics) return
|
|
const env = {
|
|
userAgent: window.navigator.userAgent,
|
|
// os may not be initialized yet
|
|
os: os || detectOS()
|
|
}
|
|
const body = {
|
|
level,
|
|
env,
|
|
// name may be undefined if it wasn't stored in local storage yet
|
|
// we fallback to local storage since on page reloads, the name may wasn't fetched from local storage yet
|
|
name: name || window.localStorage.getItem('fancy-name'),
|
|
message,
|
|
context
|
|
}
|
|
await fetch('/api/log', {
|
|
method: 'post',
|
|
headers: {
|
|
'Content-type': 'application/json'
|
|
},
|
|
body: JSON.stringify(body)
|
|
}).catch(console.error)
|
|
}
|
|
}, [me?.diagnostics, name, os])
|
|
|
|
const logger = useMemo(() => ({
|
|
info: log('info'),
|
|
warn: log('warn'),
|
|
error: log('error'),
|
|
name
|
|
}), [log, name])
|
|
|
|
useEffect(() => {
|
|
// for communication between app and service worker
|
|
const channel = new MessageChannel()
|
|
navigator?.serviceWorker?.controller?.postMessage({ action: 'MESSAGE_PORT' }, [channel.port2])
|
|
channel.port1.onmessage = (event) => {
|
|
const { message, level, context } = Object.assign({ level: 'info' }, event.data)
|
|
logger[level](message, context)
|
|
}
|
|
}, [logger])
|
|
|
|
return (
|
|
<LoggerContext.Provider value={logger}>
|
|
{children}
|
|
</LoggerContext.Provider>
|
|
)
|
|
}
|
|
|
|
export function useLogger () {
|
|
return useContext(LoggerContext)
|
|
}
|