Power Pages: Tracking clicks and downloads

Knowing how your external users are using you Power Pages is very important. Especially if you are sharing import documents through the Power Pages. In this blog post, we will show you how to set up a Power Page with a script that monitors the downloads and stores this data in Dataverse. By leveraging this functionality, you can gain valuable insights into user behaviour, track file downloads, and make data-driven decisions.

This solution was created together with my colleague Rik de Koning who speaks regularly at Power Platform events.

Power Pages configuration

Before we can save data to the Dataverse with a JavaScript we need to configure the Power Pages to allow the web api access. You need to configure the first 2 settings for each Dataverse table where data needs to be stored.

  • Open the Portal Management of your Power Pages.
  • Go to Site Settings
  • Here you need to add three settings.
  • The first setting is to enable the web api on your Dataverse table.
    • Name: Webapi/[table logical name]/enabled
    • Website: Select the website
    • Value: true
  • The second setting is to allow the web api to access the fields
    • Name: Webapi/[table logical name]/fields
    • Website: Select the website
    • Value: cre9b_userid,cre9b_document,cre9b_username,cre9b_dateandtime
  • The third setting is to allow for errors to be displayed.
    • Name: Webapi/error/innererror
    • Website: Select the website
    • Value: true

The JavaScript

In our example we created a button that both downloads a document (note with an attached file) and creates a new record in a Dataverse table. The api call to create the record is added as a onclick event on the button, while the href is linked to the actual document. The JavaScript itself needs to be added on the content page linked to the Information page. If you have another use case please look at the details of the Portal Web API on Learn Microsoft.

  • Open the Portal Management and open the Web Pages
  • Open the Information page where the JavaScript needs to be added.
  • Open the related Content page.
  • First you will need to add the Wrapper AJAX function, this will give you the function safeAjax to use the web api.
(function(webapi, $){
    function safeAjax(ajaxOptions) {
        var deferredAjax = $.Deferred();

        shell.getTokenDeferred().done(function (token) {
            // add headers for AJAX
            if (!ajaxOptions.headers) {
                $.extend(ajaxOptions, {
                    headers: {
                        "__RequestVerificationToken": token
                    }
                }); 
            } else {
                ajaxOptions.headers["__RequestVerificationToken"] = token;
            }
            $.ajax(ajaxOptions)
                .done(function(data, textStatus, jqXHR) {
                    validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                }).fail(deferredAjax.reject); //AJAX
        }).fail(function () {
            deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
        });

        return deferredAjax.promise();	
    }
    webapi.safeAjax = safeAjax;
})(window.webapi = window.webapi || {}, jQuery);
  • You can now add a function to the script that gathers the required information and send that to the safeAjax funtion. The safeAjax function then executes the api call.
function openfile(url) {
	var nowDateTime = new Date();
	var username = "{{ user.fullname }}";
	var contactid = "{{user.contactid}}";

    webapi.safeAjax({
		type: "POST",
		url: "/_api/[tablename]",
		contentType: "application/json",
		data: JSON.stringify({
			"cre9b_userid": contactid,
			"cre9b_document": url,
			"cre9b_username": username,
			"cre9b_dateandtime": nowDateTime
			
		}),
		success: function (res, status, xhr) {
      //print id of newly created table record
			console.log("entityID: "+ xhr.getResponseHeader("entityid"))
		}
	});
}
  • You can use the {{ user.[column] }} to get data out of the contacts table.
  • url is not a default options, I created a variable in a different part of the JavaScript.

For everybody that is interested in how we added the buttons I have added that code as well. With a JavaScript we created a button for each file (note) by changing the URL column into a button.

$('.entitylist').on("loaded", function () {
    // this will get the index of the corresponding table header
    var columnindex = $('th:contains("URL")').index();
    // this will loop through each table row below the corresponding table header
    $('tr').each(function () {
        var column = $('td', this).eq(columnindex);
        if (column.text().includes("https://")) {
            // this will find all a attributes based on the document links link
            const documentLinks = document.querySelectorAll(`[href="${column.text()}"]`)
            documentLinks.forEach(documentLink => {
                // this will set the className and the text of the link accordingly
                documentLink.className = "btnViewDocument"
                documentLink.target = "_blank"
                documentLink.text = "View"
                documentLink.onclick = function() { openfile(documentLink.href);};
            });
        }
    });
    // this will clear the name of the corresponding table header
    document.querySelector(`[aria-label*="URL"]`).innerHTML = ""
    document.querySelector(`[aria-label*="Created On"]`).innerHTML = "Shared On"
});

Power Automate: Dataverse Contact automatic invitation redemption

In my previouse blog post I explained how to automatically create and delete B2C account for Dataverse Contacts. In this post, I will dive deeper into the process by automating the redemption of an invitation code process. By default, new contacts need to redeem an inventation code before they can access the Power Pages. I am using the one-time-password setup for access to the Power Pages and for ease of use I don’t want the contact (external users) to have to redeem an inventation code.

To achieve this, we will automate the process by granting the contact a web role, connecting the B2C account ID, and adding the external identity. This will allow the contact to access the Power Pages without having to redeem an invitation code.

Creating the automated redemption process

  • Add the following actions below the Scope – Create B2C Users.
  • Initialize a variable called Role URL as a string.
  • Open the required web role in the Portal management tool, the ID is in the end of the URL.
https://[environment].crm4.dynamics.com/main.aspx?appid=39b012bd-1234-1234-1234-0022489fd314&pagetype=entityrecord&etn=adx_webrole&id=04ae7aa4-1234-1234-1234-0022489b74da
  • Add the following URL as the value, with your own unique web role id.
https://[environment].crm4.dynamics.com/api/data/v9.1/adx_webroles(04ae7aa4-1234-1234-1234-0022489b74da)
  • Initialize a variable called Invitation as a string.
  • Add as a value two guid()
  • Initialize a variable called External Identity URL as a String.
  • Add the following URL (with your environment name).
https://[environment].crm4.dynamics.com/api/data/v9.1/adx_externalidentities
  • Add a scope called Add Role to new contact and add the following actions.
  • Add the Dataverse Relate rows action called Add Role to Contact.
  • Set the Table name as Contacts.
  • Set the Row ID to Contact (the contact id of the contact where the flow was triggert on).
  • Set the Relationship to adx_webrole_contact.
  • Set the Relate with to the variable Role URL.
  • We are going to update the existing Dataverse Update a row action called Contact – Add User information.
  • Drag this action into the scope Add Role to new contact.
  • Set the Table name to Contacts.
  • Set the Row ID to Contact (the contact id of the contact where the flow was triggert on).
  • Set the Email Confirmed to Yes.
  • Set the Lockout Enabled to Yes.
  • Set the Login Enabled to Yes.
  • Set the Security Stamp to the guid() expression.
  • Set the Time Zone Rule Version Number to 0.
  • Set the User Name to the ID of the B2C account.
  • Add the Dataverse Add a new row action called External Identities.
  • Set the Table name to External Identities.
  • Set the Contact (Contacts) to Contacts(Contact) (the contact id of the contact where the flow was triggert on).
  • Set the Identity Provider to your Identity Provider URL
  • Set the User Name to the ID of the B2C account.
  • Add the Dataverse Relate rows action called Add External Identity to Contact.
  • Set the Table name to Contacts.
  • Set the Row ID to Contact (the contact id of the contact where the flow was triggert on).
  • Set the Relationship to adx_contact_externalidentity.
  • Set the Relate with the following code, the External Identity is the ID of the newly created External Identity.
https://[environment].crm4.dynamics.com/api/data/v9.1/adx_externalidentities()
  • The flow will now create a new B2C account when a new contact is created, link them together and automatically redeem the invitation.

Create and delete B2C accounts for Dataverse Contact

Today, we’ll be discussing a crucial aspect of B2C account management – the creation and deletion of B2C accounts in response to changes in the Dataverse Contact. This is an important topic for businesses that deal with external Power Pages user (contacts) that want to ensure the security of their records and Power Pages. In this post I will explain how to create or delete B2C accounts that are connected to a Dataverse Contact. So, let’s dive in!

Create a B2C account when a Dataverse Contact is created

Whenever a new contact is created a new B2C account needs to be created automatically that is linked to the contact. This is done thought the email address of the contact and the Azure B2C account id. These automations will limit the amount of manual admin work.

  • Create a new Power Automate flow with the name Create B2C user for Dataverse Contact.
  • Add the Dataverse trigger When a row is added.
  • Set the Change type to Added.
  • Set the Table name to Contacts.
  • Set the Scope to Organization.
  • Create the following 3 variables.
  • You will need to create an Application Registration in the B2C tenant with the following permissions.
Permission typePermissions (from least to most privileged)
Delegated (work or school account)User.ReadWrite.All, Directory.ReadWrite.All
Delegated (personal Microsoft account)
Not supported
ApplicationUser.ReadWrite.All, Directory.ReadWrite.All
  • Store the Client ID, Tenant ID and Secrect in the corresponding variables.
  • Add a HTTP action called HTTP – Delete User to the flow.
  • Set the Method to: Post.
  • Set the URI to the following code.
https://graph.microsoft.com/v1.0/users
  • Set the body to the following code.
  • In my scenario the user will not be using the password but the one-time password from B2C. That’s why I have added a guid twice as the password.
  • This call will create an account of the type email address which allows for any valid email to be used. The email does not have to be part of the B2C domain.
  • Parse the JSON response of the HTTP – Create User call.
  • Add the Dataverse action Update a row.
  • Set the Table name to Contact.
  • Set the Row id to the contact id of the trigger.
  • Add the id that was returned by the HTTP call that created the B2C account.
  • Save the flow.

Delete B2C account when Dataverse Contact is deleted

In my scenario I am maintaining the Power Pages contacts within a canvas app, and when a contact is deleted the associated B2C account needs to be deleted too.

  • Create a new Power Automate flow with a Power Apps (V2) trigger with the name Delete B2C users for deleted Dataverse Contact.
  • Add an input Text field names ActiveDirectoryID.
    • This is the Object ID of the Azure B2C Active Directory User connected to the Contact.
  • Create the following 3 variables.
  • You will need to create an Application Registration in the B2C tenant with the following permissions.
Permission typePermissions (from least to most privileged)
Delegated (work or school account)User.ReadWrite.All
Delegated (personal Microsoft account)
Not supported
ApplicationUser.ReadWrite.All
  • Store the Client ID, Tenant ID and Secret in the corresponding variables.
  • Add a HTTP action called HTTP – Delete User to the flow.
  • Set the Method to: Delete.
  • Set the URI to the following code.
https://graph.microsoft.com/v1.0/users/
  • Add the PowerApp (V2) parameter ActiveDirectoryID to the end of the URI.
  • Set the Tenant, Client ID and Secret fields with their corresponding variables.
  • Set the Authentication to Active Directory OAuth.
  • Set the Audience to the following code.
https://graph.microsoft.com
  • Save the Power Automate Flow.
  • Open or create a canvas app (Power Apps).
  • Open the Power Automate panel in the canvas app.
  • Add the Delete B2C users for deleted Dataverse Contact Power Automate flow.
  • Add a gallery with the source set to the Dataverse Contact table.
  • Add a recycle bin or other delete Icon to the gallery.
  • Add the following code to the recycle bin icon under OnSelect.
    • This will remove the contact record.
    • Starts the Power Automate Flow and sending the User Name (Users B2C object ID).
    • Notifies the users.
Remove(Contacts, ThisItem);
DeleteB2CuserfordeletedDataverseContact.Run(ThisItem.'User Name');
Notify("Record deleted successfully", NotificationType.Success);
  • Save and publish the canvas app.