import type { ClocksState, Duration, RelativeTime, ValueHistoryEntry } from '@datadog/browser-core' import { ONE_MINUTE, generateUUID, createValueHistory, elapsed, combine } from '@datadog/browser-core' import type { RumActionEvent, RumErrorEvent, RumLongTaskEvent, RumResourceEvent } from '../rumEvent.types' import { LifeCycleEventType } from './lifeCycle' import type { LifeCycle } from './lifeCycle' import type { EventCounts } from './trackEventCounts' import { trackEventCounts } from './trackEventCounts' export const EVENT_CONTEXT_TIME_OUT_DELAY = 5 * ONE_MINUTE // arbitrary type BaseTrackedEvent = TData & { id: string startClocks: ClocksState counts?: EventCounts } export type StoppedEvent = BaseTrackedEvent & { duration: Duration } export type DiscardedEvent = BaseTrackedEvent export interface StartOptions { isChildEvent?: ( id: string ) => (event: RumActionEvent | RumErrorEvent | RumLongTaskEvent | RumResourceEvent) => boolean } export interface EventTracker { start: (key: string, startClocks: ClocksState, data: TData, options?: StartOptions) => TrackedEventData stop: (key: string, stopClocks: ClocksState, data?: Partial) => StoppedEvent | undefined discard: (key: string) => DiscardedEvent | undefined getCounts: (key: string) => EventCounts | undefined findId: (startTime?: RelativeTime) => string[] stopAll: () => void } export interface TrackedEventData { id: string key: string startClocks: ClocksState data: TData historyEntry: ValueHistoryEntry eventCounts?: ReturnType } export function startEventTracker(lifeCycle: LifeCycle): EventTracker { // Used by actions to associate events with actions. const history = createValueHistory({ expireDelay: EVENT_CONTEXT_TIME_OUT_DELAY }) // Used by manual actions and resources that use named keys to match the start and stop calls. const keyedEvents = new Map>() function cleanUpEvent(event: TrackedEventData) { keyedEvents.delete(event.key) event.eventCounts?.stop() } function discardAll() { keyedEvents.forEach((event) => { cleanUpEvent(event) }) history.reset() } const sessionRenewalSubscription = lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, discardAll) function start(key: string, startClocks: ClocksState, data: TData, options?: StartOptions): TrackedEventData { const id = generateUUID() const historyEntry = history.add(id, startClocks.relative) const existing = keyedEvents.get(key) if (existing) { cleanUpEvent(existing) } const eventCounts = options?.isChildEvent ? trackEventCounts({ lifeCycle, isChildEvent: options.isChildEvent(id), }) : undefined const trackedEventData: TrackedEventData = { id, key, startClocks, data, historyEntry, eventCounts, } keyedEvents.set(key, trackedEventData) return trackedEventData } function stop(key: string, stopClocks: ClocksState, extraData?: Partial): StoppedEvent | undefined { const event = keyedEvents.get(key) if (!event) { return undefined } const finalData = extraData ? (combine(event.data, extraData) as TData) : event.data event.historyEntry.close(stopClocks.relative) const duration = elapsed(event.startClocks.timeStamp, stopClocks.timeStamp) const counts = event.eventCounts?.eventCounts cleanUpEvent(event) return { ...finalData, id: event.id, startClocks: event.startClocks, duration, counts, } } function discard(key: string): DiscardedEvent | undefined { const event = keyedEvents.get(key) if (!event) { return undefined } const counts = event.eventCounts?.eventCounts cleanUpEvent(event) event.historyEntry.remove() return { ...event.data, id: event.id, startClocks: event.startClocks, counts, } } function findId(startTime?: RelativeTime): string[] { return history.findAll(startTime) } function getCounts(key: string): EventCounts | undefined { return keyedEvents.get(key)?.eventCounts?.eventCounts } function stopTracker() { sessionRenewalSubscription.unsubscribe() discardAll() history.stop() } return { start, stop, discard, getCounts, findId, stopAll: stopTracker, } }