Skip to content

Instantly share code, notes, and snippets.

@danielnaranjo
Forked from mpyw/Logger.js
Created March 9, 2021 20:47
Show Gist options
  • Save danielnaranjo/efcfea8a26357439861fbea70a0e8ec9 to your computer and use it in GitHub Desktop.
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
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