Cramming Appian into a Browser Extension

Published: Monday, Dec 2, 2019
Cover

I’ve been at Appian for a long, long, long time. But I still remember clearly the day over 10 years ago when we first introduced our plug-in extension framework to allow outside developers to extend the Appian platform using OSGI Java code. The Appian community loved it and our employees (myself included), customers and partners have created thousands of plug-ins to make Appian even more powerful.

My colleague and I recently decided to extend Appian and make it more powerful in a totally new way, by building a browser extension. Let me explain more and describe some of the things we learned along the way.

A few years ago, Appian started adding more extension points to the product. Appian now gives developers multiple methods of integration, including embedding Appian in other web apps using Embedded Interfaces.

Integration architecture Appian's various integration methods

I was part of the team that worked on Embedded Interfaces, and it actually went through multiple revisions to ensure it was fast, secure, didn’t leak memory or CSS styles, and was customizable to match the look and feel of the embedding web app. It has become a huge success with customers who want to replace parts of their existing web portals piecemeal with Appian. It also turned out to be the key to building a browser extension for Appian.

Last year my colleague Matthew Goldberg and I were wistfully discussing how much we wished there was an Appian browser extension so we could access our Appian tasks and data quickly from our browser. We soon realized we could build one ourself by leveraging Embedded Interfaces. And since Appian gives everyone in Engineering 10% of our work time to persue side projects, the Appian Browser Extension was soon born!

Read on to learn more, or check it out yourself.

We’ve published it for Chrome and Firefox.

Appian browser extension

The Appian Browser Extension leverages your existing Appian credentials to connect to your specified Appian instance. It contains custom React code which pulls data from Appian using existing OOTB REST APIs and it renders Appian interfaces via Embedded Interfaces.

While building this browser extension with Matthew, I’ve learned a great deal and had a lot of fun. Not only did we make Appian accessible for browser users without them having to navigate to their Appian instance, we also added things that the standard Appian web client doesn’t have, including always-visible badge counts for tasks, favoriting of all pages, recent history tracking, task completion from inside Gmail, and popup notifications for new tasks. All of these required us to understand how browser extensions worked and leverage APIs that we don’t normally get to use in our normal development work.

Here are just a few of the interesting things we learned while building the Appian Browser Extension.

Polling & Popup Notifications

Since Appian doesn’t (yet) have websocket support for true push notifications, we fall back to polling. Polling in a browser extension, however, is tricky. The browser will suspend the background page of an extension when it’s not being used. The way to poll in the background is through the use of the chrome.alarms functionality, or more specifically, the chrome.alarms.create() and chrome.alarms.onAlarm.addListener() methods.

So first we create a function notifyOfNewTasks which will update the tasks badge count and create a popup notification (using chrome.notifications.create()) if there are new tasks. Since we want to continously poll, we trigger notifyOfNewTasks on extension load and at the end of the function, it creates an alarm for 1 minute from now. In our alarm listener, we once again call notifyOfNewTasks and the cycle of 1 minute alarms repeats.

Since we only want to display a popup notification if the number of tasks has increased, we need to keep track of the user’s current number of tasks. We can’t use a global variable because when the extension is suspended, transient memory is cleared. So we use chrome.storage.local.set() and chrome.storage.local.get() to retrieve the previous task count and store the new task count each time we check for new tasks. One piece of advice that tripped us up initially: chrome.storage.local.set() takes an object, e.g., {taskCount: 4} whereas chrome.storage.local.get() takes an array of keys, e.g., ['taskCount']. You’ve been warned.

GMail MutationObserver

One of my favorite features of the Appian Browser Extension is the ability to load Appian tasks and reports directly inside Gmail. If you have the Appian Browser Extension installed and you get an email in Gmail which contains an Appian task or report URL, the extension modifies the link so that instead of opening a new browser tab, it opens the task or report in a modal directly inside Gmail. So you can complete your task or review the report without leaving Gmail.

Gmail tasks
Example of loading an Appian task without leaving Gmail

In order to render an Appian task or report in a modal within Gmail, the extension creates a div which contains an iFrame. Inside the iFrame, we load the Appian Embedded Interface Javascript from the configured Appian site and then append the appropriate Appian embedded tag to render the Appian object. The Embedded Interface docs provide a detailed description of how to add Embedded Interfaces to an HTML page.

Credit to Max Harper and Kyle Simmons for doing the initial integration of Embedded Interfaces and the browser extension.

But the extension still needs to know when an email with an Appian URL has been loaded. It uses a contentScript to listen for changes to Gmail, but Gmail is funny about how it loads emails which makes it hard to know exactly when a new email has been loaded. Initially we were scanning the whole page, but often the code wouldn’t be triggered when Gmail loaded a new email. Then Matthew figured out that we could use a MutationObserver to efficiently check when the structure of the page had changed and grab all the anchor tags. Once we had the anchor tags, we could check if any matched our Appian task or report pattern, and change them to load our modal instead. This is what our MutationObserver looks like inside our contentScript Javascript:

// Listen for all links added to the page
const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    const addedNodes = Array.from(mutation.addedNodes);
    addedNodes
      .concat(
        addedNodes
          .filter(node => node.querySelectorAll)
          .flatMap(node => Array.from(node.querySelectorAll('a')))
      )
      .forEach(addedNode => {
        const appianObject = isAppianObject(addedNode, siteUrlWithBaseFolder);
        if (appianObject) {
          renderAppianObject(appianObject, siteUrlWithBaseFolder);
        }
      });
  });
});

observer.observe(document, {
  childList: true,
  subtree: true,
  characterData: false,
  attributes: false
});

Gitlab Pipeline for Auto-Publishing

Publishing a new version of a browser extension means handling passwords and/or secret keys for the Chrome and Firefox extension stores. Doing that manually each time is cumbersome and not very dev-opsy. So instead, we (and by “we” I mean mainly Matthew), automated it. Inside our Gitlab CI pipeline, we have a manual step called “deploy”. It’s manual only because we don’t want to auto-publish after each CI run. We want to have manual control over when Gitlab will auto-publish for us. This step also only applies to our master branch.

Pipeline
Example Gitlab pipeline showing a completed deploy step

Inside our deploy step, the system runs our publishExtension.sh script. That script pulls a service account token from an environment variable we set up in Gitlab, and uses that to get the necessary secret info from Appian’s Vault. Using those secrets, the script calls the chrome-webstore-upload-cli to upload a new version to the Chrome extension store and it calls web-ext-submit to upload to the Firefox Add-Ons market.

Custom Webpack

One of the trickiest parts of developing a browser extension with a framework like React is that it needs to be bundled by a tool like Webpack or Parcel. And the Create React App tool, or even a normal Webpack build, assumes you have a single entry point and are trying to generate a website, not an extension complete with manifest files and such. So we created a custom Webpack build that specifies our bundle, our eventPage, and our contentScript as entry points. It also relies on the following plugins:

However, after we’d finished wiring up our Webpack configuration, I discovered the web extension starter. This starter kit claims to help you quickly generate builds for pretty much any browser that supports the Web Extensions API. For my next browser extension, I’m going to give this a shot.

Wrap Up

I hope this shed some light on the novel parts of building a browser extension. If you have any questions about how we built the Appian Browser Extension, let me know. If you are having trouble using the Appian Browser Extension or want to suggest a new feature, please email [email protected].