Created
August 14, 2018 17:46
-
-
Save mpyw/a0cd2c8d37ae54d2a91e56fd7140ae57 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 | |
} | |
} | |
} | |
} |
Do you have any example of this in action please? Is the idea to extend LoggerComponent instead of Component, for all components you want to log? How does the middleware connect? Thanks
I've already published this as a TypeScript library package.
But basically I recommend you using Sentry.io instead.
Thanks, that looks good. How comes you recommend sentry.io instead of this?
Although CloudWatch is simple, it doesn't have sufficient features, such as an ability to keep track of errors until they are fixed. Our company productions have completely migrated to Sentry.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
likey