Article feedback/Version 5/Technical Design
This page describes technical designs for the Article Feedback Tool Version 5 (AFT V5).
See also: feature requirements page, project overview page, useful links, as well as data and metrics plan.
Overview
[edit]The front end of the AFTv5 extension builds upon the ArticleFeedback extension: on article pages, a set of javascript modules attach a new div to the bottom of the page and fill it with a feedback form. On submission, the form is replaced by a CTA (call to action). Most of this code has been rewritten to allow for the selection of one of a set of possible forms, and one of a set of possible CTAs. Right now, form options 1, 4, and 6 are available, and CTA 4, 5, and 6 being active (in part depending of whether the user is logged in or out).
The back end is entirely new: rather than displaying aggregate data, the Special page shows a feed of the responses to the article passed in to the URL: /Special:ArticleFeedbackv5 for all feedback, /Special:ArticleFeedbackv5/Page for feedback for a certain page, or /Special:ArticleFeedbackv5/Page/FeedbackId for a specific feedback entry.
Database schema
[edit]More detailed information is available on the schema documentation page
Cache
[edit]Pretty much all data is meant to be read from cache. As soon as feedback is added or updated, the cached values will be updated immediately. Look at inline data/DataModel.php
comments if you're interested in the internals.
Upgrading and Maintenance
[edit]AFTv5 can be upgraded using maintenance/update.php
.
If you are running are running AFTv5 from a different database, however, instead of running maintenance/update.php, you will need to manually apply updates though. SQL update files can be applied using maintenance/sql.php --cluster=name path/to/patch.sql
. Maintenance scripts will read the correct cluster information from your config, so you won't have to explicitly specify the cluster there.
The upgrade hook is located in ArticleFeedbackv5Hooks.php
, in the loadExtensionSchemaUpdates
method. If you are bringing in new schema changes, you need to:
- Create a SQL upgrade file in the
sql
directory - Add a
$updater->addExtensionUpdate
call toloadExtensionSchemaUpdates
indicating what to check for and what file to run if it's missing.
There are two maintenance scripts available:
maintenance/purgeCache.php
will purge all the cached data, forcing all data to be re-fetched from the database.maintenance/archiveFeedback.php
can be temporarily called (e.g. as a cronjob) to archive feedback that has not been moderated for a certain amount of time (as defined by$wgArticleFeedbackv5AutoArchiveEnabled
and$wgArticleFeedbackv5AutoArchiveTtl
)
Other scripts in AFTv5's maintenance folder are update scripts, and are not meant to be run other otherwise.
Startup
[edit]AFTv5 mainly consists of three parts: a frontend form (displayed on pages in the namespaces defined in $wgArticleFeedbackv5Namespaces
), a link to the feedback page (displayed on corresponding talk pages), and the feedback page itself.
Actually, there's a fourth part: there's also a link to the feedback page from a user's watchlist (if there is feedback for his watchlisted pages). That feedback page will only show feedback from pages on that person's watchlist. For scaling reasons, it is not encouraged to enable this on wikis with a large number of pages.
Each of these parts can only appear when the main article falls within the configurable parameters. Below is a list of them, in order of precedence (the higher options will override the lower):
- Not disabled via user preferences (AFT appears if other checks pass)
- Category blacklist (AFTv5 never appears).
- Page protection level: feedback can be enabled or disabled for in /Page?action=protect (fined-grained control for administrators), or from the special page directly via an API call (for editors).
- Category whitelist (AFTv5 appears)
- Lottery (AFTv5 appears if the page ID falls within range allowed)
We also have to make sure that the user's browser is supported.
All of these checks are performed both in JavaScript (when loading the form) and PHP (when submitting feedback). The reason for duplicating the verification checks in JavaScript is cached page outputs. If we were to perform the lottery check in PHP, to see if AFTv5 should by default be enabled on a certain page, this would work just fine. If that check works out, we could initialize our JavaScript. The problem, however, is that the result of this check may be cached. If something like Varnish is used to cache the page output, it will simply respond the output that was previously generated, not process the verification check again.
If at some point, we change the lottery-based check, we'd have to wait until the cached output for this page either expires or gets purged. For this reason, these verification checks are also included in JavaScript. E.g. we can calculate if an article wins the lottery based by the page id, a variable that will not change, will not be affected by any cached output. JavaScript has a reasonably short cache timeout compared to the back end, which only clears cache when the page is edited -- lower-traffic pages could take up to 30 days to clear.
Through PHP, we'll expose all of the required information to JavaScript in ArticleFeedbackv5Hooks.php
, in the beforePageDisplay
method (e.g. for talk pages, which can't just read the relevant article's ID from JavaScript). Caution: this exposed data may be outdated cache!
The relevant information can be found in JavaScript through $.aftUtils.article()
. $.aftUtils.verify( location )
(where location can be "article", "talk" or "special") is called to verify if AFTv5 can be displayed.
- Articles:
- Loads module
ext.articleFeedbackv5.startup
- Loads module
ext.articleFeedbackv5
- Loads module
jquery.articleFeedbackv5
- Loads module
- Loads module
- Loads module
- Talk pages:
- Loads module
ext.articleFeedbackv5.talk
- Loads module
- Special page:
- Loads module
ext.articleFeedbackv5.dashboard
- Loads module
jquery.articleFeedbackv5.special
- Loads module
- Loads module
- Watchlist page:
- Loads module
ext.articleFeedbackv5.watchlist
- Loads module
On the article and talk pages, AFT being not enabled will just stop the relevant JavaScript from loading and the page will appear as normal. On the special page, the entire contents are removed and replaced with an error message saying that AFT is not enabled for the requested page -- or, if it was just the user agent check that failed, the error message will say that.
Frontend
[edit]If startup is successful, the primary module (ext.articleFeedbackv5.js) is loaded. The primary module attaches a new div to the bottom of the page and invokes the jQuery plugin (jquery.articleFeedbackv5.js) on it -- if the user scrolls to the bottom of the page, they'll see the form there; if they click the toolbox link or one of the optional prominent links, they'll be scrolled down to the feedback form. The particular form the user sees, and the presence/location of the optional link, are chosen via bucketing ($wgArticleFeedbackv5DisplayBuckets
).
If the user hits the submit button, it runs an error check and, if there are no issues, invokes an AJAX request. Upon successful return, the form is replaced with a CTA (call to action), which is also chosen via bucketing ($wgArticleFeedbackv5CTABuckets
).
Flow
[edit]- Startup phase (
modules/ext.articleFeedbackv5/ext.articleFeedbackv5.startup.js
)- Determines whether to display the tool (
$.aftUtils.verify( 'article' )
): - If all is well, it loads the main module.
- Determines whether to display the tool (
- Main phase (
modules/ext.articleFeedbackv5/ext.articleFeedbackv5.js
)- Creates a new div, inserts it at the bottom of the article, and invokes the jQuery plugin on it
- Selects a trigger link option and adds it to the page (if any)
- * NB: The click event calls the plugin's open-as-modal event
- Init phase (
modules/jquery.articleFeedbackv5/jquery.articleFeedbackv5.js
, method$.articleFeedbackv5.init
)- Sets some state variables, chooses a display option, and binds the "appear" event to the holder div
- Load phase (
modules/jquery.articleFeedbackv5/jquery.articleFeedbackv5.js
, method$.articleFeedbackv5.load
)- Sets up the bottom of the page and overlay containers, builds the selected form, and puts it in the appropriate container
- Submit phase (
modules/jquery.articleFeedbackv5/jquery.articleFeedbackv5.js
, method$.articleFeedbackv5.submitForm
)- Runs validation, locks the form, and sends off an ajax request
- Submit response phase (
modules/jquery.articleFeedbackv5/jquery.articleFeedbackv5.js
, method$.articleFeedbackv5.submitForm
)- On success, selects a CTA and loads it into the appropriate container, and removes the feedback link(s)
- On error, sets an error state and unlocks the form
Query string options
[edit]You can choose a form and/or a trigger link option and avoid bucketing by passing the following in the url:
URL variable | Possible values | Values active on enwiki | What it does |
---|---|---|---|
aftv5_link |
A , B , C , D , E , F , G , H , or X |
X |
Show a particular trigger link |
aftv5_form |
0 , 1 , 4 , or 6 |
6 |
Show a particular form |
aftv5_cta |
0 , 1 , 2 , 3 , 4 , 5 or 6 |
4 , 5 , or 6 |
Show a particular cta after the form is submitted |
Form Bucketing Process
[edit]Bucketing basically works like this:
- If the plugin is in the debug state, and a form has been requested in the url, and that value is known, select it.
- Call
mw.user.bucket
, which will:- Retrieve a previously selected value (unless already expired) from a cookie, or
- Select a value based on the given (percentage) odds, and save that value to a cookie
- Verify the selected value is valid (e.g. CTA4 should not be displayed to logged-in users), and if not, fallback to a valid value (based on what's enabled via bucketing)
When bucketing config (e.g. percentages) are updated, be sure to update the 'version' number. Otherwise, values previously saved to the cookie will continue to be read.
Currently, bucketing is used for:
$wgArticleFeedbackv5DisplayBuckets
$wgArticleFeedbackv5LinkBuckets
$wgArticleFeedbackv5CTABuckets
: CTA buckets expire immediately. Instead of reading a previously selected value from cookie, one will be re-generated every time. As a result, people may get to see different CTA's each and every time.$wgArticleFeedbackv5Tracking
: This is meant to enable clicktracking only for a select amount of visitors. Although the infrastructure is still there, clicktracking is no longer implemented.
Submit AJAX call
[edit]This call is made when the user submits any of the feedback forms.
Call can be found at:
modules/jquery.articleFeedbackv5/jquery.articleFeedbackv5.js
, method $.articleFeedbackv5.submitForm
Response can be found at:
api/ApiArticleFeedbackv5.php
Parameters:
Name | Type | Description |
---|---|---|
pageid or title |
Integer/String | Page ID/Title to submit feedback for |
revid |
Integer | Revision ID to submit feedback for |
anontoken |
String | Token for anonymous users |
bucket |
String | Which feedback form was shown to the user |
cta |
String | CTA displayed after form submission |
link |
String | Which link the user clicked on to get to the widget |
found |
Boolean | Yes/no feedback answering the question if the page was helpful |
comment |
String | the free-form textual feedback |
Success response:
JSON object with one key, articlefeedbackv5
, containing this object:
Key | Type | Description |
---|---|---|
feedback_id |
Integer | The id of the new row in aft_feedback
|
aft_url |
String | The link to /Special:ArticleFeedbackv5 - for use in CTA5 |
permalink |
String | The parmalink URL to this feedback entry |
Error response:
JSON object with one key, error
, containing the code
and info
for the appropriate error.
Abuse Filtering
[edit]AFT makes use of $wgSpamRegex
, SpamBlacklist, and AbuseFilter. You can turn abuse filtering on by setting $wgArticleFeedbackv5AbuseFiltering
to true. If any of these three are available they will be used to filter out abusive feedback. See flowchart for details.
$wgAbuseFilterEmergencyDisableThreshold['filter']
, $wgAbuseFilterEmergencyDisableCount['filter']
, and $wgAbuseFilterEmergencyDisableAge['filter']
can be set to AFTv5-specific values for AbuseFilter's emergency shutdown. The emergency shutdown mechanism was intended to disable filters that are hit too often (to make sure that a broken filter does not keep blocking edits). Since AFTv5 feedback is very accessible, it will likely solicit more unproductive feedback and as a result, it could make perfect sense for filters to be triggered much more compared to regular page edits.
Actions
[edit]AFT makes use of AbuseFilter's "disallow" and "warn" actions, and it provides custom actions to automatically flag incoming feedback:
Action | Source | Description | Message Key | Implemented |
---|---|---|---|---|
disallow |
AbuseFilter | Prevents the posting of feedback | articlefeedbackv5-error-abuse |
Yes |
warn |
AbuseFilter | Shows a message on your first attempt, but proceeds with other actions on your second attempt | Any key you set on the rule, but abusefilter-warning-feedback is available |
Yes |
aftv5flagabuse |
AFTv5 | Flags the incoming feedback as abuse | none | Yes |
aftv5hide |
AFTv5 | Hides the incoming feedback | none | Yes |
aftv5request |
AFTv5 | Requests oversight on the incoming feedback | none | Yes |
aftv5resolve |
AFTv5 | Marks the incoming feedback as resolved | none | Yes |
Creating Filters
[edit]AFT makes use of AbuseFilter's new "group" feature -- any filters set to "feedback" will be processed; other ones will be ignored.
When you set up filters for AFT, you should:
- Be cautious about the filter name; if feedback offends a filter, the filter name will be displayed in the error message.
- Set "feedback" as the group
- Write conditions appropriate for feedback
- Select one of the action options above
Writing filters is just as risky for feedback as it is for edits. You should read through the AbuseFilter conditions documentation carefully before proceeding.
You will have access to the following variables when writing conditions:
Variable | Description |
---|---|
summary |
constant, "Article Feedback 5" |
action |
constant, "feedback" |
context |
constant, "filter" |
timestamp |
time the feedback was posted |
new_wikitext |
comment text |
new_size |
comment length |
user_editcount |
posting user's edit count |
user_name |
posting user's name |
user_emailconfirm |
posting user's email confirmation timestamp |
user_age |
posting user's account age |
user_groups |
posting user's groups |
article_articleid |
id of the article posted to |
article_namespace |
namespace of the article posted to |
article_text |
title text of the article posted to |
article_prefixedtext |
prefixed title text of the article posted to |
article_restrictions_create , _edit , _move , _upload |
any restrictions on the article posted to (names of groups allowed) |
article_recent_contributors |
last ten editors of the article posted to |
If you're adapting an edit filter to work with feedback, you should change conditions checking added_lines
to use new_wikitext
. You can remove any conditions related to the previous state of the article (e.g., removed_lines
).
You can test these filters just as you would the edit filters, by watching the AbuseFilter log.
Special Page
[edit]Feedback list
[edit]SpecialArticleFeedbackv5.php
will initially load the first batch of feedback entries for the requested parameters: filter & sort, which are read from user preferences/cookie and saved there as soon as a user selects different ones. Upon choosing another filter or sort, or simply loading the next batch of feedback, will result in an AJAX call to ApiViewFeedbackArticleFeedbackv5.php
(from the $.articleFeedbackv5special.loadFeedback
function in jquery.articleFeedbackv5.special.js
), which will respond with a JSON object that (among other values) contains the requested batch of feedback rendered in HTML. This provides for paginated (incremental) display of feedback without the need to reload the entire page.
The $.articleFeedbackv5special.pullHighlight
function pulls the highlighted post(s) separately.
Feedback post actions
[edit]Implementation of actions for the special page revolves around the $.articleFeedbackv5special.actions
array of objects. Each array element represents an action, keyed by the action name, and contains an object with the following structure:
- hasTipsy - true if the action needs a flyover panel
- tipsyHtml - html for the corresponding flyover panel
- click - code to execute after clicking the element (after tipsy has been opened already, if applicable)
- onSuccess - callback to execute after flagging action success. Callback parameters:
- id - respective post id
- data - any data returned by the AJAX call
These actions are tied to elements by adding the class '.articleFeedbackv5-' + action + '-link'
("action" being the name of the key in the JS object). If the action is supposed to open a tipsy, you'll also have to add class '.articleFeedbackv5-tipsy-link'
.
Most actions will result in an API call to ArticleFeedbackv5Flagging, which will call the appropriate method to change the feedback's status. Because opening tipsy panels has been bound to this, some none-flagging related code will use this same structure (like activity, discuss or settings).
The following actions are implemented:
JS object key | Results in ArticleFeedbackv5Flagging |
Description | $wgArticleFeedbackv5RelevanceScoring
|
---|---|---|---|
helpful |
Yes | flag post as helpful | +1
|
undo-helpful |
Yes | undo flag post as helpful | -1
|
unhelpful |
Yes | flag post as unhelpful | -1
|
undo-unhelpful |
Yes | undo flag post as unhelpful | +1
|
flag |
Yes | flag post as abuse | -5
|
unflag |
Yes | undo flag post as abuse | +5
|
feature |
Yes | mark post as useful | +50
|
unfeature |
Yes | undo mark post as useful | -50
|
resolve |
Yes | mark post as resolved | -5
|
unresolve |
Yes | undo mark post as resolved | +5
|
noaction |
Yes | mark post as non-actionable | -5
|
unnoaction |
Yes | undo mark post as non-actionable | +5
|
inappropriate |
Yes | mark post as inappropriate | -50
|
uninappropriate |
Yes | undo mark post as inappropriate | +50
|
hide |
Yes | hide post | -100
|
unhide |
Yes | undo hide post | +100
|
archive |
Yes | archive post | -50
|
unarchive |
Yes | undo archive post | +50
|
request |
Yes | request oversighting of post | +150
|
unrequest |
Yes | undo request oversighting of post | +150
|
oversight |
Yes | oversight post | -750
|
unoversight |
Yes | undo oversight post | +750
|
decline |
Yes | decline request for oversighting of post | +150
|
activity |
No | view activity log | n/a |
activity2 |
No | view activity log in permalink info section | n/a |
discuss |
No | initiate a discussion on user/talk page | n/a |
settings |
No | show an AFTv5 settings menu | n/a |
The actual AJAX call for flagging actions is performed by the $.articleFeedbackv5special.flagFeedback
function. The function locks the flagging actions for its duration to avoid multiple simultaneous requests.
User activity tracking
[edit]For lack of account details to tie the details to, anonymous users' activity on the special page is stored client-side, via cookies. This functionality provides some limitation on actions repetition by the user: if a user has marked feedback as abusive, we'll no longer want to present that action, but only the possibility to undo that flag. This is done using $.articleFeedbackv5special.setActivityFlag
in jquery.articleFeedbackv5.special.js
.
The user's filter & sort state will also be saved to a cookie ($.articleFeedbackv5special.saveFilters
).
Filters
[edit]The feedback page allows the user to see feedback grouped according to a set of filters. These are available according to permissions (e.g., to see hidden or deleted feedback).
Filters are "defined" in ArticleFeedbackv5Model::$lists
in ArticleFeedbackv5Model.php
, which comprises of a key (filter name) => value (filter details) map for all filters. The value is an array which holds the keys "permissions" (which can be any of the in $wgArticleFeedbackv5Permissions
defined permission levels) and "conditions" (which is an array of WHERE-statements to be executed against the DB.)
Example:
// filter name = 'helpful' (i18n messages will be articlefeedbackv5-special-filter-helpful and articlefeedbackv5-special-filter-helpful-watchlist) 'helpful' => array( // everyone with aft-editor permissions can see this list 'permissions' => 'aft-editor', // feedback for this list has to have a comment, not be oversighted, archived or hidden, and be voted helpful at least once 'conditions' => array( 'aft_has_comment = 1', 'aft_oversight = 0', 'aft_archive = 0', 'aft_hide = 0', 'aft_net_helpful > 0' ), ),
To retrieve the feedback for a filter, you'll call ArticleFeedbackv5Model::getList
. The retrieve the amount of feedback in a filter, you'll call ArticleFeedbackv5Model::getCount
. Both will take $name
as first argument, which is the name of the filter. $shard
is the second argument: this is the ID of the page you want to fetch feedback (or the amount of feedback) for, or null for feedback for all pages.
ArticleFeedbackv5Model::getList
will return a DataModelList, which is a ResultWrapper. To fetch the next batch of feedback, you'll provide ArticleFeedbackv5Model::getList
with the result of the DataModelList::nextOffset
: this the value to indicate where the next batch should start.
Example:
// will return a DataModelList of 50 helpful feedback posts, for page with ID 1 $feedback1 = ArticleFeedbackv5Model::getList( 'helpful', 1 ); // will return a DataModelList of the next 50 helpful feedback posts, for page with ID 1 $feedback2 = ArticleFeedbackv5Model::getList( 'helpful', 1, $feedback1->nextOffset() ); // feedback can be iterated over foreach ( $feedback1 as $post ) { /* ... */ } // will return the amount of helpful feedback posts, for page with ID 1 $count = ArticleFeedbackv5Model::getCount( 'helpful', 1 );
Logging
[edit]More detailed information is available on the schema documentation page
Central activity log (https://en.wikipedia.org/w/index.php?title=Special%3ALog&type=articlefeedbackv5) calls upon class ArticleFeedbackv5LogFormatter
to format the AFTv5 entries. This class extends from the default LogFormatter class and adds in some additional details to the entry (feedback text, page title & link, feedback n° & link)
The actions being logged are defined in ArticleFeedbackv5Activity::$actions
, which is a key (action name) => value (action details) array. The action details is an array with keys:
- permissions: Not all actions can be performed by all users; e.g. not everyone should be able to oversight feedback. This can be any of the permissions defined in
$wgArticleFeedbackv5Permissions
- sentiment: (positive|negative|neutral) depending on the sentiment, they'll be depicted on a different color in the activity log (green|red|gray)
- log_type: the value for log_type in logging table. This will usually be articlefeedbackv5, but for more sensitive information (oversight-related) we'll add them to the suppression log.
The logging of all activity happens through a function call to ApiArticleFeedbackv5Utils::logActivity (ApiArticleFeedbackv5Utils.php) which will both insert the log entry, and increment the activity count on an entry (joining with the logging table is complex and not desired on such huge dataset). FIXME: this sentence is outdated, there's no such method as of April 2024.
The "view activity" link in the feedback page toolbox (both on central feedback page & permalink) also fetches its data from this logging table.
ArticleFeedbackv5Activity::log
is the wrapper method to call when inserting log entries. This method performs some additional checks and will build the final data to be inserted.
Metrics / Clicktracking
[edit]- Note: click tracking support was removed from AFTv5 back in 2017, see phab:T160801.
AFTv5 used to use the Extension:ClickTracking extension to track how the extension is being used. The groundwork in AFTv5 is still there, but support for ClickTracking has been removed.
If at some point one would like to track events (e.g. Extension:EventLogging), calls could be added in function $.aftTrack.track
in jquery.articleFeedbackv5.track.js
and trackEvent
in ArticleFeedbackv5Hooks.php
. Or perhaps EventLogging is conceptually very different from ClickTracking in that it doesn't even make sense to use the groundwork ClickTracking used, I don't know.
Cookies
[edit]Below is a full list of cookies currently in the code. Not all of them are used though, but in that case, it's indicated below.
Unless indicated otherwise, the cookies are only used in JavaScript, not processed serverside.
Article page
[edit]AFTv5-feedback-ids This will store the ids of the last 20 feedbacks submitted by a user. This is mostly used to hide moderation tools, to discourage people to upvote their own posts. This cookie can also be used server-side in case a user submits anonymous and subsequently logs in via the âregisterâ or âloginâ links in CTA4 (call to action number 4). After logging in, we'll âclaimâ the feedback to the user. This is usually done by passing the id as GET-parameter, but if multiple steps are needed to login (wrong password), the data from the url is lost, and we'll look at the last id in this cookie.
AFTv5-submission_timestamps Will save the timestamps of submitted feedback, used to show a throttling message if a user posts too much feedback too fast (20 feedback per hour)
clicktracking-session Identifier to be able to track how people use the tool. ClickTracking is no longer enabled though, so this cookie is not being set.
mediaWiki.user.bucket:ext.articleFeedbackv5@<version>-form AFT has multiple form variants. MW's core bucketing is used to select the active form from the percentages defined in $wgArticleFeedbackv5DisplayBuckets & saves the result to cookie to make sure that people will consistently see the same form (although for WMF's sites, only the 2-step form 6 is currently active)
mediaWiki.user.bucket:ext.articleFeedbackv5@<version>-links AFT has multiple links. MW's core bucketing is used to select the active link from the percentages defined in $wgArticleFeedbackv5LinkBuckets & saves the result to cookie to make sure that people will consistently see the same link (although for WMF's sites, the only active link currently is the link in the toolbox)
mediaWiki.user.bucket:ext.articleFeedbackv5@<version>-cta AFT has multiple CTAs (call to actions). MW's core bucketing is used to select the active CTA from the percentages defined in $wgArticleFeedbackv5CTABuckets; the cookie expires immediately though (we actually do want CTAs to rotate, instead of consistently displaying the same CTA)
mediaWiki.user.bucket:ext.articleFeedbackv5@<version>-tracking When gathering usage metrics, only a certain percentage of visits were tracked. MW's core bucketing was used to select if a certain visitor would or would not get tracked (based on percentages defined in $wgArticleFeedbackv5Tracking). ClickTracking is no longer enabled though, so this cookie is not being set.
Special page
[edit]AFTv5-last-filter Will save the last selected filter/sort, so that when someone comes back later, he'll see that same filter instead of the default. This cookie will be used server-side to load the previously selected filter & sort.
AFTv5-aft-activity This will store the last 100 actions (helpful, unhelpful, flag, request) performed on feedback by the user, so reflect the correct status when browsing feedback.
mediaWiki.user.bucket:ext.articleFeedbackv5@<version>-tracking Same cookie as already mentioned for article page. Unused now.