Source: listeners/listener.js

/**
 * Base/Abstract class for all events. When the `attach` method is called, it creates an event listener.
 * When the event is triggered, it pushes the event payload to the Google Tag Manager `dataLayer`.
 *
 *  - To create a new type of event listener, extend this class and override the `eventName` property.
 *   - override `listen` to define how the event listener is attached. (optional)
 *   - override `createPayload` to define how the event data is constructed. (optional)
 *   - assign `onTrigger` to add side effects when the event is triggered. (optional)
 * @class Listener
 */
export default class Listener {
    /**
     * The name of the event that is pushed to GTM
     * @type {String}
     * @static
     * @memberof Listener
     */
    eventName = 'event';

    constructor() {
        this.effects = [];
    }

    /**
     * Adds a callback to be executed when the event is triggered.
     * The callback will have access to the event payload object.
     * @param {function(Payload): void} effect
     * @memberof Listener
     */
    set onTrigger(effect) {
        this.effects.push(effect);
    }

    /**
     * Call this method to attach the event listener.
     * It wraps the `listen` method and handles any errors thrown.
     * This allows us to safely attach event listeners and monitor for failures in Sentry.
     * @public
     * @method attach
     * @returns {void}
     * @memberof Listener
     */
    attach() {
        try {
            this.listen();
        } catch (e) {
            const newError = new Error(`Failed to attach listener: ${this.eventName}\nMessage: ${e.message}`);
            captureException(newError);
        }
    }

    /**
     * Should not be called directly: use `attach()` instead.
     * Defines the logic to trigger the event. Typically this will add an event listener or observer.
     * Subclasses should override this method.
     * - NOTE: Don't handle errors in this method. Allow them to fail loudly so they can be captured in the `attach` method.
     * @public
     * @method listen
     * @returns {void}
     * @memberof Listener
     */
    listen() {
        this.trigger();
    }

    /**
     * Creates an object of event data to be sent to Google Tag Manager.
     * This is called when the event is triggered along with any arguments passed to the `trigger` method.
     * Subclasses should override this method.
     * - NOTE: Don't handle errors in this method. Allow them to fail loudly so they can be captured in the `trigger` method.
     * @public
     * @method createPayload
     * @memberof Listener
     * @param  {...any} args - arguments needed to create the payload
     * @returns {Payload} - the event data to send to GTM
     */
    createPayload() {
        return {};
    }

    /**
     * Sends the event payload to Google Tag Manager and calls any side effect callbacks assigned to `onTrigger`.
     * This should be called somewhere in the `listen` method when the event is triggered.
     * Any args will be passed to the `createPayload` method.
     * @private
     * @param  {...any} args - arguments to pass the `createPayload` method
     */
    trigger(...args) {
        try {
            const payload = {
                event: this.eventName,
                ...this.createPayload(...args)
            };
            dataLayer.push({ ecommerce: null });
            dataLayer.push(payload);
            this.effects.forEach(
                callback => callback(payload)
            );
        } catch (error) {
            const newError = new Error(`Failed to send event data: ${this.eventName}\nMessage: ${error.message}`);
            captureException(newError);
        }
    }
}

/**
 * Handles errors by logging them to the console and sending them to Sentry.
 * @param {any} error - the error to capture
 */
function captureException(error) {
    console.error(error);
    if (typeof Sentry !== 'undefined') Sentry.captureException(error);
}