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-1209] introduce "dead" and "error" frustration types #1487

Merged
merged 19 commits into from
Apr 28, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3b4a283
♻️✅ [RUMF-1209] merge trackActions tests
BenoitZugmeyer Apr 5, 2022
c4e51a8
♻️ [RUMF-1209] frustration detection groundwork
BenoitZugmeyer Apr 5, 2022
8d40c96
✨ [RUMF-1209] introduce "dead" and "error" frustration types
BenoitZugmeyer Apr 5, 2022
d75351f
✨ [RUMF-1209] add frustration_types on actions events
BenoitZugmeyer Mar 30, 2022
4ec2927
✅ [RUMF-1209] add frustration related tests to trackActions
BenoitZugmeyer Apr 5, 2022
12deb15
👌 replace 'a action' to 'an action'
BenoitZugmeyer Apr 20, 2022
4095ac3
👌 remove TrackActionsState, replace with closures
BenoitZugmeyer Apr 20, 2022
62fbb90
👌🐛 do not set a duration for dead clicks
BenoitZugmeyer Apr 20, 2022
48e4b0a
👌 rename endClick to stopClickProcessing
BenoitZugmeyer Apr 20, 2022
4c59a4e
👌 simplify PotentialAction
BenoitZugmeyer Apr 20, 2022
266ab17
✅👌 add e2e tests
BenoitZugmeyer Apr 21, 2022
46837d3
specialize `trackAction` to `trackClickActions`
BenoitZugmeyer Apr 26, 2022
9fe95f8
unify action namings
BenoitZugmeyer Apr 26, 2022
9d51fe1
👌✅ add a e2e test for multiple frustrations
BenoitZugmeyer Apr 27, 2022
94a2f0a
👌 more explicit type for id history
BenoitZugmeyer Apr 27, 2022
00689e0
👌 replace unnecessary call to isExperimentalFeatureEnabled
BenoitZugmeyer Apr 27, 2022
7dc2112
👌 use a helper to transform a Set to an Array
BenoitZugmeyer Apr 27, 2022
125a6df
🏷️ [RUMF-1209] add "ES2015" to the test app tsconfig
BenoitZugmeyer Apr 21, 2022
6b75376
Merge branch 'main' into benoit/frustration-signals-1
BenoitZugmeyer Apr 28, 2022
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Context, ClocksState, Observable } from '@datadog/browser-core'
import { resetExperimentalFeatures, updateExperimentalFeatures, relativeNow, DOM_EVENT } from '@datadog/browser-core'
import type { Context, ClocksState, Observable, Duration } from '@datadog/browser-core'
import { timeStampNow, resetExperimentalFeatures, updateExperimentalFeatures, relativeNow } from '@datadog/browser-core'
import type { Clock } from '../../../../../core/test/specHelper'
import { createNewEvent } from '../../../../../core/test/specHelper'
import type { TestSetupBuilder } from '../../../../test/specHelper'
Expand Down Expand Up @@ -33,7 +33,7 @@ function eventsCollector<T>() {
}

describe('trackActions', () => {
const { events, pushEvent } = eventsCollector()
const { events, pushEvent } = eventsCollector<AutoAction>()
let button: HTMLButtonElement
let emptyElement: HTMLHRElement
let setupBuilder: TestSetupBuilder
Expand All @@ -42,17 +42,13 @@ describe('trackActions', () => {
function mockValidatedClickAction(
domMutationObservable: Observable<void>,
clock: Clock,
target: HTMLElement,
target: HTMLElement = button,
actionDuration: number = BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY
) {
target.addEventListener(DOM_EVENT.CLICK, () => {
clock.tick(actionDuration)
// Since we don't collect dom mutations for this test, manually dispatch one
domMutationObservable.notify()
})

clock.tick(SOME_ARBITRARY_DELAY)
target.dispatchEvent(createNewEvent('click'))
clock.tick(actionDuration)
// Since we don't collect dom mutations for this test, manually dispatch one
domMutationObservable.notify()
}

beforeEach(() => {
Expand Down Expand Up @@ -80,49 +76,9 @@ describe('trackActions', () => {
setupBuilder.cleanup()
})

describe('without frustration-signals flag', () => {
it('discards pending action on view created', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, button)
expect(findActionId()).not.toBeUndefined()

lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
id: 'fake',
startClocks: jasmine.any(Object) as unknown as ClocksState,
})
clock.tick(EXPIRE_DELAY)

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

describe('with frustration-signals flag', () => {
beforeEach(() => {
updateExperimentalFeatures(['frustration-signals'])
})
afterEach(() => {
resetExperimentalFeatures()
})

it("doesn't discard pending action on view created", () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, button)
expect(findActionId()).not.toBeUndefined()

lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
id: 'fake',
startClocks: jasmine.any(Object) as unknown as ClocksState,
})
clock.tick(EXPIRE_DELAY)

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

it('starts a action when clicking on an element', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, button)
mockValidatedClickAction(domMutationObservable, clock)
expect(findActionId()).not.toBeUndefined()
clock.tick(EXPIRE_DELAY)
expect(events).toEqual([
Expand All @@ -132,7 +88,7 @@ describe('trackActions', () => {
longTaskCount: 0,
resourceCount: 0,
},
duration: BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY,
duration: BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY as Duration,
id: jasmine.any(String),
name: 'Click me',
startClocks: jasmine.any(Object),
Expand All @@ -142,16 +98,6 @@ describe('trackActions', () => {
])
})

it('discards a action when nothing happens after a click', () => {
const { clock } = setupBuilder.build()
clock.tick(SOME_ARBITRARY_DELAY)
button.click()

clock.tick(EXPIRE_DELAY)
expect(events).toEqual([])
expect(findActionId()).toBeUndefined()
})

it('discards a pending action with a negative duration', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, button, -1)
Expand All @@ -162,67 +108,77 @@ describe('trackActions', () => {
expect(findActionId()).toBeUndefined()
})

it('ignores a actions if it fails to find a name', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, emptyElement)
expect(findActionId()).toBeUndefined()
clock.tick(EXPIRE_DELAY)

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

it('should keep track of previously validated actions', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, button)
mockValidatedClickAction(domMutationObservable, clock)
const actionStartTime = relativeNow()
clock.tick(EXPIRE_DELAY)

expect(findActionId(actionStartTime)).not.toBeUndefined()
})
})

describe('newAction', () => {
let setupBuilder: TestSetupBuilder
const { events, pushEvent } = eventsCollector<AutoAction>()
it('counts errors occurring during the action', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
const collectedRumEvent = { type: RumEventType.ERROR } as RumEvent & Context

function newClick(name: string, attribute = 'title') {
const button = document.createElement('button')
button.setAttribute(attribute, name)
document.getElementById('root')!.appendChild(button)
button.click()
}
mockValidatedClickAction(domMutationObservable, clock)

beforeEach(() => {
const root = document.createElement('root')
root.setAttribute('id', 'root')
document.body.appendChild(root)
setupBuilder = setup()
.withFakeClock()
.beforeBuild(({ lifeCycle, domMutationObservable, configuration }) =>
trackActions(lifeCycle, domMutationObservable, configuration)
)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)

clock.tick(EXPIRE_DELAY)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)

expect(events.length).toBe(1)
const action = events[0]
expect(action.counts).toEqual({
errorCount: 2,
longTaskCount: 0,
resourceCount: 0,
})
})

afterEach(() => {
const root = document.getElementById('root')!
root.parentNode!.removeChild(root)
setupBuilder.cleanup()
it('should take the name from user-configured attribute', () => {
const { domMutationObservable, clock } = setupBuilder
.withConfiguration({ actionNameAttribute: 'data-my-custom-attribute' })
.build()

button.setAttribute('data-my-custom-attribute', 'test-1')
mockValidatedClickAction(domMutationObservable, clock)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].name).toBe('test-1')
})

describe('without frustration-signals flag', () => {
it('ignores any starting action while another one is ongoing', () => {
it('discards pending action on view created', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, pushEvent)
mockValidatedClickAction(domMutationObservable, clock)
expect(findActionId()).not.toBeUndefined()

newClick('test-1')
newClick('test-2')
lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
id: 'fake',
startClocks: jasmine.any(Object) as unknown as ClocksState,
})
clock.tick(EXPIRE_DELAY)

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

it('ignores any starting action while another one is ongoing', () => {
const { domMutationObservable, clock } = setupBuilder.build()

clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()
const firstClickTimeStamp = timeStampNow()
mockValidatedClickAction(domMutationObservable, clock)
mockValidatedClickAction(domMutationObservable, clock)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].name).toBe('test-1')
expect(events[0].startClocks.timeStamp).toBe(firstClickTimeStamp)
})
})

Expand All @@ -234,60 +190,51 @@ describe('newAction', () => {
resetExperimentalFeatures()
})

it('collect actions even if another one is ongoing', () => {
it("doesn't discard pending action on view created", () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, pushEvent)
mockValidatedClickAction(domMutationObservable, clock)
expect(findActionId()).not.toBeUndefined()

lifeCycle.notify(LifeCycleEventType.VIEW_CREATED, {
id: 'fake',
startClocks: jasmine.any(Object) as unknown as ClocksState,
})
clock.tick(EXPIRE_DELAY)

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

newClick('test-1')
newClick('test-2')
it('collect actions even if another one is ongoing', () => {
const { domMutationObservable, clock } = setupBuilder.build()

clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()
const firstClickTimeStamp = timeStampNow()
mockValidatedClickAction(domMutationObservable, clock)
const secondClickTimeStamp = timeStampNow()
mockValidatedClickAction(domMutationObservable, clock)

clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(2)
expect(events[0].name).toBe('test-1')
expect(events[1].name).toBe('test-2')
expect(events[0].startClocks.timeStamp).toBe(firstClickTimeStamp)
expect(events[1].startClocks.timeStamp).toBe(secondClickTimeStamp)
})
})

it('counts errors occurring during the action', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder.build()
const collectedRumEvent = { type: RumEventType.ERROR } as RumEvent & Context
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, pushEvent)

newClick('test-1')

lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)
clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)
it('discards a action when nothing happens after a click', () => {
BenoitZugmeyer marked this conversation as resolved.
Show resolved Hide resolved
const { clock } = setupBuilder.build()
clock.tick(SOME_ARBITRARY_DELAY)
button.click()

clock.tick(EXPIRE_DELAY)
lifeCycle.notify(LifeCycleEventType.RUM_EVENT_COLLECTED, collectedRumEvent)

expect(events.length).toBe(1)
const action = events[0]
expect(action.counts).toEqual({
errorCount: 2,
longTaskCount: 0,
resourceCount: 0,
})
expect(events).toEqual([])
expect(findActionId()).toBeUndefined()
})

it('should take the name from user-configured attribute', () => {
const { lifeCycle, domMutationObservable, clock } = setupBuilder
.withConfiguration({ actionNameAttribute: 'data-my-custom-attribute' })
.build()
lifeCycle.subscribe(LifeCycleEventType.AUTO_ACTION_COMPLETED, pushEvent)

newClick('test-1', 'data-my-custom-attribute')

clock.tick(BEFORE_PAGE_ACTIVITY_VALIDATION_DELAY)
domMutationObservable.notify()

it('ignores a actions if it fails to find a name', () => {
const { domMutationObservable, clock } = setupBuilder.build()
mockValidatedClickAction(domMutationObservable, clock, emptyElement)
expect(findActionId()).toBeUndefined()
clock.tick(EXPIRE_DELAY)
expect(events.length).toBe(1)
expect(events[0].name).toBe('test-1')

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