Skip to content

Commit

Permalink
[RUM] change viewCollection to emit lifeCycle events
Browse files Browse the repository at this point in the history
  • Loading branch information
BenoitZugmeyer committed Apr 17, 2020
1 parent 40216df commit 1a1ecf1
Show file tree
Hide file tree
Showing 7 changed files with 222 additions and 195 deletions.
4 changes: 4 additions & 0 deletions packages/rum/src/lifeCycle.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ErrorMessage, RequestCompleteEvent, RequestStartEvent } from '@datadog/browser-core'
import { UserAction } from './userActionCollection'
import { View } from './viewCollection'

export enum LifeCycleEventType {
ERROR_COLLECTED,
PERFORMANCE_ENTRY_COLLECTED,
USER_ACTION_COLLECTED,
VIEW_COLLECTED,
REQUEST_STARTED,
REQUEST_COMPLETED,
SESSION_WILL_RENEW,
Expand All @@ -26,6 +28,7 @@ export class LifeCycle {
notify(eventType: LifeCycleEventType.REQUEST_STARTED, data: RequestStartEvent): void
notify(eventType: LifeCycleEventType.REQUEST_COMPLETED, data: RequestCompleteEvent): void
notify(eventType: LifeCycleEventType.USER_ACTION_COLLECTED, data: UserAction): void
notify(eventType: LifeCycleEventType.VIEW_COLLECTED, data: View): void
notify(
eventType:
| LifeCycleEventType.SESSION_WILL_RENEW
Expand All @@ -52,6 +55,7 @@ export class LifeCycle {
callback: (data: RequestCompleteEvent) => void
): Subscription
subscribe(eventType: LifeCycleEventType.USER_ACTION_COLLECTED, callback: (data: UserAction) => void): Subscription
subscribe(eventType: LifeCycleEventType.VIEW_COLLECTED, callback: (data: View) => void): Subscription
subscribe(
eventType:
| LifeCycleEventType.SESSION_WILL_RENEW
Expand Down
2 changes: 2 additions & 0 deletions packages/rum/src/rum.entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { startPerformanceCollection } from './performanceCollection'
import { startRum } from './rum'
import { startRumSession } from './rumSession'
import { startUserActionCollection, UserActionReference } from './userActionCollection'
import { startViewCollection } from './viewCollection'

export interface RumUserConfiguration extends UserConfiguration {
applicationId: string
Expand Down Expand Up @@ -72,6 +73,7 @@ datadogRum.init = monitor((userConfiguration: RumUserConfiguration) => {
const session = startRumSession(configuration, lifeCycle)
const globalApi = startRum(rumUserConfiguration.applicationId, lifeCycle, configuration, session, internalMonitoring)

startViewCollection(location, lifeCycle)
const [requestStartObservable, requestCompleteObservable] = startRequestCollection()
startPerformanceCollection(lifeCycle, session)
startDOMMutationCollection(lifeCycle)
Expand Down
25 changes: 23 additions & 2 deletions packages/rum/src/rum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
import { InternalContext, RumGlobal } from './rum.entry'
import { RumSession } from './rumSession'
import { getUserActionReference, UserActionMeasures, UserActionReference, UserActionType } from './userActionCollection'
import { trackView, viewContext, ViewMeasures } from './viewCollection'
import { viewContext, ViewMeasures } from './viewCollection'

export interface PerformancePaintTiming extends PerformanceEntry {
entryType: 'paint'
Expand Down Expand Up @@ -186,7 +186,7 @@ export function startRum(
() => lifeCycle.notify(LifeCycleEventType.WILL_UNLOAD)
)

trackView(window.location, lifeCycle, batch.upsertRumEvent)
trackView(lifeCycle, batch.upsertRumEvent)
trackErrors(lifeCycle, batch.addRumEvent)
trackRequests(configuration, lifeCycle, session, batch.addRumEvent)
trackPerformanceTiming(configuration, lifeCycle, batch.addRumEvent)
Expand Down Expand Up @@ -248,6 +248,27 @@ function startRumBatch(
}
}

function trackView(lifeCycle: LifeCycle, upsertRumEvent: (event: RumViewEvent, key: string) => void) {
lifeCycle.subscribe(LifeCycleEventType.VIEW_COLLECTED, (view) => {
upsertRumEvent(
{
date: getTimestamp(view.startTime),
duration: msToNs(view.duration),
evt: {
category: RumEventCategory.VIEW,
},
rum: {
documentVersion: view.documentVersion,
},
view: {
measures: view.measures,
},
},
view.id
)
})
}

function trackErrors(lifeCycle: LifeCycle, addRumEvent: (event: RumErrorEvent) => void) {
lifeCycle.subscribe(LifeCycleEventType.ERROR_COLLECTED, ({ message, startTime, context }: ErrorMessage) => {
addRumEvent({
Expand Down
8 changes: 0 additions & 8 deletions packages/rum/src/trackEventCounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,5 @@ export function trackEventCounts(lifeCycle: LifeCycle, callback: (eventCounts: E
subscriptions.forEach((s) => s.unsubscribe())
},
eventCounts,
reset() {
const eventCountsMap = eventCounts as { [key: string]: number }
for (const key in eventCountsMap) {
if (Object.prototype.hasOwnProperty.call(eventCountsMap, key)) {
eventCountsMap[key] = 0
}
}
},
}
}
234 changes: 114 additions & 120 deletions packages/rum/src/viewCollection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { DOM_EVENT, generateUUID, getTimestamp, monitor, msToNs, throttle } from '@datadog/browser-core'
import { DOM_EVENT, generateUUID, monitor, msToNs, throttle } from '@datadog/browser-core'

import { LifeCycle, LifeCycleEventType } from './lifeCycle'
import { PerformancePaintTiming, RumEvent, RumEventCategory } from './rum'
import { PerformancePaintTiming } from './rum'
import { trackEventCounts } from './trackEventCounts'

export interface View {
id: string
location: Location
measures: ViewMeasures
documentVersion: number
startTime: number
duration: number
}

export interface ViewMeasures {
firstContentfulPaint?: number
domInteractive?: number
Expand All @@ -16,156 +25,141 @@ export interface ViewMeasures {
userActionCount: number
}

interface ViewContext {
id: string
location: Location
}
const THROTTLE_VIEW_UPDATE_PERIOD = 3000

export let viewContext: ViewContext
export function startViewCollection(location: Location, lifeCycle: LifeCycle) {
let currentLocation = { ...location }
let endCurrentView = newView(lifeCycle, currentLocation, 0).end

const THROTTLE_VIEW_UPDATE_PERIOD = 3000
let startOrigin: number
let documentVersion: number
let viewMeasures: ViewMeasures

export function trackView(
location: Location,
lifeCycle: LifeCycle,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
const scheduleViewUpdate = throttle(monitor(() => updateView(upsertRumEvent)), THROTTLE_VIEW_UPDATE_PERIOD, {
leading: false,
// Renew view on history changes
trackHistory(() => {
if (areDifferentViews(currentLocation, location)) {
currentLocation = { ...location }
endCurrentView()
endCurrentView = newView(lifeCycle, currentLocation).end
}
})

const { reset: resetEventCounts } = trackEventCounts(lifeCycle, (eventCounts) => {
viewMeasures = { ...viewMeasures, ...eventCounts }
scheduleViewUpdate()
// Renew view on session changes
lifeCycle.subscribe(LifeCycleEventType.SESSION_WILL_RENEW, () => {
endCurrentView()
})
lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
endCurrentView = newView(lifeCycle, currentLocation).end
})
newView(location, resetEventCounts, upsertRumEvent)
trackHistory(location, resetEventCounts, upsertRumEvent)
trackTimings(lifeCycle, scheduleViewUpdate)
trackRenewSession(location, lifeCycle, resetEventCounts, upsertRumEvent)

lifeCycle.subscribe(LifeCycleEventType.WILL_UNLOAD, () => updateView(upsertRumEvent))
// End the current view on page unload
lifeCycle.subscribe(LifeCycleEventType.WILL_UNLOAD, () => {
endCurrentView()
})
}

function newView(
location: Location,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
startOrigin = !viewContext ? 0 : performance.now()
viewContext = {
id: generateUUID(),
location: { ...location },
}
documentVersion = 1
viewMeasures = {
interface ViewContext {
id: string
location: Location
}

export let viewContext: ViewContext

function newView(lifeCycle: LifeCycle, location: Location, startOrigin: number = performance.now()) {
// Setup initial values
const id = generateUUID()
let measures: ViewMeasures = {
errorCount: 0,
longTaskCount: 0,
resourceCount: 0,
userActionCount: 0,
}
resetEventCounts()
upsertViewEvent(upsertRumEvent)
}
let documentVersion = 0

function updateView(upsertRumEvent: (event: RumEvent, key: string) => void) {
documentVersion += 1
upsertViewEvent(upsertRumEvent)
}
viewContext = { id, location }

function upsertViewEvent(upsertRumEvent: (event: RumEvent, key: string) => void) {
upsertRumEvent(
{
date: getTimestamp(startOrigin),
duration: msToNs(performance.now() - startOrigin),
evt: {
category: RumEventCategory.VIEW,
},
rum: {
documentVersion,
},
view: {
measures: viewMeasures,
},
// Update the view every time the measures are changing
const scheduleViewUpdate = throttle(monitor(updateView), THROTTLE_VIEW_UPDATE_PERIOD, {
leading: false,
})
function updateMeasures(newMeasures: Partial<ViewMeasures>) {
measures = { ...measures, ...newMeasures }
scheduleViewUpdate()
}
const { stop: stopTimingsTracking } = trackTimings(lifeCycle, updateMeasures)
const { stop: stopEventCountsTracking } = trackEventCounts(lifeCycle, updateMeasures)

// Initial view update
updateView()

function updateView() {
documentVersion += 1
lifeCycle.notify(LifeCycleEventType.VIEW_COLLECTED, {
documentVersion,
id,
location,
measures,
duration: performance.now() - startOrigin,
startTime: startOrigin,
})
}

return {
end() {
stopTimingsTracking()
stopEventCountsTracking()
// Make a final view update
updateView()
},
viewContext.id
)
}
}

function trackHistory(
location: Location,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
function trackHistory(onChange: () => void) {
const originalPushState = history.pushState
history.pushState = monitor(function(this: History['pushState']) {
originalPushState.apply(this, arguments as any)
onUrlChange(location, resetEventCounts, upsertRumEvent)
onChange()
})
const originalReplaceState = history.replaceState
history.replaceState = monitor(function(this: History['replaceState']) {
originalReplaceState.apply(this, arguments as any)
onUrlChange(location, resetEventCounts, upsertRumEvent)
onChange()
})
window.addEventListener(
DOM_EVENT.POP_STATE,
monitor(() => {
onUrlChange(location, resetEventCounts, upsertRumEvent)
})
)
}

function onUrlChange(
location: Location,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
if (areDifferentViews(viewContext.location, location)) {
updateView(upsertRumEvent)
newView(location, resetEventCounts, upsertRumEvent)
}
window.addEventListener(DOM_EVENT.POP_STATE, monitor(onChange))
}

function areDifferentViews(previous: Location, current: Location) {
return previous.pathname !== current.pathname
}

function trackTimings(lifeCycle: LifeCycle, scheduleViewUpdate: () => void) {
lifeCycle.subscribe(LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED, (entry) => {
if (entry.entryType === 'navigation') {
const navigationEntry = entry as PerformanceNavigationTiming
viewMeasures = {
...viewMeasures,
domComplete: msToNs(navigationEntry.domComplete),
domContentLoaded: msToNs(navigationEntry.domContentLoadedEventEnd),
domInteractive: msToNs(navigationEntry.domInteractive),
loadEventEnd: msToNs(navigationEntry.loadEventEnd),
}
scheduleViewUpdate()
} else if (entry.entryType === 'paint' && entry.name === 'first-contentful-paint') {
const paintEntry = entry as PerformancePaintTiming
viewMeasures = {
...viewMeasures,
firstContentfulPaint: msToNs(paintEntry.startTime),
}
scheduleViewUpdate()
}
})
interface Timings {
domComplete?: number
domContentLoaded?: number
domInteractive?: number
loadEventEnd?: number
firstContentfulPaint?: number
}

function trackRenewSession(
location: Location,
lifeCycle: LifeCycle,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
lifeCycle.subscribe(LifeCycleEventType.SESSION_WILL_RENEW, () => {
updateView(upsertRumEvent)
})

lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
newView(location, resetEventCounts, upsertRumEvent)
})
function trackTimings(lifeCycle: LifeCycle, callback: (timings: Timings) => void) {
let timings: Timings = {}
const { unsubscribe: stopPerformanceTracking } = lifeCycle.subscribe(
LifeCycleEventType.PERFORMANCE_ENTRY_COLLECTED,
(entry) => {
if (entry.entryType === 'navigation') {
const navigationEntry = entry as PerformanceNavigationTiming
timings = {
...timings,
domComplete: msToNs(navigationEntry.domComplete),
domContentLoaded: msToNs(navigationEntry.domContentLoadedEventEnd),
domInteractive: msToNs(navigationEntry.domInteractive),
loadEventEnd: msToNs(navigationEntry.loadEventEnd),
}
callback(timings)
} else if (entry.entryType === 'paint' && entry.name === 'first-contentful-paint') {
const paintEntry = entry as PerformancePaintTiming
timings = {
...timings,
firstContentfulPaint: msToNs(paintEntry.startTime),
}
callback(timings)
}
}
)
return { stop: stopPerformanceTracking }
}
Loading

0 comments on commit 1a1ecf1

Please sign in to comment.