Mike Bland

Custom Links

Some wheels were meant for reinventing—if sometimes only because we need to teach ourselves how they work!

- Alexandria
Tags: programming, technical, testing

Like I mentioned in my previous post, I’ve been consumed with a passion project as of late. Not go-script-bash this time, though I did have to investigate why my coverage builds started failing due to some sort of deadlock—which led me to submit SimonKagstrom/kcov#211 to fix it, naturally. (Thanks to Simon Kagstrom for providing such a useful tool, and for merging my fix so quickly!)

Mostly, though, I’ve been working on a new project I call Custom Links, which is an application that for creating and accessing custom URLs. It allows authenticated users to create short URLs with meaningful names that redirect to longer URLs, or to easily create canonical URLs that you can redirect to different target URLs over time.

A wheel by any other name

Why did I write this? Doesn’t something similar already exist?

First, I got the idea after the Schibsted Eng Prod team decided—before I arrived and independent of my direct involvement—to publish their own Schibsted Testing on the Toilet series. Clearly you can’t expect “users” to “click” on URLs from a paper flyer; and while QR codes are possible now (which we did use), there’s still a lot of value in using short, memorable URLs like go/tott, as we used so often on TotT episodes at Google.

So I started looking around for an open source solution—and couldn’t find one! The closest I found was YOURLS, and at least when I tried setting it up in mid-April, I couldn’t figure out how to get it to create actual custom URLs instead of automatically-generated short URLs. (To be fair, the current home page does make it look like it can do that, but I haven’t deeply investigated it since.) Plus, it requires setting up PHP and MySQL, and uses its own user account system.

In the meanwhile, Luis asked Kenneth to set up a bit.ly account to create custom links. The URLs all begin with some form of bit.ly/, which isn’t as nice as go, but it was enough to start publishing TotT episodes.

However, as Kenneth found out, Bitly apparently won’t let you change the target of a custom short link after you create it. Hence, the use case of providing a canonical URL that can be updated as needed isn’t met using this solution.

So much for serverless

When I couldn’t find any other open source alternative, I figured writing such a server shouldn’t be too hard, and set out to do so. At first, sensing an opportunity to put what I learned from Serverless Single Page Apps into practice, I spent a couple days trying to think through how to implement it in a serverless fashion. However, trying to design a general-purpose application raised one gigantic problem I couldn’t devise an answer for: redirects. Yes, people have used S3 buckets to set up their own redirect “databases”, but for seemingly custom internal services that require some degree of specialized maintenance, rather than being mostly self-serve.

Confronted with this, I fell back on Ken Thompson’s old bit of programming wisdom: When in doubt, use brute force. Being a backend developer, and one who generally avoids SQL databases, I decided to bang out the backend using Node.js, using Redis as a backing store, and using the Passport authentication framework to support user authentication via Google OAuth 2.0. Learning the Redis API was easy, and getting things set up with Passport took a bit of archaeology, but all in all the backend largely came together over a weekend or so—tested six ways from Sunday, of course.

The hard part

“Users are a frontend problem.”—Written on the whiteboard of former Google colleague Matthew Simmons

While I had a functional server that would efficiently provide short link redirections, and I could easily update the Redis database directly, I accepted the reality that most users, even technical ones, expect a more user-friendly interface. Also, since I’ve done very little frontend development—and even less frontend testing—I decided I’d use this project to teach myself a few things about browser-side JavaScript and browser-driven end-to-end testing.

The first thing to decide upon was a framework. Influenced by Serverless Single Page Apps, I decided not to use one, opting to embed template elements in my single index.html page, and to use JavaScript to instantiate and update them. On top of that, I went one step beyond the book and eschewed JQuery in favor of Vanilla JS.1 I’m not sure I’d do this again for every project—as much as I hate to accept it, React and GraphQL appear to be the new hotness2—but I think it was a great exercise to go through before resorting to higher-level abstractions in the future.

Of course, me being me, I made setting up my environment to “make the right thing the easy thing” (i.e. automated testing) my top priority before going full-bore into development. Since Schibsted was using bit.ly, and this was a personal project, I could afford to force myself to learn a lot about JavaScript testing tools and frameworks rather than rush to deliver a product.

And man, did I learn. Rather than enumerate the details here, check out the Custom Links listing on my portfolio page if so inclined. The punch line, however, is thanks to creative use of go-script-bash, I managed to make it really easy to run browser tests that automatically run after a file save, to run them on the command line via Phantom JS, to collect coverage reports that can automatically open in the browser or get shipped to Coveralls—and, finally, after all of these years, to run Selenium-WebDriver tests.

The end(-ish) result

I wound up setting up these environment pieces and debugging tooling issues—such as the lack of nonblocking connect crashing PhantomJS on the Windows Subsystem for Linux (also enumerated in the portfolio listing)—all the way until my second trip to Europe. After that, I didn’t write a line of code for the entire rest of June and for the first couple weeks of July.

Finally, after fixing the aforementioned kcov issue, I dove back into Custom Links on July 21, and finally completed what I hope is a reasonably functional user interface this past Tuesday, August 8. In terms of tests (all times for my 2015 MacBook Pro with a 2.9GHz Intel Core i5 and 8GB RAM):

  • The server test suite (./go test server) contains 143 test cases and runs in two seconds.
  • The browser test suite (./go test browser) contains 123 test cases and runs in about 700 milliseconds.
  • Running the browser test suite across Chrome, Firefox, Safari, and PhantomJS simultaneously using Karma (karma start) takes about 4.5 seconds.
  • The end-to-end test suite (./go test end-to-end) contains nine test cases and runs in about twelve seconds.
  • Running all of the above at once (./go test) takes about 30 seconds.

I updated the README with basic installation and configuration instructions this past Wednesday, August 9, and now finally consider the basic product ready to try out by other humans.

Future development

There’s lots of room for improvement, of course. For starters, I may try switching from PhantomJS to Headless Chrome. I could implement other auth providers, and perhaps add a landing page to select which one to use. The code could probably be slightly reorganized, with function, class, and module comments. (I’ve leaned heavily on descriptive function, object, and test case names up until now). Perhaps I could clean up and test my scripts. Certain components and test helpers could be extracted into npm packages. The README can almost certainly be improved.

And of course, of course, I should look into Docker-izing it.

Still, even now, I think it could be useful to folks. So if you’re inclined to give it a try, I’d love to help you get set up if needed, and get your feedback on how it could be improved!

Even if no one uses it, at any rate, it really challenged me to shore up my skills in an area that I’d neglected for the better part of my career. That experience alone made the whole project worthwhile!

Footnotes

  1. The Vanilla JS website is a tongue-in-cheek illustration of how modern, standardized JavaScript performs much better than popular frameworks, and doesn’t really require much extra code. 

  2. My reluctance is due to my blood feud against teh twitbooks