ÃÛ¶¹ÊÓƵ

[Integration]{class="badge positive"}

Generate Experience Platform FPIDs with AEM Sites

[AEM Sites as a Cloud Service, AEM Sites 6.5]{class="badge informative"}

Integrating ÃÛ¶¹ÊÓƵ Experience Manager (AEM) Sites delivered via AEM Publish, with ÃÛ¶¹ÊÓƵ Experience Platform (AEP) requires AEM to generate, and maintain a unique first-party device ID (FPID) cookie, in order to uniquely track user activity.

Read the supporting documentation to learn about the details of how first-part device IDs and Experience Cloud IDs work together.

Below is an overview of how FPIDs works when using AEM as the web host.

FPID and ECIDs with AEM

Generate and persist the FPID with AEM

AEM Publish service optimizes performance by caching requests as many as it can, in both the CDN and AEM Dispatcher caches.

It’s imperative HTTP requests that generate the unique-per-user FPID cookie and return the FPID value are never cached, and served directly from AEM Publish which can implement logic to guarantee uniqueness.

Avoid generating the FPID cookie on requests for web pages, or other cacheable resources, as the combination of FPID’s uniqueness requirement would render these resources uncacheable.

The following diagram describes how AEM Publish service manages FPIDs.

FPID and AEM flow diagram

  1. Web browser makes a request for a web page hosted by AEM. The request may be served using a cached copy of the web page from CDN or AEM Dispatcher cache.
  2. If the web page cannot be served from CDN or AEM Dispatcher caches, the request reaches AEM Publish service, which generates the requested web page.
  3. The web page is then returned to the web browser, populating the caches that could not serve the request. With AEM, expect CDN and AEM Dispatcher cache hit rates to be greater than 90%.
  4. The web page contains JavaScript that makes an uncacheable asynchronous XHR (AJAX) request to a custom FPID servlet in AEM Publish service. Because this is an uncacheable request (by virtue of it’s random query parameter and Cache-Control headers), it is never cached by CDN or AEM Dispatcher, and always reaches AEM Publish service to generate the response.
  5. The custom FPID servlet in AEM Publish service processes the request, generating a new FPID when no existing FPID cookie is found, or extends the life of any existing FPID cookie. The servlet also returns the FPID in the response body for use by client-side JavaScript. Fortunately the custom FPID servlet logic is lightweight, preventing this request from impacting AEM Publish service performance.
  6. The response for the XHR request returns to the browser with the FPID cookie and the FPID as JSON in the response body for use by the Platform Web SDK.

Code sample

The following code and configuration can be deployed to AEM Publish service to create an endpoint that generates, or extends the life of an existing FPID cookie and returns the FPID as JSON.

An AEM Publish HTTP endpoint must be created to generate or extend an FPID cookie, using a .

  • The servlet is bound to /bin/aem/fpid as authentication is not required to access it. If authentication is required, bind to a Sling resource type.
  • The servlet accepts HTTP GET requests. The response is marked with Cache-Control: no-store to prevent caching, but this endpoint should be requested using unique cache-busting query parameters as well.

When an HTTP request reaches the servlet, the servlet checks if an FPID cookie exists on the request:

  • If an FPID cookie exists, extend the life of the cookie, and collect its value to write to the response.
  • If an FPID cookie does not exist, generate a new FPID cookie, and save the value to write to the response.

The servlet then writes the FPID to the response as a JSON object in the form: { fpid: "<FPID VALUE>" }.

It is important to provide the FPID to the client in the body since the FPID cookie is marked HttpOnly, meaning only the server can read its value, and client-side JavaScript cannot. To avoid unnecessarily refetching the FPID on every page load, a FPID_CLIENT cookie is aso set, indicating the FPID has been generated and exposing the value to the client-side JavaScript for use.

The FPID value is used to parameterize calls using the Platform Web SDK.

Below is example code of a AEM servlet endpoint (available via HTTP GET /bin/aep/fpid) that generates or refreshes an FPID cookie, and returns the FPID as JSON.

  • core/src/main/java/com/adobe/aem/guides/wkndexamples/core/aep/impl/FpidServlet.java
package com.adobe.aem.guides.wkndexamples.core.aep.impl;

import com.google.gson.JsonObject;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.http.Cookie;
import java.io.IOException;
import java.util.UUID;

import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_PATHS;
import static org.apache.sling.api.servlets.ServletResolverConstants.SLING_SERVLET_METHODS;

@Component(
        service = {Servlet.class},
        property = {
                SLING_SERVLET_PATHS + "=/bin/aep/fpid",
                SLING_SERVLET_METHODS + "=GET"
        }
)
public class FpidServlet extends SlingAllMethodsServlet {
    private static final Logger log = LoggerFactory.getLogger(FpidServlet.class);
    private static final String COOKIE_NAME = "FPID";
    private static final String CLIENT_COOKIE_NAME = "FPID_CLIENT";
    private static final String COOKIE_PATH = "/";
    private static final int COOKIE_MAX_AGE = 60 * 60 * 24 * 30 * 13; // 13 months
    private static final String JSON_KEY = "fpid";

    @Override
    protected final void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        // Try to get an existing FPID cookie, this will give us the user's current FPID if it exists
        final Cookie existingCookie = request.getCookie(COOKIE_NAME);

        String cookieValue;

        if (existingCookie == null) {
            //  If no FPID cookie exists, create a new FPID UUID
            cookieValue = UUID.randomUUID().toString();
        } else {
            // If a FPID cookie exists, get its FPID UUID so its life can be extended
            cookieValue = existingCookie.getValue();
        }

        // Add the FPID value to the response, either newly generated or the extended one
        // This can be read by the Server (AEM Publish) due to HttpOnly flag.
        response.addHeader("Set-Cookie",
                COOKIE_NAME + "=" + cookieValue + "; " +
                        "Max-Age=" + COOKIE_MAX_AGE + "; " +
                        "Path=" + COOKIE_PATH + "; " +
                        "HttpOnly; " +
                        "Secure; " +
                        "SameSite=Lax");

        // Also set FPID_CLIENT cookie to avoid further server-side FPID generation
        // This can be read by the client-side JavaScript to check if FPID is already generated
        // or if it needs to be requested from server (AEM Publish)
        response.addHeader("Set-Cookie",
                CLIENT_COOKIE_NAME + "=" + cookieValue + "; " +
                        "Max-Age=" + COOKIE_MAX_AGE + "; " +
                        "Path=" + COOKIE_PATH + "; " +
                        "Secure; " +
                        "SameSite=Lax");

        // Avoid caching the response
        response.addHeader("Cache-Control", "no-store");

        // Return FPID in the response as JSON for client-side access
        final JsonObject json = new JsonObject();
        json.addProperty(JSON_KEY, cookieValue);

        response.setContentType("application/json");
        response.getWriter().write(json.toString());

HTML script

A custom client-side JavaScript must be added to the page to asynchronously invokes the servlet, generating or refreshing the FPID cookie and returning the FPID in the response.

This JavaScript script is typically added to the page using one of the following methods:

The XHR call to the custom AEM FPID servlet is fast, though asynchronous, so it is possible for a user to visit a webpage served by AEM, and navigate away before the request can complete.
If this occurs, the same process will reattempt on the next page load of a web page from AEM.

The HTTP GET to the AEM FPID servlet (/bin/aep/fpid) is parameterized with a random query parameter to ensure any infrastructure between the browser and AEM Publish service does not cache the request’s response.
Similarly, the Cache-Control: no-store request header is added to support avoiding caching.

Upon an invocation of the AEM FPID servlet, the FPID is retrieved from the JSON response and used by the Platform Web SDK to send it to Experience Platform APIs.

See the Experience Platform documentation for more information on using FPIDs in identityMap

...
<script>
    // Wrap in anonymous function to avoid global scope pollution

    (function() {
        // Utility function to get a cookie value by name
        function getCookie(name) {
            const value = `; ${document.cookie}`;
            const parts = value.split(`; ${name}=`);
            if (parts.length === 2) return parts.pop().split(';').shift();
        }

        // Async function to handle getting the FPID via fetching from AEM, or reading an existing FPID_CLIENT cookie
        async function getFpid() {
            let fpid = getCookie('FPID_CLIENT');

            // If FPID can be retrieved from FPID_CLIENT then skip fetching FPID from server
            if (!fpid) {
                // Fetch FPID from the server if no FPID_CLIENT cookie value is present
                try {
                    const response = await fetch(`/bin/aep/fpid?_=${new Date().getTime() + '' + Math.random()}`, {
                        method: 'GET',
                        headers: {
                            'Cache-Control': 'no-store'
                        }
                    });
                    const data = await response.json();
                    fpid = data.fpid;
                } catch (error) {
                    console.error('Error fetching FPID:', error);
                }
            }

            console.log('My FPID is: ', fpid);
            return fpid;
        }

        // Invoke the async function to fetch or skip FPID
        const fpid = await getFpid();

        // Add the fpid to the identityMap in the Platform Web SDK
        // and/or send to AEP via AEP tags or direct AEP Web SDK calls (alloy.js)
    })();
</script>

Dispatcher allow filter

Lastly, HTTP GET requests to the custom FPID servlet must be allowed via AEM Dispatcher’s filter.any configuration.

If this Dispatcher configuration is not implemented correctly, the HTTP GET requests to /bin/aep/fpid results in a 404.

  • dispatcher/src/conf.dispatcher.d/filters/filters.any
/1099 { /type "allow" /method "GET" /url "/bin/aep/fpid" }

Experience Platform resources

Review the following Experience Platform documentation for First-party device IDs (FPIDs) and managing identity data with Platform Web SDK.

recommendation-more-help
bb44cebf-d964-4e3c-b64e-ce882243fe4d