We collect user activity in JS and ASP

    After writing the functionality of the auto-recorder of user actions, which we called breadcrumbs, in WinForms and Wpf , it's time to get to client-server technologies.

    image
    Let's start with the simple - JavaScript. Unlike desktop applications, everything is quite simple here - we subscribe to events, write down the necessary data and, in general, that's all.

    We use the standard addEventListener to subscribe to events in js. We hang event handlers on the window object in order to receive event notifications from all elements of the page.

    We will write a class that will subscribe to all the events we need:

    classeventRecorder{
       constructor() {
          this._events = [
             "DOMContentLoaded",
             "click",
             ...,
             "submit"
          ];
       }
       startListening(eventCallback) {
          this._mainCallback = function (event) {       
             this.collectBreadcrumb(event, eventCallback);
          }.bind(this);
          for (let i = 0; i < this._events.length; i++) {
             window.addEventListener(
                this._events[i],
                this._mainCallback,
                false
             );
         }
       }
       stopListening() {
          if (this._mainCallback) {
            for (let i = 0; i < this._events.length; i++) {
                window.removeEventListener(
                   this._events[i],
                   this._mainCallback,
                   false
                );
             }
         }
       }
    }
    

    Now we are waiting for the exciting torment of choosing those very valuable events that it makes sense to subscribe to. First, find the complete list of events: Events . Oh, how many of them ... In order not to litter the log with a bunch of unnecessary information, you have to choose the most important events:

    • DOMContentLoaded - to crash, you must know if the page has already been loaded
    • Mouse events ( the click , the dblclick , auxclick ) - there is not even any doubts in importance. Know when and where the user clicked - well, just a “must have.” In js click only works on left mouse clicks. To process the middle and right keys, we use the auxclick event.
    • Keyboard events ( a keyDown , keyPress , a keyUp ) - entered text, press the key combination - all here and all important.

      But what about passwords, will we collect them? This is not good, it is private ...
      We will check whether event.target is an input and get the type of this input. If you received a password - write * instead of the value.

      isSecureElement(event) {
         return event.target && event.target.type && event.target.type.toLowerCase() === "password";
      }
      
    • Events standard forms ( the submit and the reset ) - information about sending form data or cleaning.
    • The change event - where, without events, changes to the standard form elements.

    But there are events that you can’t subscribe to simply addEventListener, as we did before. These are events such as ajax requests and console logging. It is important to log Ajax requests in order to get a complete picture of user actions, in addition, crashes often occur just on interactions with the server. In the console, important debugging information can be written (in the form of warnings, errors, or simply logs) both by the site developer itself and from third-party libraries.

    For these kinds of events, you will have to write wrappers for standard js functions. In them, we replace the standard function with our own (createBreadcrumb), where in parallel with our actions (in this case, writing in breadcrumbs) we call the previously saved standard function. Here's what it looks like for console:

    exportdefaultclassconsoleEventRecorder{
        constructor() {
            this._events = [
                "log",
                "error",
                "warn"
            ];
        }
        startListening(eventCallback) {       
           for (let i = 0; i < this._events.length; i++) {
              this.wrapObject(console, this._events[i], eventCallback);
           }   
        }
        wrapObject(object, property, callback) {
            this._defaultCallback[property] = object[property];
            let wrapperClass = this;
            object[property] = function () {
                let args = Array.prototype.slice.call(arguments, 0);
                wrapperClass.createBreadcrumb(args, property, callback);
                if (typeof wrapperClass._defaultCallback[property] === "function") {
                    Function.prototype.apply.call(wrapperClass.
                    _defaultCallback[property], console, args);
                }
            };
        }
    }
    

    For ajax requests, everything is somewhat more complicated - in addition to the fact that you need to redefine the standard open function, you also need to add a callback to the onload function in order to receive data on the change in the request status, otherwise we will not receive the server response code.

    And here is what we got:

    addXMLRequestListenerCallback(callback) {
        if (XMLHttpRequest.callbacks) {
            XMLHttpRequest.callbacks.push(callback);
        } else {
            XMLHttpRequest.callbacks = [callback];
            this._defaultCallback = XMLHttpRequest.prototype.open;
            const wrapper = this;
            XMLHttpRequest.prototype.open = function () {
                const xhr = this;
                try {
                    if ('onload'in xhr) {
                        if (!xhr.onload) {
                            xhr.onload = callback;
                        } else {
                            const oldFunction = xhr.onload;
                            xhr.onload = function() {
                                callback(Array.prototype.slice.call(arguments));
                                oldFunction.apply(this, arguments);
                            }
                        }
                    }
                } catch (e) {
                    this.onreadystatechange = callback;
                }
                wrapper._defaultCallback.apply(this, arguments);
            }
        }
    }
    

    True, it is worth noting that wrappers have one serious drawback - the called function moves inside our function, and therefore the name of the file from which it was called changes.

    The full source code for the JavaScript client on ES6 can be viewed on GitHub . Customer documentation is here .

    And now a little about what can be done to solve this problem in ASP.NET. On the server side, we track all incoming requests preceding the crash. For ASP.NET (WebForms + MVC), we implement the following IHttpModule and the HttpApplication.BeginRequest event :

    using System.Web;
    publicclassAspExceptionHandler : IHttpModule {
        publicvoidOnInit(HttpApplication context) {
            try {
                if(LogifyAlert.Instance.CollectBreadcrumbs)
                    context.BeginRequest += this.OnBeginRequest;
            }
            catch { }
        }
        voidOnBeginRequest(object sender, EventArgs e) {
            AspBreadcrumbsRecorder
                .Instance
                .AddBreadcrumb(sender as HttpApplication);
        }
    }
    

    To separate and filter requests from different users, we use a cookie tracker. When saving the request information, we check if it contains the cookie we need. If not yet, add and save its value, do not forget to validate:

    using System.Web;
    publicclassAspBreadcrumbsRecorder : BreadcrumbsRecorderBase{
        internalvoidAddBreadcrumb(HttpApplication httpApplication) {
    	...
            HttpRequest request = httpApplication.Context.Request;
            HttpResponse response = httpApplication.Context.Response;
            Breadcrumb breadcrumb = new Breadcrumb();
            breadcrumb.CustomData = new Dictionary<string, string>() {
                ...
                { "session", TryGetSessionId(request, response) }
            };
            base.AddBreadcrumb(breadcrumb);
        }
        string CookieName = "BreadcrumbsCookie";
        stringTryGetSessionId(HttpRequest request, HttpResponse response) {
            string cookieValue = null;
            try {
                HttpCookie cookie = request.Cookies[CookieName];
                if(cookie != null) {
                    Guid validGuid = Guid.Empty;
                    if(Guid.TryParse(cookie.Value, out validGuid))
                        cookieValue = cookie.Value;
                } else {
                    cookieValue = Guid.NewGuid().ToString();
                    cookie = new HttpCookie(CookieName, cookieValue);
                    cookie.HttpOnly = true;
                    response.Cookies.Add(cookie);
                }
            } catch { }
            return cookieValue;
        }
    }
    

    This allows you not to log on, for example, to SessionState and separate unique sessions, even when the user is not yet logged in or the session is completely turned off .



    Thus, this approach works both in the good old ASP.NET (WebForms + MVC),
    and in the new ASP.NET Core, where things are somewhat different with the usual session:

    Middleware:

    using Microsoft.AspNetCore.Http;
    internalclassLogifyAlertMiddleware {
        RequestDelegate next;
        publicLogifyAlertMiddleware(RequestDelegate next) {
            this.next = next;
            ...
        }
        publicasync Task Invoke(HttpContext context) {
            try {
                if(LogifyAlert.Instance.CollectBreadcrumbs)
                    NetCoreWebBreadcrumbsRecorder.Instance.AddBreadcrumb(context);
                await next(context);
            }
            ...
        }
    }
    

    Saving request:

    using Microsoft.AspNetCore.Http;
    publicclassNetCoreWebBreadcrumbsRecorder : BreadcrumbsRecorderBase {
        internalvoidAddBreadcrumb(HttpContext context) {
            if(context.Request != null && context.Request.Path != null && 
                context.Response != null) {
                    Breadcrumb breadcrumb = new Breadcrumb();
                    breadcrumb.CustomData = new Dictionary<string, string>() {
                        ...
                        { "session", TryGetSessionId(context) }
                    };
                    base.AddBreadcrumb(breadcrumb);
            }
        }
        string CookieName = "BreadcrumbsCookie";
        stringTryGetSessionId(HttpContext context) {
            string cookieValue = null;
            try {
                string cookie = context.Request.Cookies[CookieName];
                if(!string.IsNullOrEmpty(cookie)) {
                    Guid validGuid = Guid.Empty;
                    if(Guid.TryParse(cookie, out validGuid))
                        cookieValue = cookie;
                }
                if(string.IsNullOrEmpty(cookieValue)) {
                    cookieValue = Guid.NewGuid().ToString();
                    context.Response.Cookies.Append(CookieName, cookieValue,
                        new CookieOptions() { HttpOnly = true });
                }
            } catch { }
            return cookieValue;
        }
    }
    

    Full source code for ASP.NET clients on GitHub: ASP.NET and ASP.NET Core .

    Also popular now: