mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/Popups
synced 2024-11-23 15:16:50 +00:00
Remove Popups instrumentation
Bug: T267211 Change-Id: I640ab367cd235ab8da7dd70dbef7ae9076712e84
This commit is contained in:
parent
1de4252bdb
commit
8f5000f346
|
@ -14,8 +14,8 @@
|
|||
|
||||
"//": "Set the coverage percentage by category thresholds.",
|
||||
"statements": 85,
|
||||
"branches": 74,
|
||||
"functions": 85,
|
||||
"branches": 72,
|
||||
"functions": 84,
|
||||
"lines": 90,
|
||||
|
||||
"//": "Fail if the coverage is below threshold.",
|
||||
|
|
|
@ -5,7 +5,6 @@ $cfg['directory_list'] = array_merge(
|
|||
$cfg['directory_list'],
|
||||
[
|
||||
'../../extensions/Gadgets',
|
||||
'../../extensions/EventLogging',
|
||||
'../../extensions/BetaFeatures',
|
||||
]
|
||||
);
|
||||
|
@ -14,7 +13,6 @@ $cfg['exclude_analysis_directory_list'] = array_merge(
|
|||
$cfg['exclude_analysis_directory_list'],
|
||||
[
|
||||
'../../extensions/Gadgets',
|
||||
'../../extensions/EventLogging',
|
||||
'../../extensions/BetaFeatures',
|
||||
]
|
||||
);
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
"BeforePageDisplay": "Popups\\PopupsHooks::onBeforePageDisplay",
|
||||
"ResourceLoaderGetConfigVars": "Popups\\PopupsHooks::onResourceLoaderGetConfigVars",
|
||||
"GetPreferences": "Popups\\PopupsHooks::onGetPreferences",
|
||||
"PreferencesFormPreSave": "Popups\\UserPreferencesChangeHandler::onPreferencesFormPreSave",
|
||||
"UserGetDefaultOptions": "Popups\\PopupsHooks::onUserGetDefaultOptions",
|
||||
"MakeGlobalVariablesScript": "Popups\\PopupsHooks::onMakeGlobalVariablesScript",
|
||||
"LocalUserCreated": "Popups\\PopupsHooks::onLocalUserCreated",
|
||||
|
@ -36,9 +35,8 @@
|
|||
"attributes": {
|
||||
"EventLogging": {
|
||||
"Schemas": {
|
||||
"Popups": 18904225,
|
||||
"ReferencePreviewsPopups": "/analytics/legacy/referencepreviewspopups/1.1.0",
|
||||
"VirtualPageView": "/analytics/legacy/virtualpageview/1.0.0"
|
||||
"ReferencePreviewsPopups": "/analytics/legacy/referencepreviewspopups/1.0.0",
|
||||
"VirtualPageView": 17780078
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -83,10 +81,6 @@
|
|||
"description": "Make Reference Previews a Beta feature.",
|
||||
"value": true
|
||||
},
|
||||
"PopupsEventLogging": {
|
||||
"description": "Whether we should log events. Note if this is enabled without using that variable events will be logged for all users without any sampling! Be careful!",
|
||||
"value": false
|
||||
},
|
||||
"PopupsStatsvSamplingRate": {
|
||||
"description": "Sampling rate for logging performance data to statsv.",
|
||||
"value": 0
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
namespace Popups\EventLogging;
|
||||
|
||||
interface EventLogger {
|
||||
|
||||
/**
|
||||
* Page Previews Event logging schema name
|
||||
*/
|
||||
public const PREVIEWS_SCHEMA_NAME = 'Popups';
|
||||
|
||||
/**
|
||||
* Log event
|
||||
*
|
||||
* @param array $event An associative array containing event data
|
||||
*/
|
||||
public function log( array $event );
|
||||
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
namespace Popups\EventLogging;
|
||||
|
||||
use ExtensionRegistry;
|
||||
|
||||
class EventLoggerFactory {
|
||||
|
||||
/**
|
||||
* @var ExtensionRegistry
|
||||
*/
|
||||
private $registry;
|
||||
|
||||
/**
|
||||
* @param ExtensionRegistry $registry MediaWiki extension registry
|
||||
*/
|
||||
public function __construct( ExtensionRegistry $registry ) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the EventLogger instance
|
||||
*
|
||||
* @return EventLogger
|
||||
*/
|
||||
public function get() {
|
||||
if ( $this->registry->isLoaded( 'EventLogging' ) ) {
|
||||
return new MWEventLogger( $this->registry );
|
||||
}
|
||||
return new NullLogger();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
namespace Popups\EventLogging;
|
||||
|
||||
use ExtensionRegistry;
|
||||
|
||||
class MWEventLogger implements EventLogger {
|
||||
|
||||
/**
|
||||
* @var ExtensionRegistry
|
||||
*/
|
||||
private $registry;
|
||||
|
||||
/**
|
||||
* @param ExtensionRegistry $registry MediaWiki extension registry
|
||||
*/
|
||||
public function __construct( ExtensionRegistry $registry ) {
|
||||
$this->registry = $registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log event
|
||||
*
|
||||
* @param array $event An associative array containing event data
|
||||
*/
|
||||
public function log( array $event ) {
|
||||
$eventLoggingSchemas = $this->registry->getAttribute( 'EventLoggingSchemas' );
|
||||
|
||||
\EventLogging::logEvent(
|
||||
self::PREVIEWS_SCHEMA_NAME,
|
||||
$eventLoggingSchemas[ self::PREVIEWS_SCHEMA_NAME ],
|
||||
$event
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
namespace Popups\EventLogging;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class NullLogger implements EventLogger {
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function log( array $event ) {
|
||||
// just do nothing
|
||||
}
|
||||
|
||||
}
|
|
@ -25,7 +25,6 @@ use Config;
|
|||
use ExtensionRegistry;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\User\UserOptionsLookup;
|
||||
use Popups\EventLogging\EventLogger;
|
||||
use Title;
|
||||
|
||||
/**
|
||||
|
@ -96,11 +95,6 @@ class PopupsContext {
|
|||
*/
|
||||
private $gadgetsIntegration;
|
||||
|
||||
/**
|
||||
* @var EventLogger
|
||||
*/
|
||||
private $eventLogger;
|
||||
|
||||
/**
|
||||
* @var UserOptionsLookup
|
||||
*/
|
||||
|
@ -110,7 +104,6 @@ class PopupsContext {
|
|||
* @param Config $config Mediawiki configuration
|
||||
* @param ExtensionRegistry $extensionRegistry MediaWiki extension registry
|
||||
* @param PopupsGadgetsIntegration $gadgetsIntegration Gadgets integration helper
|
||||
* @param EventLogger $eventLogger A logger capable of logging EventLogging
|
||||
* @param UserOptionsLookup $userOptionsLookup
|
||||
* events
|
||||
*/
|
||||
|
@ -118,12 +111,10 @@ class PopupsContext {
|
|||
Config $config,
|
||||
ExtensionRegistry $extensionRegistry,
|
||||
PopupsGadgetsIntegration $gadgetsIntegration,
|
||||
EventLogger $eventLogger,
|
||||
UserOptionsLookup $userOptionsLookup
|
||||
) {
|
||||
$this->extensionRegistry = $extensionRegistry;
|
||||
$this->gadgetsIntegration = $gadgetsIntegration;
|
||||
$this->eventLogger = $eventLogger;
|
||||
$this->userOptionsLookup = $userOptionsLookup;
|
||||
|
||||
$this->config = $config;
|
||||
|
@ -276,25 +267,4 @@ class PopupsContext {
|
|||
public function getLogger() {
|
||||
return MediaWikiServices::getInstance()->getService( 'Popups.Logger' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log disabled event
|
||||
*/
|
||||
public function logUserDisabledPagePreviewsEvent() {
|
||||
// @see https://phabricator.wikimedia.org/T167365
|
||||
$this->eventLogger->log( [
|
||||
'pageTitleSource' => 'Special:Preferences',
|
||||
'namespaceIdSource' => NS_SPECIAL,
|
||||
'pageIdSource' => -1,
|
||||
'hovercardsSuppressedByGadget' => false,
|
||||
'pageToken' => wfRandomString(),
|
||||
// we don't have access to mw.user.sessionId()
|
||||
'sessionToken' => wfRandomString(),
|
||||
'action' => 'disabled',
|
||||
'isAnon' => false,
|
||||
'popupEnabled' => false,
|
||||
'previewCountBucket' => 'unknown'
|
||||
] );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -178,7 +178,6 @@ class PopupsHooks {
|
|||
|
||||
$vars['wgPopupsVirtualPageViews'] = $config->get( 'PopupsVirtualPageViews' );
|
||||
$vars['wgPopupsGateway'] = $config->get( 'PopupsGateway' );
|
||||
$vars['wgPopupsEventLogging'] = $config->get( 'PopupsEventLogging' );
|
||||
$vars['wgPopupsRestGatewayEndpoint'] = $config->get( 'PopupsRestGatewayEndpoint' );
|
||||
$vars['wgPopupsStatsvSamplingRate'] = $config->get( 'PopupsStatsvSamplingRate' );
|
||||
$vars['wgPopupsTextExtractsIntroOnly'] = $config->get( 'PopupsTextExtractsIntroOnly' );
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
|
||||
use MediaWiki\Logger\LoggerFactory;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use Popups\EventLogging\EventLoggerFactory;
|
||||
use Popups\PopupsContext;
|
||||
use Popups\PopupsGadgetsIntegration;
|
||||
use Popups\UserPreferencesChangeHandler;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore
|
||||
|
@ -21,18 +19,6 @@ return [
|
|||
ExtensionRegistry::getInstance()
|
||||
);
|
||||
},
|
||||
'Popups.EventLogger' => static function ( MediaWikiServices $services ) {
|
||||
$factory = new EventLoggerFactory(
|
||||
ExtensionRegistry::getInstance()
|
||||
);
|
||||
return $factory->get();
|
||||
},
|
||||
'Popups.UserPreferencesChangeHandler' => static function ( MediaWikiServices $services ) {
|
||||
return new UserPreferencesChangeHandler(
|
||||
$services->getService( 'Popups.Context' ),
|
||||
$services->getUserOptionsLookup()
|
||||
);
|
||||
},
|
||||
'Popups.Logger' => static function ( MediaWikiServices $services ) {
|
||||
return LoggerFactory::getInstance( PopupsContext::LOGGER_CHANNEL );
|
||||
},
|
||||
|
@ -41,7 +27,6 @@ return [
|
|||
$services->getService( 'Popups.Config' ),
|
||||
ExtensionRegistry::getInstance(),
|
||||
$services->getService( 'Popups.GadgetsIntegration' ),
|
||||
$services->getService( 'Popups.EventLogger' ),
|
||||
$services->getUserOptionsLookup()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
namespace Popups;
|
||||
|
||||
use HTMLForm;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\User\UserOptionsLookup;
|
||||
use User;
|
||||
|
||||
/**
|
||||
* User Preferences save change listener
|
||||
*
|
||||
* @package Popups
|
||||
*/
|
||||
class UserPreferencesChangeHandler {
|
||||
|
||||
/**
|
||||
* @var PopupsContext
|
||||
*/
|
||||
private $popupsContext;
|
||||
|
||||
/**
|
||||
* @var UserOptionsLookup
|
||||
*/
|
||||
private $userOptionsLookup;
|
||||
|
||||
/**
|
||||
* @param PopupsContext $context
|
||||
* @param UserOptionsLookup $userOptionsLookup
|
||||
*/
|
||||
public function __construct(
|
||||
PopupsContext $context,
|
||||
UserOptionsLookup $userOptionsLookup
|
||||
) {
|
||||
$this->popupsContext = $context;
|
||||
$this->userOptionsLookup = $userOptionsLookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook executed on Preferences Form Save, when user disables Page Previews call PopupsContext
|
||||
* to log `disabled` event.
|
||||
*
|
||||
* @param User $user Logged-in user
|
||||
* @param array $oldUserOptions Old user options array
|
||||
*/
|
||||
public function doPreferencesFormPreSave( User $user, array $oldUserOptions ) {
|
||||
if ( !array_key_exists( PopupsContext::PREVIEWS_OPTIN_PREFERENCE_NAME, $oldUserOptions ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldSetting = (bool)$oldUserOptions[PopupsContext::PREVIEWS_OPTIN_PREFERENCE_NAME];
|
||||
$newSetting = $this->userOptionsLookup->getBoolOption(
|
||||
$user,
|
||||
PopupsContext::PREVIEWS_OPTIN_PREFERENCE_NAME
|
||||
);
|
||||
|
||||
if ( $oldSetting && !$newSetting ) {
|
||||
$this->popupsContext->logUserDisabledPagePreviewsEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserPreferencesChangeHandler
|
||||
*/
|
||||
private static function newFromGlobalState() {
|
||||
return MediaWikiServices::getInstance()->getService( 'Popups.UserPreferencesChangeHandler' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $formData Form data submitted by user
|
||||
* @param HTMLForm $form A preferences form
|
||||
* @param User $user Logged-in user
|
||||
* @param bool &$result Variable defining is form save successful
|
||||
* @param array $oldUserOptions Old user options array
|
||||
*/
|
||||
public static function onPreferencesFormPreSave(
|
||||
array $formData,
|
||||
HTMLForm $form,
|
||||
User $user,
|
||||
&$result,
|
||||
$oldUserOptions
|
||||
) {
|
||||
self::newFromGlobalState()->doPreferencesFormPreSave( $user, $oldUserOptions );
|
||||
}
|
||||
|
||||
}
|
|
@ -68,7 +68,7 @@
|
|||
"bundlesize": [
|
||||
{
|
||||
"path": "resources/dist/index.js",
|
||||
"maxSize": "13.9kB"
|
||||
"maxSize": "12.9kB"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
BIN
resources/dist/index.js
vendored
BIN
resources/dist/index.js
vendored
Binary file not shown.
BIN
resources/dist/index.js.map.json
vendored
BIN
resources/dist/index.js.map.json
vendored
Binary file not shown.
|
@ -30,6 +30,5 @@ export default {
|
|||
SETTINGS_SHOW: 'SETTINGS_SHOW',
|
||||
SETTINGS_HIDE: 'SETTINGS_HIDE',
|
||||
SETTINGS_CHANGE: 'SETTINGS_CHANGE',
|
||||
EVENT_LOGGED: 'EVENT_LOGGED',
|
||||
STATSV_LOGGED: 'STATSV_LOGGED'
|
||||
};
|
||||
|
|
|
@ -73,8 +73,7 @@ export function boot(
|
|||
config,
|
||||
url
|
||||
) {
|
||||
const editCount = config.get( 'wgUserEditCount' ),
|
||||
previewCount = userSettings.getPreviewCount();
|
||||
const editCount = config.get( 'wgUserEditCount' );
|
||||
|
||||
return {
|
||||
type: types.BOOT,
|
||||
|
@ -91,8 +90,7 @@ export function boot(
|
|||
},
|
||||
user: {
|
||||
isAnon: user.isAnon(),
|
||||
editCount,
|
||||
previewCount
|
||||
editCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -436,20 +434,6 @@ export function saveSettings( enabled ) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the queued event being logged `changeListeners/eventLogging.js`
|
||||
* change listener.
|
||||
*
|
||||
* @param {Object} event
|
||||
* @return {Object}
|
||||
*/
|
||||
export function eventLogged( event ) {
|
||||
return {
|
||||
type: types.EVENT_LOGGED,
|
||||
event
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the queued statsv event being logged.
|
||||
* See `mw.popups.changeListeners.statsv` change listener.
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* @module changeListeners/eventLogging
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an instance of the event logging change listener.
|
||||
*
|
||||
* When an event is enqueued it'll be logged using the schema. Since it's the
|
||||
* responsibility of Event Logging (and the UA) to deliver logged events,
|
||||
* `EVENT_LOGGED` is immediately dispatched rather than waiting for some
|
||||
* indicator of completion.
|
||||
*
|
||||
* @param {Object} boundActions
|
||||
* @param {EventTracker} eventLoggingTracker
|
||||
* @param {Function} getCurrentTimestamp
|
||||
* @return {ext.popups.ChangeListener}
|
||||
*/
|
||||
export default function eventLogging(
|
||||
boundActions, eventLoggingTracker, getCurrentTimestamp
|
||||
) {
|
||||
return ( oldState, newState ) => {
|
||||
const eventLoggingObj = newState.eventLogging;
|
||||
let event = eventLoggingObj.event;
|
||||
|
||||
if ( !event ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Per https://meta.wikimedia.org/wiki/Schema:Popups, the timestamp
|
||||
// property should be the time at which the event is logged and not the
|
||||
// time at which the interaction started.
|
||||
//
|
||||
// Rightly or wrongly, it's left as an exercise for the analyst to
|
||||
// calculate the time at which the interaction started as part of their
|
||||
// analyses, e.g. https://phabricator.wikimedia.org/T186016#4002923.
|
||||
event = $.extend( true, {}, eventLoggingObj.baseData, event, {
|
||||
timestamp: getCurrentTimestamp()
|
||||
} );
|
||||
|
||||
eventLoggingTracker( 'event.Popups', event );
|
||||
// Dispatch the eventLogged action so that the state tree can be
|
||||
// cleared/updated.
|
||||
boundActions.eventLogged( event );
|
||||
};
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import footerLink from './footerLink';
|
||||
import eventLogging from './eventLogging';
|
||||
import linkTitle from './linkTitle';
|
||||
import pageviews from './pageviews';
|
||||
import render from './render';
|
||||
|
@ -9,7 +8,6 @@ import syncUserSettings from './syncUserSettings';
|
|||
|
||||
export default {
|
||||
footerLink,
|
||||
eventLogging,
|
||||
linkTitle,
|
||||
pageviews,
|
||||
render,
|
||||
|
|
64
src/index.js
64
src/index.js
|
@ -16,7 +16,6 @@ import { fromElement as titleFromElement } from './title';
|
|||
import { init as rendererInit } from './ui/renderer';
|
||||
import createExperiments from './experiments';
|
||||
import { isEnabled as isStatsvEnabled } from './instrumentation/statsv';
|
||||
import { isEnabled as isEventLoggingEnabled } from './instrumentation/eventLogging';
|
||||
import changeListeners from './changeListeners';
|
||||
import * as actions from './actions';
|
||||
import reducers from './reducers';
|
||||
|
@ -79,49 +78,6 @@ function getPageviewTracker( config ) {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate analytics event tracker for logging EventLogging events
|
||||
* via [the "EventLogging subscriber" analytics event protocol][0].
|
||||
*
|
||||
* If logging EventLogging events is enabled for the duration of the user's
|
||||
* session, then the appriopriate function is `mw.track`; otherwise it's
|
||||
* `() => {}`.
|
||||
*
|
||||
* [0]: https://github.com/wikimedia/mediawiki-extensions-EventLogging/blob/d1409759/modules/ext.eventLogging.subscriber.js
|
||||
*
|
||||
* @param {Object} user
|
||||
* @param {Object} config
|
||||
* @param {Window} window
|
||||
* @return {EventTracker}
|
||||
*/
|
||||
function getEventLoggingTracker( user, config, window ) {
|
||||
return isEventLoggingEnabled(
|
||||
user,
|
||||
config,
|
||||
window
|
||||
) ? mw.track : () => {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns timestamp since the beginning of the current document's origin
|
||||
* as reported by `window.performance.now()`. See
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin
|
||||
* for a detailed explanation of the time origin.
|
||||
*
|
||||
* The value returned by this function is used for [the `timestamp` property
|
||||
* of the Schema:Popups events sent by the EventLogging
|
||||
* instrumentation](./src/changeListeners/eventLogging.js).
|
||||
*
|
||||
* @return {number|null}
|
||||
*/
|
||||
function getCurrentTimestamp() {
|
||||
if ( window.performance && window.performance.now ) {
|
||||
// return an integer; see T182000
|
||||
return Math.round( window.performance.now() );
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes the registered change listeners to the
|
||||
* [store](http://redux.js.org/docs/api/Store.html#store).
|
||||
|
@ -132,14 +88,12 @@ function getCurrentTimestamp() {
|
|||
* @param {Function} settingsDialog
|
||||
* @param {PreviewBehavior} previewBehavior
|
||||
* @param {EventTracker} statsvTracker
|
||||
* @param {EventTracker} eventLoggingTracker
|
||||
* @param {EventTracker} pageviewTracker
|
||||
* @param {Function} callbackCurrentTimestamp
|
||||
* @return {void}
|
||||
*/
|
||||
function registerChangeListeners(
|
||||
store, registerActions, userSettings, settingsDialog, previewBehavior,
|
||||
statsvTracker, eventLoggingTracker, pageviewTracker, callbackCurrentTimestamp
|
||||
statsvTracker, pageviewTracker
|
||||
) {
|
||||
registerChangeListener( store, changeListeners.footerLink( registerActions ) );
|
||||
registerChangeListener( store, changeListeners.linkTitle() );
|
||||
|
@ -150,11 +104,6 @@ function registerChangeListeners(
|
|||
store, changeListeners.syncUserSettings( userSettings ) );
|
||||
registerChangeListener(
|
||||
store, changeListeners.settings( registerActions, settingsDialog ) );
|
||||
registerChangeListener(
|
||||
store,
|
||||
changeListeners.eventLogging(
|
||||
registerActions, eventLoggingTracker, callbackCurrentTimestamp
|
||||
) );
|
||||
registerChangeListener( store,
|
||||
changeListeners.pageviews( registerActions, pageviewTracker )
|
||||
);
|
||||
|
@ -186,11 +135,6 @@ function registerChangeListeners(
|
|||
experiments = createExperiments( mw.experiments ),
|
||||
statsvTracker = getStatsvTracker( mw.user, mw.config, experiments ),
|
||||
pageviewTracker = getPageviewTracker( mw.config ),
|
||||
eventLoggingTracker = getEventLoggingTracker(
|
||||
mw.user,
|
||||
mw.config,
|
||||
window
|
||||
),
|
||||
initiallyEnabled = {
|
||||
[ previewTypes.TYPE_PAGE ]:
|
||||
createIsPagePreviewsEnabled( mw.user, userSettings, mw.config ),
|
||||
|
@ -216,9 +160,7 @@ function registerChangeListeners(
|
|||
|
||||
registerChangeListeners(
|
||||
store, boundActions, userSettings, settingsDialog,
|
||||
previewBehavior, statsvTracker, eventLoggingTracker,
|
||||
pageviewTracker,
|
||||
getCurrentTimestamp
|
||||
previewBehavior, statsvTracker, pageviewTracker
|
||||
);
|
||||
|
||||
boundActions.boot(
|
||||
|
@ -226,8 +168,6 @@ function registerChangeListeners(
|
|||
mw.user,
|
||||
userSettings,
|
||||
mw.config,
|
||||
// Probably a false positive. MediaWiki 1.36 dropped Firefox 4 support anyway.
|
||||
// eslint-disable-next-line compat/compat
|
||||
window.location.href
|
||||
);
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* @module instrumentation/eventLogging
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets whether EventLogging logging is enabled for the duration of the user's
|
||||
* session.
|
||||
* If wgPopupsEventLogging is false this will return false unless debug=true has
|
||||
* been enabled.
|
||||
* However, if the UA doesn't support [the Beacon API][1], then bucketing is
|
||||
* disabled.
|
||||
*
|
||||
* [1]: https://w3c.github.io/beacon/
|
||||
*
|
||||
* @param {mw.user} user The `mw.user` singleton instance
|
||||
* @param {mw.Map} config The `mw.config` singleton instance
|
||||
* @param {Window} window
|
||||
* @return {boolean}
|
||||
*/
|
||||
export function isEnabled( user, config, window ) {
|
||||
// if debug mode is on, always enable event logging. @see T168847
|
||||
if ( config.get( 'debug' ) === true ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return config.get( 'wgPopupsEventLogging' ) &&
|
||||
window.navigator &&
|
||||
// eslint-disable-next-line compat/compat
|
||||
typeof window.navigator.sendBeacon === 'function';
|
||||
}
|
|
@ -1,314 +0,0 @@
|
|||
/**
|
||||
* @module reducers/eventLogging
|
||||
*/
|
||||
|
||||
import actionTypes from '../actionTypes';
|
||||
import nextState from './nextState';
|
||||
import * as counts from '../counts';
|
||||
import { previewTypes } from '../preview/model';
|
||||
|
||||
/**
|
||||
* Initialize the data that's shared between all events.
|
||||
*
|
||||
* @param {Object} bootAction
|
||||
* @return {Object}
|
||||
*/
|
||||
function getBaseData( bootAction ) {
|
||||
const result = {
|
||||
pageTitleSource: bootAction.page.title,
|
||||
namespaceIdSource: bootAction.page.namespaceId,
|
||||
pageIdSource: bootAction.page.id,
|
||||
isAnon: bootAction.user.isAnon,
|
||||
popupEnabled: bootAction.initiallyEnabled[ previewTypes.TYPE_PAGE ],
|
||||
pageToken: bootAction.pageToken,
|
||||
sessionToken: bootAction.sessionToken,
|
||||
previewCountBucket: counts.getPreviewCountBucket(
|
||||
bootAction.user.previewCount
|
||||
),
|
||||
hovercardsSuppressedByGadget: bootAction.isNavPopupsEnabled
|
||||
};
|
||||
|
||||
if ( !bootAction.user.isAnon ) {
|
||||
result.editCountBucket =
|
||||
counts.getEditCountBucket( bootAction.user.editCount );
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes data specific to the action and adds the following properties:
|
||||
*
|
||||
* * `linkInteractionToken`;
|
||||
* * `pageTitleHover` and `namespaceIdHover`; and
|
||||
* * `previewType` and `perceivedWait`, if a preview has been shown.
|
||||
*
|
||||
* The linkInteractionToken is renewed on each new preview dwelling unlike the pageToken which has a
|
||||
* lifespan tied to the pageview. It is erroneous to use the same linkInteractionToken across
|
||||
* multiple previews even if the previews are for the same link.
|
||||
*
|
||||
* @param {Object} interaction
|
||||
* @param {Object} actionData Data specific to the action, e.g. see
|
||||
* {@link module:reducers/eventLogging~createClosingEvent `createClosingEvent`}
|
||||
* @return {Object}
|
||||
*/
|
||||
function createEvent( interaction, actionData ) {
|
||||
actionData.linkInteractionToken = interaction.token;
|
||||
actionData.pageTitleHover = interaction.title;
|
||||
actionData.namespaceIdHover = interaction.namespaceId;
|
||||
|
||||
// Has the preview been shown?
|
||||
if ( interaction.timeToPreviewShow !== undefined ) {
|
||||
actionData.previewType = interaction.previewType;
|
||||
actionData.perceivedWait = interaction.timeToPreviewShow;
|
||||
}
|
||||
|
||||
return actionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event that, when mixed into the base data (see
|
||||
* {@link module:reducers/eventLogging~getBaseData `getBaseData`}), represents
|
||||
* the user abandoning a link or preview.
|
||||
*
|
||||
* Since the event should be logged when the user has either abandoned a link or
|
||||
* dwelled on a different link, we refer to these events as "closing" events as
|
||||
* the link interaction has finished and a new one will be created later.
|
||||
*
|
||||
* If the link interaction is finalized, then no closing event is created.
|
||||
*
|
||||
* @param {Object} interaction
|
||||
* @return {Object|undefined}
|
||||
*/
|
||||
function createClosingEvent( interaction ) {
|
||||
const actionData = {
|
||||
totalInteractionTime:
|
||||
Math.round( interaction.finished - interaction.started )
|
||||
};
|
||||
|
||||
if ( interaction.finalized ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Has the preview been shown? If so, then, in the context of the
|
||||
// instrumentation, then the preview has been dismissed by the user
|
||||
// rather than the user has abandoned the link.
|
||||
actionData.action =
|
||||
interaction.timeToPreviewShow ? 'dismissed' : 'dwelledButAbandoned';
|
||||
|
||||
return createEvent( interaction, actionData );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reducer for actions that may result in an event being logged with [the
|
||||
* Popups schema][0] via EventLogging.
|
||||
*
|
||||
* The complexity of this reducer reflects the complexity of [the schema][0],
|
||||
* which is compounded by the introduction of two delays introduced by the
|
||||
* system to provide reasonable performance and a consistent UX.
|
||||
*
|
||||
* The reducer must:
|
||||
*
|
||||
* * Accumulate the state required to log events. This state is
|
||||
* referred to as "the interaction state" or "the interaction";
|
||||
* * Enforce the invariant that one event is logged per interaction;
|
||||
* * Defend against delayed actions being dispatched; and, as a direct
|
||||
* consequence
|
||||
* * Handle transitioning from one interaction to another at the same time.
|
||||
*
|
||||
* Furthermore, we distinguish between "finalizing" and "closing" the current
|
||||
* interaction state. Since only one event should be logged per link
|
||||
* interaction, we say that the interaction state is *finalized* when an event
|
||||
* has been logged and is *closed* when a new interaction should be created.
|
||||
* In practice, the interaction state is only finalized when the user clicks a
|
||||
* link or a preview.
|
||||
*
|
||||
* [0]: https://meta.wikimedia.org/wiki/Schema:Popups
|
||||
*
|
||||
* @param {Object|undefined} state
|
||||
* @param {Object} action
|
||||
* @return {Object} The state resulting from reducing the action with the
|
||||
* current state
|
||||
*/
|
||||
export default function eventLogging( state, action ) {
|
||||
let nextCount, newState;
|
||||
const actionTypesWithTokens = [
|
||||
actionTypes.FETCH_COMPLETE,
|
||||
actionTypes.ABANDON_END,
|
||||
actionTypes.PREVIEW_SHOW
|
||||
];
|
||||
|
||||
if ( state === undefined ) {
|
||||
state = {
|
||||
previewCount: undefined,
|
||||
baseData: {},
|
||||
interaction: undefined,
|
||||
event: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// Was the action delayed? Then it requires a token to be reduced. Enforce
|
||||
// this here to avoid repetition and reduce nesting below.
|
||||
if (
|
||||
actionTypesWithTokens.indexOf( action.type ) !== -1 &&
|
||||
( !state.interaction || action.token !== state.interaction.token )
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// If there is no interaction ongoing, ignore all actions except for:
|
||||
// * Application initialization
|
||||
// * New link dwells (which start a new interaction)
|
||||
// * Clearing queued events
|
||||
//
|
||||
// For example, after ctrl+clicking a link or preview, any other actions
|
||||
// until the new interaction should be ignored.
|
||||
if (
|
||||
!state.interaction &&
|
||||
action.type !== actionTypes.BOOT &&
|
||||
action.type !== actionTypes.LINK_DWELL &&
|
||||
action.type !== actionTypes.EVENT_LOGGED &&
|
||||
action.type !== actionTypes.SETTINGS_CHANGE
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
switch ( action.type ) {
|
||||
case actionTypes.BOOT:
|
||||
return nextState( state, {
|
||||
previewCount: action.user.previewCount,
|
||||
baseData: getBaseData( action ),
|
||||
event: {
|
||||
action: 'pageLoaded'
|
||||
}
|
||||
} );
|
||||
|
||||
case actionTypes.EVENT_LOGGED:
|
||||
newState = nextState( state, {
|
||||
event: undefined
|
||||
} );
|
||||
|
||||
// If an event was logged with an interaction token, and it is still
|
||||
// the current interaction, finish the interaction since logging is
|
||||
// the exit point of the state machine and an interaction should never
|
||||
// be logged twice.
|
||||
if (
|
||||
action.event.linkInteractionToken &&
|
||||
state.interaction &&
|
||||
( action.event.linkInteractionToken === state.interaction.token )
|
||||
) {
|
||||
newState.interaction = undefined;
|
||||
}
|
||||
return newState;
|
||||
|
||||
case actionTypes.FETCH_COMPLETE:
|
||||
return nextState( state, {
|
||||
interaction: {
|
||||
previewType: action.result.type
|
||||
}
|
||||
} );
|
||||
|
||||
case actionTypes.PREVIEW_SHOW:
|
||||
nextCount = state.previewCount + 1;
|
||||
|
||||
return nextState( state, {
|
||||
previewCount: nextCount,
|
||||
baseData: {
|
||||
previewCountBucket: counts.getPreviewCountBucket( nextCount )
|
||||
},
|
||||
interaction: {
|
||||
timeToPreviewShow:
|
||||
Math.round( action.timestamp - state.interaction.started )
|
||||
}
|
||||
} );
|
||||
|
||||
case actionTypes.LINK_DWELL:
|
||||
|
||||
// Not a new interaction?
|
||||
if ( state.interaction && action.el === state.interaction.link ) {
|
||||
return nextState( state, {
|
||||
interaction: {
|
||||
isUserDwelling: true
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
return nextState( state, {
|
||||
// TODO: Extract this object into a module that can be shared between
|
||||
// this and the preview reducer.
|
||||
interaction: {
|
||||
link: action.el,
|
||||
title: action.title,
|
||||
namespaceId: action.namespaceId,
|
||||
token: action.token,
|
||||
started: action.timestamp,
|
||||
|
||||
isUserDwelling: true
|
||||
},
|
||||
|
||||
// Was the user interacting with another link? If so, then log the
|
||||
// abandoned event.
|
||||
event: state.interaction ?
|
||||
createClosingEvent( state.interaction ) : undefined
|
||||
} );
|
||||
|
||||
case actionTypes.PREVIEW_DWELL:
|
||||
return nextState( state, {
|
||||
interaction: {
|
||||
isUserDwelling: true
|
||||
}
|
||||
} );
|
||||
|
||||
case actionTypes.LINK_CLICK:
|
||||
return nextState( state, {
|
||||
interaction: {
|
||||
finalized: true
|
||||
},
|
||||
event: createEvent( state.interaction, {
|
||||
action: 'opened',
|
||||
totalInteractionTime:
|
||||
Math.round( action.timestamp - state.interaction.started )
|
||||
} )
|
||||
} );
|
||||
|
||||
case actionTypes.ABANDON_START:
|
||||
return nextState( state, {
|
||||
interaction: {
|
||||
finished: action.timestamp,
|
||||
isUserDwelling: false
|
||||
}
|
||||
} );
|
||||
|
||||
case actionTypes.ABANDON_END:
|
||||
if ( !state.interaction.isUserDwelling ) {
|
||||
return nextState( state, {
|
||||
interaction: undefined,
|
||||
event: createClosingEvent( state.interaction )
|
||||
} );
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
case actionTypes.SETTINGS_SHOW:
|
||||
return nextState( state, {
|
||||
event: createEvent( state.interaction, {
|
||||
action: 'tapped settings cog'
|
||||
} )
|
||||
} );
|
||||
|
||||
case actionTypes.SETTINGS_CHANGE:
|
||||
if ( action.oldValue[ previewTypes.TYPE_PAGE ] &&
|
||||
!action.newValue[ previewTypes.TYPE_PAGE ]
|
||||
) {
|
||||
return nextState( state, {
|
||||
event: {
|
||||
action: 'disabled',
|
||||
popupEnabled: false
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
return state;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
import eventLogging from './eventLogging';
|
||||
import pageviews from './pageviews';
|
||||
import preview from './preview';
|
||||
import settings from './settings';
|
||||
import statsv from './statsv';
|
||||
|
||||
export default {
|
||||
eventLogging,
|
||||
pageviews,
|
||||
preview,
|
||||
settings,
|
||||
|
|
|
@ -9,15 +9,12 @@
|
|||
* @return {boolean}
|
||||
*/
|
||||
function isOwnPageAnchorLink( el ) {
|
||||
// Probably a false positive. MediaWiki 1.36 dropped Firefox 4 support anyway.
|
||||
/* eslint-disable compat/compat */
|
||||
return el.hash &&
|
||||
// Note: The protocol is ignored for the sake of simplicity.
|
||||
// Can't compare username and password because they aren't readable from `location`.
|
||||
el.host === location.host &&
|
||||
el.pathname === location.pathname &&
|
||||
el.search === location.search;
|
||||
/* eslint-enable compat/compat */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
|
||||
const PAGE_PREVIEWS_ENABLED_KEY = 'mwe-popups-enabled',
|
||||
REFERENCE_PREVIEWS_ENABLED_KEY = 'mwe-popups-referencePreviews-enabled',
|
||||
REFERENCE_PREVIEWS_LOGGING_SCHEMA = 'event.ReferencePreviewsPopups',
|
||||
PREVIEW_COUNT_KEY = 'ext.popups.core.previewCount';
|
||||
REFERENCE_PREVIEWS_LOGGING_SCHEMA = 'event.ReferencePreviewsPopups';
|
||||
|
||||
/**
|
||||
* Creates an object whose methods encapsulate all interactions with the UA's
|
||||
|
@ -78,46 +77,6 @@ export default function createUserSettings( storage ) {
|
|||
mw.track( REFERENCE_PREVIEWS_LOGGING_SCHEMA, {
|
||||
action: enabled ? 'anonymousEnabled' : 'anonymousDisabled'
|
||||
} );
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the number of previews that the user has seen.
|
||||
*
|
||||
* - If the storage isn't available, then -1 is returned.
|
||||
* - If the value in storage is not a number it will override stored value
|
||||
* to 0
|
||||
*
|
||||
* @method
|
||||
* @name UserSettings#getPreviewCount
|
||||
* @return {number}
|
||||
*/
|
||||
getPreviewCount() {
|
||||
const result = storage.get( PREVIEW_COUNT_KEY );
|
||||
|
||||
if ( result === false ) {
|
||||
return -1;
|
||||
} else if ( result === null ) {
|
||||
return 0;
|
||||
}
|
||||
let count = parseInt( result, 10 );
|
||||
|
||||
// stored number is not a zero, override it to zero and store new value
|
||||
if ( isNaN( count ) ) {
|
||||
count = 0;
|
||||
this.storePreviewCount( count );
|
||||
}
|
||||
return count;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the number of previews that the user has seen.
|
||||
*
|
||||
* @method
|
||||
* @name UserSettings#storePreviewCount
|
||||
* @param {number} count
|
||||
*/
|
||||
storePreviewCount( count ) {
|
||||
storage.set( PREVIEW_COUNT_KEY, count.toString() );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -53,8 +53,7 @@ QUnit.test( '#boot', ( assert ) => {
|
|||
},
|
||||
user: {
|
||||
isAnon: true,
|
||||
editCount: 3,
|
||||
previewCount: 22
|
||||
editCount: 3
|
||||
}
|
||||
},
|
||||
'boots with the initial state'
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import eventLogging from '../../../src/changeListeners/eventLogging';
|
||||
|
||||
function getCurrentTimestamp() {
|
||||
return 123;
|
||||
}
|
||||
|
||||
QUnit.module( 'ext.popups/eventLogging', {
|
||||
beforeEach() {
|
||||
this.boundActions = {
|
||||
eventLogged: this.sandbox.spy()
|
||||
};
|
||||
|
||||
this.eventLoggingTracker = this.sandbox.spy();
|
||||
this.changeListener = eventLogging(
|
||||
this.boundActions,
|
||||
this.eventLoggingTracker,
|
||||
getCurrentTimestamp
|
||||
);
|
||||
}
|
||||
} );
|
||||
|
||||
function createState( baseData, event ) {
|
||||
return {
|
||||
eventLogging: {
|
||||
baseData,
|
||||
event
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
QUnit.test( 'it should log the queued event', function ( assert ) {
|
||||
const baseData = {
|
||||
foo: 'bar',
|
||||
baz: 'qux'
|
||||
};
|
||||
|
||||
const newState = createState( baseData, {
|
||||
action: 'pageLoaded'
|
||||
} );
|
||||
|
||||
this.changeListener( undefined, newState );
|
||||
|
||||
assert.ok(
|
||||
this.eventLoggingTracker.calledWith(
|
||||
'event.Popups',
|
||||
{
|
||||
foo: 'bar',
|
||||
baz: 'qux',
|
||||
action: 'pageLoaded',
|
||||
timestamp: 123
|
||||
}
|
||||
),
|
||||
'It should merge the event data and the accumulated base data.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'it should call the eventLogged bound action creator', function ( assert ) {
|
||||
const newState = createState( {}, undefined );
|
||||
|
||||
this.changeListener( undefined, newState );
|
||||
|
||||
assert.notOk(
|
||||
this.boundActions.eventLogged.called,
|
||||
'It shouldn\'t call the eventLogged bound action creator if there\'s no queued event.'
|
||||
);
|
||||
|
||||
newState.eventLogging.event = {
|
||||
action: 'pageLoaded'
|
||||
};
|
||||
|
||||
this.changeListener( undefined, newState );
|
||||
|
||||
assert.ok(
|
||||
this.boundActions.eventLogged.called,
|
||||
'The EventLogging action is called.'
|
||||
);
|
||||
assert.deepEqual(
|
||||
this.boundActions.eventLogged.getCall( 0 ).args[ 0 ], {
|
||||
action: 'pageLoaded',
|
||||
timestamp: 123
|
||||
},
|
||||
'The EventLogging action is called with correct arguments.'
|
||||
);
|
||||
} );
|
|
@ -3,7 +3,6 @@ import syncUserSettings from '../../../src/changeListeners/syncUserSettings';
|
|||
QUnit.module( 'ext.popups/changeListeners/syncUserSettings', {
|
||||
beforeEach() {
|
||||
this.userSettings = {
|
||||
storePreviewCount: this.sandbox.spy(),
|
||||
storePagePreviewsEnabled: this.sandbox.spy(),
|
||||
storeReferencePreviewsEnabled: this.sandbox.spy()
|
||||
};
|
||||
|
@ -12,34 +11,6 @@ QUnit.module( 'ext.popups/changeListeners/syncUserSettings', {
|
|||
}
|
||||
} );
|
||||
|
||||
QUnit.test(
|
||||
'it shouldn\'t update the storage if the preview count hasn\'t changed',
|
||||
function ( assert ) {
|
||||
const oldState = { eventLogging: { previewCount: 222 } },
|
||||
newState = { eventLogging: { previewCount: 222 } };
|
||||
|
||||
this.changeListener( undefined, newState );
|
||||
this.changeListener( oldState, newState );
|
||||
|
||||
assert.notOk(
|
||||
this.userSettings.storePreviewCount.called,
|
||||
'The preview count is unchanged.'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test( 'it should update the storage if the previewCount has changed', function ( assert ) {
|
||||
const oldState = { eventLogging: { previewCount: 222 } },
|
||||
newState = { eventLogging: { previewCount: 223 } };
|
||||
|
||||
this.changeListener( oldState, newState );
|
||||
|
||||
assert.ok(
|
||||
this.userSettings.storePreviewCount.calledWith( 223 ),
|
||||
'The preview count is updated.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test(
|
||||
'it shouldn\'t update the storage if the enabled state hasn\'t changed',
|
||||
function ( assert ) {
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import { isEnabled } from '../../../src/instrumentation/eventLogging';
|
||||
import * as stubs from '../stubs';
|
||||
|
||||
QUnit.module( 'ext.popups/instrumentation/eventLogging', {
|
||||
beforeEach() {
|
||||
this.config = new Map();
|
||||
this.config.set( 'wgPopupsEventLogging', true );
|
||||
|
||||
this.window = {
|
||||
navigator: {
|
||||
sendBeacon() {}
|
||||
}
|
||||
};
|
||||
|
||||
this.user = stubs.createStubUser();
|
||||
|
||||
// Helper function that DRYs up the tests below.
|
||||
this.isEnabled = () => isEnabled( this.user, this.config, this.window );
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'it should return false when sendBeacon isn\'t supported', function ( assert ) {
|
||||
this.window = {};
|
||||
assert.notOk( this.isEnabled(),
|
||||
'No sendBeacon. No logging.' );
|
||||
// ---
|
||||
|
||||
this.window.navigator = {
|
||||
sendBeacon: 'NOT A FUNCTION'
|
||||
};
|
||||
|
||||
assert.notOk(
|
||||
this.isEnabled(),
|
||||
'EventLogging is disabled.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'it should respect PopupsEventLogging', function ( assert ) {
|
||||
assert.ok( this.isEnabled(), 'EventLogging is enabled.' );
|
||||
this.config.set( 'wgPopupsEventLogging', false );
|
||||
assert.notOk( this.isEnabled(), 'EventLogging is disabled.' );
|
||||
} );
|
||||
|
||||
QUnit.test( 'it should respect the debug flag always', function ( assert ) {
|
||||
this.config.set( 'wgPopupsEventLogging', false );
|
||||
this.config.set( 'debug', false );
|
||||
assert.notOk( this.isEnabled(), 'not logged' );
|
||||
|
||||
this.config.set( 'debug', true );
|
||||
assert.ok( this.isEnabled(), 'is logged!' );
|
||||
} );
|
|
@ -1,816 +0,0 @@
|
|||
import * as counts from '../../../src/counts';
|
||||
import { createModel } from '../../../src/preview/model';
|
||||
import eventLogging from '../../../src/reducers/eventLogging';
|
||||
import actionTypes from '../../../src/actionTypes';
|
||||
|
||||
QUnit.module( 'ext.popups/reducers#eventLogging', {
|
||||
beforeEach() {
|
||||
this.initialState = eventLogging( undefined, {
|
||||
type: '@@INIT'
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( '@@INIT', function ( assert ) {
|
||||
assert.deepEqual(
|
||||
this.initialState,
|
||||
{
|
||||
previewCount: undefined,
|
||||
baseData: {},
|
||||
event: undefined,
|
||||
interaction: undefined
|
||||
},
|
||||
'The initial state is correct.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'BOOT', function ( assert ) {
|
||||
const action = {
|
||||
type: actionTypes.BOOT,
|
||||
initiallyEnabled: { page: true },
|
||||
isNavPopupsEnabled: false,
|
||||
sessionToken: '0123456789',
|
||||
pageToken: '9876543210',
|
||||
page: {
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
id: 2
|
||||
},
|
||||
user: {
|
||||
isAnon: false,
|
||||
editCount: 11,
|
||||
previewCount: 22
|
||||
}
|
||||
};
|
||||
|
||||
const expectedEditCountBucket =
|
||||
counts.getEditCountBucket( action.user.editCount );
|
||||
const expectedPreviewCountBucket =
|
||||
counts.getPreviewCountBucket( action.user.previewCount );
|
||||
|
||||
let state = eventLogging( this.initialState, action );
|
||||
|
||||
assert.deepEqual(
|
||||
state,
|
||||
{
|
||||
previewCount: action.user.previewCount,
|
||||
baseData: {
|
||||
pageTitleSource: action.page.title,
|
||||
namespaceIdSource: action.page.namespaceId,
|
||||
pageIdSource: action.page.id,
|
||||
isAnon: action.user.isAnon,
|
||||
popupEnabled: action.initiallyEnabled.page,
|
||||
pageToken: action.pageToken,
|
||||
sessionToken: action.sessionToken,
|
||||
editCountBucket: expectedEditCountBucket,
|
||||
previewCountBucket: expectedPreviewCountBucket,
|
||||
hovercardsSuppressedByGadget: action.isNavPopupsEnabled
|
||||
},
|
||||
event: {
|
||||
action: 'pageLoaded'
|
||||
},
|
||||
interaction: undefined
|
||||
},
|
||||
'The boot state is correct.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
// And when the user is logged out...
|
||||
action.user.isAnon = true;
|
||||
|
||||
state = eventLogging( this.initialState, action );
|
||||
|
||||
assert.strictEqual(
|
||||
state.baseData.isAnon,
|
||||
true,
|
||||
'The user is anonymous and not logged in.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
state.baseData.editCountBucket,
|
||||
undefined,
|
||||
'It shouldn\'t add the editCountBucket property when the user is logged out.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'EVENT_LOGGED', ( assert ) => {
|
||||
let state = {
|
||||
event: {}
|
||||
};
|
||||
|
||||
let action = {
|
||||
type: actionTypes.EVENT_LOGGED,
|
||||
event: {}
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
eventLogging( state, action ),
|
||||
{
|
||||
event: undefined
|
||||
},
|
||||
'It dequeues any event queued for logging.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
state = {
|
||||
interaction: { token: 'asdf' },
|
||||
event: { linkInteractionToken: 'asdf' }
|
||||
};
|
||||
|
||||
action = {
|
||||
type: actionTypes.EVENT_LOGGED,
|
||||
event: state.event
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
eventLogging( state, action ),
|
||||
{
|
||||
event: undefined,
|
||||
interaction: undefined
|
||||
},
|
||||
'It destroys current interaction if an event for it was logged.'
|
||||
);
|
||||
|
||||
} );
|
||||
|
||||
QUnit.test( 'PREVIEW_SHOW', ( assert ) => {
|
||||
const count = 22,
|
||||
expectedCount = count + 1,
|
||||
token = '1234567890';
|
||||
|
||||
let state = {
|
||||
previewCount: count,
|
||||
baseData: {
|
||||
previewCountBucket: counts.getPreviewCountBucket( count )
|
||||
},
|
||||
event: undefined,
|
||||
|
||||
// state.interaction.started is used in this part of the reducer.
|
||||
interaction: {
|
||||
token
|
||||
}
|
||||
};
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.PREVIEW_SHOW,
|
||||
token
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.previewCount,
|
||||
expectedCount,
|
||||
'It updates the user\'s preview count.'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
state.baseData,
|
||||
{
|
||||
previewCountBucket: counts.getPreviewCountBucket( expectedCount )
|
||||
},
|
||||
'It re-buckets the user\'s preview count.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.module( 'ext.popups/reducers#eventLogging @integration', {
|
||||
beforeEach() {
|
||||
this.link = $( '<a>' ).get( 0 );
|
||||
}
|
||||
} );
|
||||
|
||||
QUnit.test( 'LINK_DWELL starts an interaction', function ( assert ) {
|
||||
const state = {
|
||||
interaction: undefined
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token: '0987654321',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
eventLogging( state, action ),
|
||||
{
|
||||
interaction: {
|
||||
link: action.el,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token: action.token,
|
||||
started: action.timestamp,
|
||||
|
||||
isUserDwelling: true
|
||||
},
|
||||
event: undefined
|
||||
},
|
||||
'The link dwell state is correct.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'LINK_DWELL doesn\'t start a new interaction under certain conditions', function ( assert ) {
|
||||
const now = Date.now();
|
||||
|
||||
let state = {
|
||||
interaction: undefined
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token: '0987654321',
|
||||
timestamp: now
|
||||
};
|
||||
|
||||
state = eventLogging( state, action );
|
||||
|
||||
action.token = '1234567890';
|
||||
action.timestamp = now + 200;
|
||||
|
||||
state = eventLogging( state, action );
|
||||
|
||||
assert.deepEqual(
|
||||
state.interaction,
|
||||
{
|
||||
link: action.el,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token: '0987654321',
|
||||
started: now,
|
||||
|
||||
isUserDwelling: true
|
||||
},
|
||||
'The link dwell state is correct.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'LINK_DWELL should enqueue a "dismissed" or "dwelledButAbandoned" event under certain conditions', function ( assert ) {
|
||||
const token = '0987654321',
|
||||
now = Date.now();
|
||||
|
||||
// Read: The user dwells on link A, abandons it, and dwells on link B fewer
|
||||
// than 300 ms after (before the ABANDON_END action is reduced).
|
||||
let state = eventLogging( undefined, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_START,
|
||||
timestamp: now + 250
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: $( '<a>' ),
|
||||
title: 'Bar',
|
||||
namespaceId: 1,
|
||||
token: '1234567890',
|
||||
timestamp: now + 500
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
pageTitleHover: 'Foo',
|
||||
namespaceIdHover: 1,
|
||||
linkInteractionToken: '0987654321',
|
||||
totalInteractionTime: 250, // 250 - 0
|
||||
action: 'dwelledButAbandoned'
|
||||
},
|
||||
'The link dwell state is correct.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
state = eventLogging( undefined, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_CLICK,
|
||||
el: this.link
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: $( '<a>' ),
|
||||
title: 'Bar',
|
||||
namespaceId: 1,
|
||||
token: 'banana',
|
||||
timestamp: now + 500
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.event,
|
||||
undefined,
|
||||
'It shouldn\'t enqueue either event if the interaction is finalized.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'LINK_CLICK should enqueue an "opened" event', function ( assert ) {
|
||||
const token = '0987654321',
|
||||
now = Date.now();
|
||||
|
||||
let state = {
|
||||
interaction: undefined
|
||||
};
|
||||
|
||||
const expectedState = state = eventLogging( state, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_CLICK,
|
||||
el: this.link,
|
||||
timestamp: now + 250
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
action: 'opened',
|
||||
pageTitleHover: 'Foo',
|
||||
namespaceIdHover: 1,
|
||||
linkInteractionToken: token,
|
||||
totalInteractionTime: 250
|
||||
},
|
||||
'The event is enqueued and the totalInteractionTime property is an integer.'
|
||||
);
|
||||
|
||||
expectedState.interaction.finalized = true;
|
||||
|
||||
assert.deepEqual(
|
||||
state.interaction,
|
||||
expectedState.interaction,
|
||||
'It should finalize the interaction.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'PREVIEW_SHOW should update the perceived wait time of the interaction', function ( assert ) {
|
||||
const now = Date.now(),
|
||||
token = '1234567890';
|
||||
|
||||
let state = {
|
||||
interaction: undefined
|
||||
};
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.PREVIEW_SHOW,
|
||||
token,
|
||||
timestamp: now + 500
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.interaction, {
|
||||
link: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
started: now,
|
||||
|
||||
isUserDwelling: true,
|
||||
|
||||
timeToPreviewShow: 500
|
||||
},
|
||||
'The preview show state is correct.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'LINK_CLICK should include perceivedWait if the preview has been shown', function ( assert ) {
|
||||
const token = '0987654321',
|
||||
now = Date.now();
|
||||
|
||||
let state = {
|
||||
interaction: undefined
|
||||
};
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.PREVIEW_SHOW,
|
||||
token,
|
||||
timestamp: now + 750
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.LINK_CLICK,
|
||||
el: this.link,
|
||||
timestamp: now + 1050
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
action: 'opened',
|
||||
pageTitleHover: 'Foo',
|
||||
namespaceIdHover: 1,
|
||||
linkInteractionToken: token,
|
||||
totalInteractionTime: 1050,
|
||||
|
||||
// N.B. that the FETCH_* actions have been skipped.
|
||||
previewType: undefined,
|
||||
perceivedWait: 750
|
||||
},
|
||||
'The previewType and perceivedWait properties are set if the preview has been shown.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'FETCH_COMPLETE', ( assert ) => {
|
||||
const token = '1234567890',
|
||||
initialState = {
|
||||
interaction: {
|
||||
token
|
||||
}
|
||||
},
|
||||
model = createModel(
|
||||
'Foo',
|
||||
'https://en.wikipedia.org/wiki/Foo',
|
||||
'en',
|
||||
'ltr',
|
||||
'',
|
||||
{}
|
||||
);
|
||||
let state = eventLogging( initialState, {
|
||||
type: actionTypes.FETCH_COMPLETE,
|
||||
result: model,
|
||||
token
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.interaction.previewType,
|
||||
model.type,
|
||||
'It mixes in the preview type to the interaction state.'
|
||||
);
|
||||
|
||||
// ---
|
||||
state = eventLogging( initialState, {
|
||||
type: actionTypes.FETCH_COMPLETE,
|
||||
result: model,
|
||||
token: 'banana'
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
initialState,
|
||||
state,
|
||||
'It should NOOP if there\'s a new interaction.'
|
||||
);
|
||||
|
||||
// ---
|
||||
delete initialState.interaction;
|
||||
|
||||
state = eventLogging( initialState, {
|
||||
type: actionTypes.FETCH_COMPLETE,
|
||||
result: model,
|
||||
token: '0123456789'
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
initialState,
|
||||
state,
|
||||
'It should NOOP if the interaction has been finalised.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'ABANDON_START', function ( assert ) {
|
||||
let state = {
|
||||
interaction: {}
|
||||
};
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_START,
|
||||
timestamp: Date.now()
|
||||
} );
|
||||
|
||||
assert.notOk(
|
||||
state.interaction.isUserDwelling,
|
||||
'It should mark the link or preview as having been abandoned.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'ABANDON_END', function ( assert ) {
|
||||
let state = {
|
||||
interaction: {}
|
||||
};
|
||||
|
||||
let action = {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token: '1234567890',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
state = eventLogging( state, action );
|
||||
|
||||
action = {
|
||||
type: actionTypes.ABANDON_END,
|
||||
token: '1234567890'
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
eventLogging( state, action ),
|
||||
state,
|
||||
'ABANDON_END should NOOP if the user is dwelling on the preview or the link.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
action.token = '0987654321';
|
||||
|
||||
assert.deepEqual(
|
||||
eventLogging( state, action ),
|
||||
state,
|
||||
'ABANDON_END should NOOP if the current interaction has changed.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'PREVIEW_DWELL', ( assert ) => {
|
||||
let state = {
|
||||
interaction: {}
|
||||
};
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.PREVIEW_DWELL
|
||||
} );
|
||||
|
||||
assert.ok(
|
||||
state.interaction.isUserDwelling,
|
||||
'It should mark the link or preview as being dwelled on.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'SETTINGS_SHOW should enqueue a "tapped settings cog" event', function ( assert ) {
|
||||
const initialState = {
|
||||
interaction: { started: 0, finished: 0 }
|
||||
},
|
||||
token = '0123456789';
|
||||
|
||||
let state = eventLogging( initialState, {
|
||||
type: actionTypes.SETTINGS_SHOW
|
||||
} );
|
||||
|
||||
// Note well that this is a valid event. The "tapped settings cog" event is
|
||||
// also logged as a result of clicking the footer link.
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
action: 'tapped settings cog',
|
||||
linkInteractionToken: undefined,
|
||||
namespaceIdHover: undefined,
|
||||
pageTitleHover: undefined
|
||||
},
|
||||
'It shouldn\'t fail if there\'s no interaction.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
state = eventLogging( initialState, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: Date.now()
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.SETTINGS_SHOW
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
action: 'tapped settings cog',
|
||||
linkInteractionToken: token,
|
||||
totalInteractionTime: 0,
|
||||
namespaceIdHover: 1,
|
||||
pageTitleHover: 'Foo'
|
||||
},
|
||||
'It should include the interaction information if there\'s an interaction.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'SETTINGS_CHANGE should enqueue disabled event', ( assert ) => {
|
||||
let state = eventLogging( undefined, {
|
||||
type: actionTypes.SETTINGS_CHANGE,
|
||||
oldValue: { page: false },
|
||||
newValue: { page: false }
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.event,
|
||||
undefined,
|
||||
'It shouldn\'t enqueue a "disabled" event when there is no change'
|
||||
);
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.SETTINGS_CHANGE,
|
||||
oldValue: { page: true },
|
||||
newValue: { page: false }
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
action: 'disabled',
|
||||
popupEnabled: false
|
||||
},
|
||||
'It should enqueue a "disabled" event when the previews has been disabled'
|
||||
);
|
||||
|
||||
delete state.event;
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.SETTINGS_CHANGE,
|
||||
oldValue: { page: false },
|
||||
newValue: { page: true }
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.event,
|
||||
undefined,
|
||||
'It shouldn\'t enqueue a "disabled" event when page previews has been enabled'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'ABANDON_END should enqueue an event', function ( assert ) {
|
||||
const token = '0987654321',
|
||||
now = Date.now();
|
||||
|
||||
const dwelledState = eventLogging( undefined, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
let state = eventLogging( dwelledState, {
|
||||
type: actionTypes.ABANDON_START,
|
||||
token,
|
||||
timestamp: now + 500
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_END,
|
||||
token
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
pageTitleHover: 'Foo',
|
||||
namespaceIdHover: 1,
|
||||
linkInteractionToken: token,
|
||||
totalInteractionTime: 500,
|
||||
action: 'dwelledButAbandoned'
|
||||
},
|
||||
'It should enqueue a "dwelledButAbandoned" event when the preview hasn\'t been shown.'
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
state.interaction,
|
||||
undefined,
|
||||
'It should close the interaction.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
state = eventLogging( dwelledState, {
|
||||
type: actionTypes.PREVIEW_SHOW,
|
||||
token,
|
||||
timestamp: now + 700
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_START,
|
||||
token,
|
||||
timestamp: now + 850
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_END,
|
||||
token
|
||||
} );
|
||||
|
||||
assert.deepEqual(
|
||||
state.event,
|
||||
{
|
||||
pageTitleHover: 'Foo',
|
||||
namespaceIdHover: 1,
|
||||
linkInteractionToken: token,
|
||||
totalInteractionTime: 850,
|
||||
action: 'dismissed',
|
||||
|
||||
// N.B. that the FETCH_* actions have been skipped.
|
||||
previewType: undefined,
|
||||
|
||||
perceivedWait: 700
|
||||
},
|
||||
'It should enqueue a "dismissed" event when the preview has been shown.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( 'ABANDON_END doesn\'t enqueue an event under certain conditions', function ( assert ) {
|
||||
const token = '0987654321',
|
||||
now = Date.now();
|
||||
|
||||
const dwelledState = eventLogging( undefined, {
|
||||
type: actionTypes.LINK_DWELL,
|
||||
el: this.link,
|
||||
title: 'Foo',
|
||||
namespaceId: 1,
|
||||
token,
|
||||
timestamp: now
|
||||
} );
|
||||
|
||||
let state = eventLogging( dwelledState, {
|
||||
type: actionTypes.ABANDON_END,
|
||||
token: '1234567890'
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.event,
|
||||
undefined,
|
||||
'It shouldn\'t enqueue an event if there\'s a new interaction.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
state = eventLogging( dwelledState, {
|
||||
type: actionTypes.ABANDON_END,
|
||||
token
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.event,
|
||||
undefined,
|
||||
'It shouldn\'t enqueue an event if the user is dwelling on the preview or the link.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
state = eventLogging( dwelledState, {
|
||||
type: actionTypes.LINK_CLICK,
|
||||
timestamp: now + 500
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.EVENT_LOGGED,
|
||||
event: {}
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_START,
|
||||
token,
|
||||
timestamp: now + 700
|
||||
} );
|
||||
|
||||
state = eventLogging( state, {
|
||||
type: actionTypes.ABANDON_END,
|
||||
token,
|
||||
timestamp: now + 1000 // ABANDON_END_DELAY is 300 ms.
|
||||
} );
|
||||
|
||||
assert.strictEqual(
|
||||
state.event,
|
||||
undefined,
|
||||
'It shouldn\'t enqueue an event if the interaction is finalized.'
|
||||
);
|
||||
} );
|
|
@ -1,7 +1,7 @@
|
|||
import statsv from '../../../src/reducers/statsv';
|
||||
import actionTypes from '../../../src/actionTypes';
|
||||
|
||||
QUnit.module( 'ext.popups/reducers#eventLogging', {
|
||||
QUnit.module( 'ext.popups/reducers#statsv', {
|
||||
beforeEach() {
|
||||
this.initialState = statsv( undefined, {
|
||||
type: '@@INIT'
|
||||
|
|
|
@ -49,56 +49,3 @@ QUnit.test( '#isReferencePreviewsEnabled', function ( assert ) {
|
|||
'#isReferencePreviewsEnabled is now false.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( '#getPreviewCount should return the count as a number', function ( assert ) {
|
||||
assert.strictEqual(
|
||||
this.userSettings.getPreviewCount(),
|
||||
0,
|
||||
'#getPreviewCount returns 0 when the storage is empty.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
this.storage.set( 'ext.popups.core.previewCount', false );
|
||||
|
||||
assert.strictEqual(
|
||||
this.userSettings.getPreviewCount(),
|
||||
-1,
|
||||
'#getPreviewCount returns -1 when the storage isn\'t available.'
|
||||
);
|
||||
|
||||
// ---
|
||||
|
||||
this.storage.set( 'ext.popups.core.previewCount', '111' );
|
||||
|
||||
assert.strictEqual(
|
||||
this.userSettings.getPreviewCount(),
|
||||
111,
|
||||
'#getPreviewCount returns the total.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( '#storePreviewCount should store the count as a string', function ( assert ) {
|
||||
this.userSettings.storePreviewCount( 222 );
|
||||
|
||||
assert.strictEqual(
|
||||
this.storage.get( 'ext.popups.core.previewCount' ),
|
||||
'222',
|
||||
'Storage returns the total as a string.'
|
||||
);
|
||||
} );
|
||||
|
||||
QUnit.test( '#getPreviewCount should override value in storage when is not a number', function ( assert ) {
|
||||
this.storage.set( 'ext.popups.core.previewCount', 'NaN' );
|
||||
|
||||
assert.strictEqual(
|
||||
this.userSettings.getPreviewCount(),
|
||||
0,
|
||||
'#getPreviewCount returns a sane default.'
|
||||
);
|
||||
assert.strictEqual(
|
||||
this.storage.get( 'ext.popups.core.previewCount' ),
|
||||
'0',
|
||||
'Storage returns a sane default as a string.'
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -21,7 +21,6 @@
|
|||
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls;
|
||||
use Popups\EventLogging\EventLogger;
|
||||
use Popups\PopupsContext;
|
||||
use Popups\PopupsGadgetsIntegration;
|
||||
|
||||
|
@ -41,15 +40,11 @@ class PopupsContextTest extends MediaWikiTestCase {
|
|||
* Helper method to quickly build Popups Context
|
||||
* @param ExtensionRegistry|null $registry
|
||||
* @param PopupsGadgetsIntegration|null $integration
|
||||
* @param EventLogger|null $eventLogger
|
||||
* @return PopupsContext
|
||||
*/
|
||||
protected function getContext( $registry = null, $integration = null, $eventLogger = null ) {
|
||||
protected function getContext( $registry = null, $integration = null ) {
|
||||
$config = new GlobalVarConfig();
|
||||
$registry = $registry ?: ExtensionRegistry::getInstance();
|
||||
if ( $eventLogger === null ) {
|
||||
$eventLogger = $this->createMock( EventLogger::class );
|
||||
}
|
||||
if ( $integration === null ) {
|
||||
$integration = $this->createMock( PopupsGadgetsIntegration::class );
|
||||
$integration->method( 'conflictsWithNavPopupsGadget' )
|
||||
|
@ -60,7 +55,6 @@ class PopupsContextTest extends MediaWikiTestCase {
|
|||
$config,
|
||||
$registry,
|
||||
$integration,
|
||||
$eventLogger,
|
||||
$userOptionsLookup
|
||||
);
|
||||
}
|
||||
|
@ -344,17 +338,4 @@ class PopupsContextTest extends MediaWikiTestCase {
|
|||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::logUserDisabledPagePreviewsEvent
|
||||
*/
|
||||
public function testLogsEvent() {
|
||||
$loggerMock = $this->createMock( EventLogger::class );
|
||||
$loggerMock->expects( $this->once() )
|
||||
->method( 'log' );
|
||||
|
||||
$context = $this->getContext( null, null, $loggerMock );
|
||||
$context->logUserDisabledPagePreviewsEvent();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
* @ingroup extensions
|
||||
*/
|
||||
|
||||
use Popups\EventLogging\EventLogger;
|
||||
use Popups\EventLogging\NullLogger;
|
||||
use Popups\PopupsContext;
|
||||
use Popups\PopupsGadgetsIntegration;
|
||||
|
||||
|
@ -43,25 +41,21 @@ class PopupsContextTestWrapper extends PopupsContext {
|
|||
* @param Config $config MediaWiki config
|
||||
* @param ExtensionRegistry $extensionRegistry MediaWiki extension registry
|
||||
* @param PopupsGadgetsIntegration|null $gadgetsIntegration Gadgets integration helper
|
||||
* @param EventLogger|null $eventLogger EventLogger
|
||||
* @param UserOptionsLookup $userOptionsLookup
|
||||
*/
|
||||
public function __construct(
|
||||
Config $config,
|
||||
ExtensionRegistry $extensionRegistry,
|
||||
PopupsGadgetsIntegration $gadgetsIntegration,
|
||||
EventLogger $eventLogger,
|
||||
UserOptionsLookup $userOptionsLookup
|
||||
) {
|
||||
$gadgetsIntegration = $gadgetsIntegration ?:
|
||||
new PopupsGadgetsIntegration( $config, $extensionRegistry );
|
||||
$eventLogger = $eventLogger ?: new NullLogger();
|
||||
|
||||
parent::__construct(
|
||||
$config,
|
||||
$extensionRegistry,
|
||||
$gadgetsIntegration,
|
||||
$eventLogger,
|
||||
$userOptionsLookup
|
||||
);
|
||||
}
|
||||
|
|
|
@ -151,7 +151,6 @@ class PopupsHooksTest extends MediaWikiTestCase {
|
|||
public function testOnResourceLoaderGetConfigVars() {
|
||||
$vars = [ 'something' => 'notEmpty' ];
|
||||
$config = [
|
||||
'wgPopupsEventLogging' => false,
|
||||
'wgPopupsRestGatewayEndpoint' => '/api',
|
||||
'wgPopupsVirtualPageViews' => true,
|
||||
'wgPopupsGateway' => 'mwApiPlain',
|
||||
|
@ -160,7 +159,7 @@ class PopupsHooksTest extends MediaWikiTestCase {
|
|||
];
|
||||
$this->setMwGlobals( $config );
|
||||
PopupsHooks::onResourceLoaderGetConfigVars( $vars, '' );
|
||||
$this->assertCount( 7, $vars, 'A configuration is retrieved.' );
|
||||
$this->assertCount( 6, $vars, 'A configuration is retrieved.' );
|
||||
|
||||
foreach ( $config as $key => $value ) {
|
||||
$this->assertSame(
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
use Popups\EventLogging\EventLoggerFactory;
|
||||
use Popups\EventLogging\MWEventLogger;
|
||||
use Popups\EventLogging\NullLogger;
|
||||
|
||||
/**
|
||||
* @group Popups
|
||||
* @coversDefaultClass \Popups\EventLogging\EventLoggerFactory
|
||||
*/
|
||||
class EventLoggerFactoryTest extends MediaWikiUnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::get
|
||||
* @covers \Popups\EventLogging\MWEventLogger::__construct
|
||||
*/
|
||||
public function testReturnsMWEventWhenEventLoggingIsAvailable() {
|
||||
$mock = $this->createMock( ExtensionRegistry::class );
|
||||
$mock->expects( $this->once() )
|
||||
->method( 'isLoaded' )
|
||||
->with( 'EventLogging' )
|
||||
->willReturn( true );
|
||||
|
||||
$factory = new EventLoggerFactory( $mock );
|
||||
$this->assertInstanceOf( MWEventLogger::class,
|
||||
$factory->get(),
|
||||
'A functional event logger is instantiated.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::__construct
|
||||
* @covers ::get
|
||||
*/
|
||||
public function testReturnsMWEventWhenEventLoggingIsNotAvailable() {
|
||||
$mock = $this->createMock( ExtensionRegistry::class );
|
||||
$mock->expects( $this->once() )
|
||||
->method( 'isLoaded' )
|
||||
->with( 'EventLogging' )
|
||||
->willReturn( false );
|
||||
|
||||
$factory = new EventLoggerFactory( $mock );
|
||||
$this->assertInstanceOf( NullLogger::class,
|
||||
$factory->get(),
|
||||
'A no-op event logger is instantiated.' );
|
||||
}
|
||||
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of the MediaWiki extension Popups.
|
||||
*
|
||||
* Popups 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.
|
||||
*
|
||||
* Popups 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 Popups. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @file
|
||||
* @ingroup extensions
|
||||
*/
|
||||
|
||||
use MediaWiki\User\UserOptionsLookup;
|
||||
use Popups\PopupsContext;
|
||||
use Popups\UserPreferencesChangeHandler;
|
||||
|
||||
/**
|
||||
* @group Popups
|
||||
* @coversDefaultClass \Popups\UserPreferencesChangeHandler
|
||||
*/
|
||||
class UserPreferencesChangeHandlerTest extends MediaWikiUnitTestCase {
|
||||
|
||||
/**
|
||||
* @covers ::doPreferencesFormPreSave
|
||||
* @covers ::__construct
|
||||
* @dataProvider provideDataForEventHandling
|
||||
*/
|
||||
public function testEventHandling( $oldOption, $newOption, $expectedMethodCallsCount ) {
|
||||
$contextMock = $this->createMock( PopupsContext::class );
|
||||
$contextMock->expects( $expectedMethodCallsCount )
|
||||
->method( 'logUserDisabledPagePreviewsEvent' );
|
||||
|
||||
/** @var User $userMock */
|
||||
$userMock = $this->createMock( User::class );
|
||||
|
||||
$userOptionsLookupMock = $this->createMock( UserOptionsLookup::class );
|
||||
$userOptionsLookupMock
|
||||
->method( 'getBoolOption' )
|
||||
->willReturn( $newOption );
|
||||
|
||||
$oldOptions = [
|
||||
PopupsContext::PREVIEWS_OPTIN_PREFERENCE_NAME => $oldOption
|
||||
];
|
||||
$listener = new UserPreferencesChangeHandler(
|
||||
$contextMock,
|
||||
$userOptionsLookupMock
|
||||
);
|
||||
$listener->doPreferencesFormPreSave( $userMock, $oldOptions );
|
||||
}
|
||||
|
||||
public function provideDataForEventHandling() {
|
||||
return [
|
||||
[ PopupsContext::PREVIEWS_DISABLED, PopupsContext::PREVIEWS_DISABLED, $this->never() ],
|
||||
[ PopupsContext::PREVIEWS_ENABLED, PopupsContext::PREVIEWS_ENABLED, $this->never() ],
|
||||
[ PopupsContext::PREVIEWS_DISABLED, PopupsContext::PREVIEWS_ENABLED, $this->never() ],
|
||||
[ PopupsContext::PREVIEWS_ENABLED, PopupsContext::PREVIEWS_DISABLED, $this->once() ]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue