diff --git a/packages/rum-core/src/domain/action/computeFrustration.spec.ts b/packages/rum-core/src/domain/action/computeFrustration.spec.ts
index fad5cee0b3..c97bf79097 100644
--- a/packages/rum-core/src/domain/action/computeFrustration.spec.ts
+++ b/packages/rum-core/src/domain/action/computeFrustration.spec.ts
@@ -68,6 +68,13 @@ describe('computeFrustration', () => {
expect(getFrustrations(clicks[1])).toEqual([])
})
+ it('does not add a dead frustration when clicking to scroll', () => {
+ clicks[0] = createFakeClick({ userActivity: { scroll: true } })
+ clicks[1] = createFakeClick()
+ computeFrustration(clicks, rageClick)
+ expect(getFrustrations(clicks[1])).toEqual([])
+ })
+
it('adds an error frustration to clicks that have an error', () => {
clicks[1] = createFakeClick({ hasError: true })
computeFrustration(clicks, rageClick)
@@ -101,6 +108,12 @@ describe('isRage', () => {
)
})
+ it('does not consider rage when at least one click is related to a "scroll" event', () => {
+ expect(isRage([createFakeClick(), createFakeClick({ userActivity: { scroll: true } }), createFakeClick()])).toBe(
+ false
+ )
+ })
+
it('does not consider as rage two clicks happening at the same time', () => {
expect(isRage([createFakeClick(), createFakeClick()])).toBe(false)
})
@@ -143,6 +156,10 @@ describe('isDead', () => {
expect(isDead(createFakeClick({ hasPageActivity: false, userActivity: { input: true } }))).toBe(false)
})
+ it('does not consider as dead when the click is related to a "scroll" event', () => {
+ expect(isDead(createFakeClick({ hasPageActivity: false, userActivity: { scroll: true } }))).toBe(false)
+ })
+
for (const { element, expected } of [
{ element: '', expected: false },
{ element: '', expected: false },
diff --git a/packages/rum-core/src/domain/action/computeFrustration.ts b/packages/rum-core/src/domain/action/computeFrustration.ts
index 36741c549f..1250d448af 100644
--- a/packages/rum-core/src/domain/action/computeFrustration.ts
+++ b/packages/rum-core/src/domain/action/computeFrustration.ts
@@ -18,6 +18,7 @@ export function computeFrustration(clicks: Click[], rageClick: Click) {
}
const hasSelectionChanged = clicks.some((click) => click.getUserActivity().selection)
+ const didScrollOccur = clicks.some((click) => click.getUserActivity().scroll)
clicks.forEach((click) => {
if (click.hasError) {
click.addFrustration(FrustrationType.ERROR_CLICK)
@@ -25,7 +26,9 @@ export function computeFrustration(clicks: Click[], rageClick: Click) {
if (
isDead(click) &&
// Avoid considering clicks part of a double-click or triple-click selections as dead clicks
- !hasSelectionChanged
+ !hasSelectionChanged &&
+ // Avoid considering clicks that resulted in a scroll as dead clicks
+ !didScrollOccur
) {
click.addFrustration(FrustrationType.DEAD_CLICK)
}
@@ -34,7 +37,7 @@ export function computeFrustration(clicks: Click[], rageClick: Click) {
}
export function isRage(clicks: Click[]) {
- if (clicks.some((click) => click.getUserActivity().selection)) {
+ if (clicks.some((click) => click.getUserActivity().selection || click.getUserActivity().scroll)) {
return false
}
for (let i = 0; i < clicks.length - (MIN_CLICKS_PER_SECOND_TO_CONSIDER_RAGE - 1); i += 1) {
@@ -65,7 +68,7 @@ const DEAD_CLICK_EXCLUDE_SELECTOR =
'a[href] *'
export function isDead(click: Click) {
- if (click.hasPageActivity || click.getUserActivity().input) {
+ if (click.hasPageActivity || click.getUserActivity().input || click.getUserActivity().scroll) {
return false
}
return !elementMatches(click.event.target, DEAD_CLICK_EXCLUDE_SELECTOR)
diff --git a/packages/rum-core/src/domain/action/listenActionEvents.spec.ts b/packages/rum-core/src/domain/action/listenActionEvents.spec.ts
index e9663bcb41..88fc4bfbef 100644
--- a/packages/rum-core/src/domain/action/listenActionEvents.spec.ts
+++ b/packages/rum-core/src/domain/action/listenActionEvents.spec.ts
@@ -207,6 +207,41 @@ describe('listenActionEvents', () => {
}
})
+ describe('scroll', () => {
+ it('click that do not trigger a scroll event should not report scroll user activity', () => {
+ emulateClick()
+ expect(hasScrollUserActivity()).toBe(false)
+ })
+
+ it('click that triggers a scroll event during the click should report a scroll user activity', () => {
+ emulateClick({
+ beforeMouseUp() {
+ emulateScrollEvent()
+ },
+ })
+ expect(hasScrollUserActivity()).toBe(true)
+ })
+
+ it('click that triggers a scroll event slightly after the click should report a scroll user activity', () => {
+ emulateClick()
+ emulateScrollEvent()
+ expect(hasScrollUserActivity()).toBe(true)
+ })
+
+ it('scroll events that precede clicks should not be taken into account', () => {
+ emulateScrollEvent()
+ emulateClick()
+ expect(hasScrollUserActivity()).toBe(false)
+ })
+
+ function emulateScrollEvent() {
+ window.dispatchEvent(createNewEvent('scroll'))
+ }
+ function hasScrollUserActivity() {
+ return actionEventsHooks.onPointerUp.calls.mostRecent().args[2]().scroll
+ }
+ })
+
function emulateClick({
beforeMouseUp,
target = document.body,
diff --git a/packages/rum-core/src/domain/action/listenActionEvents.ts b/packages/rum-core/src/domain/action/listenActionEvents.ts
index 4574ceff4b..76e75a0f4b 100644
--- a/packages/rum-core/src/domain/action/listenActionEvents.ts
+++ b/packages/rum-core/src/domain/action/listenActionEvents.ts
@@ -6,6 +6,7 @@ export type MouseEventOnElement = PointerEvent & { target: Element }
export interface UserActivity {
selection: boolean
input: boolean
+ scroll: boolean
}
export interface ActionEventsHooks {
onPointerDown: (event: MouseEventOnElement) => ClickContext | undefined
@@ -20,6 +21,7 @@ export function listenActionEvents(
let userActivity: UserActivity = {
selection: false,
input: false,
+ scroll: false,
}
let clickContext: ClickContext | undefined
@@ -34,6 +36,7 @@ export function listenActionEvents(
userActivity = {
selection: false,
input: false,
+ scroll: false,
}
clickContext = onPointerDown(event)
}
@@ -53,6 +56,16 @@ export function listenActionEvents(
{ capture: true }
),
+ addEventListener(
+ configuration,
+ window,
+ DOM_EVENT.SCROLL,
+ () => {
+ userActivity.scroll = true
+ },
+ { capture: true, passive: true }
+ ),
+
addEventListener(
configuration,
window,
diff --git a/packages/rum-core/test/createFakeClick.ts b/packages/rum-core/test/createFakeClick.ts
index 6fc65016c3..9c5a439f67 100644
--- a/packages/rum-core/test/createFakeClick.ts
+++ b/packages/rum-core/test/createFakeClick.ts
@@ -12,7 +12,7 @@ export function createFakeClick({
}: {
hasError?: boolean
hasPageActivity?: boolean
- userActivity?: { selection?: boolean; input?: boolean }
+ userActivity?: { selection?: boolean; input?: boolean; scroll?: boolean }
event?: Partial
} = {}) {
const stopObservable = new Observable()
@@ -37,6 +37,7 @@ export function createFakeClick({
getUserActivity: () => ({
selection: false,
input: false,
+ scroll: false,
...userActivity,
}),
addFrustration: jasmine.createSpy(),
diff --git a/test/e2e/scenario/rum/actions.scenario.ts b/test/e2e/scenario/rum/actions.scenario.ts
index e8d8820e37..25e832732a 100644
--- a/test/e2e/scenario/rum/actions.scenario.ts
+++ b/test/e2e/scenario/rum/actions.scenario.ts
@@ -305,6 +305,54 @@ describe('action collection', () => {
expect(actionEvents[0].action.frustration!.type).toEqual([])
})
+ createTest('do not consider clicks leading to scrolls as "dead_click"')
+ .withRum({ trackUserInteractions: true })
+ .withBody(html`
+
+
+
+
+ `)
+ .run(async ({ intakeRegistry }) => {
+ const button = await $('button')
+ await button.click()
+
+ await flushEvents()
+ const actionEvents = intakeRegistry.rumActionEvents
+
+ expect(actionEvents.length).toBe(1)
+ expect(actionEvents[0].action.frustration!.type).toEqual([])
+ })
+
+ createTest('do not consider clicks leading to scrolls as "rage_click"')
+ .withRum({ trackUserInteractions: true })
+ .withBody(html`
+
+
+
+
+ `)
+ .run(async ({ intakeRegistry }) => {
+ const button = await $('button')
+ await Promise.all([button.click(), button.click(), button.click()])
+
+ await flushEvents()
+ const actionEvents = intakeRegistry.rumActionEvents
+
+ expect(actionEvents.length).toBe(3)
+ expect(actionEvents[0].action.frustration!.type).toEqual([])
+ })
+
createTest('collect a "rage click"')
.withRum({ trackUserInteractions: true })
.withBody(html`