Skip to content

Commit

Permalink
feat: events api improvements (#1319)
Browse files Browse the repository at this point in the history
  • Loading branch information
subzero10 committed Mar 27, 2024
1 parent a941eca commit 339cfe9
Show file tree
Hide file tree
Showing 24 changed files with 270 additions and 92 deletions.
21 changes: 20 additions & 1 deletion packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
runBeforeNotifyHandlers,
shallowClone,
logger,
logDeprecatedMethod,
generateStackTrace,
filter,
filterUrl,
Expand Down Expand Up @@ -302,8 +303,26 @@ export abstract class Client {
return this
}

/**
* @deprecated Use {@link event} instead.
*/
logEvent(data: Record<string, unknown>): void {
this.__eventsLogger.logEvent(data)
logDeprecatedMethod(this.logger, 'Honeybadger.logEvent', 'Honeybadger.event')
this.event('log', data)
}

event(data: Record<string, unknown>): void;
event(type: string, data: Record<string, unknown>): void;
event(type: string | Record<string, unknown>, data?: Record<string, unknown>): void {
if (typeof type === 'object') {
data = type
type = type['event_type'] as string ?? undefined
}
this.__eventsLogger.log({
event_type: type,
ts: new Date().toISOString(),
...data
})
}

__getBreadcrumbs() {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const CONFIG = {
revision: null,
reportData: null,
breadcrumbsEnabled: true,
// we could decide the value of eventsEnabled based on `env` and `developmentEnvironments`
eventsEnabled: false,
maxBreadcrumbs: 40,
maxObjectDepth: 8,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import events from './plugins/events'

export { Client } from './client'
export * from './store'
export * as Types from './types'
export * as Util from './util'

export const Plugins = {
events
}
43 changes: 43 additions & 0 deletions packages/core/src/plugins/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* eslint-disable prefer-rest-params */
import { Client } from '../client'
import { globalThisOrWindow, instrumentConsole } from '../util';
import { Plugin } from '../types'

export default function (_window = globalThisOrWindow()): Plugin {
return {
shouldReloadOnConfigure: false,
load: (client: Client) => {
function sendEventsToInsights() {
return client.config.eventsEnabled
}

if (!sendEventsToInsights()) {
return
}

instrumentConsole(_window, (level, args) => {
if (!sendEventsToInsights()) {
return
}

if (args.length === 0) {
return
}

const data: Record<string, unknown> = {
severity: level,
}

if (typeof args[0] === 'string') {
data.message = args[0]
data.args = args.slice(1)
}
else {
data.args = args
}

client.event('log', data)
})
}
}
}
15 changes: 8 additions & 7 deletions packages/core/src/throttled_events_logger.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Transport, Config, EventsLogger, Logger } from './types'
import { Transport, Config, EventsLogger, Logger, EventPayload } from './types'
import { NdJson } from 'json-nd'
import { endpoint } from './util'
import { CONFIG as DEFAULT_CONFIG } from './defaults';

export class ThrottledEventsLogger implements EventsLogger {
private queue: Record<string, unknown>[] = []
private queue: EventPayload[] = []
private isProcessing = false
private logger: Logger

constructor(private config: Partial<Config>, private transport: Transport) {
constructor(private readonly config: Partial<Config>, private transport: Transport) {
this.config = {
...DEFAULT_CONFIG,
...config,
Expand All @@ -22,8 +22,8 @@ export class ThrottledEventsLogger implements EventsLogger {
}
}

logEvent(data: Record<string, unknown>) {
this.queue.push(data)
log(payload: EventPayload) {
this.queue.push(payload)

if (!this.isProcessing) {
this.processQueue()
Expand Down Expand Up @@ -82,8 +82,9 @@ export class ThrottledEventsLogger implements EventsLogger {
/**
* todo: improve this
*
* The EventsLogger overrides the console methods
* so if we want to log something we need to use the original methods
* The EventsLogger overrides the console methods to enable automatic instrumentation
* of console logs to the Honeybadger API.
* So if we want to log something in here we need to use the original methods.
*/
private originalLogger() {
return {
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ export interface Logger {

export interface EventsLogger {
configure: (opts: Partial<Config>) => void
logEvent(data: Record<string, unknown>): void
log(data: EventPayload): void
}

export type EventPayload = {
event_type: string;
ts: string; // ISO Date string
} & Record<string, unknown>

export interface Config {
apiKey?: string,
endpoint: string,
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,3 +564,39 @@ function getSourceCodeSnippet(fileData: string, lineNumber: number, sourceRadius
export function isBrowserConfig(config: BrowserConfig | Config): config is BrowserConfig {
return (config as BrowserConfig).async !== undefined
}

/** globalThis has fairly good support. But just in case, lets check its defined.
* @see {https://caniuse.com/?search=globalThis}
*/
export function globalThisOrWindow () {
if (typeof globalThis !== 'undefined') {
return globalThis
}

if (typeof self !== 'undefined') {
return self
}

return window
}

const _deprecatedMethodCalls: Record<string, number> = {}
/**
* Logs a deprecation warning, every X calls to the method.
*/
export function logDeprecatedMethod(logger: Logger, oldMethod: string, newMethod: string, callCountThreshold = 100) {
const key = `${oldMethod}-${newMethod}`
if (typeof _deprecatedMethodCalls[key] === 'undefined') {
_deprecatedMethodCalls[key] = 0
}

if (_deprecatedMethodCalls[key] % callCountThreshold !== 0) {
_deprecatedMethodCalls[key]++
return
}

const msg = `Deprecation warning: ${oldMethod} has been deprecated; please use ${newMethod} instead.`
logger.warn(msg)

_deprecatedMethodCalls[key]++
}
39 changes: 39 additions & 0 deletions packages/core/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { nullLogger, TestClient, TestTransport } from './helpers'
import { Notice } from '../src/types'
import { makeBacktrace, DEFAULT_BACKTRACE_SHIFT } from '../src/util'
import { ThrottledEventsLogger } from '../src/throttled_events_logger';
import { NdJson } from 'json-nd';

class MyError extends Error {
context = null
Expand Down Expand Up @@ -1028,6 +1029,44 @@ describe('client', function () {
})
})

describe('event', function () {
it('sends an event with only a payload object', function (done) {
const transport = client.transport();
const transportSpy = jest.spyOn(transport, 'send')
const timestamp = new Date().toISOString()
const expectedRequestPayload = {
ts: timestamp,
message: 'expected message'
}
client.event(expectedRequestPayload)

setTimeout(() => {
expect(transportSpy).toHaveBeenCalledWith(expect.anything(), NdJson.stringify([expectedRequestPayload]))
done()
})
})

it('sends an event with an event type and a payload object', function (done) {
const transport = client.transport();
const transportSpy = jest.spyOn(transport, 'send')
const timestamp = new Date().toISOString()
const payload = {
ts: timestamp,
message: 'expected message'
}
client.event('expected event', payload)

setTimeout(() => {
const expectedRequestPayload = {
event_type: 'expected event',
...payload
}
expect(transportSpy).toHaveBeenCalledWith(expect.anything(), NdJson.stringify([expectedRequestPayload]))
done()
})
})
})

it('has default filters', function () {
expect(client.config.filters).toEqual(['creditcard', 'password'])
})
Expand Down
14 changes: 14 additions & 0 deletions packages/core/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export class TestTransport implements Transport {
return {};
}
send<T>(_options: TransportOptions, _payload: T): Promise<{ statusCode: number; body: string }> {
if (typeof _payload === 'string') {
// sending an event
return Promise.resolve({ body: '', statusCode: 200 });
}

// sending a notice
return Promise.resolve({ body: JSON.stringify({ id: 'uuid' }), statusCode: 201 });
}
}
Expand All @@ -29,6 +35,14 @@ export class TestClient extends BaseClient {
throw new Error('Method not implemented.');
}

public eventsLogger() {
return this.__eventsLogger;
}

public transport() {
return this.__transport;
}

protected showUserFeedbackForm(_options: UserFeedbackFormOptions): Promise<void> {
throw new Error('Method not implemented.');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { nullLogger, TestClient, TestTransport } from '../../helpers';
import eventsLogger from '../../../../src/browser/integrations/events';
import { nullLogger, TestClient, TestTransport } from '../helpers'
import eventsLogger from '../../src/plugins/events'

const consoleMocked = () => {
return {
Expand All @@ -12,16 +12,14 @@ const consoleMocked = () => {
}

describe('window.console integration for events logging', function () {
let client: TestClient, mockLogEvent, mockConsole
let client: TestClient, mockConsole

beforeEach(function () {
jest.clearAllMocks()
client = new TestClient({
logger: nullLogger()
}, new TestTransport())
mockLogEvent = jest.fn()
mockConsole = consoleMocked()
client.logEvent = mockLogEvent
})

it('should skip install by default', function () {
Expand All @@ -39,10 +37,17 @@ describe('window.console integration for events logging', function () {
})

it('should send events to Honeybadger', async function () {
const eventsLoggerSpy = jest.spyOn(client.eventsLogger(), 'log')
client.config.eventsEnabled = true
const window = { console: mockConsole }
eventsLogger(<never>window).load(client)
mockConsole.log('testing')
expect(mockLogEvent.mock.calls.length).toBe(1)
expect(eventsLoggerSpy).toHaveBeenCalledWith({
event_type: 'log',
ts: expect.any(String),
severity: 'log',
message: 'testing',
args: []
})
})
})
12 changes: 6 additions & 6 deletions packages/core/test/throttled_events_logger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('ThrottledEventsLogger', () => {
const consoleLogSpy = jest.spyOn(console, 'debug')
const transport = new TestTransport()
const eventsLogger = new ThrottledEventsLogger({ debug: true }, transport)
eventsLogger.logEvent({ name: 'foo' })
eventsLogger.log({ event_type: 'event', ts: new Date().toISOString(), name: 'foo' })
await wait(100)
// @ts-ignore
expect(eventsLogger.queue.length).toBe(0)
Expand All @@ -35,11 +35,11 @@ describe('ThrottledEventsLogger', () => {
const transport = new TestTransport()
const transportSpy = jest.spyOn(transport, 'send')
const eventsLogger = new ThrottledEventsLogger({ debug: true }, transport)
const event1 = { name: 'foo' }
const event2 = { name: 'foo', nested: { value: { play: 1 } } }
eventsLogger.logEvent(event1)
eventsLogger.logEvent(event2)
eventsLogger.logEvent(event1)
const event1 = { event_type: 'event', ts: new Date().toISOString(), name: 'foo' }
const event2 = { event_type: 'event', ts: new Date().toISOString(), name: 'foo', nested: { value: { play: 1 } } }
eventsLogger.log(event1)
eventsLogger.log(event2)
eventsLogger.log(event1)
await wait(200)
// @ts-ignore
expect(eventsLogger.queue.length).toBe(0)
Expand Down
Loading

0 comments on commit 339cfe9

Please sign in to comment.