< Summary

Information
Class: OzonOrdersWeb.Middleware.UserHistoryMiddleware
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Middleware/UserHistoryMiddleware.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 138
Coverable lines: 138
Total lines: 197
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 66
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
InvokeAsync()0%3906620%
GetHistoryTrackingScript(...)0%2040%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Middleware/UserHistoryMiddleware.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.RegularExpressions;
 3
 4namespace OzonOrdersWeb.Middleware;
 5
 6public class UserHistoryMiddleware
 7{
 8    private readonly RequestDelegate _next;
 9
 010    public UserHistoryMiddleware(RequestDelegate next)
 11    {
 012        _next = next;
 013    }
 14
 15    public async Task InvokeAsync(HttpContext context)
 16    {
 17        // Пропускаем статические файлы, API и другие специальные пути
 018        var path = context.Request.Path.Value?.ToLower() ?? "";
 019        if (path.StartsWith("/lib/") ||
 020            path.StartsWith("/css/") ||
 021            path.StartsWith("/js/") ||
 022            path.StartsWith("/images/") ||
 023            path.StartsWith("/favicon") ||
 024            path.StartsWith("/_framework/") ||
 025            path.StartsWith("/api/") ||
 026            path.StartsWith("/hangfire") ||
 027            path.StartsWith("/notificationhub") ||
 028            path.StartsWith("/orderhistoryhub") ||
 029            path.StartsWith("/priceupdatehub") ||
 030            path.StartsWith("/.well-known/") ||
 031            path.StartsWith("/devtools/") ||
 032            path.StartsWith("/json") ||
 033            context.Request.Headers["Accept"].ToString().Contains("application/json") ||
 034            context.Request.Headers["Accept"].ToString().Contains("application/xml"))
 35        {
 036            await _next(context);
 037            return;
 38        }
 39
 40        // Перехватываем только HTML ответы
 041        var originalBodyStream = context.Response.Body;
 042        using var responseBody = new MemoryStream();
 043        context.Response.Body = responseBody;
 44
 45        try
 46        {
 047            await _next(context);
 48
 49            // Проверяем, что это HTML ответ и статус успешный
 050            var contentType = context.Response.ContentType?.ToLower() ?? "";
 051            var isHtml = contentType.Contains("text/html") &&
 052                         context.Response.StatusCode >= 200 &&
 053                         context.Response.StatusCode < 300;
 54
 055            string? responseText = null;
 56
 057            if (isHtml)
 58            {
 059                responseBody.Seek(0, SeekOrigin.Begin);
 060                var bodyText = await new StreamReader(responseBody, Encoding.UTF8).ReadToEndAsync();
 61
 62                // Проверяем, что это действительно HTML (содержит теги)
 063                if (bodyText.Contains("<html", StringComparison.OrdinalIgnoreCase) ||
 064                    bodyText.Contains("<!DOCTYPE", StringComparison.OrdinalIgnoreCase) ||
 065                    bodyText.Contains("<body", StringComparison.OrdinalIgnoreCase))
 66                {
 067                    var historyScript = "<script src=\"/js/observability/client-telemetry.js\"></script>\n"
 068                                        + GetHistoryTrackingScript(context);
 69
 070                    var bodyCloseIndex = bodyText.LastIndexOf("</body>", StringComparison.OrdinalIgnoreCase);
 071                    if (bodyCloseIndex >= 0)
 72                    {
 073                        responseText = bodyText.Insert(bodyCloseIndex, historyScript + "\n");
 74                    }
 75                    else
 76                    {
 077                        var htmlCloseIndex = bodyText.LastIndexOf("</html>", StringComparison.OrdinalIgnoreCase);
 078                        responseText = htmlCloseIndex >= 0
 079                            ? bodyText.Insert(htmlCloseIndex, historyScript + "\n")
 080                            : bodyText + historyScript;
 81                    }
 82                }
 83            }
 84
 85            // Возвращаем оригинальный поток ДО записи (иначе при ошибке обработчик
 86            // попытается писать в уже освобождённый MemoryStream — ObjectDisposedException).
 087            context.Response.Body = originalBodyStream;
 88
 089            if (responseText != null)
 90            {
 091                var responseBytes = Encoding.UTF8.GetBytes(responseText);
 092                context.Response.ContentLength = responseBytes.Length;
 093                await originalBodyStream.WriteAsync(responseBytes);
 94            }
 95            else
 96            {
 097                responseBody.Seek(0, SeekOrigin.Begin);
 098                await responseBody.CopyToAsync(originalBodyStream);
 99            }
 0100        }
 101        finally
 102        {
 103            // Гарантированно восстанавливаем исходный поток ответа: иначе вышестоящий
 104            // ExceptionHandler/StatusCodePages при повторном выполнении пишет в закрытый MemoryStream.
 0105            context.Response.Body = originalBodyStream;
 106        }
 0107    }
 108
 109    private string GetHistoryTrackingScript(HttpContext context)
 110    {
 0111        var currentPath = (context.Request.Path + context.Request.QueryString).ToString();
 0112        var currentTitle = context.Request.Path.Value?.Split('/').LastOrDefault() ?? "Страница";
 113
 114        // Экранируем специальные символы для JavaScript
 0115        var escapedPath = currentPath.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\"", "\\\"");
 0116        var escapedTitle = currentTitle.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\"", "\\\"");
 117
 0118        return $@"
 0119<script>
 0120(function() {{
 0121    'use strict';
 0122
 0123    // Функция для сохранения истории в local storage
 0124    function savePageToHistory(path, title) {{
 0125        try {{
 0126            const historyKey = 'userPageHistory';
 0127            const maxHistoryItems = 50; // Максимальное количество записей
 0128
 0129            let history = JSON.parse(localStorage.getItem(historyKey) || '[]');
 0130
 0131            // Добавляем текущую страницу
 0132            const historyItem = {{
 0133                path: path,
 0134                title: title || document.title || path,
 0135                timestamp: new Date().toISOString(),
 0136                displayTime: new Date().toLocaleString('ru-RU', {{
 0137                    day: '2-digit',
 0138                    month: '2-digit',
 0139                    year: 'numeric',
 0140                    hour: '2-digit',
 0141                    minute: '2-digit',
 0142                    second: '2-digit'
 0143                }})
 0144            }};
 0145
 0146            // Удаляем дубликаты (если та же страница уже есть)
 0147            history = history.filter(item => item.path !== path);
 0148
 0149            // Добавляем новую запись в начало
 0150            history.unshift(historyItem);
 0151
 0152            // Ограничиваем количество записей
 0153            if (history.length > maxHistoryItems) {{
 0154                history = history.slice(0, maxHistoryItems);
 0155            }}
 0156
 0157            // Сохраняем в local storage
 0158            localStorage.setItem(historyKey, JSON.stringify(history));
 0159        }} catch (e) {{
 0160            console.error('Ошибка при сохранении истории:', e);
 0161        }}
 0162    }}
 0163
 0164    // Сохраняем текущую страницу при загрузке
 0165    if (document.readyState === 'loading') {{
 0166        document.addEventListener('DOMContentLoaded', function() {{
 0167            savePageToHistory('{escapedPath}', '{escapedTitle}');
 0168        }});
 0169    }} else {{
 0170        savePageToHistory('{escapedPath}', '{escapedTitle}');
 0171    }}
 0172
 0173    // Отслеживаем переходы по ссылкам
 0174    document.addEventListener('click', function(e) {{
 0175        const link = e.target.closest('a[href]');
 0176        if (link && link.href && !link.href.startsWith('javascript:') && !link.href.startsWith('#')) {{
 0177            // Небольшая задержка для сохранения перед переходом
 0178            setTimeout(function() {{
 0179                const href = link.getAttribute('href');
 0180                if (href && !href.startsWith('http') || href.startsWith(window.location.origin)) {{
 0181                    const fullPath = href.startsWith('http') ? href : (window.location.origin + href);
 0182                    savePageToHistory(fullPath, link.textContent.trim() || link.title || href);
 0183                }}
 0184            }}, 100);
 0185        }}
 0186    }});
 0187
 0188    // Отслеживаем изменения истории браузера (назад/вперед)
 0189    window.addEventListener('popstate', function() {{
 0190        setTimeout(function() {{
 0191            savePageToHistory(window.location.pathname + window.location.search, document.title);
 0192        }}, 100);
 0193    }});
 0194}})();
 0195</script>";
 196    }
 197}