-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9c89ccd
commit b5e4c6d
Showing
14 changed files
with
520 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export function isIFrameWindow (windowObject) { | ||
if (!windowObject) return false | ||
return windowObject.self !== windowObject.top | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/** | ||
* Generates a CSS selector path for the given element, if possible | ||
* @param {HTMLElement} elem | ||
* @param {boolean} includeId | ||
* @param {boolean} includeClass | ||
* @returns {string|undefined} | ||
*/ | ||
export const generateSelectorPath = (elem) => { | ||
if (!elem) return | ||
|
||
const getNthOfTypeIndex = (node) => { | ||
try { | ||
let i = 1 | ||
const { tagName } = node | ||
while (node.previousElementSibling) { | ||
if (node.previousElementSibling.tagName === tagName) i++ | ||
node = node.previousElementSibling | ||
} | ||
return i | ||
} catch (err) { | ||
// do nothing for now. An invalid child count will make the path selector not return a nth-of-type selector statement | ||
} | ||
} | ||
|
||
let pathSelector = '' | ||
let index = getNthOfTypeIndex(elem) | ||
|
||
try { | ||
while (elem?.tagName) { | ||
const { id, localName } = elem | ||
const selector = [ | ||
localName, | ||
id ? `#${id}` : '', | ||
pathSelector ? `>${pathSelector}` : '' | ||
].join('') | ||
|
||
pathSelector = selector | ||
elem = elem.parentNode | ||
} | ||
} catch (err) { | ||
// do nothing for now | ||
} | ||
|
||
return pathSelector ? index ? `${pathSelector}:nth-of-type(${index})` : pathSelector : undefined | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
src/features/generic_events/aggregate/user-actions/aggregated-user-action.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../constants' | ||
|
||
export class AggregatedUserAction { | ||
constructor (evt, selectorPath) { | ||
this.event = evt | ||
this.count = 1 | ||
this.originMs = Math.floor(evt.timeStamp) | ||
this.relativeMs = [0] | ||
this.selectorPath = selectorPath | ||
this.rageClick = undefined | ||
} | ||
|
||
/** | ||
* Aggregates the count and maintains the relative MS array for matching events | ||
* Will determine if a rage click was observed as part of the aggregation | ||
* @param {Event} evt | ||
* @returns {void} | ||
*/ | ||
aggregate (evt) { | ||
this.count++ | ||
this.relativeMs.push(Math.floor(evt.timeStamp - this.originMs)) | ||
if (this.isRageClick()) this.rageClick = true | ||
} | ||
|
||
/** | ||
* Determines if the current set of relative ms values constitutes a rage click | ||
* @returns {boolean} | ||
*/ | ||
isRageClick () { | ||
const len = this.relativeMs.length | ||
return (this.event.type === 'click' && len >= RAGE_CLICK_THRESHOLD_EVENTS && this.relativeMs[len - 1] - this.relativeMs[len - RAGE_CLICK_THRESHOLD_EVENTS] < RAGE_CLICK_THRESHOLD_MS) | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { generateSelectorPath } from '../../../../common/dom/selector-path' | ||
import { OBSERVED_WINDOW_EVENTS } from '../../constants' | ||
import { AggregatedUserAction } from './aggregated-user-action' | ||
|
||
export class UserActionsAggregator { | ||
/** @type {AggregatedUserAction=} */ | ||
#aggregationEvent = undefined | ||
#aggregationKey = '' | ||
|
||
get aggregationEvent () { | ||
// if this is accessed externally, we need to be done aggregating on it | ||
// to prevent potential mutability and duplication issues, so the state is cleared upon returning. | ||
// This value may need to be accessed during an unload harvest. | ||
const finishedEvent = this.#aggregationEvent | ||
this.#aggregationKey = '' | ||
this.#aggregationEvent = undefined | ||
return finishedEvent | ||
} | ||
|
||
/** | ||
* Process the event and determine if a new aggregation set should be made or if it should increment the current aggregation | ||
* @param {Event} evt The event supplied by the addEventListener callback | ||
* @returns {AggregatedUserAction|undefined} The previous aggregation set if it has been completed by processing the current event | ||
*/ | ||
process (evt) { | ||
if (!evt) return | ||
const selectorPath = getSelectorPath(evt) | ||
const aggregationKey = getAggregationKey(evt, selectorPath) | ||
if (!!aggregationKey && aggregationKey === this.#aggregationKey) { | ||
// an aggregation exists already, so lets just continue to increment | ||
this.#aggregationEvent.aggregate(evt) | ||
} else { | ||
// return the prev existing one (if there is one) | ||
const finishedEvent = this.#aggregationEvent | ||
// then set as this new event aggregation | ||
this.#aggregationKey = aggregationKey | ||
this.#aggregationEvent = new AggregatedUserAction(evt, selectorPath) | ||
return finishedEvent | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Generates a selector path for the event, starting with simple cases like window or document and getting more complex for dom-tree traversals as needed. | ||
* Will return a random selector path value if no other path can be determined, to force the aggregator to skip aggregation for this event. | ||
* @param {Event} evt | ||
* @returns {string} | ||
*/ | ||
function getSelectorPath (evt) { | ||
let selectorPath | ||
if (OBSERVED_WINDOW_EVENTS.includes(evt.type) || evt.target === window) selectorPath = 'window' | ||
else if (evt.target === document) selectorPath = 'document' | ||
// if still no selectorPath, generate one from target tree that includes elem ids | ||
else selectorPath = generateSelectorPath(evt.target) | ||
// if STILL no selectorPath, it will return undefined which will skip aggregation for this event | ||
return selectorPath | ||
} | ||
|
||
/** | ||
* Returns an aggregation key based on the event type and the selector path of the event's target. | ||
* Scrollend events are aggregated into one set, no matter what. | ||
* @param {Event} evt | ||
* @param {string} selectorPath | ||
* @returns {string} | ||
*/ | ||
function getAggregationKey (evt, selectorPath) { | ||
let aggregationKey = evt.type | ||
/** aggregate all scrollends into one set (if sequential), no matter what their target is | ||
* the aggregation group's selector path with be reflected as the first one observed | ||
* due to the way the aggregation logic works (by storing the initial value and aggregating it) */ | ||
if (evt.type !== 'scrollend') aggregationKey += '-' + selectorPath | ||
return aggregationKey | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.