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.

Revisions

  1. @mpyw mpyw created this gist Aug 14, 2018.
    185 changes: 185 additions & 0 deletions Logger.js
    Original 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
    }
    }
    }
    }