Serverless Prediction of a Product Feedback
Tags: azuredevstories, azure, serverless, azurefunctions
Published: 2020-05-23 10:52:09

> This article is part of [#ServerlessSeptember](https://aka.ms/ServerlessSeptember2020). You'll find other helpful articles, detailed tutorials, and videos in this all-things-Serverless content collection. New articles from community members and cloud advocates are published every week from Monday to Thursday through September. 
 
> Find out more about how Microsoft Azure enables your Serverless functions at [https://docs.microsoft.com/azure/azure-functions/](https://docs.microsoft.com/azure/azure-functions/?WT.mc_id=servsept20-devto-cxaall). 

# Story behind my use-case 

If you are developing any product, *feedback* is much more important. You have to trace each and every feedback like feature request or Bugs/Issues. Even Microsoft has a dedicated website [https://feedback.azure.com/](https://feedback.azure.com/) for tracking all its feedback related to their *Azure Product*. For issues they have to use GitHub issues [https://github.com/MicrosoftDocs/azure-docs/issues](https://github.com/MicrosoftDocs/azure-docs/issues) for all their open Source Projects. This would be a great way to track all the feedback on the open-source project. But how we can track Internal Project feedback efficiently! 

Because ...

![](https://media.tenor.com/images/a22a51ec2a7b6f777a6270bbea4ac828/tenor.gif)

In this blog, we are going to use a better way to solve this problem of getting feedback about our internal projects with the help of azure server-less computing and AI 😎

Let's jump into our Project

![](https://media.giphy.com/media/26BGIqWh2R1fi6JDa/giphy.gif)

# Architecture


![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/AzureDevStories.jpg)

Let's assume that we are going to develop a new internal product called *myproduct* (wow what a unique name 🤣 )

![](https://media.giphy.com/media/l0uc76ZPu3wuSp5uB4/giphy.gif)

I'm going to use Yammer to get all the feedback's from our internal users.

> **Yammer** is your social layer across Microsoft 365, integrating with the apps and services you already use to stay productive. You can create and edit documents, take notes, and share resources as a group. And get back to your conversations from anywhere in Microsoft 365.

If you notice the architecture, most of the main components are server-less like azure functions and logic app. There are many advantages of using server-less is our architecture. some of them are:

- Cost Efficient (Consumption model)
- Less Development time (logic-app : drag/drop model)
- Quick Deployments etc., 


# Components used

- [Yammer](https://www.microsoft.com/en-in/microsoft-365/yammer/yammer-overview)
- [LUIS](https://azure.microsoft.com/en-in/services/cognitive-services/language-understanding-intelligent-service/)
- [Logic Apps](https://azure.microsoft.com/en-in/services/logic-apps/)
- [AzureDevOps WorkItems](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/agile-process-workflow?view=azure-devops)
- [Cosmos DB](https://azure.microsoft.com/en-in/services/cosmos-db/)
- [Signal R](https://docs.microsoft.com/en-us/azure/azure-signalr/signalr-overview)
- [Azure Functions](https://azure.microsoft.com/en-in/services/functions/)

The components in this architecture are more flexible. i.e, you can easily replace one component with the other similar one.

For e.g 

1. You can use *MS Teams* or any other internal chat-based tool instead of yammer for getting the feedback.
2. *Azure DevOps WorkItem* Tracking can be replaced by *github/JIRA* kind of tools easily.
3. *Cosmos DB* can be replaced by the *Azure Table Storage*.

See how flexible this architecture is when we start using more and more server-less components 😉


![](https://media.giphy.com/media/d1MoLyoyipfMc/giphy.gif)


# Workflow 

Here I'm focusing on a product that was developed for internal purposes. So our end-users are nothing but the employees in our organization. So I used *Yammer* as my main platform to gather all the feedback and bugs about *myproduct* from users.

## Logic App

As soon as a user posted in yammer our logic app will be triggered. Let's see how our logic app now.

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/LogicApp.JPG)

### Yammer
![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/DevTo/LogicApp_LUIS.JPG)

### LUIS
I'm using LUIS to predict what are the intents in the yammer post(utterances)

Intents: Bugs🐞 /Features
Utterances: Yammer post which users posted

> An *Utterances* are input from the user that your app needs to interpret.<br>
> An *Intent* represents a task or action the user wants to perform. It is a purpose or goal expressed in a user's utterance.

I trained by LUIS with some user-defined Intents. You can also find my exported LUIS model in the [Github](https://github.com/jayendranarumugam/AzureDevStories/tree/master/LUIS%20App)

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/DevTo/LogicApp_LUIS.JPG)

### AzureDevOps WorkItems

If the predicted intent is Bug then I'll create a bug work-item in AzureDevOps else if it is Feedback then I'll create Feature work-item as simple as that.

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/DevTo/LogicApp_AzDevOps_Bug.JPG)

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/DevTo/LogicApp_AzDevOps_Feature.JPG)

### CosmosDB 

All are fine, but what happens if the intent is neither bug nor feedback?
In such a case, **None** Intent will be our predicted Intent. This None Intent is more important for Re-training our LUIS model. So I'm going to save these intents safely in CosmosDB

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/DevTo/LogicApp_Cosmos.JPG)


## Azure Functions

OK, I've saved the *None* Intents in cosmos DB, now I'm going to build a static website where developers can view all the information about the none intents in near **real-time** without doing any refresh.

This is quite an interesting part, where I've learned how powerful the Azure-Functions are! Thanks to [Anthony Chu](https://twitter.com/nthonyChu) for his wonderful [blog](https://anthonychu.ca/)

![](https://anthonychu.ca/post-assets/2018-09-24/architecture-overview.png)

*Image Credits: [Anthony Chu](https://twitter.com/nthonyChu)*

I've used Signal-R for a Broadcasting real-time updates from cosmos DB using CosmosDB Change-Feed in Azure Function.

If you see the below image I've four azure functions totally.

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/AzFunction.JPG)

Let's start with `OnDocumentsChanged` class, which will be triggered automatically for every insert/update on the target Cosmos DB.

For every new/updated item in the cosmos DB, I'll simply adding those items as messages in the `SignalRHub(SignalRHubItems)`



``` C#
public static class OnDocumentsChanged
    {
        [FunctionName("OnDocumentsChanged")]
        public static async Task Run([CosmosDBTrigger(
            databaseName: "MyProductDB",
            collectionName: "Items",
            ConnectionStringSetting = "AzureWebJobsCosmosDBConnectionString",
            LeaseCollectionName = "leases",
            CreateLeaseCollectionIfNotExists = true)]
             IEnumerable<object> updatedItems,
            [SignalR(HubName = "SignalRHubItems")] IAsyncCollector<SignalRMessage> signalRMessages,
            ILogger log)
        {
            foreach(var item in updatedItems)
            {
                await signalRMessages.AddAsync(new SignalRMessage
                {
                    Target = "SignalRHubUpdatedItems",
                    Arguments = new[] { item }
                });
            }
        }
```

The `SignalRInfo` class will be called from the static web page to make connections to SignalRhub to read the message from that.

``` C#
public static class SignalRInfo
    {
        [FunctionName("SignalRInfo")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Anonymous,"get","post")] HttpRequest req,
            [SignalRConnectionInfo(HubName = "SignalRHubItems")] SignalRConnectionInfo connectionInfo,
            ILogger log)
        {
            return new OkObjectResult(connectionInfo);
        }
    }
```
The `GetItems` class will also be called from the static web page to make connections to CosmosDB to get/read all the Items.

``` C#
public static class GetItems
    {
        [FunctionName("GetItems")]
        public static IActionResult Run(
            [HttpTrigger(AuthorizationLevel.Anonymous,"get","post")] HttpRequest req,
            [CosmosDB("MyProductDB", "Items", ConnectionStringSetting = "AzureWebJobsCosmosDBConnectionString")]
                IEnumerable<object> updatedItems,
            ILogger log)
        {
            try{
                return new OkObjectResult(updatedItems);
            }
            catch(Exception e){
                log.LogError(e.Message.ToString());
            }
             return new OkObjectResult(updatedItems);
            
        }
    }
```
Finally `StaticFileFunction` class is simple Az-function to show our statics web page *(index.html)*, inside `www` folder

``` C#
public static class StaticFileFunction
    {
        const string staticFilesFolder = "www";
        static string defaultPage = String.IsNullOrEmpty(GetEnvironmentVariable("DEFAULT_PAGE")) ?
    "index.html" : GetEnvironmentVariable("DEFAULT_PAGE");

        [FunctionName("StaticFileFunction")]
        public static HttpResponseMessage Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            try
            {
                var filePath = GetFilePath(req, log);

                var response = new HttpResponseMessage(HttpStatusCode.OK);
                var stream = new FileStream(filePath, FileMode.Open);
                response.Content = new StreamContent(stream);
                response.Content.Headers.ContentType =new MediaTypeHeaderValue(GetMimeType(filePath));
                return response;
            }
            catch (Exception e)
            {
                string name = e.Message.ToString();
                return new HttpResponseMessage(HttpStatusCode.NotFound);
            }
        }

        private static string GetScriptPath()
            => Path.Combine(GetEnvironmentVariable("HOME"), @"site\wwwroot");

        private static string GetEnvironmentVariable(string name)
            => System.Environment.GetEnvironmentVariable(name, EnvironmentVariableTarget.Process);
        private static string GetFilePath(HttpRequest req, ILogger log)
        {
            var path = req.Query["file"];

            var staticFilesPath = Path.GetFullPath(Path.Combine(GetScriptPath(), staticFilesFolder));
            var fullPath = Path.GetFullPath(Path.Combine(staticFilesPath, path));

            if (!IsInDirectory(staticFilesPath, fullPath))
            {
                throw new ArgumentException("Invalid path");
            }

            var isDirectory = Directory.Exists(fullPath);
            if (isDirectory)
            {
                fullPath = Path.Combine(fullPath, defaultPage);
            }

            return fullPath;
        }

        private static bool IsInDirectory(string parentPath, string childPath)
        {
            var parent = new DirectoryInfo(parentPath);
            var child = new DirectoryInfo(childPath);

            var dir = child;
            do
            {
                if (dir.FullName == parent.FullName)
                {
                    return true;
                }
                dir = dir.Parent;
            } while (dir != null);

            return false;
        }

        private static string GetMimeType(string filePath)
        {
            var provider = new FileExtensionContentTypeProvider();
            string contentType;
            provider.TryGetContentType(filePath, out contentType);
            return contentType;
        }
    }

```

That's it! Let's test it out 

# Testing 

## Posted some posts in Yammer.

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/Yammer_Post1.JPG)

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/Yammer_Post2.JPG)

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/Yammer_Post3.JPG) 

## Checking AzureDevOps

It's created bugs and features correctly. However one of the posts got missed meaning it's predicted as *None* Intent.

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/AzureDevOps_WorkItems.JPG)

## Checking CosmosDB

So our post got inserted in cosmos DB correctly.
![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/CosmosDB.JPG)

## Static Web Page

![](https://raw.githubusercontent.com/jayendranarumugam/AzureDevStories/master/Images/AzFunction_CosmosLiveFeed.JPG)

Here is the live-stream

![](https://github.com/jayendranarumugam/AzureDevStories/blob/master/Images/DevTo/Demo.gif?raw=true)


And it's Working Finally!

![](https://media.giphy.com/media/SAIGPdSohpC7JdRcHT/giphy.gif)


# Source Code
This is an Open-Source Project, view the full source code in the below link and feel free to provide feedback/issues to me 😎

{% github jayendranarumugam/azuredevstories %}

# Reference
- [https://anthonychu.ca/post/azure-functions-static-file-server/](https://anthonychu.ca/post/azure-functions-static-file-server/)
- [https://anthonychu.ca/post/cosmosdb-real-time-azure-functions-signalr-service/](https://anthonychu.ca/post/cosmosdb-real-time-azure-functions-signalr-service/)