fix(frontend): dont log debug messages in production build

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-06-25 22:58:17 +02:00
parent 85ba675507
commit 1251f2f650
2 changed files with 193 additions and 65 deletions

View file

@ -3,68 +3,207 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Logger, LogLevel } from './logger' import { Logger } from './logger'
import { Settings } from 'luxon' import { Settings } from 'luxon'
import { Mock } from 'ts-mockery'
let testMode = false
let devMode = false
jest.mock('./test-modes', () => ({
get isTestMode() {
return testMode
},
get isDevMode() {
return devMode
}
}))
describe('Logger', () => { describe('Logger', () => {
let consoleMock: jest.SpyInstance
let originalNow: () => number let originalNow: () => number
let dateShift = 0 let dateShift = 0
function mockConsole(methodToMock: LogLevel, onResult: (result: string) => void) { let infoLogMock: jest.SpyInstance
consoleMock = jest.spyOn(console, methodToMock).mockImplementation((...data: string[]) => { let warnLogMock: jest.SpyInstance
const result = data.reduce((state, current) => state + ' ' + current) let errorLogMock: jest.SpyInstance
onResult(result) let debugLogMock: jest.SpyInstance
let defaultLogMock: jest.SpyInstance
let isLocalStorageAccessDenied = false
const mockLocalStorage = () => {
const storage = new Map<string, string>()
const localStorageMock = Mock.of<Storage>({
getItem: (key) => {
if (isLocalStorageAccessDenied) {
throw new Error('Access Denied!')
} else {
return storage.get(key) ?? null
}
},
setItem: (key: string, value: string) => storage.set(key, value),
removeItem: (key: string) => storage.delete(key)
})
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
}) })
} }
beforeEach(() => { beforeEach(() => {
jest.resetModules()
jest.restoreAllMocks()
testMode = false
devMode = false
isLocalStorageAccessDenied = false
mockLocalStorage()
infoLogMock = jest.spyOn(console, 'info').mockReturnValue()
warnLogMock = jest.spyOn(console, 'warn').mockReturnValue()
errorLogMock = jest.spyOn(console, 'error').mockReturnValue()
debugLogMock = jest.spyOn(console, 'debug').mockReturnValue()
defaultLogMock = jest.spyOn(console, 'log').mockReturnValue()
originalNow = Settings.now originalNow = Settings.now
Settings.now = () => new Date(2021, 9, 25, dateShift, 1 + dateShift, 2 + dateShift, 3 + dateShift).valueOf() Settings.now = () => new Date(2021, 9, 25, dateShift, 1 + dateShift, 2 + dateShift, 3 + dateShift).valueOf()
}) })
afterEach(() => { afterEach(() => {
Settings.now = originalNow Settings.now = originalNow
consoleMock.mockReset()
}) })
it('logs a debug message into the console', (done) => { describe('debug logging', () => {
it('wont log without test mode, dev mode and local storage', () => {
dateShift = 0 dateShift = 0
mockConsole(LogLevel.DEBUG, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 00:01:02] %c(prefix) color: yellow color: orange beans')
done()
})
new Logger('prefix').debug('beans') new Logger('prefix').debug('beans')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).not.toBeCalled()
expect(defaultLogMock).not.toBeCalled()
}) })
it('logs a info message into the console', (done) => { it('will enable debug logging in production mode but with local storage setting', () => {
dateShift = 1 dateShift = 1
mockConsole(LogLevel.INFO, (result) => {
expect(consoleMock).toBeCalled() window.localStorage.setItem('debugLogging', 'ACTIVATED!')
expect(result).toEqual('%c[2021-10-25 01:02:03] %c(prefix) color: yellow color: orange toast')
done() new Logger('prefix').debug('muffin')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).toHaveBeenCalledWith(
'%c[2021-10-25 01:02:03] %c(prefix)',
'color: yellow',
'color: orange',
'muffin'
)
expect(defaultLogMock).not.toBeCalled()
}) })
it('wont log in production mode but without local storage access', () => {
dateShift = 0
isLocalStorageAccessDenied = true
new Logger('prefix').debug('beans')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).not.toBeCalled()
expect(defaultLogMock).not.toBeCalled()
})
it('will enable debug logging enabled in test mode', () => {
dateShift = 3
testMode = true
new Logger('prefix').debug('muffin')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).toHaveBeenCalledWith(
'%c[2021-10-25 03:04:05] %c(prefix)',
'color: yellow',
'color: orange',
'muffin'
)
expect(defaultLogMock).not.toBeCalled()
})
it('will enable debug logging enabled in dev mode', () => {
dateShift = 4
devMode = true
new Logger('prefix').debug('muffin')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).toHaveBeenCalledWith(
'%c[2021-10-25 04:05:06] %c(prefix)',
'color: yellow',
'color: orange',
'muffin'
)
expect(defaultLogMock).not.toBeCalled()
})
})
it('logs a info message into the console', () => {
dateShift = 5
new Logger('prefix').info('toast')
expect(infoLogMock).toHaveBeenCalledWith(
'%c[2021-10-25 05:06:07] %c(prefix)',
'color: yellow',
'color: orange',
'toast'
)
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).not.toBeCalled()
expect(defaultLogMock).not.toBeCalled()
new Logger('prefix').info('toast') new Logger('prefix').info('toast')
}) })
it('logs a warn message into the console', (done) => { it('logs a warn message into the console', () => {
dateShift = 2 dateShift = 6
mockConsole(LogLevel.WARN, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 02:03:04] %c(prefix) color: yellow color: orange eggs')
done()
})
new Logger('prefix').warn('eggs') new Logger('prefix').warn('eggs')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).toHaveBeenCalledWith(
'%c[2021-10-25 06:07:08] %c(prefix)',
'color: yellow',
'color: orange',
'eggs'
)
expect(errorLogMock).not.toBeCalled()
expect(debugLogMock).not.toBeCalled()
expect(defaultLogMock).not.toBeCalled()
}) })
it('logs a error message into the console', (done) => { it('logs a error message into the console', () => {
dateShift = 3 dateShift = 7
mockConsole(LogLevel.ERROR, (result) => {
expect(consoleMock).toBeCalled()
expect(result).toEqual('%c[2021-10-25 03:04:05] %c(prefix) color: yellow color: orange bacon')
done()
})
new Logger('prefix').error('bacon') new Logger('prefix').error('bacon')
expect(infoLogMock).not.toBeCalled()
expect(warnLogMock).not.toBeCalled()
expect(errorLogMock).toHaveBeenCalledWith(
'%c[2021-10-25 07:08:09] %c(prefix)',
'color: yellow',
'color: orange',
'bacon'
)
expect(debugLogMock).not.toBeCalled()
expect(defaultLogMock).not.toBeCalled()
}) })
}) })

View file

@ -3,25 +3,27 @@
* *
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { isDevMode, isTestMode } from './test-modes'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
export enum LogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error'
}
type OutputFunction = (...data: unknown[]) => void
/** /**
* Simple logger that prefixes messages with a timestamp and a name. * Simple logger that prefixes messages with a timestamp and a name.
*/ */
export class Logger { export class Logger {
private readonly scope: string private readonly scope: string
private readonly debugLoggingEnabled: boolean
constructor(scope: string) { constructor(scope: string) {
this.scope = scope this.scope = scope
this.debugLoggingEnabled = isDevMode || isTestMode || this.isDebugLoggingEnabled()
}
private isDebugLoggingEnabled() {
try {
return !!window?.localStorage?.getItem('debugLogging')
} catch {
return false
}
} }
/** /**
@ -30,7 +32,9 @@ export class Logger {
* @param data data to log * @param data data to log
*/ */
debug(...data: unknown[]): void { debug(...data: unknown[]): void {
this.log(LogLevel.DEBUG, ...data) if (this.debugLoggingEnabled) {
this.log(console.debug, ...data)
}
} }
/** /**
@ -39,7 +43,7 @@ export class Logger {
* @param data data to log * @param data data to log
*/ */
info(...data: unknown[]): void { info(...data: unknown[]): void {
this.log(LogLevel.INFO, ...data) this.log(console.info, ...data)
} }
/** /**
@ -48,7 +52,7 @@ export class Logger {
* @param data data to log * @param data data to log
*/ */
warn(...data: unknown[]): void { warn(...data: unknown[]): void {
this.log(LogLevel.WARN, ...data) this.log(console.warn, ...data)
} }
/** /**
@ -57,26 +61,11 @@ export class Logger {
* @param data data to log * @param data data to log
*/ */
error(...data: unknown[]): void { error(...data: unknown[]): void {
this.log(LogLevel.ERROR, ...data) this.log(console.error, ...data)
} }
private log(loglevel: LogLevel, ...data: unknown[]) { private log(logSink: (...data: unknown[]) => void, ...data: unknown[]) {
const preparedData = [...this.prefix(), ...data] logSink(...this.prefix(), ...data)
const logOutput = Logger.getLogOutput(loglevel)
logOutput(...preparedData)
}
private static getLogOutput(logLevel: LogLevel): OutputFunction {
switch (logLevel) {
case LogLevel.INFO:
return console.info
case LogLevel.DEBUG:
return console.debug
case LogLevel.ERROR:
return console.error
case LogLevel.WARN:
return console.warn
}
} }
private prefix(): string[] { private prefix(): string[] {