In this Use Case we explore how you can collect direct call information from Microsoft Teams and combine it with the Nimbus reporting template

We can accomplish this by using the Microsoft Graph API to retrieve the data and then storing it in a table. The data can then be transformed to meet our requirements and imported into the Nimbus report.

The "Analytics & reports" section of the Teams admin center displays helpful statistics on usage and calls, including the "PSTN and SMS usage" report. This report includes data on direct calls that were not made during a Nimbus session and is thus currently not available in the Nimbus Power BI reporting template.


Preconditions

You require tenant administrator rights to create an enterprise application in your tenant.

Also make sure that:

  • A Nimbus service is set up and contains some calls to evaluate. (lightbulb) You can also use test calls to increase metrics.
  • Teams direct PSTN calls have been made on the tenant.
  • A Nimbus PowerBI template is connected and refreshed with data from your tenant.
  • You created a database and/or a table in your Microsoft Dataverse (via PowerApps Admin center).

(info) Note: it is recommended that you are familiar with the following documentation:


(lightbulb) = A hint to signal learnings, improvements or useful information in context.(info) = Info points out essential notes or related page in context.
(warning) = Notifies you about fallacies and tricky parts that help avoid problems.(question) = Asks and answers common questions and troubleshooting points.
(error) = Warns you of actions with irreversible / data-destructive consequence.(tick) = Intructs you to perform a certain (prerequired) action to complete a related step.

Create an application with permissions on the necessary Graph API ressources

As a tenant admin, log into portal.azure.com and create an enterprise application. You can name it "Call Reports API Access" or similar. (lightbulb) In principle, we follow the instructions here https://learn.microsoft.com/en-us/graph/use-postman

  1. In the permissions section, add the following permissions on the MS Graph API:

    Application PermissionsDelegated Permissions

    CallRecords.Read.All

    Reports.Read.All

    User.Read.All

    Reports.Read.All

    User.Read

    User.Read.All

  2. Generate a secret for the app.

  3. Then save your application.

    (tick) Note down the the following information:

    • tenantI
    • clientI
    • appI
    • secret value

    (tick) Now we are ready to test the application in the next step.

Test the application

(info) In this step, we want to test if we can now access the GraphAPI ressource using our new application.  You can test the application using any HTTP client. We recommend to use postman.

Create the following requests:

Set up the authorization for each request using the following values:

Response excerpt

    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#Collection(microsoft.graph.callRecords.pstnCallLogRow)",
    "@odata.count": 9,
    "value": [
        {
            "id": "99999999-91ea-9999-9928-999849999968",
            "callId": "489175056_133955568@99.999.9.99",
            "userId": "4876a373-4ab2-9999-9c36-d3682fb2a3f4",
            "userPrincipalName": "nimbus.service@mytenant.com",
            "userDisplayName": "Nimbus Enterprise Service",
            "startDateTime": "2023-03-10T09:41:26.7040194Z",
            "endDateTime": "2023-03-10T09:47:17.7040194Z",
            "duration": 351,
            "charge": 0,
            "callType": "ucap_in",
            "currency": "GBP",
            "calleeNumber": "+4999209999999",
            "usageCountryCode": "GB",
            "tenantCountryCode": "GB",
            "connectionCharge": 0,
            "callerNumber": "+4199999000999",
            "destinationContext": null,
            "destinationName": null,
            "conferenceId": null,
            "licenseCapability": "MCOEV_VIRTUALUSER",
            "inventoryType": "Service",
            "operator": null,
            "callDurationSource": "Microsoft"
        },
....
JSON

(tick) We have access to the Teams call report data and can use it in the next steps.

Create the table

  1. Go to https://make.powerapps.com/ and select your working environment.
  2. Create your database and table. (info) Follow the instructions here Add a Microsoft Dataverse database - Power Platform | Microsoft Learn.
    (lightbulb) We want to store the original data from the response into the table with their original property names and original values. Additionally, we want to create the columns for the Nimbus report in the same table with sounding column names and transformed values.
  3. For each property in the json response of the last step, add a column. (lightbulb) We add a prefix "source_ " to the column names to indicate that these columns hold the original data.
    • source_startDateTime
    • source_startEndTime
    • source_duration
    • source_calleeNumber
    • source_callernumber
    • source_userPrincipleName
    • source_<columnname> 
    • ...
  4. Now, add some new columns used for the Nimbus data aggregation. (lightbulb) We add a prefix "target_ " to the column names to indicate that these columns hold the transformed data for the target system - the Nimbus report.

    Add the columns as follows:

    Column NameColumn PropertiesCalculated Field

    target_UserSessionOutcomeId

    Behaviour=Calculated

    Datatype=Single line of text

    If source_callType contains "in"

    and source_finalSipCode equals 0

    then Action  (target_UserSessionOutcomeId = 1)


    If source_callType contains "in"

    and source_finalSipCode equals 200

    then Action  (target_UserSessionOutcomeId = 1)


    If source_callType contains "in"

    and source_finalSipCode equals 487

    then Action  (target_UserSessionOutcomeId = 3)


    If source_callType contains "in"

    and source_finalSipCode equals 603

    then Action  (target_UserSessionOutcomeId = 2)


    And add the same for the Outbound calls:

    If source_callType contains "out"

    and source_finalSipCode equals 0

    then Action  (target_UserSessionOutcomeId = 11)


    If source_callType contains "out"

    and source_finalSipCode equals 200

    then Action  (target_UserSessionOutcomeId = 11)


    If source_callType contains "out"

    and source_finalSipCode equals 487

    then Action  (target_UserSessionOutcomeId = 14)


    If source_callType contains "out"

    and source_finalSipCode equals 603

    then Action  (target_UserSessionOutcomeId = 15)


    (info) map eventual other values in source_finalSipCode to UserSessionOutcomeId's 

    target_TaskDirectionId

    Behaviour=Calculated

    Datatype=Whole number

    If source_callType contains "in" then Action = 1

    If source_callType contains "in" then Action = 2

    target_Duration

    Behaviour=Calculated

    Datatype=Decimal

    Action= DiffInMinutes(source_startdatetime, source_enddatetime)

    target_TaskTypeId

    Behaviour=Calculated

    Datatype=Whole number

    If source_callType equals "ucap_out" 

    then Action  (target_TaskTypeId = 3)


    If source_callType equals "ucap_in" 

    then Action  (target_TaskTypeId = 4)


    (info) map eventual other values in source_callType to TaskTypeId 

    The "Direct Routing Usage report" and "Calling Plans Usage Report" have different responses and the source columns may differ. Available fields are described here Microsoft Teams PSTN usage report - Microsoft Teams | Microsoft Learn

Create the  Power Automate flow

In this step we create a  Power Automate flow that allows to fill the table with the MS teams reporting data.

  1. Go to https://make.powerautomate.com/ and select your working environment (it should be the same as in the previous step).
  2. Add a new scheduled flow.

    DescriptionSettings
    Start with the Recurrence trigger

    Choose your interval

    Add a Initialize Variable to the flow
    • Name = fromDateTime
    • Value = formatDateTime(addDays(utcNow(),-1),'yyyy-MM-dd')

    Add a Initialize Variable to the flow
    • Name = toDateTime
    • Value = formatDateTime(utcNow(),'yyyy-MM-dd')

    Add a HTTP Element to the flow

    Add three Compose elements to the flow to store the data.

    Add the following inputs:

    • Input for Compose = Body of the HTTP Element body('HTTP')
    • Input for Record Count = body('HTTP')?['@odata.count']
    • Input for Values = body('HTTP')?['value']

    Add a Apply to Each element to the flow

    Iterate over @{outputs('Values')}

    OPTIONAL Add a Parse JSON element to the flow

    Iterate over the Current Item. The schema is a s follows (we've generated it from the response value of the second step):

    Note: You are not obliged to do this step. It is a convenient way to access the response's data in the next steps and allows for response data validation in case the response changes. Your flow will run into an error right where the problem is, which might faciliate debugging.

    (info) Make sure you adapt the schema with yours. If you are using DirectRouting, the schema of the response is different.

    (lightbulb) Information as of  

    Calling Plans ResponseDirect Routing Response
    {
        "type": "object",
        "properties": {
            "id": {
                "type": "string"
            },
            "callId": {
                "type": "string"
            },
            "userId": {
                "type": "string"
            },
            "userPrincipalName": {
                "type": "string"
            },
            "userDisplayName": {
                "type": "string"
            },
            "startDateTime": {
                "type": "string"
            },
            "endDateTime": {
                "type": "string"
            },
            "duration": {
                "type": "integer"
            },
            "charge": {
                "type": "integer"
            },
            "callType": {
                "type": "string"
            },
            "currency": {
                "type": "string"
            },
            "calleeNumber": {
                "type": "string"
            },
            "usageCountryCode": {
                "type": "string"
            },
            "tenantCountryCode": {
                "type": "string"
            },
            "connectionCharge": {
                "type": "integer"
            },
            "callerNumber": {
                "type": "string"
            },
            "destinationContext": {},
            "destinationName": {},
            "conferenceId": {},
            "licenseCapability": {
                "type": "string"
            },
            "inventoryType": {
                "type": "string"
            },
            "operator": {},
            "callDurationSource": {
                "type": "string"
            }
        }
    }
    JSON
    {
        "type": "object",
        "properties": {
            "id": {
                "type": "string"
            },
            "correlationId": {
                "type": "string"
            },
            "userId": {
                "type": "string"
            },
            "userPrincipalName": {
                "type": "string"
            },
            "userDisplayName": {
                "type": "string"
            },
            "startDateTime": {
                "type": "string"
            },
            "inviteDateTime": {
                "type": "string"
            },
            "failureDateTime": {
                "type": "string"
            },
            "endDateTime": {
                "type": "string"
            },
            "duration": {
                "type": "integer"
            },
            "callType": {
                "type": "string"
            },
            "successfulCall": {
                "type": "boolean"
            },
            "callerNumber": {
                "type": "string"
            },
            "calleeNumber": {
                "type": "string"
            },
            "mediaPathLocation": {
                "type": "string"
            },
            "signalingLocation": {
                "type": "string"
            },
            "finalSipCode": {
                "type": "integer"
            },
            "callEndSubReason": {
                "type": "integer"
            },
            "finalSipCodePhrase": {
                "type": "string"
            },
            "trunkFullyQualifiedDomainName": {
                "type": "string"
            },
            "mediaBypassEnabled": {
                "type": "boolean"
            }
        }
    }
    JSON





    OPTIONAL Add a "Condition" to the flow

    We want to filter out the Nimbus service calls from the list of all PSTN calls.

    (info) Make sure to adapt the filter to your scenario. If you are using DirectRouting, the values in callType are slightly different.

    Information as of 01.03.2023 from Microsoft Teams PSTN usage report - Microsoft Teams | Microsoft Learn

    Calling Plans callTypesDirect Routing callTypes
     Teams user call types
    • user_in  - the user received an inbound PSTN call
    • user_out  - the user placed an outbound PSTN call
    • user_out_conf  - the user added two or more PSTN participants to the call such as a three-way conference call
    • user_out_transfer  - the user transferred the call to a PSTN number
    • user_out_forwarding  - the user forwarded the call to a PSTN number
    • conf_in  - an inbound call to the Audio Conferencing bridge
    • conf_out  - an outbound call from the Audio Conferencing bridge usually to add a PSTN number to the conference
    • unassigned_in  - an inbound PSTN call via Calling Plan to an unassigned number

    Teams bots call types (Nimbus service calls)
    • ucap_in  - an inbound PSTN call to Teams bot such as auto attendant or call queue
    • ucap_out  - an outbound PSTN call from a Teams bot such as auto attendant or call queue
    Teams user call types
    • dr_in  - the user received an inbound PSTN call
    • dr_out  - the user placed an outbound PSTN call
    • dr_out_user_conf  - the user added a PSTN participant to the call
    • dr_out_user_forwarding  - the user forwarded the call to a PSTN number
    • dr_out_user_transfer  - the user transferred the call to a PSTN number
    • dr_emergency_out  - the user made an emergency call
    • dr_unassigned_in  - an inbound PSTN call via Direct Routing to an unassigned number

    Teams bots call types (Nimbus service calls)
    • dr_in_bot  - an inbound PSTN call to a Teams bot such as auto attendant or call queue
    • dr_out_bot  - an outbound PSTN call from a Teams bot such as auto attendant or call queue

    Add a "Update a row" from the Dataverse connector to the flow.

    Make sure to use the correct connection.

    • Row id = body('Parse_JSON')?['id']
    • Name = concat('Import_', variables('toDateTime'))

    map the other column saccordingly using the parsed json.

    (tick) We can now run the flow in the next step.

Import the data

We want to run the flow once and import all the data from the past.

  1. Set the variables in the Power Automate flow to the following values and then run it.
    • fromDateTime = 2023-01-01
      (or even older, this is a fixed value from the date you want the call records to be imported).
    • toDateTime = formatDateTime(utcNow(),'yyyy-MM-dd')
  2. Verify if the table holds the expected data - especially verify the calculated columns.
    1. If your imported values are incorrect and you need to reimport everything in a new fresh table, run a bulk delete in po we r apps: Settings > Advanced Sesstings > Settings > Data Management > Bulk delete > New.
    2. If your imported values are OK, change the variables in the  Power Automate flow as follows:
      • fromDateTime = formatDateTime(addDays(utcNow(),-1),'yyyy-MM-dd')
      • toDateTime = formatDateTime(utcNow(),'yyyy-MM-dd')

(lightbulb) The flow will now run once a day and get the data from the previous day.

Add the table to your Nimbus PowerBI report

What is now left is to aggregate the data into the Nimbus report in the next step.

  1. Open the Nimbus power BI report
  2. Click on Dataverse and select your table. Select Import as import method.
  3. You can now apply the necessary steps to merge the data into the UserSessions Table. Open the query editor and apply the following steps :
    1. Select the imported table and rename the following columns to 

      column name in sourcecolumn name in UserSessions
      source_startdatetimeUserSelectedAt
      source_useridUserId
      target_UserSessionOutcomeIdOutcomeId
      target_DurationConnectedTime
      target_TaskTypeIdTaskTypeId
    2. Apply a SelectColumns step to only keep relevant columns

      = Table.SelectColumns(cre41_currencyNullto0,{"target_UserSessionOutcomeId", "source_startdatetime", "source_enddatetime", "target_Duration",.....})

    3. Select table UserSessions and apply the following steps:

      = Table.Combine({cre41_directpstncalls})

      = Table.CombineColumns(#"Appended Query",{"cre41_directpstncallsid", "Id"},Combiner.CombineTextByDelimiter("", QuoteStyle.None),"Id")

      = Table.SelectRows(CombineID, each [cre41_enddatetime] <> null and [cre41_enddatetime] <> "")

(tick) The direct call report data is now available in the UserSessions table. The data can be viewed on the User Sessions Tab.

UCIDUC  NIMB 052