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 10 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
17 changes: 4 additions & 13 deletions packages/core/src/transport.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import lodashMerge from 'lodash.merge'

import { monitor } from './internalMonitoring'
import { Context, DOM_EVENT, jsonStringify, objectValues } from './utils'
import { Context, DOM_EVENT, jsonStringify, noop, objectValues } from './utils'

/**
* Use POST request without content type to:
Expand Down Expand Up @@ -33,7 +33,6 @@ function addBatchTime(url: string) {
}

export class Batch<T> {
private beforeFlushOnUnloadHandlers: Array<() => void> = []
private pushOnlyBuffer: string[] = []
private upsertBuffer: { [key: string]: string } = {}
private bufferBytesSize = 0
Expand All @@ -45,7 +44,8 @@ export class Batch<T> {
private bytesLimit: number,
private maxMessageSize: number,
private flushTimeout: number,
private contextProvider: () => Context
private contextProvider: () => Context,
private beforeUnloadCallback: () => void = noop
) {
this.flushOnVisibilityHidden()
this.flushPeriodically()
Expand All @@ -59,10 +59,6 @@ export class Batch<T> {
this.addOrUpdate(message, key)
}

beforeFlushOnUnload(handler: () => void) {
this.beforeFlushOnUnloadHandlers.push(handler)
}

flush() {
if (this.bufferMessageCount !== 0) {
const messages = [...this.pushOnlyBuffer, ...objectValues(this.upsertBuffer)]
Expand Down Expand Up @@ -160,12 +156,7 @@ export class Batch<T> {
* register first to be sure to be called before flush on beforeunload
* caveat: unload can still be canceled by another listener
*/
window.addEventListener(
DOM_EVENT.BEFORE_UNLOAD,
monitor(() => {
this.beforeFlushOnUnloadHandlers.forEach((handler) => handler())
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
})
)
window.addEventListener(DOM_EVENT.BEFORE_UNLOAD, monitor(this.beforeUnloadCallback))

/**
* Only event that guarantee to fire on mobile devices when the page transitions to background state
Expand Down
9 changes: 8 additions & 1 deletion packages/rum/src/lifeCycle.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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_RENEWED,
RESOURCE_ADDED_TO_BATCH,
DOM_MUTATED,
WILL_UNLOAD,
}

export interface Subscription {
Expand All @@ -24,11 +27,13 @@ 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_RENEWED
| LifeCycleEventType.RESOURCE_ADDED_TO_BATCH
| LifeCycleEventType.DOM_MUTATED
| LifeCycleEventType.WILL_UNLOAD
): void
notify(eventType: LifeCycleEventType, data?: any) {
const eventCallbacks = this.callbacks[eventType]
Expand All @@ -48,11 +53,13 @@ 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_RENEWED
| LifeCycleEventType.RESOURCE_ADDED_TO_BATCH
| LifeCycleEventType.DOM_MUTATED,
| LifeCycleEventType.DOM_MUTATED
| LifeCycleEventType.WILL_UNLOAD,
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
callback: () => void
): Subscription
subscribe(eventType: LifeCycleEventType, callback: (data?: any) => void) {
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, session)
const [requestStartObservable, requestCompleteObservable] = startRequestCollection()
startPerformanceCollection(lifeCycle, session)
startDOMMutationCollection(lifeCycle)
Expand Down
35 changes: 29 additions & 6 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 './viewTracker'
import { viewContext, ViewMeasures } from './viewCollection'

export interface PerformancePaintTiming extends PerformanceEntry {
entryType: 'paint'
Expand Down Expand Up @@ -182,10 +182,11 @@ export function startRum(
url: viewContext.location.href,
},
}),
() => globalContext
() => globalContext,
() => lifeCycle.notify(LifeCycleEventType.WILL_UNLOAD)
)

trackView(window.location, lifeCycle, session, batch.upsertRumEvent, batch.beforeFlushOnUnload)
trackView(lifeCycle, batch.upsertRumEvent)
trackErrors(lifeCycle, batch.addRumEvent)
trackRequests(configuration, lifeCycle, session, batch.addRumEvent)
trackPerformanceTiming(configuration, lifeCycle, batch.addRumEvent)
Expand Down Expand Up @@ -221,23 +222,24 @@ function startRumBatch(
configuration: Configuration,
session: RumSession,
rumContextProvider: () => Context,
globalContextProvider: () => Context
globalContextProvider: () => Context,
beforeUnloadCallback: () => void
) {
const batch = new Batch<Context>(
new HttpRequest(configuration.rumEndpoint, configuration.batchBytesLimit, true),
configuration.maxBatchSize,
configuration.batchBytesLimit,
configuration.maxMessageSize,
configuration.flushTimeout,
() => lodashMerge(withSnakeCaseKeys(rumContextProvider()), globalContextProvider())
() => lodashMerge(withSnakeCaseKeys(rumContextProvider()), globalContextProvider()),
beforeUnloadCallback
)
return {
addRumEvent: (event: RumEvent, context?: Context) => {
if (session.isTracked()) {
batch.add({ ...context, ...withSnakeCaseKeys((event as unknown) as Context) })
}
},
beforeFlushOnUnload: (handler: () => void) => batch.beforeFlushOnUnload(handler),
upsertRumEvent: (event: RumEvent, key: string) => {
if (session.isTracked()) {
batch.upsert(withSnakeCaseKeys((event as unknown) as Context), key)
Expand All @@ -246,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
}
}
},
}
}
171 changes: 171 additions & 0 deletions packages/rum/src/viewCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { DOM_EVENT, generateUUID, monitor, msToNs, throttle } from '@datadog/browser-core'

import { LifeCycle, LifeCycleEventType } from './lifeCycle'
import { PerformancePaintTiming } from './rum'
import { RumSession } from './rumSession'
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
domContentLoaded?: number
domComplete?: number
loadEventEnd?: number
errorCount: number
resourceCount: number
longTaskCount: number
userActionCount: number
}

const THROTTLE_VIEW_UPDATE_PERIOD = 3000

export function startViewCollection(location: Location, lifeCycle: LifeCycle, session: RumSession) {
let currentLocation = { ...location }
const startOrigin = 0
let currentView = newView(lifeCycle, currentLocation, session, startOrigin)

// Renew view on history changes
trackHistory(() => {
if (areDifferentViews(currentLocation, location)) {
currentLocation = { ...location }
currentView.end()
currentView = newView(lifeCycle, currentLocation, session)
}
})

// Renew view on session changes
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
currentView.end()
currentView = newView(lifeCycle, currentLocation, session)
})

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

interface ViewContext {
id: string
location: Location
sessionId: string | undefined
}

export let viewContext: ViewContext

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

viewContext = { id, location, sessionId: session.getId() }

// 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
},
}
}

function trackHistory(onHistoryChange: () => void) {
const originalPushState = history.pushState
history.pushState = monitor(function(this: History['pushState']) {
originalPushState.apply(this, arguments as any)
onHistoryChange()
})
const originalReplaceState = history.replaceState
history.replaceState = monitor(function(this: History['replaceState']) {
originalReplaceState.apply(this, arguments as any)
onHistoryChange()
})
window.addEventListener(DOM_EVENT.POP_STATE, monitor(onHistoryChange))
}

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

interface Timings {
domComplete?: number
domContentLoaded?: number
domInteractive?: number
loadEventEnd?: number
firstContentfulPaint?: number
}

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