Avoid Careless Production Mistakes with Custom Scripts

In the wise words of @stahnma,

“Everybody has a testing environment. Some people are lucky enough to have a totally separate environment to run production in.”

Thankfully, the project I’m working on has multiple environments: production (the most recently-released version), staging (for pre-release testing), dev (for dev team use and testing), and multiple review apps (for feature validation). Additionally, each of the developers on the team will keep the app running locally throughout the day. Suffice it to say, it can be a hassle to keep track of which site we’re using at any given moment.

I would prefer to avoid the heart-sinking feeling of looking up at the address bar and realizing that I’ve been ruining the customer’s production data for the last 30 minutes. Thankfully, I haven’t experienced that yet, and I’m doing everything I can to avoid it in the future. What I need is a bright red stop sign warning me away from using the production application unless I absolutely intend to do that.

The Solution

Greasemonkey (see also: Tampermonkey and open-source Violentmonkey) is a popular user script manager. It lets you write custom JavaScript and execute it client-side on any web page. This seemed like a good option for a few reasons:

  1. Scripts are written in JavaScript.
  2. I can customize a page exactly how I want, without interfering with how other users see it.
  3. There’s no need to change application code and possibly introduce a bug.
  4. I’ve heard of, but never used Greasemonkey, so I was looking for a chance to pick up a new skill.

However, the solution also has its downsides:

  1. Scripts are written in JavaScript.
  2. Sharing is slightly more difficult. If someone else wants to use this tool, they need to install Greasemonkey and add this script themselves.
  3. If teammates use it and make changes, everyone will need to manually update the script.

Getting started

I wanted to title the page and change the navigation bar to unique colors to make it obvious which environment I’m in.

Original header of the application

To do this, first install a script manager from your browser’s extension web store. Greasemonkey is only available for Firefox, but other alternatives, such as Tampermonkey or Violentmonkey, may be available for your browser.

Next, locate the Greasemonkey icon and create a new user script.

Creating a Greasemonkey script

Getting started was quite simple, thanks to some comprehensive documentation.

The script

A Greasemonkey script will begin with a header that specifies some metadata and rules.


// ==UserScript==
// @name StopProd
// @namespace StopProd
// @match *://*PRIVATE.URL.OF.PROD.APP/*
// @grant none
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @run-at      document-idle
// ==/UserScript==

I decided to pull in jQuery and a function designed for Greasemonkey scripts (waitForKeyElements) that helps with AJAX requests. These are brought in with the @require tag. The @match tag indicates where to run this script. Match pattern syntax can be seen here, and additional configuration metadata options are outlined here.

The next step is to put the waitForKeyElements function to good use. This will require that the given element matching the selector is present before you execute the action. If a site does not perform any AJAX requests, this may not be necessary.


// ==UserScript==
// @name StopProd
// @namespace StopProd
// @match *://*PRIVATE.URL.OF.PROD.APP/*
// @grant none
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @run-at      document-idle
// ==/UserScript==

waitForKeyElements(".navbar-default", function () {
  // Do the stuff
});

Because we know the page is loaded, we can find elements, add content, and update styles.

A minimum viable product in this case is just differentiating the production application:


// ==UserScript==
// @name StopProd
// @namespace StopProd
// @match *://*PRIVATE.URL.OF.PROD.APP/*
// @grant none
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @run-at      document-idle
// ==/UserScript==

waitForKeyElements(".navbar-default", function () {
  var header = $('.navbar-header');
  var title = document.createElement('h1')
  title.href = url;
  title.style.color = '#ffffff';
  title.appendChild(document.createTextNode('PRODUCTION'));

  header.css('background-color', 'red');
  header.append(title);
});

However, with just a few tweaks, this can be outfitted to work on the other environments as well.


// ==UserScript==
// @name StopProd
// @namespace StopProd
// @match *://*PRIVATE.URL.OF.PROD.APP/*
// @match *://*PRIVATE.URL.OF.OTHER.APPS/*
// @grant none
// @require  http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js
// @require  https://gist.github.com/raw/2625891/waitForKeyElements.js
// @run-at      document-idle
// ==/UserScript==

waitForKeyElements(".navbar-default", function () {
  var url = window.location.href;
  var header = $('.navbar-header');
  var title = document.createElement('h1')
  title.href = url;
  title.style.color = '#ffffff';

  var color = 'green';

  if (url.match(/prod/)) {
    color = 'red';
    title.appendChild(document.createTextNode('PRODUCTION'));
  }
  if (url.match(/stage/)) {
    color = 'orange';
    title.appendChild(document.createTextNode('STAGING'));
  }
  if (url.match(/dev/)) {
    color = 'yellow';
    title.appendChild(document.createTextNode('DEV'));
  }
  if (url.match(/pr-\d+/)) {
    // turns "https://pr-123.herokuapp.com/" into "PR-123"
    const app = url.match(/pr-\d+/)[0].match(/pr-\d+/)[0].toUpperCase();
    title.appendChild(document.createTextNode(app));
  }

  header.css('background-color', color);
  header.append(title);
});

The result

This is what each of the environments look like:

How the script changes the header

As you can see, they are clearly very different from the original header.

Original header of the application

Concluding Thoughts

There are other ways to put up barriers to accidental changes. Here are a few alternatives that come to mind:

  • Use separate logins for different environments (e.g. no more ‘admin’ that is available in all environments).
  • Block the website or require a password with a browser extension or desktop application.
  • Change the look of the application based on an environment variable. This would be viable for non-web-based applications, but it requires code changes.
  • Change the terminal prompt, etc. of different machines.

My solution was really simple to write, and it took just a short amount of research and experimentation to get it working. Now, when using the application in different environments, it is immediately clear which one I’m using, without looking at the address bar.