Reading/Web/Preference Persistence For Anonymous Users/Prototype Summary (Deprecated)

From mediawiki.org

This Prototype is deprecated[edit]

We moved on to Reading/Web/Projects/Preference persistence prototype 2 summary

This page is kept as an archive of explored solutions

Current status[edit]

Through the previous discussions mentioned above, we’ve gained consensus on the problem space and agree that persisting certain preferences for logged-out users is worth pursuing. During the prototype phase, The Web Team has produced various prototypes and arrived at a draft design spec for this feature.  

Technical design (draft)[edit]

The client preferences feature can be broken down into three separate components. One for reading and setting the preferences on page-load, one for performing CRUD operations on the client preferences, and one for registering client preferences on the server-side to prevent misuse.

ClientPreferences PreferenceLoader[edit]

An inline JS script that’s loaded in the head (or somewhere near the top) of the HTML document, which reads the client preferences values from localStorage and applies those values to the class attribute on the body element.

For safety & security, this inline script will not apply any new classes to the DOM. Instead, it will only modify existing class names to reflect values stored in localStorage. This will ensure that gadgets or malicious scripts will not be able to insert arbitrary class names into the DOM on page load.

E.g.

The script will changes the DOM from

<html class=”mw-client-pref_dark-mode_0”>

To

<html class=”mw-client-pref_dark-mode_1”>

Which will enable/disable dark-mode by modifying a class-name, but the script won’t add or remove the class-name itself.  

Requirements

  • Performance - this script interrupts the browser rendering cycle by forcing synchronous javascript execution on page load. It should be as minimal and performant as possible.
  • Fault tolerance - due to being loaded inline, the script will execute on Grade C browsers so it should be written defensively.
  • Safety - Gadgets or user-scripts shouldn’t be able to add or remove class-names from the DOM on page-load.

Prototypes (live in production)

ClientPreferences Class[edit]

This class is the public API for ClientPreferences.

It should contain all of the necessary business logic for interacting with ClientPreferences. It should be a Javascript module, available from MediaWiki core and accessible via ResourceLoader to all skins and extensions.

This class is responsible for getting/adding/removing ClientPreferences. It should interact with the storage layer safely and ensure data integrity (i.e. edge-cases and data validation).

It should also change the values in the HTML when the localStorage values are updated.

Requirements

  • Availability - Written as a ResourceLoader JS module in MediaWiki core.
  • Logic - Perform CRUD operations on ClientPreferences values.
  • Storage - Ensure safe storage of client Preferences data.

Prototypes

ClientPreferences Registry[edit]

This is a back-end (PHP) module in MediaWiki Core that registers and validates client preferences. For security considerations, ClientPreferences should only be available to skins or extensions, not gadgets or user-scripts. To facilitate this, ClientPreferences will provide a registration mechanism via a key in skin.json or extension.json where skins or extensions can declare new client preferences.

The values set in the ClientPreferences key will be appended to the DOM as class-names on the HTML element. These class names will represent the default values of the preference, which will be updated via the inline script on page load if it changes.  

e.g.

In skin.json (or extension.json) the following will declare a new client preference named ‘vector-limited-width’ with the default value of 1 (true).

clientPreferencesAllowList = { 'vector-limited-width': 1 };

That will append the following class to the HTML element:

<html class=”vector-limited-width_1”>

Then if the user will select a different value for this preference, that value will be saved in localStorage, then on page-load, the inline script will change that class-name as such (and the feature will be disabled):

<html class=”vector-limited-width_0”>

Requirements

  • Performance - Limit the amount of preferences in order to reduce the performance impact on page load.

Prototypes

Prototype findings[edit]

Performance[edit]

Cookies

In T331681 - Measure performance of cookie-based anonymous client preferences we measured the performance impact of the existing cookie-based storage implementation via a SpeedTest instance comparing before and after the cookie has been set.

Using the Barack Obama page as test-case, the finds show that the inline script, accessing the cookie and writing the body CSS class on page load had the following performance impact:

  • First contentful paint: 1ms slower.
  • Long tasks before first paint: +1 addition (3 vs 4)
  • Total transfer time: +1kb (449kb vs 450kb)
  • Last visual change: no difference
  • Total blocking time: no difference

LocalStorage

In Profile Performance of LocalStorage-based and client-side cookie-based User Preference Storage we did performance testing of this feature using localStorage instead of cookies as the storage mechanism.

Findings based on local profiling on Macbook Pro with 6x CPU throttling :

  • Cookies approach - Total task time: 9.83 ms
  • LocalStorage approach - Total task time: 11.53 ms

Analysis:

The differences in page-load times observed between the cookie and localStorage approaches were negligible in our testing. Although, as Peter mentions in this comment, the performance impact of this feature (either cookie or localStorage) is difficult to measure because the delay that it causes early on in the rendering cycle can have downstream effects.

LocalStorage might have a slightly more negative impact on page-load performance, however, the value of these preferences might get quite long. Let’s say we have as many as 7 preferences, the storage value might then look like this:  

dark-mode-enabled vector-full-width-disabled vector-font-size-110 vector-toc-collapsed vector-main-menu-expanded vector-page-tools-menu-expanded minerva-sections-expanded-enabled

If we were to use a cookie, we would be sending that value with every request. That in turn, would have a negative performance impact on every HTTP request for all logged-out users.

Based on these findings, combined with the privacy impact of potentially fingerprinting users based on these cookie values, the Web team is leaning towards using localStorage as the persistence layer for this feature.

Safety and Security[edit]

Early on in this process, we realized that due to performance impact, we should limit this feature to trusted extensions and skins. Letting gadgets or user scripts write to the DOM during page-load would pose safety and security risks. Therefore, creating a server-side allow-list will be a requirement for this feature.

PHP ClientPreferences Registry prototype

When prototyping the ClientPreferences Class, we realized that we should use the existing mediawiki.storage wrapper to safely write to localStorage (which can be full, unavailable or undefined in certain situations – all covered by the mw.storage API). Therefore, mediawiki.storage would be a hard dependency for this class, and this class should receive it as an input.

E.g.

mw.clientPreferences = new ClientPreferences( mw.storage );

Storage[edit]

ClientPreference values should be stored as a string in localStorage under a single key in order to minimize synchronous reads during page load.

Storage structure (working draft)[edit]

LocalStorage key:  

mw-client-preferences

LocalStorage value:

e.g. dark-mode

dark-mode_1

e.g. dark-mode, font-size, collapsed menu and hidden ToC

dark-mode_1 | vector-2022_font-size_120 | vector-2022_main-menu-collapsed_1 | toc-hidden_1

Storage value naming convention

  • Lower-case, snake-cased to conform with CSS conventions.
  • `vector` - (optional) preference repository source, i.e. skin or extension name. If omitted, then the preference is assumed to come from MediaWiki core.
  • `dark-mode` - preference name
  • `1` - preference value. Must be an integer. Arrays, objects or string are not permitted. Decimals are not permitted because of incompatibility with CSS class names.
    • ‘enabled’ = 1
    • ‘disabled’ = 0
    • ‘110’ = 110
  • `|`pipe separator. Used for splitting and parsing the values in JS.

Requirements

  • Consistency - The storage values should have consistent identifiers to reveal the preference source, name and state.

Public API[edit]

These are draft ideas as to what the public facing API of ClientPreferences can look like, and their possible outputs, based on two prototypes in T337411


mw.clientPreferences.get()

=>  [‘dark-mode’, ‘vector-full-width’, ‘font-size-110’] ?

=>  {‘dark-mode’:true , ‘vector-full-width’:false, ‘font-size’: 110} ?

mw.clientPreferences.set( dark-mode’, false )

=> true (true on success? false on error? throw on error?)

mw.clientPreferences.set( ‘font-size’, 110 )

=> true? Preference value?

mw.clientPreferences.get( ‘font-size’)

=> 110? ‘110’? {‘font-size’: 110} ?

Based on previous POC patches and the prototypes we have the current POC is covered in these patches gerrit/core/932819 and gerrit/Vector/933499  .

Multivalued preference[edit]

For now each multivalued preference will be assigned a binary key vector-feature-${featureName}-${featureValue}-${featureState} for each value vs vector-feature-${featureName}-${featureState} for normal binary values

while using preferences save on multiple values we should make sure to set all other values to 0/false.

The createRadioToggleRow() function from clientPreferences.js in patch https://gerrit.wikimedia.org/r/c/mediawiki/skins/Vector/+/933499  shows the example.

If instead of small, medium, and large values we have numeric ones it will act the same.

In the end one of the multi-select values will be true and the others will be false.

and the appropriate class will be enabled and the others would be disabled. then the css will take over

Inline script[edit]