FAQ

await transcend.showConsentManager({ viewState: 'CompleteOptions' });

The available values for viewState can be found in our open source consent manager UI repository.

If you are running the consent manager on multiple domains, you can customize the link to your privacy policy by setting data-privacy-policy on your airgap.js <script> tag. For example, data-privacy-policy="https://privacy.transcend.io/". This data attribute will override the value of the privacy policy link that is set in the Admin Dashboard.

This value can also be set after the script has loaded, using transcend.setPrivacyPolicy(string).

The CompleteOptionsInverted view state also uses the data-secondary-policy data attribute and transcend.setSecondaryPolicy(string) function to display a secondary policy link.

This could be happening for a couple of different reasons:

  1. You are not in a region with applicable data laws. For California/US residents, users are opted in by default, and websites are not required to obtain consent/show a banner. For EU citizens, users are opted out by default, which means websites must ask for consent with a banner.

💡 Solution: Use our userscript here to simulate accessing the site from a given region (EU or US).

  1. You have already confirmed your consent. To clear out your consent, type the following into your dev console:
addEventListener('click', airgap.reset);
// click anywhere on the page

See Creating your own UI.

The airgap.getConsent() API returns an object in this format:

interface TranscendConsentPreferences {
  /** consent to individual tracking purposes */
  purposes: {
    /**
     * true -> confirmed consent
     * false -> unconfirmed consent or opted out
     * 'Auto' -> unconfirmed consent, but opted in by default
     */
    [TrackingPurpose in string]: boolean | 'Auto';
  };
  /**
   * Was tracking consent confirmed by the user?
   * If this is false, the consent was resolved from defaults & is not yet confirmed
   **/
  confirmed: boolean;
  /**
   * Consent update/sync timestamp (ISO 8601 format)
   * e.g. "2021-06-29T19:16:13.964Z"
   */
  timestamp: string;
}

It's noteworthy that the value for each of the purposes can be one of 3 values:

  • true: The website visitor explicitly confirmed consent for that purpose.
  • false: The website visitor explicitly unconfirmed consent for that purpose, OR the website visitor was opted out by default based on your regional experiences configuration.
  • 'Auto': The website visitor is opted in by default based on your regional experiences configuration. The website visitor has never explicitly confirmed consent for that purpose.

Example:

await airgap.sync();
const { purposes } = airgap.getConsent();
console.log(JSON.stringify(purposes, null, 2));
// example log message: { Functional: true, Advertising: true, Analytics: true }

Absolutely! See the following example:

airgap.addEventListener(
  'consent-change',
  ({ detail: { consent, oldConsent, changes } }) => {
    console.log('old consent:', oldConsent);
    console.log('new consent:', consent);
    console.log('consent changes:', changes);
  }
);

Consent preferences are persisted in localStorage directly on the end-user's browser. If the end user has previously been on your website on that browser, then the consent preferences will be restored from their prior session.

See Syncing consent to your backend.

See Restoring consent to users from your backend.

Consent changes require proper authorization. See consent authorization to learn more.

  • Option 1: Set specific tracking preferences airgap.js onload

    <script
      src=".../airgap.js"
      onLoad="airgap.setConsent(event, { Functional: true, ... })"
    ></script>
    

    HTML

    <button onClick="airgap.setConsent(event, { Functional: true, ... })">
      Save tracking preferences
    </button>
    

    JavaScript

    saveConsentButton.addEventListener('click', (event) => {
      airgap.setConsent(event, { Functional: true /* ... */ });
    });
    
  • Option 2: Opt-in or opt-out to all tracking preferences (airgap.optIn() and airgap.optOut()) HTML

    <button onclick="airgap.optOut(event)">Opt-out</button>
    
    <button onclick="airgap.optIn(event)">Opt-in</button>
    

    JavaScript:

    optOutButton.addEventListener('click', airgap.optOut);
    
    optInButton.addEventListener('click', airgap.optIn);
    
  • Set "Do not sell my personal information" tracking preference

    // "Do not sell my personal information" consent (CCPA/CPRA)
    saleOfInfoOptOutButton.addEventListener('click', (interaction) => {
      airgap.setConsent(interaction, { SaleOfInfo: false });
    });
    
    saleOfInfoOptInButton.addEventListener('click', (interaction) => {
      airgap.setConsent(interaction, { SaleOfInfo: true });
    });
    

It's simple to use your A/B testing software with the consent manager, because you can create variants and track outcomes programmatically.

If you are concerned about the ePrivacy Directive and use of local storage, you can limit our replay quarantine feature to use a non-persistent in-memory queue by setting data-replay="mutations" on your airgap.js script tag. Our quarantine can also be completely disabled with data-replay="off".

The default behavior is to persist the replay quarantine between pageviews.

Note that setting the quarantine to use non-persistent storage will cause the queue to be cleared on each page load.

You may load airgap.js asynchronously, though you should be aware that airgap.js can not regulate any scripts that load before it. Asynchronous loading is recommended for single-page applications in order to further reduce page load time.

// Call `await getAirgap()` to load airgap.js

// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const BUNDLE_ID = 'your-bundle-id-here';

let airgapAPI;
const getAirgap = () =>
  (airgapAPI ??= new Promise((resolve, reject) => {
    // Stub airgap.js ready queue
    if (!self?.airgap?.ready) {
      self.airgap = {
        readyQueue: [],
        ready(callback) {
          this.readyQueue.push(callback);
        },
        ...self?.airgap,
      };
    }

    // Wait for airgap.js to be ready
    self.airgap.ready((airgap) => {
      resolve(airgap);
    });

    const script = document.createElement('script');

    // Reject promise if airgap.js fails to load
    script.addEventListener('error', (evt) => {
      reject(evt);
    });

    // Specify load options:
    // e.g.
    // script.dataset.lazyLoadUi = 'on';
    // script.dataset.prompt = 'auto';
    // script.dataset.dismissedViewState = 'Closed';

    // Load airgap.js script asynchronously
    script.async = script.defer = true;
    script.src = `https://transcend-cdn.com/cm/${BUNDLE_ID}/airgap.js`;
    document.documentElement?.appendChild?.(script);
  }));

Cross-domain consent sync works without any special configuration. In order to also get cross-domain quarantine sync, you need to host your own first-party sync endpoint.

This is possible through our cross-domain iframe postMessage interface, Transcend XDI.

This API is automatically scoped to only allow the domains in your domain list to get consent and quarantine data. By default, private quarantine data synchronized through this API never leaves the browser.

// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const BUNDLE_ID = 'your-bundle-id-here';

let xdiAPI;
const getXDI = () =>
  (xdiAPI ??= new Promise((resolve, reject) => {
    // Stub Transcend XDI ready queue
    if (!self?.transcend?.xdi?.ready) {
      self.transcend = {
        ...self?.transcend,
        xdi: {
          readyQueue: [],
          ready(callback) {
            this.readyQueue.push(callback);
          },
          ...self?.transcend?.xdi,
        },
      };
    }

    // Wait for Transcend XDI to be ready
    self.transcend.xdi.ready((xdi) => {
      resolve(xdi);
    });

    const script = document.createElementNS(
      'http://www.w3.org/1999/xhtml',
      'script'
    );
    script.addEventListener('error', (evt) => {
      reject(evt);
    });

    // Load Transcend XDI script asynchronously
    script.async = script.defer = true;
    script.src = `https://transcend-cdn.com/cm/${BUNDLE_ID}/xdi.js`;
    document.documentElement.appendChild(script);
  }));

let xdiChannel;
const connect = async () =>
  (xdiChannel ??= await new Promise(async (resolve) => {
    const xdi = await getXDI();
    resolve(
      await xdi.connect(
        `https://${YOUR_SYNC_ENDPOINT}.com/consent-manager/${BUNDLE_ID}`
      )
    );
  }));

const getConsent = async () => {
  const channel = await connect();
  const { consent } = await channel.run('ConsentManager:Sync', {
    sync: ['consent'],
  });
  return consent;
};

console.log(await getConsent());

Absolutely. With this snippet, you're ready to go!

// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const isCaliforniaResident =
  self.Intl &&
  new Intl.DateTimeFormat().resolvedOptions().timeZone ===
    'America/Los_Angeles';

const doSomethingThatRequiresFacebookLDUOverrides = async () => {
  if (isCaliforniaResident) {
    // Wait for airgap.js to init
    // This automatically starts applying our global LDU/etc. overrides
    await getAirgap();
  }
  // loadFacebookPixel(...)
};

See the airgap.js API reference for more API information.

We fully regulate almost every API used for tracking in modern browsers. Notably absent from our first-class regulatory coverage are the following technologies, all of which are on our roadmap with clear paths forward to add regulation support:

  • CSS / CSSOM
  • SVG-namespaced subresources
  • Realms
    • Same-origin frame (w/o our consent manager) subresources
    • ECMAScript ShadowRealms API
  • Automated page navigation (e.g. loading a new page without responding to a user gesture)
  • Trusted Types override/quarantine functionality — Trusted Types-originating requests can be allowed/denied but not yet overridden or quarantined in a seamless manner.

Note: All of these technologies (except automated page navigation) are regulated with blocking when using a Content Security Policy. You can enable our dynamic Content Security Policy feature in your Consent Management dashboard.

Transcend Consent Management was designed to load quickly, operate efficiently during website usage and speed up the website experience. Check out Transcend's blog post on Architecting Transcend Consent Management for performance for more information on how airgap.js was built with speed and efficiency in mind.

The following performance metrics were measured on an Apple M1 Pro laptop under light-medium load, using the transcend.io bundle configuration:

  • Request processing overhead
    • Average (hot JIT): ~35μs/request
    • Worst case (cold JIT): ~350μs/request
    • This is the entire overhead for 'pure network APIs' (e.g. fetch, XMLHttpRequest, etc.)
  • 'HTML string' DOM mutation processing overhead: ~65μs/request-causing node (in addition to request processing overhead)
    • HTML string consumers include innerHTML, outerHTML, iframe.srcdoc, and document.write

As a stress test, we benchmarked re-processing transcend.io's entire homepage (equivalent to calling document.documentElement.innerHTML = document.documentElement.innerHTML) and measured an average total latency of 24.6ms while processing ~250 request-causing elements per call.

Note: Performance overhead scales linearly with the size of your bundle configuration.

Currently, as of January 5, 2023 our uncompressed sizes are as follows:

  • airgap.js core: 37.9 KB compressed / 92.6 KB uncompressed (can be loaded asynchronously)
  • UI: 59.9 KB compressed / 208.5 KB uncompressed (async and optionally lazy-loaded)
  • Private cross-domain sync host (XDI): 11.4 KB compressed / 24.1 KB uncompressed (lazy-loaded)

📏 Size changes are coming soon! The following changes will happen later in development.

These are estimates of the size changes that will happen with our upcoming changes:

  • +5-10 KB (uncompressed) to airgap.js core to regulate the remaining technologies on our Regulation Roadmap
  • -7-10 KB (uncompressed) from all of airgap.js core and XDI through clearly attainable optimizations in common components

Note: Size figures do not include additional embedded configuration data.

We currently do not configure any browser-caching for our Consent JS files (https://transcend-cdn.com/cm/...). The only caching that happens is DNS caching + CDN caching. This simplifies publishing changes to your consent manager bundle, but could result in slower load times for users on low bandwidth.

Going forward, our plan is to configure browser-caching for 4 hours by default, and allow each customer to configure this time period from the Admin Dashboard. When initially setting up the bundle, customers can set this time period to 0 (disabling browser cache), and then increase the time period as the Consent bundle stabilizes and no longer need to be updated frequently.

Even with CDN caching enabled, it is possible to generate a dynamic query parameter that can be used to cache-bust according to your own set of criteria. For example, you could create a cache-buster to re-fetch the script once per hour, even if the CDN caching was configured to 4 hours.

function roundToHour(date) {
  p = 60 * 60 * 1000; // milliseconds in an hour
  return new Date(Math.round(date.getTime() / p) * p);
}
const cacheString = roundToHour(new Date()).toISOString();

const script = document.createElement('script');

script.setAttribute(
  'src',
  'https://transcend-cdn.com/cm/ee571c7f-030a-41b2-affa-70df8a47b57b/airgap.js?=' +
    cacheString
);
document.head.appendChild(script);

An alternative method that we can also support is to have separate paths, one for versioned files and one that always has the latest version. When using the latest version/static URL, you can cache for some short period of time (or have no cache). When using the versioned URL, you can cache indefinitely and “cache-bust” by updating the URL to point to the newer hash/version.

The safest/recommended approach is still to include the script directly into the HTML header. But if that is not feasible, then you can also use Tag Managers to inject the script. When using a tag manager, it is important to configure the airgap.js script to be the first script injected. This allows the airgap.js script to regulate subsequent scripts that get injected.

Read on for more specific instructions.

You can store a flag on the user's machine that will check if the banner was shown for each user, with the following code:

const hasSeenNotice =
  localStorage.getItem('privacyPolicyNotice15-1-2024') === 'true';
if (!hasSeenNotice) {
  transcend.showConsentManager({ viewState: 'PrivacyPolicyNotice' });
  localStorage.setItem('privacyPolicyNotice15-1-2024', 'true');
}

You can change and customize which View State to show, in this example PrivacyPolicyNotice. You can also update your code with the key describing each new update requiring the display of the banner, in the above example privacyPolicyNotice15-1-2024.

The GTM script itself should be safe to load without having any tracking side effects. You will need to configure the airgap.js script as the first script to load by using the “Consent Initialization” trigger. This is loaded before all other page view triggers.

References:

Adobe Tag Manager can be used to inject airgap.js by configuring Rule Ordering such that airgap.js can be the first script to load. Rules have an “order priority” number where lower numbered rules fire first, so you can create a new Rule to load airgap.js with the priority ordering of “0” and event type of “Core - Library Loaded”. You’ll also need to do an audit of all other rules configured to make sure that there is no other script which has a lower order priority than airgap, as rules with the same order run in no particular order.

However, even with this configuration, it’s possible that a rule will fire out of order, because Adobe does not offer any guarantees around how extensions are implemented. They have this call-out in their docs: “Ultimately, the responsibility for executing actions in order lie with the extension developer of the event type that you are using. Adobe extension developers ensure their extensions work as intended. Adobe provides guidance to third-party extension developers to do this properly, but cannot guarantee how these guidelines are followed.” So the only way to ensure that things work as expected is to test thoroughly and to have continuous monitoring of what tools are running on your site (which is provided through our telemetry and dashboards).

References:

The initial request to fetch the Tealium script manager should not be tracking user data as a side effect. To have Transcend Consent Management load as early as possible, we recommend adding it to the utag.sync.js script. The script is placed in the <head> section of your page code and loads synchronously to comply with the most common vendor requirements. Instructions for how to do this can be found here. You'll want to modify the utag.sync.js script to load our Consent script as follows:

// Get your bundle ID at https://app.transcend.io/consent-manager/developer-settings
// Bundle ID is in this format: https://transcend-cdn.com/cm/{bundle-id}/airgap.js
const script = document.createElement('script');

// Specify load options:
// e.g.
// script.dataset.lazyLoadUi = 'on';
// script.dataset.prompt = 'auto';
// script.dataset.dismissedViewState = 'Closed';

// Load airgap.js script synchronously
script.src = `https://transcend-cdn.com/cm/${BUNDLE_ID}/airgap.js`;
document.documentElement?.appendChild?.(script);

References: