Wednesday, November 20, 2013

Scheduled Task to Add Colleagues in SharePoint

Here is the use case in SharePoint 2010/2013: HR wants all employees to add some key people in your organization as colleagues, so that everyone inside the company would be able to see their posts and feeds. A simple powershell script can do the job:

## Add SharePoint Snap-in
Add-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue

## Get profiles
$site = Get-SPSite("https://my_company_portal")
$context = Get-SPServiceContext $site
$profilemanager = New-object Microsoft.Office.Server.UserProfiles.UserProfileManager($context)
 
## People who will be the common colleagues for all employees
$vipNames = "domain\user1", "domain\user2", "domain\user3"
$vipMembers = @()
foreach($vipName in $vipNames) {
    try {      
        $vipUser = $profilemanager.GetUserProfile($ecName)
        $vipMembers += $vipUser
    } catch {
        write-output $_.Exception.Message
   }   
}
 
## Colleague setting
$group = "General"
$groupType = [microsoft.office.server.userprofiles.colleaguegrouptype]::General
$privacy = [microsoft.office.server.userprofiles.privacy]::Public
$isInWorkGroup = $false
 
## Go through all users and add colleagues
$profiles = $profilemanager.GetEnumerator()
foreach($user in $profiles) {
    foreach($vipMember in $vipMembers) {
        if (($user.ID -ne $vipMember.ID) -and !($user.Colleagues.IsColleague($vipMember[0].ID))) {
            try {      
                $user.Colleagues.CreateWithoutEmailNotification($vipMember, $groupType, $group, $isInWorkGroup, $privacy)
            } catch {
                write-output $_.Exception.Message
            }
        }
    }
}
How about the new hires? We can setup a scheduled task to run the powershell script so the profile update will be applied weekly or daily. Open Task Scheduler from Administrative Tools or Control Panel, create a Task with following settings:
  • General tab:
    • Name: "Add colleagues"
    • Make sure the account running the task having sufficient permission to update the SharePoint Profile database
    • Check (enable) "Run whether user is logged on or not"
    • Check (enable) "Run as highest privileges"
  • Triggers tab:
    • Create a new trigger: Weekly or daily at midnight
  • Actions tab:
    • Select "Start a program" type
    • Program/script: powershell
    • Add arguments (optional): -Command "C:\SharePointScheduledTasks\addColleagues.ps1"

In case you don't like powershell script, following code is the equivalent c# version doing the exact same task:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using Microsoft.SharePoint;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
 
class Program
{
    static void Main(string[] args)
    {
        UpdateUsersProfile("https://my_company_portal");
    }
 
    private static void UpdateUsersProfile(string siteUrl)
    {
        using (SPSite spSite = new SPSite(siteUrl))
        {
            SPServiceContext context = SPServiceContext.GetContext(spSite);
            UserProfileManager profileManager = new UserProfileManager(context);
 
            List<UserProfile> vipMeMembers = GetVipMembers(profileManager);
 
            foreach (UserProfile profile in profileManager)
            {
                AddColleagues(profile, vipMeMembers);
            }
        }
    }
 
    private static List<UserProfile> GetVipMembers(UserProfileManager profileManager)
    {
        List<string> vipUserNames = new List<string>() { "domain\\user1", "domain\\user2", "domain\\user3" };
        List<UserProfile> vipMembers = new List<UserProfile>();
 
        foreach (string name in vipUserNames)
        {
            try
            {
                UserProfile vipMember = profileManager.GetUserProfile(name);
                vipMembers.Add(vipMember);
            }
            catch (Exception ex)
            {
                // User doesn't exit
            }
        }
        return vipMembers;
    }
 
    private static void AddColleagues(UserProfile user, List<UserProfile> colleagues)
    {
        foreach (UserProfile colleague in colleagues)
        {
            if (user.ID != colleague.ID && !user.Colleagues.IsColleague(colleague.ID))
            {
                user.Colleagues.CreateWithoutEmailNotification(colleague, 
                    ColleagueGroupType.General, "General", false, Privacy.Public);
            }
        }
    }
}

Monday, November 11, 2013

Display MongoDB Data Using ASP.NET MVC

In last post I presented a logging server solution for the mobile platform using node.js and MongoDB. Today I will build a ASP.NET MVC application to display the logged data stored in MongoDB.

First download MongoDB latest driver from github. There're a few download options out there. I just used the msi installer, and two key dlls, MongoDB.Driver.dll and MongoDB.Bson.dll, were installed in my C:\Program Files (x86)\MongoDB\CSharpDriver 1.8.3\ folder. Then we are ready to create a ASP.NET MVC 3 or MVC 4 project using basic template in Visual Studio 2010, and add MongoDB.driver.dll and MongoDB.Bson.dll to the project References.

The mongoDB database can be running inside the same machine or in a separate machine. The only difference is the connection string, which follows the format of "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]". In my example here I have one MongoDB instance running locally so my connection string is "mongodb://localhost:27017".

Next we create a log item model and a repository to interact with MongoDB with three actions available: search logs within a time span, get original log detail by its ID, and delete a log by its ID (for simplicity reason exception handling is skipped):

using System;
using System.Collections.Generic;
using System.Linq;
 
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
 
namespace LogsReporting
{
    public class LogItem
    {
        public ObjectId ID { get; set; }
        public DateTime Date { get; set; }
        public string OS { get; set; }
        public string Version { get; set; }
        public string Level { get; set; }
        public string Manufacturer { get; set; }
        public string Model { get; set; }
        public string ScreenSize { get; set; }
        public string Language { get; set; }
        public string Orientation { get; set; }
        public string Timezone { get; set; }
        public string Message { get; set; }
    }
 
    public interface ILogsRepository 
    {
        string GetOriginalLog(string id);
        void DeleteLog(string id);
        IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to);
    }
 
    public class LogsRepository : ILogsRepository
    {
        private MongoCollection<BsonDocument> _collection; 
        public LogsRepository()
        {
            string connectionString = "mongodb://localhost:27017";
            MongoServer server = new MongoClient(connectionString).GetServer();
            MongoDatabase db = server.GetDatabase("Data");
            _collection = db.GetCollection("Logs");
        }
 
        public string GetOriginalLog(string id)
        {
            ObjectId bsonId;
            if (ObjectId.TryParse(id, out bsonId))
            {
                var item = _collection.FindOneById(bsonId);
                if (item != null)
                    return item.ToString();
            }
            return string.Empty;
        }
 
        public void DeleteLog(string id)
        {
            ObjectId bsonId;
            if (ObjectId.TryParse(id, out bsonId))
            {
                _collection.Remove(Query.EQ("_id", bsonId));
            }
        }
 
        public IEnumerable<LogItem> GetLogItems(DateTime from, DateTime to)
        {
            List<LogItem> logs = new List<LogItem>();
            var docs = _collection.Find(Query.And(Query.GTE("time", from), Query.LTE("time", to)))
                .SetSortOrder(SortBy.Descending("time"));
            foreach (var doc in docs)
            {
                LogItem log = new LogItem()
                {
                    ID = doc.GetValue("_id").AsObjectId,
                    Date = TimeZone.CurrentTimeZone.ToLocalTime(doc.GetValue("time").ToUniversalTime()),
                    OS = doc.GetValue("os", "").ToString(),
                    Version = doc.GetValue("version", "").ToString(),
                    Manufacturer = doc.GetValue("manufacturer", "").ToString(),
                    Model = doc.GetValue("model", "").ToString(),
                    Language = doc.GetValue("lang", "").ToString(),
                    ScreenSize = doc.GetValue("screen", "").ToString(),
                    Orientation = doc.GetValue("orientation", "").ToString(),
                    Timezone = doc.GetValue("timezone", "").ToString(),
                    Level = doc.GetValue("level", "").ToString(),
                    Message = doc.GetValue("log", "").ToString()
                };
                if (!string.IsNullOrEmpty(log.Message) && log.Message.Length > 40)
                    log.Message = log.Message.Substring(0, 40) + " ...";
 
                logs.Add(log);
            }
            return logs;
        }
    }
}
MongoDB is schemaless, and can store arbitrary format of data. For convenience let's assume the logged data has following fields: time, os, version, manufacturer, model, language, screen-size, orientation, time-zone, log-level, and log message (refer to Windows 8 logging & reporting). Only "time" field (time stamp) is mandatory here so we could safely filter log entries by time.

The controller provides three actions: show recent logs, show the detail of a given log and delete a log. For demo purpose we only show data logged in the past week:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace LogsReporting
{
    public class LogsController : Controller
    {
        private static readonly ILogsRepository _logs = new LogsRepository();
 
        public ActionResult Index()
        {
            var logs = _logs.GetLogItems(DateTime.Now.AddDays(-7), DateTime.Now);
            return View(logs);
        }
 
        public string Detail(string id)
        {
            var originalLog = _logs.GetOriginalLog(id);
            return originalLog;
        }
 
        public ActionResult Delete(string id)
        {
            _logs.DeleteLog(id);
            return RedirectToAction("Index");
        }
    }
}
The presentation layer is pretty straightforward: a grid view shows the logs' info with "Detail" and "Delete" links. Clicking "Detail" link will open up a new window to display the raw log message. A confirmation popup prompts for a "Delete" action:
@model IEnumerable<LogsReporting.LogItem>
 
<script type="text/javascript">
    function showDetailPopup(id) {
        var url = '@Url.Action("Detail")' + '/' + id;
        window.open(url, "detailWindow", 'width=600px,height=400px');
    }
</script> 
 
<h2 style="text-align: center;">Logs Reporting</h2>
 
<table cellpadding="5px">
    <tr>
        <th>@Html.DisplayNameFor(model => model.Date)</th>
        <th>@Html.DisplayNameFor(model => model.OS)</th>
        <th>@Html.DisplayNameFor(model => model.Version)</th>
        <th>@Html.DisplayNameFor(model => model.Manufacturer)</th>
        <th>@Html.DisplayNameFor(model => model.Model)</th>
        <th>@Html.DisplayNameFor(model => model.ScreenSize)</th>
        <th>@Html.DisplayNameFor(model => model.Language)</th>
        <th>@Html.DisplayNameFor(model => model.Orientation)</th>
        <th>@Html.DisplayNameFor(model => model.Timezone)</th>
        <th>@Html.DisplayNameFor(model => model.Level)</th>
        <th>@Html.DisplayNameFor(model => model.Message)</th>
        <th></th>
    </tr>
@{int i = 0;}
@foreach (var item in Model) {
    <tr style='font-size: 9pt; @(i++%2==0 ? "background-color: #bbbbbb" : "")'> 
        <td>@Html.DisplayFor(modelItem => item.Date)</td>
        <td>@Html.DisplayFor(modelItem => item.OS)</td>
        <td>@Html.DisplayFor(modelItem => item.Version)</td>
        <td>@Html.DisplayFor(modelItem => item.Manufacturer)</td>
        <td>@Html.DisplayFor(modelItem => item.Model)</td>
        <td>@Html.DisplayFor(modelItem => item.ScreenSize)</td>
        <td>@Html.DisplayFor(modelItem => item.Language)</td>
        <td>@Html.DisplayFor(modelItem => item.Orientation)</td>
        <td>@Html.DisplayFor(modelItem => item.Timezone)</td>
        <td>@Html.DisplayFor(modelItem => item.Level)</td>
        <td>@Html.DisplayFor(modelItem => item.Message)</td>
        <td>
            <a href="#" onclick="showDetailPopup('@item.ID');">Details</a> |
            @Html.ActionLink("Delete", "Delete", new { id = item.ID }, 
                new { onclick = "return confirm('Are you sure to delete this log entry?');" })
        </td>
    </tr>
}
</table>

Following screen-shot shows how the view looks like:

Updated on Nov. 12 Just for fun I created a traditional ASP.NET web form .aspx page to show the exact same view:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="LogReporting.aspx.cs" Inherits="LogsReporting.LogReporting" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Logs Reporting</title>
    <style type="text/css">
        h2 {text-align: center;}
        table {text-align: left; padding: 5px;}
        table td, .detail {font-size: 9pt; padding: 5px;}
    </style>
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            $("tr:odd").css("background-color", "#bbbbbb");
        });
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Panel ID="gridPanel" runat="server">
            <h2>Logs Reporting</h2>
            <asp:GridView ID="gvLogs" runat="server" AutoGenerateColumns="False" GridLines="None"
                OnRowDataBound="gvLogs_RowDataBound" EmptyDataText="No logs available">
                <Columns>
                    <asp:BoundField DataField="Date" HeaderText="Date"></asp:BoundField>
                    <asp:BoundField DataField="OS" HeaderText="OS"></asp:BoundField>
                    <asp:BoundField DataField="Version" HeaderText="Version"></asp:BoundField>
                    <asp:BoundField DataField="Language" HeaderText="Language"></asp:BoundField>
                    <asp:BoundField DataField="Manufacturer" HeaderText="Manufacturer"></asp:BoundField>
                    <asp:BoundField DataField="Model" HeaderText="OS"></asp:BoundField>
                    <asp:BoundField DataField="ScreenSize" HeaderText="Version"></asp:BoundField>
                    <asp:BoundField DataField="Level" HeaderText="Level"></asp:BoundField>
                    <asp:BoundField DataField="Message" HeaderText="Message"></asp:BoundField>
                    <asp:TemplateField>
                        <ItemTemplate>
                            <asp:LinkButton ID="lbtDetail" runat="server" Text="Detail"></asp:LinkButton>
                            <asp:LinkButton ID="lbtDelete" runat="server" Text="Delete" OnClick="lbtDelete_Click"
                                OnClientClick="return confirm('Are you sure to delete this log entry?');"></asp:LinkButton>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
        </asp:Panel>
 
        <asp:Panel ID="detailPanel" runat="server" CssClass="detail">
            <asp:Literal ID="logDetail" runat="server"></asp:Literal>
        </asp:Panel>
    </div>
    </form>
</body>
</html>
The code behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Configuration;
 
using MongoDB.Driver;
using MongoDB.Bson;
using MongoDB.Driver.Builders;
 
namespace LogsReporting
{
    public partial class LogReporting : System.Web.UI.Page
    {
        private ILogsRepository _repo = new LogsRepository();
 
        protected void Page_Load(object sender, EventArgs e)
        {
            bool showDetail = !string.IsNullOrEmpty(Request.QueryString["id"]);
            detailPanel.Visible = showDetail;
            gridPanel.Visible = !showDetail;
 
            if (showDetail)
            {
                logDetail.Text = _repo.GetOriginalLog(Request.QueryString["id"]);
            } 
            else if (!IsPostBack)
            {
                var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now);
                bindDataSource(dataSource);
            } 
        }
 
        private void bindDataSource(IEnumerable<LogItem> logs)
        {
            gvLogs.DataSource = logs;
            gvLogs.DataBind();
        }
 
        protected void gvLogs_RowDataBound(object sender, GridViewRowEventArgs e)
        {
            if (e.Row.RowType == DataControlRowType.DataRow)
            {
                LogItem item = e.Row.DataItem as LogItem;
                LinkButton lbtDetail = e.Row.FindControl("lbtDetail") as LinkButton;
                LinkButton lbtDelete = e.Row.FindControl("lbtDelete") as LinkButton;
                string detailUrl = string.Format("{0}?id={1}", Request.Url.AbsolutePath, item.ID);
                lbtDetail.Attributes.Add("onClick", "window.open('" + detailUrl + "', '', 'width=600,height=400')");
                lbtDelete.CommandArgument = item.ID.ToString();
              }
        }
 
        protected void lbtDelete_Click(object sender, EventArgs e)
        {
            var logID = Convert.ToString(((LinkButton)sender).CommandArgument);
            _repo.DeleteLog(logID);
            var dataSource = _repo.GetLogItems(DateTime.Today.AddDays(-7), DateTime.Now);
            bindDataSource(dataSource);
        }
    }
}

Thursday, November 07, 2013

A Simple Logging Server Implementation Using Node.js and MongoDB

In my previous post I built a logging & reporting module for Windows 8 store apps. In this post I am going to build a simple while fast and scalable logging server.

Open source node.js is used to serve as http logging server. Node.js is a JavaScript server-side solution. It's light, efficient, and using event-driven and non-blocking I/O. These features make it a good fit for logging JSON objects sent by the client.

Traditionally the logging service would just save logs to file system. However it's hard to do data analysis and poor performance to do query over big size of logged data if using plain text file logs. Alternatively we can use document type NoSQL as the data store. In our use case, each log is an independent information unit, and document stores works extremely well in such scenario. Data insert and query would be faster and more efficient than traditional SQL database. We pick MongoDB as it's easy to use in different platform, well documented and has native support to JSON data.

Node.js requires a driver or module to connect to MongoDB database. We use mongojs in our example:

npm install mongojs

The whole logging server implementation is less than 100-line of code:

var http = require('http');
var url = require('url');

var mongojs = require('mongojs');
var db = mongojs('Data');
var collection = db.collection('Logs');

var contentType = {'Content-Type': 'text/html'};
var OK = 'OK', BAD = 'BAD REQUEST';
var MAX_URL_LENGTH = 2048, MAX_POST_DATA = 10240;

function createLog(request) {
    var logObj = {};
    try {
        logObj.time = new Date();
        logObj.method = request.method;
        logObj.host = request.headers.host;
        logObj.url = request.url;
        var queryStrings = url.parse(request.url, true).query;
        // Check if it's an empty object
        if (Object.keys(queryStrings).length) {
            logObj.query = queryStrings;
            for (var attrname in queryStrings) { 
                logObj[attrname] = queryStrings[attrname]; 
            }
        }
    } catch (ex) {}
    return logObj;
}

http.createServer(function (request, response) {
    // Bypass request with URL larger than 2K
    if (request.url.length > MAX_URL_LENGTH) {
        response.writeHead(414, contentType);
        response.end(BAD);
    } else if (request.method == 'GET') {  // HTTP GET
        var logObj = createLog(request);
        if (logObj.query) {
            delete logObj.query;
            collection.save(logObj);
            response.writeHead(200, contentType);
            response.end(OK);
        } else {  // Empty request
            response.writeHead(400, contentType);
            response.end(BAD);
            request.connection.destroy();
        }
    } else if(request.method == 'POST') {  // HTTP POST
        var logObj = createLog(request);
        var postData = '';
        request.on('data', function(data) {
            postData += data;
            // Bypass request with POST data larger than 10K
            if(postData.length > MAX_POST_DATA) {
                postData = "";
                response.writeHead(413, contentType);
                response.end(BAD);
                request.connection.destroy();
            }
        });
        request.on('end', function() {
            if (!postData && !logObj.query) {  // Empty request
                response.writeHead(400, contentType);
                response.end(BAD);
                request.connection.destroy();
            } else {
                if (postData) {
                    try {
                        postObjs = JSON.parse(postData);
                        for (var attrname in postObjs) { 
                            logObj[attrname] = postObjs[attrname]; 
                        }    
                    } catch (ex) {
                        logObj.postData = postData;
                    }
                }
                if (logObj.query) {
                    delete logObj.query;
                }
                collection.save(logObj);
                response.writeHead(200, contentType);
                response.end(OK);
            } 
        });
    }
}).listen(process.env.PORT || 8080);

Friday, November 01, 2013

WinJSLog - A Logging and Reporting JavaScript Module For Windows 8 Store Apps

Background

Usually developers use console.log() or WinJS.Log() function to log error and debug information. But that only helps during debugging in local machine. How about those users who installed your app and had errors or experienced crashes? Microsoft has Quality reports in Windows 8 app's dashboard if its telemetry service is enabled, but that function is quite limited, and it only collects crash data when the user accepts the so called Customer Experience Improvement Program (CEIP). It would be great to have errors and custom data reported to your own server so you can do some investigation at real-time. In this post I will have some discussion on this topic and present an implementation to log and report crashes, errors, and potentially any info you want for a Windows 8 store app.

Why logging and reporting are important for mobile apps?

Is your Mobile app working nicely in different devices? Are there any unexpected issues happening in different environment? The customer feedback is likely your main source of investigation. But that's not something you as a developer should rely on. Some issues may never surface, and people would just uninstall the app when they are not happy on it. Unless for some royal users, no many people would spend their precious time to write feedback or report to help you to improve your application.

Even you are lucky enough to have a nice user having a polite comment like this: "This's a good app! However sometimes it crashes when I am on editing mode. Please fix it.", you have no idea what's the real problem is. You may end up spending countless time just to reproduce the issue in specific situation.

So you want to monitor your app's running issues in real-time, and you want to get as much info when crash or error occurs. When logging service is on board, you may want more data to analyze the customer usage. Then it becomes an analytics framework and you have another communication channel to your app (carefully not to abuse using it).

All that sound to be essential to maintain the app quality for a distributed mobile app. But surprisingly I couldn't find a public available solution online. So I decided to write my own, and I called it WinJSLog. The goal of the implementation is light, effective, and easy to use.

Why an app crashes?

I have discussed WinJS exceptions in a previous post. Basically the unhandled exception will result in app crash by default. Crashes is the worst user experience ever because the running app is suddenly disappear without any prompt and reason to end user. A better experience would be redirect the user to a landing page. People may get confused too "why I am switched to a different place?", but at least the app maintains the running condition, giving user a chance to do other stuff or retry the previous action.

Unhandled exception in Windows 8 can be caught in Windows.Application.onerror global event. Following code snippet shows how to register an event handler to redirect user to a specific page when unhandled exception occurs:

    Windows.Application.onerror = function (e) {
        showErrorPage();
        return true; // telling WinJS that the exception was handled, don't terminate
    }

What data to collect?

Errors like "Object reference is null" is not so helpful. As a development you want as much info as possible to identify an issue, such as the stack trace which provides more context about the error. The device and environment variance could make some issue super hard to figure out. A problem may only happen in one specific device, in one specific language or in one specific screen size.

My implementation includes the error message detail, as well as following context info:

  • application version
  • language
  • device maker
  • device model
  • screen size
  • suspend and resume history
  • page navigation history
  • method invocation history
Above context information could help us to pin point the issues occurring in certain environment. Ideally the memory usage info should be included but I have not figured out how to get that data from WinJS libraries yet.

What's the data format?

Data are collected as key-value JavaScript objects, and JSON is used to transfer the logged data to different media, like file system or cross the network. JSON was born for such usage and it's a total no-brainer. Following JSON data is an example of crash log sent from a Windows 8 test app running in a VMWare virtual machine:

{
    "os" : "Windows 8",
    "version" : "1.0.0.1",
    "manufacturer" : "VMware, Inc.",
    "model" : "VMware Virtual Platform",
    "lang" : "en-CA",
    "screen" : "1680x1050",
    "orientation" : "landscape",
    "timezone" : "-5", 
    "latitude" : "43.666698",
    "longitude" : "-79.416702",
    "logtime" : "November 1, 2013 9:42:56 AM", 
    "pagetrace" : "home[9:42:27AM]=>user[9:42:41AM]=>test[9:42:48AM]",
    "level" : "crash", 
    "log": [ 
            {
             "source" : "promise", 
             "message" : "'username' is undefined", 
             "description" : "'username' is undefined",
             "stacktrace" : "ReferenceError: 'username' is undefined\n 
                at ready (ms-appx://testApp/pages/test/test.js:85:13)\n 
                at Pages_ready (ms-appx://microsoft.winjs.1.0/js/base.js:4511:29)\n
                at notifySuccess (ms-appx://microsoft.winjs.1.0/js/base.js:1404:21)\n 
                at enter (ms-appx://microsoft.winjs.1.0/js/base.js:1091:21)\n
                at _run (ms-appx://microsoft.winjs.1.0/js/base.js:1307:17)\n
                at _completed (ms-appx://microsoft.winjs.1.0/js/base.js:1275:13)\n
                at notifySuccess (ms-appx://microsoft.winjs.1.0/js/base.js:1404:21)\n
                at enter (ms-appx://microsoft.winjs.1.0/js/base.js:1091:21)\n
                at _run (ms-appx://microsoft.winjs.1.0/js/base.js:1307:17)\n
                at _completed (ms-appx://microsoft.winjs.1.0/js/base.js:1275:13)", 
             "level" : "crash", 
             "time":"2013-11-01T14:42:56.438Z"
           }
        ]
}
In next post I will present a simple logging server implementation to handle such requests.

When to send the logged data? What happen if no Internet access?

Crash or unhandled exception is considered serious issue, and the error detail should be sent to the logging server immediately when it happens. For regular error messages or debugging info we could accumulate them and send them out once current page is completed or a different page is visited. There's also a timer process to check and cleanup the pending logs in background (2 minutes by default and is configurable), so the logged message could be delivered even the user stay on one page for a long time.

The logged data will be stored to file system if Internet service is not available. When the application starts up, it will check if such log file(s) exist, if so the log file(s) will be read and send to logging server if Internet is available. Usually there're many operations during the application booting up. To avoid the action congestion, it's a good idea to defer to run the check-and-send action a little bit later than the application launch (30 seconds by default). If the Internet access is not available, the background process will also periodically recheck the network status (2 minutes by default) and send out the file logs when Internet access becomes available.

Other consideration

You app may target to some specific features such as map and camera stuff, and the location and camera info would be good for you to diagnose the issue. In such case, the location and camera data should be obtained right in the spot when they are used, and store them in a global variable like sessionState, so the logging code can read it directly without a redundant code executed again, which makes the logging service lighter and less intrusive.

What happens if logging server requires authentication? As I discussed in WinJS Authentication, it would be better to allow anonymous access for such general logging service. Hard-coded logging username/password is not safe, and it's not feasible to let user to type the username and password. Would it be a potential DOS/DDOS attach when anonymous access is open? No service in Internet can avoid that, but firewall in front of your servers should take care of that threat. However we can still do a bit more to protect ourself in the code level, such as not logging abnormal data with extremely large size of URL or message.

How about the errors inside logging service? In general any errors or potential errors within logging and reporting code should be silently skipped. Optionally those errors can be sent to the logging server, or send to a different server if errors occur during sending data to logging server.

How to use the code?

The WinJSLog module exposes following functions:

    function fatal(error)                // log crash
    function error(description, error)   // log error
    function warning(error, description) // log warning
    function page(pageName)              // navigate to a page
    function method(methodName)          // invoke a method
    function registerLogging()           // register logging
    function unregisterLogging()         // unregister logging

First include the winjslog.js file inside default.html or your custom page, then register the logging service before app.start():

(function () {
    "use strict";

    WinJS.Binding.optimizeBindingReferences = true;

    var app = WinJS.Application;
    var nav = WinJS.Navigation;

    app.addEventListener("activated", function (args) {
      ...// skip for abreviation
    }); 
   
    app.onerror = function (e) {
        Logging.fatal(e);
        Logging.page("homepage");
        nav.navigate("/pages/home/home.html");
        
        return true; // the app will terminate if false
    };

    Logging.registerLogging("http://myloggingserver/");
    
    app.start();

    ...
In above code an onerror event handler is registered to catch the unhandled exception to avoid the app's crash, log the crash by Logging.fatal(e), then redirect to home page. Note that the action of redirecting to home page is also logged as a navigation path by Logging.page() function. Following code snippet shows how to log a function invocation and its potential error message:
    function doComplicatedTask() {
        Logging.method("doComplicatedTask");
        try {
            ... // job with potential error
        } catch (e) {
            logging.error("Error occur!", e);
        }
    }
Alternatively you can log lower level warning, info and debug message by using Logging.warning(), Logging.info() and logging.debug() functions respectively.

Source code of WinJSLog.js

Update: the source code now is available at github and you can get the latest version there.

(function () {
    "use strict";

    var logs = [], pages = [], methods = []; // log data
    var loggingServer, loggingEnabled, debugEnabled; // service setting
    var version, manufacturer, model; // app context
    var currentLevel = 'debug', levels = { 'debug': 1, 'info': 2, 'warning': 3, 'error': 4, 'crash': 5 };

    // register logging service
    function registerLogging(serverUrl, doDebugging, deferRunInSeconds, recheckInSeconds) {
        if (!serverUrl) {
            throw 'registerLogging error: logging server not defined.';
        }

        loggingEnabled = true;
        loggingServer = serverUrl;
        debugEnabled = doDebugging || false;
        deferRunInSeconds = deferRunInSeconds || 30;
        recheckInSeconds = recheckInSeconds || 60;

        // Register event handler for suspend, resume and relaunch (terminated then restarted)
        WinJS.Application.addEventListener("activated", restoreSessionData);
        Windows.UI.WebUI.WebUIApplication.addEventListener("suspending", suspend);
        Windows.UI.WebUI.WebUIApplication.addEventListener("resuming", resume);

        // defer to run the check-log-file-and-send process
        WinJS.Promise.timeout(deferRunInSeconds * 1000).then(function () {
            cleanupLogs(recheckInSeconds);
        });
    }

    // unregister logging service
    function unregisterLogging() {
        loggingEnabled = false;
        WinJS.Application.removeEventListener("activated", restoreSessionData);
        Windows.UI.WebUI.WebUIApplication.removeEventListener("suspending", suspend);
        Windows.UI.WebUI.WebUIApplication.removeEventListener("resuming", resume);
    }

    // restore logging data after app relaunch from terminated state
    function restoreSessionData(args) {
        if (args && args.detail && args.detail.kind === Windows.ApplicationModel.Activation.ActivationKind.launch) {
            if (args.detail.previousExecutionState === Windows.ApplicationModel.Activation.ApplicationExecutionState.terminated) {
                var sessionState = WinJS.Application.sessionState;
                if (sessionState.logPages)
                    pages = sessionState.logPages;
                if (sessionState.logMethods)
                    methods = sessionState.logMethods;
                if (sessionState.logLogs)
                    logs = sessionState.logLogs;
                if (sessionState.logLevel)
                    currentLevel = sessionState.logLevel;
            }
        }
    }

    // log suspending event and store log data into session
    function suspend() {
        if (loggingEnabled) {
            var pageEntry = { 'time': new Date(), 'page': 'suspending' };
            pages.push(pageEntry);
            var sessionState = WinJS.Application.sessionState;
            sessionState.logPages = pages;
            sessionState.logMethods = methods;
            sessionState.logLogs = logs;
            sessionState.logLevel = currentLevel;
        }
    }

    // log resuming event
    function resume() {
        if (loggingEnabled) {
            var pageEntry = { 'time': new Date(), 'page': 'resuming' };
            pages.push(pageEntry);
        }
    }

    // log an event: navigate to a page
    function pageLog(pageId) {
        if (loggingEnabled) {
            try {
                processMemoryLogs();
            } catch (e) { }
            var pageEntry = { 'time': new Date(), 'page': pageId };
            pages.push(pageEntry);
        }
    }

    // log an event: invoke a method
    function methodLog(methodName) {
        if (loggingEnabled) {
            methods.push(methodName);
        }
    }

    // log a crash; a crash or unhandled exception can be caught by WinJS.Application.onerror event
    function crashLog(err) {
        if (loggingEnabled) {
            setLevel('crash');
            var errWrapper = getErrorObject(err);
            errWrapper.level = "crash";
            errWrapper.time = new Date();
            logs.push(errWrapper);
            try {
                processMemoryLogs();
            } catch (e) { }
        }
    }

    // log an error
    function errorLog(description, err) {
        if (loggingEnabled) {
            setLevel('error');
            logs.push(getLogObject('error', description, err));
        }
    }

    // log a warning message
    function warningLog(description, err) {
        if (loggingEnabled && debugEnabled) {
            setLevel('warning');
            logs.push(getLogObject('warning', description, err));
        }
    }

    // log an info message
    function infoLog(description) {
        if (loggingEnabled && debugEnabled) {
            setLevel('info');
            logs.push(getLogObject('info', description));
        }
    }

    // log a debug message
    function debugLog(description) {
        if (loggingEnabled && debugEnabled) {
            setLevel('debug');
            logs.push(getLogObject('debug', description));
        }
    }

    // build a log object
    function getLogObject(level, description, err) {
        var logObject = getErrorObject(err);
        if (logObject.description) {
            logObject.description = logObject.description + description;
        } else {
            logObject.description = description || '';
        }
        logObject.level = level || 'unknown';
        logObject.time = new Date();
        return logObject;
    }

    // build an error object
    function getErrorObject(err) {
        var errObject = {};
        if (err) {
            if (err.detail && typeof err.detail === 'object') {
                var detail = err.detail;
                if (detail.promise) {
                    errObject.source = "promise";
                }
                if (detail.errorMessage) {
                    errObject.message = detail.errorMessage;
                    if (detail.errorLine)
                        errObject.codeline = detail.errorLine;
                    if (detail.errorUrl)
                        errObject.sourcUrl = detail.errorUrl;
                } else if (detail.error && typeof detail.error === 'object') {
                    errObject.message = detail.error.message || 'unknown';
                    if (detail.error.description)
                        errObject.description = detail.error.description;
                    if (detail.error.stack)
                        errObject.stacktrace = detail.error.stack;
                } else {
                    errObject.message = detail.message || 'unknown';
                    if (detail.description)
                        errObject.description = detail.description;
                    if (detail.number)
                        errObject.codeline = detail.number;
                    if (detail.stack)
                        errObject.stacktrace = detail.stack;
                }
            } else {
                errObject.message = err.message || err.exception || err;
            }
        }
        return errObject;
    }

    // determine the highest log level for current log entry
    function setLevel(level) {
        if (levels[level] > levels[currentLevel]) {
            currentLevel = level;
        }
    }

    // periodically check the memory logs and storage logs, and send logs to server if Internet is available
    function cleanupLogs(recheckInseonds) {
        if (loggingEnabled) {
            processMemoryLogs();
            processFileLogs();
            setTimeout(function () {
                cleanupLogs(recheckInseonds);
            }, recheckInseonds * 1000);
        }
    }

    // construct log message and send to server if Internet is available, otherwise save it to local storage
    function processMemoryLogs() {
        if (logs.length > 0) {
            var data = getContext();
            var date = new Date();
            data.logtime = date.toLocaleString() + ' [' + date.toISOString() + ']';
            if (pages.length > 0) {
                var pagetrace = pages.map(function (item) {
                    if (item.time && item.time.toLocaleTimeString)
                        return item.page + "[" + item.time.toLocaleTimeString().replace(' ', '') + ']';
                    else
                        return item.page + "[" + item.time + ']';
                }).join(' => ');
                data.pagetrace = pagetrace;
            }
            if (methods.length > 0) {
                data.methodtrace = methods.join(' => ');
            }
            data.level = currentLevel;
            data.log = logs.slice(0); //(logs.length == 1) ? logs[0] : logs.slice(0);

            if (isConnectedToInternet()) {
                sendLogsToServer(JSON.stringify(data));
            } else {
                saveLogsToFile(data);
            }
        }

        // clean up the logs
        methods = [];
        logs = [];
        currentLevel = 'debug';
    }

    // read all saved log files and send them to server if Internet is available
    function processFileLogs() {
        if (isConnectedToInternet()) {
            var localFolder = Windows.Storage.ApplicationData.current.localFolder;
            localFolder.getFilesAsync().then(function (files) {
                files.forEach(function (file) {
                    if (file && file.displayName && file.displayName.indexOf("logs") == 0) {
                        Windows.Storage.FileIO.readTextAsync(file).then(function (text) {
                            sendLogsToServer(text);
                        }).then(function () {
                            file.deleteAsync();
                        }).done(function () { }, function (err) { });
                    }
                });
            });
        }
    }

    // save a log entry to file system if Internet is not available
    function saveLogsToFile(obj) {
        var fileName = "logs.txt";
        var content = JSON.stringify(obj);
        var localFolder = Windows.Storage.ApplicationData.current.localFolder;
        var saveOption = Windows.Storage.CreationCollisionOption;
        localFolder.createFileAsync(fileName, saveOption.generateUniqueName).then(
            function (file) {
                return Windows.Storage.FileIO.writeTextAsync(file, content);
            }).done(function () {
                console.log("Log saved");
            }, function (error) {
                console.log("Log saved error");
            });
    }

    // send log message to logging server
    function sendLogsToServer(jsonData) {
        WinJS.xhr({
            type: "post",
            url: loggingServer,
            headers: { "Content-type": "application/json" },
            data: jsonData
        }).done(function completed(c) {
            console.log("log sent");
        },
        function error(e) { // One more try? send to different server? or silently skip?
            console.log("log sent error");
        });
    }

    // get current application context
    function getContext() {
        if (!version) {
            var appVersion = Windows.ApplicationModel.Package.current.id.version;
            version = appVersion.major + "." + appVersion.minor + "." + appVersion.build + "." + appVersion.revision;
            try {
                var deviceInfo = new Windows.Security.ExchangeActiveSyncProvisioning.EasClientDeviceInformation();
                manufacturer = deviceInfo.systemManufacturer;
                model = deviceInfo.systemProductName;
            } catch (e) {
                manufacturer = 'unknown';
                model = 'unknown';
            }
        }
        var context = {};
        context.version = version;
        context.manufacturer = manufacturer;
        context.model = model;
        context.os = "Windows 8";
        context.lang = navigator.appName == "Netscape" ? navigator.language : navigator.userLanguage;
        context.screen = screen.width + "x" + screen.height;
        context.orientation = getOrientation();
        context.timezone = (-(new Date()).getTimezoneOffset() / 60).toString();
        return context;
    }

    // determine current orientation
    function getOrientation() {
        var orientation = "unknown";
        switch (Windows.Graphics.Display.DisplayProperties.currentOrientation) {
            case Windows.Graphics.Display.DisplayOrientations.landscape:
                orientation = "landscape";
                break;
            case Windows.Graphics.Display.DisplayOrientations.portrait:
                orientation = "portrait";
                break;
            case Windows.Graphics.Display.DisplayOrientations.landscapeFlipped:
                orientation = "landscapeFlipped";
                break;
            case Windows.Graphics.Display.DisplayOrientations.portraitFlipped:
                orientation = "portraitFlipped";
                break;
        }
        return orientation;
    }

    // check if Internet access is available
    function isConnectedToInternet() {
        var connectivity = Windows.Networking.Connectivity;
        var profile = connectivity.NetworkInformation.getInternetConnectionProfile();
        if (profile) {
            var connected = (profile.getNetworkConnectivityLevel() == connectivity.NetworkConnectivityLevel.internetAccess);
            return connected;
        } else {
            return false;
        }
    }

    WinJS.Namespace.define("Logging", {
        registerLogging: registerLogging,
        unregisterLogging: unregisterLogging,
        page: pageLog,
        method: methodLog,
        fatal: crashLog,
        error: errorLog,
        warning: warningLog,
        info: infoLog,
        debug: debugLog
    });
})();