📜 ⬆️ ⬇️

We collect user activity in JS and ASP

After writing the functionality of the autorecorder of user actions, called by us breadcrumbs, in WinForms and Wpf , it is time to get to client-server technologies.

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

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

Let's write a class that will subscribe to all the events we need:
')
class eventRecorder { 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 a fascinating torment of choice of the most valuable events for which it makes sense to subscribe. First we find a complete list of events: Events . Oh, how many of them ... In order not to clutter up the log with a bunch of extra information, you have to choose the most important events:


But there are events for which you are not subscribing to a simple addEventListener, as we did before. These are events such as ajax requests and console logging. Ajax requests are important to log in order to get a complete picture of user actions, and besides, crashes often occur during server interactions. In the console, important debugging information can be written (in the form of warnings, errors, well, or just logs) both by the site developer and from third-party libraries.

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

 export default class consoleEventRecorder { 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 - besides the need to override the standard open function, you must also add a callback to the onload function to receive data on the request status change, otherwise we will not get the server response code.

And that's what happened with us:

 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); } } } 

However, 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 of 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 that precede the fall. For ASP.NET (WebForms + MVC) we implement on the basis of IHttpModule and the event HttpApplication.BeginRequest :

 using System.Web; public class AspExceptionHandler : IHttpModule { public void OnInit(HttpApplication context) { try { if(LogifyAlert.Instance.CollectBreadcrumbs) context.BeginRequest += this.OnBeginRequest; } catch { } } void OnBeginRequest(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 information about the request, we check if there is a cookie we need in it. If not yet, add and save its value, do not forget to validate:

 using System.Web; public class AspBreadcrumbsRecorder : BreadcrumbsRecorderBase{ internal void AddBreadcrumb(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"; string TryGetSessionId(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 pledge, for example, on SessionState and to separate unique sessions, even when the user is not yet authorized or the session is turned off altogether.



Thus, this approach works like in good old ASP.NET (WebForms + MVC),
so in the new ASP.NET Core, where with the usual session the case is somewhat different:

Middleware:

 using Microsoft.AspNetCore.Http; internal class LogifyAlertMiddleware { RequestDelegate next; public LogifyAlertMiddleware(RequestDelegate next) { this.next = next; ... } public async Task Invoke(HttpContext context) { try { if(LogifyAlert.Instance.CollectBreadcrumbs) NetCoreWebBreadcrumbsRecorder.Instance.AddBreadcrumb(context); await next(context); } ... } } 

Saving the request:

 using Microsoft.AspNetCore.Http; public class NetCoreWebBreadcrumbsRecorder : BreadcrumbsRecorderBase { internal void AddBreadcrumb(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"; string TryGetSessionId(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 .

Source: https://habr.com/ru/post/343816/


All Articles