Multi-Tenancy and Advanced Customizations for Embedded Analytics with Cumul.io

In this article I’ll share with you a cool project some of our cumulians have developed. By the end of it, we achieve a multi-tenant analytics platform where the user not only sees only data relevant to them, but also gets greeted with a theme that is meant for them too. Most importantly, all of this is achieved by a simple trick with the use of Cumul.io tags and Auth0. There are a few features that this project highlights:

  • It’s an example of how you can leverage the tags that are available in Cumul.io to simplify user access
  • Shows a neat trick on how you can use tags to tell the application which custom events result in a drill through dashboard
  • Uses these custom events to embed drill through dashboards
  • It also uses custom CSS overrides to produce custom feel and look of the dashboard based on whoever logs in.

In the end we get an application that uses template dashboards that were set up in Cumul.io. But, we use them in a multi-tenant application, meaning they can be used by more than one user. To achieve this, we apply filters and styles based on the user that logged in.

Notice below that when Brad logs in, he has access to 3 dashboards. He also has a certain theme to his dashboards and is seeing everything in English.

When Brad logs in

Whereas Angelina has access to only 2 dashboards and she is initially seeing everything in French. She also has a different colour scheme.

When Angelina logs in

On top of this, we are also displaying drill-through dashboards for both of them on their Marketing/Commercialization dashboard:

Drill through dashboard displayed by a custom event

How?

The whole article can be seen as a continuation of the Multi-Tenancy on Cumul.io Dashboards with Auth0 article. If you haven’t already, I recommend you have a quick read through. This article went through how to set up users with Auth0, and use user information to filter the dashboard. The initial setup here is much the same, but we will be adding some extra data about/for the user in Auth0.

First we will walk through how we set up our demo dashboards. For the purposes of this walk through we have created mock data that we’ve also made public. Then, we will walk through how we set up our Auth0 application and users.

As a final step, we will also briefly walk through some key parts of the application code. This will range from loading dashboards to listening to custom events.

As always, you can access the public repo to replicate the project entirely if you so wish 🙂 . To do so, clone cumul.io-multitenancy-auth0-example. The Readme includes detailed instructions to run the project!

Let’s run through how to set this whole thing up:

  1. Setting up the Dashboards in Cumul.io
  2. Define user information in Auth0
  3. Set up the application to load the dashboards

Step 1: Set Up the Dashboards – Cumul.io

For the purposes of this demo we have created our own set of dashboards. However, you are free to create as many dashboards as you like. The code we write in the next sections will allow for you to flexibly add or remove dashboards. But let’s have a look at the ones we have created;

Overall, we will have 3 main dashboards: Marketing, Sales and Leads. And additionally, we also have a drill through dashboard called ‘Marketing Drillthrough’. This one will be shown when a custom event is triggered from the Marketing dashboard.

If you would like to create similar dashboards, you can use our mock data that we’ve made public below. We’ve also made the dashboards public if you would like to use them, but make sure to duplicate them and make copies of them on your Cumul.io account:

Setting Tags

Once you have your dashboards ready, we want to give them tags. Later, we will use these tags in our app to get the dashboards we want to use and embed. This will allow us to set up a system that only provides a tag and gets dashboards without having to know all of their individual ids. It also allows you to easily add/replace dashboards in your app by just adding/removing tags in Cumul.io.

Adding tags to dashboards is very simple. Here, due to lack of imagination, we are going to give the tag auth0-mt to all of our main dashboards (Marketing, Sales and Leads). Of course, you are free to give whatever tag you find more appropriate!

Finally, we are going to give some tags to our drill-through dashboard. We have here again a tag to describe a drill-through dashboard, auth0-mt-dt, but we are also adding 2 extra tags:

Custom Event Tag

These tags start with a ce- and are used as a way for this application to know what custom event should drill through to this dashboard. It is set up so that it looks like this:

ce-<custom_event_name>

Parameter Tag

Similar to the custom event tag, we are also adding a parameter tag. This will be used in our application in the case where a custom event is used to fill in some parameters. The tag also provides information on what type the parameter is and what custom event needs it. It is set up in the following way:

p-<custom_event_name>-<parameter_type>-<parameter_name>

We will add one custom event and one parameter tag to our drill through dashboard:

ce-select_campaign

p-select_campaign-hierarchy_array-campaignName

Note: Make sure that your custom event name, parameter type or parameter name do not have a ‘-‘ in them. We use this character to decode the information in the tags. If you are interested in how this works, you can have a look at getExtraTags() in dashboardClient.js

Finally, this is what our tags for the drill-through dashboard will look like:

Set Custom Events and Parameters

One last thing to do on the dashboards is setting up custom events and parameters. In our scenario we display a drill through dashboard when a campaign is selected in the Marketing dashboard. We simply go to the settings of this chart and in ‘Interactivity’ we turn on Custom Events:

Notice the name of the custom event is the same as the one that was provided in the tags for the drill through dashboard. We will later set up our application so that when this custom event is triggered, we know to display the correct dashboard by checking it’s ce- tag.

We then set up our Marketing drill-through dashboard. Here, we set the parameters on which we want to filter the dashboard on. We set 2 parameters, campaignName and companyId. The first will be used with the custom event that selects the campaign to drill through on. The second will be used to filter all dashboards so that the user only sees data relevant to their companyId. We will set the companyId for a user in Auth0 in the following step. In the dashboard editor, we go to the Filters and add the following Parameters and Dashboard Filters.:

For all of the other dashboards we only set the companyId parameter and use it in a dashboard filter.:

Step 2: Set Up User Info – Auth0

The first thing you need for this step is an account with Auth0 which you can set up here. For this demo, we have set up 2 users, our trustee mascots, Angelina Julie and Brad Pots. We will use the Auth0 user management system to provide extra metadata about the user. These will include everything from custom themes to which dashboards they have access to.

This demo project is set up to use information provided in user_metada and app_metadata for each user to style and filter all of our template dashboards we set up in the previous section. In addition, we use the User Management in Auth0 to determine some access rights for each user too.

The following instructions are so that you can run the demo project yourself in case you want to:

  1. Once you have an account, create a ‘Single Page Web Application’ and take note of the ‘Domain’ and ‘Client ID’ to add to your auth_config.json at the root of your project.
  2. Set the Allowed Callback URLs, Allowed Logout URLs and Allowed Web Origins to http://localhost:3000 and save the changes and deactivate google-oauth2.
  3. In Applications -> APIs copy API audience to add to your auth_config.json
  4. Add some users in User Management -> Users.

user_metadata

For each user you create, add some properties to the user_metadata. Here are the ones used in our demo application :

  • firstName – displays a name in the application
  • language – shows the application and dashboards in the users default language (can be changed in app after start up too)
  • base-color – styles the sidebar and dashboard (should be in hex)
  • colors – to be used in the dashboards
  • logoUrl – to be used in the dashboards (optional)

This is what our user_metadata for Angelina and Brad look like:

{
  "firstName": "Angelina",
  "language": "fr",
  "base-color": "#009dff",
  "colors": [
    "#5867c3",
    "#00c5dc",
    "#ff525e",
    "#ffaa00",
    "#ffdb03",
    "#86de40",
    "#59b339"
  ],
  "logoUrl": "https://static.cumul.io/multitenancy-demo/logos/hd_antwerpen.png"
}

{
  "firstName": "Brad",
  "language": "en",
  "base-color": "#ff784f",
  "colors": [
    "#880065",
    "#b3005e",
    "#d62750",
    "#ef513e",
    "#fd7b27",
    "#ffa600",
    "#fdae6b"
  ],
  "logoUrl": "https://static.cumul.io/multitenancy-demo/logos/hd_leuven.png"
}

app_metadata

Add the following properties to each users app_metadata:

  • parameters – to contain parameter names and values that are always applied to an authorization. E.g.: used for row-level security per client
  • scope – to be used to store dashboards that a user has access to (names to all be the English name of the dashboard in lowercase)

This is what our app_metadata for Angelina and Brad look like:

{
  "parameters": {
    "companyName": [
      "HD Antwerpen"
    ],
    "companyId": [
      123456
    ]
  },
  "scope": {
    "dashboards": [
      "marketing",
      "marketing_drillthrough",
      "sales"
    ]
  }
}

{
  "parameters": {
    "companyName": [
      "HD Leuven"
    ],
    "companyId": [
      654321
    ]
  },
  "scope": {
    "dashboards": [
      "marketing",
      "leads",
      "sales",
      "marketing_drillthrough"
    ]
  }
}

user_metadata can be thought of as user preferences that could be easily changed by the users themselves. app_metadata on the other hand holds user information that only a admin would be able to control and edit.

Step 3: Set Up the Application

Alright, so far we’ve seen the dashboards we will be using and we’ve created users in Auth0. Now let’s see how we use the information set in Auth0 in our application. In this section we will cover a few key steps that were implemented in the demo project so as to;

  1. Get dashboards based on their tags
  2. Filter dashboards based on the user that is logged in
  3. Embed and filter drill through dashboard on custom event in the Marketing Dashboard

Seems like a lot, but we will only highlight some of the key parts of the project to simplify. By the end, you will have enough information to run and modify the project to your will! To follow these steps I recommend taking a look at the cumulio-multitenancy-auth0-example repo. We will refer to and summarize code snippets from this project! If you want to run the project yourself, the Readme also includes detailed instructions on how to do so.

Get Dashboards Based on Tags

The functions related to getting dashboards can all be found in <a href="https://github.com/TuanaCelik/cumul.io-multitenancy-auth0-example/blob/main/dashboardClient.js">dashboardClient.js</a> at the root of the project. To get dashboards, we need to make a request to the Cumul.io API specifying which dashboards we want. To do so, we have created 2 functions; getMainDashboards() and getDrillThroughDashboards(). Before anything else, we create our Cumul.io client with our Cumul.io API key and token (which you can find in your Cumul.io profile);

//create your Cumul.io client
const client = new Cumulio({
  api_key: YOUR_CUMULIO_API_KEY,
  api_token: YOUR_CUMULIO_API_TOKEN,
});

Get the main dashboards

//functions to get the main dashboards with the "auth0-mt" tag  
getMainDashboards(client) {
    return client.get("securable", {
      where: {
        type: "dashboard",
      },
      options: {
        public: false,
      },
      attributes: ["id", "name"],
      include: [
        {
          model: "Tag",
          where: {
            tag: "auth0-mt",
          },
          jointype: "inner",
          attributes: ["id", "tag"],
        },
      ],
    });
  }

In this API call, we are getting all private dashboards. In the include property we set a where filter on the Tag resource (filtering to “auth0-mt”) and do an inner join. This means we only get the dashboards that have that Tag resource associated with them (the specific Tag resource itself is also returned by that API call).

Get the drill through dashboard

//functions to get the drill through dashboards with the "auth0-mt-dt" tag  
getDrillThroughDashboards(client) {
    return client.get("securable", {
      where: {
        type: "dashboard",
      },
      options: {
        public: false,
      },
      attributes: ["id", "name"],
      search: {
        match_types: ["tag"],
        keyphrase: "auth0-mt-dt",
      },
      include: [
        {
          model: "Tag",
          attributes: ["id", "tag"],
        },
      ],
    });
  }

In this API call however, we use the search property to get the dashboards that have the drill-through tag. We use this because the response also includes all other tags as well (like the custom event and parameter tags). We use the include property to include all other Tag resources associated with those dashboards (notice that there is no where or jointype property specified here). This allows us to get all drill-through dashboards together with all their other associated Tags.

Store all dashboard info

In DashboardClient we map each of these dashboards we fetch in a dashboards data structure.

this.dashboards = { tabs: [], drill_throughs: [] };

Where tabs map to all of the main dashboards and drill_throughs to all the drill through dashboards we received from the API calls above.

Let’s add the main dashboards to the tabs property:

const mainDashboards = await this.getMainDashboards(client);
// list of dashboard id's, their names and their tags
this.dashboards.tabs = mainDashboards.rows.map(row => {
      return {id: row.id, name: row.name}
});

And let’s also fill in the extra properties for drill through dashboards in the getExtraTags() function:

const drillThroughDashboards = await this.getDrillThroughDashboards(client);
    
await drillThroughDashboards.rows.forEach((drill_through_dashboard) => {
      this.getExtraTags(drill_through_dashboard);
    });

The getExtraTags() function creates the following object for each drill through dashboard by filtering and decoding it’s extra ce- and p- tags:

let drill_through = {
      id: dashboard.id,
      name: dashboard.name,
      customEvents: {},
      parameters: [],
      eventName: "",
 };

Filter Dashboards on User Login

Once we have our dashboards, we want to make sure the logged in user only sees the dashboards that they are allowed to see. Not only that but we also want to make sure that they only see data that is relevant to them.

Get dashboards that user has access to

As a first step, we create a /dashboards end point in server.js. This checks a user’s properties and returns only those dashboards that the user is allowed to see. Let’s have a look at how we had defined a user’s dashboard access in Auth0. Here is Angelina’s app_metadata:

{
  "parameters": {
    "companyName": [
      "HD Antwerpen"
    ],
    "companyId": [
      123456
    ]
  },
  "scope": {
    "dashboards": [
      "marketing",
      "marketing_drillthrough",
      "sales"
    ]
  }
}

In server.js we will check the “scope” property and return dashboards whose names only match the ones there. To do so we create a getUserProperty() function:

function getUserProperty(user, property) {
  return user[authConfig.namespace + property];
}

We then use this function while fetching all of the user related dashboards and returning them in the /dashboards endpoint:

app.get("/dashboards", checkJwt, (req, res) => {
  let scope = getUserProperty(req.user, "scope");
  let scoped_dashboards = { tabs: [], drill_throughs: [] };
  if (scope.dashboards) {
    if (scope.dashboards.includes("*")) {
      scoped_dashboards.tabs = dashboards.tabs;
      scoped_dashboards.drill_throughs = dashboards.drill_throughs;
    } else {
      if (dashboards.tabs) {
        scope.dashboards.forEach((dashboard) => {
          // for each user-scoped dashboard, filter out the dashboard from the list of available dashboards & add the dashboard's properties
          let dashboardList = dashboards.tabs.filter((t) => {
            return dashboard == t.name.en.toLowerCase().replace(/\s/g, "_");
          });
          if (dashboardList.length == 1)
            scoped_dashboards.tabs.push(dashboardList[0]);
          else if (dashboardList.length > 1)
            console.log(
              "There are dashboards with the same name, meaning none are added to the scoped list."
            );
        });
        if (dashboards.drill_throughs)
          scoped_dashboards.drill_throughs = dashboards.drill_throughs.filter(
            (t) =>
              scope.dashboards.includes(
                t.name.en.toLowerCase().replace(/\s/g, "_")
              )
          );
      }
    }
  }
  return res.status(200).json(scoped_dashboards);
});

Note that if the dashboards property in Auth0 includes a “*” this code treats this as meaning that the user has access to all dashboards.

We fetch dashboards from the /dashboards endpoint in src/app.js and load them by adding the the following piece of code in a fetchAndLoadDashboards() function:

const fetchAndLoadDashboards = async () => {
  const accessCredentials = await auth0.getTokenSilently();
  let res = await fetch("/dashboards", {
    headers: new Headers({
      Authorization: `Bearer ${accessCredentials}`,
     }),
   });
  res = await res.json();
  if (res && res.tabs) {
    //Add Tabs to the UI
  }
}

Filter dashboards based on user info

Finally, we will also like to filter the actual data that the user can see in the dashboards. For this we will use the parameter and filter we set up in Step 1. We create an /authorization end point and return a new authorization key and token in server.js. While doing so, we include elements from user_metadata and app_metadata. This allows Cumul.io to filter dashboards and set themes based on user authentication. Here are some of the key steps to accomplish this.

app.get("/authorization", checkJwt, (req, res) => {
  let options = {
      type: "sso",
      expiry: "1 day",
      inactivity_interval: "30 minutes",
      integration_id: integrationId,
      metadata: {},
    };

    Object.keys(getUserProperty(req.user, "parameters")).forEach((key) => {
      options.metadata[key] = getUserProperty(req.user, "parameters")[key];
    });

   return client.create("authorization", options).then((result) => {
        return res.status(200).json({ key: result.id, token: result.token });
      })
      .catch((error) => {
        console.log("ERROR: ", JSON.stringify(error));
      });
});

Notice here, we fill every key-value pair of the parameter property that belongs to a user in options.metadata. This is how we ensure that the companyId parameter that we set in the dashboards are filled with the value set in Auth0 for that user.

Similarly, we also fill in the custom themes and colours for the user by adding it to options before we create the authorization tokens. For more on this, have a look at the source code in server.js.

Custom Event For Drill Through Dashboard

In this final step we will try to achieve the following:

In short, the user can select a certain data point in the bar chart, and we display a drill through dashboard that contains data relevant to the selection.

So that we display the correct dashboard based on a given event sent by the dashboard, we store custom event names and the dashboards they should load after fetching all the dashboards from server.js. To do so we add the following code into fetchAndLoadDashboards() in app.js:

  if (res && res.drill_throughs) {
    res.drill_throughs.forEach((drill_through) => {
      // add to list of possible dashboards
      dashboards[drill_through.name.en.toLowerCase().replaceAll(" ", "_")] = {
        id: drill_through.id,
        name: drill_through.name,
        isLoaded: false,
        isDrillthrough: true,
        key: "",
        token: "",
      };
      // add drillthrough custom events to the list
      Object.keys(drill_through.customEvents).forEach((customEvent) => {
          customEvents[customEvent] = drill_through.customEvents[customEvent];
          customEvents[
            customEvent
          ].dashboardToSelect = drill_through.name.en
            .toLowerCase()
            .replaceAll(" ", "_");
      });
    });
  }

Listen to custom events

Now, we can listen to events by calling Cumulio.onCustomEvent() and select the correct dashboard to embed. Not only that, but since we have to filter the drill through dashboard based on which element in the bar chart was clicked, we also have to create a new authorization token (similar to the previous section). This new authorization token will include values for the parameter we set in Step 1.

Cumulio.onCustomEvent((e) => {

  if (customEvents[e.data.event]) {
    let params = {};
    customEvents[e.data.event].required_parameters.forEach((param) => {
      if (param.type === "hierarchy_array") {
        // params["< YOUR PARAMETER NAME HERE >"] = {value: ["< YOUR PARAMETER VALUE HERE >"]};
        if (param.name == "campaignName") {
          params[param.name] = { value: [e.data.category.id] };
        }
      } 
      else
        console.log(
          "Parameter type of Custom Event " +
            e.data.event +
            " from dashboard " +
            e.dashboard +
            " is not recognized."
        );
    });
    selectDashboard(
      customEvents[e.data.event].dashboardToSelect,
      null,
      JSON.stringify(params),
      "#drill-through-dashboard-container"
    );
  }

Note that based on what type of chart the event is triggered from, the value you want may come in a different property. Here, as we used a bar chart our value is stored in e.data.category.id.

Conclusion

This was quite an advanced use case of Cumul.io and its functionalities. But the advantages and flexibility it provides is undeniable. A combination of tags, custom events, authentication steps have allowed us to create a multi-tenant, highly customizable analytics platform. Another cool thing about this particular project I would like to highlight is how flexible it is to modifications. So if you would like to you only need to clone the repository, and once you follow the steps in the readme you can:

  1. Add tags to your dashboards (and make sure you change them in dashboardClient.js if necessary).
  2. If you want to, create some custom events and filters on the dashboards you’re using.
  3. In src/app.js make sure you’re filling in all the parameters you want to for the events you’re listening to. For this step we’ve already included dummy code for you to get going.

And that’s it! 🎉

Finally, here are some resources you might find helpful:

Add a Comment