Remove Popups instrumentation

Bug: T267211
Change-Id: I640ab367cd235ab8da7dd70dbef7ae9076712e84
This commit is contained in:
Sam Smith 2021-03-02 17:08:45 +00:00 committed by Jdlrobson
parent 1de4252bdb
commit 8f5000f346
36 changed files with 14 additions and 2058 deletions

View file

@ -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.",

View file

@ -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',
]
);

View file

@ -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

View file

@ -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 );
}

View file

@ -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();
}
}

View file

@ -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
);
}
}

View file

@ -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
}
}

View file

@ -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'
] );
}
}

View file

@ -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' );

View file

@ -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()
);
}

View file

@ -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 );
}
}

View file

@ -68,7 +68,7 @@
"bundlesize": [
{
"path": "resources/dist/index.js",
"maxSize": "13.9kB"
"maxSize": "12.9kB"
}
]
}

Binary file not shown.

Binary file not shown.

View file

@ -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'
};

View file

@ -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.

View file

@ -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 );
};
}

View file

@ -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,

View file

@ -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
);

View file

@ -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';
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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 */
}
/**

View file

@ -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() );
}
};
}

View file

@ -53,8 +53,7 @@ QUnit.test( '#boot', ( assert ) => {
},
user: {
isAnon: true,
editCount: 3,
previewCount: 22
editCount: 3
}
},
'boots with the initial state'

View file

@ -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.'
);
} );

View file

@ -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 ) {

View file

@ -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!' );
} );

View file

@ -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.'
);
} );

View file

@ -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'

View file

@ -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.'
);
} );

View file

@ -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();
}
}

View file

@ -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
);
}

View file

@ -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(

View file

@ -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.' );
}
}

View file

@ -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() ]
];
}
}