diff --git a/includes/Hooks.php b/includes/Hooks.php index 6d78b3d..644ab19 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -357,6 +357,7 @@ class Hooks { ], "packageFiles" => [ "resources/skins.vector.search/skins.vector.search.js", + "resources/skins.vector.search/instrumentation.js", "resources/skins.vector.search/App.vue", [ "name" => "resources/skins.vector.search/config.json", diff --git a/resources/mediawiki.d.ts b/resources/mediawiki.d.ts index ff25583..90a0d50 100644 --- a/resources/mediawiki.d.ts +++ b/resources/mediawiki.d.ts @@ -4,6 +4,13 @@ interface MwApi { type MwApiConstructor = new( options?: Object ) => MwApi; +interface MwUri { + query: Record; + toString(): string; +} + +type UriConstructor = new( uri: string ) => MwUri; + interface MediaWiki { util: { /** @@ -71,6 +78,18 @@ interface MediaWiki { * @param messageName i18n message name */ msg( messageName: string|null ): string; + + /** + * Track an analytic event. + * + * See https://gerrit.wikimedia.org/g/mediawiki/core/+/d7fe1ff0fe52735b1f41e91879c9617b376e807d/resources/src/mediawiki.base/mediawiki.base.js#375. + * + * @param topic The topic name + * @param [data] The data describing the event + */ + track(topic: string, data?: Record|number|string): void; + + Uri: UriConstructor; } declare const mw: MediaWiki; diff --git a/resources/skins.vector.search/App.vue b/resources/skins.vector.search/App.vue index 2913e14..9964746 100644 --- a/resources/skins.vector.search/App.vue +++ b/resources/skins.vector.search/App.vue @@ -16,17 +16,25 @@ :search-language="language" :show-thumbnail="showThumbnail" :show-description="showDescription" + @fetch-end="instrumentation.onFetchEnd" + @suggestion-click="instrumentation.onSuggestionClick" > + diff --git a/resources/skins.vector.search/instrumentation.js b/resources/skins.vector.search/instrumentation.js new file mode 100644 index 0000000..8cf814d --- /dev/null +++ b/resources/skins.vector.search/instrumentation.js @@ -0,0 +1,114 @@ +/* global FetchEndEvent, SuggestionClickEvent, SubmitEvent */ +/** @module Instrumentation */ + +/** + * The value of the `inputLocation` property of any and all SearchSatisfaction events sent by the + * corresponding instrumentation. + * + * @see https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/skins/Vector/+/refs/heads/master/includes/Constants.php + */ +var INPUT_LOCATION_MOVED = 'header-moved', + wgScript = mw.config.get( 'wgScript' ); + +/** + * @param {FetchEndEvent} event + */ +function onFetchEnd( event ) { + mw.track( 'mediawiki.searchSuggest', { + action: 'impression-results', + numberOfResults: event.numberOfResults, + // resultSetType: '', + // searchId: '', + query: event.query, + inputLocation: INPUT_LOCATION_MOVED + } ); +} + +/** + * @param {SuggestionClickEvent|SubmitEvent} event + */ +function onSuggestionClick( event ) { + mw.track( 'mediawiki.searchSuggest', { + action: 'click-result', + numberOfResults: event.numberOfResults, + index: event.index + } ); +} + +/** + * Generates the value of the `wprov` parameter to be used in the URL of a search result and the + * `wprov` hidden input. + * + * See https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/WikimediaEvents/+/refs/heads/master/modules/ext.wikimediaEvents/searchSatisfaction.js + * and also the top of that file for additional detail about the shape of the parameter. + * + * @param {number} index + * @return {string} + */ +function getWprovFromResultIndex( index ) { + + // If the user hasn't highlighted an autocomplete result. + if ( index === -1 ) { + return 'acrw1'; + } + + return 'acrw1' + index; +} + +/** + * @typedef {Object} SearchResultPartial + * @property {string} title + */ + +/** + * @typedef {Object} GenerateUrlMeta + * @property {number} index + */ + +/** + * Used by the `wvui-typeahead-search` component to generate URLs for the search results. Adds a + * `wprov` paramater to the URL to satisfy the SearchSatisfaction instrumentation. + * + * @see getWprovFromResultIndex + * + * @param {SearchResultPartial|string} suggestion + * @param {GenerateUrlMeta} meta + * @return {string} + */ +function generateUrl( suggestion, meta ) { + var result = new mw.Uri( wgScript ); + + if ( typeof suggestion !== 'string' ) { + suggestion = suggestion.title; + } + + result.query.title = 'Special:Search'; + result.query.suggestion = suggestion; + result.query.wprov = getWprovFromResultIndex( meta.index ); + + return result.toString(); +} + +module.exports = { + listeners: { + onFetchEnd: onFetchEnd, + onSuggestionClick: onSuggestionClick, + + // As of writing (2020/12/08), both the "click-result" and "submit-form" kind of + // mediawiki.searchSuggestion events result in a "click" SearchSatisfaction event being + // logged [0]. However, when processing the "submit-form" kind of mediawiki.searchSuggestion + // event, the SearchSatisfaction instrument will modify the DOM, adding a hidden input + // element, in order to set the appropriate provenance parameter (see [1] for additional + // detail). + // + // In this implementation of the mediawiki.searchSuggestion protocol, we don't want to + // trigger the above behavior as we're using Vue.js, which doesn't expect the DOM to be + // modified underneath it. + // + // [0] https://gerrit.wikimedia.org/g/mediawiki/extensions/WikimediaEvents/+/df97aa9c9407507e8c48827666beeab492fd56a8/modules/ext.wikimediaEvents/searchSatisfaction.js#735 + // [1] https://phabricator.wikimedia.org/T257698#6416826 + onSubmit: onSuggestionClick + }, + getWprovFromResultIndex: getWprovFromResultIndex, + generateUrl: generateUrl +}; diff --git a/resources/skins.vector.search/skins.vector.search.js b/resources/skins.vector.search/skins.vector.search.js index a1b0d21..4b9f40b 100644 --- a/resources/skins.vector.search/skins.vector.search.js +++ b/resources/skins.vector.search/skins.vector.search.js @@ -1,3 +1,4 @@ +/** @module search */ var Vue = require( 'vue' ).default || require( 'vue' ), App = require( './App.vue' ), diff --git a/resources/skins.vector.search/types.js b/resources/skins.vector.search/types.js new file mode 100644 index 0000000..3f058cc --- /dev/null +++ b/resources/skins.vector.search/types.js @@ -0,0 +1,17 @@ +/** + * @typedef {Object} FetchEndEvent + * @property {number} numberOfResults + * @property {string} query + */ + +/** + * @typedef {Object} SuggestionClickEvent + * @property {number} numberOfResults + * @property {number} index + */ + +/** + * @typedef {SuggestionClickEvent} SubmitEvent + */ + +/* exported SuggestionClickEvent, SubmitEvent, FetchEndEvent */