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-373] Add View load duration and load type #388

Merged
merged 48 commits into from
May 20, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d5e20f7
:sparkles: add page load UA
mquentin Apr 21, 2020
3f0b7fe
rm inputEventCounts
mquentin Apr 21, 2020
a24903d
:sparkles: fix duplicated UserActionType.LOAD_VIEW sent on update
mquentin Apr 21, 2020
f8c40a3
:sparkles: clean currentUserAction = undefined process
mquentin Apr 21, 2020
d8d12a3
Merge branch 'maxime.quentin/RUMF-384SDKcollectPageLoad' into maxime.…
mquentin Apr 29, 2020
f99052e
add view loading state in the UA lifecycle
mquentin Apr 29, 2020
6eeb910
cancel current UA in case of load view
mquentin Apr 29, 2020
4ade8fd
:sparkles: collect loadDuration
mquentin Apr 30, 2020
8c28472
:sparkles: add view load type
mquentin Apr 30, 2020
3dff2fd
:white_check_mark: add soft, hard load tests
mquentin May 4, 2020
52d880b
:white_check_mark: add load duration View update subscription
mquentin May 4, 2020
2278761
rename load types
mquentin May 4, 2020
53d468d
:white_check_mark: add UA vs view load tests
mquentin May 4, 2020
a843204
Merge branch 'master' into maxime.quentin/SDKviewAddLoadDurationAndLo…
mquentin May 4, 2020
64c39ef
:ok_hand: v1
mquentin May 5, 2020
86a9b74
:truck: create the new trackPageActivities module and it generic wait…
mquentin May 5, 2020
39146fa
:ok_hand: v2
mquentin May 5, 2020
6b8d47b
:pencil: about UA lifecycle
mquentin May 5, 2020
8a6994d
:white_check_mark: add view id checks
mquentin May 5, 2020
bfa22eb
:white_check_mark: add test when missing id
mquentin May 5, 2020
c1162c4
:sparkles: Collect new UA when the page is loading
mquentin May 6, 2020
f49c377
rename load types
mquentin May 6, 2020
35e4ed5
:ok_hand: add trackPageActivities.spec.ts test file
mquentin May 7, 2020
00d5f00
:ok_hand: add stop processes to newUserAction and newViewLoading
mquentin May 11, 2020
12e185c
:ok_hand: split load waitPageactivity and UA waitpageactivity
mquentin May 11, 2020
da8801b
:ok_hand: move lifecycle doc
mquentin May 11, 2020
02f1c74
:ok_hand: rm unused exports
mquentin May 11, 2020
d8779f0
:ok_hand: refactor stopCurrentUserAction process
mquentin May 11, 2020
8aef86e
:ok_hand: move stopCurrentUserAction() at the newView Level
mquentin May 11, 2020
34f9ee8
:ok_hand: add newView to the test scenario
mquentin May 11, 2020
946104d
:ok_hand: fix lint
mquentin May 12, 2020
e53c670
:ok_hand: fix user action stop process
mquentin May 12, 2020
761ef8a
:ok_hand: improve tests and reformat waitPageActivitiesCompletion cal…
mquentin May 13, 2020
bfb1503
:ok_hand: move tests into waitPageActivitiesCompletion into trackPage…
mquentin May 13, 2020
c2999e3
:ok_hand: nit
mquentin May 13, 2020
8131f75
:ok_hand: improve view loading type testing
mquentin May 13, 2020
94fc77f
:ok_hand: remove stop ua function dependency in the ViewCollection
mquentin May 14, 2020
3a64616
Merge branch 'master' into maxime.quentin/SDKviewAddLoadDurationAndLo…
mquentin May 14, 2020
6cb2807
🎨 rename laodDuration to loadTime
mquentin May 14, 2020
75d0f7a
✨ add the loadTime and loadType in the RumEvent batch
mquentin May 14, 2020
4614cfd
👌 implement new reviews
mquentin May 15, 2020
1eaf083
✅ clarify loading time tests
mquentin May 15, 2020
7035c08
✅ remove view collection dependence from user action testing
mquentin May 18, 2020
aa2180d
✅ clarify the LifeCycleEventType.VIEW_COLLECTED subscribe
mquentin May 18, 2020
50fdd0b
Merge branch 'master' into maxime.quentin/SDKviewAddLoadDurationAndLo…
mquentin May 18, 2020
502a7d2
♻️ stop current user action
mquentin May 18, 2020
a43f468
Merge branch 'master' into maxime.quentin/SDKviewAddLoadDurationAndLo…
mquentin May 18, 2020
65bb6c8
Merge branch 'master' into maxime.quentin/SDKviewAddLoadDurationAndLo…
mquentin May 20, 2020
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
5 changes: 4 additions & 1 deletion packages/rum/src/lifeCycle.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { ErrorMessage, RequestCompleteEvent, RequestStartEvent } from '@datadog/browser-core'
import { UserAction } from './userActionCollection'
import { View } from './viewCollection'
import { View, ViewLoad } from './viewCollection'

export enum LifeCycleEventType {
ERROR_COLLECTED,
PERFORMANCE_ENTRY_COLLECTED,
USER_ACTION_COLLECTED,
VIEW_COLLECTED,
VIEW_LOAD_COMPLETED,
REQUEST_STARTED,
REQUEST_COMPLETED,
SESSION_RENEWED,
Expand All @@ -28,6 +29,7 @@ export class LifeCycle {
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.VIEW_LOAD_COMPLETED, data: ViewLoad): void
notify(
eventType:
| LifeCycleEventType.SESSION_RENEWED
Expand All @@ -54,6 +56,7 @@ export class LifeCycle {
): 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.VIEW_LOAD_COMPLETED, callback: (data: ViewLoad) => void): Subscription
subscribe(
eventType:
| LifeCycleEventType.SESSION_RENEWED
Expand Down
81 changes: 68 additions & 13 deletions packages/rum/src/userActionCollection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Context, DOM_EVENT, generateUUID, monitor, Observable } from '@datadog/browser-core'
import { getElementContent } from './getElementContent'
import { LifeCycle, LifeCycleEventType, Subscription } from './lifeCycle'
import { trackEventCounts } from './trackEventCounts'
import { EventCounts, trackEventCounts } from './trackEventCounts'
mquentin marked this conversation as resolved.
Show resolved Hide resolved
import { View } from './viewCollection'

// Automatic user action collection lifecycle overview:
mquentin marked this conversation as resolved.
Show resolved Hide resolved
//
Expand Down Expand Up @@ -66,55 +67,110 @@ export interface AutoUserAction {
export type UserAction = CustomUserAction | AutoUserAction

export function startUserActionCollection(lifeCycle: LifeCycle) {
const subscriptions: Subscription[] = []
mquentin marked this conversation as resolved.
Show resolved Hide resolved
let currentViewId: string | undefined

addEventListener(DOM_EVENT.CLICK, processClick, { capture: true })
function processClick(event: Event) {
if (!(event.target instanceof Element)) {
return
}

newUserAction(lifeCycle, UserActionType.CLICK, getElementContent(event.target))
}

addEventListener(DOM_EVENT.CLICK, processClick, { capture: true })
subscriptions.push(lifeCycle.subscribe(LifeCycleEventType.VIEW_COLLECTED, processViewLoading))
mquentin marked this conversation as resolved.
Show resolved Hide resolved
function processViewLoading(loadedView: View) {
if (!loadedView || loadedView.id === currentViewId) {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
return
}
currentViewId = loadedView.id
newViewLoading(lifeCycle, loadedView.location.pathname, loadedView.startTime)
}

return {
stop() {
removeEventListener(DOM_EVENT.CLICK, processClick, { capture: true })
subscriptions.forEach((s) => s.unsubscribe())
},
}
}

let currentUserAction: { id: string; startTime: number } | undefined
interface PartialUserAction {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
id: string
startTime: number
type: UserActionType
name: string
}
let currentUserAction: PartialUserAction | undefined

interface ViewLoadingState {
pathname: string
startTime: number
stopPageActivitiesTracking: () => void
}
let currentViewLoadingState: ViewLoadingState | undefined

function newViewLoading(lifeCycle: LifeCycle, pathname: string, startTime: number) {
// Cancel current user action
currentUserAction = undefined
mquentin marked this conversation as resolved.
Show resolved Hide resolved

if (currentViewLoadingState) {
currentViewLoadingState.stopPageActivitiesTracking()
mquentin marked this conversation as resolved.
Show resolved Hide resolved
}

const { observable: pageActivitiesObservable, stop: stopPageActivitiesTracking } = trackPageActivities(lifeCycle)
currentViewLoadingState = {
pathname,
startTime,
stopPageActivitiesTracking,
}

waitUserActionCompletion(pageActivitiesObservable, (endTime) => {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
stopPageActivitiesTracking()
if (currentViewLoadingState !== undefined) {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
// Validation timeout completion does not return an end time
const loadingEndTime = endTime || performance.now()
mquentin marked this conversation as resolved.
Show resolved Hide resolved
lifeCycle.notify(LifeCycleEventType.VIEW_LOAD_COMPLETED, {
duration: loadingEndTime - currentViewLoadingState.startTime,
startTime: currentViewLoadingState.startTime,
})
}
currentViewLoadingState = undefined
})
}

function newUserAction(lifeCycle: LifeCycle, type: UserActionType, name: string) {
if (currentUserAction) {
// Discard any new user action if another one is already occuring.
if (currentViewLoadingState || currentUserAction) {
// Discard any new click user action if another one is already occuring.
mquentin marked this conversation as resolved.
Show resolved Hide resolved
mquentin marked this conversation as resolved.
Show resolved Hide resolved
// Discard any new user action if page is in a loading state.
return
}

const id = generateUUID()
const startTime = performance.now()
currentUserAction = { id, startTime }
currentUserAction = { id, startTime, type, name }

const { observable: pageActivitiesObservable, stop: stopPageActivitiesTracking } = trackPageActivities(lifeCycle)
const { eventCounts, stop: stopEventCountsTracking } = trackEventCounts(lifeCycle)

waitUserActionCompletion(pageActivitiesObservable, (endTime) => {
stopPageActivitiesTracking()
stopEventCountsTracking()
if (endTime !== undefined) {
if (endTime !== undefined && currentUserAction !== undefined) {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
mquentin marked this conversation as resolved.
Show resolved Hide resolved
lifeCycle.notify(LifeCycleEventType.USER_ACTION_COLLECTED, {
id,
name,
startTime,
type,
duration: endTime - startTime,
duration: endTime - currentUserAction.startTime,
mquentin marked this conversation as resolved.
Show resolved Hide resolved
measures: {
errorCount: eventCounts.errorCount,
longTaskCount: eventCounts.longTaskCount,
resourceCount: eventCounts.resourceCount,
},
name: currentUserAction.name,
startTime: currentUserAction.startTime,
type: currentUserAction.type,
mquentin marked this conversation as resolved.
Show resolved Hide resolved
})
}
currentUserAction = undefined
mquentin marked this conversation as resolved.
Show resolved Hide resolved
})
}

Expand Down Expand Up @@ -212,7 +268,6 @@ function waitUserActionCompletion(
clearTimeout(validationTimeoutId)
clearTimeout(idleTimeoutId)
clearTimeout(maxDurationTimeoutId)
currentUserAction = undefined
completionCallback(endTime)
}
}
Expand Down
43 changes: 39 additions & 4 deletions packages/rum/src/viewCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export interface View {
documentVersion: number
startTime: number
duration: number
loadDuration?: number
loadType: ViewLoadType
}

export interface ViewLoad {
startTime: number
mquentin marked this conversation as resolved.
Show resolved Hide resolved
duration: number
}

export interface ViewMeasures {
Expand All @@ -26,26 +33,33 @@ export interface ViewMeasures {
userActionCount: number
}

export enum ViewLoadType {
INITIAL_PAGE_LOAD = 'initial page load',
ROUTE_CHANGE = 'route change',
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
}

export 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)
let currentViewLoadType: ViewLoadType = ViewLoadType.INITIAL_PAGE_LOAD
mquentin marked this conversation as resolved.
Show resolved Hide resolved
let currentView = newView(lifeCycle, currentLocation, session, currentViewLoadType, startOrigin)

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

// Renew view on session renewal
lifeCycle.subscribe(LifeCycleEventType.SESSION_RENEWED, () => {
currentView.end()
currentView = newView(lifeCycle, currentLocation, session)
currentView = newView(lifeCycle, currentLocation, session, currentViewLoadType)
})

// End the current view on page unload
Expand All @@ -58,6 +72,7 @@ interface ViewContext {
id: string
location: Location
sessionId: string | undefined
loadType: ViewLoadType
}

export let viewContext: ViewContext
Expand All @@ -66,6 +81,7 @@ function newView(
lifeCycle: LifeCycle,
location: Location,
session: RumSession,
loadType: ViewLoadType,
startOrigin: number = performance.now()
) {
// Setup initial values
Expand All @@ -77,8 +93,9 @@ function newView(
userActionCount: 0,
}
let documentVersion = 0
let loadDuration: number

viewContext = { id, location, sessionId: session.getId() }
viewContext = { id, location, loadType, sessionId: session.getId() }
mquentin marked this conversation as resolved.
Show resolved Hide resolved

// Update the view every time the measures are changing
const { throttled: scheduleViewUpdate, stop: stopScheduleViewUpdate } = throttle(
Expand All @@ -95,6 +112,12 @@ function newView(
const { stop: stopTimingsTracking } = trackTimings(lifeCycle, updateMeasures)
const { stop: stopEventCountsTracking } = trackEventCounts(lifeCycle, updateMeasures)

function updateLoadDuration(loadDurationValue: number) {
loadDuration = loadDurationValue
scheduleViewUpdate()
}
const { stop: stopLoadDurationTracking } = trackLoadDuration(lifeCycle, updateLoadDuration)
mquentin marked this conversation as resolved.
Show resolved Hide resolved

// Initial view update
updateView()

Expand All @@ -103,6 +126,8 @@ function newView(
lifeCycle.notify(LifeCycleEventType.VIEW_COLLECTED, {
documentVersion,
id,
loadDuration,
loadType,
location,
measures,
duration: performance.now() - startOrigin,
Expand Down Expand Up @@ -175,3 +200,13 @@ function trackTimings(lifeCycle: LifeCycle, callback: (timings: Timings) => void
)
return { stop: stopPerformanceTracking }
}

function trackLoadDuration(lifeCycle: LifeCycle, callback: (loadDurationValue: number) => void) {
const { unsubscribe: stopViewLoadTracking } = lifeCycle.subscribe(
LifeCycleEventType.VIEW_LOAD_COMPLETED,
(viewLoad: ViewLoad) => {
callback(viewLoad.duration)
}
)
return { stop: stopViewLoadTracking }
}
63 changes: 61 additions & 2 deletions packages/rum/test/userActionCollection.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { DOM_EVENT, ErrorMessage, Observable, RequestCompleteEvent } from '@datadog/browser-core'
import {
DOM_EVENT,
ErrorMessage,
getHash,
getPathName,
getSearch,
Observable,
RequestCompleteEvent,
} from '@datadog/browser-core'
import { LifeCycle, LifeCycleEventType } from '../src/lifeCycle'
import {
$$tests,
Expand All @@ -13,11 +21,14 @@ import {
UserActionType,
} from '../src/userActionCollection'
const { waitUserActionCompletion, trackPageActivities, resetUserAction, newUserAction } = $$tests
import { View, ViewLoadType } from '../src/viewCollection'

// Used to wait some time after the creation of a user action
const BEFORE_USER_ACTION_VALIDATION_DELAY = USER_ACTION_VALIDATION_DELAY * 0.8
// Used to wait some time before the (potential) end of a user action
const BEFORE_USER_ACTION_END_DELAY = USER_ACTION_END_DELAY * 0.8
// Used to wait some time after the (potential) end of a user action
const AFTER_USER_ACTION_END_DELAY = USER_ACTION_END_DELAY * 1.2
// Used to wait some time but it doesn't matter how much.
const SOME_ARBITRARY_DELAY = 50
// A long delay used to wait after any user action is finished.
Expand Down Expand Up @@ -81,7 +92,7 @@ describe('startUserActionCollection', () => {
userActionCollectionSubscription.stop()
})

it('starts a user action when clicking on an element', () => {
function mockValidatedClickUserAction() {
button.addEventListener(DOM_EVENT.CLICK, () => {
clock.tick(BEFORE_USER_ACTION_VALIDATION_DELAY)
// Since we don't collect dom mutations for this test, manually dispatch one
Expand All @@ -92,6 +103,54 @@ describe('startUserActionCollection', () => {
button.click()

clock.expire()
}

it('cancels user action on view loading', () => {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
mquentin marked this conversation as resolved.
Show resolved Hide resolved
const fakeLocation: Partial<Location> = { pathname: '/foo' }
const mockView: Partial<View> = {
documentVersion: 0,
id: 'foo',
location: fakeLocation as Location,
}
lifeCycle.notify(LifeCycleEventType.VIEW_COLLECTED, mockView as View)

mockValidatedClickUserAction()

expect(events).toEqual([])
})

it('starts a user action when clicking on an element after a view loading', () => {
mquentin marked this conversation as resolved.
Show resolved Hide resolved
const fakeLocation: Partial<Location> = { pathname: '/foo' }
const mockView: Partial<View> = {
documentVersion: 0,
id: 'foo',
location: fakeLocation as Location,
}
lifeCycle.notify(LifeCycleEventType.VIEW_COLLECTED, mockView as View)

// View loads are completed like a UA would have been completed when there is no activity for a given time
clock.tick(AFTER_USER_ACTION_END_DELAY)
clock.expire()
mquentin marked this conversation as resolved.
Show resolved Hide resolved

mockValidatedClickUserAction()
expect(events).toEqual([
{
duration: BEFORE_USER_ACTION_VALIDATION_DELAY,
id: jasmine.any(String),
measures: {
errorCount: 0,
longTaskCount: 0,
resourceCount: 0,
},
name: 'Click me',
startTime: jasmine.any(Number),
type: UserActionType.CLICK,
},
])
})

it('starts a user action when clicking on an element', () => {
mockValidatedClickUserAction()
expect(events).toEqual([
{
duration: BEFORE_USER_ACTION_VALIDATION_DELAY,
Expand Down
Loading