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.


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]

AFTv5 Schema

More detailed information is available on the schema documentation page


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 ArticleFeedbackv5.hooks.php, in the loadExtensionSchemaUpdates method. If you are bringing in new schema changes, you need to:

  1. Create a SQL upgrade file in the sql directory
  2. Add a $updater->addExtensionUpdate call to loadExtensionSchemaUpdates 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.


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 ArticleFeedbackv5.hooks.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
  • Talk pages:
    • Loads module
  • Special page:
    • Loads module ext.articleFeedbackv5.dashboard
      • Loads module jquery.articleFeedbackv5.special
  • Watchlist page:
    • Loads module ext.articleFeedbackv5.watchlist

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.


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


  1. 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.
  2. 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
  3. 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
  4. 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
  5. Submit phase (modules/jquery.articleFeedbackv5/jquery.articleFeedbackv5.js, method $.articleFeedbackv5.submitForm)
    Runs validation, locks the form, and sends off an ajax request
  6. 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 mechanism flowchart

Bucketing basically works like this:

  1. If the plugin is in the debug state, and a form has been requested in the url, and that value is known, select it.
  2. Call mw.user.bucket, which will:
    1. Retrieve a previously selected value (unless already expired) from a cookie, or
    2. Select a value based on the given (percentage) odds, and save that value to a cookie
  3. 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:

  1. $wgArticleFeedbackv5DisplayBuckets
  2. $wgArticleFeedbackv5LinkBuckets
  3. $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.
  4. $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:


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]

AFTv5 abuse filtering

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.


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:

  1. Be cautious about the filter name; if feedback offends a filter, the filter name will be displayed in the error message.
  2. Set "feedback" as the group
  3. Write conditions appropriate for feedback
  4. 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).


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 ArticleFeedbackv5.model.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.)


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


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


More detailed information is available on the schema documentation page

Central activity log ( 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).

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]

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 ArticleFeedbackv5.hooks.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.


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.