Reading:
Of PlayStations and Pandemics: Designing A Bot To Monitor Product Availability
Share:
Selenium 4 introduces relative locators
Selenium 4 introduces relative locators. This new feature allows the user to locate an object in relation to another object on the screen! Don't wait, get an instant demo today.

Of PlayStations and Pandemics: Designing A Bot To Monitor Product Availability

Learn how Del Dewar built a PS5 stock bot and the lessons he learnt about test automation

Content in review

… And Practicing Solid Automation Test Coding Principles While You Do It

By Del Dewar

The Problem I Wanted To Solve

Sony launched their much-anticipated PlayStation 5 (PS5 hereafter) gaming console in November 2020, almost exactly midway through the waves of COVID-19 lockdowns around the world. So unless you had placed a pre-order a few months earlier in September, you would have found it incredibly difficult to find retailers who had any consoles in stock and available for sale.

Scour the news and you’ll find stories of  unscrupulous individuals, known as ‘scalpers’ who use some kind of automation (commonly referred to as ‘bots’ by the media) to monitor PS5 inventory across several retailers 24 X 7. The bots then make multiple (automated) purchases the moment any consoles hit the virtual shelves, leaving regular customers frustrated and empty-handed. These scalpers then sell those consoles on eBay for ludicrously inflated prices, sometimes for even three or four times the original retail price, exploiting the desperation of others and obliging their own greed.

It quickly became clear that if I wanted to get my hands on a PS5, I was going to have to do something more than browse my favourite retailers and leisurely hit the refresh button every so often. I had no intention of becoming a scalper, but surely, I could leverage my testing and automations skills to put together something that would give me a fighting chance of getting my hands on that elusive prize. So I set about putting together a design for my very own bot.

What Is A Bot, Really?

The term ‘bot’ has a bad reputation these days, particularly when you’ve read the inevitable news articles about unscrupulous scalpers who use their singing, dancing bots to snap all the PS5 consoles on the market, denying ordinary users the chance to get their hands on the much sought-after tech.

With that in mind, I want to define ‘bot’ precisely for purposes of this article.

Image 1: Good Bot/Bad Bot

I’m not talking about building a bot with the intention of taking advantage of loopholes or automating multiple purchases so you can become an unethical scalper yourself.

Rather, I am talking about a passive inventory monitoring bot that will give you an audible alert when it finds that one of your preferred retailers is showing the PS5 consoles as in stock. The alert gives you the essential advantage of time to stake your claim before the masses (and the scalpers) descend on the same site.

Trying and Failing To Purchase A PS5 With A Livestreamed Bot Alert

If you go to Twitter and type “PS5 Inventory” or “PS5 Stock UK” into the search bar, it’s hard to see the wood for the trees sometimes. Many people mean well, but when you’re 30 minutes into a two-hour livestream entitled ‘Watch me bag a PS5’ that involves nothing more than them manually and painfully refreshing the retailer’s page again and again, laced with insightful commentary such as ‘Nope, still out of stock,’ you begin to question your own choices.

Look a little closer, however, and you’ll spot some better sources of information that reliably predict inventory drops with uncanny accuracy, including volume of units, dates and even time frames on certain occasions.

One such reliable source of information proved to be from an account called Ps5Instant.  The account owner very helpfully livestreamed their own PS5 inventory monitoring bot on their sibling YouTube channel so that others could have a chance at getting a PS5. The bot itself continually cycled through a large selection of retailers in the UK and printed out the status on a terminal window. When any retailer had a PS5 in stock, the bot played a World War 2 air-raid siren for all to hear.

‘Marvellous,’ I thought to myself, ‘I don’t need to write a bot. I’ll just join the people watching the channel and then bag myself a PS5 along with everyone else!’

 Image 2: Attempt 1 #fail

I quickly gave up on that notion after I tried the livestream method once:

  • Game UK, a UK games retailer, were rumoured to be receiving a complement of PS5 consoles. 

  • These consoles would become available to purchase on their website at some point during the day, although the exact time was unknown.

  • The point at which the stock becomes available to purchase online is commonly known as a ‘stock drop.’ My bot would have to monitor for stock drops. 

  • I had the Ps5Instant livestream running in the background.

  • There were thousands of people watching.

  • I didn’t hear the stock-alert siren sound (I had inadvertently muted the audio). So I joined the Game queuing system late.

  • I was in the queue for about two hours.

  • The stock of PS5 units quickly sold out.

  • No PS5 for me.

Birth Of My Own Bot

Although I’d given up on the livestreamed bot alert, I was still curious about what it would take to build a rudimentary bot. I knew that any bot I would write would need to do a similar job, perhaps a tad faster and in a more precise way.

After all, the singing, dancing bot was checking an awful lot of retailers (Game, Currys, Argos, Amazon, and several more) regardless of where the proposed stock drop was rumoured to be.

Perhaps I could steal an advantage if I focused on one retailer instead of a dozen?

My Bot’s Requirements: Using Good Automated Test Design Principles 

When I’m coding something, I tend to spend a lot of time iterating over chunks of code, verifying small details, until I get it to a standard that I’m happy with. I knew I would have to suppress that impulse for this project, otherwise the PlayStation 6 would be hitting the shelves before I completed any code to my level of satisfaction.

I wanted the bot to be targeted, not just in terms of retailer but also in terms of the insertion point(s). Would the bot operate within a browser or could we step it down a level?

I wanted the bot to be reliable: it couldn’t flake out due to something not responding or something not being found. If there was an unavoidable error condition, I’d want to report it concisely and move on.

I wanted the bot to be informative. It had to be clear which retailer I’d be checking even though there would be only one. And I wanted the bot to report the version of the console it found:  one version of the console comes with a Blu-ray optical drive, and the other does not. Finally, the bot would have to report the status of said product at said retailer.

I wanted the bot to be maintainable. The likelihood of having to change things often was a real possibility as I learned more about how the site worked. So the design would have to be simple, not over-engineered.

I wanted the bot to be speedy. The less bloated the code was, the better chance it would run faster and more efficiently. At least that’s what I hoped.

The eagle-eyed among you may recognise the words highlighted above. They come from the TRIMS mnemonic in the Automation in Testing namespace as a guide for writing automation that supports your testing. Well, as it happens it’s also a great guide for writing makeshift bots. No, really…..

Choosing A Retailer To Monitor

As I hinted earlier, I wanted an advantage over the Ps5Instant bot. The easiest way to do that appeared to be to focus on one retailer as opposed to many. With that in mind, I opted for Game UK as they seemed to have had relatively regular stock drops, and unlike some other retailers, didn’t drop stock at 3 a.m. to prevent their website from crashing (Yep, I’m looking at you, Argos.) They are the high-street (‘high-end’ for you US folks) retailer most people would think about when looking for anything to do with console gaming.

Choosing The Point Of Entry

From what I could see in the Game UK website, the browser level would be the easiest point of entry for a bot. The PS5 console page looked like this at the time I wrote this article:

Image 3: Game’s PS5 Page

And the consoles were three-quarters of the way down the page, displaying seemingly permanent ‘Out of stock’ indicators.

Image 4: Consoles

The obvious element to check would be those buttons: more specifically, the labels within. So I decided to start there.

Choosing A Tech Stack

Python is probably my go-to language for putting something together quickly, so along with the Selenium bindings, this seemed like a logical starting point. I did, however, have half an eye on scrapy as I thought a spider might be easier to put together more quickly. So, knocked together a quick spider class to inspect the PS5 page.

The result? Nothing. Nada. Niets.

The reason? This is how scrapy saw the page:

Image 5: Scrapy's default view of the page

Because all the meaningful content was rendered with JavaScript, scrapy didn’t see any of it. There are of course middleware components such as scrapy-selenium to handle such pages. But it would take time to bring those in and update my code. So I honestly couldn’t see any advantage to using scrapy over the regular Python-Selenium browser route.

Navigating A Page Without Clearly Identifiable Elements

If you care to look at the source for the PlayStation 5 page, it’s not particularly blessed with uniquely identifiable elements. In fact, the only thing I could unequivocally tie to either console was the title attributes in the images. So, in order to reliably find the button (which wasn’t actually a button, but a link) I would do the following:

image = driver.find_element_by_css_selector('img[title="PlayStation 5"]')

parent = image.find_element_by_xpath('..')

link = parent.find_element_by_tag_name('a')

Code Snippet 1: Finding the button/link

The above logic would also work when searching for the PS5 Digital Edition by using the title text “PlayStation 5 Digital Edition”.

So once I’d found the appropriate image thanks to the precise descriptive title attribute, I’d find the parent block which would hold everything for that item. Then I would find the link within that block which was nested within a span. That would yield the text on the link.

Once I had the text, I could simply do an assertion on it and then print something to stdout :

if link.text.lower() == 'out of stock':

    status = colored('OUT OF STOCK', red)

else:

    status = colored('IN STOCK', green)

Code Snippet 2: The critical check

It’d also be nice to have that all-important audio alert, but nothing as vulgar as a klaxon or an air-raid siren. I’d just get the bot to tell me that stock was available:

os.system('say "PS5 available at Game UK"')

Code Snippet 3: Tell me something

However, it sounded a bit robotic, so I gave the voice a decent (Scottish) accent:

os.system('say -v Fiona "PS5 available at Game UK"')

Code Snippet 4: Tell me something in a great accent

I was keen to make sure both paths worked so it didn’t just burp and die when a PS5 was detected for real. So I tested it with a product that was in stock and available to purchase, in this case the PS5 HD Camera. So, with both paths working as expected, I started to think about my “fail fast and move on” requirement, so I decided to use a try-except-finally pattern:

driver = webdriver.Chrome()

try:

    # navigate to url

    # find image, parent block, and link

    if link.text.lower() == 'out of stock':

            # Failure

            print("Boo")

    else:

            # We got one!

            print("Yay")

    # Log it to stdout

except WebDriverException as e:

    print("Something funky has happened")

    # Log it to stdout

finally:

    driver.close()

Code Snippet 5: Basic pattern

That was the basic structure of the routine. So I created a GameUk class, initialised the browser in the constructor, and created a check_stock method that would do the business above.

Running The Bot In A Headless Browser

I’d chosen Google Chrome and downloaded a copy of Chromedriver to put in my local Python virtual environment bin directory, but of course the intention was to always run it headless. This was easily achieved during setup by adding an argument before launching it:

from selenium import webdriver

chrome_options = webdriver.ChromeOptions()

chrome_options.add_argument("--headless")

Code Snippet 6: Going headless

But running a headless browser on a JavaScript-heavy web page brought its own little niggle. It couldn’t find anything on the page anymore. Enter the user-agent string.

Using A Package To Simulate A User Agent

Once I mocked a static user-agent string, things began to work again, and I assumed that since I wouldn’t have been the first person to encounter such an issue, there had to be a package for this sort of thing. Hello, fake_useragent :

from fake_useragent import UserAgent

chrome_options = webdriver.ChromeOptions()

ua = UserAgent()

chrome_options.add_argument(f'user-agent={ua.random}')

Code Snippet 7: Random user-agents

Avoiding IP Blocking With A Dynamic Proxy

So even though I was using random user-agent strings, my IP would be constant and subject to a block by the retailer. So I attempted to introduce a rotating proxy facility to keep them off my trail. I did not expect to run the bot for more than a few hours at a time, but I still felt that I should change the bot’s visible IP frequently.

https://free-proxy-list.net/ seemed like an obvious place to start, so I took advantage of other solutions that captured a dozen or so proxies from this list. Then I chose one at random, before adding it to my chrome_options as an argument.

It worked for a while, as long as  I got a random list of proxies and selected one of them randomly. But the available proxies were dreadfully slow and everything timed out painfully and consistently. I even tried bypassing the randomness by  hand-picking some local (UK) elite and anonymous proxies that I thought would perform better.  But they didn’t.

So, for the moment, I had to put that part of the bot into mothballs and move on without it. My backup plan was to use my VPN subscription to mask my IP.

Not long afterwards, I got wind of another stock drop at Game UK, so I wound it up and got ready to rip.

Entering The Ring With My Bot

Image 6: Attempt 2 #fail

To set the scene:

  • Game UK were going to have another stock drop.

  • I had the YouTube livestream running in the background.

  • I had my bot running in the terminal window.

  • My sound was ON this time.

  • My bot alert went off first.

  • I had nothing prepared and flapped a bit.

  • I opened a browser window and went to the default PS5 page.

  • It took *ages* to load.

  • During that loading time, the YouTube siren went off.

  • The masses descended.

  • The page finally loaded but I was now at least one minute behind the masses.

  • I clicked on the PS5 Digital Edition. More loading and waiting.

  • I was in the queue (or so I thought).

  • Another minute passed.

  • I realised I had missed the CAPTCHA dialog at the bottom of the page.

  • I clicked on all the traffic lights.

  • I was in the queue (for real this time). I stayed there for over two hours

  • The stock sold out.

  • I was unceremoniously dumped back to the default PS5 page.

  • No PS5 for me.

Improving My Bot

In hindsight, I should have had the PS5 page loaded and ready to refresh or load as soon as my alert went off, although I had something in mind already that could remedy that. I had managed to carve out a small advantage as I’d hoped, but I watched it ebb away as I flapped and floundered in a mix of panic and excitement, and I again made some grievous errors that cost me my prize.

While I didn’t feel that I needed to scrap everything and go back to the drawing board, I did wonder whether I’d missed an obvious trick.

Did I Need To Change The Tech Stack?

I considered whether there was some way to exercise the same sort of checks further down the stack. However, the likelihood of finding some public- endpoint that didn’t require authentication AND that would somehow give me access to crucial information was highly unlikely.

I’d also heard that Game UK were actively trying to combat the scalpers by putting impediments in place to prevent bots from doing things like adding items to a basket. So it didn’t feel like an avenue of exploration that would yield advantageous results.

A quick scan around the web to see what other people were doing in terms of building their own bot didn’t reveal anything surprising, except perhaps how shoddy some of the logic was, not to mention the frequent overreliance on brittle XPath locators taken from the root of the DOM.

I pressed ahead and made a few minor enhancements to the bot. I resigned myself to trying again, whenever that would be, but I knew I had to be more efficient when it came to the human side of that stock alert response.

Taking Advantage Of A Key Behaviour

Game UK were indeed planning another stock drop soon, and one key thing I noticed this time was that pre-order items had suddenly become visible on the site via an additional page of search results. I believe I found it by searching for PS5 hardware bundles, and I’m pretty sure this page had not been visible before.

Within this page were references to the consoles themselves as well as offers where the console was typically bundled with PS5 peripherals such as media remotes, HD Cameras, headphones, free 12-month PS+ subscriptions, and so on.

If you attempted to click on any of these items, you would be redirected to the default PS5 page at Game. A closer inspection of the parent page, however, revealed that each item in the pre-order list had its own unique URL.

This was very interesting. Surely, they wouldn’t redirect the user on the day of the stock drop, would they? I had an idea.

A Change Of Tack

So, let’s take the digital console as an example. Its direct URL was:

https://www.game.co.uk/en/playstation-5-digital-edition-2826341

And if all I had to do was understand where it actually landed, I could do that without the browser. I could simply use the Python requests package instead.

import requests

from fake_headers import Headers

PS5DE = "https://www.game.co.uk/en/playstation-5-digital-edition-2826341"

DEFAULT = "https://www.game.co.uk/playstation-5"


headers = Headers(os="mac", headers=True).generate()    # random header

response = requests.get(PS5DE, allow_redirects=False, headers=headers)

print(response.status_code, response.url)

Code Snippet 8: An alternative approach

When the allow_redirects flag was set to False:

302 https://www.game.co.uk/en/playstation-5-digital-edition-2826341

And once again with the allow_redirects flag set to True:

200 https://www.game.co.uk/playstation-5

I assumed that when stock was released and this call was made, the response.status_code would still be 200, but response.url would be the direct URL itself and not the default PS5 web page. Thus, the logic for determining whether there was stock would look like this:

import requests

from fake_headers import Headers


PS5DE = "https://www.game.co.uk/en/playstation-5-digital-edition-2826341"

DEFAULT = "https://www.game.co.uk/playstation-5"


headers = Headers(os="mac", headers=True).generate()    # random header

response = requests.get(PS5DE, allow_redirects=False, headers=headers, timeout=5)

if response.status_code == 200 and response.url == DEFAULT:

    # Boo, no stock!

elif response.status_code == 200 and response.url != DEFAULT:

    # Yay, stock!

else:

    # Handle other scenarios

Code Snippet 9: Alternative approach, similar logic

That would mean I could take a selection of these products from the pre-order page, exercise this simple check against each of them, then alert myself to a stock drop in much the same way as I did with the other (web) bot.

What’s more, this could be done in a few seconds. I decided to leave the web bot as is for the moment and create a separate script that I could run alongside it. After I’d retrofitted the new logic in pretty much the same try-except-finally pattern as before, I tried it out.

It was fast. Much faster than the web bot.

I definitely fancied my chances with this one.

My Brother Bot Wins The Bout

Image 7: bouncing badass baby brother bot

 Image 8: Attempt 3 #success

Round three began:

  • It was drop day at Game UK again.

  • The rumour was they’d drop in or around 9.45am again.

  • I had the YouTube livestream running in the background.

  • My web bot was running (just in case my assumptions were flawed).

  • My bouncing baby badass brother bot, which I’d christened bbbbb, was also running.

  • 9.45 a.m. came around.

  • The alert for my new bot went off first.

  • I clicked the terminal to activate the hyperlink to the product URL.

  • I navigated past the CAPTCHA and clicked all the tractors.

  • I was in the queue.

  • My web bot alert sounded 30 seconds later.

  • 40 seconds after that, the YouTube siren went off.

  • The masses descended.

  • My queue time went down to 21 minutes :-)

  • But then went back up to ‘more than an hour’ :-(

  • I heard Game were having trouble with some new anti-bot updates.

  • After three hours in the queue I feared the worst.

  • After four hours my queue time went down to five minutes.

  • Then I was in.

  • I couldn’t get my first choice (digital edition) as it was sold out.

  • I opted for a bundle instead.

  • I added it to my basket.

  • I chose ‘Guest checkout.’

  • Payment was made.

  • Confirmation received.

  • I GOT ONE!

And here’s the proof:

Image 9: The prize

My New Bot Snags Another PS5… For A Friend

It also transpired that a colleague at work was also desperate to get his hands on a PS5 console for him and his kids, and that he’d been trying in vain since launch day. He too had been following the YouTube livestream for some time without success.

Learning of my own success, he asked for help, and I pushed the code for my new bot up to GitHub after making a couple of modifications so it would run on his Ubuntu 20.04 setup at home.

When Game made their subsequent drop a week later, he claimed that at least five minutes elapsed between my bot sounding its alert and the YouTube klaxon going off. He was able to secure a bundle and check out in around 30 minutes.

A Retrospective, and Notes For Testers

You may be forgiven for reaching this point and thinking ‘What on earth has this to do with software testing?’ I’ll admit, on the surface, the links between this experience report and software testing may appear to be tangential. However, the more I reflect on the journey, the more I see how my software testing skills served me well throughout.

For starters, there was a considerable amount of exploration required to get to where I did. The challenge here was not just about understanding which retailers were receiving consoles and when, but what the larger consumer community were doing to act upon the rumoured dates and times. Where were they getting their information? How could they trust it? Understanding what they were doing (and how) gave me a fairly solid baseline to work from.

The other great unknown was how the retailer’s website would handle the massive surge in customer traffic once their PS5 stock was available to purchase online. There would have to be some queuing system, anti-bot deterrents like CAPTCHA, or (as Game UK did) limiting the number of times an ‘Add to basket’ button could be clicked in any one-minute period. The community also gave some very useful tips about checking out: for example, they suggested not to sign in to your account (if you had one) but to use  the guest checkout facility instead to retain your place in the queue.

In terms of the code, the TRIMS mnemonic served me really well.  The bots I created were targeted, focusing on one particular entry point into the retailer’s ecosystem. They turned out to be pretty reliable, eliminating flakiness (as best I could) and embracing a ‘fail-fast-and-move-on’ approach that served me well. They were informative, providing me with enough information at the point where the critical assertions were made (In stock? Out of stock?). They were relatively maintainable (once I took some time to refine the code) and easily tweaked if necessary. Most importantly for this context, they were speedy, so much so it allowed me to get a vital head start on the thousands of people who were all vying for the same prize.

If you take anything from this article: understand that your testing skills are hugely valuable and highly transferable. Writing a PS5 stock monitoring bot was as much about understanding the landscape and behaviours as it was about writing the code.

Further reading and repositories

If you’re so inclined, you can find both my web-bot and my PS5-winning link-bot (bbbbb) on GitHub at https://github.com/deefex/ps5bot. There are instructions in the READMEs on how to clone, install, check and run them.

I also managed to do some additional sleuthing and found the bot that the Ps5Instant Twitter account was using on their YouTube livestreams. It’s freely available on GitHub and is called streetmerchant. It’s (obviously) a much more complex beast and searches for a much wider catalog of things in addition to PS5 consoles. But if you’re comfortable navigating through TypeScript, it is possible to tweak it and have it search exclusively for PS5 consoles across UK retailers.

Just remember, mine is quicker though ;-)

See also the presentation Patterns of a “good” Test Automation Framework, Locators & Data! at the Ministry of Testing Dojo.

Author Bio

You wouldn’t know to look at him, but Del Dewar has been a software testing professional for the best part of 30 years. After many years of being a permanent employee, holding almost every software testing job title there is, he’s currently an independent contractor. He’s also a black belt in Wado-Ryu karate and is a certified MAMIL (Middle Aged Man In Lycra) and can be spotted cycling around the roads of central Scotland. Oh, and he hates brussels sprouts. No, really. They’re evil.

Selenium 4 introduces relative locators
Selenium 4 introduces relative locators. This new feature allows the user to locate an object in relation to another object on the screen! Don't wait, get an instant demo today.
Ask Me Anything: AI/ML in Testing & Starting A Testing Company
Grow Your Technical Confidence
What's that Smell? Tidying Up Our Test Code - Angie Jones
Explore MoT
Episode One: The Companion
A free monthly virtual software testing community gathering
MoT Advanced Certificate in Test Automation
MoT Advanced Certificate in Test Automation
From £599