-
-
Save danielnaranjo/efcfea8a26357439861fbea70a0e8ec9 to your computer and use it in GitHub Desktop.
Send your browser console errors to AWS CloudWatch. Inspired by https://github.com/agea/console-cloud-watch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { Component } from 'react' | |
import CloudWatchLogs from 'aws-sdk/clients/cloudwatchlogs' | |
import Fingerprint2 from 'fingerprintjs2' | |
import StackTrace from 'stacktrace-js' | |
import { promisify } from 'es6-promisify' | |
export default class Logger { | |
events = [] | |
originalConsole = null | |
intervalId = null | |
constructor(accessKeyId, secretAccessKey, region, group, levels = ['error'], interval = 10000, mute = false) { | |
this.valid = accessKeyId && secretAccessKey && region && group | |
this.client = new CloudWatchLogs({ accessKeyId, secretAccessKey, region }) | |
this.client.createLogStreamAsync = promisify(this.client.createLogStream) | |
this.client.putLogEventsAsync = promisify(this.client.putLogEvents) | |
this.group = group | |
this.levels = levels | |
this.interval = interval | |
this.mute = mute | |
} | |
setCache(key, value) { | |
global.localStorage.setItem(`ConsoleCloudWatch:${key}`, value) | |
} | |
getCache(key) { | |
return global.localStorage.getItem(`ConsoleCloudWatch:${key}`) | |
} | |
deleteCache(key) { | |
return global.localStorage.removeItem(`ConsoleCloudWatch:${key}`) | |
} | |
init() { | |
const original = {} | |
for (const level of this.levels) { | |
original[level] = global.console[level] | |
global.console[level] = (message, ...args) => { | |
this.onError(message) | |
if (!this.mute) { | |
original[level](message, ...args) | |
} | |
} | |
} | |
this.originalConsole = original | |
this.intervalId = global.setInterval(this.onInterval.bind(this), this.interval) | |
global.addEventListener('error', this.onError.bind(this)) | |
} | |
refresh() { | |
this.deleteCache('key') | |
this.deleteCache('sequenceToken') | |
this.events.splice(0) | |
} | |
async onError(e, info = {}) { | |
if (!this.valid) { | |
return | |
} | |
this.events.push({ | |
message: await this.createPushMessageFromError(e, info), | |
timestamp: new Date().getTime(), | |
}) | |
} | |
async onInterval() { | |
if (!this.valid) { | |
return | |
} | |
const pendingEvents = this.events.splice(0) | |
if (!pendingEvents.length) { | |
return | |
} | |
const key = await this.createOrRetrieveKey() | |
if (!key) { | |
return | |
} | |
const params = { | |
logEvents: pendingEvents, | |
logGroupName: this.group, | |
logStreamName: key, | |
} | |
const sequenceToken = this.getCache('sequenceToken') | |
if (sequenceToken) { | |
params.sequenceToken = sequenceToken | |
} | |
let nextSequenceToken, match | |
try { | |
({ nextSequenceToken } = await this.client.putLogEventsAsync(params)) | |
} catch (e) { | |
if (!e || e.code !== 'InvalidSequenceTokenException' || !(match = e.message.match(/The next expected sequenceToken is: (\w+)/))) { | |
this.originalConsole.error(e) | |
this.refresh() | |
return | |
} | |
} | |
this.setCache('sequenceToken', nextSequenceToken || match[1]) | |
} | |
async createOrRetrieveKey() { | |
let key | |
if ((key = this.getCache('key'))) { | |
return key | |
} | |
try { | |
key = await new Promise((resolve) => new Fingerprint2().get(resolve)) | |
await this.client.createLogStreamAsync({ | |
logGroupName: this.group, | |
logStreamName: key, | |
}) | |
} catch (e) { | |
if (!e || e.code !== 'ResourceAlreadyExistsException') { | |
this.originalConsole.error(e) | |
this.refresh() | |
return | |
} | |
} | |
this.setCache('key', key) | |
return key | |
} | |
async createPushMessageFromError(e, info = {}) { | |
const message = e && e.message ? e.message : e | |
const timestamp = new Date().getTime() | |
const userAgent = global.navigator.userAgent | |
let stack = null | |
if (e && e.message && e.stack) { | |
stack = e.stack | |
try { | |
stack = await StackTrace.fromError(e, { offline: true }) | |
} catch (_) { | |
} | |
} | |
return JSON.stringify({ | |
message, | |
timestamp, | |
userAgent, | |
stack, | |
...info, | |
}) | |
} | |
createLoggerMiddleware() { | |
return (store) => (next) => (action) => { | |
try { | |
return next(action) | |
} catch (e) { | |
this.onError(e, { | |
action, | |
state: store.getState(), | |
category: 'redux', | |
}) | |
} | |
} | |
} | |
createLoggerComponent() { | |
const logger = this | |
return class LoggerComponent extends Component { | |
state = { | |
e: null, | |
} | |
componentDidCatch(e, info) { | |
this.setState({ e }) | |
logger.onError(e, { | |
info, | |
category: 'react', | |
}) | |
} | |
render() { | |
if (this.state.e) { | |
return <div>Fatal Error: {this.state.e.message}</div> | |
} | |
return this.props.children | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment