Created
August 14, 2018 17:46
-
-
Save mpyw/a0cd2c8d37ae54d2a91e56fd7140ae57 to your computer and use it in GitHub Desktop.
Revisions
-
mpyw created this gist
Aug 14, 2018 .There are no files selected for viewing
This file contains hidden or 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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,185 @@ 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 } } } }