Skip to content

Instantly share code, notes, and snippets.

@mpyw
Created August 14, 2018 17:46
Show Gist options
  • Save mpyw/a0cd2c8d37ae54d2a91e56fd7140ae57 to your computer and use it in GitHub Desktop.
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
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
}
}
}
}
@elmpp
Copy link

elmpp commented Nov 21, 2019

likey

@StuartMorris0
Copy link

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

@mpyw
Copy link
Author

mpyw commented Feb 27, 2020

@mpyw
Copy link
Author

mpyw commented Feb 27, 2020

But basically I recommend you using Sentry.io instead.

@StuartMorris0
Copy link

Thanks, that looks good. How comes you recommend sentry.io instead of this?

@mpyw
Copy link
Author

mpyw commented Feb 27, 2020

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