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

Change view logic to emit LifeCycle events #366

Merged
merged 14 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved

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
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
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()
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
},
viewContext.id
)
}
}

function trackHistory(
location: Location,
resetEventCounts: () => void,
upsertRumEvent: (event: RumEvent, key: string) => void
) {
function trackHistory(onChange: () => void) {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
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