How to auto-logoff the window for cases open in a new browser tab using Theme-Cosmos

One of the critical feature in Theme-Cosmos is the ability to open work objects in separate browser tabs.

browser tabs.gif

Any link that renders the ‘preview’ button on hover will generate a valid ‘href’ link that the browser can use to open the case in a new browser tab. To open the case in a new tab, you can use ‘right click → open in new tab’ or ‘Cmd click’

One of the issue is that these tabs becomes independent. Each tab runs on a separate thread with a uniquely generated thread name. There is no communication between these tabs. As such if the user logs out from one tab, the other tabs will continue to be visible even if the session is no longer valid. Doing any action or a refresh will automatically bring back the user to the login page.

In this post, we are going to look at ways to bring some communication between these tabs. The main focus is the logoff use case, but other message could be sent to.

The method used in this post is based on the broadcast channel API - this API is not available on Safari or IE11. Another option is to use localstorage to send message. For more details - see https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API

Note: The code and example were tested on Pega 8.6 and Theme-Cosmos 3.0. Some changes might be required with older version of the Pega Platform.

1/ Simple synchronization between tabs for logoff

To automatically reload the other browser tabs when a logout has been executed, you can add the following code to a JS file attached to your portal harness. The window.location.reload will trigger a browser reload on the other tabs and should bring back each tab to the login page.

var PegaChannel = new BroadcastChannel('pega-data');

pega.ui.EventsEmitter.subscribe("BeforeLogOff", function() {
     PegaChannel.postMessage('logoff');
}, null, null, null);
PegaChannel.addEventListener('message', function(event) {
    if (event.data === 'logoff') {
        setTimeout(function() { window.location.reload(); }, 1000);
    }
});

2/ Using pxSessionTimer

If you want to auto-logoff a user after a certain period of inactivity, you can use the pxSessionTimer. Include the non-auto section into your portal header (UserHeader). One of the challenge using pxSessionTimer is that each tab will run independently and if one of the tab is not used for a certain period of time, it will trigger automatically a logout even if the other tabs are being actively used.

We will extend the code above to provide more synchronization between each tab and use the broadcast channel to use for both the logoff event as well as to reset the timer.

Here is the code to add to your portal header

var PegaChannel = new BroadcastChannel('pega-data');
var skipResetTimeoutWarning = false;

function closeLogoffTimer() {
    for (var i = 0; i < window.frames.length; i++) {
        if (window.frames[i].frameElement && window.frames[i].frameElement.src && window.frames[i].frameElement.src.indexOf(
                "ShowLogoffTimer") !== -1 && typeof window.frames[i].window.closeModal === "function") {
            try {
                window.frames[i].window.closeModal();
            } catch (e) {}
        }
    }
}

pega.ui.EventsEmitter.subscribe("BeforeLogOff", function() {
    PegaChannel.postMessage('logoff');
}, null, null, null);
PegaChannel.addEventListener('message', function(event) {
    console.log("PegaChannel event", event);
    if (event.data === 'logoff') {
        closeLogoffTimer();
        setTimeout(function() { window.location.reload(); }, 1000);
    } else if (event.data === 'restartTimeoutWarning') {
        closeLogoffTimer();
        desktop_restartTimeoutWarningTimer(true);
    }
});

function sendRestartTimeoutWarningMessage() {
    PegaChannel.postMessage('restartTimeoutWarning');
}

function desktop_restartTimeoutWarningTimer(skipBroadcast) {
    if (!skipBroadcast && skipResetTimeoutWarning) {
        skipResetTimeoutWarning = false;
        return;
    }
    if (pega.desktop.TimeoutTime && pega.desktop.TimeoutTime > 0) {
        var nTimeoutWarningTime = (pega.desktop.TimeoutTime - pega.desktop.TimeoutWarningWindow) * 60000;
        console.log("desktop_restartTimeoutWarningTimer - clear timer");
        clearTimeout(pega.desktop.TimeoutWarningCountdown);
        if (!skipBroadcast) sendRestartTimeoutWarningMessage();
        if (nTimeoutWarningTime >= 0) {
            pega.desktop.TimeoutWarningCountdown = self.setTimeout("desktop_showTimeoutWarning('" + pega.desktop.TimeoutWarningWindow +
                "')", nTimeoutWarningTime);
        }
    }
}

function desktop_showTimeoutWarning(strTime) {
    var iTime = parseInt(strTime);
    iTime = iTime * 60000;
    var oSafeURL = new SafeURL("@baseclass.ShowLogoffTimer");
    oSafeURL.put("time", iTime);
    skipResetTimeoutWarning = true;
    pega.u.d.convertToRunActivityAction(oSafeURL);
    pega.openUrlInModal.showModalDialog(oSafeURL, iTime, 236, 620, function(ret) {
        if (!ret || ret === null || ret === "ok") {
            desktop_restartTimeoutWarningTimer();
            sendRestartTimeoutWarningMessage();
        } else {
            pega.ui.HarnessContextMgr.set("gDirtyOverride", false);
            PegaChannel.postMessage('logoff');
            pega.u.d.replace('pyActivity=LogOff&pzPrimaryPageName=pyDisplayHarness', null);
        }
    });
}

Note: the console.log are for debug and troubleshooting purpose - they should be removed in production code. The video below should explain how this code works and how to test the implementation and different use cases

The attached RAP contains the JS file and the UserHeader changes with these changes.

demologout.zip (30.6 KB)

@RichardMarsot - this approach is not helping us , our usecase is like lauching a workobject from a email link . we want to implement the session timer for this usecase . please suggest

@RichardMarsot Thanks much, Richard! This approach solves our session timer synchronization issue across multiple browser tabs of a Theme Cosmos application.

@RichardMarsot User can open any number of tabs they want. Can this cause any performance issue? If user opens 10+ tabs, can this cause timeout in individual tabs or memory issue? Any suggestion or best practice we have to follow here?

In the multi doc container feature we use in UI-Kit, I believe we restrict the number of cases we can open in parallel. Is there any such recommendation here?

Thanks

Ganesh

@RichardMarsot Thanks for the script it works fine. But if we open the tabs , we are getting one extra thread UserPortal Tab thread and if we refresh the TAB it gets killed, although the TAB is opn

Question do we need this UserPortal Thread.

I have attached the screen shots for better understanding.

TabsThreadissue.docx (387 KB)

@RichardMarsot

Since timeouts are throttled in background/inactive tabs, would it effect functioning of pxSessionTimer?

@RichardMarsot

this code works fine, but i’m having 403 errors. and I try to encrypt url, but i’m geeting pega_rules_utilities in not defined.

Hi @RichardMarsot,

We have followed the same implementation but the broadcast api is not working. Once we click the something on the new tab the reset timer message is not displayed on the main tab.We are on pega 23

Thanks

@RichardMarsot We are using this script in Infinity 23 and we are getting Security-Broken Access Control warning for this.

Access Control Check - https://docs-previous.pega.com/security/86/using-access-control-checks.

Is there a way to get fix for the below issues

  1. Registration/Encryption of PUBLIC API’s used in the script to avoid BAC issues.
  2. dynamic generation of URLs which don’t use encoding APIs should be flagged to avoid injection attacks, unauthorized data exposure and cross-site scripting attacks.

@santhoshrams16632372 You should use the semantic URL in the email to open the work object - see Theme-Cosmos: Open work object in Full portal on Copy sharable Link | Support Center - this will load the full portal with the case - from there the session timer should kick off as usual

@GaneshKumarC3200 one of the issue with the multi-doc container was the hardcoded limit of 16 tabs - With Theme-cosmos you can open any number of cases as a new browser tab - each tab will run on a separate thread - the thread will be passivated if no activity is made on the thread after a while (30 minutes by default for threads and 15 minutes for data pages - see Understanding passivation and requestor timeouts) -

Threads are relatively lightweight but increase server side memory needs. When user closes the tab, the thread is not deleted by default because the ‘unload’ browser event does not guarantee that an XMLHTTP request can be performed. Some customers have asked for a more aggressive approach to remove thread when user closes the tab - This can be done with a little bit of JS loaded in the Review harness and the portal harness - see snippet of code below - the core of this code uses the same strategy used in this doc with a broadcast channel - when the tab is closed, a message is broadcasted to the other tabs to ask to send the deleteThread command. A little bit of glue is added to handle browser refresh because when you do a browser refresh, it triggers unload and load - In this case, you do not want to delete the thread.

Here is the code example - note that you can drop this JS code in userworkform but it is NOT RECOMMENDED because this will also run everywhere including dev studio - best is to add this code to portal harness and review harness of your case - console log should help troubleshoot any issue

function cleanThread(threadname) {
  console.log("removing thread " + threadname);
    var safeURL = SafeURL_createFromURL(pega.u.d.url);
    safeURL.put("pyActivity", "removeThead");
    safeURL.put("threadName", threadname);
    pega.u.d.asyncRequest('GET', safeURL);
}
var otherthreads = {};
console.log("Cosmosthreadcleanup INIT");
var PegaChannel = new BroadcastChannel('pega-data');
PegaChannel.addEventListener('message', function(event) {
    try {
                console.log("Cosmosthreadcleanup received msg", event.data);
                console.log("Cosmosthreadcleanup otherthreads", otherthreads);
 
        var obj = JSON.parse(event.data);
        if (obj.action === 'close') {
            obj.threads.forEach(function(thread) {
                otherthreads[thread.name] = Date.now()
            });
            setTimeout(function() {
                Object.keys(otherthreads).forEach(function(val) {
                    if (Date.now() - otherthreads[val] > 1000) {
                        cleanThread(val);
                        delete otherthreads[val];
                    }
                });
            }, 5000);
        } else if (obj.action === 'open') {
            obj.threads.forEach(function(thread) {
                delete otherthreads[thread.name];
            });
        }
    } catch (e) { console.log(" In Catch Cosmosthreadcleanup received msg", event.data);}
});
var portalthread = pega.u.d.getPortalThreadName();
var isInitialized = false;
    window.onbeforeunload = function() {
          console.log("Cosmosthreadcleanup sending msg close");
        PegaChannel.postMessage(JSON.stringify({
            action: 'close',
            threads: [{
                name: pega.u.d.getPortalThreadName()
            }, {
                name: pega.u.d.getThreadName()
            }]
        }));
      cleanThread(pega.u.d.getPortalThreadName());
    };
    pega.ui.EventsEmitter.subscribe("AfterDCUpdate", function() {
        if (!isInitialized) {
            isInitialized = true;
          console.log("Cosmosthreadcleanup sending msg open");
            PegaChannel.postMessage(JSON.stringify({
                action: 'open',
                threads: [{
                    name: pega.u.d.getPortalThreadName()
            }, {
                    name: pega.u.d.getThreadName()
            }]
            }));
        }
    }, null, null, null);

@RichardMarsot Thank you very much for the explanation!

@RichardMarsot This script is working in UserWorkForm, but if we add to UserPortal Harness it only works at few places, for example if the case is moved to Performance Harness it is not working. Threads are not getting killed.

@RichardMarsot I wanted to know if this issue is resolved in Pega 8.8.X versions?

Hi @RichardMarsot, the script provided is not working in 8.8.4 version. it works fine in 8.6.1

Issue is the timeout countdown timer being paused after the pop-up is displayed.

The timeout countdown gets paused once the user either changes to another tab in the browser, or leaves the computer into standby status. The issue appears when the timeout screen is displayed, and then the user remains the session open

@RichardMarsot ,is there any way to implement session timer in constellation??

@ReshmaK3 - like I said in my original post: ‘You can drop this JS code in userworkform but it is NOT RECOMMENDED because this will also run everywhere including dev studio - best is to add this code to portal harness and review harness of your case’. If you also use perform, you can add it to this harness too - if you create a JS file, you just have to reference this file in the 3 harnesses (portal, review work- and perform work-)

@RichardMarsot we are facing below issues when using cleanThread script that you attached.

Issue1- login as user portal, open a work item in another tab, from the work tab if we switch portal, portal UI is not loading fine , it is all distorted.

Issue2- From dev portal, we are launching manager portal, now switch to user portal, the portal UI is distorted.

Please see attached screenshot. Can you please help here?

@RichardMarsot hi Richard , is there any way we can save any data before automatically logging out by using pxsessiontimer ?, any idea would be of great help