Skip to content

Commit

Permalink
[RUMF-1176] collect other console logs new (#1316)
Browse files Browse the repository at this point in the history
  • Loading branch information
amortemousque authored Feb 16, 2022
1 parent 9b48504 commit 78c8390
Show file tree
Hide file tree
Showing 15 changed files with 526 additions and 212 deletions.
108 changes: 108 additions & 0 deletions packages/core/src/domain/console/consoleObservable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* eslint-disable no-console */
import { isIE } from '../../tools/browserDetection'
import type { Subscription } from '../../tools/observable'
import type { ConsoleLog } from './consoleObservable'
import { ConsoleApiName, initConsoleObservable } from './consoleObservable'

// prettier: avoid formatting issue
// cf https://github.com/prettier/prettier/issues/12211
;[ConsoleApiName.log, ConsoleApiName.info, ConsoleApiName.warn, ConsoleApiName.debug, ConsoleApiName.error].forEach(
(api) => {
describe(`console ${api} observable`, () => {
let consoleStub: jasmine.Spy
let consoleSubscription: Subscription
let notifyLog: jasmine.Spy

beforeEach(() => {
consoleStub = spyOn(console, api)
notifyLog = jasmine.createSpy('notifyLog')

consoleSubscription = initConsoleObservable([api]).subscribe(notifyLog)
})

afterEach(() => {
consoleSubscription.unsubscribe()
})

it(`should notify ${api}`, () => {
console[api]('foo', 'bar')

const consoleLog = notifyLog.calls.mostRecent().args[0]

expect(consoleLog).toEqual(
jasmine.objectContaining({
message: `console ${api}: foo bar`,
api,
})
)
})

it('should keep original behavior', () => {
console[api]('foo', 'bar')

expect(consoleStub).toHaveBeenCalledWith('foo', 'bar')
})

it('should format error instance', () => {
console[api](new TypeError('hello'))
const consoleLog = notifyLog.calls.mostRecent().args[0]
expect(consoleLog.message).toBe(`console ${api}: TypeError: hello`)
})

it('should stringify object parameters', () => {
console[api]('Hello', { foo: 'bar' })
const consoleLog = notifyLog.calls.mostRecent().args[0]
expect(consoleLog.message).toBe(`console ${api}: Hello {\n "foo": "bar"\n}`)
})

it('should allow multiple callers', () => {
const notifyOtherCaller = jasmine.createSpy('notifyOtherCaller')
const instrumentedConsoleApi = console[api]
const otherConsoleSubscription = initConsoleObservable([api]).subscribe(notifyOtherCaller)

console[api]('foo', 'bar')

expect(instrumentedConsoleApi).toEqual(console[api])
expect(notifyLog).toHaveBeenCalledTimes(1)
expect(notifyOtherCaller).toHaveBeenCalledTimes(1)

otherConsoleSubscription.unsubscribe()
})
})
}
)

describe('console error observable', () => {
let consoleSubscription: Subscription
let notifyLog: jasmine.Spy

beforeEach(() => {
spyOn(console, 'error').and.callFake(() => true)
notifyLog = jasmine.createSpy('notifyLog')

consoleSubscription = initConsoleObservable([ConsoleApiName.error]).subscribe(notifyLog)
})

afterEach(() => {
consoleSubscription.unsubscribe()
})

it('should generate a handling stack', () => {
function triggerError() {
console.error('foo', 'bar')
}
triggerError()
const consoleLog = notifyLog.calls.mostRecent().args[0]
expect(consoleLog.handlingStack).toMatch(/^Error:\s+at triggerError (.|\n)*$/)
})

it('should extract stack from first error', () => {
console.error(new TypeError('foo'), new TypeError('bar'))
const stack = (notifyLog.calls.mostRecent().args[0] as ConsoleLog).stack
if (!isIE()) {
expect(stack).toMatch(/^TypeError: foo\s+at/)
} else {
expect(stack).toContain('TypeError: foo')
}
})
})
82 changes: 82 additions & 0 deletions packages/core/src/domain/console/consoleObservable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { callMonitored } from '../internalMonitoring'
import { computeStackTrace } from '../tracekit'
import { createHandlingStack, formatErrorMessage, toStackTraceString } from '../../tools/error'
import { mergeObservables, Observable } from '../../tools/observable'
import { find, jsonStringify } from '../../tools/utils'

export const ConsoleApiName = {
log: 'log',
debug: 'debug',
info: 'info',
warn: 'warn',
error: 'error',
} as const

export type ConsoleApiName = typeof ConsoleApiName[keyof typeof ConsoleApiName]

export interface ConsoleLog {
message: string
api: ConsoleApiName
stack?: string
handlingStack?: string
}

const consoleObservablesByApi: { [k in ConsoleApiName]?: Observable<ConsoleLog> } = {}

export function initConsoleObservable(apis: ConsoleApiName[]) {
const consoleObservables = apis.map((api) => {
if (!consoleObservablesByApi[api]) {
consoleObservablesByApi[api] = createConsoleObservable(api)
}
return consoleObservablesByApi[api]!
})

return mergeObservables<ConsoleLog>(...consoleObservables)
}

/* eslint-disable no-console */
function createConsoleObservable(api: ConsoleApiName) {
const observable = new Observable<ConsoleLog>(() => {
const originalConsoleApi = console[api]

console[api] = (...params: unknown[]) => {
originalConsoleApi.apply(console, params)
const handlingStack = createHandlingStack()

callMonitored(() => {
observable.notify(buildConsoleLog(params, api, handlingStack))
})
}

return () => {
console[api] = originalConsoleApi
}
})

return observable
}

function buildConsoleLog(params: unknown[], api: ConsoleApiName, handlingStack: string): ConsoleLog {
const log: ConsoleLog = {
message: [`console ${api}:`, ...params].map((param) => formatConsoleParameters(param)).join(' '),
api,
}

if (api === ConsoleApiName.error) {
const firstErrorParam = find(params, (param: unknown): param is Error => param instanceof Error)
log.stack = firstErrorParam ? toStackTraceString(computeStackTrace(firstErrorParam)) : undefined
log.handlingStack = handlingStack
}

return log
}

function formatConsoleParameters(param: unknown) {
if (typeof param === 'string') {
return param
}
if (param instanceof Error) {
return formatErrorMessage(computeStackTrace(param))
}
return jsonStringify(param, undefined, 2)
}
91 changes: 0 additions & 91 deletions packages/core/src/domain/error/trackConsoleError.spec.ts

This file was deleted.

70 changes: 0 additions & 70 deletions packages/core/src/domain/error/trackConsoleError.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export {
updateExperimentalFeatures,
resetExperimentalFeatures,
} from './domain/configuration'
export { trackConsoleError } from './domain/error/trackConsoleError'
export { trackRuntimeError } from './domain/error/trackRuntimeError'
export { computeStackTrace, StackTrace } from './domain/tracekit'
export { BuildEnv, BuildMode, defineGlobal, makePublicApi } from './boot/init'
Expand Down Expand Up @@ -56,6 +55,7 @@ export { Context, ContextArray, ContextValue } from './tools/context'
export { areCookiesAuthorized, getCookie, setCookie, deleteCookie, COOKIE_ACCESS_DELAY } from './browser/cookie'
export { initXhrObservable, XhrCompleteContext, XhrStartContext } from './browser/xhrObservable'
export { initFetchObservable, FetchCompleteContext, FetchStartContext, FetchContext } from './browser/fetchObservable'
export { initConsoleObservable, ConsoleLog, ConsoleApiName } from './domain/console/consoleObservable'
export { BoundedBuffer } from './tools/boundedBuffer'
export { catchUserErrors } from './tools/catchUserErrors'
export { createContextManager } from './tools/contextManager'
Expand Down
Loading

0 comments on commit 78c8390

Please sign in to comment.