Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ factorize LifeCycle and simplify its types #2165

Merged
merged 7 commits into from
Apr 13, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint-local-rules/disallowSideEffects.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ function reportPotentialSideEffect(context, node) {
case 'TSTypeAliasDeclaration':
case 'TSModuleDeclaration':
case 'TSDeclareFunction':
case 'TSInstantiationExpression':
case 'Literal':
case 'Identifier':
return
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export * from './tools/utils/timeUtils'
export * from './tools/utils/arrayUtils'
export * from './tools/serialisation/sanitize'
export * from './tools/getGlobalObject'
export { AbstractLifeCycle } from './tools/abstractLifeCycle'
export * from './domain/eventRateLimiter/createEventRateLimiter'
export * from './tools/utils/browserDetection'
export { sendToExtension } from './tools/sendToExtension'
Expand Down
46 changes: 46 additions & 0 deletions packages/core/src/tools/abstractLifeCycle.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { AbstractLifeCycle } from './abstractLifeCycle'

describe('AbstractLifeCycle', () => {
const LifeCycle = AbstractLifeCycle<{
foo: 'bar'
no_data: void
}>

it('does nothing when notifying without subscribers', () => {
const lifeCycle = new LifeCycle()
expect(() => lifeCycle.notify('foo', 'bar')).not.toThrow()
})

it('notifies subscribers', () => {
const lifeCycle = new LifeCycle()
const subscriber1Spy = jasmine.createSpy()
const subscriber2Spy = jasmine.createSpy()
lifeCycle.subscribe('foo', subscriber1Spy)
lifeCycle.subscribe('foo', subscriber2Spy)

lifeCycle.notify('foo', 'bar')

expect(subscriber1Spy).toHaveBeenCalledOnceWith('bar')
expect(subscriber2Spy).toHaveBeenCalledOnceWith('bar')
})

it('notifies subscribers for events without data', () => {
const lifeCycle = new LifeCycle()
const subscriberSpy = jasmine.createSpy()
lifeCycle.subscribe('no_data', subscriberSpy)

lifeCycle.notify('no_data')

expect(subscriberSpy).toHaveBeenCalledOnceWith(undefined)
})

it('does not notify unsubscribed subscribers', () => {
const lifeCycle = new LifeCycle()
const subscriberSpy = jasmine.createSpy()
lifeCycle.subscribe('foo', subscriberSpy).unsubscribe()

lifeCycle.notify('foo', 'bar')

expect(subscriberSpy).not.toHaveBeenCalled()
})
})
45 changes: 45 additions & 0 deletions packages/core/src/tools/abstractLifeCycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { Subscription } from './observable'

/**
* Type helper to extract event types that have "void" data. This allows to call `notify` without a
* second argument. Ex:
*
* ```
* interface EventMap {
* foo: void
* }
* const LifeCycle = AbstractLifeCycle<EventMap>
* new LifeCycle().notify('foo')
* ```
*/
type EventTypesWithoutData<EventMap> = {
[K in keyof EventMap]: EventMap[K] extends void ? K : never
}[keyof EventMap]

export class AbstractLifeCycle<EventMap> {
private callbacks: { [key in keyof EventMap]?: Array<(data: any) => void> } = {}

notify<EventType extends EventTypesWithoutData<EventMap>>(eventType: EventType): void
notify<EventType extends keyof EventMap>(eventType: EventType, data: EventMap[EventType]): void
notify(eventType: keyof EventMap, data?: unknown) {
const eventCallbacks = this.callbacks[eventType]
if (eventCallbacks) {
eventCallbacks.forEach((callback) => callback(data))
}
}

subscribe<EventType extends keyof EventMap>(
eventType: EventType,
callback: (data: EventMap[EventType]) => void
): Subscription {
if (!this.callbacks[eventType]) {
this.callbacks[eventType] = []
}
this.callbacks[eventType]!.push(callback)
return {
unsubscribe: () => {
this.callbacks[eventType] = this.callbacks[eventType]!.filter((other) => callback !== other)
},
}
}
}
4 changes: 2 additions & 2 deletions packages/logs/src/boot/startLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { startNetworkErrorCollection } from '../domain/logsCollection/networkErr
import { startRuntimeErrorCollection } from '../domain/logsCollection/runtimeError/runtimeErrorCollection'
import { LifeCycle, LifeCycleEventType } from '../domain/lifeCycle'
import { startLoggerCollection } from '../domain/logsCollection/logger/loggerCollection'
import type { CommonContext, RawAgentLogsEvent } from '../rawLogsEvent.types'
import type { CommonContext } from '../rawLogsEvent.types'
import { startLogsBatch } from '../transport/startLogsBatch'
import { startLogsBridge } from '../transport/startLogsBridge'
import type { Logger } from '../domain/logger'
Expand All @@ -41,7 +41,7 @@ export function startLogs(
lifeCycle.subscribe(LifeCycleEventType.LOG_COLLECTED, (log) => sendToExtension('logs', log))

const reportError = (error: RawError) =>
lifeCycle.notify<RawAgentLogsEvent>(LifeCycleEventType.RAW_LOG_COLLECTED, {
lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, {
rawLogsEvent: {
message: error.message,
date: error.startClocks.timeStamp,
Expand Down
39 changes: 8 additions & 31 deletions packages/logs/src/domain/lifeCycle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Context, Subscription } from '@datadog/browser-core'
import { AbstractLifeCycle } from '@datadog/browser-core'
import type { Context } from '@datadog/browser-core'
import type { LogsEvent } from '../logsEvent.types'
import type { CommonContext, RawLogsEvent } from '../rawLogsEvent.types'
import type { Logger } from './logger'
Expand All @@ -8,38 +9,14 @@ export const enum LifeCycleEventType {
LOG_COLLECTED,
}

export class LifeCycle {
private callbacks: { [key in LifeCycleEventType]?: Array<(data: any) => void> } = {}

notify<E extends RawLogsEvent = RawLogsEvent>(
eventType: LifeCycleEventType.RAW_LOG_COLLECTED,
data: RawLogsEventCollectedData<E>
): void
notify(eventType: LifeCycleEventType.LOG_COLLECTED, data: LogsEvent & Context): void
notify(eventType: LifeCycleEventType, data?: any) {
const eventCallbacks = this.callbacks[eventType]
if (eventCallbacks) {
eventCallbacks.forEach((callback) => callback(data))
}
}
subscribe(
eventType: LifeCycleEventType.RAW_LOG_COLLECTED,
callback: (data: RawLogsEventCollectedData) => void
): Subscription
subscribe(eventType: LifeCycleEventType.LOG_COLLECTED, callback: (data: LogsEvent & Context) => void): Subscription
subscribe(eventType: LifeCycleEventType, callback: (data?: any) => void) {
if (!this.callbacks[eventType]) {
this.callbacks[eventType] = []
}
this.callbacks[eventType]!.push(callback)
return {
unsubscribe: () => {
this.callbacks[eventType] = this.callbacks[eventType]!.filter((other) => callback !== other)
},
}
}
interface LifeCycleEventMap {
[LifeCycleEventType.RAW_LOG_COLLECTED]: RawLogsEventCollectedData
[LifeCycleEventType.LOG_COLLECTED]: LogsEvent & Context
}

export const LifeCycle = AbstractLifeCycle<LifeCycleEventMap>
export type LifeCycle = AbstractLifeCycle<LifeCycleEventMap>

export interface RawLogsEventCollectedData<E extends RawLogsEvent = RawLogsEvent> {
rawLogsEvent: E
messageContext?: object
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Context, ClocksState, ConsoleLog } from '@datadog/browser-core'
import { timeStampNow, ConsoleApiName, ErrorSource, initConsoleObservable } from '@datadog/browser-core'
import type { RawConsoleLogsEvent } from '../../../rawLogsEvent.types'
import type { LogsConfiguration } from '../../configuration'
import type { LifeCycle } from '../../lifeCycle'
import { LifeCycleEventType } from '../../lifeCycle'
Expand All @@ -22,7 +21,7 @@ const LogStatusForApi = {
}
export function startConsoleCollection(configuration: LogsConfiguration, lifeCycle: LifeCycle) {
const consoleSubscription = initConsoleObservable(configuration.forwardConsoleLogs).subscribe((log: ConsoleLog) => {
lifeCycle.notify<RawConsoleLogsEvent>(LifeCycleEventType.RAW_LOG_COLLECTED, {
lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, {
rawLogsEvent: {
date: timeStampNow(),
message: log.message,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { TimeStamp } from '@datadog/browser-core'
import { includes, display, combine, ErrorSource, timeStampNow } from '@datadog/browser-core'
import type { CommonContext, RawLoggerLogsEvent } from '../../../rawLogsEvent.types'
import type { CommonContext } from '../../../rawLogsEvent.types'
import type { LifeCycle } from '../../lifeCycle'
import { LifeCycleEventType } from '../../lifeCycle'
import type { Logger, LogsMessage } from '../../logger'
Expand All @@ -26,7 +26,7 @@ export function startLoggerCollection(lifeCycle: LifeCycle) {
display(logsMessage.status, logsMessage.message, combine(logger.getContext(), messageContext))
}

lifeCycle.notify<RawLoggerLogsEvent>(LifeCycleEventType.RAW_LOG_COLLECTED, {
lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, {
rawLogsEvent: {
date: savedDate || timeStampNow(),
message: logsMessage.message,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
tryToClone,
isServerError,
} from '@datadog/browser-core'
import type { RawNetworkLogsEvent } from '../../../rawLogsEvent.types'
import type { LogsConfiguration } from '../../configuration'
import type { LifeCycle } from '../../lifeCycle'
import { LifeCycleEventType } from '../../lifeCycle'
Expand Down Expand Up @@ -46,7 +45,7 @@ export function startNetworkErrorCollection(configuration: LogsConfiguration, li
}

function onResponseDataAvailable(responseData: unknown) {
lifeCycle.notify<RawNetworkLogsEvent>(LifeCycleEventType.RAW_LOG_COLLECTED, {
lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, {
rawLogsEvent: {
message: `${format(type)} error ${request.method} ${request.url}`,
date: request.startClocks.timeStamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
getFileFromStackTraceString,
initReportObservable,
} from '@datadog/browser-core'
import type { RawReportLogsEvent } from '../../../rawLogsEvent.types'
import type { LogsConfiguration } from '../../configuration'
import type { LifeCycle } from '../../lifeCycle'
import { LifeCycleEventType } from '../../lifeCycle'
Expand Down Expand Up @@ -40,7 +39,7 @@ export function startReportCollection(configuration: LogsConfiguration, lifeCycl
message += ` Found in ${getFileFromStackTraceString(report.stack)!}`
}

lifeCycle.notify<RawReportLogsEvent>(LifeCycleEventType.RAW_LOG_COLLECTED, {
lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, {
rawLogsEvent: {
date: timeStampNow(),
message,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Context, RawError, ClocksState } from '@datadog/browser-core'
import { noop, ErrorSource, trackRuntimeError, Observable } from '@datadog/browser-core'
import type { RawRuntimeLogsEvent } from '../../../rawLogsEvent.types'
import type { LogsConfiguration } from '../../configuration'
import type { LifeCycle } from '../../lifeCycle'
import { LifeCycleEventType } from '../../lifeCycle'
Expand All @@ -23,7 +22,7 @@ export function startRuntimeErrorCollection(configuration: LogsConfiguration, li
const { stop: stopRuntimeErrorTracking } = trackRuntimeError(rawErrorObservable)

const rawErrorSubscription = rawErrorObservable.subscribe((rawError) => {
lifeCycle.notify<RawRuntimeLogsEvent>(LifeCycleEventType.RAW_LOG_COLLECTED, {
lifeCycle.notify(LifeCycleEventType.RAW_LOG_COLLECTED, {
rawLogsEvent: {
message: rawError.message,
date: rawError.startClocks.timeStamp,
Expand Down
90 changes: 24 additions & 66 deletions packages/rum-core/src/domain/lifeCycle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Context, PageExitEvent, RawError, RelativeTime, Subscription } from '@datadog/browser-core'
import type { Context, PageExitEvent, RawError, RelativeTime } from '@datadog/browser-core'
import { AbstractLifeCycle } from '@datadog/browser-core'
import type { RumPerformanceEntry } from '../browser/performanceCollection'
import type { RumEventDomainContext } from '../domainContext.types'
import type { RawRumEvent } from '../rawRumEvent.types'
Expand Down Expand Up @@ -36,71 +37,25 @@ export const enum LifeCycleEventType {
RAW_ERROR_COLLECTED,
}

export class LifeCycle {
private callbacks: { [key in LifeCycleEventType]?: Array<(data: any) => void> } = {}

notify(eventType: LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, data: RumPerformanceEntry[]): void
notify(eventType: LifeCycleEventType.REQUEST_STARTED, data: RequestStartEvent): void
notify(eventType: LifeCycleEventType.REQUEST_COMPLETED, data: RequestCompleteEvent): void
notify(eventType: LifeCycleEventType.AUTO_ACTION_COMPLETED, data: AutoAction): void
notify(eventType: LifeCycleEventType.VIEW_CREATED, data: ViewCreatedEvent): void
notify(eventType: LifeCycleEventType.VIEW_UPDATED, data: ViewEvent): void
notify(eventType: LifeCycleEventType.VIEW_ENDED, data: ViewEndedEvent): void
notify(eventType: LifeCycleEventType.PAGE_EXITED, data: PageExitEvent): void
notify(
eventType: LifeCycleEventType.RAW_ERROR_COLLECTED,
data: { error: RawError; savedCommonContext?: CommonContext; customerContext?: Context }
): void
notify(eventType: LifeCycleEventType.SESSION_EXPIRED | LifeCycleEventType.SESSION_RENEWED): void
notify(eventType: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, data: RawRumEventCollectedData): void
notify(eventType: LifeCycleEventType.RUM_EVENT_COLLECTED, data: RumEvent & Context): void
notify(eventType: LifeCycleEventType, data?: any) {
const eventCallbacks = this.callbacks[eventType]
if (eventCallbacks) {
eventCallbacks.forEach((callback) => callback(data))
}
}

subscribe(
eventType: LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED,
callback: (data: RumPerformanceEntry[]) => void
): Subscription
subscribe(eventType: LifeCycleEventType.REQUEST_STARTED, callback: (data: RequestStartEvent) => void): Subscription
subscribe(
eventType: LifeCycleEventType.REQUEST_COMPLETED,
callback: (data: RequestCompleteEvent) => void
): Subscription
subscribe(eventType: LifeCycleEventType.AUTO_ACTION_COMPLETED, callback: (data: AutoAction) => void): Subscription
subscribe(eventType: LifeCycleEventType.VIEW_CREATED, callback: (data: ViewCreatedEvent) => void): Subscription
subscribe(eventType: LifeCycleEventType.VIEW_UPDATED, callback: (data: ViewEvent) => void): Subscription
subscribe(eventType: LifeCycleEventType.VIEW_ENDED, callback: (data: ViewEndedEvent) => void): Subscription
subscribe(eventType: LifeCycleEventType.PAGE_EXITED, callback: (data: PageExitEvent) => void): Subscription
subscribe(
eventType: LifeCycleEventType.RAW_ERROR_COLLECTED,
callback: (data: { error: RawError; savedCommonContext?: CommonContext; customerContext?: Context }) => void
): Subscription
subscribe(
eventType: LifeCycleEventType.SESSION_EXPIRED | LifeCycleEventType.SESSION_RENEWED,
callback: () => void
): Subscription
subscribe(
eventType: LifeCycleEventType.RAW_RUM_EVENT_COLLECTED,
callback: (data: RawRumEventCollectedData) => void
): Subscription
subscribe(
eventType: LifeCycleEventType.RUM_EVENT_COLLECTED,
callback: (data: RumEvent & Context) => void
): Subscription
subscribe(eventType: LifeCycleEventType, callback: (data?: any) => void) {
if (!this.callbacks[eventType]) {
this.callbacks[eventType] = []
}
this.callbacks[eventType]!.push(callback)
return {
unsubscribe: () => {
this.callbacks[eventType] = this.callbacks[eventType]!.filter((other) => callback !== other)
},
}
// Note: this interface needs to be exported even if it is not used outside of this module, else TS
// fails to build the rum-core package with error TS4058
export interface LifeCycleEventMap {
[LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED]: RumPerformanceEntry[]
[LifeCycleEventType.AUTO_ACTION_COMPLETED]: AutoAction
[LifeCycleEventType.VIEW_CREATED]: ViewCreatedEvent
[LifeCycleEventType.VIEW_UPDATED]: ViewEvent
[LifeCycleEventType.VIEW_ENDED]: ViewEndedEvent
[LifeCycleEventType.REQUEST_STARTED]: RequestStartEvent
[LifeCycleEventType.REQUEST_COMPLETED]: RequestCompleteEvent
[LifeCycleEventType.SESSION_EXPIRED]: void
[LifeCycleEventType.SESSION_RENEWED]: void
[LifeCycleEventType.PAGE_EXITED]: PageExitEvent
[LifeCycleEventType.RAW_RUM_EVENT_COLLECTED]: RawRumEventCollectedData
[LifeCycleEventType.RUM_EVENT_COLLECTED]: RumEvent & Context
[LifeCycleEventType.RAW_ERROR_COLLECTED]: {
error: RawError
savedCommonContext?: CommonContext
customerContext?: Context
}
}

Expand All @@ -111,3 +66,6 @@ export interface RawRumEventCollectedData<E extends RawRumEvent = RawRumEvent> {
rawRumEvent: E
domainContext: RumEventDomainContext<E['type']>
}

export const LifeCycle = AbstractLifeCycle<LifeCycleEventMap>
export type LifeCycle = AbstractLifeCycle<LifeCycleEventMap>