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

⚡️ [RUMF-1113] Notify performance entries by batch #1255

Merged
merged 8 commits into from
Jan 18, 2022
Merged
16 changes: 9 additions & 7 deletions packages/rum-core/src/boot/startRum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,14 @@ describe('rum events url', () => {
clock.tick(10)
changeLocation('http://foo.com/?bar=qux')

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, {
entryType: 'longtask',
startTime: relativeNow() - 5,
toJSON: noop,
duration: 5,
} as RumPerformanceEntry)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
{
entryType: 'longtask',
startTime: relativeNow() - 5,
toJSON: noop,
duration: 5,
} as RumPerformanceEntry,
])

clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

Expand Down Expand Up @@ -269,7 +271,7 @@ describe('rum events url', () => {

serverRumEvents.length = 0

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, FAKE_NAVIGATION_ENTRY)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [FAKE_NAVIGATION_ENTRY])
clock.tick(THROTTLE_VIEW_UPDATE_PERIOD)

expect(serverRumEvents.length).toEqual(1)
Expand Down
51 changes: 27 additions & 24 deletions packages/rum-core/src/browser/performanceCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,15 @@ export function supportPerformanceEntry() {

export function startPerformanceCollection(lifeCycle: LifeCycle, configuration: RumConfiguration) {
retrieveInitialDocumentResourceTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, configuration, timing)
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})

if (supportPerformanceObject()) {
handlePerformanceEntries(lifeCycle, configuration, performance.getEntries())
handleRumPerformanceEntries(lifeCycle, configuration, filterRumPerformanceEntries(performance.getEntries()))
}
if (window.PerformanceObserver) {
const handlePerformanceEntryList = monitor((entries: PerformanceObserverEntryList) =>
handlePerformanceEntries(lifeCycle, configuration, entries.getEntries())
handleRumPerformanceEntries(lifeCycle, configuration, filterRumPerformanceEntries(entries.getEntries()))
)
const mainEntries = ['resource', 'navigation', 'longtask', 'paint']
const experimentalEntries = ['largest-contentful-paint', 'first-input', 'layout-shift']
Expand Down Expand Up @@ -144,12 +144,12 @@ export function startPerformanceCollection(lifeCycle: LifeCycle, configuration:
}
if (!supportPerformanceTimingEvent('navigation')) {
retrieveNavigationTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, configuration, timing)
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})
}
if (!supportPerformanceTimingEvent('first-input')) {
retrieveFirstInputTiming((timing) => {
handleRumPerformanceEntry(lifeCycle, configuration, timing)
handleRumPerformanceEntries(lifeCycle, configuration, [timing])
})
}
}
Expand Down Expand Up @@ -284,28 +284,31 @@ function computeRelativePerformanceTiming() {
return result as RelativePerformanceTiming
}

function handlePerformanceEntries(lifeCycle: LifeCycle, configuration: RumConfiguration, entries: PerformanceEntry[]) {
entries.forEach((entry) => {
if (
entry.entryType === 'resource' ||
entry.entryType === 'navigation' ||
entry.entryType === 'paint' ||
entry.entryType === 'longtask' ||
entry.entryType === 'largest-contentful-paint' ||
entry.entryType === 'first-input' ||
entry.entryType === 'layout-shift'
) {
handleRumPerformanceEntry(lifeCycle, configuration, (entry as unknown) as RumPerformanceEntry)
}
})
}
function handleRumPerformanceEntries(
lifeCycle: LifeCycle,
configuration: RumConfiguration,
entries: RumPerformanceEntry[]
) {
const filteredEvents = entries.filter(
(entry) => !isIncompleteNavigation(entry) && !isForbiddenResource(configuration, entry)
)

function handleRumPerformanceEntry(lifeCycle: LifeCycle, configuration: RumConfiguration, entry: RumPerformanceEntry) {
if (isIncompleteNavigation(entry) || isForbiddenResource(configuration, entry)) {
return
if (filteredEvents.length) {
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, filteredEvents)
}
}

lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, entry)
function filterRumPerformanceEntries(entries: PerformanceEntry[]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏I find it a bit confusing to have different levels of filtering. Maybe this function could be used similarly to isIncompleteNavigation and isForbiddenResource? Or maybe we could simply remove it and let subscribers do the check themselves...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filterRumPerformanceEntries filters first because it garantes that the rest of the code deals only with RumPerformanceEntry. Maybe a different naming would be better?

Copy link
Contributor

@bcaudan bcaudan Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a bit confusing to have different levels of filtering.

+1

we could also have a filterRumAllowedPerformanceEntries that do all filtering operations

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 suggestion: ‏Also you could use a isRumPerformanceEntry(entry: PerformanceEntry): entry is RumPerformanceEntry to have a nice typeguard without needing to cast as unknown as RumPerformanceEntry

Copy link
Contributor Author

@amortemousque amortemousque Jan 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type guard does not work because there is no type relation between PerformanceEntry and RumPerformanceEntry. Also having a single filterRumAllowedPerformanceEntries where there is every check is weird since we can have PerformanceEntry[] or RumPerformanceEntry[] as input.
I identified two possible options:

  • The PerformanceEntry[] as to go through the first filter to be checked and cast to RumPerformanceEntry[].
  • The filter function as to be defined something like filterRumAllowedPerformanceEntries(entries: Array<PerformanceEntry | RumPerformanceEntry>)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the implementation according to what we said during the daily. Let me know what you think.

return (entries.filter(
({ entryType }) =>
entryType === 'resource' ||
entryType === 'navigation' ||
entryType === 'paint' ||
entryType === 'longtask' ||
entryType === 'largest-contentful-paint' ||
entryType === 'first-input' ||
entryType === 'layout-shift'
) as unknown) as RumPerformanceEntry[]
}

function isIncompleteNavigation(entry: RumPerformanceEntry) {
Expand Down
8 changes: 4 additions & 4 deletions packages/rum-core/src/domain/lifeCycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { AutoAction, AutoActionCreatedEvent } from './rumEventsCollection/action
import { ViewEvent, ViewCreatedEvent, ViewEndedEvent } from './rumEventsCollection/view/trackViews'

export enum LifeCycleEventType {
PERFORMANCE_ENTRY_COLLECTED,
PERFORMANCE_ENTRIES_COLLECTED,
AUTO_ACTION_CREATED,
AUTO_ACTION_COMPLETED,
AUTO_ACTION_DISCARDED,
Expand Down Expand Up @@ -40,7 +40,7 @@ export enum LifeCycleEventType {
export class LifeCycle {
private callbacks: { [key in LifeCycleEventType]?: Array<(data: any) => void> } = {}

notify(eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, data: RumPerformanceEntry): 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
Expand Down Expand Up @@ -69,8 +69,8 @@ export class LifeCycle {
}

subscribe(
eventType: LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
callback: (data: RumPerformanceEntry) => void
eventType: LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED,
callback: (data: RumPerformanceEntry[]) => void
): Subscription
subscribe(eventType: LifeCycleEventType.REQUEST_STARTED, callback: (data: RequestStartEvent) => void): Subscription
subscribe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,32 +33,31 @@ describe('long task collection', () => {

it('should only listen to long task performance entry', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()
;[
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
LONG_TASK,
{ duration: 100 as Duration, entryType: 'navigation', startTime: 1234 },
{ duration: 100 as Duration, entryType: 'resource', startTime: 1234 },
{ duration: 100 as Duration, entryType: 'paint', startTime: 1234 },
].forEach((entry) => {
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, entry as RumPerformanceEntry)
})
] as RumPerformanceEntry[])

expect(rawRumEvents.length).toBe(1)
})

it('should only collect when session has a replay plan', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()

sessionManager.setReplayPlan()
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, LONG_TASK)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK])
expect(rawRumEvents.length).toBe(1)

sessionManager.setLitePlan()
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, LONG_TASK)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK])
expect(rawRumEvents.length).toBe(1)
})

it('should create raw rum event from performance entry', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, LONG_TASK)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [LONG_TASK])

expect(rawRumEvents[0].startTime).toBe(1234 as RelativeTime)
expect(rawRumEvents[0].rawRumEvent).toEqual({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@ import { LifeCycle, LifeCycleEventType } from '../../lifeCycle'
import { RumSessionManager } from '../../rumSessionManager'

export function startLongTaskCollection(lifeCycle: LifeCycle, sessionManager: RumSessionManager) {
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => {
if (entry.entryType !== 'longtask') {
return
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType !== 'longtask') {
break
}
const session = sessionManager.findTrackedSession(entry.startTime)
if (!session || session.hasLitePlan) {
break
}
const startClocks = relativeToClocks(entry.startTime)
const rawRumEvent: RawRumLongTaskEvent = {
date: startClocks.timeStamp,
long_task: {
id: generateUUID(),
duration: toServerDuration(entry.duration),
},
type: RumEventType.LONG_TASK,
}
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, {
rawRumEvent,
startTime: startClocks.relative,
domainContext: { performanceEntry: entry.toJSON() },
})
}
const session = sessionManager.findTrackedSession(entry.startTime)
if (!session || session.hasLitePlan) {
return
}
const startClocks = relativeToClocks(entry.startTime)
const rawRumEvent: RawRumLongTaskEvent = {
date: startClocks.timeStamp,
long_task: {
id: generateUUID(),
duration: toServerDuration(entry.duration),
},
type: RumEventType.LONG_TASK,
}
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, {
rawRumEvent,
startTime: startClocks.relative,
domainContext: { performanceEntry: entry.toJSON() },
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('resourceCollection', () => {
name: 'https://resource.com/valid',
startTime: 1234 as RelativeTime,
})
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, performanceEntry)
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [performanceEntry])

expect(rawRumEvents[0].startTime).toBe(1234 as RelativeTime)
expect(rawRumEvents[0].rawRumEvent).toEqual({
Expand Down Expand Up @@ -152,12 +152,11 @@ describe('resourceCollection', () => {
describe('tracing info', () => {
it('should be processed from traced initial document', () => {
const { lifeCycle, rawRumEvents } = setupBuilder.build()
lifeCycle.notify(
LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
lifeCycle.notify(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, [
createResourceEntry({
traceId: '1234',
})
)
}),
])
const traceInfo = (rawRumEvents[0].rawRumEvent as RawRumResourceEvent)._dd!
expect(traceInfo).toBeDefined()
expect(traceInfo.trace_id).toBe('1234')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ export function startResourceCollection(lifeCycle: LifeCycle) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processRequest(request))
})

lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => {
if (entry.entryType === 'resource' && !isRequestKind(entry)) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processResourceEntry(entry))
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRIES_COLLECTED, (entries) => {
for (const entry of entries) {
if (entry.entryType === 'resource' && !isRequestKind(entry)) {
lifeCycle.notify(LifeCycleEventType.RAW_RUM_EVENT_COLLECTED, processResourceEntry(entry))
}
}
})
}
Expand Down
Loading