Techniques for Loading Vendor Libraries from an Extension

This post is intended for extension developers who already have at least a basic understanding of what an Adobe Experience Platform Launch extension can do and how it is composed. If you’re unfamiliar with extension development, I recommend starting out by reading our extension development documentation or watching this video on extension development.

Most often, when someone builds an extension for Adobe Experience Platform Launch, the purpose is to enable Launch users to easily implement an existing marketing technology on a website. The vendor of the marketing technology almost always has an existing library that provides necessary functionality. Without Launch, the website owner typically must copy, paste, and configure a snippet of code provided by the vendor into their website’s HTML. This snippet of code, when run, will typically load the vendor’s library from a content delivery network (CDN) where the vendor has chosen to host it. The vendor’s library contains the client-side code necessary to perform the intended marketing tasks.

With Launch, the website owner optimally doesn’t have to mess with code at all, but instead can install and configure the technology using a simple user interface within Launch. This doesn’t mean the extension developer has to rewrite the vendor’s existing library though. Instead, the extension can focus on providing the glue between Launch and the existing vendor library.

A question frequently arises: From the extension I’m building, what are my options for loading the vendor library onto the Launch user’s website?

Here, we’ll cover three techniques for loading a vendor library. In our examples, we’ll be implementing an extension for the Pinterest Conversion Tag. Let’s first get a better understanding of how its vendor library is loaded outside the context of Launch.

Dissecting the base code

If we take a look at the Pinterest Conversion Tag documentation, we’ll find a section referring to “base code”. The base code is the snippet of code that a website owner must paste within their website’s HTML. When dealing with marketing technologies, base code typically does some variant of these three things:

  1. Sets up a global function that the website owner will use to interact with the vendor library. This will often have some queuing mechanism so the website owner can call the function even before the vendor library has finished loading. Once the vendor library finishes loading, the library will process any queued calls and then replace the global function with a new one that will bypass the queue and instead immediately process any future calls to the function.
  2. Loads the vendor library.
  3. Makes any initial calls to the global function described in #1. This usually entails configuration or any initial tracking.

For the Pinterest Conversion Tag, here’s the relevant base code that Pinterest provides to a website owner:

!function(e){if(!window.pintrk){window.pintrk=function(){window.pintrk.queue.push(
Array.prototype.slice.call(arguments))};var
n=window.pintrk;n.queue=[],n.version="3.0";var
t=document.createElement("script");t.async=!0,t.src=e;var
r=document.getElementsByTagName("script")[0];r.parentNode.insertBefore(t,r)}}("https://s.pinimg.com/ct/core.js");
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');

That might seem unintelligible by mere humans, but that’s largely because it’s minified. To make it easier to dissect, let’s run it through an unminifier like https://unminify.com/. The output is as follows:

! function(e) {
if (!window.pintrk) {
window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments))
};
var
n = window.pintrk;
n.queue = [], n.version = "3.0";
var
t = document.createElement("script");
t.async = !0, t.src = e;
var
r = document.getElementsByTagName("script")[0];
r.parentNode.insertBefore(t, r)
}
}("https://s.pinimg.com/ct/core.js");
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');

Better, but with a little renaming here and reformatting there, we can make it more legible:

!function(scriptUrl) {
if (!window.pintrk) {
window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments)
);
};
window.pintrk.queue = [];
window.pintrk.version = "3.0";
var scriptElement = document.createElement("script");
scriptElement.async = true;
scriptElement.src = scriptUrl;
var firstScriptElement =
document.getElementsByTagName("script")[0];
firstScriptElement.parentNode.insertBefore(
scriptElement, firstScriptElement
);
}
}("https://s.pinimg.com/ct/core.js");
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');

Bring out the scalpel — we’re ready to dissect. The code starts off with an IIFE (immediately-invoked function expression). That’s this thing:

!function(scriptUrl) {
...
}("https://s.pinimg.com/ct/core.js");

It might sound and look fancy, but it’s no big deal. It’s a JavaScript pattern that prevents variables declared inside it (like scriptElement and firstScriptElement) from ending up as globals on window. Notably, the value of scriptUrl will become https://s.pinimg.com/ct/core.js, which is where the vendor library is hosted.

Next, we find this code:

window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments)
);
};
window.pintrk.queue = [];
window.pintrk.version = "3.0";

If window.pintrk doesn’t exist, the base code goes through the work of setting it up as a global function that the website owner will use to track conversions. In this case, you’ll see that each time the function is called, it will add the arguments into an array at window.pintrk.queue. This is the queueing part I mentioned in point #1. The intention here is that if any code on the website calls window.pintrk() before the Pinterest vendor library has finished loading, the information about the function call will get queued up on window.pintrk.queue. Then, when the Pinterest vendor library has finished loading, the library will walk through the queue and process all the calls that were previously enqueued. The vendor library will then overwrite window.pintrk with a new function that processes any future calls to window.pintrk() immediately. This is a common pattern found with many vendor libraries.

The next part of the base code is the most relevant to the topic at hand:

var scriptElement = document.createElement("script");
scriptElement.async = true;
scriptElement.src = scriptUrl;
var firstScriptElement =
document.getElementsByTagName("script")[0];
firstScriptElement.parentNode.insertBefore(
scriptElement, firstScriptElement
);

The base code creates a script element, sets it to load asynchronously, sets the src URL (https://s.pinimg.com/ct/core.js), then adds the script element to the document by adding it right before the first script element already on the document.

Lastly, there are two calls to pintrk():

pintrk('load', 'YOUR_TAG_ID');
pintrk('page');

The first call configures the Pinterest Conversion Tracker with a specific ID assigned for my website. The second call tracks a page event.

Now that we have an idea of what the base code does, let’s see how we can integrate it into a Pinterest Conversion Tracker extension. For these examples, we’ll assume we’re creating an action type called Load Tracker.

1. Load the vendor library at runtime from the vendor’s host

This is the most common option, and for good reason. As mentioned previously, the base code will likely already be loading the vendor library from a CDN. We can avoid copying and pasting vendor library code into the extension if we just make the extension load the vendor library code from the existing CDN. The URL of the Pinterest vendor library is https://s.pinimg.com/ct/core.js.

This approach is usually the easiest to maintain, since any updates that are made to the file on the CDN will automatically be loaded by the extension; we don’t have to copy and paste the changes into our extension each time there is a change to the vendor library.

Implementation

In the case of the Pinterest Conversion Tracker, we could simply paste the whole base code directly into our Load Tracker action module as follows:

module.exports = function() {
!function(scriptUrl) {
if (!window.pintrk) {
window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments)
);
};
window.pintrk.queue = [];
window.pintrk.version = "3.0";
var scriptElement = document.createElement("script");
scriptElement.async = true;
scriptElement.src = scriptUrl;
var firstScriptElement =
document.getElementsByTagName("script")[0];
firstScriptElement.parentNode.insertBefore(
scriptElement, firstScriptElement
);
}
}("https://s.pinimg.com/ct/core.js");
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');
};

There are a couple optional steps we can take to make this leaner.

First, we can scrap the IIFE since it isn’t providing value at this point. The variables the base code declares (scriptElement and firstScriptElement) would naturally be scoped by the function our module is exporting, so we don’t run the risk of them becoming globals on window if we remove the IIFE. Even if the variables were declared above our exported function, they’re still within a CommonJS module, which provides its own scope. If that doesn’t make much sense, just take my word for it; ditch the IIFE.

Second, Launch provides some “core modules” that are utilities any extension can use. By leveraging core modules, we can reduce the amount of duplicate code across extensions. There happens to be a core module that loads a script from a remote location by creating a script element and adding it to the document. Sound familiar? We’ll replace some of the base code by leveraging this core module called @adobe/reactor-load-script that will load the script.

var loadScript = require('@adobe/reactor-load-script');
var scriptUrl = 'https://s.pinimg.com/ct/core.js';
module.exports = function() {
if (!window.pintrk) {
window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments)
);
};
window.pintrk.queue = [];
window.pintrk.version = "3.0";
loadScript(scriptUrl);
}
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');
};

2. Load the vendor library at runtime from the Launch library host

While loading a library at runtime from the vendor’s CDN is convenient, it does present an additional risk to website owners. The CDN may fail, the file may be updated with a critical bug at any time, or the file could be compromised for nefarious purposes. If these scenarios occur, the website loading the vendor library may likewise be adversely affected. Some businesses using Launch are more risk-adverse, especially those in the financial industry. They prefer to have strict control over the code that gets loaded onto their website at runtime and will be less amenable to using an extension that loads a file from a vendor’s CDN.

To address these concerns, we can choose to include the vendor library as a separate file inside our extension. We would then indicate to Launch that the file should be hosted alongside the main Launch library. At runtime, our extension will load the vendor library from the same server that delivered the main Launch library to the website.

For example, if Bank XYZ chooses to host their Launch library on their own servers, Launch would provide Bank XYZ a zip file containing multiple files that they will need to host on their own servers. One of the files would be the main Launch library file that the website loads. Another file would be the vendor library that our extension provided. At runtime, our extension, which is included in the main Launch file, would load the separate vendor library file from Bank XYZ’s servers.

By providing the vendor library alongside the main Launch library file, Bank XYZ can have greater control over the content delivered to their website.

Implementation

Let’s see how we do this in our Pinterest extension. First, we’ll download the vendor library to our machine. In the case of Pinterest, the vendor library is found at https://s.pinimg.com/ct/core.js. We’ll name the file pinterest.js and add it to a new directory in our project called vendor. The filename and location are not particularly important; it just needs to be within our extension project.

Next, in our extension.json file, we need to indicate to Launch that our file should be delivered alongside the main Launch library. We do this by defining hostedLibFiles as follows:

{
...
"hostedLibFiles": ["vendor/pinterest.js"]
}

Next, we’ll take our Load Tracker action code that we completed in the last section and tweak it to load the vendor library from the same server hosting the main Launch library. We get the appropriate URL by passing the filename (without any path) of the vendor file (pinterest.js) to turbine.getHostedLibFileUrl(). Nothing else changes.

var loadScript = require('@adobe/reactor-load-script');
var scriptUrl = turbine.getHostedLibFileUrl('pinterest.js');
module.exports = function() {
if (!window.pintrk) {
window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments)
);
};
window.pintrk.queue = [];
window.pintrk.version = "3.0";
loadScript(scriptUrl);
}
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');
};

Note that if Pinterest ever updates the vendor library on their CDN, we’ll likely want to update our pinterest.js file and release the changes in a new version of our extension.

In some cases, the vendor library we are bundling inside our extension may load additional code from third-party servers. This is actually the case with the Pinterest vendor library. In such a case, bundling the vendor library with our extension might not fully alleviate concerns of some risk-averse users.

3. Directly embed the vendor library

Our last option is to take the code from the vendor library and drop it right into our Load Tracker action. For better or worse, it will increase the size of the main Launch library but also avoid the need to make the additional HTTP request for a separate file.

Implementation

Again, we’ll take our Load Action code we completed in the last section and replace the line where we load the script with the script’s content itself.

module.exports = function() {
if (!window.pintrk) {
window.pintrk = function() {
window.pintrk.queue.push(
Array.prototype.slice.call(arguments)
);
};
window.pintrk.queue = [];
window.pintrk.version = "3.0";
// Paste the full vendor library code here.
}
pintrk('load', 'YOUR_TAG_ID');
pintrk('page');
};

What about code unrelated to marketing technology vendors?

Even though this post has focused on libraries from marketing vendors, these techniques apply to any chunk of code our extension may be dealing with. We may wish to load the chunk of code from a CDN, load it from the Launch library host, or just embed it right in our extension.

Equipped with this information, you should be well on your way to developing a Launch extension for your favorite marketing technology.