- PVSM.RU - https://www.pvsm.ru -
После написания функционала авторекордера действий пользователя, названного нами breadcrumbs, в WinForms [1] и Wpf [2], пришло время добраться и до клиент-серверных технологий.
Начнем с простого — JavaScript. В отличии от десктопных приложений тут все довольно просто — подписываемся на события, записываем необходимые данные и, в общем-то, всё.
Используем стандартный addEventListener [3] для подписки на события в js. Вешаем обработчики событий на объект window для того, чтобы получать уведомления о событиях со всех элементов страницы.
Напишем класс, который будет подписываться на все нужные нам события:
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
);
}
}
}
}
Теперь нас ждут увлекательные муки выбора тех самых ценных событий, на которые имеет смысл подписаться. Для начала найдем полный список событий: Events [4]. Ох, как же их много… Чтобы не захламлять лог кучей лишней информации, придется выбрать самые главные события:
А как же быть с паролями, мы же их соберем? Нехорошо это, неприватно…
Сделаем проверку на то, является ли event.target инпутом и получим тип этого инпута. Если получили password — запишем * вместо значения.
isSecureElement(event) {
return event.target && event.target.type && event.target.type.toLowerCase() === "password";
}
Но есть события, на которые не подпишешься простым addEventListener, как мы делали это ранее. Это такие события, как ajax запросы и логирование в console. Ajax запросы важно логировать для того, чтобы получить полную картину действий пользователя, к тому же, падения часто происходят как раз на взаимодействиях с сервером. В console же может писаться важная отладочная информация (в виде предупреждений, ошибок, ну или просто логов) как самим разработчиком сайта, так и из сторонних библиотек.
Для таких видов событий придется писать обертки для стандартных js функций. В них подменяем стандартную функцию на свою собственную (createBreadcrumb), где параллельно с нашими действиями (в данном случае записью в breadcrumbs) вызываем предварительно сохраненную стандартную функцию. Вот как это выглядит для 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);
}
};
}
}
Для ajax запросов все несколько сложнее — тут помимо того, что надо переопределить стандартную функцию open, надо еще и добавить callback на функцию onload, чтобы получать данные на изменении статуса requestа, иначе не получим код ответа сервера.
И вот что у нас получилось:
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);
}
}
}
Правда, стоит отметить, что у оберток есть один серьезный недостаток — вызываемая функция перемещается внутрь нашей функции, в связи с чем меняется имя файла, из которого она была вызвана.
Полный исходный код JavaScript клиента на ES6 можно посмотреть на GitHub [15]. Документация по клиенту здесь [16].
А теперь немного о том, что можно сделать для решения этой задачи в ASP.NET. На серверной стороне трекаем все входящие реквесты, предшествующие падению. Для ASP.NET (WebForms + MVC) реализуем на базе IHttpModule и эвента HttpApplication.BeginRequest [17]:
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);
}
}
Для разделения и фильтрации реквестов от разных пользователей используем куку-трекер. При сохранении информации о реквесте проверяем, есть ли в нём нужная нам кука. Если ещё нет, добавляем и сохраняем её значение, не забываем валидировать:
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;
}
}
Это позволяет не закладываться, например, на SessionState и отделять уникальные сеансы, даже когда пользователь ещё не авторизован [18] или сессия вообще выключена [19].
Таким образом, такой подход работает как в старом добром ASP.NET (WebForms + MVC),
так и в новом ASP.NET Core, где с привычной всем сессией дела несколько по-другому:
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);
}
...
}
}
Сохранение реквеста:
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;
}
}
Полный исходный код ASP.NET клиетов на GitHub: ASP.NET [20] и ASP.NET Core [21].
Автор: r_kolesnikov
Источник [22]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/269931
Ссылки в тексте:
[1] WinForms: https://habrahabr.ru/company/devexpress/blog/342530/
[2] Wpf: https://habrahabr.ru/company/devexpress/blog/343358/
[3] addEventListener: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
[4] Events: https://developer.mozilla.org/en-US/docs/Web/Events
[5] DOMContentLoaded: https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded
[6] click: https://developer.mozilla.org/en-US/docs/Web/Events/click
[7] dblclick: https://developer.mozilla.org/en-US/docs/Web/Events/dblclick
[8] auxclick: https://developer.mozilla.org/en-US/docs/Web/Events/auxclick
[9] keyDown: https://developer.mozilla.org/en-US/docs/Web/Events/keydown
[10] keyPress: https://developer.mozilla.org/en-US/docs/Web/Events/keypress
[11] keyUp: https://developer.mozilla.org/en-US/docs/Web/Events/keyup
[12] submit: https://developer.mozilla.org/en-US/docs/Web/Events/submit
[13] reset: https://developer.mozilla.org/en-US/docs/Web/Events/reset
[14] change: https://developer.mozilla.org/en-US/docs/Web/Events/change
[15] GitHub: https://github.com/DevExpress/Logify.Alert.Clients/tree/develop/js/Logify.Alert.JS
[16] здесь: https://logify.devexpress.com/Alert/Documentation/Send/ApiReference/JS?utm_source=publication_breadcrumbs&utm_campaign=blog
[17] HttpApplication.BeginRequest: https://msdn.microsoft.com/en-us/library/system.web.httpapplication.beginrequest(v=vs.110).aspx
[18] авторизован: https://msdn.microsoft.com/en-us/library/system.web.sessionstate.httpsessionstate.sessionid.aspx#Remarks
[19] выключена: https://msdn.microsoft.com/en-us/library/system.web.sessionstate.sessionstatemode(v=vs.110).aspx
[20] ASP.NET: https://github.com/DevExpress/Logify.Alert.Clients/tree/develop/dotnet/Logify.Alert.Web
[21] ASP.NET Core: https://github.com/DevExpress/Logify.Alert.Clients/tree/develop/dotnet/Logify.Alert.NetCore.Web
[22] Источник: https://habrahabr.ru/post/343816/
Нажмите здесь для печати.