Skip to content

Evaluate master measure data

qlik/api: Where possible, use qlik/api rather than this framework.

Third-party cookies: This tutorial leverages cookies for auth, which are blocked by some browser vendors. Please use an OAuth solution where possible, which doesn’t leverage cookies.

Overview

In this tutorial, you are going to learn how to get and monitor a list of master measure values for use within your mashup. There are some scenarios where just having a number of KPIs in your mashup is all that you need. For example, you could have a portal page where you want to display a few master measure values while using third-party visualizations or even vanilla HTML. This tutorial uses enigma.js to connect to an app, pull in the desired master measures, and then evaluate and monitor them.

Prerequisites

To use the example page in this tutorial, ensure that you have the following:

  • The ID of an existing Qlik Sense application to embed that contains one or more master measures.
  • A web integration configured that contains the location of the web page that will be embedding the Qlik Cloud application.
  • A content security policy entry with the frame-ancestors directive added for the location of the webpage that will be embedding the Qlik Sense application.

Variable substitution and vocabulary

Throughout this tutorial, variables will be used to communicate value placement. The variable substitution format is <VARIABLE_NAME>. Here is a list of variables that appear in the sample code.

VariableDescription
<TENANT>The domain for the tenant you are accessing. Equivalent to tenant.region.qlikcloud.com.
<WEB_INTEGRATION_ID>The unique identifier of the web integration.
<APP_ID>The unique identifier of the Qlik Sense application.
<MASTER_MEASURE_NAMES>A list of master measure names. These are the measures that will be fetched and evaluated from the app.

Pseudocode

The code example in this tutorial provides a basic, fully functional web page (note however that all data is only displayed within the browser’s console — the web page itself will be blank) that executes the following sequence:

  1. Logs into the Qlik Cloud tenant.
  2. Fetches the CSRF token so that a WebSocket connection can be made to the Engine.
  3. Connects to the application using enigma.js.
  4. Establishes listeners on the WebSocket session closed and suspended events.
  5. Gets a list of all master measure definitions from the app and logs them to the console.
  6. Filters the list of all master measures down to only those that have matching names found with the MASTER_MEASURE_NAMES variable and logs them to the console.
  7. Builds a HyperCube of the desired master measures (no dimensions) and logs it to the console.
  8. Creates a dictionary of the measure names and values from step 7 for ease of use and logs it to the console.
  9. Establishes a listener for change events on that HyperCube so that if selections are made, the app is reloaded, etc — the HyperCube will be updated and logged to the console again.

The intent here is that you take the dictionary object that contains the master measures and associated values and display that information on your page however you’d like. You would want to make sure that you update your display on the page each time the HyperCube fires a change event.

Sample code

The sample code showcases a few capabilities in one example, providing a boilerplate you can modify. Here are some of the features:

  • Catches session events, such as displaying a custom modal dialog or redirecting to another web page.

  • Handles browser support for third-party cookies. Refer to this article for more information.

<html>

<head>
    <script src="https://unpkg.com/enigma.js/enigma.min.js"></script>
</head>

<body>

    <script>

        //    CONFIGURATION

        const TENANT = '<TENANT>';
        const WEB_INTEGRATION_ID = '<WEB-INTEGRATION-ID>';
        const APP_ID = '<APP-ID>';
        const MASTER_MEASURE_NAMES = ['<My Measure 1 Name>', '<My Measure 2 Name>'];

        //    MAIN

        (async function main() {
            const isLoggedIn = await qlikLogin();
            const qcsHeaders = await getQCSHeaders();
            const [session, enigmaApp] = await getEnigmaSessionAndApp(qcsHeaders, APP_ID);
            handleDisconnect(session);
            const allMasterMeasuresList = await getMeasureList(enigmaApp);
            const masterMeasureValuesDict = await masterMeasureHypercubeValues(enigmaApp, allMasterMeasuresList, MASTER_MEASURE_NAMES);
        })();

        //    LOGIN

        async function qlikLogin() {
            const loggedIn = await fetch(`https://${TENANT}/api/v1/users/me`, {
                mode: 'cors',
                credentials: 'include',
                headers: {
                    'qlik-web-integration-id': WEB_INTEGRATION_ID,
                },
            })
            if (loggedIn.status !== 200) {
                if (sessionStorage.getItem('tryQlikAuth') === null) {
                    sessionStorage.setItem('tryQlikAuth', 1);
                    window.location = `https://${TENANT}/login?qlik-web-integration-id=${WEB_INTEGRATION_ID}&returnto=${location.href}`;
                    return await new Promise(resolve => setTimeout(resolve, 10000)); // prevents further code execution
                } else {
                    sessionStorage.removeItem('tryQlikAuth');
                    const message = 'Third-party cookies are not enabled in your browser settings and/or browser mode.';
                    alert(message);
                    throw new Error(message);
                }
            }
            sessionStorage.removeItem('tryQlikAuth');
            console.log('Logged in!');
            return true;
        }

        async function getQCSHeaders() {
            const response = await fetch(`https://${TENANT}/api/v1/csrf-token`, {
                mode: 'cors',
                credentials: 'include',
                headers: {
                    'qlik-web-integration-id': WEB_INTEGRATION_ID
                },
            })

            const csrfToken = new Map(response.headers).get('qlik-csrf-token');
            return {
                'qlik-web-integration-id': WEB_INTEGRATION_ID,
                'qlik-csrf-token': csrfToken,
            };
        }


        //    ENIGMA ENGINE CONNECTION

        async function getEnigmaSessionAndApp(qcsHeaders, appId) {
            const params = Object.keys(qcsHeaders)
                .map((key) => `${key}=${qcsHeaders[key]}`)
                .join('&');

            return (async () => {
                const schema = await (await fetch('https://unpkg.com/enigma.js@2.7.0/schemas/12.612.0.json')).json();

                try {
                    return await createEnigmaAppSession(schema, appId, params);
                }
                catch {
                    // If the socket is closed immediately following the connection this
                    // could be due to an edge-case race condition where the newly created
                    // user does not yet have access to the app due to access control propagation.
                    // This bit of code will make another attempt after a 1.5 seconds.
                    const waitSecond = await new Promise(resolve => setTimeout(resolve, 1500));
                    try {
                        return await createEnigmaAppSession(schema, appId, params);
                    }
                    catch (e) {
                        throw new Error(e);
                    }
                }
            })();
        }

        async function createEnigmaAppSession(schema, appId, params) {
            const session = enigma.create({
                schema,
                url: `wss://${TENANT}/app/${appId}?${params}`
            });
            const enigmaGlobal = await session.open();
            const enigmaApp = await enigmaGlobal.openDoc(appId);
            return [session, enigmaApp];
        }

        //    HANDLE ENGINE SESSION CLOSURE

        function handleDisconnect(session) {
            session.on('closed', () => {
                console.log('Due to inactivity or loss of connection, this session has ended.');
            });

            session.on('suspended', () => {
                console.log('Due to loss of connection, this session has been suspended.');
            });

            window.addEventListener('offline', () => {
                session.close();
            });
        }

        //    GET LIST OF ALL MASTER MEASURES

        async function getMeasureList(enigmaApp) {
            const measureListProp = {
                "qInfo": {
                    "qType": "MeasureList",
                    "qId": ""
                },
                "qMeasureListDef": {
                    "qType": "measure",
                    "qData": {
                        "title": "/qMetaDef/title",
                        "tags": "/qMetaDef/tags"
                    }
                }
            }
            const measureListObj = await enigmaApp.createSessionObject(measureListProp);
            const measureList = await measureListObj.getLayout();
            console.log('All Master Measures:', measureList.qMeasureList.qItems);
            return measureList.qMeasureList.qItems;
        }

        //    CREATE HYPERCUBE WITH MULTIPLE MASTER MEASURES (INCLUDE MATCHING NAMES ONLY)

        async function masterMeasureHypercubeValues(enigmaApp, allMasterMeasuresList, desiredMasterMeasureNamesList) {
            var matchingMeasures = [];
            allMasterMeasuresList.forEach(measureObject => {
                if (desiredMasterMeasureNamesList.includes(measureObject.qMeta.title)) {
                    matchingMeasures.push({
                        "qLibraryId": measureObject.qInfo.qId
                    })
                }
            });

            if (!matchingMeasures.length > 0) {
                console.log('No matching master measures found! Exiting...');
                return
            }
            console.log('Matching Master Measure IDs:', matchingMeasures);

            const measureDef = {
                "qInfo": {
                    "qType": 'hypercube',
                },
                "qHyperCubeDef": {
                    "qDimensions": [],
                    "qMeasures": matchingMeasures,
                    "qInitialDataFetch": [
                        {
                            "qHeight": 1,
                            "qWidth": matchingMeasures.length,
                        },
                    ],
                },
            };
            const measureObj = await enigmaApp.createSessionObject(measureDef);
            const measureObjHypercube = (await measureObj.getLayout()).qHyperCube;
            console.log('Hypercube:', measureObjHypercube);

            measureObj.on('changed', async () => {
              const measureObjHypercube = (await measureObj.getLayout()).qHyperCube;
              return processMeasureHypercube(measureObjHypercube);
            })

            return processMeasureHypercube(measureObjHypercube);
        }


        //    HELPER FUNCTION TO PROCESS HYPERCUBE INTO USER FRIENDLY DICTIONARY

        function processMeasureHypercube(hypercube) {
            const masterMeasureValuesDict = Object.create(null);
            let i = 0;
            hypercube.qMeasureInfo.forEach(measure => {
                masterMeasureValuesDict[measure.qFallbackTitle] = hypercube.qDataPages[0].qMatrix[0][i].qText;
                i++;
            })

            console.log('Master Measure Result Dictionary:', masterMeasureValuesDict);
            return masterMeasureValuesDict;
        }

    </script>

</body>

</html>

Was this page helpful?