How I Fixed Amazon's Default Sorting Behavior

How I Fixed Amazon's Default Sorting Behavior

Prerequisites, Development, Distribution, Feedback, Source Code

Introduction

Have you ever struggled with Amazon fixating on its default featured sorting option the moment you search anything new?

featured sort.png

Many people switch their sorting to Average Customer Review. I know I do. I know my friends who go with Price: Low to High. But we have to change sorting for every single search. And that's not right!

Well, I would like to introduce you to a browser extension - Default Amazon Sort that I created to help our friend Amazon do a better job at understanding their users' default sorting needs.

Let me take you on the development journey.

Prerequisites

  1. Knowledge of basic frontend development
  2. Chrome Web Store Developer License

Development

Package Structure

Package
   | manifest.json (MUST)
   | background script (MUST)
   | foreground script
   | popup page
   | options page
   | _locales
        | [language_code]
           | messages.json

1. manifest.json

This is the entry point to any extension. It contains details about the extension package like name, description, icons, permissions, background scripts, HTML for a popup on clicking the extension, etc. Chrome (or now even Chromium-based Edge) reads this file and loads all other related files to build pages on click and perform any DOM manipulations mentioned in those files.

Below is the manifest.json for my extension. I seek permissions only for the Amazon websites for their marketplaces, checking for activeTab and Chrome's storage.

{
  "name": "__MSG_appName__",
  "short_name": "DAS",
  "version": "2.2.2",
  "description": "__MSG_appDesc__",
  "permissions": ["activeTab", "declarativeContent", "storage", "https://www.amazon.cn/*", "https://www.amazon.in/*", "https://www.amazon.co.jp/*", "https://www.amazon.com.sg/*", "https://www.amazon.com.tr/*", "https://www.amazon.ae/*", "https://www.amazon.fr/*", "https://www.amazon.de/*", "https://www.amazon.it/*", "https://www.amazon.nl/*", "https://www.amazon.es/*", "https://www.amazon.uk/*", "https://www.amazon.ca/*", "https://www.amazon.com.mx/*", "https://www.amazon.com/*", "https://www.amazon.com.au/*", "https://www.amazon.com.br/*"],
  "background": {
    "scripts": ["background.js"],
    "persistent": false
  },
  "page_action": {
    "default_popup": "popup.html",
    "default_icon": {
      "128": "images/das128.png"
    }
  },
  "icons": {
    "128": "images/das128.png"
  },
  "manifest_version": 2,
  "default_locale": "en"
}

2. background script

The background script (typically named background.js) holds one-time background processing code when an extension is installed or updated. Think of it as an environment setter for the extension on your browser. Theoretically, you can have an extension with just the manifest.json. But since it would not be able to do anything, there is no practicality to it. Hence, I say having a background script is a bare minimum.

P.S. No DOM manipulation can be performed here as it is called only once to set a few rules and is not active during any user interaction

I inspected all of Amazon's websites to understand the commonality and the terms used for their respective sorting options. I created a map of all those sorting options. I also keep a global variable to check for PrimeOnly filter if that's something the user wants. And another variable to map with a manual switch to turn of the extension.

// Event listener for when the extension is installed
// We can set global variables as JSON, int, string or boolean values in its callback
chrome.runtime.onInstalled.addListener(function() {
  // Initializing a global variable "sort" as 'customerReview' to initialize default sorting option as Average Customer Review when the extension is installed
  chrome.storage.sync.set({sort: 'customerReview'}, function() {
    console.log('Default sort is set to Avg. Customer Review');
  });
  // Creating an enum of sorting options
  chrome.storage.sync.set({order: {featured: 0, lowToHigh: 1, highToLow: 2, customerReview: 3, newestArrivals: 4}}, function() {
    console.log('Sort order dictionary');
  });
  // Creating a map for above values with their respective terms used in Amazon's webpages 
  chrome.storage.sync.set({sortOrderMap: {featured: 'relevanceblender', lowToHigh: 'price-asc-rank', highToLow: 'price-desc-rank', customerReview: 'review-rank', newestArrivals: 'date-desc-rank'}}, function () {
    console.log('Sort Order Map built')
  })
  // Setting initial value for enabling PrimeOnly filter as true
  chrome.storage.sync.set({primeOnlyEnabled: true}, function() {
    console.log(`primeOnly is set to true`);
  })
  // Variable for manual toggling of extension
  chrome.storage.sync.set({extensionEnabled: true}, function() {
    console.log(`extensionEnabled is set to true`);
  })
  chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
    chrome.declarativeContent.onPageChanged.addRules([{
      conditions: [new chrome.declarativeContent.PageStateMatcher({
        pageUrl: {},
      })],
      actions: [new chrome.declarativeContent.ShowPageAction()]
    }]);
  });
});

// EXAMPLE event listener for when the tabs are changed/updated or on a new window
chrome.tabs.onUpdated.addListener(function(tabId, info, tab) {
  // Passing a script "content.js" to be executed. This is the foreground script
    chrome.tabs.executeScript(
        tabId,
        {file: 'content.js'},
        () => chrome.runtime.lastError);
});

3. foreground script

This is your principal DOM manipulator resource. You call this guy when you need to interfere with a webpage's DOM elements. All the document.getElementByName() or document.querySelectorAll(), etc help finding DOM elements of the active tab. You can also make use of the global variables set in background storage for setting conditions if you need them.

I check for user's default selections and act on active amazon websites based on them

// Fetching "extensionEnabled" variable value and checking it in the callback function  whether the extension is not disabled by the user
chrome.storage.sync.get('extensionEnabled', function (data) {
    // checking state of extensionEnabled and the URL pathname
    if (data.extensionEnabled == true && window.location.pathname == '/s') {
        chrome.storage.sync.get('primeOnlyEnabled', function (data) {
            // subsequent actions
            if (document.querySelectorAll('[type= checkbox]')[0].checked != data.primeOnlyEnabled) {
                document.querySelectorAll('[type= checkbox]')[0].click();
            }
        })

        chrome.storage.sync.get('order', function (order) {
            chrome.storage.sync.get('sortOrderMap', function (sortOrderMap) {
                // getting user's selected default sorting behaviour
                chrome.storage.sync.get('sort', function (data) {
                    // DOM operations to open sorting pulldown menu and select user's default sorting option
                    if (document.getElementsByTagName('SELECT')[1].value != sortOrderMap.sortOrderMap[data.sort]) {
                        document.getElementsByTagName('SELECT')[1].click();
                        window.setTimeout(function() {
                            document.getElementsByTagName('UL')[document.getElementsByTagName('UL').length - 1].children[order.order[data.sort]].children[0].click();
                        }, 500)
                    }
                })
            })
        })
    }
})

4. Popup page

This is the look of a popup (think of it as a modal) window that shows up when you click an extension. This is the Point of Interaction for the user with our extension. It can range from a complex website to a simple html with a couple of buttons

I provide a simple layout consisting of a switch for enabling/disabling the extension and for setting the PrimeOnly filter AND a pulldown menu for the user to select their default sorting behavior. I then use those user interactions to update our global variables.

// Getting user's choice of default sorting behavior
let changeSort = document.getElementById('changeSort');

// Updating global variable "sort" with the above value
chrome.storage.sync.get('order', function(data) {
    sortDictionary = data.order;
    chrome.storage.sync.get('sort', function(data) {
        changeSort.getElementsByTagName('option')[sortDictionary[data.sort]].selected = 'selected';
    });
})

And here is how the popup looks when a user clicks my extension

popup ui.png

5. options page

This is an extra optional page for the extension developer if they want a full tab for the user to interact with their extension. I didn't need one. So, I went ahead without it.

You can access the "Options" page by right-clicking on the extension

extension right click.png

6. locales

This is the internationalization/localization of our extension section. The way to do this is to save relevant tag names inside message.json in folders named as localization codes such as en, ja, hi, cn, etc. We can fetch these options from the code chrome.i18n.getMessage('<localizationVariableName>') and set it to appropriate html elements in popup.js.

This is how the localization file for Japanese looks

{
    "appName": {
        "message": "アマゾンでデフォルトの並べ順"
    },
    "appDesc": {
        "message": "アマゾンで商品を検索した結果のデフォルトの並べ順を変更できるようなエクステンションです。"
    },
    "changeSortLabel": {
        "message": "デフォルトの並べ順"
    },
    .........
    .........
    .........
    "primeOnlyLabel": {
        "message": "Prime"
    },
    "switchLabel": {
        "message": "オフ・オン"
    }
}

Distribution

Navigate to Google Developer Dashboard and click on + New Item button on the right

chrome webstore dashboard add new item.png

Wrap the extension in a zip and upload it on the console.

Fill in the name and description of the extension, choose appropriate thumbnails and videos as part of extension publication on the webstore. Answer questions on the relevant target audience to help Chrome better publish the extension. And SUBMIT.

Wait for a couple of days for the Google Chrome team to review your code and the final publication.

Feedback

My sole purpose for this extension was to help me reduce redundant automatable actions for myself. But it gives me immense pleasure that this extension could reach so many people.

I would like to thank Jake Moore for his suggestion that led me to include the "PrimeOnly" option. I had initially added the default sorting behavior only.

primeOnly review.png

This review made my day

best review.png

Source Code

I have open-sourced my code for this extension for everyone. You can access them below.