diff --git a/MultimediaViewer.php b/MultimediaViewer.php index ad4609258..2e668f9eb 100644 --- a/MultimediaViewer.php +++ b/MultimediaViewer.php @@ -45,6 +45,16 @@ if ( !isset( $wgMediaViewerAttributionLoggingSamplingFactor ) ) { $wgMediaViewerAttributionLoggingSamplingFactor = false; } +if ( !isset( $wgMediaViewerDimensionLoggingSamplingFactor ) ) { + /** + * 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. + * @var int|bool + */ + $wgMediaViewerDimensionLoggingSamplingFactor = false; +} + if ( !isset( $wgMediaViewerActionLoggingSamplingFactorMap ) ) { /** * 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. @@ -813,6 +823,7 @@ $wgResourceModules += array( 'mmv.provider', 'mmv.routing', 'mmv.logging.DurationLogger', + 'mmv.logging.DimensionLogger', 'jquery.fullscreen', 'jquery.hidpi', 'jquery.scrollTo', @@ -902,6 +913,19 @@ $wgResourceModules += array( ), ), + 'mmv.logging.DimensionLogger' => $wgMediaViewerResourceTemplate + array( + 'scripts' => array( + 'mmv/logging/mmv.logging.DimensionLogger.js', + ), + + 'dependencies' => array( + 'mmv.base', + 'mmv.logging.Logger', + 'oojs', + 'jquery.hidpi', + ), + ), + 'mmv.head' => $wgMediaViewerResourceTemplate + array( 'scripts' => array( 'mmv/mmv.head.js', @@ -936,10 +960,13 @@ $wgExtensionFunctions[] = function () { $wgEventLoggingSchemas[ 'MultimediaViewerNetworkPerformance' ] = 7917896; $wgEventLoggingSchemas[ 'MultimediaViewerDuration' ] = 8572641; $wgEventLoggingSchemas[ 'MultimediaViewerAttribution' ] = 9758179; + $wgEventLoggingSchemas[ 'MultimediaViewerDimensions' ] = 10014238; $wgResourceModules['mmv.logging.ActionLogger']['dependencies'][] = 'ext.eventLogging'; $wgResourceModules['mmv.logging.Performance']['dependencies'][] = 'ext.eventLogging'; $wgResourceModules['mmv.logging.DurationLogger']['dependencies'][] = 'ext.eventLogging'; + $wgResourceModules['mmv.logging.AttributionLogger']['dependencies'][] = 'ext.eventLogging'; + $wgResourceModules['mmv.logging.DimensionLogger']['dependencies'][] = 'ext.eventLogging'; } }; diff --git a/MultimediaViewerHooks.php b/MultimediaViewerHooks.php index 2b208c6a7..f23cde3e3 100644 --- a/MultimediaViewerHooks.php +++ b/MultimediaViewerHooks.php @@ -139,8 +139,9 @@ class MultimediaViewerHooks { * @return bool */ public static function resourceLoaderGetConfigVars( &$vars ) { - global $wgAPIPropModules, $wgMediaViewerActionLoggingSamplingFactorMap, $wgNetworkPerformanceSamplingFactor, $wgMediaViewerDurationLoggingSamplingFactor, - $wgMediaViewerAttributionLoggingSamplingFactor, $wgMediaViewerIsInBeta, $wgMediaViewerUseThumbnailGuessing; + global $wgMediaViewerActionLoggingSamplingFactorMap, $wgNetworkPerformanceSamplingFactor, + $wgMediaViewerDurationLoggingSamplingFactor, $wgMediaViewerAttributionLoggingSamplingFactor, + $wgMediaViewerDimensionLoggingSamplingFactor, $wgMediaViewerIsInBeta, $wgMediaViewerUseThumbnailGuessing; $vars['wgMultimediaViewer'] = array( 'infoLink' => self::$infoLink, 'discussionLink' => self::$discussionLink, @@ -150,6 +151,7 @@ class MultimediaViewerHooks { 'networkPerformanceSamplingFactor' => $wgNetworkPerformanceSamplingFactor, 'actionLoggingSamplingFactorMap' => $wgMediaViewerActionLoggingSamplingFactorMap, 'attributionSamplingFactor' => $wgMediaViewerAttributionLoggingSamplingFactor, + 'dimensionSamplingFactor' => $wgMediaViewerDimensionLoggingSamplingFactor, 'tooltipDelay' => 1000, ); $vars['wgMediaViewer'] = true; @@ -192,6 +194,8 @@ class MultimediaViewerHooks { 'tests/qunit/mmv/logging/mmv.logging.DurationLogger.test.js', 'tests/qunit/mmv/logging/mmv.logging.Performance.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/model/mmv.model.test.js', 'tests/qunit/mmv/model/mmv.model.IwTitle.test.js', 'tests/qunit/mmv/model/mmv.model.TaskQueue.test.js', diff --git a/resources/mmv/logging/mmv.logging.AttributionLogger.js b/resources/mmv/logging/mmv.logging.AttributionLogger.js index 4c0ac0dd5..9519a4819 100644 --- a/resources/mmv/logging/mmv.logging.AttributionLogger.js +++ b/resources/mmv/logging/mmv.logging.AttributionLogger.js @@ -63,5 +63,6 @@ this.log( data ); }; + mw.mmv.logging.AttributionLogger = AttributionLogger; mw.mmv.attributionLogger = new AttributionLogger(); }( mediaWiki, jQuery, OO ) ); \ No newline at end of file diff --git a/resources/mmv/logging/mmv.logging.DimensionLogger.js b/resources/mmv/logging/mmv.logging.DimensionLogger.js new file mode 100644 index 000000000..22a58d058 --- /dev/null +++ b/resources/mmv/logging/mmv.logging.DimensionLogger.js @@ -0,0 +1,76 @@ +/* + * 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 . + */ + +( function ( mw, $, oo ) { + 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 imagethat will be displayed + * @param {{width: Number, height: Number}} 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: $.devicePixelRatio(), + imgWidth: imageWidths.cssWidth, + imageAspectRatio: imageWidths.cssWidth / imageWidths.cssHeight, + thumbWidth: imageWidths.real, + context: context, + samplingFactor: this.samplingFactor + }; + mw.log( 'mw.mmw.logger.DimensionLogger', data ); + + this.log( data ); + }; + + mw.mmv.logging.DimensionLogger = DimensionLogger; + mw.mmv.dimensionLogger = new DimensionLogger(); +}( mediaWiki, jQuery, OO ) ); \ No newline at end of file diff --git a/resources/mmv/mmv.js b/resources/mmv/mmv.js index 37d7d090f..46d440c23 100644 --- a/resources/mmv/mmv.js +++ b/resources/mmv/mmv.js @@ -179,14 +179,18 @@ * @param {mw.mmv.LightboxInterface} ui lightbox that got resized */ MMVP.resize = function ( ui ) { - var viewer = this, - image = this.thumbs[ this.currentIndex ].image, - imageWidths; + var imageWidths, canvasDimensions, + viewer = this, + image = this.thumbs[ this.currentIndex ].image; this.preloadThumbnails(); if ( image ) { imageWidths = ui.canvas.getCurrentImageWidths(); + canvasDimensions = ui.canvas.getDimensions(); + + mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'resize' ); + this.fetchThumbnailForLightboxImage( image, imageWidths.real ).then( function( thumbnail, image ) { @@ -229,6 +233,7 @@ */ MMVP.loadImage = function ( image, initialImage ) { var imageWidths, + canvasDimensions, imagePromise, metadataPromise, start, @@ -259,10 +264,11 @@ // this.preloadFullscreenThumbnail( image ); // disabled - #474 imageWidths = this.ui.canvas.getCurrentImageWidths(); - + canvasDimensions = this.ui.canvas.getDimensions(); start = $.now(); + mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'show' ); imagePromise = this.fetchThumbnailForLightboxImage( image, imageWidths.real ); this.resetBlurredThumbnailStates(); @@ -614,16 +620,20 @@ this.thumbnailPreloadQueue = this.pushLightboxImagesIntoQueue( function( lightboxImage ) { return function() { + var imageWidths, canvasDimensions; + // viewer.ui.canvas.getLightboxImageWidths needs the viewer to be open // because it needs to read the size of visible elements if ( !viewer.isOpen ) { return; } - return viewer.fetchThumbnailForLightboxImage( - lightboxImage, - viewer.ui.canvas.getLightboxImageWidths( lightboxImage ).real - ); + imageWidths = viewer.ui.canvas.getLightboxImageWidths( lightboxImage ); + canvasDimensions = viewer.ui.canvas.getDimensions(); + + mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' ); + + return viewer.fetchThumbnailForLightboxImage( lightboxImage, imageWidths.real ); }; } ); @@ -634,10 +644,11 @@ * Preload the fullscreen size of the current image. */ MMVP.preloadFullscreenThumbnail = function( image ) { - this.fetchThumbnailForLightboxImage( - image, - this.ui.canvas.getLightboxImageWidthsForFullscreen( image ).real - ); + var imageWidths = this.ui.canvas.getLightboxImageWidthsForFullscreen( image), + canvasDimensions = this.ui.canvas.getDimensions( true ); + + mw.mmv.dimensionLogger.logDimensions( imageWidths, canvasDimensions, 'preload' ); + this.fetchThumbnailForLightboxImage( image, imageWidths.real ); }; /** @@ -716,7 +727,7 @@ thumbnailPromise = this.guessedThumbnailInfoProvider.get( fileTitle, sampleUrl, width, originalWidth, originalHeight ).then( null, function () { // catch rejection, use fallback - return viewer.thumbnailInfoProvider.get( fileTitle, width ); + return viewer.thumbnailInfoProvider.get( fileTitle, width ); } ); } else { thumbnailPromise = this.thumbnailInfoProvider.get( fileTitle, width ); diff --git a/resources/mmv/ui/mmv.ui.canvas.js b/resources/mmv/ui/mmv.ui.canvas.js index 321da9bd4..a01247527 100644 --- a/resources/mmv/ui/mmv.ui.canvas.js +++ b/resources/mmv/ui/mmv.ui.canvas.js @@ -339,13 +339,13 @@ }; /** - * Gets the widths for a given lightbox image. - * @param {mw.mmv.LightboxImage} image - * @returns {mw.mmv.model.ThumbnailWidth} + * Returns width and height of the canvas area (i.e. the space available for the image). + * @param {boolean} forFullscreen if true, return size in fullscreen mode; otherwise, return current size + * (which might still be fullscreen mode). + * @return {{width: Number, height: Number}} width and height in CSS pixels */ - C.getLightboxImageWidths = function ( image ) { - var thumb = image.thumbnail, - $window = $( window ), + C.getDimensions = function ( forFullscreen ) { + var $window = $( window ), $aboveFold = $( '.mw-mmv-above-fold' ), isFullscreened = !!$aboveFold.closest( '.jq-fullscreened' ).length, // Don't rely on this.$imageWrapper's sizing because it's fragile. @@ -354,8 +354,30 @@ availableWidth = $window.width(), availableHeight = $window.height() - ( isFullscreened ? 0 : $aboveFold.height() ); + if ( forFullscreen ) { + return { + width: screen.width, + height: screen.height + }; + } else { + return { + width: availableWidth, + height: availableHeight + }; + } + }; + + /** + * Gets the widths for a given lightbox image. + * @param {mw.mmv.LightboxImage} image + * @returns {mw.mmv.model.ThumbnailWidth} + */ + C.getLightboxImageWidths = function ( image ) { + var thumb = image.thumbnail, + canvasDimensions = this.getDimensions(); + return this.thumbnailWidthCalculator.calculateWidths( - availableWidth, availableHeight, thumb.width, thumb.height ); + canvasDimensions.width, canvasDimensions.height, thumb.width, thumb.height ); }; /** @@ -366,10 +388,11 @@ * @returns {mw.mmv.model.ThumbnailWidth} */ C.getLightboxImageWidthsForFullscreen = function ( image ) { - var thumb = image.thumbnail; + var thumb = image.thumbnail, + canvasDimensions = this.getDimensions( true ); return this.thumbnailWidthCalculator.calculateWidths( - screen.width, screen.height, thumb.width, thumb.height ); + canvasDimensions.width, canvasDimensions.height, thumb.width, thumb.height ); }; /** diff --git a/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js b/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js new file mode 100644 index 000000000..cf5107803 --- /dev/null +++ b/tests/qunit/mmv/logging/mmv.logging.AttributionLogger.test.js @@ -0,0 +1,22 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.AttributionLogger', QUnit.newMwEnvironment() ); + + QUnit.test( 'log()', 2, function ( assert ) { + var fakeEventLog = { logEvent : this.sandbox.stub() }, + logger = new mw.mmv.logging.AttributionLogger(), + image = { author: 'foo', source: 'bar', license: {} }, + emptyImage = {}; + + this.sandbox.stub( logger, 'loadDependencies' ).returns( $.Deferred().resolve() ); + this.sandbox.stub( mw, 'log' ); + + logger.samplingFactor = 1; + logger.setEventLog( fakeEventLog ); + + logger.logAttribution( image ); + assert.ok( true, 'logDimensions() did not throw errors' ); + + logger.logAttribution( emptyImage ); + assert.ok( true, 'logDimensions() did not throw errors for empty image' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js b/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js new file mode 100644 index 000000000..365b0750e --- /dev/null +++ b/tests/qunit/mmv/logging/mmv.logging.DimensionLogger.test.js @@ -0,0 +1,17 @@ +( function ( mw, $ ) { + QUnit.module( 'mmv.logging.DimensionLogger', QUnit.newMwEnvironment() ); + + QUnit.test( 'log()', 1, function ( assert ) { + var fakeEventLog = { logEvent : this.sandbox.stub() }, + 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.ok( true, 'logDimensions() did not throw errors' ); + } ); +}( mediaWiki, jQuery ) ); diff --git a/tests/qunit/mmv/mmv.test.js b/tests/qunit/mmv/mmv.test.js index 105f7d171..14eb36f51 100644 --- a/tests/qunit/mmv/mmv.test.js +++ b/tests/qunit/mmv/mmv.test.js @@ -116,7 +116,9 @@ canvas: { set : $.noop, unblurWithAnimation: $.noop, unblur: $.noop, - getCurrentImageWidths: function () { return { real : 0 }; } }, + getCurrentImageWidths: function () { return { real : 0 }; }, + getDimensions: function () { return {}; } + }, panel: { setImageInfo: $.noop, scroller: { @@ -172,7 +174,9 @@ canvas : { set : $.noop, unblurWithAnimation: $.noop, unblur: $.noop, - getCurrentImageWidths : function () { return { real : 0 }; } }, + getCurrentImageWidths : function () { return { real : 0 }; }, + getDimensions: function () { return {}; } + }, panel : { setImageInfo : $.noop, scroller: { @@ -364,8 +368,11 @@ viewer.ui = { setFileReuseData: $.noop, setupForLoad : $.noop, - canvas : { set : $.noop, - getCurrentImageWidths : function () { return { real : 0 }; } }, + canvas : { + set : $.noop, + getCurrentImageWidths : function () { return { real : 0 }; }, + getDimensions: function () { return {}; } + }, panel : { setImageInfo : this.sandbox.stub(), scroller: {