jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/336670 )
Change subject: Track document visibility in reading depth schema
......................................................................
Track document visibility in reading depth schema
Calculate the number of milliseconds that the page is visible for by
tracking how long the document is hidden for via the Page Visibility API
and subtracting that from the ReadingDepth.totalLength property.
Supporting changes:
* Add the supportsNavigationTiming function to remove the navStart
module-level variable, which is only used in the logEvent function.
* Add the onLoad and onBeforeUnload functions, so that the order of
execution can be better documented.
Bug: T155639
Change-Id: I762ec3fc91decf3cffa869dbd783faf62f01329a
---
M modules/ext.wikimediaEvents.readingDepth.js
1 file changed, 122 insertions(+), 21 deletions(-)
Approvals:
Bmansurov: Looks good to me, approved
jenkins-bot: Verified
diff --git a/modules/ext.wikimediaEvents.readingDepth.js
b/modules/ext.wikimediaEvents.readingDepth.js
index 8bd9969..2b5392d 100644
--- a/modules/ext.wikimediaEvents.readingDepth.js
+++ b/modules/ext.wikimediaEvents.readingDepth.js
@@ -34,11 +34,9 @@
}
}
- var msPaused = 0,
+ var pausedAt,
+ msPaused = 0,
perf = window.performance,
- // This copies logic in mw.now for consistency.
- // if not defined no events will be logged.
- navStart = perf && perf.timing && perf.timing.navigationStart,
EVENT = {
pageTitle: config.get( 'wgTitle' ),
namespaceId: config.get( 'wgNamespaceNumber' ),
@@ -51,6 +49,45 @@
};
/**
+ * Pause the user's page session length timer based on information that
they
+ * have hidden the page, e.g. they opened another tab.
+ *
+ * If the timer is paused, then NOP.
+ *
+ * @param {number} [at=mw.now] The time at which the timer was paused
+ */
+ function pause( at ) {
+ if ( !pausedAt ) {
+ pausedAt = at || mw.now();
+ }
+ }
+
+ /**
+ * Resume the user's page session length timer.
+ *
+ * If the timer is running, then NOP.
+ */
+ function resume() {
+ if ( pausedAt ) {
+ // mw.now invokes [window.]performance.now when it's
available
+ //
<https://phabricator.wikimedia.org/rMWe7d3bce00f418b1f2bc7732aa946b7c9d7c563d0#91287c6c>.
+ //
+ // performance.now is specified such that it's not
subject to system clock
+ // skew or adjustments
<http://caniuse.com/#feat=high-resolution-time> and
+ // is supported by the same set of browsers as the
Navigation Timing API
+ // <http://caniuse.com/#search=navigation>, which,
we've already noted is
+ // supported by more browsers than the Beacon API
+ // <http://caniuse.com/#feat=beacon>.
+ //
+ // Because this code will only be executed by UA's that
support the Beacon
+ // API, we can rely on mw.now not to be subject to
system clock skew or
+ // adjustments.
+ msPaused += mw.now() - pausedAt;
+ pausedAt = null;
+ }
+ }
+
+ /**
* Log an event to the Schema:ReadingDepth
*
* @param {string} action a valid value for the action property inside
the
@@ -58,23 +95,39 @@
*/
function logEvent( action ) {
var now,
- // will always be defined.
- domInteractive = perf.timing.domInteractive,
- fp = getFirstPaintTime( perf ),
+ timing = perf.timing,
+ domInteractive = timing.domInteractive,
+ navigationStart = timing.navigationStart,
+ firstPaint = getFirstPaintTime( perf ),
+
+ // Used while calculating the totalLength and
visibleLength properties.
+ delta, adjustedEpoch, hiddenFor,
+
data = $.extend( {}, EVENT, {
action: action,
- domInteractiveTime: domInteractive - navStart,
- firstPaintTime: fp ? fp - navStart : undefined
- } ),
- // time to start measuring from with preference for
first paint
- from = fp || domInteractive;
+ domInteractiveTime: domInteractive -
navigationStart,
+ firstPaintTime: firstPaint ? firstPaint -
navigationStart : undefined
+ } );
if ( action === 'pageUnloaded' ) {
now = mw.now();
- // times are measured from firstPaint or domInteractive
depending what's available.
- // Since we record these separately it's clear which is
being used.
- data.totalLength = Math.round( now - from );
- data.visibleLength = Math.round( now - from - msPaused
);
+
+ // If the first paint time is available, then use it as
the epoch for
+ // calculation by adjusting the original - the "DOM
interactive" time - by
+ // the difference between the two.
+ delta = firstPaint ? ( firstPaint - domInteractive ) :
0;
+ adjustedEpoch = domInteractive + delta;
+
+ // In the case where the page is loaded but hidden,
e.g. the user
+ // opens the page in a new tab, then the total amount
of time that the
+ // page is hidden for is calculated from the original
epoch. As above,
+ // an adjustment needs to be made.
+ //
+ // If the page hasn't been hidden, however, then NOP.
+ hiddenFor = Math.max( 0, msPaused - delta );
+
+ data.totalLength = Math.round( now - adjustedEpoch );
+ data.visibleLength = Math.round( data.totalLength -
hiddenFor );
}
mw.track( 'event.ReadingDepth', data );
@@ -100,10 +153,23 @@
/**
* Checks whether the current browser supports sendBeacon feature.
*
+ * FIXME: Rename this to be consistent with `supportsNavigationTiming`,
which
+ * itself is consistent with the wording used in `isEnabled`.
+ *
* @return {boolean}
*/
function isSendBeaconCapable() {
return $.isFunction( navigator.sendBeacon );
+ }
+
+ /**
+ * Checks whether the UA supports the Navigation Timing API.
+ *
+ * @return {boolean}
+ */
+ function supportsNavigationTiming() {
+ // This copies the logic in mw.now for consistency.
+ return Boolean( perf && perf.timing &&
perf.timing.navigationStart );
}
/**
@@ -115,17 +181,52 @@
* @return {boolean}
*/
function isEnabled() {
- return navStart &&
- config.get( 'wgWMEReadingDepthEnabled' ) &&
+ return config.get( 'wgWMEReadingDepthEnabled' ) &&
+ supportsNavigationTiming() &&
isSendBeaconCapable() &&
isInSample( config.get(
'wgWMEReadingDepthSamplingRate', 0 ) );
}
- if ( isEnabled() ) {
- $( window ).on( 'beforeunload', function () {
- logEvent( 'pageUnloaded' );
+ /**
+ * Handles the window being unloaded.
+ *
+ * The "pageUnloaded" ReadingDepth event is logged.
+ */
+ function onBeforeUnload() {
+ logEvent( 'pageUnloaded' );
+ }
+
+ /**
+ * Handles the document and its resources having been loaded enough...
+ *
+ * 1. If the document is loaded and is hidden, then the timer is marked
as
+ * having been paused at when [the document readyState became
+ * "interactive"][0] as that's the default epoch from which the
+ * `ReadingDepth.totalLength` property is measured from.
+ * 2. The document visibility change handler is set up.
+ * 3. The "pageLoaded" ReadingDepth event is logged.
+ *
+ * [0]:
https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming/domInteractive
+ */
+ function onLoad() {
+ if ( document.hidden ) {
+ pause( perf.timing.domInteractive );
+ }
+
+ $( document ).on( 'visibilitychange', function () {
+ if ( document.hidden ) {
+ pause();
+ } else {
+ resume();
+ }
} );
+
logEvent( 'pageLoaded' );
}
+ if ( isEnabled() ) {
+ $( window ).on( 'beforeunload', onBeforeUnload );
+ onLoad();
+ }
+
}( jQuery, mediaWiki, mediaWiki.config, mediaWiki.user, mediaWiki.experiments
) );
--
To view, visit https://gerrit.wikimedia.org/r/336670
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I762ec3fc91decf3cffa869dbd783faf62f01329a
Gerrit-PatchSet: 16
Gerrit-Project: mediawiki/extensions/WikimediaEvents
Gerrit-Branch: master
Gerrit-Owner: Jdlrobson <[email protected]>
Gerrit-Reviewer: Bmansurov <[email protected]>
Gerrit-Reviewer: EBernhardson <[email protected]>
Gerrit-Reviewer: Jdlrobson <[email protected]>
Gerrit-Reviewer: Jhernandez <[email protected]>
Gerrit-Reviewer: Phuedx <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits