Written by The Jahia Team
   Estimated reading time:

Introduction

When integrating with some applications, you might want to use Marketing Factory’s personalization functionalities without serving full-page HTML content generated by Jahia’s Digital Experience Manager. For example, you might want to retrieve a list of images that are stored in DX, and make sure that personalization or even test optimizations (such as A/B testing) are applied to the elements of the list. In this case, the list of images is stored within DX’s CMS and the personalization configuration is stored within the data that is managed by Marketing Factory’s DX modules.

If we reduce the example from a list to a single variant element, this is actually very close to what an ad-server might look like, where you want to retrieve a single image for a specific user profile. This image will be delivered based on the criteria (such as segmentation, profile properties, location) that have been set up for personalization. We are in this scenario only interested in this (or these) resource, and not on a full-blown HTML page.

This is now possible in Marketing Factory through something called the content personalization rendering API (aka personalization API).

This API is primarily a content rendering API, and will not cover setting up or modifying existing personalization or test optimization components. These must simply be setup using Marketing Factory’s UI. This should be acceptable in most use cases, where it is acceptable to use a back-end UI to edit/setup things and then use an API to access the result of the applied personalization.

We provide two versions of this API: REST and GraphQL. We recommend using the GraphQL version as the REST version might be phased out in future versions.

Content personalization GraphQL API

In order to use the GraphQL API you will need to make sure you are using Jahia Digital Experience Manager version 7.2.3.0 or more recent. Earlier versions do not contain the GraphQL module and therefore the API will not be available.

Let's look at an example of how to use this.

Steps:

  1. Add a Personalization/Optimization to your content (using the Jahia Digital Experience Manager UI or the Content & Media Manager UI).
  2. Open the "Jahia GraphQL Core Provider : graphiql" in the dev Tools
  3. Try to use the following query if you have setup some personalization:
  4. {
      jcr {
        nodePerson: nodeByPath(path: "PATH_TO_YOUR_PERSONALIZED_NODE") {
          uuid
          name
          marketingFactory {
            personalization {
              name
              uuid
              path
              property(name: "text", language: "en") {
                value
              }
            }
          }
        }
      }
    }
    
  5. If instead you setup an optimization you can use the following query:
  6. {
      jcr {
        nodeOpti: nodeByPath(path: "PATH_TO_YOUR_OPTIMIZED_NODE") {
          uuid
          name
          marketingFactory {
            optimization {
              name
              path
              uuid
              property(name: "text", language: "en") {
                value
              }
            }
          }
        }
      }
    }

In the above example you will need to get the path to the personalized or optimized nodes. This is usually something in the form of "/sites/content/PATH" based on what you have setup in the Content & Media Manager module or in your site contents. The nodes you will retrieve using these queries are the nodes that match the personalization/optimizations that were setup in Marketing Factory's Personalization and Optimization tests UI. 

You can learn more about the GraphQL API by using the GraphiQL Docs browser and looking at the "marketingFactory" field of the JCRNode type. You can also learn more about Jahia Digital Experience Manager's GraphQL API here.

Content personalization rendering REST API

This rendering API output JSON structures, much in a similar way that DX’s REST API does, but it is a bit different in format and especially in functionality. It is also customizable since it uses DX views to perform its rendering, making it possible to adjust the final result output using custom modules (for example to customize the JSON output of a specific DX component).

Before going into the specific format of the JSON structures, let’s look at the URL format:

http://server:port/CONTEXT/PATH_TO_CONTENT.mf.json?QUERY_PARAMETERS

where:

  • CONTEXT is the context in which the DX application is deployed (by default the ROOT context is used in which case CONTEXT should be empty)
  • PATH_TO_CONTENT should be the typical path to a content object (without any extension), for example : /sites/digitall/home/texts
  • QUERY_PARAMETERS are described in the following table.
Name Value type Default value Optional Description
hidden boolean false Yes If activated, the JSON output will contain content properties marked as hidden
pretty boolean false Yes If activated, the JSON output will be indented to make it easier to read. This is useful for debugging but shouldn’t be used in production
depth integer 1 Yes This parameter controls the depth of the tree that is generated in the JSON output. Usually a depth of 2 might be useful when rendering a list of elements to avoid having to request the elements in seperate HTTP requests.
wemSessionId string null Yes This parameter contains a valid Unomi session ID (usually obtained from a previous requset). If not specified, it will first look in the server session for an object named "wemSessionId" and if it could not be found, a new sessionId will be generated by the context server. In all cases, the Unomi session ID will be part of the JSON result.
wemProfileId string null Yes This parameter contains a valid Unomi profile ID. If not specified, the API will try to find the value either in a cookie called "context-profile-id", or in a server-side session object named "wemProfileId" and setup by a previous request.
wemPersonaId string null Yes This parameter contains a valid Unomi Persona ID. If specified and valid, the persona ID will be used to perform personalization and test optimizations against the persona instead of the current profile. The list of personas may be retrieved using Unomi’s Persona API.

Here is an example of such an URL :

http://localhost:8080/sites/digitall/home/texts.mf.json?hidden=true&pretty=true&depth=5

and here in an excerpt of the result (edited for compactness):

{
  "path": "/sites/digitall/home/texts",
  "parentPath": "/sites/digitall/home",
  "identifier": "42e3db2a-6394-4f8d-9c0e-7d8ade34ca4d",
  "index": 1,
  "depth": "5",
  "nodename": "texts",
  "properties": {
    "j:nodename": "texts",
    "j:fullpath": "/sites/digitall/home/texts",
    "jcr:createdBy": "root",
    
  },
  "types": {
    "primaryNodeType": "jnt:contentList",
    
  },
  "title": "texts",
  "classes": "selectable",
  "hasChildren": true,
  "childNodes": [
    {
      "path": "/sites/digitall/home/texts/rich-text",
      "parentPath": "/sites/digitall/home/texts",
      "identifier": "9769d43a-bc76-4681-90e1-633b863a9562",
      "index": 1,
      "depth": "4",
      "nodename": "rich-text",
      "properties": {
        "j:nodename": "rich-text",
        "j:fullpath": "/sites/digitall/home/texts/rich-text",
        "jcr:createdBy": "root",
        "text": "<p><strong>Bold Text<\/strong><\/p>\n\n<p>&nbsp;<\/p>"
      },
      "types": {
        "primaryNodeType": "jnt:bigText",
        
      },
      "title": "Bold Text",
      "classes": "selectable",
      "hasChildren": false
    },
    {
      "path": "/sites/digitall/home/texts/experience-rich-text-2/rich-text-1",
      "parentPath": "/sites/digitall/home/texts/experience-rich-text-2",
      "identifier": "5ab49763-18b3-4613-973f-c056bf8a3e8b",
      "index": 1,
      "depth": "3",
      "nodename": "rich-text-1",
      "properties": {
        "j:nodename": "rich-text-1",
        "j:fullpath": "/sites/digitall/home/texts/experience-rich-text-2/rich-text-1",
        "jcr:createdBy": "root",
        "wem:tab": "",
        "wem:jsonFilter": "",
        
        "text": "<p>fallback<\/p>"
      },
      "types": {
        "primaryNodeType": "jnt:bigText",
        "mixinTypes": ["wemmix:editItem"],
        "parentPrimaryNodeType": "wemnt:personalizedContent",
        "parentMixinTypes": []
      },
      "title": "fallback",
      "classes": "selectable",
      "hasChildren": false
    },
    {
      "path": "/sites/digitall/home/texts/optimization-variant-a/variant-c",
      "parentPath": "/sites/digitall/home/texts/optimization-variant-a",
      "identifier": "3ea48051-6584-4493-9a21-edf72d98b495",
      "index": 1,
      "depth": "3",
      "nodename": "variant-c",
      "properties": {
        "j:nodename": "variant-c",
        "j:fullpath": "/sites/digitall/home/texts/optimization-variant-a/variant-c",
        
        "text": "<p>variant-c<\/p>"
      },
      "types": {
        "primaryNodeType": "jnt:bigText",
        "mixinTypes": ["wemmix:editItem"],
        "parentPrimaryNodeType": "wemnt:optimizationTest",
        "parentMixinTypes": []
      },
      "title": "variant-c",
      "classes": "selectable",
      "hasChildren": false
    }
  ],
  "wemSessionId": "1fc2bcb8-99fe-47d1-8432-213ff495d6ff",
  "wemProfileId": "0e4768b6-b129-45a7-a312-19544d112b7b",
  "wemPersonaId": ""
}

In this example, there are multiple things to note:

  • We are rendering a list of objects, so this is why we see a root object that has child nodes
  • The first child is a regular rich text object with a text value.
  • The second child is a personalized rich text object. As you can see it has a mixin type "wemmix:editItem" and a parent node type "wemnt:personalizedContent" that indicates we have actually rendered a variant of a personalized content list. Note that in this output you cannot see the top personalization node (the "wemnt:personalizedContent" personalization node) or the other variants, only the one that was rendered by Marketing Factory for the current profile is part of the output. This way integrators don’t have to understand all the inner workings of the personalization technology, they can just set it up and get the expected result in JSON format.
  • The third child is an optimization test (aka A/B testing node) node. Again a variant was selected server-side and only the selected node is part of the output. The top optimization test node and the other variants are not part of the output.

Proper session and profile tracking

Whenever interacting with Unomi’s API, Digital Experience Manager’s API or Marketing Factory’s Personalization API, you must make sure that you are tracking and transfer any personalization identifiers that you may have retrieved as part of a request’s response.

For example, if you perform a context retrieval call using Unomi’s REST API such as :

http://UNOMI_HOST:UNOMI_PORT/context.json?sessionId=MY_SESSION_ID

You will probably retrieve a profileId as a result of this context retrieval. If you provided a new session ID in MY_SESSION_ID the session will be created in Unomi with this identifier and you will need to use that for all sub-sequent calls to any APIs. For exemple, if you use the personalization REST API to render content, you will need to do something like this:

http://localhost:8080/sites/digitall/home/texts.mf.json?hidden=true&pretty=true&depth=5&wemSessionId=MY_SESSION_ID&wemProfileId=MY_PROFILE_ID

where MY_PROFILE_ID is the profile ID that was returned as a result of the request to the context.json servlet.

If instead you are using the Personalization GraphQL API, you can specify the sessionId and profileId as arguments to the marketingFactory field as in the following example:

  1. {
      jcr {
        nodePerson: nodeByPath(path: "PATH_TO_YOUR_PERSONALIZED_NODE") {
          uuid
          name
          marketingFactory(profileId: "MY_PROFILE_ID", sessionId: "MY_SESSION_ID" {
            personalization {
              name
              uuid
              path
              property(name: "text", language: "en") {
                value
              }
            }
          }
        }
      }
    }

If you do not specify a profileId or a sessionId for the GraphQL arguments that's entirely possible if you are properly managing cookies or passing them as query string parameters to the GraphQL endpoint. However if you are running into problems with session or profile tracking not working properly because of some middleware layer, it might be a good idea to use the field arguments to ensure proper tracking.

It is also recommended that you use HTTP clients that can either handle cookies properly, or you will have to manually manage the cookies for proper DX server session tracking (e.g. the JSESSIONID cookie). Just as a general rule, check whether the HTTP client you will use supports cookies out of the box, and if not, you will need to parse the returned cookies and send them back when performing requests to both DX and Unomi. The following table gives you a list of the most important cookies and their usage:

Name Origin Type Description
JSESSIONID DX string This cookie is used to track sessions server-side inside DX
context-profile-id Unomi string Used by Unomi to track the profile id. If a wemProfileId parameter is specified it will override the cookie value but it would be best that they always be in sync.

DX Login session integration

Marketing Factory also provides integration with DX’s login servlet. Again, in order for proper session and profile tracking to happen, you must add a request parameter to the /cms/login servlet when submitting a DX login. Here is an example

http://DX_HOST:DX_PORT/CONTEXT/cms/login?wemSessionId=MY_SESSION_ID

The body of the request is in typical multipart/form-data format (default type for HTTP POST Form submissions) that contains the following fields:

Name Origin Type Description
doLogin boolean Yes Must be set to "true" for the login to be performed
username string Yes The user unique name
password string Yes The user’s password
restMode boolean Yes Must be set to "true" for this use case
redirectActive boolean Yes Must be set to "false" for this use case
site string Yes The site against which to login, which is used mostly to redirect back to the site after the login. In the case of integration with Marketing Factory this is also used to know in which site to retrieve the Context Server configuration.

Please note that the required column relates to requirements for the integration with Marketing Factory. In more general uses cases the requirements are different. Please refer to the Jahia REST API documentation for more precise login servlet information.

Typical personalization flows

Introduction

In the following sections, we will describe typical personalization flows that application developers might want to integrate. Also note that while the steps are quite detailed, most of them happen automatically when using the personalization API. For example all the interactions between DX and Unomi are handled by Marketing Factory. Other steps such as the login or the session ID generation must be explicitely integrated into the custom applications. We mark the implicit operations with a [I] and the explicit operations with a [E].

Conventions

"Get personalized JSON" or "Get test optimization JSON" are not really different requests, they are both requests to the personalization API we have described previously

"DX" refers to the Marketing Factory DX modules, not the DX server by itself.

"Unomi" refers to the Apache Unomi Context Server.

Anonymous personalization

The following diagram describes the flow of a typical integration of personalization WITHOUT authentication into a custom application:

Here is a more detail description of the steps:

  1. [E] First the application generates a session ID. It may either be random, stored or anything. It is not recommended to re-use session IDs, although there could be some cases where it could be useful.
  2. [E] We can now request the personalized JSON content, passing in the session ID that was generated, as well as (optionally) any previously stored profile ID (from previous requests for example) and an optional personaId if we want to render the result for a given persona instead of the current profile.
  3. [I] The Marketing Factory DX modules collect all the filters setup for each variant, as well as the personalization configuration for each personalization content node.
  4. [I] DX then sends a request to Unomi that contains the filters to execute against the context that corresponds to the current session profile and persona).
  5. [I] The Unomi executes the filters and returns the context that contains the session and profile properties, along with the results of the filters execution.
  6. [I] The DX server uses the filter results to filter the content and returns the JSON with the filtered personalized content, as well as the sessionId, the profileId (warning it might change in the case of profile merging) and the personaId if one was used.

Authenticated personalization

The following diagram describes the flow of a typical integration of personalization WITH authentication into a custom application:

Here is a more detailed description of the steps:

  1. [E] First the application generates a session ID. It may either be random, stored or anything. It is not recommended to re-use session IDs, although there could be some cases where it could be useful.
  2. [E] This first context request is required because only the context servlet in Apache Unomi is allowed to create new sessions.
  3. [I] The context is returned as a response of the request, and it contains a JSON structure that contains the sessionId, the profileId, profile and session properties
  4. [E] A login POST request is sent to DX with the required login credentials.
  5. [I] Marketing Factory will detect the login and if it is successful, will generate a login event that it sends to Unomi. This event contains all the user properties except the password, to augment the existing Unomi profile. Also this may trigger profile merging operations inside Unomi if multiple profiles are referencing the same profile (this typically happens when a user logs in from different clients).
  6. [I] DX returns the result of login. If the login was successful it simply returns "OK" as a text response body.
  7. [E] The app issues a second context request to get the updated profile and session properties that were the result of the login request. This is necessary because the login event was send from server-to-server and we therefore don’t get access to the updated properties directly. Again the proper sessionId must be passed to the context request.
  8. [I] The updated context is returned. Important note: the profile ID might have changed because of merging. Make sure you read the profile ID and compare it to values you might have and update them with the profile ID returned in this response.
  9. [E] We can now request the personalized JSON content, passing in the session ID that was generated, as well as (optionally) any previously stored profile ID (from previous requests for example) and an optional personaId if we want to render the result for a given persona instead of the current profile.
  10. [I] The Marketing Factory DX modules collect all the filters setup for each variant, as well as the personalization configuration for each personalization content node.
  11. [I] DX then sends a request to Unomi that contains the filters to execute against the context that corresponds to the current session profile and persona).
  12. [I] The Unomi executes the filters and returns the context that contains the session and profile properties, along with the results of the filters execution.
  13. [I] The DX server returns the JSON with the personalized content, as well as the sessionId, the profileId (warning it might change in the case of profile merging) and the personaId if one was used.

Anonymous test optimization

The following diagram describes the flow of a typical integration of anonymous test optimization into a custom application:

Here is a more detailed description of the steps:

  1. [E] First the application generates a session ID. It may either be random, stored or anything. It is not recommended to re-use session IDs, although there could be some cases where it could be useful.
  2. [E] The application then sends a request for the JSON structure resulting of the test optimization for the current profile, session and optional persona selected. Note that the only required parameter is the session ID.
  3. [I] DX then asks Unomi for the current context, because it uses a session property to store the selected variant ID for the test optimization. If it is found, then we skip directly to step 5
  4. [I] Unomi sends back the context corresponding to the request session, profile and persona.
  5. [I] If a variant was already selected, it is read from the session properties that were send in the context and we then skip to step 8. If no variant was selected, DX selects a variant using the configured test optimization setup.
  6. [I] If no variant was selected, DX send a variant selection event to Unomi to notify it and any rules related to variant selection are executed.
  7. [I] If no variant was selected, DX sends a request to save the selected variant ID in the session. Note: this still will probably disappear in future versions of Marketing Factory, and directly integrated into a rule in the Unomi server.
  8. [I] The JSON content corresponding to the test optimization for the current session, profile and persona is returned, along with the identifiers for the session,profile and persona.