[search] Instrument Vue.js-based search widget

Add event listeners and associated helpers to emit SearchSatisfaction
events via the `mediawiki.searchSuggest` protocol.

Bug: T257698
Change-Id: Ica040cd18d6c4bf8a1b1f607bb4647c7e8eb7108
This commit is contained in:
Sam Smith 2020-11-24 22:16:03 +00:00 committed by Jdlrobson
parent a52da8ddaa
commit aa10668e6d
6 changed files with 181 additions and 1 deletions

View File

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

View File

@ -4,6 +4,13 @@ interface MwApi {
type MwApiConstructor = new( options?: Object ) => MwApi;
interface MwUri {
query: Record<string, unknown>;
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<string, unknown>|number|string): void;
Uri: UriConstructor;
}
declare const mw: MediaWiki;

View File

@ -16,17 +16,25 @@
:search-language="language"
:show-thumbnail="showThumbnail"
:show-description="showDescription"
@fetch-end="instrumentation.onFetchEnd"
@suggestion-click="instrumentation.onSuggestionClick"
>
<input type="hidden"
name="title"
value="Special:Search"
>
<input type="hidden"
name="wprov"
:value="wprov"
>
</wvui-typeahead-search>
</div>
</template>
<script>
var wvui = require( 'wvui' );
/* global SubmitEvent */
var wvui = require( 'wvui' ),
instrumentation = require( './instrumentation.js' );
module.exports = {
name: 'App',
@ -91,6 +99,26 @@ module.exports = {
type: Boolean,
default: true
}
},
data: function () {
return {
// -1 here is the default "active suggestion index" defined in the
// `wvui-typeahead-search` component (see
// https://gerrit.wikimedia.org/r/plugins/gitiles/wvui/+/c7af5d6d091ffb3beb4fd2723fdf50dc6bb2789b/src/components/typeahead-search/TypeaheadSearch.vue#167).
wprov: instrumentation.getWprovFromResultIndex( -1 ),
instrumentation: instrumentation.listeners
};
},
methods: {
/**
* @param {SubmitEvent} event
*/
onSubmit: function ( event ) {
this.wprov = instrumentation.getWprovFromResultIndex( event.index );
instrumentation.listeners.onSubmit( event );
}
}
};
</script>

View File

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

View File

@ -1,3 +1,4 @@
/** @module search */
var
Vue = require( 'vue' ).default || require( 'vue' ),
App = require( './App.vue' ),

View File

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