// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Net; using System.Web; using System.Web.Mvc; using System.Web.UI; using Microsoft.Practices.ObjectBuilder2; using Newtonsoft.Json.Linq; using Tools.CrashReporter.CrashReportWebSite.DataModels; using Tools.CrashReporter.CrashReportWebSite.DataModels.Repositories; using Tools.CrashReporter.CrashReportWebSite.ViewModels; namespace Tools.CrashReporter.CrashReportWebSite.Controllers { /// /// A controller to handle the Bugg data. /// public class BuggsController : Controller { private int pageSize = 50; /// /// An empty constructor. /// public BuggsController() { } /// /// The Index action. /// /// The form of user data passed up from the client. /// The view to display a list of Buggs on the client. public ActionResult Index( FormCollection BuggsForm ) { using (var logTimer = new FAutoScopedLogTimer( this.GetType().ToString(), bCreateNewLog: true )) { var formData = new FormHelper( Request, BuggsForm, "CrashesInTimeFrameGroup" ); var results = GetResults( formData ); results.GenerationTime = logTimer.GetElapsedSeconds().ToString( "F2" ); return View( "Index", results ); } } /// /// The Show action. /// /// The form of user data passed up from the client. /// The unique id of the Bugg. /// The view to display a Bugg on the client. public ActionResult Show( FormCollection buggsForm, int id ) { using( var logTimer = new FAutoScopedLogTimer( this.GetType().ToString() + "(BuggId=" + id + ")", bCreateNewLog: true ) ) { // Set the display properties based on the radio buttons var displayModuleNames = buggsForm["DisplayModuleNames"] == "true"; var displayFunctionNames = buggsForm["DisplayFunctionNames"] == "true"; var displayFileNames = buggsForm["DisplayFileNames"] == "true"; var displayFilePathNames = false; if( buggsForm["DisplayFilePathNames"] == "true" ) { displayFilePathNames = true; displayFileNames = false; } var displayUnformattedCallStack = buggsForm["DisplayUnformattedCallStack"] == "true"; var model = GetResult(id); model.Bugg.PrepareBuggForJira(model.Crashes); // Handle 'CopyToJira' button var buggIdToBeAddedToJira = 0; foreach (var entry in buggsForm.Cast().Where(entry => entry.ToString().Contains("CopyToJira-"))) { int.TryParse(entry.ToString().Substring("CopyToJira-".Length), out buggIdToBeAddedToJira); break; } if (buggIdToBeAddedToJira != 0) { model.Bugg.JiraProject = buggsForm["JiraProject"]; model.Bugg.CopyToJira(); } var jc = JiraConnection.Get(); var bValidJira = false; // Verify valid JiraID, this may be still a TTP if( !string.IsNullOrEmpty( model.Bugg.TTPID ) ) { var jira = 0; int.TryParse(model.Bugg.TTPID, out jira); if (jira == 0) { bValidJira = true; } } if( jc.CanBeUsed() && bValidJira ) { // Grab the data form JIRA. var jiraSearchQuery = "key = " + model.Bugg.TTPID; var jiraResults = new Dictionary>(); try { jiraResults = jc.SearchJiraTickets( jiraSearchQuery, new string[] { "key", // string "summary", // string "components", // System.Collections.ArrayList, Dictionary, name "resolution", // System.Collections.Generic.Dictionary`2[System.String,System.Object], name "fixVersions", // System.Collections.ArrayList, Dictionary, name "customfield_11200" // string } ); } catch (System.Exception) { model.Bugg.JiraSummary = "JIRA MISMATCH"; model.Bugg.JiraComponentsText = "JIRA MISMATCH"; model.Bugg.JiraResolution = "JIRA MISMATCH"; model.Bugg.JiraFixVersionsText = "JIRA MISMATCH"; model.Bugg.JiraFixCL = "JIRA MISMATCH"; } // Jira Key, Summary, Components, Resolution, Fix version, Fix changelist if(jiraResults.Any()) { var jira = jiraResults.First(); var summary = (string)jira.Value["summary"]; var componentsText = ""; var components = (System.Collections.ArrayList)jira.Value["components"]; foreach (Dictionary component in components) { componentsText += (string)component["name"]; componentsText += " "; } var resolutionFields = (Dictionary)jira.Value["resolution"]; var resolution = resolutionFields != null ? (string)resolutionFields["name"] : ""; var fixVersionsText = ""; var fixVersions = (System.Collections.ArrayList)jira.Value["fixVersions"]; foreach( Dictionary fixVersion in fixVersions ) { fixVersionsText += (string)fixVersion["name"]; fixVersionsText += " "; } var fixCl = jira.Value["customfield_11200"] != null ? (int)(decimal)jira.Value["customfield_11200"] : 0; //Conversion to ado.net entity framework model.Bugg.JiraSummary = summary; model.Bugg.JiraComponentsText = componentsText; model.Bugg.JiraResolution = resolution; model.Bugg.JiraFixVersionsText = fixVersionsText; if( fixCl != 0 ) { model.Bugg.FixedChangeList = fixCl.ToString(); } } } // Apply any user settings if( buggsForm.Count > 0 ) { if( !string.IsNullOrEmpty( buggsForm["SetStatus"] ) ) { model.Bugg.Status = buggsForm["SetStatus"]; model.Bugg.Crashes.ForEach(data =>data.Status = buggsForm["SetStatus"]); } if( !string.IsNullOrEmpty( buggsForm["SetFixedIn"] ) ) { model.Bugg.FixedChangeList = buggsForm["SetFixedIn"]; model.Bugg.Crashes.ForEach(data => data.FixedChangeList = buggsForm["SetFixedIn"]); } if( !string.IsNullOrEmpty( buggsForm["SetTTP"] ) ) { model.Bugg.TTPID = buggsForm["SetTTP"]; model.Bugg.Crashes.ForEach(data => data.Jira = buggsForm["SetTTP"]); } using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { unitOfWork.BuggRepository.Update(model.Bugg); unitOfWork.Save(); } } // Set up the view model with the crash data int page; int.TryParse(buggsForm["Page"], out page); model.Crashes = model.Crashes.Skip(page * pageSize).Take(pageSize).ToList(); var newCrash = model.Crashes.FirstOrDefault(); if( newCrash != null ) { var callStack = new CallStackContainer( newCrash ); // Set callstack properties callStack.bDisplayModuleNames = displayModuleNames; callStack.bDisplayFunctionNames = displayFunctionNames; callStack.bDisplayFileNames = displayFileNames; callStack.bDisplayFilePathNames = displayFilePathNames; callStack.bDisplayUnformattedCallStack = displayUnformattedCallStack; model.CallStack = callStack; // Shorten very long function names. foreach( var entry in model.CallStack.CallStackEntries ) { entry.FunctionName = entry.GetTrimmedFunctionName( 128 ); } model.SourceContext = newCrash.SourceContext; model.LatestCrashSummary = newCrash.Summary; } model.LatestCrashSummary = newCrash.Summary; model.Bugg.LatestCrashSummary = newCrash.Summary; model.GenerationTime = logTimer.GetElapsedSeconds().ToString( "F2" ); //Populate Jira Projects var jiraConnection = JiraConnection.Get(); var response = jiraConnection.JiraRequest("/issue/createmeta", JiraConnection.JiraMethod.GET, null, HttpStatusCode.OK); using (var responseReader = new StreamReader(response.GetResponseStream())) { var responseText = responseReader.ReadToEnd(); JObject jsonObject = JObject.Parse(responseText); JToken fields = jsonObject["projects"]; foreach (var project in fields) { model.JiraProjects.Add(new SelectListItem() { Text = project["name"].ToObject(), Value = project["key"].ToObject() }); } } model.PagingInfo = new PagingInfo { CurrentPage = page, PageSize = pageSize, TotalResults = model.Bugg.NumberOfCrashes.Value }; return View( "Show", model ); } } #region Private Methods /// /// Retrieve all Buggs matching the search criteria. /// /// The incoming form of search criteria from the client. /// A view to display the filtered Buggs. private BuggsViewModel GetResults(FormHelper FormData) { // Right now we take a Result IQueryable starting with ListAll() Buggs then we widdle away the result set by tacking on // Linq queries. Essentially it's Results.ListAll().Where().Where().Where().Where().Where().Where() // Could possibly create more optimized queries when we know exactly what we're querying // The downside is that if we add more parameters each query may need to be updated.... Or we just add new case statements // The other downside is that there's less code reuse, but that may be worth it. var fromDate = FormData.DateFrom; var toDate = FormData.DateTo.AddDays(1); IQueryable results = null; var skip = (FormData.Page - 1) * FormData.PageSize; var take = FormData.PageSize; var sortedResultsList = new List(); var groupCounts = new SortedDictionary(); int totalCountedRecords = 0; using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { var userGroupId = unitOfWork.UserGroupRepository.First(data => data.Name == FormData.UserGroup).Id; // Get every Bugg. var resultsAll = unitOfWork.BuggRepository.ListAll(); // Look at all Buggs that are still 'open' i.e. the last crash occurred in our date range. results = FilterByDate(resultsAll, FormData.DateFrom, FormData.DateTo); // Filter results by build version. if (!string.IsNullOrEmpty(FormData.VersionName)) { results = FilterByBuildVersion(results, FormData.VersionName); } // Filter by BranchName if (!string.IsNullOrEmpty(FormData.BranchName)) { results = results.Where( data => data.Crashes.Any(da => da.Branch.Equals(FormData.BranchName))); } // Filter by PlatformName if (!string.IsNullOrEmpty(FormData.PlatformName)) { results = results.Where( data => data.Crashes.Any(da => da.PlatformName.Equals(FormData.PlatformName))); } // Run at the end if (!string.IsNullOrEmpty(FormData.SearchQuery)) { try { var queryString = HttpUtility.HtmlDecode(FormData.SearchQuery.ToString()) ?? ""; // Take out terms starting with a - var terms = queryString.Split("-, ;+".ToCharArray(), StringSplitOptions.RemoveEmptyEntries); var allFuncionCallIds = new HashSet(); foreach (var term in terms) { var functionCallIds = unitOfWork.FunctionRepository.Get(functionCallInstance => functionCallInstance.Call.Contains(term)).Select(x => x.Id).ToList(); foreach (var id in functionCallIds) { allFuncionCallIds.Add(id); } } // Search for all function ids. OR operation, not very efficient, but for searching for one function should be ok. results = allFuncionCallIds.Aggregate(results, (current, id) => current.Where(x => x.Pattern.Contains(id + "+") || x.Pattern.Contains("+" + id))); } catch (Exception ex) { Debug.WriteLine("Exception in Search: " + ex.ToString()); } } // Filter by Crash Type if (FormData.CrashType != "All") { switch (FormData.CrashType) { case "Crashes": results = results.Where(buggInstance => buggInstance.CrashType == 1); break; case "Assert": results = results.Where(buggInstance => buggInstance.CrashType == 2); break; case "Ensure": results = results.Where(buggInstance => buggInstance.CrashType == 3); break; case "CrashesAsserts": results = results.Where(buggInstance => buggInstance.CrashType == 1 || buggInstance.CrashType == 2); break; } } // Filter by user group if present if (!string.IsNullOrEmpty(FormData.UserGroup)) results = FilterByUserGroup(results, FormData.UserGroup); if (!string.IsNullOrEmpty(FormData.JiraId)) { results = FilterByJira(results, FormData.JiraId); } // Grab just the results we want to display on this page totalCountedRecords = results.Count(); var crashesByUserGroupCounts = unitOfWork.CrashRepository.ListAll().Where(data => data.User.UserGroupId == userGroupId && data.TimeOfCrash >= fromDate && data.TimeOfCrash <= toDate && data.PatternId != null) .GroupBy(data => data.PatternId) .Select(data => new {data.Key, Count = data.Count()}) .OrderByDescending(data => data.Count) .ToDictionary(data => data.Key, data => data.Count); var crashesInTimeFrameCounts = unitOfWork.CrashRepository.ListAll().Where(data => data.TimeOfCrash >= fromDate && data.TimeOfCrash <= toDate && data.PatternId != null) .GroupBy(data => data.PatternId) .Select(data => new {data.Key, Count = data.Count()}) .OrderByDescending(data => data.Count) .ToDictionary(data => data.Key, data => data.Count); var affectedUsers = unitOfWork.CrashRepository.ListAll().Where(data => data.User.UserGroupId == userGroupId && data.TimeOfCrash >= fromDate && data.TimeOfCrash <= toDate && data.PatternId != null) .Select(data => new {PatternId = data.PatternId, ComputerName = data.ComputerName}) .Distinct() .GroupBy(data => data.PatternId) .Select(data => new {data.Key, Count = data.Count()}) .OrderByDescending(data => data.Count) .ToDictionary(data => data.Key, data => data.Count); results = GetSortedResults(results, FormData.SortTerm, FormData.SortTerm == "descending", FormData.DateFrom, FormData.DateTo, FormData.UserGroup); sortedResultsList = results.ToList(); // Get UserGroup ResultCounts var groups = results.SelectMany(data => data.UserGroups) .GroupBy(data => data.Name) .Select(data => new {Key = data.Key, Count = data.Count()}) .ToDictionary(x => x.Key, y => y.Count); groupCounts = new SortedDictionary(groups); foreach (var bugg in sortedResultsList) { if (bugg.PatternId.HasValue && crashesByUserGroupCounts.ContainsKey(bugg.PatternId.Value)) { bugg.CrashesInTimeFrameGroup = crashesByUserGroupCounts[bugg.PatternId.Value]; } else bugg.CrashesInTimeFrameGroup = 0; if (bugg.PatternId.HasValue && crashesInTimeFrameCounts.ContainsKey(bugg.PatternId.Value)) { bugg.CrashesInTimeFrameAll = crashesInTimeFrameCounts[bugg.PatternId.Value]; } else bugg.CrashesInTimeFrameAll = 0; if (bugg.PatternId.HasValue && affectedUsers.ContainsKey(bugg.PatternId.Value)) { bugg.NumberOfUniqueMachines = affectedUsers[bugg.PatternId.Value]; } else bugg.NumberOfUniqueMachines = 0; } sortedResultsList = sortedResultsList.OrderByDescending(data => data.CrashesInTimeFrameGroup).Skip(skip) .Take(totalCountedRecords >= skip + take ? take : totalCountedRecords).ToList(); foreach (var bugg in sortedResultsList) { var crash = unitOfWork.CrashRepository.First(data => data.BuggId == bugg.Id); bugg.FunctionCalls = crash.GetCallStack().GetFunctionCalls(); } } List branchNames; List versionNames; List platformNames; using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { branchNames = unitOfWork.CrashRepository.GetBranchesAsListItems(); versionNames = unitOfWork.CrashRepository.GetVersionsAsListItems(); platformNames = unitOfWork.CrashRepository.GetPlatformsAsListItems(); } return new BuggsViewModel() { Results = sortedResultsList, PagingInfo = new PagingInfo { CurrentPage = FormData.Page, PageSize = FormData.PageSize, TotalResults = totalCountedRecords }, SortTerm = FormData.SortTerm, SortOrder = FormData.SortOrder, UserGroup = FormData.UserGroup, CrashType = FormData.CrashType, SearchQuery = FormData.SearchQuery, BranchNames = branchNames, VersionNames = versionNames, PlatformNames = platformNames, DateFrom = (long)(FormData.DateFrom - CrashesViewModel.Epoch).TotalMilliseconds, DateTo = (long)(FormData.DateTo - CrashesViewModel.Epoch).TotalMilliseconds, VersionName = FormData.VersionName, PlatformName = FormData.PlatformName, BranchName = FormData.BranchName, GroupCounts = groupCounts, Jira = FormData.JiraId, }; } /// /// Retrieve all Data for a single bug given by the id /// /// /// /// private BuggViewModel GetResult(int id) { var model = new BuggViewModel(); using (var unitOfWork = new UnitOfWork(new CrashReportEntities())) { // Create a new view and populate with crashes List crashes = null; model.Bugg = unitOfWork.BuggRepository.GetById(id); crashes = model.Bugg.Crashes.OrderByDescending(data => data.TimeOfCrash).ToList(); model.Bugg.CrashesInTimeFrameAll = crashes.Count; model.Bugg.CrashesInTimeFrameGroup = crashes.Count; model.Bugg.NumberOfCrashes = crashes.Count; model.Crashes = crashes; } return model; } /// /// Filter a set of Buggs by a jira ticket. /// /// The unfiltered set of Buggs /// The ticket by which to filter the list of buggs /// private IQueryable FilterByJira(IQueryable results, string jira) { return results.Where(bugg => bugg.TTPID == jira); } /// /// Filter a set of Buggs to a date range. /// /// The unfiltered set of Buggs. /// The earliest date to filter by. /// The latest date to filter by. /// The set of Buggs between the earliest and latest date. private IQueryable FilterByDate(IQueryable results, DateTime dateFrom, DateTime dateTo) { dateTo = dateTo.AddDays(1); var buggsInTimeFrame = results.Where(bugg => (bugg.TimeOfFirstCrash >= dateFrom && bugg.TimeOfFirstCrash <= dateTo) || (bugg.TimeOfLastCrash >= dateFrom && bugg.TimeOfLastCrash <= dateTo) || (bugg.TimeOfFirstCrash <= dateFrom && bugg.TimeOfLastCrash >= dateTo)); return buggsInTimeFrame; } /// /// Filter a set of Buggs by build version. /// /// The unfiltered set of Buggs. /// The build version to filter by. /// The set of Buggs that matches specified build version private IQueryable FilterByBuildVersion(IQueryable results, string buildVersion) { if (!string.IsNullOrEmpty(buildVersion)) { results = results.Where(data => data.BuildVersion.Contains(buildVersion)); } return results; } /// /// Filter a set of Buggs by user group name. /// /// The unfiltered set of Buggs. /// The user group name to filter by. /// The set of Buggs by users in the requested user group. private IQueryable FilterByUserGroup(IQueryable setOfBuggs, string groupName) { return setOfBuggs.Where(data => data.UserGroups.Any(ug => ug.Name == groupName)); } /// /// Sort the container of Buggs by the requested criteria. /// /// A container of unsorted Buggs. /// The term to sort by. /// Whether to sort by descending or ascending. /// The date of the earliest Bugg to examine. /// The date of the most recent Bugg to examine. /// The user group name to filter by. /// A sorted container of Buggs. private IOrderedQueryable GetSortedResults(IQueryable results, string sortTerm, bool bSortDescending, DateTime dateFrom, DateTime dateTo, string groupName) { IOrderedQueryable orderedResults = null; try { switch (sortTerm) { case "Id": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Id, bSortDescending); break; case "BuildVersion": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.BuildVersion, bSortDescending); break; case "LatestCrash": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.TimeOfLastCrash, bSortDescending); break; case "FirstCrash": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.TimeOfFirstCrash, bSortDescending); break; case "NumberOfCrashes": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.NumberOfCrashes, bSortDescending); break; case "NumberOfUsers": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.NumberOfUniqueMachines, bSortDescending); break; case "Pattern": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Pattern, bSortDescending); break; case "CrashType": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.CrashType, bSortDescending); break; case "Status": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Status, bSortDescending); break; case "FixedChangeList": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.FixedChangeList, bSortDescending); break; case "TTPID": orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.TTPID, bSortDescending); break; default: orderedResults = EnumerableOrderBy(results, buggCrashInstance => buggCrashInstance.Id, bSortDescending); break; } return orderedResults; } catch (Exception ex) { Debug.WriteLine("Exception in GetSortedResults: " + ex.ToString()); } return orderedResults; } /// /// /// /// /// /// /// /// private IOrderedQueryable EnumerableOrderBy(IQueryable query, Expression> predicate, bool bDescending) { return bDescending ? query.OrderByDescending(predicate) : query.OrderBy(predicate); } /// Dispose the controller - safely getting rid of database connections /// protected override void Dispose(bool disposing) { base.Dispose(disposing); } #endregion } }