Merge "Remove instrumentation"

This commit is contained in:
jenkins-bot 2022-07-05 17:53:24 +00:00 committed by Gerrit Code Review
commit c536aaee17
33 changed files with 43 additions and 2219 deletions

View file

@ -41,11 +41,7 @@
"ResourceModules": {
"mmv": {
"scripts": [
"mmv/logging/mmv.logging.Api.js",
"mmv/logging/mmv.logging.AttributionLogger.js",
"mmv/logging/mmv.logging.DimensionLogger.js",
"mmv/logging/mmv.logging.ViewLogger.js",
"mmv/logging/mmv.logging.PerformanceLogger.js",
"mmv/model/mmv.model.js",
"mmv/model/mmv.model.IwTitle.js",
"mmv/model/mmv.model.License.js",
@ -329,10 +325,7 @@
"scripts": [
"mmv.bootstrap/mmv.Config.js",
"mmv.bootstrap/mmv.HtmlUtils.js",
"mmv.bootstrap/mmv.bootstrap.js",
"mmv.bootstrap/mmv.logging.Logger.js",
"mmv.bootstrap/mmv.logging.ActionLogger.js",
"mmv.bootstrap/mmv.logging.DurationLogger.js"
"mmv.bootstrap/mmv.bootstrap.js"
],
"styles": [
"mmv.bootstrap/mmv.bootstrap.less"
@ -391,11 +384,6 @@
"tests/qunit/mmv/mmv.EmbedFileFormatter.test.js",
"tests/qunit/mmv/mmv.Config.test.js",
"tests/qunit/mmv/mmv.HtmlUtils.test.js",
"tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js",
"tests/qunit/mmv/logging/mmv.logging.PerformanceLogger.test.js",
"tests/qunit/mmv/logging/mmv.logging.ActionLogger.test.js",
"tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js",
"tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js",
"tests/qunit/mmv/logging/mmv.logging.ViewLogger.test.js",
"tests/qunit/mmv/model/mmv.model.test.js",
"tests/qunit/mmv/model/mmv.model.IwTitle.test.js",
@ -468,30 +456,6 @@
"tif": "default"
}
},
"MediaViewerNetworkPerformanceSamplingFactor": {
"description": "If set, records image load network performance via EventLogging once per this many requests. False if unset.",
"value": false
},
"MediaViewerDurationLoggingSamplingFactor": {
"description": "If set, records loading times via EventLogging. A value of 1000 means there will be an 1:1000 chance to log the duration event. False if unset.",
"value": false
},
"MediaViewerDurationLoggingLoggedinSamplingFactor": {
"description": "If set, records loading times via EventLogging with factor specific to loggedin users. A value of 1000 means there will be an 1:1000 chance to log the duration event. False if unset.",
"value": false
},
"MediaViewerAttributionLoggingSamplingFactor": {
"description": "If set, records whether image attribution data was available. A value of 1000 means there will be an 1:1000 chance to log the attribution event. False if unset.",
"value": false
},
"MediaViewerDimensionLoggingSamplingFactor": {
"description": "If set, records whether image dimension data was available. A value of 1000 means there will be an 1:1000 chance to log the dimension event. False if unset.",
"value": false
},
"MediaViewerActionLoggingSamplingFactorMap": {
"description": "If set, records user actions via EventLogging and applies a sampling factor according to the map. A \"default\" key in the map must be set. False if unset.",
"value": false
},
"MediaViewerUseThumbnailGuessing": {
"description": "When this is enabled, MediaViewer will try to guess image URLs instead of making an imageinfo API to get them from the server. This speeds up image loading, but will result in 404s when $wgGenerateThumbnailOnParse (so the thumbnails are only generated as a result of the API request). MediaViewer will catch such 404 errors and fall back to the API request, but depending on how the site is set up, the 404 might get cached, or redirected, causing the image load to fail. The safe way to use URL guessing is with a 404 handler: https://www.mediawiki.org/wiki/Manual:Thumb.php#404_Handler",
"value": false

View file

@ -169,13 +169,7 @@ class Hooks implements MakeGlobalVariablesScriptHook {
* @param array &$vars
*/
public static function onResourceLoaderGetConfigVars( array &$vars ) {
global $wgMediaViewerActionLoggingSamplingFactorMap,
$wgMediaViewerNetworkPerformanceSamplingFactor,
$wgMediaViewerDurationLoggingSamplingFactor,
$wgMediaViewerDurationLoggingLoggedinSamplingFactor,
$wgMediaViewerAttributionLoggingSamplingFactor,
$wgMediaViewerDimensionLoggingSamplingFactor,
$wgMediaViewerUseThumbnailGuessing, $wgMediaViewerExtensions,
global $wgMediaViewerUseThumbnailGuessing, $wgMediaViewerExtensions,
$wgMediaViewerImageQueryParameter, $wgMediaViewerRecordVirtualViewBeaconURI;
$vars['wgMultimediaViewer'] = [
@ -183,12 +177,6 @@ class Hooks implements MakeGlobalVariablesScriptHook {
'discussionLink' => self::$discussionLink,
'helpLink' => self::$helpLink,
'useThumbnailGuessing' => (bool)$wgMediaViewerUseThumbnailGuessing,
'durationSamplingFactor' => $wgMediaViewerDurationLoggingSamplingFactor,
'durationSamplingFactorLoggedin' => $wgMediaViewerDurationLoggingLoggedinSamplingFactor,
'networkPerformanceSamplingFactor' => $wgMediaViewerNetworkPerformanceSamplingFactor,
'actionLoggingSamplingFactorMap' => $wgMediaViewerActionLoggingSamplingFactorMap,
'attributionSamplingFactor' => $wgMediaViewerAttributionLoggingSamplingFactor,
'dimensionSamplingFactor' => $wgMediaViewerDimensionLoggingSamplingFactor,
'imageQueryParameter' => $wgMediaViewerImageQueryParameter,
'recordVirtualViewBeaconURI' => $wgMediaViewerRecordVirtualViewBeaconURI,
'tooltipDelay' => 1000,

View file

@ -516,22 +516,6 @@
* @return {jQuery.Promise}
*/
MMVB.openImage = function ( element, title ) {
var $element = $( element );
mw.mmv.durationLogger.start( [ 'click-to-first-image', 'click-to-first-metadata' ] );
if ( $element.is(
'a.image, ' +
'[typeof*="mw:File"] a.mw-file-description, ' +
// TODO: Remove mw:Image when version 2.4.0 of the content is no
// longer supported
'[typeof*="mw:Image"] a.mw-file-description'
) ) {
mw.mmv.actionLogger.log( 'thumbnail' );
} else if ( $element.is( '.magnify a' ) ) {
mw.mmv.actionLogger.log( 'enlarge' );
}
this.ensureEventHandlersAreSetUp();
return this.loadViewer( true ).then( function ( viewer ) {
@ -585,10 +569,8 @@
/**
* Handles the browser location hash on pageload or hash change
*
* @param {boolean} initialHash Whether this is called for the hash that came with the pageload
*/
MMVB.hash = function ( initialHash ) {
MMVB.hash = function () {
var bootstrap = this;
// There is no point loading the mmv if it isn't loaded yet for hash changes unrelated to the mmv
@ -603,10 +585,6 @@
// the page is loaded with an invalid MMV url
if ( !viewer.isOpen ) {
bootstrap.cleanupOverlay();
} else if ( initialHash ) {
mw.mmv.actionLogger.log( 'hash-load' );
} else {
mw.mmv.actionLogger.log( 'history-navigation' );
}
} );
};

View file

@ -1,194 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var L;
/**
* Writes log entries
*
* @class mw.mmv.logging.ActionLogger
* @extends mw.mmv.logging.Logger
* @constructor
*/
function ActionLogger() {}
OO.inheritClass( ActionLogger, mw.mmv.logging.Logger );
L = ActionLogger.prototype;
/**
* Sampling factor key-value map.
*
* The map's keys are the action identifiers and the values are the sampling factor for each action type.
* There is a "default" key defined providing a default sampling factor for actions that aren't explicitly
* set in the map.
*
* @property {Object.<string, number>}
* @static
*/
L.samplingFactorMap = mw.config.get( 'wgMultimediaViewer' ).actionLoggingSamplingFactorMap;
/**
* @override
* @inheritdoc
*/
L.schema = 'MediaViewer';
/**
* Possible log actions, and their associated English developer log strings.
*
* These events are not de-duped. Eg. if the user opens the same site link
* in 10 tabs, there will be 10 file-description-page events. If they view the
* same image 10 times by hitting the prev/next buttons, there will be 10
* image-view events, etc.
*
* @property {Object}
* @static
*/
L.logActions = {
thumbnail: 'User clicked on a thumbnail to open Media Viewer.',
enlarge: 'User clicked on an enlarge link to open Media Viewer.',
fullscreen: 'User entered fullscreen mode.',
defullscreen: 'User exited fullscreen mode.',
close: 'User closed Media Viewer.',
'view-original-file': 'User clicked on the direct link to the original file',
'file-description-page': 'User opened the file description page.',
'file-description-page-abovefold': 'User opened the file description page via the above-the-fold button.',
'use-this-file-open': 'User opened the dialog to use this file.',
'image-view': 'User viewed an image.',
'metadata-open': 'User opened the metadata panel.',
'metadata-close': 'User closed the metadata panel.',
'metadata-scroll-open': 'User opened the metadata panel by scrolling.',
'metadata-scroll-close': 'User closed the metadata panel by scrolling.',
'next-image': 'User viewed the next image.',
'prev-image': 'User viewed the previous image.',
'terms-open': 'User opened the usage terms.',
'license-page': 'User opened the license page.',
'author-page': 'User opened the author page.',
'source-page': 'User opened the source page.',
'hash-load': 'User loaded the image via a hash on pageload.',
'history-navigation': 'User navigated with the browser history.',
'optout-loggedin': 'opt-out (via quick link at bottom of metadata panel) by logged-in user',
'optout-anon': 'opt-out by anonymous user',
'optin-loggedin': 'opt-in (via quick link at bottom of metadata panel) by logged-in user',
'optin-anon': 'opt-in by anonymous user',
'about-page': 'User opened the about page.',
'discuss-page': 'User opened the discuss page.',
'help-page': 'User opened the help page.',
'location-page': 'User opened the location page.',
'download-select-menu-original': 'User selected the original size in the download dropdown menu.',
'download-select-menu-small': 'User selected the small size in the download dropdown menu.',
'download-select-menu-medium': 'User selected the medium size in the download dropdown menu.',
'download-select-menu-large': 'User selected the large size in the download dropdown menu.',
download: 'User clicked on the button to download a file.',
'download-view-in-browser': 'User clicked on the link to view the image in the browser in the download tab.',
'right-click-image': 'User right-clicked on the image.',
'share-page': 'User opened the link to the current image.',
'share-link-copied': 'User copied the share link.',
'embed-html-copied': 'User copied the HTML embed code.',
'embed-wikitext-copied': 'User copied the wikitext embed code.',
'embed-switched-to-html': 'User switched to the HTML embed code.',
'embed-switched-to-wikitext': 'User switched to the wikitext embed code.',
'embed-select-menu-wikitext-default': 'User switched to the default thumbnail size on wikitext.',
'embed-select-menu-wikitext-small': 'User switched to the small thumbnail size on wikitext.',
'embed-select-menu-wikitext-medium': 'User switched to the medium thumbnail size on wikitext.',
'embed-select-menu-wikitext-large': 'User switched to the large thumbnail size on wikitext.',
'embed-select-menu-html-original': 'User switched to the original thumbnail size on html.',
'embed-select-menu-html-small': 'User switched to the small thumbnail size on html.',
'embed-select-menu-html-medium': 'User switched to the medium thumbnail size on html.',
'embed-select-menu-html-large': 'User switched to the large thumbnail size on html.',
'use-this-file-close': 'User closed the dialog to use this file.',
'download-open': 'User opened the dialog to download this file.',
'download-close': 'User closed the dialog to download this file.',
'options-open': 'User opened the enable/disable dialog.',
'options-close': 'User either canceled an enable/disable action or closed a confirmation window.',
'disable-about-link': 'User clicked on the "Learn more" link in the disable window.',
'enable-about-link': 'User clicked on the "Learn more" link in the enable window.',
'image-unview': 'User stopped looking at the current image.'
};
/**
* Logs an action
*
* @param {string} action The key representing the action
* @param {boolean} forceEventLog True if we want the action to be logged regardless of the sampling factor
* @return {jQuery.Promise}
*/
L.log = function ( action, forceEventLog ) {
var actionText = this.logActions[ action ] || action,
self = this;
if ( this.isEnabled( action ) ) {
mw.log( actionText );
}
if ( forceEventLog || self.isInSample( action ) ) {
return this.loadDependencies().then( function () {
self.eventLog.logEvent( self.schema, {
action: action,
samplingFactor: self.getActionFactor( action )
} );
return true;
} );
} else {
return $.Deferred().resolve( false );
}
};
/**
* Returns the sampling factor for a given action
*
* @param {string} action The key representing the action
* @return {number} Sampling factor
*/
L.getActionFactor = function ( action ) {
return this.samplingFactorMap[ action ] || this.samplingFactorMap.default;
};
/**
* Returns whether or not we should measure this request for this action
*
* @param {string} action The key representing the action
* @return {boolean} True if this request needs to be sampled
*/
L.isInSample = function ( action ) {
var factor = this.getActionFactor( action );
if ( typeof factor !== 'number' || factor < 1 ) {
return false;
}
return Math.floor( Math.random() * factor ) === 0;
};
/**
* Returns whether logging this event is enabled. This is intended for console logging, which
* (in debug mode) should be done even if the request is not being sampled, as long as logging
* is enabled for some sample.
*
* @param {string} action The key representing the action
* @return {boolean} True if this logging is enabled
*/
L.isEnabled = function ( action ) {
var factor = this.getActionFactor( action );
return typeof factor === 'number' && factor >= 1;
};
mw.mmv.logging.ActionLogger = ActionLogger;
mw.mmv.actionLogger = new ActionLogger();
}() );

View file

@ -1,160 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var L;
/**
* Writes EventLogging entries for duration measurements
*
* @class mw.mmv.logging.DurationLogger
* @extends mw.mmv.logging.Logger
* @constructor
*/
function DurationLogger() {
this.starts = Object.create( null );
this.stops = Object.create( null );
}
OO.inheritClass( DurationLogger, mw.mmv.logging.Logger );
L = DurationLogger.prototype;
/**
* @override
* @inheritdoc
*/
L.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).durationSamplingFactor;
// If a sampling factor specific to loggedin users is set and we're logged in, apply it
if ( mw.config.get( 'wgMultimediaViewer' ).durationSamplingFactorLoggedin && !mw.user.isAnon() ) {
L.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).durationSamplingFactorLoggedin;
}
/**
* @override
* @inheritdoc
*/
L.schema = 'MultimediaViewerDuration';
/**
* Saves the start of a duration
*
* @param {string|string[]} typeOrTypes Type(s) of duration being measured.
* @chainable
*/
L.start = function ( typeOrTypes ) {
var i,
start = Date.now();
if ( !typeOrTypes ) {
throw new Error( 'Must specify type' );
}
if ( !Array.isArray( typeOrTypes ) ) {
typeOrTypes = [ typeOrTypes ];
}
for ( i = 0; i < typeOrTypes.length; i++ ) {
// Don't overwrite an existing value
if ( !( typeOrTypes[ i ] in this.starts ) ) {
this.starts[ typeOrTypes[ i ] ] = start;
}
}
return this;
};
/**
* Saves the stop of a duration
*
* @param {string} type Type of duration being measured.
* @param {number} start Start timestamp to substitute the one coming from start()
* @chainable
*/
L.stop = function ( type, start ) {
var stop = Date.now();
if ( !type ) {
throw new Error( 'Must specify type' );
}
// Don't overwrite an existing value
if ( !( type in this.stops ) ) {
this.stops[ type ] = stop;
}
// Don't overwrite an existing value
if ( start !== undefined && !( type in this.starts ) ) {
this.starts[ type ] = start;
}
return this;
};
/**
* Records the duration log event
*
* @param {string} type Type of duration being measured.
* @param {Object} extraData Extra information to add to the log event data
* @chainable
*/
L.record = function ( type, extraData ) {
var e, duration;
if ( !type ) {
throw new Error( 'Must specify type' );
}
if ( !( type in this.starts ) || this.starts[ type ] === undefined ) {
return;
}
if ( !( type in this.stops ) || this.stops[ type ] === undefined ) {
return;
}
duration = this.stops[ type ] - this.starts[ type ];
e = {
type: type,
duration: duration,
loggedIn: !mw.user.isAnon(),
samplingFactor: this.samplingFactor
};
if ( extraData ) {
// eslint-disable-next-line no-jquery/no-each-util
$.each( extraData, function ( key, value ) {
e[ key ] = value;
} );
}
if ( this.isEnabled() ) {
mw.log( 'mw.mmw.logger.DurationLogger', e );
}
this.log( e );
delete this.starts[ type ];
delete this.stops[ type ];
return this;
};
mw.mmv.durationLogger = new DurationLogger();
}() );

View file

@ -1,167 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var L;
/**
* Abstract class providing common code for EventLogging loggers
*
* @class mw.mmv.logging.Logger
* @abstract
*/
function Logger() {
this.Geo = undefined;
this.eventLog = undefined;
}
L = Logger.prototype;
/**
* Sampling factor key-value map.
*
* Makes the logger sample log events instead of recording each one if > 0. Disables logging if === 0.
*
* @property {number}
*/
L.samplingFactor = 0;
/**
* EventLogging schema
*
* @property {string}
*/
L.schema = '';
/**
* Sets the Geo object providing country information about the visitor
*
* @param {Object} Geo object containing country GeoIP information about the user
*/
L.setGeo = function ( Geo ) {
this.Geo = Geo;
};
/**
* Sets the eventLog object providing a facility to record events
*
* @param {mw.eventLog} eventLog EventLogging instance
*/
L.setEventLog = function ( eventLog ) {
this.eventLog = eventLog;
};
/**
* Loads the dependencies that allow us to log events
*
* @return {jQuery.Promise}
*/
L.loadDependencies = function () {
var self = this,
waitForEventLog = $.Deferred();
// Waits for dom readiness because we don't want to have these dependencies loaded in the head
$( function () {
// window.Geo is currently defined in components that are loaded independently, there is no cheap
// way to load just that information. Either we piggy-back on something that already loaded it
// or we just don't have it
if ( window.Geo ) {
self.setGeo( window.Geo );
}
try {
mw.loader.using( 'ext.eventLogging', function () {
self.setEventLog( mw.eventLog );
waitForEventLog.resolve();
} );
} catch ( e ) {
waitForEventLog.reject();
}
} );
return waitForEventLog;
};
/**
* Returns whether or not we should measure this request
*
* @return {boolean} True if this request needs to be sampled
*/
L.isInSample = function () {
if ( typeof this.samplingFactor !== 'number' || this.samplingFactor < 1 ) {
return false;
}
return Math.floor( Math.random() * this.samplingFactor ) === 0;
};
/**
* Returns whether logging this event is enabled. This is intended for console logging, which
* (in debug mode) should be done even if the request is not being sampled, as long as logging
* is enabled for some sample.
*
* @return {boolean} True if this logging is enabled
*/
L.isEnabled = function () {
return typeof this.samplingFactor === 'number' && this.samplingFactor >= 1;
};
/**
* True if the schema has a country field. Broken out in a separate function so it's easy to mock.
*
* @return {boolean}
*/
L.schemaSupportsCountry = function () {
return this.eventLog && ( { // don't die if eventLog is a mock
// EventLogging only downloads schemas in debug mode, can't check for country dynamically
MediaViewer: false,
MultimediaViewerDimensions: false,
MultimediaViewerNetworkPerformance: true,
MultimediaViewerAttribution: false,
MultimediaViewerDuration: true
}[ this.schema ] || false );
};
/**
* Logs EventLogging data while including Geo data if any
*
* @param {Object} data
* @return {jQuery.Promise}
*/
L.log = function ( data ) {
var self = this;
if ( self.isInSample() ) {
return this.loadDependencies().then( function () {
// Add Geo information if there's any
if (
self.Geo && self.Geo.country !== undefined &&
self.schemaSupportsCountry()
) {
data.country = self.Geo.country;
}
self.eventLog.logEvent( self.schema, data );
} );
} else {
return $.Deferred().resolve();
}
};
mw.mmv.logging = {};
mw.mmv.logging.Logger = Logger;
}() );

View file

@ -16,10 +16,9 @@
*/
( function () {
var $document = $( document ),
start;
var $document = $( document );
// If the user disabled MediaViewer in his preferences, we do not set up click handling.
// If MediaViewer is disabled by the user, do not set up click handling.
// This is loaded before user JS so we cannot check wgMediaViewer.
if (
mw.config.get( 'wgMediaViewerOnClick' ) !== true ||
@ -35,15 +34,11 @@
return;
}
start = Date.now();
// We wait for document readiness because mw.loader.using writes to the DOM
// which can cause a blank page if it happens before DOM readiness
$( function () {
mw.loader.using( [ 'mmv.bootstrap.autostart' ], function () {
mw.mmv.bootstrap.whenThumbsReady().then( function () {
mw.mmv.durationLogger.stop( 'early-click-to-replay-click', start ).record( 'early-click-to-replay-click' );
// We have to copy the properties, passing e doesn't work. Probably because of preventDefault()
$( e.target ).trigger( { type: 'click', which: 1, replayed: true } );
} );

View file

@ -86,10 +86,7 @@
this.$downloadButton = $( '<a>' )
.attr( 'target', '_blank' )
.attr( 'download', '' )
.addClass( 'mw-ui-button mw-ui-progressive mw-mmv-download-go-button' )
.on( 'click', function () {
mw.mmv.actionLogger.log( 'download' );
} );
.addClass( 'mw-ui-button mw-ui-progressive mw-mmv-download-go-button' );
this.$selectionArrow = $( '<span>' )
.addClass( 'mw-ui-button mw-ui-progressive mw-mmv-download-select-menu' )
@ -121,10 +118,6 @@
'original'
);
this.downloadSizeMenu.getMenu().on( 'select', function ( item ) {
mw.mmv.actionLogger.log( 'download-select-menu-' + item.data.name );
} );
$container.append( this.downloadSizeMenu.$element );
};
@ -138,10 +131,7 @@
.attr( 'target', '_blank' )
.addClass( 'mw-mmv-download-preview-link' )
.text( mw.message( 'multimediaviewer-download-preview-link-title' ).text() )
.appendTo( $container )
.on( 'click', function () {
mw.mmv.actionLogger.log( 'download-view-in-browser' );
} );
.appendTo( $container );
};
DP.createAttributionButton = function ( $container ) {

View file

@ -122,10 +122,6 @@
}
} );
this.embedTextHtml.on( 'copy', function () {
mw.mmv.actionLogger.log( 'embed-html-copied' );
} );
this.embedTextWikitext = new mw.widgets.CopyTextLayout( {
help: mw.message( 'multimediaviewer-embed-explanation' ).text(),
helpInline: true,
@ -146,10 +142,6 @@
}
} );
this.embedTextWikitext.on( 'copy', function () {
mw.mmv.actionLogger.log( 'embed-wikitext-copied' );
} );
$container.append(
this.embedTextHtml.$element,
this.embedTextWikitext.$element
@ -204,10 +196,6 @@
'default'
);
this.embedSizeSwitchWikitext.getMenu().on( 'select', function ( item ) {
mw.mmv.actionLogger.log( 'embed-select-menu-wikitext-' + item.data.name );
} );
// Html sizes pulldown menu
this.embedSizeSwitchHtml = this.utils.createPulldownMenu(
[ 'small', 'medium', 'large', 'original' ],
@ -215,10 +203,6 @@
'original'
);
this.embedSizeSwitchHtml.getMenu().on( 'select', function ( item ) {
mw.mmv.actionLogger.log( 'embed-select-menu-html-' + item.data.name );
} );
this.embedSizeSwitchHtmlLayout = new OO.ui.FieldLayout( this.embedSizeSwitchHtml, { align: 'top' } );
this.embedSizeSwitchWikitextLayout = new OO.ui.FieldLayout( this.embedSizeSwitchWikitext, { align: 'top' } );
@ -272,8 +256,6 @@
EP.handleTypeSwitch = function ( item ) {
var value = item.getData();
mw.mmv.actionLogger.log( 'embed-switched-to-' + value );
if ( value === 'html' ) {
this.currentMainEmbedText = this.embedTextHtml;
this.embedSizeSwitchWikitext.getMenu().toggle( false );

View file

@ -50,19 +50,12 @@
}
} );
this.pageInput.on( 'copy', function () {
mw.mmv.actionLogger.log( 'share-link-copied' );
} );
this.$pageLink = $( '<a>' )
.addClass( 'mw-mmv-share-page-link' )
.prop( 'alt', mw.message( 'multimediaviewer-link-to-page' ).text() )
.prop( 'target', '_blank' )
.html( '&nbsp;' )
.appendTo( this.$pane )
.on( 'click', function () {
mw.mmv.actionLogger.log( 'share-page' );
} );
.appendTo( this.$pane );
this.pageInput.$element.appendTo( this.$pane );

View file

@ -1,58 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
/**
* Runs performance analysis on requests via mw.mmv.logging.PerformanceLogger
*
* @class mw.mmv.logging.Api
* @extends mw.Api
* @constructor
* @param {string} type The type of the requests to be made through this API.
* @param {Object} options See mw.Api#defaultOptions
*/
function Api( type, options ) {
mw.Api.call( this, options );
/** @property {mw.mmv.logging.PerformanceLogger} performance Used to record performance data. */
this.performance = new mw.mmv.logging.PerformanceLogger();
/** @property {string} type Type of requests being sent via this API. */
this.type = type;
}
OO.inheritClass( Api, mw.Api );
/**
* Runs an AJAX call to the server.
*
* @override
* @param {Object} parameters
* @param {Object} [ajaxOptions]
* @return {jQuery.Promise} Done: API response data. Fail: Error code.
*/
Api.prototype.ajax = function ( parameters, ajaxOptions ) {
var start = Date.now(),
api = this;
return mw.Api.prototype.ajax.call( this, parameters, ajaxOptions ).done( function ( result, jqxhr ) {
api.performance.recordJQueryEntryDelayed( api.type, Date.now() - start, jqxhr );
} );
};
mw.mmv.logging.Api = Api;
}() );

View file

@ -1,73 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var AL;
/**
* Writes EventLogging entries for duration measurements
*
* @class mw.mmv.logging.AttributionLogger
* @extends mw.mmv.logging.Logger
* @constructor
*/
function AttributionLogger() {}
OO.inheritClass( AttributionLogger, mw.mmv.logging.Logger );
AL = AttributionLogger.prototype;
/**
* @override
* @inheritdoc
*/
AL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).attributionSamplingFactor;
/**
* @override
* @inheritdoc
*/
AL.schema = 'MultimediaViewerAttribution';
/**
* Logs attribution data
*
* @param {mw.mmv.model.Image} image Image data
*/
AL.logAttribution = function ( image ) {
var data;
data = {
authorPresent: !!image.author,
sourcePresent: !!image.source,
licensePresent: !!image.license,
loggedIn: !mw.user.isAnon(),
samplingFactor: this.samplingFactor
};
if ( this.isEnabled() ) {
mw.log( 'author: ' + ( data.authorPresent ? 'present' : 'absent' ) +
', source: ' + ( data.sourcePresent ? 'present' : 'absent' ) +
', license: ' + ( data.licensePresent ? 'present' : 'absent' ) );
}
this.log( data );
};
mw.mmv.logging.AttributionLogger = AttributionLogger;
mw.mmv.attributionLogger = new AttributionLogger();
}() );

View file

@ -1,81 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var DL;
/**
* Writes EventLogging entries for size measurements related to thumbnail size selection
* (bucket size vs. display size).
*
* @class mw.mmv.logging.DimensionLogger
* @extends mw.mmv.logging.Logger
* @constructor
*/
function DimensionLogger() {}
OO.inheritClass( DimensionLogger, mw.mmv.logging.Logger );
DL = DimensionLogger.prototype;
/**
* @override
* @inheritdoc
*/
DL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).dimensionSamplingFactor;
/**
* @override
* @inheritdoc
*/
DL.schema = 'MultimediaViewerDimensions';
/**
* Logs dimension data.
*
* @param {mw.mmv.model.ThumbnailWidth} imageWidths Widths of the image that will be displayed
* @param {Object} canvasDimensions Canvas width and height in CSS pixels
* @param {string} context Reason for requesting the image, one of 'show', 'resize', 'preload'
*/
DL.logDimensions = function ( imageWidths, canvasDimensions, context ) {
var data;
data = {
screenWidth: screen.width,
screenHeight: screen.height,
viewportWidth: $( window ).width(),
viewportHeight: $( window ).height(),
canvasWidth: canvasDimensions.width,
canvasHeight: canvasDimensions.height,
devicePixelRatio: window.devicePixelRatio || 1,
imgWidth: imageWidths.cssWidth,
imageAspectRatio: imageWidths.cssWidth / imageWidths.cssHeight,
thumbWidth: imageWidths.real,
context: context,
samplingFactor: this.samplingFactor
};
if ( this.isEnabled() ) {
mw.log( 'mw.mmw.logger.DimensionLogger', data );
}
this.log( data );
};
mw.mmv.logging.DimensionLogger = DimensionLogger;
mw.mmv.dimensionLogger = new DimensionLogger();
}() );

View file

@ -1,457 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
var PL;
/**
* Measures the network performance
* See <https://meta.wikimedia.org/wiki/Schema:MultimediaViewerNetworkPerformance>
*
* @class mw.mmv.logging.PerformanceLogger
* @extends mw.mmv.logging.Logger
* @constructor
*/
function PerformanceLogger() {}
OO.inheritClass( PerformanceLogger, mw.mmv.logging.Logger );
PL = PerformanceLogger.prototype;
/**
* @override
* @inheritdoc
*/
PL.samplingFactor = mw.config.get( 'wgMultimediaViewer' ).networkPerformanceSamplingFactor;
/**
* @override
* @inheritdoc
*/
PL.schema = 'MultimediaViewerNetworkPerformance';
/**
* Global setup that should be done while the page loads
*/
PL.init = function () {
var performance = this.getWindowPerformance();
// by default logging is cut off after 150 resources, which is not enough in debug mode
// only supported by IE
if ( mw.config.get( 'debug' ) && performance && performance.setResourceTimingBufferSize ) {
performance.setResourceTimingBufferSize( 500 );
}
};
/**
* Gather network performance for a given URL
* Will only run on a sample of users/requests. Avoid using this on URLs that aren't
* cached by the browser, as it will consume unnecessary bandwidth for the user.
*
* @param {string} type the type of request to be measured
* @param {string} url URL to be measured
* @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to the extra stats.
* @return {jQuery.Promise} A promise that resolves when the contents of the URL have been fetched
*/
PL.record = function ( type, url, extraStatsDeferred ) {
var deferred = $.Deferred(),
request,
perf = this,
start;
try {
request = this.newXHR();
request.onprogress = function ( e ) {
var percent;
if ( e.lengthComputable ) {
percent = ( e.loaded / e.total ) * 100;
}
deferred.notify( request.response, percent );
};
request.onreadystatechange = function () {
var total = Date.now() - start;
if ( request.readyState === 4 ) {
deferred.notify( request.response, 100 );
deferred.resolve( request.response );
perf.recordEntryDelayed( type, total, url, request, extraStatsDeferred );
}
};
start = Date.now();
request.open( 'GET', url, true );
request.send();
} catch ( e ) {
// old browser not supporting XMLHttpRequest or CORS, or CORS is not permitted
return deferred.reject();
}
return deferred;
};
/**
* Records network performance results for a given url
* Will record if enough data is present and it's not a local cache hit
*
* @param {string} type the type of request to be measured
* @param {number} total the total load time tracked with a basic technique
* @param {string} url URL of that was measured
* @param {XMLHttpRequest} request HTTP request that just completed
* @param {jQuery.Deferred.<string>} [extraStatsDeferred] A promise which resolves to extra stats to be included.
* @return {jQuery.Promise}
*/
PL.recordEntry = function ( type, total, url, request, extraStatsDeferred ) {
var matches,
logger = this,
stats = { type: type,
contentHost: location.host,
isHttps: location.protocol === 'https:',
total: total },
connection = this.getNavigatorConnection();
if ( !this.performanceChecked ) {
this.performanceChecked = {};
}
if ( url && url.length ) {
// There is no need to measure the same url more than once
if ( url in this.performanceChecked ) {
return $.Deferred().reject();
}
this.performanceChecked[ url ] = true;
matches = url.match( /^https?:\/\/([^/?#]+)(?:[/?#]|$)/i );
stats.isHttps = url.indexOf( 'https' ) === 0;
}
if ( !matches || matches.length !== 2 ) {
stats.urlHost = stats.contentHost;
} else {
stats.urlHost = matches[ 1 ];
}
this.populateStatsFromXhr( stats, request );
this.populateStatsFromPerformance( stats, url );
// Add connection information if there's any
if ( connection ) {
if ( connection.bandwidth ) {
if ( connection.bandwidth === Infinity ) {
stats.bandwidth = -1;
} else {
stats.bandwidth = Math.round( connection.bandwidth );
}
}
if ( connection.metered ) {
stats.metered = connection.metered;
}
}
return ( extraStatsDeferred || $.Deferred().reject() ).done( function ( extraStats ) {
stats = $.extend( stats, extraStats );
} ).always( function () {
logger.log( stats );
} );
};
/**
* Processes an XMLHttpRequest (or jqXHR) object
*
* @param {Object} stats stats object to extend with additional statistics fields
* @param {XMLHttpRequest} request
*/
PL.populateStatsFromXhr = function ( stats, request ) {
var age,
contentLength,
xcache,
xvarnish,
varnishXCache,
lastModified;
if ( !request ) {
return;
}
stats.status = request.status;
// Chrome disallows header access for CORS image requests, even if the responose has the
// proper header :-/
contentLength = request.getResponseHeader( 'Content-Length' );
if ( contentLength === null ) {
return;
}
xcache = request.getResponseHeader( 'X-Cache' );
if ( xcache ) {
stats.XCache = xcache;
varnishXCache = this.parseVarnishXCacheHeader( xcache );
// eslint-disable-next-line no-jquery/no-each-util
$.each( varnishXCache, function ( key, value ) {
stats[ key ] = value;
} );
}
xvarnish = request.getResponseHeader( 'X-Varnish' );
if ( xvarnish ) {
stats.XVarnish = xvarnish;
}
stats.contentLength = parseInt( contentLength, 10 );
age = parseInt( request.getResponseHeader( 'Age' ), 10 );
if ( !isNaN( age ) ) {
stats.age = age;
}
stats.timestamp = new Date( request.getResponseHeader( 'Date' ) ).getTime() / 1000;
lastModified = request.getResponseHeader( 'Last-Modified' );
if ( lastModified ) {
stats.lastModified = new Date( lastModified ).getTime() / 1000;
}
};
/**
* Populates statistics based on the Request Timing API
*
* @param {Object} stats
* @param {string} url
*/
PL.populateStatsFromPerformance = function ( stats, url ) {
var performance = this.getWindowPerformance(),
timingEntries, timingEntry;
// If we're given an xhr and we have access to the Navigation Timing API, use it
if ( performance && performance.getEntriesByName ) {
// This could be tricky as we need to match encoding (the Request Timing API uses
// percent-encoded UTF-8). The main use case we are interested in is thumbnails and
// jQuery AJAX. jQuery uses encodeURIComponent to construct URL parameters, and
// thumbnail URLs come from MediaWiki API which also encodes them, so both should be
// all right.
timingEntries = performance.getEntriesByName( url );
if ( timingEntries.length ) {
// Let's hope it's the first request for the given URL we are interested in.
// This could fail in exotic cases (e.g. we send an AJAX request for a thumbnail,
// but it exists on the page as a normal thumbnail with the exact same size),
// but it's unlikely.
timingEntry = timingEntries[ 0 ];
stats.total = Math.round( timingEntry.duration );
stats.redirect = Math.round( timingEntry.redirectEnd - timingEntry.redirectStart );
stats.dns = Math.round( timingEntry.domainLookupEnd - timingEntry.domainLookupStart );
stats.tcp = Math.round( timingEntry.connectEnd - timingEntry.connectStart );
stats.request = Math.round( timingEntry.responseStart - timingEntry.requestStart );
stats.response = Math.round( timingEntry.responseEnd - timingEntry.responseStart );
stats.cache = Math.round( timingEntry.domainLookupStart - timingEntry.fetchStart );
} else if ( performance.getEntriesByType( 'resource' ).length === 150 && this.isEnabled() ) {
// browser stops logging after 150 entries
mw.log( 'performance buffer full, results are probably incorrect' );
}
}
};
/**
* Like recordEntry, but takes a jqXHR argument instead of a normal XHR one.
* Due to the way some parameters are retrieved, this will work best if the context option
* for the ajax request was not used.
*
* @param {string} type the type of request to be measured
* @param {number} total the total load time tracked with a basic technique
* @param {jQuery.jqXHR} jqxhr
*/
PL.recordJQueryEntry = function ( type, total, jqxhr ) {
var perf = this;
// We take advantage of the fact that the context of the jqXHR deferred is the AJAX
// settings object. The deferred has already resolved so chaining to it does not influence
// the timing.
jqxhr.done( function () {
var url;
if ( !this.url ) {
mw.log.warn( 'Cannot find URL - did you use context option?' );
} else {
url = this.url;
// The performance API returns absolute URLs, but the one in the settings object is
// usually relative.
if ( !url.match( /^(\w+:)?\/\// ) ) {
url = location.protocol + '//' + location.host + url;
}
}
if ( this.crossDomain && this.dataType === 'jsonp' ) {
// Cross-domain jQuery requests return a fake jqXHR object which is useless and
// would only cause logging errors.
jqxhr = undefined;
}
// jQuery does not expose the original XHR object, but the jqXHR wrapper is similar
// enogh that we will probably get away by passing it instead.
perf.recordEntry( type, total, url, jqxhr );
} );
};
/**
* Records network performance results for a given url
* Will record if enough data is present and it's not a local cache hit
* Will run after a delay to make sure the window.performance entry is present
*
* @param {string} type the type of request to be measured
* @param {number} total the total load time tracked with a basic technique
* @param {string} url URL of that was measured
* @param {XMLHttpRequest} request HTTP request that just completed
* @param {jQuery.Promise.<string>} extraStatsDeferred A promise which resolves to extra stats.
*/
PL.recordEntryDelayed = function ( type, total, url, request, extraStatsDeferred ) {
var perf = this;
// The timeout is necessary because if there's an entry in window.performance,
// it hasn't been added yet at this point
setTimeout( function () {
perf.recordEntry( type, total, url, request, extraStatsDeferred );
}, 0 );
};
/**
* Like recordEntryDelayed, but for jQuery AJAX requests.
*
* @param {string} type the type of request to be measured
* @param {number} total the total load time tracked with a basic technique
* @param {jQuery.jqXHR} jqxhr
*/
PL.recordJQueryEntryDelayed = function ( type, total, jqxhr ) {
var perf = this;
// The timeout is necessary because if there's an entry in window.performance,
// it hasn't been added yet at this point
setTimeout( function () {
perf.recordJQueryEntry( type, total, jqxhr );
}, 0 );
};
/**
* Parses an X-Cache header from Varnish and extracts varnish information
*
* @param {string} header The X-Cache header from the request
* @return {Object} The parsed X-Cache data
*/
PL.parseVarnishXCacheHeader = function ( header ) {
var parts,
part,
subparts,
i,
results = {},
matches;
if ( !header || !header.length ) {
return results;
}
parts = header.split( ',' );
for ( i = 0; i < parts.length; i++ ) {
part = parts[ i ];
subparts = part.trim().split( ' ' );
// If the subparts aren't space-separated, it's an unknown format, skip
if ( subparts.length < 2 ) {
continue;
}
matches = part.match( /\(([0-9]+)\)/ );
// If there is no number between parenthesis for a given server
// it's an unknown format, skip
if ( !matches || matches.length !== 2 ) {
continue;
}
results[ 'varnish' + ( i + 1 ) ] = subparts[ 0 ];
results[ 'varnish' + ( i + 1 ) + 'hits' ] = parseInt( matches[ 1 ], 10 );
}
return results;
};
/**
* Returns the window's Performance object
* Allows us to override for unit tests
*
* @return {Object} The window's Performance object
*/
PL.getWindowPerformance = function () {
return window.performance;
};
/**
* Returns the navigator's Connection object
* Allows us to override for unit tests
*
* @return {Object} The navigator's Connection object
*/
PL.getNavigatorConnection = function () {
// eslint-disable-next-line compat/compat
return navigator.connection || navigator.mozConnection || navigator.webkitConnection;
};
/**
* Returns a new XMLHttpRequest object
* Allows us to override for unit tests
*
* @return {XMLHttpRequest} New XMLHttpRequest
*/
PL.newXHR = function () {
return new XMLHttpRequest();
};
/**
* @override
* @inheritdoc
*/
PL.log = function ( data ) {
var trackedWidths = mw.mmv.ThumbnailWidthCalculator.prototype.defaultOptions.widthBuckets.slice( 0 );
trackedWidths.push( 600 ); // Most common non-bucket size
// Track thumbnail load time with statsv, sampled
if ( this.isInSample() &&
data.type === 'image' &&
data.imageWidth > 0 &&
data.total > 20 &&
trackedWidths.indexOf( data.imageWidth ) !== -1
) {
mw.track( 'timing.media.thumbnail.client.' + data.imageWidth, data.total );
}
if ( this.isEnabled() ) {
mw.log( 'mw.mmv.logging.PerformanceLogger', data );
}
return mw.mmv.logging.Logger.prototype.log.call( this, data );
};
new PerformanceLogger().init();
mw.mmv.logging.PerformanceLogger = PerformanceLogger;
}() );

View file

@ -26,9 +26,8 @@
* @constructor
* @param {mw.mmv.Config} config mw.mmv.Config object
* @param {Object} windowObject Browser window object
* @param {mw.mmv.logging.ActionLogger} actionLogger ActionLogger object
*/
function ViewLogger( config, windowObject, actionLogger ) {
function ViewLogger( config, windowObject ) {
/**
* Was the last image view logged or was logging skipped?
*
@ -70,13 +69,6 @@
* @property {Object}
*/
this.window = windowObject;
/**
* Action logger
*
* @property {mw.mmv.logging.ActionLogger}
*/
this.actionLogger = actionLogger;
}
VL = ViewLogger.prototype;
@ -90,7 +82,6 @@
}
this.wasLastViewLogged = false;
this.actionLogger.log( 'image-unview', true );
};
/**
@ -186,5 +177,6 @@
this.wasLastViewLogged = wasEventLogged;
};
mw.mmv.logging = mw.mmv.logging || {};
mw.mmv.logging.ViewLogger = ViewLogger;
}() );

View file

@ -29,7 +29,8 @@
*/
function MultimediaViewer( config ) {
var apiCacheMaxAge = 86400, // one day (24 hours * 60 min * 60 sec)
apiCacheFiveMinutes = 300; // 5 min * 60 sec
apiCacheFiveMinutes = 300, // 5 min * 60 sec
api = new mw.Api();
/**
* @property {mw.mmv.Config}
@ -47,7 +48,7 @@
* @property {mw.mmv.provider.ImageInfo}
* @private
*/
this.imageInfoProvider = new mw.mmv.provider.ImageInfo( new mw.mmv.logging.Api( 'imageinfo' ), {
this.imageInfoProvider = new mw.mmv.provider.ImageInfo( api, {
language: this.config.language(),
maxage: apiCacheFiveMinutes
} );
@ -56,14 +57,14 @@
* @property {mw.mmv.provider.FileRepoInfo}
* @private
*/
this.fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( new mw.mmv.logging.Api( 'filerepoinfo' ),
this.fileRepoInfoProvider = new mw.mmv.provider.FileRepoInfo( api,
{ maxage: apiCacheMaxAge } );
/**
* @property {mw.mmv.provider.ThumbnailInfo}
* @private
*/
this.thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( new mw.mmv.logging.Api( 'thumbnailinfo' ),
this.thumbnailInfoProvider = new mw.mmv.provider.ThumbnailInfo( api,
{ maxage: apiCacheMaxAge } );
/**
@ -114,7 +115,7 @@
/**
* @property {mw.mmv.logging.ViewLogger} view -
*/
this.viewLogger = new mw.mmv.logging.ViewLogger( this.config, window, mw.mmv.actionLogger );
this.viewLogger = new mw.mmv.logging.ViewLogger( this.config, window );
/**
* Stores whether the real image was loaded and displayed already.
@ -198,7 +199,7 @@
* @param {mw.mmv.LightboxInterface} ui lightbox that got resized
*/
MMVP.resize = function ( ui ) {
var imageWidths, canvasDimensions,
var imageWidths,
viewer = this,
image = this.thumbs[ this.currentIndex ].image,
ext = this.thumbs[ this.currentIndex ].title.getExtension().toLowerCase();
@ -207,9 +208,6 @@
if ( image ) {
imageWidths = ui.canvas.getCurrentImageWidths();
canvasDimensions = ui.canvas.getDimensions();
mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'resize' );
this.fetchThumbnailForLightboxImage(
image, imageWidths.real
@ -258,7 +256,6 @@
*/
MMVP.loadImage = function ( image, initialImage, useReplaceState ) {
var imageWidths,
canvasDimensions,
imagePromise,
metadataPromise,
pluginsPromise,
@ -300,12 +297,9 @@
// this.preloadFullscreenThumbnail( image ); // disabled - #474
imageWidths = this.ui.canvas.getCurrentImageWidths();
canvasDimensions = this.ui.canvas.getDimensions();
start = Date.now();
mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'show' );
imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real, extraStatsDeferred );
this.resetBlurredThumbnailStates();
@ -324,20 +318,6 @@
return;
}
if ( viewer.imageDisplayedCount++ === 0 ) {
mw.mmv.durationLogger.stop( 'click-to-first-image' );
metadataPromise.then( function ( imageInfo, repoInfo ) {
if ( imageInfo && imageInfo.anonymizedUploadDateTime ) {
mw.mmv.durationLogger.record( 'click-to-first-image', {
uploadTimestamp: imageInfo.anonymizedUploadDateTime
} );
}
return $.Deferred().resolve( imageInfo, repoInfo );
} );
}
// eslint-disable-next-line mediawiki/class-doc
imageElement.className = 'mw-mmv-final-image ' + image.filePageTitle.getExtension().toLowerCase();
imageElement.alt = image.alt;
@ -366,10 +346,6 @@
return;
}
if ( viewer.metadataDisplayedCount++ === 0 ) {
mw.mmv.durationLogger.stop( 'click-to-first-metadata' ).record( 'click-to-first-metadata' );
}
viewer.ui.panel.setImageInfo( image, imageInfo, repoInfo );
// File reuse steals a bunch of information from the DOM, so do it last
@ -451,8 +427,6 @@
* @param {number} loadTime Time it took to load the thumbnail
*/
MMVP.displayRealThumbnail = function ( thumbnail, imageElement, imageWidths, loadTime ) {
var viewer = this;
this.realThumbnailShown = true;
this.setImage( this.ui, thumbnail, imageElement, imageWidths );
@ -467,10 +441,6 @@
}
this.viewLogger.attach( thumbnail.url );
mw.mmv.actionLogger.log( 'image-view' ).then( function ( wasEventLogged ) {
viewer.viewLogger.setLastViewLogged( wasEventLogged );
} );
};
/**
@ -717,7 +687,7 @@
this.thumbnailPreloadQueue = this.pushLightboxImagesIntoQueue( function ( lightboxImage, extraStatsDeferred ) {
return function () {
var imageWidths, canvasDimensions;
var imageWidths;
// viewer.ui.canvas.getLightboxImageWidths needs the viewer to be open
// because it needs to read the size of visible elements
@ -726,9 +696,6 @@
}
imageWidths = viewer.ui.canvas.getLightboxImageWidths( lightboxImage );
canvasDimensions = viewer.ui.canvas.getDimensions();
mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' );
return viewer.fetchThumbnailForLightboxImage( lightboxImage, imageWidths.real, extraStatsDeferred );
};
@ -743,10 +710,8 @@
* @param {mw.mmv.LightboxImage} image
*/
MMVP.preloadFullscreenThumbnail = function ( image ) {
var imageWidths = this.ui.canvas.getLightboxImageWidthsForFullscreen( image ),
canvasDimensions = this.ui.canvas.getDimensions( true );
var imageWidths = this.ui.canvas.getLightboxImageWidthsForFullscreen( image );
mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' );
this.fetchThumbnailForLightboxImage( image, imageWidths.real );
};
@ -876,7 +841,6 @@
* Opens the next image
*/
MMVP.nextImage = function () {
mw.mmv.actionLogger.log( 'next-image' );
this.loadIndex( this.currentIndex + 1 );
};
@ -884,7 +848,6 @@
* Opens the previous image
*/
MMVP.prevImage = function () {
mw.mmv.actionLogger.log( 'prev-image' );
this.loadIndex( this.currentIndex - 1 );
};

View file

@ -251,8 +251,6 @@
* Detaches the interface from the DOM.
*/
LIP.unattach = function () {
mw.mmv.actionLogger.log( 'close' );
// We trigger this event on the document because unattach() can run
// when the interface is unattached
// We're calling this before cleaning up (below) the DOM, as that
@ -375,14 +373,10 @@
this.isFullscreen = e.fullscreen;
if ( this.isFullscreen ) {
mw.mmv.actionLogger.log( 'fullscreen' );
this.$fullscreenButton
.prop( 'title', mw.message( 'multimediaviewer-defullscreen-popup-text' ).text() )
.attr( 'alt', mw.message( 'multimediaviewer-defullscreen-popup-text' ).text() );
} else {
mw.mmv.actionLogger.log( 'defullscreen' );
this.$fullscreenButton
.prop( 'title', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() )
.attr( 'alt', mw.message( 'multimediaviewer-fullscreen-popup-text' ).text() );

View file

@ -25,12 +25,6 @@
* @param {string} imageQueryParameter When defined, is a query parameter to add to every image request
*/
function Image( imageQueryParameter ) {
/**
* @property {mw.mmv.logging.PerformanceLogger}
* @private
*/
this.performance = new mw.mmv.logging.PerformanceLogger();
this.imageQueryParameter = imageQueryParameter;
/**
@ -43,21 +37,18 @@
}
/**
* Loads an image and returns it. Includes performance metrics via mw.mmv.logging.PerformanceLogger.
* When the browser supports it, the image is loaded as an AJAX request.
* Loads an image and returns it. When the browser supports it, the image is loaded as an AJAX
* request.
*
* @param {string} url
* @param {jQuery.Deferred.<string>} extraStatsDeferred A promise which resolves to extra statistics.
* @return {jQuery.Promise.<HTMLImageElement>} A promise which resolves to the image object.
* When loaded via AJAX, it has progress events, which return an array with the content loaded
* so far and with the progress as a floating-point number between 0 and 100.
*/
Image.prototype.get = function ( url, extraStatsDeferred ) {
Image.prototype.get = function ( url ) {
var provider = this,
cacheKey = url,
extraParam = {},
start,
rawGet,
uri;
if ( this.imageQueryParameter ) {
@ -71,16 +62,7 @@
}
if ( !this.cache[ cacheKey ] ) {
if ( this.imagePreloadingSupported() ) {
rawGet = provider.rawGet.bind( provider, url, true );
this.cache[ cacheKey ] = this.performance.record( 'image', url, extraStatsDeferred ).then( rawGet, rawGet );
} else {
start = Date.now();
this.cache[ cacheKey ] = this.rawGet( url );
this.cache[ cacheKey ].always( function () {
provider.performance.recordEntry( 'image', Date.now() - start, url, undefined, extraStatsDeferred );
} );
}
this.cache[ cacheKey ] = this.rawGet( url, this.imagePreloadingSupported() );
this.cache[ cacheKey ].fail( function ( error ) {
mw.log( provider.constructor.name + ' provider failed to load: ', error );
} );

View file

@ -207,20 +207,15 @@
canvas.$container.closest( '.metadata-panel-is-open' ).length === 0
) {
e.stopPropagation(); // don't let $imageWrapper handle this
mw.mmv.actionLogger.log( 'view-original-file' ).always( function () {
$( document ).trigger( 'mmv-viewfile' );
} );
$( document ).trigger( 'mmv-viewfile' );
}
} );
// open the download panel on right clicking the image
this.$image.on( 'mousedown.mmv-canvas', function ( e ) {
if ( e.which === 3 ) {
mw.mmv.actionLogger.log( 'right-click-image' );
if ( !canvas.downloadOpen ) {
$( document ).trigger( 'mmv-download-open', e );
e.stopPropagation();
}
if ( e.which === 3 && !canvas.downloadOpen ) {
$( document ).trigger( 'mmv-download-open', e );
e.stopPropagation();
}
} );
};

View file

@ -123,8 +123,6 @@
* Opens a dialog.
*/
DP.openDialog = function () {
mw.mmv.actionLogger.log( this.eventPrefix + '-open' );
this.startListeningToOutsideClick();
this.$dialog.show();
this.isOpen = true;
@ -135,10 +133,6 @@
* Closes a dialog.
*/
DP.closeDialog = function () {
if ( this.isOpen ) {
mw.mmv.actionLogger.log( this.eventPrefix + '-close' );
}
this.stopListeningToOutsideClick();
this.$dialog.hide();
this.isOpen = false;

View file

@ -260,20 +260,11 @@
MPP.initializeCredit = function () {
this.$credit = $( '<p>' )
.addClass( 'mw-mmv-credit empty' )
.appendTo( this.$imageMetadataLeft )
.on( 'click.mmv-mp', '.mw-mmv-credit-fallback', function () {
mw.mmv.actionLogger.log( 'author-page' );
} );
.appendTo( this.$imageMetadataLeft );
// we need an inline container for tipsy, otherwise it would be centered weirdly
this.$authorAndSource = $( '<span>' )
.addClass( 'mw-mmv-source-author' )
.on( 'click', '.mw-mmv-author a', function () {
mw.mmv.actionLogger.log( 'author-page' );
} )
.on( 'click', '.mw-mmv-source a', function () {
mw.mmv.actionLogger.log( 'source-page' );
} );
.addClass( 'mw-mmv-source-author' );
this.creditField = new mw.mmv.ui.TruncatableTextField(
this.$credit,
@ -323,10 +314,7 @@
this.$license = $( '<a>' )
.addClass( 'mw-mmv-license' )
.prop( 'href', '#' )
.appendTo( this.$licenseLi )
.on( 'click', function () {
mw.mmv.actionLogger.log( 'license-page' );
} );
.appendTo( this.$licenseLi );
this.$restrictions = $( '<span>' )
.addClass( 'mw-mmv-restrictions' )
@ -388,8 +376,7 @@
this.$location = $( '<a>' )
.addClass( 'mw-mmv-location' )
.appendTo( this.$locationLi )
.on( 'click', function () { mw.mmv.actionLogger.log( 'location-page' ); } );
.appendTo( this.$locationLi );
};
/**
@ -399,8 +386,7 @@
this.$mmvAboutLink = $( '<a>' )
.prop( 'href', mw.config.get( 'wgMultimediaViewer' ).infoLink )
.text( mw.message( 'multimediaviewer-about-mmv' ).text() )
.addClass( 'mw-mmv-about-link' )
.on( 'click', function () { mw.mmv.actionLogger.log( 'about-page' ); } );
.addClass( 'mw-mmv-about-link' );
this.$mmvAboutLinks = $( '<div>' )
.addClass( 'mw-mmv-about-links' )
@ -782,8 +768,6 @@
MPP.setImageInfo = function ( image, imageData, repoData ) {
var panel = this;
mw.mmv.attributionLogger.logAttribution( imageData );
if ( imageData.creationDateTime ) {
panel.setDateTime( this.formatDate( imageData.creationDateTime ), true );
} else if ( imageData.uploadDateTime ) {

View file

@ -185,7 +185,6 @@
if ( scrollTopTarget === scrollTop ) {
return $.Deferred().resolve().promise();
} else {
mw.mmv.actionLogger.log( direction === 'up' ? 'metadata-open' : 'metadata-close' );
if ( direction === 'up' && !panelIsOpen ) {
// FIXME nasty. This is not really an event but a command sent to the metadata panel;
// child UI elements should not send commands to their parents. However, there is no way
@ -239,13 +238,8 @@
if ( panelIsOpen && !this.panelWasOpen ) { // just opened
this.$container.trigger( 'mmv-metadata-open' );
// This will include keyboard- and mouseclick-initiated open events as well,
// since the panel is anomated, which counts as scrolling.
// Filtering these seems too much trouble to be worth it.
mw.mmv.actionLogger.log( 'metadata-scroll-open' );
} else if ( !panelIsOpen && this.panelWasOpen ) { // just closed
this.$container.trigger( 'mmv-metadata-close' );
mw.mmv.actionLogger.log( 'metadata-scroll-close' );
}
this.panelWasOpen = panelIsOpen;
};

View file

@ -147,8 +147,6 @@
* @fires mmv-permission-grow
*/
P.grow = function () {
mw.mmv.actionLogger.log( 'terms-open' );
// FIXME: Use CSS transition
// eslint-disable-next-line no-jquery/no-animate
this.$box.addClass( 'full-size' )

View file

@ -75,9 +75,7 @@
SBP.initDescriptionPageButton = function () {
this.buttons.$descriptionPage = this.createButton(
'empty mw-mmv-description-page-button mw-ui-big mw-ui-button mw-ui-progressive'
).on( 'click', function () {
mw.mmv.actionLogger.log( 'file-description-page-abovefold' );
} );
);
};
/**

View file

@ -240,7 +240,7 @@
}
this.addText( $div, msgs, true );
this.addInfoLink( $div, ( enabled ? 'enable' : 'disable' ) + '-about-link' );
this.addInfoLink( $div );
this.makeButtons( $div, smsg, enabled );
this[ propName ] = $div;
@ -287,8 +287,6 @@
$buttons.prop( 'disabled', true );
dialog.config.setMediaViewerEnabledOnClick( enabled ).done( function () {
mw.mmv.actionLogger.log( 'opt' + ( enabled ? 'in' : 'out' ) + '-' + ( mw.user.isAnon() ? 'anon' : 'loggedin' ) );
if ( enabled ) {
dialog.showEnableConfirmation();
} else {
@ -379,14 +377,12 @@
* Adds the info link to the panel.
*
* @param {jQuery} $div The panel to which we're adding the link.
* @param {string} eventName
*/
ODP.addInfoLink = function ( $div, eventName ) {
ODP.addInfoLink = function ( $div ) {
$( '<a>' )
.addClass( 'mw-mmv-project-info-link' )
.prop( 'href', mw.config.get( 'wgMultimediaViewer' ).helpLink )
.text( mw.message( 'multimediaviewer-options-learn-more' ) )
.on( 'click', function () { mw.mmv.actionLogger.log( eventName ); } )
.appendTo( $div.find( '.mw-mmv-options-text' ) );
};

View file

@ -1,46 +0,0 @@
QUnit.module( 'mmv.logging.ActionLogger', QUnit.newMwEnvironment() );
QUnit.test( 'log()', function ( assert ) {
var fakeEventLog = { logEvent: this.sandbox.stub() };
var logger = new mw.mmv.logging.ActionLogger();
var action1key = 'test-1';
var action1value = 'Test';
var action2key = 'test-2';
var action2value = 'Foo $1 $2 bar';
var unknownAction = 'test-3';
var clock = this.sandbox.useFakeTimers();
this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() );
this.sandbox.stub( mw, 'log' );
logger.samplingFactorMap = { default: 1 };
logger.setEventLog( fakeEventLog );
logger.logActions = {};
logger.logActions[ action1key ] = action1value;
logger.logActions[ action2key ] = action2value;
logger.log( unknownAction );
clock.tick( 10 );
assert.strictEqual( mw.log.lastCall.args[ 0 ], unknownAction, 'Log message defaults to unknown key' );
assert.strictEqual( fakeEventLog.logEvent.called, true, 'event log has been recorded' );
mw.log.reset();
fakeEventLog.logEvent.reset();
logger.log( action1key );
clock.tick( 10 );
assert.strictEqual( mw.log.lastCall.args[ 0 ], action1value, 'Log message is translated to its text' );
assert.strictEqual( fakeEventLog.logEvent.called, true, 'event log has been recorded' );
mw.log.reset();
fakeEventLog.logEvent.reset();
logger.samplingFactorMap = { default: 0 };
logger.log( action1key, true );
clock.tick( 10 );
assert.strictEqual( mw.log.called, false, 'No logging when disabled' );
assert.strictEqual( fakeEventLog.logEvent.called, true, 'event log has been recorded' );
clock.restore();
} );

View file

@ -1,20 +0,0 @@
QUnit.module( 'mmv.logging.AttributionLogger', QUnit.newMwEnvironment() );
QUnit.test( 'log()', function ( assert ) {
var fakeEventLog = { logEvent: this.sandbox.stub() };
var logger = new mw.mmv.logging.AttributionLogger();
var image = { author: 'foo', source: 'bar', license: {} };
var emptyImage = {};
this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() );
this.sandbox.stub( mw, 'log' );
logger.samplingFactor = 1;
logger.setEventLog( fakeEventLog );
logger.logAttribution( image );
assert.true( true, 'logDimensions() did not throw errors' );
logger.logAttribution( emptyImage );
assert.true( true, 'logDimensions() did not throw errors for empty image' );
} );

View file

@ -1,15 +0,0 @@
QUnit.module( 'mmv.logging.DimensionLogger', QUnit.newMwEnvironment() );
QUnit.test( 'log()', function ( assert ) {
var fakeEventLog = { logEvent: this.sandbox.stub() };
var logger = new mw.mmv.logging.DimensionLogger();
this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() );
this.sandbox.stub( mw, 'log' );
logger.samplingFactor = 1;
logger.setEventLog( fakeEventLog );
logger.logDimensions( 640, 480, 200, 'resize' );
assert.true( true, 'logDimensions() did not throw errors' );
} );

View file

@ -1,218 +0,0 @@
( function () {
QUnit.module( 'mmv.logging.DurationLogger', QUnit.newMwEnvironment( {
beforeEach: function () {
this.clock = this.sandbox.useFakeTimers();
// since jQuery 2/3, $.now will capture a reference to Date.now
// before above fake timer gets a chance to override it, so I'll
// override that new behavior in order to run these tests...
// @see https://github.com/sinonjs/lolex/issues/76
this.oldNow = $.now;
$.now = function () { return Date.now(); };
},
afterEach: function () {
$.now = this.oldNow;
this.clock.restore();
}
} ) );
QUnit.test( 'start()', function ( assert ) {
var durationLogger = new mw.mmv.durationLogger.constructor();
durationLogger.samplingFactor = 1;
try {
durationLogger.start();
} catch ( e ) {
assert.true( true, 'Exception raised when calling start() without parameters' );
}
assert.strictEqual( $.isEmptyObject( durationLogger.starts ), true, 'No events saved by DurationLogger' );
durationLogger.start( 'foo' );
assert.strictEqual( durationLogger.starts.foo, 0, 'Event start saved' );
this.clock.tick( 1000 );
durationLogger.start( 'bar' );
assert.strictEqual( durationLogger.starts.bar, 1000, 'Later event start saved' );
durationLogger.start( 'foo' );
assert.strictEqual( durationLogger.starts.foo, 0, 'Event start not overritten' );
this.clock.tick( 666 );
durationLogger.start( [ 'baz', 'bob', 'bar' ] );
assert.strictEqual( durationLogger.starts.baz, 1666, 'First simultaneous event start saved' );
assert.strictEqual( durationLogger.starts.bob, 1666, 'Second simultaneous event start saved' );
assert.strictEqual( durationLogger.starts.bar, 1000, 'Third simultaneous event start not overwritten' );
} );
QUnit.test( 'stop()', function ( assert ) {
var durationLogger = new mw.mmv.durationLogger.constructor();
try {
durationLogger.stop();
} catch ( e ) {
assert.true( true, 'Exception raised when calling stop() without parameters' );
}
durationLogger.stop( 'foo' );
assert.strictEqual( durationLogger.stops.foo, 0, 'Event stop saved' );
this.clock.tick( 1000 );
durationLogger.stop( 'foo' );
assert.strictEqual( durationLogger.stops.foo, 0, 'Event stop not overwritten' );
durationLogger.stop( 'foo', 1 );
assert.strictEqual( durationLogger.starts.foo, 1, 'Event start saved' );
durationLogger.stop( 'foo', 2 );
assert.strictEqual( durationLogger.starts.foo, 1, 'Event start not overwritten' );
} );
QUnit.test( 'record()', function ( assert ) {
var dependenciesDeferred = $.Deferred(),
fakeEventLog = { logEvent: this.sandbox.stub() },
durationLogger = new mw.mmv.durationLogger.constructor();
durationLogger.samplingFactor = 1;
durationLogger.schemaSupportsCountry = this.sandbox.stub().returns( true );
this.sandbox.stub( mw.user, 'isAnon' ).returns( false );
this.sandbox.stub( durationLogger, 'loadDependencies' ).returns( dependenciesDeferred.promise() );
try {
durationLogger.record();
} catch ( e ) {
assert.true( true, 'Exception raised when calling record() without parameters' );
}
durationLogger.setEventLog( fakeEventLog );
durationLogger.start( 'bar' );
this.clock.tick( 1000 );
durationLogger.stop( 'bar' );
durationLogger.record( 'bar' );
assert.strictEqual( fakeEventLog.logEvent.called, false, 'Event queued if dependencies not loaded' );
// Queue a second item
durationLogger.start( 'bob' );
this.clock.tick( 4000 );
durationLogger.stop( 'bob' );
durationLogger.record( 'bob' );
assert.strictEqual( fakeEventLog.logEvent.called, false, 'Event queued if dependencies not loaded' );
dependenciesDeferred.resolve();
this.clock.tick( 10 );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
assert.deepEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ], { type: 'bar', duration: 1000, loggedIn: true, samplingFactor: 1 },
'EventLogging data is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
assert.deepEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ], { type: 'bob', duration: 4000, loggedIn: true, samplingFactor: 1 },
'EventLogging data is correct' );
assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'logEvent called when processing the queue' );
durationLogger.start( 'foo' );
this.clock.tick( 3000 );
durationLogger.stop( 'foo' );
durationLogger.record( 'foo' );
this.clock.tick( 10 );
assert.strictEqual( fakeEventLog.logEvent.getCall( 2 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
assert.deepEqual( fakeEventLog.logEvent.getCall( 2 ).args[ 1 ], { type: 'foo', duration: 3000, loggedIn: true, samplingFactor: 1 },
'EventLogging data is correct' );
assert.strictEqual( durationLogger.starts.bar, undefined, 'Start value deleted after record' );
assert.strictEqual( durationLogger.stops.bar, undefined, 'Stop value deleted after record' );
durationLogger.setGeo( { country: 'FR' } );
mw.user.isAnon.returns( true );
durationLogger.start( 'baz' );
this.clock.tick( 2000 );
durationLogger.stop( 'baz' );
durationLogger.record( 'baz' );
this.clock.tick( 10 );
assert.strictEqual( fakeEventLog.logEvent.getCall( 3 ).args[ 0 ], 'MultimediaViewerDuration', 'EventLogging schema is correct' );
assert.deepEqual( fakeEventLog.logEvent.getCall( 3 ).args[ 1 ], { type: 'baz', duration: 2000, loggedIn: false, country: 'FR', samplingFactor: 1 },
'EventLogging data is correct' );
assert.strictEqual( durationLogger.starts.bar, undefined, 'Start value deleted after record' );
assert.strictEqual( durationLogger.stops.bar, undefined, 'Stop value deleted after record' );
durationLogger.stop( 'fooz', Date.now() - 9000 );
durationLogger.record( 'fooz' );
this.clock.tick( 10 );
assert.deepEqual( fakeEventLog.logEvent.getCall( 4 ).args[ 1 ], { type: 'fooz', duration: 9000, loggedIn: false, country: 'FR', samplingFactor: 1 },
'EventLogging data is correct' );
assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'logEvent has been called fives times at this point in the test' );
durationLogger.stop( 'foo' );
durationLogger.record( 'foo' );
this.clock.tick( 10 );
assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'Record without a start doesn\'t get logged' );
durationLogger.start( 'foofoo' );
durationLogger.record( 'foofoo' );
this.clock.tick( 10 );
assert.strictEqual( fakeEventLog.logEvent.callCount, 5, 'Record without a stop doesn\'t get logged' );
durationLogger.start( 'extra' );
this.clock.tick( 5000 );
durationLogger.stop( 'extra' );
durationLogger.record( 'extra', { bim: 'bam' } );
this.clock.tick( 10 );
assert.deepEqual( fakeEventLog.logEvent.getCall( 5 ).args[ 1 ], { type: 'extra', duration: 5000, loggedIn: false, country: 'FR', samplingFactor: 1, bim: 'bam' },
'EventLogging data is correct' );
} );
QUnit.test( 'loadDependencies()', function ( assert ) {
var promise,
durationLogger = new mw.mmv.durationLogger.constructor();
this.sandbox.stub( mw.loader, 'using' );
mw.loader.using.withArgs( 'ext.eventLogging' ).throwsException( 'EventLogging is missing' );
promise = durationLogger.loadDependencies();
this.clock.tick( 10 );
assert.strictEqual( promise.state(), 'rejected', 'Promise is rejected' );
// It's necessary to reset the stub, otherwise the original withArgs keeps running alongside the new one
mw.loader.using.restore();
this.sandbox.stub( mw.loader, 'using' );
mw.loader.using.withArgs( 'ext.eventLogging' ).throwsException( 'EventLogging is missing' );
promise = durationLogger.loadDependencies();
this.clock.tick( 10 );
assert.strictEqual( promise.state(), 'rejected', 'Promise is rejected' );
// It's necessary to reset the stub, otherwise the original withArgs keeps running alongside the new one
mw.loader.using.restore();
this.sandbox.stub( mw.loader, 'using' );
mw.loader.using.withArgs( 'ext.eventLogging' ).callsArg( 1 );
promise = durationLogger.loadDependencies();
this.clock.tick( 10 );
assert.strictEqual( promise.state(), 'resolved', 'Promise is resolved' );
} );
}() );

View file

@ -1,341 +0,0 @@
/*
* This file is part of the MediaWiki extension MultimediaViewer.
*
* MultimediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MultimediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MultimediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
QUnit.module( 'mmv.logging.PerformanceLogger', QUnit.newMwEnvironment() );
function createFakeXHR( response ) {
return {
readyState: 0,
open: function () {},
send: function () {
var xhr = this;
setTimeout( function () {
xhr.readyState = 4;
xhr.response = response;
if ( typeof xhr.onreadystatechange === 'function' ) {
xhr.onreadystatechange();
}
}, 0 );
}
};
}
QUnit.test( 'recordEntry: basic', function ( assert ) {
var performance = new mw.mmv.logging.PerformanceLogger(),
fakeEventLog = { logEvent: this.sandbox.stub() },
type = 'gender',
total = 100,
// we'll be waiting for 4 promises to complete
asyncs = [ assert.async(), assert.async(), assert.async(), assert.async() ];
this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
this.sandbox.stub( performance, 'isInSample' );
performance.setEventLog( fakeEventLog );
performance.isInSample.returns( false );
performance.recordEntry( type, total ).then( null, function () {
assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'No stats should be logged if not in sample' );
asyncs.pop()();
} );
performance.isInSample.returns( true );
performance.recordEntry( type, total ).then( null, function () {
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' );
assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' );
asyncs.pop()();
} );
performance.recordEntry( type, total, 'URL' ).then( null, function () {
assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' );
asyncs.pop()();
} );
performance.recordEntry( type, total, 'URL' ).then( null, function () {
assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should not be logged a second time for the same URL' );
asyncs.pop()();
} );
} );
QUnit.test( 'recordEntry: with Navigation Timing data', function ( assert ) {
var fakeRequest,
varnish1 = 'cp1061',
varnish2 = 'cp3006',
varnish3 = 'cp3005',
varnish1hits = 0,
varnish2hits = 2,
varnish3hits = 1,
xvarnish = '1754811951 1283049064, 1511828531, 1511828573 1511828528',
xcache = varnish1 + ' miss (0), ' + varnish2 + ' miss (2), ' + varnish3 + ' frontend hit (1), malformed(5)',
age = '12345',
contentLength = '23456',
urlHost = 'fail',
date = 'Tue, 04 Feb 2014 11:11:50 GMT',
timestamp = 1391512310,
url = 'https://' + urlHost + '/balls.jpg',
redirect = 500,
dns = 2,
tcp = 10,
request = 25,
response = 50,
cache = 1,
perfData = {
initiatorType: 'xmlhttprequest',
name: url,
duration: 12345,
redirectStart: 1000,
redirectEnd: 1500,
domainLookupStart: 2,
domainLookupEnd: 4,
connectStart: 50,
connectEnd: 60,
requestStart: 125,
responseStart: 150,
responseEnd: 200,
fetchStart: 1
},
country = 'FR',
type = 'image',
performance = new mw.mmv.logging.PerformanceLogger(),
status = 200,
metered = true,
bandwidth = 45.67,
fakeEventLog = { logEvent: this.sandbox.stub() },
done = assert.async();
this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
performance.setEventLog( fakeEventLog );
performance.schemaSupportsCountry = this.sandbox.stub().returns( true );
this.sandbox.stub( performance, 'getWindowPerformance' ).returns( {
getEntriesByName: function () {
return [ perfData, {
initiatorType: 'bogus',
duration: 1234,
name: url
} ];
}
} );
this.sandbox.stub( performance, 'getNavigatorConnection' ).returns( { metered: metered, bandwidth: bandwidth } );
this.sandbox.stub( performance, 'isInSample' ).returns( true );
fakeRequest = {
getResponseHeader: function ( header ) {
switch ( header ) {
case 'X-Cache':
return xcache;
case 'X-Varnish':
return xvarnish;
case 'Age':
return age;
case 'Content-Length':
return contentLength;
case 'Date':
return date;
}
},
status: status
};
performance.setGeo( { country: country } );
performance.recordEntry( type, 100, url, fakeRequest ).then( null, function () {
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish1, varnish1, 'varnish1 is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish2, varnish2, 'varnish2 is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish3, varnish3, 'varnish3 is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish4, undefined, 'varnish4 is undefined' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish1hits, varnish1hits, 'varnish1hits is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish2hits, varnish2hits, 'varnish2hits is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish3hits, varnish3hits, 'varnish3hits is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].varnish4hits, undefined, 'varnish4hits is undefined' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].XVarnish, xvarnish, 'XVarnish is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].XCache, xcache, 'XCache is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].age, parseInt( age, 10 ), 'age is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].contentLength, parseInt( contentLength, 10 ), 'contentLength is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].contentHost, location.host, 'contentHost is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].urlHost, urlHost, 'urlHost is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].timestamp, timestamp, 'timestamp is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, perfData.duration, 'total is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].redirect, redirect, 'redirect is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].dns, dns, 'dns is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].tcp, tcp, 'tcp is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].request, request, 'request is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].response, response, 'response is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].cache, cache, 'cache is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].country, country, 'country is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].isHttps, true, 'isHttps is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].status, status, 'status is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].metered, metered, 'metered is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].bandwidth, Math.round( bandwidth ), 'bandwidth is correct' );
done();
} );
} );
QUnit.test( 'recordEntry: with async extra stats', function ( assert ) {
var performance = new mw.mmv.logging.PerformanceLogger(),
fakeEventLog = { logEvent: this.sandbox.stub() },
type = 'gender',
total = 100,
overriddenType = 'image',
foo = 'bar',
extraStatsPromise = $.Deferred(),
clock = this.sandbox.useFakeTimers();
this.sandbox.stub( performance, 'loadDependencies' ).returns( $.Deferred().resolve() );
this.sandbox.stub( performance, 'isInSample' );
performance.setEventLog( fakeEventLog );
performance.isInSample.returns( true );
performance.recordEntry( type, total, 'URL1', undefined, extraStatsPromise );
assert.strictEqual( fakeEventLog.logEvent.callCount, 0, 'Stats should not be logged if the promise hasn\'t completed yet' );
extraStatsPromise.reject();
extraStatsPromise.then( null, function () {
assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should be logged' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].type, type, 'type is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 0 ).args[ 1 ].total, total, 'total is correct' );
} );
// make sure first promise is completed before recording another entry,
// to make sure data in fakeEventLog doesn't suffer race conditions
clock.tick( 10 );
clock.restore();
extraStatsPromise = $.Deferred();
performance.recordEntry( type, total, 'URL2', undefined, extraStatsPromise );
assert.strictEqual( fakeEventLog.logEvent.callCount, 1, 'Stats should not be logged if the promise hasn\'t been resolved yet' );
extraStatsPromise.resolve( { type: overriddenType, foo: foo } );
return extraStatsPromise.then( function () {
assert.strictEqual( fakeEventLog.logEvent.callCount, 2, 'Stats should be logged' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 0 ], 'MultimediaViewerNetworkPerformance', 'EventLogging schema is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].type, overriddenType, 'type is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].total, total, 'total is correct' );
assert.strictEqual( fakeEventLog.logEvent.getCall( 1 ).args[ 1 ].foo, foo, 'extra stat is correct' );
} );
} );
QUnit.test( 'parseVarnishXCacheHeader', function ( assert ) {
var varnish1 = 'cp1061',
varnish2 = 'cp3006',
varnish3 = 'cp3005',
testString = varnish1 + ' miss (0), ' + varnish2 + ' miss (0), ' + varnish3 + ' frontend hit (1)',
performance = new mw.mmv.logging.PerformanceLogger(),
varnishXCache = performance.parseVarnishXCacheHeader( testString );
assert.strictEqual( varnishXCache.varnish1, varnish1, 'First varnish server name extracted' );
assert.strictEqual( varnishXCache.varnish2, varnish2, 'Second varnish server name extracted' );
assert.strictEqual( varnishXCache.varnish3, varnish3, 'Third varnish server name extracted' );
assert.strictEqual( varnishXCache.varnish4, undefined, 'Fourth varnish server is undefined' );
assert.strictEqual( varnishXCache.varnish1hits, 0, 'First varnish hit count extracted' );
assert.strictEqual( varnishXCache.varnish2hits, 0, 'Second varnish hit count extracted' );
assert.strictEqual( varnishXCache.varnish3hits, 1, 'Third varnish hit count extracted' );
assert.strictEqual( varnishXCache.varnish4hits, undefined, 'Fourth varnish hit count is undefined' );
testString = varnish1 + ' miss (36), ' + varnish2 + ' miss (2)';
varnishXCache = performance.parseVarnishXCacheHeader( testString );
assert.strictEqual( varnishXCache.varnish1, varnish1, 'First varnish server name extracted' );
assert.strictEqual( varnishXCache.varnish2, varnish2, 'Second varnish server name extracted' );
assert.strictEqual( varnishXCache.varnish3, undefined, 'Third varnish server is undefined' );
assert.strictEqual( varnishXCache.varnish1hits, 36, 'First varnish hit count extracted' );
assert.strictEqual( varnishXCache.varnish2hits, 2, 'Second varnish hit count extracted' );
assert.strictEqual( varnishXCache.varnish3hits, undefined, 'Third varnish hit count is undefined' );
varnishXCache = performance.parseVarnishXCacheHeader( 'garbage' );
assert.true( $.isEmptyObject( varnishXCache ), 'Varnish cache results are empty' );
} );
QUnit.test( 'record()', function ( assert ) {
var type = 'foo',
url = 'http://example.com/',
response = {},
done = assert.async(),
performance = new mw.mmv.logging.PerformanceLogger();
performance.newXHR = function () { return createFakeXHR( response ); };
performance.recordEntryDelayed = function ( recordType, _, recordUrl, recordRequest ) {
assert.strictEqual( recordType, type, 'type is recorded correctly' );
assert.strictEqual( recordUrl, url, 'url is recorded correctly' );
assert.strictEqual( recordRequest.response, response, 'response is recorded correctly' );
done();
};
return performance.record( type, url ).done( function ( recordResponse ) {
assert.strictEqual( recordResponse, response, 'response is passed to callback' );
} );
} );
QUnit.test( 'record() with old browser', function ( assert ) {
var type = 'foo',
url = 'http://example.com/',
done = assert.async(),
performance = new mw.mmv.logging.PerformanceLogger();
performance.newXHR = function () { throw new Error( 'XMLHttpRequest? What\'s that?' ); };
performance.record( type, url ).fail( function () {
assert.true( true, 'the promise is rejected when XMLHttpRequest is not supported' );
done();
} );
} );
QUnit.test( 'mw.mmv.logging.Api', function ( assert ) {
var api,
oldRecord = mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed,
oldAjax = mw.Api.prototype.ajax,
ajaxCalled = false,
fakeJqxhr = {};
mw.Api.prototype.ajax = function () {
ajaxCalled = true;
return $.Deferred().resolve( {}, fakeJqxhr );
};
mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed = function ( type, total, jqxhr ) {
assert.strictEqual( type, 'foo', 'type was passed correctly' );
assert.strictEqual( jqxhr, fakeJqxhr, 'jqXHR was passed correctly' );
};
api = new mw.mmv.logging.Api( 'foo' );
api.ajax();
assert.true( ajaxCalled, 'parent ajax() function was called' );
mw.mmv.logging.PerformanceLogger.prototype.recordJQueryEntryDelayed = oldRecord;
mw.Api.prototype.ajax = oldAjax;
} );
}() );

View file

@ -17,31 +17,6 @@
}
} ) );
QUnit.test( 'unview()', function ( assert ) {
var logger = { log: function () {} },
viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: function () {} }, {}, logger );
this.sandbox.stub( logger, 'log' );
viewLogger.unview();
assert.strictEqual( logger.log.called, false, 'action logger not called' );
viewLogger.setLastViewLogged( false );
viewLogger.unview();
assert.strictEqual( logger.log.called, false, 'action logger not called' );
viewLogger.setLastViewLogged( true );
viewLogger.unview();
assert.strictEqual( logger.log.calledOnce, true, 'action logger called' );
viewLogger.unview();
assert.strictEqual( logger.log.calledOnce, true, 'action logger not called again' );
} );
QUnit.test( 'focus and blur', function ( assert ) {
var $fakeWindow = $( '<div>' ),
viewLogger = new mw.mmv.logging.ViewLogger( { recordVirtualViewBeaconURI: function () {} }, $fakeWindow, { log: function () {} } );

View file

@ -31,7 +31,6 @@
imageProvider = new mw.mmv.provider.Image();
imageProvider.imagePreloadingSupported = function () { return false; };
imageProvider.performance.recordEntry = function () {};
return imageProvider.get( url ).then( function ( image ) {
assert.true( image instanceof HTMLImageElement,
@ -49,7 +48,6 @@
imageProvider = new mw.mmv.provider.Image();
imageProvider.imagePreloadingSupported = function () { return false; };
imageProvider.performance.recordEntry = function () {};
return QUnit.whenPromisesComplete(
imageProvider.get( url ).then( function ( image ) {
@ -71,70 +69,6 @@
);
} );
QUnit.test( 'Image load XHR progress funneling', function ( assert ) {
var i = 0,
imageProvider = new mw.mmv.provider.Image(),
oldPerformance = imageProvider.performance,
fakeURL = 'fakeURL',
response = 'response',
done1 = assert.async(),
done2 = assert.async();
imageProvider.performance.delay = 0;
imageProvider.imagePreloadingSupported = function () { return true; };
imageProvider.rawGet = function () { return $.Deferred().resolve(); };
imageProvider.performance.newXHR = function () {
return { readyState: 4,
response: response,
send: function () {
var self = this;
// The timeout is necessary because without it notify() happens before
// the imageProvider has time to chain its progress() to the returned deferred
setTimeout( function () {
self.onprogress( { lengthComputable: true, loaded: 10, total: 20 } );
self.onreadystatechange();
} );
},
open: function () {} };
};
imageProvider.performance.recordEntry = function ( type, total, url ) {
assert.strictEqual( type, 'image', 'Type matches' );
assert.strictEqual( url, fakeURL, 'URL matches' );
done1();
imageProvider.performance = oldPerformance;
return $.Deferred().resolve();
};
imageProvider.get( fakeURL )
.fail( function () {
assert.true( false, 'Image failed to (pretend to) load' );
done2();
} )
.then( function () {
assert.true( true, 'Image was pretend-loaded' );
done2();
} )
.progress( function ( response2, percent ) {
if ( i === 0 ) {
assert.strictEqual( percent, 50, 'Correctly propagated a 50% progress event' );
assert.strictEqual( response2, response2, 'Partial response propagated' );
} else if ( i === 1 ) {
assert.strictEqual( percent, 100, 'Correctly propagated a 100% progress event' );
assert.strictEqual( response2, response2, 'Partial response propagated' );
} else {
assert.true( false, 'Only 2 progress events should propagate' );
}
i++;
} );
} );
QUnit.test( 'Image load fail', function ( assert ) {
var imageProvider = new mw.mmv.provider.Image(),
oldMwLog = mw.log,
@ -142,7 +76,6 @@
mwLogCalled = false;
imageProvider.imagePreloadingSupported = function () { return false; };
imageProvider.performance.recordEntry = function () {};
mw.log = function () { mwLogCalled = true; };
imageProvider.get( 'doesntexist.png' ).fail( function () {
@ -186,15 +119,18 @@
} );
QUnit.test( 'imageQueryParameter', function ( assert ) {
var imageProvider = new mw.mmv.provider.Image( 'foo' );
var imageProvider = new mw.mmv.provider.Image( 'foo' ),
done = assert.async();
imageProvider.imagePreloadingSupported = function () { return false; };
imageProvider.rawGet = function () { return $.Deferred().resolve(); };
imageProvider.performance.recordEntry = function ( type, total, url ) {
imageProvider.rawGet = function ( url ) {
assert.strictEqual( url, 'http://www.wikipedia.org/?foo', 'Extra parameter added' );
return $.Deferred().resolve();
};
imageProvider.get( 'http://www.wikipedia.org/' );
imageProvider.get( 'http://www.wikipedia.org/' ).then( function () {
done();
} );
} );
}() );

View file

@ -192,41 +192,4 @@
scroller.unattach();
} );
QUnit.test( 'Metadata scroll logging', function ( assert ) {
var $qf = $( '#qunit-fixture' ),
$container = $( '<div>' ).css( 'height', 100 ).appendTo( $qf ),
$aboveFold = $( '<div>' ).css( 'height', 50 ).appendTo( $container ),
localStorage = mw.mmv.testHelpers.getFakeLocalStorage(),
scroller = new mw.mmv.ui.MetadataPanelScroller( $container, $aboveFold, localStorage ),
keydown = $.Event( 'keydown' );
stubScrollFunctions( this.sandbox, scroller );
this.sandbox.stub( mw.mmv.actionLogger, 'log' );
keydown.which = 38; // Up arrow
scroller.keydown( keydown );
assert.strictEqual( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-open' ), true, 'Opening keypress logged' );
mw.mmv.actionLogger.log.reset();
keydown.which = 38; // Up arrow
scroller.keydown( keydown );
assert.strictEqual( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-close' ), true, 'Closing keypress logged' );
mw.mmv.actionLogger.log.reset();
keydown.which = 40; // Down arrow
scroller.keydown( keydown );
assert.strictEqual( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-open' ), true, 'Opening keypress logged' );
mw.mmv.actionLogger.log.reset();
keydown.which = 40; // Down arrow
scroller.keydown( keydown );
assert.strictEqual( mw.mmv.actionLogger.log.calledWithExactly( 'metadata-close' ), true, 'Closing keypress logged' );
mw.mmv.actionLogger.log.reset();
} );
}() );