Plugin Framework

The plugin framework provides the capability to include custom enhancements or modifications within Heap's event pipeline, empowering you to execute specific enrichment, redaction, or other modifications that would not otherwise be handled by Heap's existing APIs.

The new SDK is structured into three distinct layers: Adapters, Transformers, and Networkers. You can add custom functionality within the Adapters and Transformers layers, as described below.

📘

Plugin Persistence

Adapters and Transformers plugins are stored in memory, so these APIs will need to be called on each page where you want to use the plugin framework.

Adapters

Adapters are modules executed during initialization of the web SDK, acting as the entry point for essential functionalities within their respective contexts. They play a crucial role in initializing features to capture and support relevant client-side actions.

These modules are executed prior to events being sent to Heap and have the ability to block our event processing pipeline until execution is complete, or 5 seconds has passed. As such, the primary use case for custom adapters is to perform asynchronous setup calls that your events may be dependent on.

addAdapter

Adds a custom adapter to the Heap Web SDK registry

var adapterName = 'myCustomAdapter';
var adapterFn = function() {
  return executeAdapterFn()
    .then(function() {
      heap.markAdapterAsResolved(adapterName);
    });
};

var adapter = {
  name: adapterName,
  fn: adapterFn
};

var config = {
  isBlocking: true,
  blockDurationInMs: 3500
};

heap.addAdapter(adapter, config);
const adapterName = 'myCustomAdapter';
const adapterFn = async () => {
  await executeAdapterFn();
  heap.markAdapterAsResolved(adapterName);
}

const adapter: { name: string; fn: Function } = {
  name: adapterName,
  fn: adapterFn,
}

const config: { isBlocking?: boolean; blockDurationInMs?: number } = {
  isBlocking: true,
  blockDurationInMs: 3500,
}

heap.addAdapter(adapter, config);

Arguments

  1. (required) adapter: adapter object containing name and fn to be executed at heap initialization time

    Adapter PropertyTypeDescription
    namestringRequired.
    Unique identifier for the custom adapter
    fnFunctionRequired.
    Adapter function to be executed at heap initialization time
  2. (optional) config: configuration object for the corresponding adapter

    Config PropertyTypeDescriptionDefault
    isBlockingbooleanOptional.
    Sets whether event processing should be blocked until adapter has resolved, or 5000 milliseconds has passed.
    false
    blockDurationInMsnumberOptional.
    Sets the duration to block the queue. Max duration is 5000 milliseconds.
    5000

markAdapterAsResolved

Flags the adapter with the provided name as resolved. Should be used within the adapter function to unblock the Heap event queue once the adapter's asynchronous blocking step is complete.

🚧

Default Adapter Resolution

If the adapter takes longer than 5000 milliseconds to call markAdapterAsResolved, the Heap SDK will automatically mark the adapter as resolved.

If the adapter is non-blocking, then this API is unnecessary.

var adapterName = 'myCustomAdapter';
var adapterFn = function() {
  return executeAdapterFn()
    .then(function() {
      heap.markAdapterAsResolved(adapterName);
    });
};

var adapter = {
  name: adapterName,
  fn: adapterFn
};
const adapterName = 'myCustomAdapter';
const adapterFn = async () => {
  await executeAdapterFn();
  heap.markAdapterAsResolved(adapterName);
}

const adapter: { name: string; fn: Function } = {
  name: adapterName,
  fn: adapterFn,
}

Arguments

  1. (required) adapterName: name of the custom adapter to resolve

Transformers

🚧

Using Transformers

It is important to understand Heap's event schema to use transformers correctly. Read more about the structure of our events in Plugin Framework - Heap Event Schema.

Because Heap events have a defined schema, we recommend using custom transformers primarily for data redaction and transformation. For adding additional event properties, you should default to using our addEventProperties and addUserProperties APIs detailed in API Reference.

Transformers are isolated services responsible for the enrichment, transformation, and redaction of heap event messages. A transformer pipeline is a predefined sequence of steps that each transformer follows. The pipeline is identified based on the type of event.

Transformer Pipelines

Message TypePipeline
Trackgeneral
Usermetadata
Sessionmetadata
Pageviewmetadata

addTransformerFn

Adds custom transformer to the registry. By default, your transformer will be added to the general pipeline, unless otherwise specified.

var name = 'redactTrackMessageTransformer';
var fn = function(messages) {
  return redactTrackMessages(messages);
};
var pipeline = 'general';

heap.addTransformerFn(name, fn, pipeline);

const name: string = 'redactTrackMessageTransformer';
const fn = (messages: Array<EventMessage>): Array<EventMessage> => {
  return redactTrackMessages(messages);
};
const pipeline: 'general' | 'metadata' = 'general';

heap.addTransformerFn(name, fn, pipeline);

Arguments

  1. (required) name: name of the custom transformer
  2. (required) fn: custom transformation function to modify the Heap event message.
    1. takes in an array of Heap event messages, and returns an array of the transformed event messages
  3. (optional) pipeline: general | metadata - determines which type of message to apply transformations to, defaulting to general pipeline (track events)

removeTransformer

Removes specified transformer from the registry.

heap.removeTransformer('myCustomTransformer');
heap.removeTransformer('myCustomTransformer');

Arguments

  1. (required) name: name of transformer (string)

resetTransformers

Removes all custom transformers from the registry.

heap.resetTransformers();
heap.resetTransformers();

Examples

Adding event properties from an asynchronous source

If you want to add event properties that are dependent on an asynchronous call to another library or data warehouse, adapters will allow you to pause the event queue while you fetch that data. This ensures that all events will contain your properties of interest.

var adapterName = 'asyncSourcePropertiesAdapter';

var addAdditionalPropertiesFromAsyncSource = function() {
  return fetchProperties()
    .then(function(properties) {
      heap.addEventProperties(properties);
      heap.markAdapterAsResolved(adapterName);
    });
};

var adapter = { name: adapterName, fn: addAdditionalPropertiesFromAsyncSource };
var config = { isBlocking: true, blockDurationInMs: 5000 };

heap.addAdapter(adapter, config);

const adapterName = 'asyncSourcePropertiesAdapter';

const addAdditionalPropertiesFromAsyncSource = async () => {
  const properties = await fetchProperties(); 
  heap.addEventProperties(properties);
  heap.markAdapterAsResolved(adapterName);
}

const adapter = { name: adapterName, fn: addAdditionalPropertiesFromAsyncSource };
const config = { isBlocking: true, blockDurationInMs: 5000 };

heap.addAdapter(adapter, config);

Redacting URL query params from all events

To remove query params for all events, we will need to transform the metadata events to redact query params. Because events inherit properties from the pageview and session events, redaction of the page information on those events will also be applied to associated track events.

var transformerName = 'redactUrlQueryFromAllEventsTransformer';
var transformerFn = function(messages) {
  var redactUrlQuery = function(message) {
    try {
      // Event is a pageview event
      if (message.type === 'meta_pageview') {
        message.data.pageview_info.url.query = '****';
      }

      // Event is a session event
      if (message.type === 'meta_session') {
        message.data.initial_pageview_info.url.query = '****';
      }

      // Event is a user event
      if (message.type === 'meta_user') {
        message.data.initial_session_info.initial_pageview_info.url.query = '****';
      }

      return message;
    } catch (e) {
      return message;
    }
  };

  return messages.map(redactUrlQuery);
};

// Using metadata pipeline since we are modifying user, session, and pageview messages
var pipeline = 'metadata';

heap.addTransformerFn(transformerName, transformerFn, pipeline);

const transformerName = 'redactUrlQueryFromAllEventsTransformer';
const transformerFn = (messages: Array<EventMessage>): Array<EventMessage> => {
  const redactUrlQuery = (message: EventMessage) => {
    try {
      // Event is a pageview event
      if (message.type === 'meta_pageview') {
        message.data.pageview_info.url.query = '****';
      }

      // Event is a session event
      if (message.type === 'meta_session') {
        message.data.initial_pageview_info.url.query = '****';
      }

      // Event is a user event
      if (message.type === 'meta_user') {
        message.data.initial_session_info.initial_pageview_info.url.query = '****';
      }
      
      return message;
    } catch (e) {
      return message;
    }
  }
   
  return messages.map(redactUrlQuery);
}
 
// Using metadata pipeline since we are modifying user, session, and pageview messages
const pipeline = 'metadata'; 

heap.addTransformerFn(transformerName, transformerFn, pipeline);

Redacting target text on interaction events for a subset of users

Assume you want to redact target text on interaction track events for users that reside in Europe. To do so, we would use adapters to call some asynchronous API to determine whether that client is from Europe and set the client state accordingly. Transformers would then consume from that client state to determine whether to redact target text.

// Create adapter to set state on whether user is from Europe
var adapterName = 'isUserInEuropeAdapter';
var isUserInEurope = function () {
  return fetchIsUserInEurope()
    .then(function (isUserInEurope) {
      window.isUserInEurope = isUserInEurope;
      heap.markAdapterAsResolved(adapterName);
    });
};

// Defaults to 5 seconds of blocking
var config = { isBlocking: true };
heap.addAdapter({ name: adapterName, fn: isUserInEurope }, config);

// Create transformer to redact target text from users in Europe
var transformerName = 'redactTargetTextForUsersInEuropeTransformer';
var transformerFn = function (messages) {
  if (!window.isUserInEurope) {
    return messages;
  }

  var redactTargetTextForUsersInEurope = function (message) {
    try {
      // Check if message is an interaction event before redacting
      if (message.data.event.interaction) {
        message.data.event.interaction.nodes[0].targetText = '****';
      }

      return message;
    } catch (e) {
      return message;
    }
  };

  return messages.map(redactTargetTextForUsersInEurope);
};

// Using default general pipeline since we are modifying track events
heap.addTransformerFn(transformerName, transformerFn);

// Create adapter to set state on whether user is from Europe
const adapterName = 'isUserInEuropeAdapter';
const isUserInEurope = async () => {
  const isUserInEurope = await fetchIsUserInEurope();
  window.isUserInEurope = isUserInEurope;
  heap.markAdapterAsResolved(adapterName);
}
// Defaults to 5 seconds of blocking
const config = { isBlocking: true };
heap.addAdapter({ name: adapterName, fn: isUserInEurope }, config);

// Create transformer to redact target text from users in Europe
const transformerName = 'redactTargetTextForUsersInEuropeTransformer';
const transformerFn = (messages: Array<EventMessage>): Array<EventMessage> => {
  if (!window.isUserInEurope) {
    return messages;
  }
  const redactTargetTextForUsersInEurope = (message: EventMessage) => {
    try {
      // Check if message is an interaction event before redactiong
      if (message.data.event.interaction) {
        message.data.event.interaction.nodes[0].targetText = '****';
      }
      
      return message;
    } catch (e) {
      return message;
    }
  }
   
  return messages.map(redactTargetTextForUsersInEurope);
}
 
// Using default general pipeline since we are modifying track events
heap.addTransformerFn(transformerName, transformerFn);