Customer Experience Advanced Topics

Custom Data Encryption

NOTE : This section explains how custom fields are handled in Pixel Apps.  The logic may change when we encrypt the custom fields that cxscript would send to an external API endpoint.

When the user’s browser receives the CXScript Javascript, embedded within is a public key (randomly assigned by the script server, but for the OptumID API endpoint will additionally have whatever public key is provided). This public key is then used to encrypt the data field of the custom data fields JSON, which is in the following format:

{
    "encryptedData": {
        "encryptionType": "ecc",
        "type": "customData",
        "clientPublicKey": "...",
        "iv": "...",
        "cipherText": "_",
        "serverPublicKey": "..."
    }
}

Example :

{
    "encryptedData": {
        "encryptionType": "ecc",
        "type": "customData",
        "clientPublicKey": "abcdefghijklmnopqrstuvwxyz",
        "iv": "nuUp3pA26DzIttUU",
        "cipherText": "y25GnJPEVlwhxcllB8kq5m02o5EGA+vlfGP/B7DU3F6T0AjSUO9bLTZxlEU87GCPcOlo8BZylpLUwYlaEZQtb2LbwKJiwhE/4tUxhmG7m77M7f5AFKLE0uoU5g==",
        "serverPublicKey": "abcdefghijklmnopqrstuvwxyz"
    }
}

The privateKeyName field specifies a md5 hash of the private key corresponding to the public key used, which is then used by the CSE transformer to identify the appropriate key for decryption. The encryptionType field specifies the type of encryption used. The default is currently ECC. The type field specifies the type of encrypted data being sent. For the purposes of custom data fields, this value will always be "customData".

In the CSE transformer, the following is the process to decode the custom data fields:

  • Parse the JSON.
  • Using the encryptedData section of the data
  • Select your private key specified by privateKeyName field and encryptionType field.
  • Iterate through each chunk of the payload array:
    • Base64 decode that chunk
    • Decrypt it using your private key, specified by privateKeyName
    • Join the decrypted chunks, into a single string
    • JSON parse that string.
The CX Script is asynchronous. You cannot use window.Rakanto.event('sendCustomData') right after the call to bootstrap the script, the script hasn't been downloaded, and the  method hasn't been loaded yet. You CAN however send events using the following method.

NOTE THE Rakanto() method at the bottom.

```html
<html>
  <head>
    <title>CX Script Page</title>
    <script type='text/javascript'>
      sessionStorage.setItem('darids','UHG.Optum.Pixel.CXScriptExample.CustomData');
      (function (cx, darids, demarcs) {
        window['RakantoObject'] = 'Rakanto';

        window['Rakanto'] = window['Rakanto'] || function () {
          (window['Rakanto'].q = window['Rakanto'].q || []).push([Date.now()].concat(Array.prototype.slice.call(arguments)))
        }, window['Rakanto'].l = Date.now(),window['Rakanto'].demarcs = demarcs;

        a = document.createElement('script'),
        a.src = cx,
        a.id = 'rakanto',
        a.async = true,
        (darids)? a.setAttribute('data-px-darids', darids):null,
        m = document.getElementsByTagName('script')[0],
        m.parentNode.insertBefore(a, m);
        })('https://repo-stg.rakanto.com/rakanto/cx/cx.js');
      Rakanto('sendCustomData', {}, {namespace:'UHG.Optum.Pixel.CXScriptExample.CustomData', foo: 'bar'});
    </script>
  </head>
  <body>
      <h1>Pixel Customer Experience Script</h1>
      </p>(This example will also send custom data upon initialization)</p>
  </body>
</html>
```

Sending a JSON object as the value in a custom field

You are allowed to send any supported javascript datatype as the value in a custom field.

window.Rakanto.event("sendCustomData", { namespace: "UHG.Optum.ETIPS", myCar: {make: "saab",newHotness: "yep", convertible: true } )

The custom fields will be presented in kibana in their entireity, in this case an object to, CF_UHG.Optum.ETIPS.myCar: {make: "saab",newHotness: "yep", convertible: true } and sub values to, CF_UHG.Optum.ETIPS.myCar.make = 'saab', CF_UHG.Optum.ETIPS.myCar.newHotness = 'yep', CF_UHG.Optum.ETIPS.myCar.convertible = true.

Recent versions of Safari, Firefox and other browsers have implemented privacy enhancing features. These changes will prevent tracking users across sites or storing JavaScript set cookie data for more than a day on Safari. The CX script will continue to work as before when browsing on Chrome, however Google have plans to implement these features in the near future.

The most significant barrier in tracking users across sites is cookie partitioning. Cookies are small bits of data which are set by the first or third party server or in the page JavaScript. We store a unique ID to track a user's journey in cookies. Cookie partitioning severely restricts 3rd party cookie functionality. In the past a third party cookie set on site "optum.com" could be read on site "uhg.com", this is no longer the case in Safari. Cookie partitioning restricts cookies set on a site to be specific to that domain alone. The files are cached in the namespace for the specific domain you are visiting. Cookies are unique to the current domain. In addition, cookies expiration has been changed. 3rd party server/JavaScript set cookies now may only exist for a maximum of 1 day, First party Javascript set cookies 1 week, and unlimited days for server set cookies. This change will pose a challenge for tracking a user who browses the site, every few days, only eventually logging in.

Partitioning is meant to block 3rd party scripts from setting/reading cookies across domains. Pixel has been updated to set its required tracking Id in the first party domain. For browsers affected by partitioning, Pixel tracking cookies will have an expiration of 1 week from the most recent visit.

If you would like a longer cookie duration, create a CNAME subdomain for your domain. The new subdomain you create should be called pixel-repo.YOURSITE.COM => it should be an alias for, or a proxy to repo.rakanto.com. Then in the invocation code, set the flag: "enable_extended_tracking_cookie=true". The script will automaticly attempt to get a longer duration cookie set at pixel-repo.YOURSITE.COM.

If you see errors in the network tab, with addresses such as: /cookie?ubrid=v2.0-dd5085874041091045b30964c8b28043-1363-1369-1670812376128-0000803080-1673452061936 The cname alias isn't in place, yet the script is trying it. In this case remove the enable_extended_tracking_cookie flag, or set it to false.

for example:

  <head>
    <script type='text/javascript'>
        window.localStorage.setItem("DARID","UHG.Optum.Rx.MemberPortal.Stage");
            (function (cx, darids, demarcs) {

                window['Rakanto'] = window['Rakanto'] || function () {
                    (window['Rakanto'].q = window['Rakanto'].q || []).push([Date.now()].concat(Array.prototype.slice.call(arguments)))
                },
                window['Rakanto'].l = Date.now() ,window['Rakanto'].demarcs = demarcs,
                window['Rakanto'].enable_extended_tracking_cookie = true;  // have the CX script use the CNAME subdomain to extend the life of the cookie.

                a = document.createElement('script'),
                a.src = cx,
                a.id = 'rakanto',
                a.async = true,
                (darids)? a.setAttribute('data-px-darids', darids):null,
                m = document.getElementsByTagName('script')[0],
                m.parentNode.insertBefore(a, m);
            })('https://repo-stg.rakanto.com/rakanto/cx/cx.js', 'UHG.Optum.Rx.MemberPortal.Stage',['/member/signin?v=3','https://st1.healthsafe-id.com/rt/username/orx/en']);
      </script>
  </head>
Make a C record in the DNS entry.

pixel-repo.YOURDOMAIN  is an alias for repo.rakanto.com

The cx script will ping an endpoint at pixel-repo.YOURDOMAIN in an attempt to set the cookie longer than a week. If the CNAME isn't present the ubrid will expire after one week. If the CNAME isn't present, the cx script will STILL attempt to call pixel-repo.YOURDOMAIN. In this case, you will see errors regarding the non-resolving link.

UHG has many domains, these domains often link to one another. The organization wants a user's journey to be tracked from as they navigate between sites. The only way to send identifying information from one site to another in Safari is via a querystring. To enable this, Pixel can automatically decorate the outbound queyrstring with the tracking Id.

To use this feature, provide the pixel script an initialization list of "demarcs" or demarcation URLs. When the browsers "load" event is fired, links specified in the "demarcs" list are appended with tracking queryparams

Ex. "https://another-domain.com/deeplink?ref_rakantoid=v2.0-4ec78459be48f63a57f7b1a54db37ef4-4800-4803-1661802403458-0000000252-1661803310002" .

If the target site is instrumented with the cx script, the ref_rakantoid from the first site will be used as the UBRID from that point forward. A "referrerUbrid" event will be sent indicating the OriginalUbrid, and the New Ubrid for that user.

Please consider using "demarcs" when calling HSID for login. Doing so enables user information, such as username, to be added to future "logged in" client side events. You will then be able to identify users by username on their journey through your site.

Example with 2 demarcs, a relative one where your site handles a 302 redirect, '/member/signin?v=3', and a direct one 'https://st1.healthsafe-id.com/rt/username/orx/en' if either site contain query parameters, the UBRID will be appended to those. The demarcs must be a left anchored match of the domain (if provided) and the pathname.

The following example will work on a optumrx page, appending the  the 'signin' links, and 'forgot username' links with the querystring.

https://chp-stage.optumrx.com/public/landing

<html>
  <head>
    <script type='text/javascript'>
        window.localStorage.setItem("DARID","UHG.Optum.Rx.MemberPortal.Stage");
            (function (cx, darids, demarcs) {
                window['RakantoObject'] = 'Rakanto';

                window['Rakanto'] = window['Rakanto'] || function () {
                    (window['Rakanto'].q = window['Rakanto'].q || []).push([Date.now()].concat(Array.prototype.slice.call(arguments)))
                }, window['Rakanto'].l = Date.now(), window['Rakanto'].demarcs = demarcs;

                a = document.createElement('script'),
                a.src = cx,
                a.id = 'rakanto',
                a.async = true,
                (darids)? a.setAttribute('data-px-darids', darids):null,
                m = document.getElementsByTagName('script')[0],
                m.parentNode.insertBefore(a, m);
            })('https://repo.rakanto.com/rakanto/cx/cx.js', 'UHG.Optum.Rx.MemberPortal.Stage',['/member/signin?v=3','https://st1.healthsafe-id.com/rt/username/orx/en']);
      </script>
  </head>

  <body>
      <h1>Pixel Customer Experience Script</h1>
      <h2>Setting demarc endpoints</h2>
  </body>
</html>

Multiple API endpoints

The endpoint must respond to POST and GET requests. The POST returns a '{}', the GET returns a PNG image (a single pixel).

For example here is our Openresty (nginx configuration)

location /cx_collector/ {
set $json_log '';
# Add headers to the response
add_header Access-Control-Allow-Origin *;
# nginx_node_id:  unique identifier of the nginx node - meaningless value.  It is set in the host_vars/{host} file
# Is to make it easy to see which NGINX node the request went through to help tracing and troubleshooting
add_header  X-ps-id  {{nginx_node_id}};
client_max_body_size {{max_request_body_size}};
client_body_buffer_size {{max_request_buffer_size}};

# This handles the request counter increment
rewrite_by_lua_file request_helper.lua;

# https://agentzh.blogspot.com/2011/03/how-nginx-location-if-works.html
if ($request_method = POST ) {
    echo_read_request_body;
    echo "{}";
}

if ($request_method = GET ) {
    rewrite ^ /pixel.png break;
}

# Create the CSEvent
log_by_lua_file log_helper.lua;
}

To send data to an additional API endpoint ( yours ) simply add the code mentioned in the above scriptlet WITH A TRAILING SLASH, // Add this if you want to send data to your own endpoint as well. window['Rakanto'].Endpoints = [{ api_endpoint: 'https://YOUR-ENDPOINT/', public_key: 'YOUR-KEY-TEXT', encryption_type: 'ecc' }];

Scriptlet

Scriplet is a mechanism used by Authentication Services like HSID and OptumID. Scriplet takes the UBRID and other information and makes it available in a cookie, and allows sending data to multiple endpoints.

In the event the RakantoClientSideData cookie value cannot be accessed in the backend code, please add the following logic:

document.cookie = 'RakantoClientSideData=' + btoa(JSON.stringify(value)) + '; expires=' + now.toUTCString() +';path=/';

btoa function is added to encode the JSON string to Base64 string that would be decoded again to the original string in (for example) Java code.

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Scriptlet Sample Code</title>

    <script type='text/javascript'>
        window.optumPageDataLayer = {
        darids: ['UHG.Optum.Pixel', 'UHG.Optum.Pixel.CXScriptTest.{{app_env}}', 'UHG.Optum.Pixel.CXScriptTest.{{app_env}}.DataLayer','UHG.Optum.Pixel.CXScriptTest.{{app_env}}.CustomData','UHG.Optum.Pixel.CXScriptTest.{{app_env}}.Async']
        };

        (function (cx, darids, a, m) {
        window['RakantoObject'] = 'Rakanto';
        window['Rakanto'] = window['Rakanto'] || function () {
        (window['Rakanto'].q = window['Rakanto'].q || []).push([Date.now()].concat(Array.prototype.slice.call(arguments)))
        },
        window['Rakanto'].l = Date.now();

        //Scriptlet Begin
        Object.defineProperty(window['Rakanto'],'ClientSideData',{
            _ClientSideData: {},
            get() { return this._ClientSideData; },
            set(value) {
            this._ClientSideData = value;
            var now = new Date();
            var minutes = 30;
            var expireTime = now.getTime() + minutes * 60 * 1000;
            now.setTime(expireTime);
            document.cookie = 'RakantoClientSideData=' + JSON.stringify(value) + '; expires=' + now.toUTCString() +';path=/';
            }
        });


        // Add this if you want to send data to your own endpoint as well.
        window['Rakanto'].Endpoints = [{
                            api_endpoint: 'https://YOUR-ENDPOINT/',
                            public_key: 'YOUR-KEY-TEXT',
                            encryption_type: 'ecc'
                        }];];

        //Scriptlet End

        a = document.createElement('script'), m = document.getElementsByTagName('script')[0];
        a.id = 'rakanto';
        a.src = cx;
        if (darids){a.setAttribute('data-px-darids', darids)};
        a.async = 1;
        m.parentNode.insertBefore(a, m)
        })('https://repo-stg.rakanto.com/rakanto/cx/cx.js');

        Rakanto('sendCustomData', {}, {namespace:'UHG.Optum.Pixel.CXScriptTest.stage.CustomData',foo: 'bar'});
    </script>
    </head>
    <body>
            <p>CX Script Scriplet Example!</p>
    </body>
</html>

JSON format

Here is an example below of a JSON message that the browser will send while running the CXScript.

{
    "browserEvent": "sendCustomData",
    "browserEventTimestamp": 1755535058943,
    "pixelSessionId": "op-e8ae8461-e423-4b33-81e9-a5d97a9e11c5",
    "ubrid": "v2.0-3b5b2a48f86c6480d1253740469349b0-1166-1470139-1747630801963-0000047051-1747678518377",
    "cookiesEnabled": "Y",
    "networkLatencyMillis": 66,
    "timeOnPageTotalMillis": 8771,
    "timeOnSessionMillis": 8771,
    "cxScriptMetadata": {
        "cxScriptGitHash": "602314b",
        "cxScriptDownloadTime": 332,
        "cxScriptExecutionTimeMillis": 10,
        "cxScriptVersion": "4.1.0"
    },
    "mid": "",
    "screenRes": "982x1728",
    "colorDepth": 24,
    "referralURL": "",
    "browserWidth": 1511,
    "browserHeight": 529,
    "event_enqueued_count": 5,
    "event_sent_count": 5,
    "darids": [
        "UHG.Optum.Pixel",
        "UHG.Optum.Pixel.CXScriptTest.dev",
        "UHG.Optum.Pixel.CXScriptTest.dev.DataLayer",
        "UHG.Optum.Pixel.CXScriptTest.dev.CustomData",
        "UHG.Optum.Pixel.CXScriptTest.dev.Async",
        "UHG.Optum.Pixel.SelfService"
    ],
    "encryptedData": {
        "encryptionType": "ecc",
        "type": "customData",
        "clientPublicKey": "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEIocPulYmS1qBlGszphHzt2pXTEpZp1+8qODmZCdb1bem3pKUq5thQkAM+p2YPfM2eXGWKOQI4jLXm4oFdCWOcH0EKCZ6ry0DfnmfNw+jjrF4EStdHPEQ9/QSPxDqjEG6",
        "iv": "N61tCvqx/HrEWO80",
        "cipherText": "ZzbkSk/mvhinFMPI2f6eDwYRSwJpD3P8WW8z0utlxXFhusvw8MMvMVXmywzPU5SdeegZqV2altnO5nmyIcPDmNeXyHm0wJjyUi2Ujhx2ky3tLPqbU9hKpIc9fA==",
        "serverPublicKey": "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAELBaIFhbuCjbIMa2Qu8q80o0px6oJZKQ9IBhmhBuVFMx/uUv0qJtspuTx6moNRNXUrL+ZVO/TUDFFaf/KZh7hu5ALPQ10WENbW+4oIot+CWcT1tts4kc1a4S0IzXhsLMb"
    }
}

Manually sending user identity

Some end users have supplemental identity profiles they would like to track. For instance, a company which is acquired by a larger entity might like to capture, the old identity as well as the new one when tracking metrics. To capture the additional authorization system and user information, call the method below, and pass in a userIdentity object.

{ authSystem: "Active_directory" userId: "bob.dobbs", ...otherUserInformation }

Required keys in the userIdentityObject are 'authSystem', and 'userId', otherUserInformation are additional key value pairs will be passed along as well. The data is sent as a 'setUserIdentity' event. Data contained in the event will supplement the user lookup service, and add these parameters to the user information, viewable in Kibana.

Multiple userIdentityObjects may be sent in. You might want to do this if you have multiple authSystems a user is associated with, and you need to capture an additional userId for the additional system.

    window.Rakanto.event("setUserIdentity",{ authSystem: "Active_directory" userId: "bob.dobbs", ...otheruserinformation });

    the userIdentity is { authSystem: "Active_directory" userId: "bob.dobbs" }
    one or more userIdentityes may be sent at a time.

    window.Rakanto.event("setUserIdentity",{ authSystem: "ActiveDirectory" userId: "bob.dobbs" }, { authSystem: "LethargicDirectory" userId: "bob.dobbs2" });

Explanation of JSON Fields:

browserEvent

  • event: Fired for all events.
  • The event which the CX Script is responding to. The event can be either:
    • click ( when something is clicked, we currently are not supporting this)
    • load (after page is rendered and visible to user.)
    • startPageLoad (when page first starts to be loaded and our script gets first loaded for that page. This is why you want the script at the top of the header section, it will enable you to find out now long your dependent assets take to download.)
    • customData Accompanies all other Pixel requests, containing custom data
    • visit-timeout ( when a page times out, session expired)
    • xmlHttpLoaded ( used for tracking XHR requests, may or may not be enabled. )

browserEventTimestamp

  • event: Fired for all events.
  • description: When the event was fired, in milliseconds since epoch

customData

  • event: Fired alongside all events, inside the sendCustomData event
  • description: Contains arbitrary data specified by the page, which is then encrypted in the browser before being sent to Pixel, as described above

networkLatencyMillis

  • event: Fired for all events.
  • description: Time delta between unload of previous page, and startPageLoad of current one. A measure for the most recent of how long it takes from the browser to make a call to the CSE collector, and get a returned HTTP response. Indication of how much latency there is between a user's browser and the pixel web server handling the request.

optumPixelId

  • event: Fired for all events.
  • description: Deprecated, but remains for now. UUID generated for this user for this session. Ubrid will supersede this, or perhaps this will become the value of ubrid.

pxPageCount

  • event: Fired for all events.
  • description: Number of times this page has been visited during the current session

pxEventCount

  • event: Fired for all events.
  • description: Number of events fired so far during the current page visit.

timeBetweenPages

  • event: load
  • description: Intrapage load time, fired on load event. Gives the delta from unload to render of next page. Time it took to click a link and go to next page and start to render the page.

timeOnPageRender

  • event: load
  • description: delta between when landed on page and load (page rendered) event fired. How long between when you landed on the page and the page was rendered and visible to the user.

timeOnPageTotal

  • event: Fired for all events.
  • description: time since script was parsed.

mid

  • event: Fired for all events.
  • description: memberid if available in cookie

timeOnSite

  • event: Fired for all events.
  • description: Total time in the site for this browser session. Calculated as, time from the first page viewed, and the last page viewed before leaving site or timing out of the session.

xmlHttpLoaded

  • event: all xhr requests ( ajax calls )
  • description: Returns the URL of any AJAX calls, as well as when they were sent.

Ubrid

  • event: Fired for all events.
  • description: The universal browser Id, a unique identifier for the browser to persist across sessions and sites.

HttpURL

  • event: Fired for all events.
  • description: Returns the page the script is firing from

CXScriptGitHash

  • event: Fired for all events.
  • description: Returns the Git Hash of the CX Script

PreviousPageHostAndPath

  • event: startPageLoad
  • description: Returns the URL of the previous page visited, instrumented with CX Script

CXScriptDownloadTime

  • event: Fired for all events.
  • description: Time taken by the page to download the CX Script

userPageHistory

  • event: startPageLoad

  • description: when the script is first parsed. A list of the last 10 or fewer URLs visited and the timestamp of when they visited it by the user this session as a JSON array. pageUrl is the page viewed. pageViewDurationMillis is time on page updated every 1/10th second. unixTimestampMillis is milliseconds since Jan 1, 1970. Ex:

      "userPageHistory": [
        {"pageUrl":"<https://rcpt-cs-tms.ocp-ctc-core-nonprod.optum.com/ubes/>","pageViewDurationMillis": 1000,""unixTimestampMillis": 1582050811076},
        {"pageUrl":"<https://rcpt-cs-tms.ocp-ctc-core-nonprod.optum.com/ubes/>","pageViewDurationMillis": 1000,""unixTimestampMillis": 1582050862150}
        ]