/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
extensions/amp-analytics/0.1/activity-impl.js
365 строк
10 KB
Shihua Zheng
commit (#39097)
06 июн 2023, 02:37
Не верифицирован
06 июн 2023, 02:37
649e4ac
Код
Авторство
О чём код?
/** * @fileoverview Provides an ability to collect data about activities the user * has performed on the page. */ import {hasOwn} from '#core/types/object'; import {Services} from '#service'; import {listen} from '#utils/event-helper'; import {registerServiceBuilderForDoc} from '../../../src/service-helpers'; /** * The amount of time after an activity the user is considered engaged. * @private @const {number} */ const DEFAULT_ENGAGED_SECONDS = 5; /** * @enum {string} */ const ActivityEventType = { ACTIVE: 'active', INACTIVE: 'inactive', }; /** * @typedef {{ * type: string, * time: number * }} */ let ActivityEventDef; /** * Find the engaged time between the event and the time (exclusive of the time) * @param {ActivityEventDef} activityEvent * @param {number} time * @return {number} * @private */ function findEngagedTimeBetween(activityEvent, time) { let engagementBonus = 0; if (activityEvent.type === ActivityEventType.ACTIVE) { engagementBonus = DEFAULT_ENGAGED_SECONDS; } return Math.min(time - activityEvent.time, engagementBonus); } class ActivityHistory { /** * Creates an instance of ActivityHistory. */ constructor() { /** @private {number} */ this.totalEngagedTime_ = 0; /** * prevActivityEvent_ remains undefined until the first valid push call. * @private {ActivityEventDef|undefined} */ this.prevActivityEvent_ = undefined; } /** * Indicate that an activity took place at the given time. * @param {ActivityEventDef} activityEvent */ push(activityEvent) { if ( this.prevActivityEvent_ && this.prevActivityEvent_.time < activityEvent.time ) { this.totalEngagedTime_ += findEngagedTimeBetween( this.prevActivityEvent_, activityEvent.time ); } this.prevActivityEvent_ = activityEvent; } /** * Get the total engaged time up to the given time recorded in * ActivityHistory. * @param {number} time * @return {number} */ getTotalEngagedTime(time) { let totalEngagedTime = 0; if (this.prevActivityEvent_ !== undefined) { totalEngagedTime = this.totalEngagedTime_ + findEngagedTimeBetween(this.prevActivityEvent_, time); } return totalEngagedTime; } } /** * Array of event types which will be listened for on the document to indicate * activity. Other activities are also observed on the AmpDoc and Viewport * objects. See {@link setUpActivityListeners_} for listener implementation. * @private @const {Array<string>} */ const ACTIVE_EVENT_TYPES = [ 'mousedown', 'mouseup', 'mousemove', 'keydown', 'keyup', ]; /** * Array of event types which will be listened for on the document to indicate * leave from document. Other activities are also observed on the AmpDoc and Viewport * objects. See {@link setUpActivityListeners_} for listener implementation. * @private @const {Array<string>} */ const INACTIVE_EVENT_TYPES = ['mouseleave']; /** * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampDoc */ export function installActivityServiceForTesting(ampDoc) { registerServiceBuilderForDoc(ampDoc, 'activity', Activity); } export class Activity { /** * Activity tracks basic user activity on the page. * - Listeners are not registered on the activity event types until the * AmpDoc's `whenFirstVisible` is resolved. * - When the `whenFirstVisible` of AmpDoc is resolved, a first activity * is recorded. * - The first activity in any second causes all other activities to be * ignored. This is similar to debounce functionality since some events * (e.g. scroll) could occur in rapid succession. * - In any one second period, active events or inactive events can override * each other. Whichever type occured last has precedence. * - Active events give a 5 second "bonus" to engaged time. * - Inactive events cause an immediate stop to the engaged time bonus of * any previous activity event. * - At any point after instantiation, `getTotalEngagedTime` can be used * to get the engage time up to the time the function is called. If * `whenFirstVisible` has not yet resolved, engaged time is 0. * @param {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc */ constructor(ampdoc) { /** @const {!../../../src/service/ampdoc-impl.AmpDoc} ampdoc */ this.ampdoc = ampdoc; /** @private @const {function()} */ this.boundStopIgnore_ = this.stopIgnore_.bind(this); /** @private @const {function()} */ this.boundHandleActivity_ = this.handleActivity_.bind(this); /** @private @const {function()} */ this.boundHandleInactive_ = this.handleInactive_.bind(this); /** @private @const {function()} */ this.boundHandleVisibilityChange_ = this.handleVisibilityChange_.bind(this); /** * Contains the incrementalEngagedTime timestamps for named triggers. * @private {{[key: string]: number}} */ this.totalEngagedTimeByTrigger_ = { /* * "$triggerName" : ${lastRequestTimestamp} */ }; /** @private {Array<!UnlistenDef>} */ this.unlistenFuncs_ = []; /** @private {boolean} */ this.ignoreActivity_ = false; /** @private {boolean} */ this.ignoreInactive_ = false; /** @private @const {!ActivityHistory} */ this.activityHistory_ = new ActivityHistory(); /** @private @const {!../../../src/service/viewport/viewport-interface.ViewportInterface} */ this.viewport_ = Services.viewportForDoc(this.ampdoc); this.ampdoc.whenFirstVisible().then(this.start_.bind(this)); } /** @private */ start_() { /** @private @const {number} */ this.startTime_ = Date.now(); // record an activity since this is when the page became visible this.handleActivity_(); this.setUpActivityListeners_(); } /** * @private * @return {number} */ getTimeSinceStart_() { const timeSinceStart = Date.now() - this.startTime_; // Ensure that a negative time is never returned. This may cause loss of // data if there is a time change during the session but it will decrease // the likelyhood of errors in that situation. return timeSinceStart > 0 ? timeSinceStart : 0; } /** * Return to a state where neither activities or inactivity events are * ignored when that event type is fired. * @private */ stopIgnore_() { this.ignoreActivity_ = false; this.ignoreInactive_ = false; } /** @private */ setUpActivityListeners_() { this.setUpListenersFromArray_( this.ampdoc.getRootNode(), ACTIVE_EVENT_TYPES, this.boundHandleActivity_ ); this.setUpListenersFromArray_( this.ampdoc.getRootNode(), INACTIVE_EVENT_TYPES, this.boundHandleInactive_ ); this.unlistenFuncs_.push( this.ampdoc.onVisibilityChanged(this.boundHandleVisibilityChange_) ); // Viewport.onScroll does not return an unlisten function. // TODO(britice): If Viewport is updated to return an unlisten function, // update this to capture the unlisten function. this.viewport_.onScroll(this.boundHandleActivity_); } /** * @private * @param {!EventTarget} target * @param {Array<string>} events * @param {function()} listener */ setUpListenersFromArray_(target, events, listener) { for (let i = 0; i < events.length; i++) { this.unlistenFuncs_.push(listen(target, events[i], listener)); } } /** @private */ handleActivity_() { if (this.ignoreActivity_) { return; } this.ignoreActivity_ = true; this.ignoreInactive_ = false; this.handleActivityEvent_(ActivityEventType.ACTIVE); } /** @private */ handleInactive_() { if (this.ignoreInactive_) { return; } this.ignoreInactive_ = true; this.ignoreActivity_ = false; this.handleActivityEvent_(ActivityEventType.INACTIVE); } /** * @param {ActivityEventType} type * @private */ handleActivityEvent_(type) { const timeSinceStart = this.getTimeSinceStart_(); const secondKey = Math.floor(timeSinceStart / 1000); const timeToWait = 1000 - (timeSinceStart % 1000); // stop ignoring activity at the start of the next activity bucket setTimeout(this.boundStopIgnore_, timeToWait); this.activityHistory_.push({ type, time: secondKey, }); } /** @private */ handleVisibilityChange_() { if (this.ampdoc.isVisible()) { this.handleActivity_(); } else { this.handleInactive_(); } } /** * Remove all listeners associated with this Activity instance. * @private */ unlisten_() { for (let i = 0; i < this.unlistenFuncs_.length; i++) { const unlistenFunc = this.unlistenFuncs_[i]; // TODO(britice): Due to eslint typechecking, this check may not be // necessary. if (typeof unlistenFunc === 'function') { unlistenFunc(); } } this.unlistenFuncs_ = []; } /** * @private * @visibleForTesting */ cleanup_() { this.unlisten_(); } /** * Get total engaged time since the page became visible. * @return {number} */ getTotalEngagedTime() { const secondsSinceStart = Math.floor(this.getTimeSinceStart_() / 1000); return this.activityHistory_.getTotalEngagedTime(secondsSinceStart); } /** * Get the incremental engaged time since the last push and reset it if asked. * @param {string} name * @param {boolean=} reset * @return {number} */ getIncrementalEngagedTime(name, reset = true) { if (!hasOwn(this.totalEngagedTimeByTrigger_, name)) { if (reset) { this.totalEngagedTimeByTrigger_[name] = this.getTotalEngagedTime(); } return this.getTotalEngagedTime(); } const currentIncrementalEngagedTime = this.totalEngagedTimeByTrigger_[name]; if (reset === false) { return this.getTotalEngagedTime() - currentIncrementalEngagedTime; } this.totalEngagedTimeByTrigger_[name] = this.getTotalEngagedTime(); return ( this.totalEngagedTimeByTrigger_[name] - currentIncrementalEngagedTime ); } }