Asset Uploading for Web

Introduction

Web pages are built up from many parts: the DOM, CSS, images, and fonts. The Fullstory script captures the DOM directly and includes it in the session, but it does not capture external assets that are referenced from the DOM by URL. For example, imagine that the data capture script encounters an image, like this:

<img src="https://example.com/image.png">

The URL “https://example.com/image.png” will appear in the session, but the image itself will not. Instead, when Fullstory’s servers process the session, they will recognize this URL and fetch the asset, making it available for playback.

In certain cases, this process breaks down, because Fullstory’s servers are unable to fetch the asset. This can happen for a variety of reasons:

  • The asset may be unavailable without authentication. Because Fullstory does not record cookies, our servers are unable to access the asset in this case.
  • The asset may not be available over the internet at all. For example, this can happen for assets that are bundled with Electron or Ionic apps and are accessed via the file system.
  • The asset may have been deleted by the time Fullstory’s servers attempt to fetch it. Sites which rapidly deploy a series of new versions in quick succession can sometimes encounter this issue.

To handle these situations, Fullstory offers an alternative approach: asset uploading. This feature allows you to push assets directly to Fullstory’s servers. With this feature, Fullstory will no longer attempt to fetch these assets over the internet and, as long as assets are uploaded whenever a new version of your site is deployed, Fullstory playback will always have access to everything it needs to provide a high quality rendering of every session.

Installing the Asset Uploader

To make use of asset uploading, you’ll need to install the fullstory-asset-uploader tool on your local machine. Versions are available for the following platforms via the download links here:

Depending on your system’s configuration, running it may result in an error message stating “fullstory-asset-uploader cannot be opened because the developer cannot be verified.”  You can resolve this by running the following command after decompressing the tool:

xattr -d com.apple.quarantine fullstory-asset-uploader

While there is not a Windows executable, Windows 10/11 users may use Windows Subsystem for Linux and run UNIX-based commands/executables in Windows Terminal.

Generating an API Key

To actually upload assets, you’ll need to have admin permissions in your account as well as an API key set to admin level permissions. You can generate one by logging into your Fullstory account and visiting the “API Keys” section of your account settings.

More details about API key management can be found in our documentation.

Creating a Simple Asset Map

To make use of uploaded assets, Fullstory’s servers need more than just the files themselves. We also need to know which URL(s) each asset corresponds to in sessions, so that we can wire everything together. To specify this mapping, you’ll need to create an asset map, which is just a file in JSONC format - essentially, JSON with comments. This is what a simple asset map looks like:

{
  // A mapping from URLs to file paths.
  "assets": {
    "https://example.com/images/image.png": {
      "path": "site-assets/image.png"
    },
    "https://example.com/headers/masthead.png": {
      "path": "site-assets/masthead.png"
    }
  },

  // The asset map format this file uses. Only version 2 is supported for web
  // asset uploading.
  "version": "2"
}

The two top-level properties in this example, “assets” and “version”, are the only required properties in an asset map. In this example, each entry in “assets” maps a URL to a path on the file system. These paths are interpreted relative to the location of the asset map itself, so you’ll want to place the asset map at the root of the directory tree that contains your assets.

You may want to use a wildcard as a replacement for hashes in paths, e.g. when you have random alphanumeric strings appended to assets like styles.79873.css. While we do support wildcards, meaning you could use styles.*.css, we do not recommend using them because this may introduce fidelity issues in future in Session Replay, e.g. for older sessions that relied on styles or assets from previous versions, new assets would effectively overwrite the old assets, leading to inaccuracies. How much of a concern this will be depends on how often you update assets and long your session playback retention period is (please note that this will not impact the analytics data, but purely the visual representation of the digital behaviour).

It’s worth noting that, in the real world, you probably won’t write your asset map by hand. We recommend writing some code to generate it as a side effect of your build process; often this code will run as a part of your configuration for a tool like gulp or webpack.

Example Asset Map Generation Script

In a simple case, customers may want to simply create an asset map file that simply mirrors web URL to the relative location of files within the filesystem. In this example, we’ll assume you have a folder named site-assets, which contains resource files needed to build the application. These files are organized into sub-folders, like so:

$ ls -R 
site-assets

./site-assets:

footers       headers images       videos

./site-assets/footers:
footer-full.png footer-handheld.png footer-landing.png

./site-assets/headers:
about.png contact.png masthead.png product-1.png product-2.png product-3.png

./site-assets/images:
image.png logo_lg.png logo_sm.png watermark.png

We’ll use the following shell script, placed in a file called assetgen.sh at the same level containing site-assets:

$ cat assetgen.sh 
#!/bin/bash

if [ "$2" == "" ]; then
  cat << ARG_EOF

Usage: $0 [PATH_TO_ASSETS] [BASE_URL_PATH]
...where
 PATH_TO_ASSETS is a folder containing asset files organized into subfolders (e.g. site-assets)
  BASE_URL_PATH is a URL prefix used for specifying location mapped to asset (e.g. https://example.com)

ARG_EOF
 exit 1
fi

FOLDER=$1
BASEURL=$2

######

FILES=$(find . -type f | egrep "^./$FOLDER/")
ASSETS=$(
 echo "$FILES" | while read FILE; do
   FILEPATH=$( echo -e "$FILE" | cut -f3- -d"/" )
   cat << EOF
   "$BASEURL/$FILEPATH": {
     "path": "$FOLDER/$FILEPATH"
   },
EOF
 done
)
ASSETS=${ASSETS::${#ASSETS}-1}
ASSETMAP=$(cat << ASSETMAP_EOF
{
 "assets": {
$ASSETS
 },
 "version": "2"
}
ASSETMAP_EOF
)

echo "$ASSETMAP"


## Perhaps instead of echoing to stdout, you want to write to a file or some other operation.

We can run the script, supplying as arguments:

  • the folder containing the assets
  • a base URL prefix for mapping each of the files

The URL of each asset will be generated by appending the relative path of that asset to the base URL provided. So, the result of this execution is the following JSON-formatted text:

$ bash assetgen.sh site-assets https://example.com
{
 "assets": {
   "https://example.com/images/watermark.png": {
     "path": "site-assets/images/watermark.png"
   },
   "https://example.com/images/logo_sm.png": {
     "path": "site-assets/images/logo_sm.png"
   },
   "https://example.com/images/image.png": {
     "path": "site-assets/images/image.png"
   },
   "https://example.com/images/logo_lg.png": {
     "path": "site-assets/images/logo_lg.png"
   },
   "https://example.com/footers/footer-landing.png": {
     "path": "site-assets/footers/footer-landing.png"
   },
   "https://example.com/footers/footer-handheld.png": {
     "path": "site-assets/footers/footer-handheld.png"
   },
   "https://example.com/footers/footer-full.png": {
     "path": "site-assets/footers/footer-full.png"
   },
   "https://example.com/headers/product-3.png": {
     "path": "site-assets/headers/product-3.png"
   },
   "https://example.com/headers/product-2.png": {
     "path": "site-assets/headers/product-2.png"
   },
   "https://example.com/headers/product-1.png": {
     "path": "site-assets/headers/product-1.png"
   },
   "https://example.com/headers/contact.png": {
     "path": "site-assets/headers/contact.png"
   },
   "https://example.com/headers/masthead.png": {
     "path": "site-assets/headers/masthead.png"
   },
   "https://example.com/headers/about.png": {
     "path": "site-assets/headers/about.png"
   }
 },
 "version": "2"
}

You could also pipe that stdout output to a file, for use with fullstory-asset-uploader:

$ bash assetgen.sh site-assets https://example.com > asset_map.json

You’ve successfully created an asset map! This resulting file asset_map.json can be used as ASSET_MAP.JSON in the next step when uploading assets to Fullstory.

Uploading Assets to Fullstory

Once you have an asset map containing your assets, you can use fullstory-asset-uploader to upload them to Fullstory, using a command that looks like this:

fullstory-asset-uploader upload <ASSET_MAP.JSON\> --api-key <FULLSTORY_API_KEY>

If you prefer, you can specify the API key by setting a FULLSTORY_API_KEY environment variable instead.

The tool's output will look similar to this:

Note the hash value at the end of the output, which starts with “sha256:”. This hash value is referred to as an “asset map id”. It uniquely identifies both the asset map and the content of every asset it includes. If you’re familiar with git, the asset map id is the equivalent of a commit hash.

The asset map id is used to tell the Fullstory script which asset map it should use for a given session. We’ll discuss how that works in the next section but, before you can use the asset map id, you’ll need to capture it.

If you’re uploading your assets manually, you can just copy the asset map id and paste it where it needs to go but, in the real world you’ll probably be running fullstory-asset-uploader via a script that runs as part of your build or deployment process. When fullstory-asset-uploader successfully uploads your assets, it returns a zero exit code and prints the asset map id to stdout. If it fails, it returns a non-zero exit code. Regardless of success or failure, all of the informational output goes to stderr, so it won’t interfere with your ability to capture the asset map id.

Linking an Asset Map to Your Sessions

As your site changes over time, you’ll upload many different asset maps. You may also have different asset maps for different sites that you operate, or for different A/B test treatments, or for different versions of an Electron or Ionic app. In many of these situations, there will be several different “active” asset maps at once. This means that you need to tell the Fullstory script which asset map id should be linked to each session.

There are two different approaches you can use to do this; one or the other may be more convenient depending on the details of your build process.

Using a Global Variable

One option is to set the _fs_asset_map_id global variable using a line of JavaScript that looks like this:

window['_fs_asset_map_id'] = 'ASSET_MAP_ID';

You can generate a separate <script> element containing this line of code and place it in the <head> of your page, before the Fullstory snippet, or you can include it in the snippet itself:

  <script>
    window['_fs_debug'] = false;
    window['_fs_host'] = 'fullstory.com';
    window['_fs_script'] = 'edge.fullstory.com/s/fs.js';
    window['_fs_org'] = 'XXXXX';
    window['_fs_namespace'] = 'FS';
    window['_fs_asset_map_id'] = 'ASSET_MAP_ID';
    …
  </script>

This approach is usually the most convenient, because it doesn’t require modifying your main JavaScript bundle. This can result in a simpler build and deployment process.

NOTE: Make sure to include sha256: in your ASSET_MAP_ID or fetching the asset map will fail.

 

Using the API

You can also use the FS.setVars() API using a line of JavaScript that looks like this:

FS.setVars('document', { assetMapId: 'ASSET_MAP_ID' });

Understanding Document Scope

Regardless of which of the two approaches above you take, the effect is ultimately to set a document-scope Fullstory variable called assetMapId. (This is why the API call includes the string “document”). Just as the user-scope Fullstory variables you may be familiar with are associated with the active user, document-scope variables are associated with the active document. In other words, once set, these document-scoped variables will be present until the user navigates to a new page.

The fact that document-scope variables are associated with the document means that they persist even if you use APIs like FS.shutdown() and FS.restart() to initialize a new session. This means that you can set assetMapId once at startup and never think about it again.

It’s also important to know that document-scope Fullstory variables can only be set once. Once the assetMapId for a page is set, you can’t change it.

Separating Build and Deployment

In practice, most customers separate their build process (which includes generating JavaScript bundles) from their deployment process (which includes publishing those JavaScript bundles to their live site). It often doesn’t make sense to actually upload assets to Fullstory during the build process, especially since the same build process is often used for local development. To facilitate this, fullstory-asset-uploader offers an additional command generate:

fullstory-asset-uploader generate <ASSET_MAP.JSON>

This command generates an asset map id just like the upload command, but it doesn’t actually upload the asset map or the assets themselves to Fullstory’s servers. Note that you don’t need an API key to run this command.

It often makes sense to use the generate command as part of your build process to generate JavaScript code that includes the asset map id, then use the upload command during your deployment process so that Fullstory has the necessary assets once the site goes live.

URL Templates (Optional)

The simple asset map format described above has a limitation: you need to explicitly specify the URL of every asset in the asset map. At times, this is inconvenient. For example, you may have both staging and production sites which include all the same assets. Rather than list each asset twice with different URLs, or create two different asset maps, you can use URL templates to associate the same asset with different URLs in each session.

A URL template can appear wherever a URL would appear in an asset map. Here’s what a URL template looks like:

https://${env}.${domain}/images/image.png

URL templates use the ${template string} syntax you may be familiar with from JavaScript to interpolate the values of one or more template parameters into a URL. The values of these template parameters are set using JavaScript code that runs on your website. Different sessions may use different values for these parameters. Fullstory’s servers substitute the parameter values in the session into each URL template to produce a final URL for each asset in your asset map.

In the following sections, we’ll describe this process in more detail.

Declaring Template Parameters

To use URL templates in your asset map, you must first declare the parameters that they use by adding a “parameters” property to your asset map. This empty asset map declares two parameters, “domain” and “env”:

{
// key-value pairs of any parameters needed in your asset map
// See below section on setting the values on the client side
"parameters": {
// domain that will be set on the client side, or default to "example.com"
"domain": { "default": "example.com" },
// env that will be set on the client side, or default to "prod"
"env": { "default": "prod" }
// Optionally use the build-in parameter "fs:origin"
"fs:origin": { }
},
"assets": {
"${fs:origin}/images/image1.png": {
"path": "site'-assets/image1.png"
}
"https://${env}.${domain}/images/image2.png": {
"path": "site'-assets/image2.png"
}
...
"version": "2"
}

Each entry in the “parameters” object defines a parameter; the key is the parameter’s name, and the value is an object that defines options for that parameter. There are no required options, so the options object may be totally empty.

Adding URL Templates to an Asset Map

Once your parameters are defined, you can use them in any URL in your asset map, converting that URL into a URL template:

{
  "parameters": {
    "domain": {...},
    "env": {...}
  },
  "assets": {
    // A URL template that uses the "domain" parameter.
    "https://www.${domain}/images/image1.png": {
      "path": "site-assets/image1.png"
    },
    // A URL template that uses both parameters.
    "https://${env}.${domain}/images/image2.png": {
      "path": "site-assets/image2.png"
    },
    // An asset map can mix URLs and URL templates freely.
    "https://www.example.com/images/image3.png": {
      "path": "site-assets/image3.png"
    }
  },
  "version": "2"
}

Setting Template Parameter Values

In order to use URL templates, you must add JavaScript code to your site to define the value of each parameter. URL template parameters take their value from document-scope Fullstory variables with the same name. This example code sets the “domain” and “env” variables:

FS.setVars('document', {
  domain: 'example.com',
  env: 'staging'
});

The values of these variables will be included in the session. When Fullstory’s servers process the session, the values are substituted for the parameters of the same name wherever they appear in a URL template in the asset map. For example, this URL template in the asset map above:

https://${env}.${domain}/images/image.png

Will be transformed to this URL after the variables in the JavaScript snippet above are substituted in:

https://staging.example.com/images/image.png

For a session that uses the asset map in the previous section and includes the variable values above, Fullstory’s servers will behave as if the asset map looked like this:

{
  "assets": {
    "https://www.example.com/images/image1.png": {
      "path": "site-assets/image1.png"
    },
    "https://staging.example.com/images/image2.png": {
      "path": "site-assets/image2.png"
    },
    "https://www.example.com/images/image3.png": {
      "path": "site-assets/image3.png"
    }
  },
  "version": "2"
}

Be aware that, like assetMapId and all document-scope variables, URL template parameters can only be set once for a given page.

Unset Template Parameters and Default Values

If you don’t provide a value for a URL template parameter via the FS.setVars() API, URL templates that reference that parameter will be ignored for that session. You will usually want to specify values for every parameter you use. However, you have the option of providing a default value for a parameter which will be used if you don’t set the corresponding document-scope Fullstory variable. The default value is specified using a “default” property in the parameter declaration’s options object. The asset map syntax looks like this:

{
  "parameters": {
    "env": { "default": "prod" }
  },
  "assets": {
    "https://${env}.example.com/images/image.png": {
      "path": "site-assets/image.png"
    }
  },
  "version": "2"
}

Built-in Template Parameters

It’s quite common to use the document’s domain, or more generally the document’s origin (which includes the protocol and port), as a URL template parameter. To simplify this common case, Fullstory provides a built-in template parameter called fs:origin.

For example, if you capture a page with this URL: https://dev.example.com:8080/images/image.png.

Thee value of fs:origin will be: https://dev.example.com:8080.

Here’s an example of how to use it:

{
  "parameters": {
    // You need to declare even built-in parameters.
    "fs:origin": { }
  },
  "assets": {
    "${fs:origin}/images/image.png": {
      "path": "site-assets/image.png"
    }
  },
  "version": "2"
}

Currently, fs:origin is the only built-in template parameter. Any new built-in parameters we add in the future will also start with an fs: prefix, so you don’t need to worry about their names conflicting with the parameters that you define.

Need to get in touch with us?

The Fullstory Team awaits your every question.

Ask the Community Technical Support