Consent Tracking in SFRA

In Commerce Cloud, merchants can track personal information about their shoppers to improve their shopping experience. As a merchant, you can collect and honor your shoppers’ consent preferences when they are using your site.

The following example shows one way to implement consent tracking on a storefront adapted from SFRA and is provided for informational purposes only. As always, merchants can consult their own legal department or advisors to review and consent management process.

The implementation uses:

Tracking Preference

The Tracking site preference determines the default tracking behavior. When set to Opt-in, personal information is not tracked by default for all shoppers visiting the site; when not set to Opt-in, personal information is tracked.

To set this preference, select Merchant Tools > site > Site Preferences > Privacy.

The example assumes the Tracking preference is set to Opt-In.

Session Tracking Flag

The example presents a consent request message to the shopper, who can choose to allow tracking. If the shopper allows tracking, it is enabled during the shopper’s session. For more information, see Browser-Based Local Data Storage.

To enable tracking on a session:

To check the current value of the session's tracking flag:

Implementation Overview

The sample implementation adds a <span> tag to every page in the storefront. The <span> tag stores information about the shopper’s consent choices and makes the information available to client-side code running in the browser. If the shopper has not yet given consent, the client-side code inspects the <span> tag and displays a Tracking Consent window.

The <span> tag is created by the template app_storefront_base/cartridge/templates/default/common/consent.isml.

<span class="api-${pdict.consentApi} ${pdict.tracking_consent == null ? '' : 'consented' } tracking-consent" 
    data-url="${URLUtils.url('ConsentTracking-GetContent', 'cid', 'tracking_hint')}"
    data-reject="${URLUtils.url('ConsentTracking-SetSession', 'consent', 'false')}"
    data-accept="${URLUtils.url('ConsentTracking-SetSession', 'consent', 'true')}"
    data-acceptText="${Resource.msg('button.consentTracking.yes', 'common', null)}"
    data-rejectText="${Resource.msg('button.consentTracking.no', 'common', null)}"
    data-heading="${Resource.msg('heading.consentTracking.track.consent', 'common', null)}"
    ></span>

This template is rendered by the ConsentTracking-Check route in the app_storefront_base/cartridge/controllers/ConsentTracking.js controller.

server.get('Check', consentTracking.consent, function (req, res, next) {
    res.render('/common/consent', {
        consentApi: Object.prototype.hasOwnProperty.call(req.session.raw, 'setTrackingAllowed')
    });
    next();
});

This route is called every time a storefront page is rendered. In the route, the middleware step consentTracking.consent is invoked before the consent.isml template is rendered.

This middleware step is implemented in the server-side script app_storefront_base/cartridge/scripts/middleware/consentTracking.js.

'use strict';

/**
 * Middleware to use consent tracking check
 * @param {Object} req - Request object
 * @param {Object} res - Response object
 * @param {Function} next - Next call in the middleware chain
 * @returns {void}
 */
function consent(req, res, next) {
    var consented = req.session.privacyCache.get('consent');
    if (consented === null || consented === undefined) {
        req.session.privacyCache.set('consent', null);
    } else if (consented === false) {
        req.session.privacyCache.set('consent', false);
        req.session.raw.setTrackingAllowed(false);
    } else if (consented === true) {
        req.session.privacyCache.set('consent', true);
        req.session.raw.setTrackingAllowed(true);
    }

    res.setViewData({
        tracking_consent: req.session.privacyCache.get('consent')
    });
    next();
}

module.exports = {
    consent: consent
};

It checks the value of the session's consent property. Regardless if the value of the property is either true or false, it calls the setTrackingAllowed() method on the session to record the shopper’s choice.

It then adds the value of the consent property to the response's view data, storing it in the tracking_consent property. The tracking_consent property is made available to the consent.isml template via the pdict property.

The first time this code is executed in a session, the value of the session's consent property is null, so the tracking_consent property is also null. Because the value of the tracking_consent property is null, the first line in the consent.isml template:

<span class="api-${pdict.consentApi} ${pdict.tracking_consent == null ? '' : 'consented' } tracking-consent"

Outputs the following HTML:

 <span class="api-true tracking-consent" ...

In this output, the consented attribute value is absent, and its absence indicates to the client-side code that the shopper has not yet consented to tracking. The absence of this attribute causes the client-side code to display the Tracking Consent window, prompting the shopper to grant or deny consent.

After the shopper grants or denies consent, subsequent invocations of the consentTracking.consent middleware step set the tracking_consent property to a non-null value. As a consequence, the resulting HTML output looks like this code snippet:
 <span class="api-true consented tracking-consent" ...

In this output, the consented attribute value is present, which indicates to the client-side code that the shopper has either granted or denied access to tracking. Whichever choice the shopper made, it is no longer necessary to display the Tracking Consent window.

Every time the consent.isml template is rendered, the consentApi property is passed into the template via the pdict property. The value of the consentApi property is the result of the following call:

Object.prototype.hasOwnProperty.call(req.session.raw, 'setTrackingAllowed') 

The call checks if the session has a setTrackingAllowed property. If it does, this call resolves to true; otherwise, false. This call always evaluates to true for Salesforce B2C Commerce version 18.4 and later.

The client-side script, app_storefront_base/cartridge/client/default/js/components/consentTracking.js, inspects the <span> tag created by the consent.isml template.

'use strict';

/**
 * Renders a modal window that tracks the shoppers consenting to accepting site 
 * tracking policy
 */
function showConsentModal() {
    var urlContent = $('.tracking-consent').data('url');
    var urlAccept = $('.tracking-consent').data('accept');
    var urlReject = $('.tracking-consent').data('reject');
    var textYes = $('.tracking-consent').data('accepttext');
    var textNo = $('.tracking-consent').data('rejecttext');
    var textHeader = $('.tracking-consent').data('heading');

    var htmlString = '<!-- Modal -->'
        + '<div class="modal show" id="consent-tracking" role="dialog" style="display: block;">'
        + '<div class="modal-dialog">'
        + '<!-- Modal content-->'
        + '<div class="modal-content">'
        + '<div class="modal-header">'
        + textHeader
        + '</div>'
        + '<div class="modal-body"></div>'
        + '<div class="modal-footer">'
        + '<div class="button-wrapper">'
        + '<button class="affirm btn btn-primary" data-url="' + urlAccept + '">'
        + textYes
        + '</button>'
        + '<button class="decline btn btn-primary" data-url="' + urlReject + '">'
        + textNo
        + '</button>'
        + '</div>'
        + '</div>'
        + '</div>'
        + '</div>'
        + '</div>';
    $.spinner().start();

    $('body').append(htmlString);
 
    $.ajax({
        url: urlContent,
        type: 'get',
        dataType: 'html',
        success: function (response) {
            $('.modal-body').html(response);
        },
        error: function () {
            $('#consent-tracking').remove();
        }
    });

    $('#consent-tracking .button-wrapper button').click(function (e) {
        e.preventDefault();
        var url = $(this).data('url');
        $.ajax({
            url: url,
            type: 'get',
            dataType: 'json',
            success: function () {
                $('#consent-tracking .modal-body').remove();
                $('#consent-tracking').remove();
                $.spinner().stop();
            },
            error: function () {
                $('#consent-tracking .modal-body').remove();
                $('#consent-tracking').remove();
                $.spinner().stop();
            }
        });
    });
}

module.exports = function () {
    if ($('.consented').length === 0 && $('.tracking-consent').hasClass('api-true')) {
        showConsentModal();
    }

    if ($('.tracking-consent').hasClass('api-true')) {
        $('.tracking-consent').click(function () {
            showConsentModal();
        });
    }
};

This script constructs a section of HTML, stores it in the variable htmlString, and appends it to the <body> element of the DOM. The script then performs an Ajax call to get the value of the tracking_hint content asset. If the call is successful, the script inserts the value of the content asset into the <div> tag whose class attribute is modal-body. If the call fails, the script removes the entire section of appended HTML from the DOM.

The script then creates an event listener on both <button> elements in the appended HTML. If the shopper clicks either button in the Tracking Consent window, the script makes a second Ajax call, either enabling or disabling tracking on the session. The script then removes the Tracking Consent window from the DOM.

Lastly, this script exports a function whose purpose is to conditionally call the showConsentModal() function. The exported function is invoked in two situations: after each storefront page is fully loaded in the browser window, and when the shopper clicks Consent to Track in the Edit Profile form on the My Account page. For more information, see the app_storefront_base/cartridge/templates/default/account/editProfileForm.isml template.