diff --git a/.gitattributes b/.gitattributes index 9cf0e89d..1ff0c423 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,34 +1,63 @@ -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto -*.jpg binary -*.png binary -*.gif binary +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp -*.cs text=auto diff=csharp eol=crlf -*.vb text=auto eol=crlf -*.resx text=auto -*.html text=auto -*.htm text=auto -*.css text=auto -*.scss text=auto -*.sass text=auto -*.less text=auto -*.js text=auto -*.sql text=auto eol=crlf +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary -*.csproj text=auto eol=crlf -*.vbproj text=auto eol=crlf -*.fsproj text=auto eol=crlf -*.dbproj text=auto eol=crlf -*.sln text=auto eol=crlf +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary -*.sh eol=lf +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index ba6c5e2e..d5996225 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ _ReSharper*/ src/Tools/MarkdownToNamespaceDoc/packages/*/ src/Tools/TsGenerator/.vs/* src/Help/* +**/.vs/* src/Server/Coderr.Server.Web.Tests/applicationhost.config /src/Server/Coderr.Server.Web/node_modules/* -/src/Server/Coderr.Server.Web/wwwroot/dist** +/src/Server/Coderr.Server.Web/wwwroot/dist/** +/src/Server/Coderr.Server.Web/npm-shrinkwrap.json +/src/Server/node_modules/** diff --git a/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs b/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs index 002d1c6b..ea3b2486 100644 --- a/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs +++ b/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs @@ -3,6 +3,9 @@ namespace Coderr.Server.Abstractions.Boot { + /// + /// Abstraction for the .NET Core configuration files. + /// public interface IConfigurationSection { string this[string name] { get; } diff --git a/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj b/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj index 565350a1..3001061f 100644 --- a/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj +++ b/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/HighlightedContextDataProviderContext.cs b/src/Server/Coderr.Server.Abstractions/Incidents/HighlightedContextDataProviderContext.cs new file mode 100644 index 00000000..cc43999d --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/HighlightedContextDataProviderContext.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + public class HighlightedContextDataProviderContext + { + private readonly IList _items; + + public HighlightedContextDataProviderContext(IList items) + { + _items = items ?? throw new ArgumentNullException(nameof(items)); + Tags = new string[0]; + } + + public int ApplicationId { get; set; } + public string Description { get; set; } + + /// + /// Namespace + name of exception + /// + public string FullName { get; set; } + + public int IncidentId { get; set; } + + public IEnumerable Items => _items; + + public string StackTrace { get; set; } + public string[] Tags { get; set; } + + public void AddValue(HighlightedContextData contextData) + { + if (contextData == null) throw new ArgumentNullException(nameof(contextData)); + _items.Add(contextData); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs b/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs index 8adb3f07..d7e7ee47 100644 --- a/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs +++ b/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs @@ -6,6 +6,6 @@ namespace Coderr.Server.Abstractions.Incidents { public interface IHighlightedContextDataProvider { - Task CollectAsync(int incidentId, ICollection data); + Task CollectAsync(HighlightedContextDataProviderContext context); } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs b/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs index 4630d971..00e2e0f6 100644 --- a/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs +++ b/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs @@ -4,8 +4,11 @@ namespace Coderr.Server.Abstractions.Incidents { + /// + /// Checks if there is a solution available for the current incident. + /// public interface ISolutionProvider { - Task SuggestSolutionAsync(int incidentId, ICollection suggestedSolutions); + Task SuggestSolutionAsync(SolutionProviderContext context); } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/SolutionProviderContext.cs b/src/Server/Coderr.Server.Abstractions/Incidents/SolutionProviderContext.cs new file mode 100644 index 00000000..f69a78df --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/SolutionProviderContext.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + public class SolutionProviderContext + { + private readonly List _possibleSolutions; + + public SolutionProviderContext(List possibleSolutions) + { + _possibleSolutions = possibleSolutions; + } + + public int ApplicationId { get; set; } + public string Description { get; set; } + + /// + /// Namespace + name of exception + /// + public string FullName { get; set; } + + public int IncidentId { get; set; } + + public string StackTrace { get; set; } + public string[] Tags { get; set; } + + public void AddSuggestion(string suggestion, string motivation) + { + if (suggestion == null) throw new ArgumentNullException(nameof(suggestion)); + if (motivation == null) throw new ArgumentNullException(nameof(motivation)); + _possibleSolutions.Add(new SuggestedIncidentSolution {Reason = motivation, SuggestedSolution = suggestion}); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs b/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs index 8e55df14..e2778da1 100644 --- a/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs +++ b/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs @@ -149,7 +149,7 @@ public static string ToFriendlyString(this IPrincipal principal) return "Anonymous"; } - string str = cc.Identity.Name + " ["; + string str = cc.Identity.Name + " Claims["; foreach (var claim in cc.Claims) { var pos = claim.Type.LastIndexOf('/'); diff --git a/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs b/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs index b69ca4b1..5194afe3 100644 --- a/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs +++ b/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs @@ -4,8 +4,8 @@ namespace Coderr.Server.Abstractions.Security { public class CoderrClaims { - public const string Application = "http://coderrapp.com/claims/application"; - public const string ApplicationAdmin = "http://coderrapp.com/claims/application/admin"; + public const string Application = "http://coderr/claims/application"; + public const string ApplicationAdmin = "http://coderr/claims/application/admin"; public static readonly ClaimsPrincipal SystemPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[] { diff --git a/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj b/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj index 30624038..3ad022de 100644 --- a/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj +++ b/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj @@ -5,11 +5,14 @@ Coderr.Server.Api.Client.Tests - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj b/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj index f460fa1b..5f32f6f7 100644 --- a/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj +++ b/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj b/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj index e80d8e6c..9db506b3 100644 --- a/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj +++ b/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj @@ -32,6 +32,6 @@ - + diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs index 984cb392..540aa4f0 100644 --- a/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs @@ -4,6 +4,7 @@ namespace Coderr.Server.Api.Core.Accounts.Queries { + [Message] public class ListAccounts : Query { } diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs index 0ce67ebc..908e9a31 100644 --- a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs @@ -7,7 +7,6 @@ namespace Coderr.Server.Api.Core.Incidents.Queries /// public class FindIncidentsResultItem { - private string _name; /// /// Creates new instance of . @@ -68,16 +67,7 @@ protected FindIncidentsResultItem() /// /// Incident name /// - public string Name - { - set - { - if (value.Length > 40) - value = value.Substring(0, 35) + "[...]"; - _name = value; - } - get => _name; - } + public string Name { get; set; } /// /// Total number of received reports (increased even if the number of stored reports are at the limit) diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollection.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollection.cs new file mode 100644 index 00000000..d228a3db --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollection.cs @@ -0,0 +1,37 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Fetch a specific collection from all reports, sorted in descending order. + /// + public class GetCollection : Query + { + public GetCollection(int incidentId, string collectionName) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + IncidentId = incidentId; + CollectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); + } + + protected GetCollection() + { + } + + /// + /// Collection name like "ErrorProperties" or "HttpRequest". + /// + public string CollectionName { get; } + + /// + /// Incident that the collection belongs to. + /// + public int IncidentId { get; } + + /// + /// Collection limit. + /// + public int MaxNumberOfCollections { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResult.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResult.cs new file mode 100644 index 00000000..01ecb34b --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResult.cs @@ -0,0 +1,13 @@ +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Result for . + /// + public class GetCollectionResult + { + /// + /// Fetched collections. + /// + public GetCollectionResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResultItem.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResultItem.cs new file mode 100644 index 00000000..6952e2f5 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResultItem.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// A collection + /// + public class GetCollectionResultItem + { + /// + /// Properties in the collection (for instance "Url" if this is the HTTP request). + /// + public Dictionary Properties { get; set; } + + /// + /// Date for the report that this collection is for. + /// + public DateTime ReportDate { get; set; } + + /// + /// Id of the report that this collection was received in. + /// + public int ReportId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs index 735aa8bf..e5ce4864 100644 --- a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs @@ -26,7 +26,7 @@ public class HighlightedContextData public string Url { get; set; } /// - /// Value to show + /// Value to show (one per report or only from the latest report). /// /// /// Values should be sorted i priority order (first item will be displayed directly) diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs index 41563aec..4b2f60a8 100644 --- a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs @@ -20,10 +20,4 @@ public class GetVersionHistoryResult /// public GetVersionHistoryResultSet[] ReportCounts { get; set; } } - - public class GetVersionHistoryResultSet - { - public string Name { get; set; } - public int[] Values { get; set; } - } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultSet.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultSet.cs new file mode 100644 index 00000000..c6070030 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultSet.cs @@ -0,0 +1,8 @@ +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + public class GetVersionHistoryResultSet + { + public string Name { get; set; } + public int[] Values { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj b/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj index 01ac790b..b4e5b83b 100644 --- a/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj +++ b/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj @@ -5,11 +5,14 @@ Coderr.Server.App.Tests - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/Server/Coderr.Server.App/Coderr.Server.App.csproj b/src/Server/Coderr.Server.App/Coderr.Server.App.csproj index 1ac7dbe3..a8c117f6 100644 --- a/src/Server/Coderr.Server.App/Coderr.Server.App.csproj +++ b/src/Server/Coderr.Server.App/Coderr.Server.App.csproj @@ -22,12 +22,12 @@ NU1701 - + - - - - + + + + diff --git a/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs b/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs index bf986609..6660d9a9 100644 --- a/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs +++ b/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs @@ -249,7 +249,7 @@ private async Task CreateIdentity(int accountId, string userName if (isSysAdmin || accountId == 1) claims.Add(new Claim(ClaimTypes.Role, CoderrRoles.SysAdmin)); - return new ClaimsIdentity(claims.ToArray()); + return new ClaimsIdentity(claims.ToArray(), AuthenticationTypes.Default); } } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs new file mode 100644 index 00000000..f3df184c --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Requests; +using Coderr.Server.App.Core.Accounts; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Invitations.CommandHandlers +{ + public class AcceptInvitationHandler : IMessageHandler + { + private readonly IAccountService _accountService; + + public AcceptInvitationHandler(IAccountService accountService) + { + _accountService = accountService; + } + + public async Task HandleAsync(IMessageContext context, AcceptInvitation message) + { + await _accountService.AcceptInvitation(context.Principal, message); + } + } +} diff --git a/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs index 322913f1..c12f7353 100644 --- a/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs +++ b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs @@ -135,17 +135,18 @@ protected virtual async Task SendInvitationEmailAsync(IMessageContext context, I var msg = new EmailMessage { Subject = "You have been invited by " + invitation.InvitedBy + " to Coderr.", - TextBody = string.Format(@"Hello, + TextBody = $@"Hello, -{0} has invited to you join their team at Coderr, a service used to keep track of exceptions in .NET applications. +{invitation.InvitedBy} has invited to you join their team at Coderr, a service used to keep track of exceptions in .NET applications. Click on the following link to accept the invitation: -{2}/account/accept/{1} +{url}/invitation/accept/{invitation.InvitationKey} + +{reason} -{3} Best regards, The Coderr team -", invitation.InvitedBy, invitation.InvitationKey, url, reason), +", Recipients = new[] {new EmailAddress(invitation.EmailToInvitedUser)} }; diff --git a/src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs index ee0e86d7..a6766257 100644 --- a/src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs @@ -54,10 +54,10 @@ IDictionary IConfigurationSection.ToDictionary() void IConfigurationSection.Load(IDictionary items) { - AccountName = items.GetString("AccountName"); + AccountName = items.GetString("AccountName", ""); AccountPassword = items.GetString("AccountPassword", ""); - SmtpHost = items.GetString("SmtpHost"); - PortNumber = items.GetInteger("PortNumber", 0); + SmtpHost = items.GetString("SmtpHost", ""); + PortNumber = items.GetInteger("PortNumber", null); UseSsl = items.GetBoolean("UseSSL", false); } } diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs index 8a3a97c5..a2f20b54 100644 --- a/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs +++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs @@ -10,5 +10,6 @@ public class ApplicationUsageStatisticsDto public int IgnoredCount { get; set; } public decimal? NumberOfDevelopers { get; set; } public int? EstimatedNumberOfErrors { get; set; } + public bool IsEmpty => ReportCount == 0 && ClosedCount == 0; } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs index b2b27098..2c7229bf 100644 --- a/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs +++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs @@ -236,7 +236,8 @@ private async Task ReportMonth(DateTime lastMonth) _config.Save(); } - if (apps.Count == 0) + var allIsEmpty = apps.All(x => x.Value.IsEmpty); + if (allIsEmpty) return false; var dto = new UsageStatisticsDto diff --git a/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj b/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj index f965421d..595f9ce1 100644 --- a/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj +++ b/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj @@ -5,6 +5,6 @@ - + diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs index 80afa6a1..1bc139ce 100644 --- a/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs +++ b/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs @@ -9,8 +9,6 @@ namespace Coderr.Server.Domain.Core.Incidents /// public interface IIncidentRepository { - Task> GetAllAsync(IEnumerable incidentIds); - /// /// Get incident /// @@ -20,6 +18,13 @@ public interface IIncidentRepository /// No incident was found with the given key. Task GetAsync(int id); + /// + /// Get specified incidents + /// + /// ids to fecth + /// All specified (or an exception will be thrown if any of them are missing) + Task> GetManyAsync(IEnumerable incidentIds); + /// /// Count the number of incidents for the given application /// diff --git a/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj b/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj index a2778230..c9ae4886 100644 --- a/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj +++ b/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj @@ -6,9 +6,9 @@ - + - + @@ -20,7 +20,7 @@ - 2.0.0 + 2.2.0 diff --git a/src/Server/Coderr.Server.Infrastructure/Security/Class1.cs b/src/Server/Coderr.Server.Infrastructure/Security/Class1.cs new file mode 100644 index 00000000..0a93ed76 --- /dev/null +++ b/src/Server/Coderr.Server.Infrastructure/Security/Class1.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Infrastructure.Security +{ + public static class AuthenticationTypes + { + public const string Default = "ApplicationCookie"; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj index 89d59a26..f5a8b9d9 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/InvalidReport.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/InvalidReport.cs similarity index 95% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/InvalidReport.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/InvalidReport.cs index 9ba6534f..309d4102 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/InvalidReport.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/InvalidReport.cs @@ -1,6 +1,6 @@ using System; -namespace Coderr.Server.ReportAnalyzer.Inbound.Commands +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands { /// /// Failed to identify an incoming report. Stored for debugging purposes. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/NamespaceDoc.cs similarity index 85% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/NamespaceDoc.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/NamespaceDoc.cs index 0c4dd561..1aeeeab9 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/NamespaceDoc.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/NamespaceDoc.cs @@ -1,4 +1,4 @@ -namespace Coderr.Server.ReportAnalyzer.Inbound.Commands +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands { /// /// These classes are created by the "Receiver" area when new reports are being received. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessFeedback.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessFeedback.cs similarity index 96% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessFeedback.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessFeedback.cs index 9e6fa369..dda2d037 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessFeedback.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessFeedback.cs @@ -1,6 +1,6 @@ using System; -namespace Coderr.Server.ReportAnalyzer.Inbound.Commands +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands { /// /// Feedback item as received by the client library diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReport.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReport.cs similarity index 95% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReport.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReport.cs index ed25f000..604310eb 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReport.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReport.cs @@ -1,6 +1,6 @@ using System; -namespace Coderr.Server.ReportAnalyzer.Inbound.Commands +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands { /// /// Command diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReportContextInfoDto.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportContextInfoDto.cs similarity index 96% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReportContextInfoDto.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportContextInfoDto.cs index 7557dc61..0d25c6dc 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReportContextInfoDto.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportContextInfoDto.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Coderr.Server.ReportAnalyzer.Inbound.Commands +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands { /// /// Context collection diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReportExceptionDto.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportExceptionDto.cs similarity index 96% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReportExceptionDto.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportExceptionDto.cs index 45ab867c..35db5355 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ProcessReportExceptionDto.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportExceptionDto.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Coderr.Server.ReportAnalyzer.Inbound.Commands +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands { /// /// Model used to wrap all information from an exception. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ReadMe.md b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ReadMe.md similarity index 97% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ReadMe.md rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ReadMe.md index c7ad8fd2..4a563cb5 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Commands/ReadMe.md +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ReadMe.md @@ -1 +1 @@ -Commands used to trigger the processing. +Commands used to trigger the processing. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NamespaceDoc.cs similarity index 90% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NamespaceDoc.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NamespaceDoc.cs index f8f2ac89..d843276d 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NamespaceDoc.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NamespaceDoc.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace Coderr.Server.ReportAnalyzer.Inbound.Models +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models { /// /// These classes is an exact match of the client library DTOs. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportContextInfo.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportContextInfo.cs similarity index 91% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportContextInfo.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportContextInfo.cs index 31468132..49f30027 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportContextInfo.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportContextInfo.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Coderr.Server.ReportAnalyzer.Inbound.Models +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models { public class NewReportContextInfo { diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportDTO.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportDTO.cs similarity index 95% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportDTO.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportDTO.cs index 5b4cb178..fdcd8e5a 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportDTO.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportDTO.cs @@ -1,6 +1,6 @@ using System; -namespace Coderr.Server.ReportAnalyzer.Inbound.Models +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models { /// /// Report as uploaded by the client API. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportException.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportException.cs similarity index 96% rename from src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportException.cs rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportException.cs index 25139020..defa2ad1 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Models/NewReportException.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportException.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Coderr.Server.ReportAnalyzer.Inbound.Models +namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models { /// /// Model used to wrap all information from an exception. diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj b/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj index db0152d9..32486e20 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj +++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj @@ -6,11 +6,14 @@ - - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj b/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj index e1380ca4..436dc6fd 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj +++ b/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj @@ -6,17 +6,17 @@ $(DefaultItemExcludes);**\*.DotSettings; - + - + - - - - - + + + + + diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/DuplicateChecker.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/DuplicateChecker.cs new file mode 100644 index 00000000..0c49d649 --- /dev/null +++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/DuplicateChecker.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models; + +namespace Coderr.Server.ReportAnalyzer.Inbound +{ + public class DuplicateChecker + { + private Dictionary _lastReportIndex = new Dictionary(); + + public DuplicateChecker() + { + } + + public bool IsDuplicate(string remoteAddress, NewReportDTO report) + { + lock (_lastReportIndex) + { + // duplicate + if (_lastReportIndex.TryGetValue(report.ReportId, out var when)) + { + _lastReportIndex[report.ReportId] = DateTime.UtcNow; + return true; + } + + if (_lastReportIndex.Count >= 100) + { + var idsToRemove = _lastReportIndex.OrderBy(x => x.Value).Take(10); + foreach (var id in idsToRemove) + { + _lastReportIndex.Remove(id.Key); + } + + _lastReportIndex[report.ReportId] = DateTime.UtcNow; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs index f822256a..2f472879 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; using Coderr.Server.Api.Core.Feedback.Commands; -using Coderr.Server.ReportAnalyzer.Inbound.Commands; using DotNetCqs; using Coderr.Server.Abstractions.Boot; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands; using log4net; using Newtonsoft.Json; diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs index 73ce38d9..675b8f09 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs @@ -2,9 +2,9 @@ using System.Linq; using System.Threading.Tasks; using Coderr.Server.Domain.Core.ErrorReports; -using Coderr.Server.ReportAnalyzer.Inbound.Commands; using DotNetCqs; using Coderr.Server.Abstractions.Boot; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands; using log4net; namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs index 0f5688a0..99508c7d 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs @@ -1,6 +1,6 @@ using System.Linq; using Coderr.Server.Domain.Core.ErrorReports; -using Coderr.Server.ReportAnalyzer.Inbound.Commands; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands; using Newtonsoft.Json; namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers.Reports diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs index b0363c1e..fa43640e 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs @@ -7,8 +7,8 @@ using System.Text; using System.Threading.Tasks; using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports; -using Coderr.Server.ReportAnalyzer.Inbound.Commands; -using Coderr.Server.ReportAnalyzer.Inbound.Models; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models; using DotNetCqs; using DotNetCqs.Queues; using Griffin.Data; @@ -27,6 +27,7 @@ public class SaveReportHandler private readonly IMessageQueue _queue; private readonly IAdoNetUnitOfWork _unitOfWork; private readonly int _maxSizeForJsonErrorReport; + private static readonly DuplicateChecker DuplicateChecker = new DuplicateChecker(); /// /// Creates a new instance of . @@ -59,8 +60,8 @@ public async Task BuildReportAsync(ClaimsPrincipal user, string appKey, string s var application = await GetAppAsync(appKey); if (application == null) { - _logger.Warn("Unknown appKey: " + appKey + " from " + remoteAddress); - throw new InvalidCredentialException("AppKey was not found in the database. Key '" + appKey + "'."); + _logger.Warn($"Unknown appKey: {appKey} from {remoteAddress}"); + throw new InvalidCredentialException($"AppKey was not found in the database. Key '{appKey}'."); } if (!ReportValidator.ValidateBody(application.SharedSecret, signatureProvidedByTheClient, reportBody)) @@ -74,6 +75,12 @@ public async Task BuildReportAsync(ClaimsPrincipal user, string appKey, string s if (report == null) return; + if (DuplicateChecker.IsDuplicate(remoteAddress, report)) + { + _logger.Debug($"Duplicate report {report.ReportId} from {remoteAddress}"); + return; + } + // correct incorrect clients if (report.CreatedAtUtc > DateTime.UtcNow) report.CreatedAtUtc = DateTime.UtcNow; diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs index d9ccced3..a4ac5df8 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs @@ -126,12 +126,12 @@ public string Description /// /// Incident have been solved (bug as been identified and corrected) /// - public bool IsClosed => State == IncidentState.Closed; + public bool IsClosed => State == AnalyzedIncidentState.Closed; /// /// Incident is ignored, i.e. do not track any more reports or send any notifications. /// - public bool IsIgnored => State == IncidentState.Ignored; + public bool IsIgnored => State == AnalyzedIncidentState.Ignored; /// /// Incident is opened again after being closed. @@ -172,7 +172,7 @@ public string Description /// public string StackTrace { get; set; } - public IncidentState State { get; private set; } + public AnalyzedIncidentState State { get; private set; } /// @@ -222,7 +222,7 @@ public bool IsReportIgnored(string applicationVersion) public void ReOpen() { PreviousSolutionAtUtc = SolvedAtUtc; - State = IncidentState.New; + State = AnalyzedIncidentState.New; ReOpenedAtUtc = DateTime.UtcNow; IsReOpened = true; } diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs index 1d6839ec..6d853f46 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs @@ -3,7 +3,7 @@ /// /// Current state of an incident /// - public enum IncidentState + public enum AnalyzedIncidentState { /// /// Incident have arrived but have not yet been categorized. diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs index 3bd97829..99ece746 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs @@ -23,9 +23,9 @@ public object Adapt(ValueAdapterContext context, object currentValue) var uaParser = Parser.GetDefault(); var c = uaParser.Parse(context.Value.ToString()); - context.AddCustomField("UserAgent.Family", c.UserAgent.Family); + context.AddCustomField("UserAgent.Family", c.UA.Family); context.AddCustomField("UserAgent.Version", - string.Format("{0} v{1}.{2}", c.UserAgent.Family, c.UserAgent.Major, c.UserAgent.Minor)); + string.Format("{0} v{1}.{2}", c.UA.Family, c.UA.Major, c.UA.Minor)); context.AddCustomField("UserInfo", "DeviceType", c.Device.Family); diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs index 74080f22..71469b8e 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs @@ -40,53 +40,55 @@ public UpdateSimilaritiesFromNewReport(ISimilarityRepository similarityRepositor /// public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e) { - _logger.Debug("Updating similarities"); + _logger.Debug("Updating similarities " + e.Incident.Id); var adapters = _adapterRepository.GetAdapters(); var sw2 = new Stopwatch(); sw2.Start(); - long step1, step2, step3; + long beginStep, afterFindStep, afterReposStep; try { _logger.Debug("Finding for incident: " + e.Incident.Id); - var similaritiesReport = _similarityRepository.FindForIncident(e.Incident.Id); + beginStep = sw2.ElapsedMilliseconds; - step1 = sw2.ElapsedMilliseconds; + var similaritiesReport = _similarityRepository.FindForIncident(e.Incident.Id); var isNew = false; if (similaritiesReport == null) { - _logger.Debug("Not found, creating a new"); similaritiesReport = new SimilaritiesReport(e.Incident.Id); isNew = true; } var analyzer = new SimilarityAnalyzer(similaritiesReport); - step2 = sw2.ElapsedMilliseconds; + afterFindStep = sw2.ElapsedMilliseconds; analyzer.AddReport(e.Report, adapters); if (isNew) { - _logger.Debug("Creating..."); + _logger.Debug("Creating new similarity report..."); await _similarityRepository.CreateAsync(similaritiesReport); } else { - _logger.Debug("Updating..."); + _logger.Debug("Updating existing similarity report..."); await _similarityRepository.UpdateAsync(similaritiesReport); } - step3 = sw2.ElapsedMilliseconds; + afterReposStep = sw2.ElapsedMilliseconds; _logger.Debug("similarities done "); } catch (Exception exception) { _logger.Error("failed to add report to incident " + e.Incident.Id, exception); + + // Live changes since we get deadlocks? + // TODO: WHY do we get deadlocks, aren't we the only ones reading from the similarity tables? return; } sw2.Stop(); if (sw2.ElapsedMilliseconds > 200) { - _logger.InfoFormat("Slow similarity handling, times: {0}/{1}/{2}/{3}", step1, step2, step3, + _logger.InfoFormat("Slow similarity handling, times: {0}/{1}/{2}/{3}", beginStep, afterFindStep, afterReposStep, sw2.ElapsedMilliseconds); } } diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs index 4a6909c2..baa3ddaf 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs @@ -49,7 +49,7 @@ public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e) ExtractTagsFromCollections(e, ctx); - _logger.DebugFormat("Done, identified {0} new tags", string.Join(", ", ctx.NewTags)); + _logger.DebugFormat("Done, identified {0} new tags", string.Join(",", ctx.NewTags)); if (ctx.NewTags.Count == 0) return; @@ -70,6 +70,7 @@ private void ExtractTagsFromCollections(ReportAddedToIncident e, TagIdentifierCo var tags = tagsStr.Split(','); foreach (var tag in tags) { + _logger.Debug($"Adding tag '{tag}' to incident {e.Incident.Id}"); ctx.AddTag(tag.Trim(), 1); } } diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs index 00e9bb76..1fad38ac 100644 --- a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs +++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs @@ -16,6 +16,10 @@ public class AspNetMvcAndWebApiIdentifier : ITagIdentifier public void Identify(TagIdentifierContext context) { if (context == null) throw new ArgumentNullException("context"); + + + context.AddIfFound("System.Web", "http"); + var orderNumber = context.AddIfFound("System.Web.Mvc", "asp.net-mvc"); if (orderNumber != -1) { diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj b/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj index 43a28085..d1b089a8 100644 --- a/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj +++ b/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj @@ -12,14 +12,17 @@ - + - - - - + + + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj b/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj index 851013e8..96d21357 100644 --- a/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj +++ b/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj @@ -6,10 +6,10 @@ - + - - + + diff --git a/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/ListAccountsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/ListAccountsHandler.cs new file mode 100644 index 00000000..86b1f585 --- /dev/null +++ b/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/ListAccountsHandler.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Queries; +using DotNetCqs; +using Griffin.Data; +using Griffin.Data.Mapper; + +namespace Coderr.Server.SqlServer.Core.Accounts.QueryHandlers +{ + public class ListAccountsHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + private static readonly IEntityMapper _mapper = new MirrorMapper(); + + public ListAccountsHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, ListAccounts query) + { + var sql = "SELECT Id AccountId, UserName, CreatedAtUtc, Email FROM Accounts"; + var users = await _unitOfWork.ToListAsync(_mapper, sql); + return new ListAccountsResult() { Accounts = users.ToArray() }; + } + } +} diff --git a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs index 9164369a..fa3914e9 100644 --- a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs +++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs @@ -5,9 +5,7 @@ using Coderr.Server.Abstractions.Boot; using Coderr.Server.Abstractions.Security; using Coderr.Server.App.Core.ApiKeys; -using Coderr.Server.Infrastructure.Security; using Coderr.Server.SqlServer.Core.ApiKeys.Mappings; -using Coderr.Server.ReportAnalyzer.Abstractions; using Griffin.Data; using Griffin.Data.Mapper; @@ -41,7 +39,7 @@ public ApiKeyRepository(IAdoNetUnitOfWork uow) public Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId) { _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId AND ApplicationId = @appId", - new {appId = applicationId, keyId = apiKeyId}); + new { appId = applicationId, keyId = apiKeyId }); return Task.FromResult(null); } @@ -52,8 +50,8 @@ public Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId) /// public Task DeleteAsync(int keyId) { - _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId", new {keyId}); - _uow.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @keyId", new {keyId}); + _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId", new { keyId }); + _uow.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @keyId", new { keyId }); return Task.FromResult(null); } @@ -84,7 +82,7 @@ public async Task GetByKeyAsync(string apiKey) key.Add(app); } - + return key; } @@ -174,7 +172,7 @@ await _uow.ToListAsync("SELECT ApplicationId FROM ApiKeyApplications WHERE foreach (var applicationId in removed) { _uow.Execute("DELETE FROM ApiKeyApplications WHERE ApiKeyId = @1 AND ApplicationId = @2", - new[] {key.Id, applicationId}); + new[] { key.Id, applicationId }); } var added = apps.Except(existingMappings); diff --git a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/MirrorMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/MirrorMapper.cs deleted file mode 100644 index 1c6b8a4b..00000000 --- a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/MirrorMapper.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Data; -using System.Reflection; -using Griffin.Data.Mapper; - -namespace Coderr.Server.SqlServer.Core.ApiKeys.Mappings -{ - public class MirrorMapper : IEntityMapper where T : new() - { - private MethodInfo[] _setters; - - public void Map(IDataRecord source, T destination) - { - Map(source, (object) destination); - } - - public object Create(IDataRecord record) - { - return new T(); - } - - public void Map(IDataRecord source, object destination) - { - if (_setters == null) - _setters = MapPropertySetters(source, typeof(T)); - - for (var i = 0; i < _setters.Length; i++) - { - var value = source[i]; - if (value is DBNull) - continue; - - _setters[i].Invoke(destination, new[] {value}); - } - } - - private MethodInfo[] MapPropertySetters(IDataRecord source, Type type) - { - var fields = new MethodInfo[source.FieldCount]; - for (var i = 0; i < source.FieldCount; i++) - { - var name = source.GetName(i); - fields[i] = - type.GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) - .SetMethod; - } - return fields; - } - } -} \ No newline at end of file diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs index 39d3e38b..9ef4a5fc 100644 --- a/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs +++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs @@ -69,7 +69,7 @@ public async Task GetTotalCountForAppInfoAsync(int applicationId) } } - public Task> GetAllAsync(IEnumerable incidentIds) + public Task> GetManyAsync(IEnumerable incidentIds) { if (incidentIds == null) throw new ArgumentNullException(nameof(incidentIds)); var ids = string.Join(",", incidentIds); diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/Class1.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/Class1.cs new file mode 100644 index 00000000..64e33b07 --- /dev/null +++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/Class1.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Queries; +using DotNetCqs; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.SqlServer.Core.Incidents.Queries +{ + internal class GetCollectionHandler : IQueryHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + private ILog _logger = LogManager.GetLogger(typeof(GetCollectionHandler)); + + public GetCollectionHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetCollection query) + { + if (query.MaxNumberOfCollections == 0) + query.MaxNumberOfCollections = 1; + + var sql = @"WITH ReportsWithCollection (ErrorReportId) + AS + ( + select distinct TOP(10) ErrorReports.Id + FROM ErrorReports + JOIN ErrorReportCollectionProperties ep ON (ep.ReportId = ErrorReports.Id) + WHERE ep.Name = @collectionName + AND ErrorReports.IncidentId=@incidentId + ) + + select erp.PropertyName, erp.Value, ErrorReports.CreatedAtUtc, ErrorReports.Id ReportId + from ErrorReportCollectionProperties erp + join ReportsWithCollection rc on (erp.ReportId = rc.ErrorReportId) + join ErrorReports on (ErrorReports.ID = rc.ErrorReportId) + WHERE erp.Name = @collectionName"; + + var items = new List(); + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = sql; + cmd.AddParameter("incidentId", query.IncidentId); + cmd.AddParameter("collectionName", query.CollectionName); + using (var reader = await cmd.ExecuteReaderAsync()) + { + GetCollectionResultItem item = null; + var lastReportId = 0; + while (reader.Read()) + { + var reportId = (int)reader["ReportId"]; + if (reportId != lastReportId || item == null) + { + item = new GetCollectionResultItem + { + ReportId = (int)reader["ReportId"], + ReportDate = (DateTime)reader["CreatedAtUtc"], + Properties = new Dictionary() + }; + items.Add(item); + } + + lastReportId = reportId; + var key = (string)reader["PropertyName"]; + var value = (string)reader["Value"]; + if (item.Properties.ContainsKey(key)) + { + _logger.Info( + $"Report {reportId} have value for key {key} current: {item.Properties[key]} new: {value}."); + } + else + item.Properties.Add(key, value); + } + } + } + + return new GetCollectionResult + { + Items = items.ToArray() + }; + } + } +} diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs index 08669eb1..641c4fc4 100644 --- a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs +++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs @@ -23,6 +23,7 @@ public void Map(IDataRecord source, FindIncidentsResultItem destination) destination.ApplicationId = source["ApplicationId"].ToString(); destination.IsReOpened = source["IsReopened"].Equals(1); destination.ReportCount = (int) source["ReportCount"]; + destination.CreatedAtUtc = (DateTime)source["CreatedAtUtc"]; var value = source["UpdatedAtUtc"]; if (!(value is DBNull)) @@ -30,8 +31,6 @@ public void Map(IDataRecord source, FindIncidentsResultItem destination) value = source["LastReportAtUtc"]; destination.LastReportReceivedAtUtc = (DateTime) (value is DBNull ? destination.LastUpdateAtUtc : value); - - destination.CreatedAtUtc = (DateTime) source["CreatedAtUtc"]; } } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs index 0f65c14a..53c408b5 100644 --- a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs +++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs @@ -5,7 +5,6 @@ using Coderr.Server.Abstractions.Incidents; using Coderr.Server.Api.Core.Incidents.Queries; using DotNetCqs; -using Coderr.Server.ReportAnalyzer.Abstractions; using Griffin.Data; using Griffin.Data.Mapper; using log4net; @@ -37,8 +36,8 @@ public async Task HandleAsync(IMessageContext context, GetInc _logger.Info("GetIncident step 1"); var sql = "SELECT Incidents.*, Users.Username as AssignedTo " + - " FROM Incidents WITH (ReadPast)" + - " LEFT JOIN Users WITH (ReadPast) ON (AssignedToId = Users.AccountId) " + + " FROM Incidents WITH(READUNCOMMITTED)" + + " LEFT JOIN Users WITH(READUNCOMMITTED) ON (AssignedToId = Users.AccountId) " + " WHERE Incidents.Id = @id"; var result = await _unitOfWork.FirstAsync(sql, new { Id = query.IncidentId }); @@ -87,8 +86,27 @@ public async Task HandleAsync(IMessageContext context, GetInc await GetStatSummary(query, facts); _logger.Info("GetIncident step 7"); - var contextData = new List(); var solutions = new List(); + var suggestedSolutionContext = new SolutionProviderContext(solutions) + { + ApplicationId = result.ApplicationId, + Description = result.Description, + FullName = result.FullName, + IncidentId = result.Id, + StackTrace = result.StackTrace, + Tags = result.Tags + }; + + var contextData = new List(); + var highlightedContext = new HighlightedContextDataProviderContext(contextData) + { + ApplicationId = result.ApplicationId, + Description = result.Description, + FullName = result.FullName, + IncidentId = result.Id, + StackTrace = result.StackTrace, + Tags = result.Tags + }; var quickFactContext = new QuickFactContext(result.ApplicationId, query.IncidentId, facts); foreach (var provider in _quickfactProviders) { @@ -96,11 +114,11 @@ public async Task HandleAsync(IMessageContext context, GetInc } foreach (var provider in _highlightedContextDataProviders) { - await provider.CollectAsync(query.IncidentId, contextData); + await provider.CollectAsync(highlightedContext); } foreach (var provider in _solutionProviders) { - await provider.SuggestSolutionAsync(query.IncidentId, solutions); + await provider.SuggestSolutionAsync(suggestedSolutionContext); } _logger.Info("GetIncident step 8"); @@ -116,7 +134,7 @@ private async Task GetContextCollectionNames(GetIncidentResult result) using (var cmd = _unitOfWork.CreateDbCommand()) { cmd.CommandText = @"select distinct Name -from [IncidentContextCollections] +from [IncidentContextCollections] WITH(READUNCOMMITTED) where IncidentId=@incidentId"; cmd.AddParameter("incidentId", result.Id); using (var reader = await cmd.ExecuteReaderAsync()) @@ -136,7 +154,7 @@ private async Task GetReportStatistics(GetIncidentResult result) using (var cmd = _unitOfWork.CreateCommand()) { cmd.CommandText = @"select cast(createdatutc as date) as Date, count(*) as Count -from errorreports +from errorreports WITH(READUNCOMMITTED) where incidentid=@incidentId AND CreatedAtUtc > @date group by cast(createdatutc as date)"; @@ -166,12 +184,15 @@ private async Task GetStatSummary(GetIncident query, ICollection fact using (var cmd = _unitOfWork.CreateDbCommand()) { cmd.CommandText = @" -select count(distinct emailaddress) from IncidentFeedback +select count(distinct emailaddress) +from IncidentFeedback where @minDate < CreatedAtUtc AND emailaddress is not null AND DATALENGTH(emailaddress) > 0 AND IncidentId = @incidentId; -select count(*) from IncidentFeedback + +select count(*) +from IncidentFeedback where @minDate < CreatedAtUtc AND Description is not null AND DATALENGTH(Description) > 0 @@ -212,7 +233,7 @@ private string[] GetTags(int incidentId) { cmd.CommandText = @"Declare @Tags AS Nvarchar(MAX); SELECT @Tags = COALESCE(@Tags + ';', '') + TagName - FROM IncidentTags + FROM IncidentTags WITH(READUNCOMMITTED) WHERE IncidentId=@id ORDER BY OrderNumber, TagName SELECT @Tags"; @@ -235,8 +256,8 @@ private string[] GetEnvironments(int incidentId) { cmd.CommandText = @"Declare @Names AS Nvarchar(MAX); SELECT @Names = COALESCE(@Names + ';', '') + Name - FROM IncidentEnvironments ie - JOIN Environments ae ON (ae.Id = ie.EnvironmentId) + FROM IncidentEnvironments ie WITH(READUNCOMMITTED) + JOIN Environments ae WITH(READUNCOMMITTED) ON (ae.Id = ie.EnvironmentId) WHERE IncidentId=@id ORDER BY Name SELECT @Names"; diff --git a/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs index c8cfb933..1d64902e 100644 --- a/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs +++ b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs @@ -16,13 +16,35 @@ public class MigrationRunner private readonly string _scriptNamespace; private readonly MigrationScripts _scripts; private ILog _logger = LogManager.GetLogger(typeof(MigrationRunner)); + private Assembly _scriptAssembly; public MigrationRunner(Func connectionFactory, string migrationName, string scriptNamespace) { _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory)); _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName)); _scriptNamespace = scriptNamespace; - _scripts = new MigrationScripts(migrationName); + _scriptAssembly = GetScriptAssembly(migrationName); + _scripts = new MigrationScripts(migrationName, _scriptAssembly); + } + + private Assembly GetScriptAssembly(string migrationName) + { + if (migrationName == null) throw new ArgumentNullException(nameof(migrationName)); + + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.IsDynamic) + continue; + if (assembly.GetName().Name?.StartsWith("Coderr", StringComparison.OrdinalIgnoreCase) != true) + continue; + + var isFound = assembly + .GetManifestResourceNames() + .Any(x => x.StartsWith(_scriptNamespace) && x.Contains($"{_migrationName}.v")); + if (isFound) + return assembly; + } + throw new InvalidOperationException($"Failed to find scripts for migration '{migrationName}'."); } public void Run() @@ -57,7 +79,7 @@ protected bool CanSchemaBeUpgraded() protected void LoadScripts() { var names = - Assembly.GetExecutingAssembly() + _scriptAssembly .GetManifestResourceNames() .Where(x => x.StartsWith(_scriptNamespace) && x.Contains($"{_migrationName}.v")); diff --git a/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs index c0e32826..3ccf3dc8 100644 --- a/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs +++ b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs @@ -12,6 +12,12 @@ public class MigrationScripts { private readonly string _migrationName; private readonly Dictionary _versions = new Dictionary(); + private Assembly _scriptAssembly; + public MigrationScripts(string migrationName, Assembly scriptAssembly) + { + _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName)); + _scriptAssembly = scriptAssembly ?? throw new ArgumentNullException(nameof(scriptAssembly)); + } public MigrationScripts(string migrationName) { @@ -32,7 +38,7 @@ public string LoadScript(int version) if (version <= 0) throw new ArgumentOutOfRangeException(nameof(version)); var scriptName = _versions[version]; - var res = Assembly.GetExecutingAssembly().GetManifestResourceStream(scriptName); + var res = _scriptAssembly.GetManifestResourceStream(scriptName); if (res == null) throw new InvalidOperationException("Failed to find schema " + scriptName); @@ -80,11 +86,13 @@ private static void ExecuteSql(IDbConnection connection, string sql) transaction.Commit(); } - + } public int GetHighestVersion() { + if (_versions.Count == 0) + return -1; return _versions.Max(x => x.Key); } } diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs index 814034e1..60a507e8 100644 --- a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs +++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs @@ -33,7 +33,7 @@ public IncidentBeingAnalyzedMapper() .Ignore(); Property(x => x.State) - .ToPropertyValue(x => (IncidentState) x) + .ToPropertyValue(x => (AnalyzedIncidentState) x) .ToColumnValue(x => (int) x); } } diff --git a/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs b/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs index 9eb16059..ff92b283 100644 --- a/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs +++ b/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs @@ -12,6 +12,7 @@ namespace Coderr.Server.SqlServer public class UnitOfWorkWithTransaction : IAdoNetUnitOfWork { private ILog _logger = LogManager.GetLogger(typeof(UnitOfWorkWithTransaction)); + private SqlCommand _lastCommand; public UnitOfWorkWithTransaction(SqlTransaction transaction) @@ -26,7 +27,11 @@ public void Dispose() if (Transaction == null) return; - _logger.Info("Rolling back " + GetHashCode()); + if (_lastCommand != null) + _logger.Debug($"Rolling back {GetHashCode()}, last command: {_lastCommand.CommandText}"); + else + _logger.Info($"Rolling back {GetHashCode()}"); + var connection = Transaction.Connection; Transaction.Rollback(); Transaction.Dispose(); @@ -38,10 +43,10 @@ public void Dispose() public void SaveChanges() { - //Already commited. - // some scenariors requires early SaveChanges + // Already committed. + // some scenarios requires early SaveChanges // to prevent dead locks. - // when there is time, find and eliminte the reason of the deadlocks :( + // when there is time, find and eliminate the reason of the deadlocks :( if (Transaction == null) return; @@ -56,6 +61,7 @@ public IDbCommand CreateCommand() { var cmd = Transaction.Connection.CreateCommand(); cmd.Transaction = Transaction; + _lastCommand = cmd; return cmd; } diff --git a/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs b/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs index 39c6b2c2..34017aee 100644 --- a/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs +++ b/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs @@ -5,9 +5,7 @@ using Coderr.Server.Abstractions.Security; using Coderr.Server.Api.Web.Overview.Queries; using Coderr.Server.Domain.Core.Incidents; -using Coderr.Server.Infrastructure.Security; using DotNetCqs; -using Coderr.Server.ReportAnalyzer.Abstractions; using Griffin.Data; namespace Coderr.Server.SqlServer.Web.Overview @@ -56,7 +54,7 @@ public async Task HandleAsync(IMessageContext context, GetOve var appIds = new List(); using (var cmd = _unitOfWork.CreateCommand()) { - cmd.CommandText = "SELECT id FROM Applications"; + cmd.CommandText = "SELECT id FROM Applications WITH(READUNCOMMITTED)"; using (var reader = cmd.ExecuteReader()) { while (reader.Read()) @@ -92,7 +90,7 @@ public async Task HandleAsync(IMessageContext context, GetOve FROM ( select Incidents.ApplicationId , cast(Incidents.CreatedAtUtc as date) as Date, count(Incidents.Id) as Count - from Incidents + from Incidents WITH(READUNCOMMITTED) where Incidents.CreatedAtUtc >= @minDate AND Incidents.CreatedAtUtc <= GetUtcDate() AND Incidents.ApplicationId in ({ApplicationIds}) @@ -169,25 +167,29 @@ private async Task GetStatSummary(GetOverview query, GetOverviewResult result) { using (var cmd = _unitOfWork.CreateDbCommand()) { - cmd.CommandText = $@"select count(id) from incidents + cmd.CommandText = $@"select count(id) +from incidents With(READUNCOMMITTED) where CreatedAtUtc >= @minDate AND CreatedAtUtc <= GetUtcDate() AND Incidents.ApplicationId IN ({ApplicationIds}) AND Incidents.State <> {(int)IncidentState.Ignored} AND Incidents.State <> {(int)IncidentState.Closed}; -select count(id) from errorreports +select count(id) +from errorreports WITH(READUNCOMMITTED) where CreatedAtUtc >= @minDate AND ApplicationId IN ({ApplicationIds}) -select count(distinct emailaddress) from IncidentFeedback +select count(distinct emailaddress) +from IncidentFeedback WITH(READUNCOMMITTED) where CreatedAtUtc >= @minDate AND CreatedAtUtc <= GetUtcDate() AND ApplicationId IN ({ApplicationIds}) AND emailaddress is not null AND DATALENGTH(emailaddress) > 0; -select count(*) from IncidentFeedback +select count(*) +from IncidentFeedback WITH(READUNCOMMITTED) where CreatedAtUtc >= @minDate AND CreatedAtUtc <= GetUtcDate() AND ApplicationId IN ({ApplicationIds}) @@ -241,13 +243,13 @@ private async Task GetTodaysOverviewAsync(GetOverview query) FROM ( select Incidents.ApplicationId , DATEPART(HOUR, Incidents.CreatedAtUtc) as Date, count(Incidents.Id) as Count - from Incidents + from Incidents WITH(READUNCOMMITTED) where Incidents.CreatedAtUtc >= @minDate AND CreatedAtUtc <= GetUtcDate() AND Incidents.ApplicationId IN ({0}) group by Incidents.ApplicationId, DATEPART(HOUR, Incidents.CreatedAtUtc) ) cte -right join applications on (applicationid=applications.id)", ApplicationIds); +right join applications WITH(READUNCOMMITTED) on (applicationid=applications.id)", ApplicationIds); cmd.AddParameter("minDate", startDate); diff --git a/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs b/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs index e83d5f83..ab8d723c 100644 --- a/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs +++ b/src/Server/Coderr.Server.Web/Areas/Installation/Controllers/AccountController.cs @@ -45,36 +45,36 @@ public async Task Admin(AccountViewModel model) account.Activate(); account.IsSysAdmin = true; var con = SetupTools.DbTools.OpenConnection(); - var uow = new AdoNetUnitOfWork(con); - var repos = new AccountRepository(uow); - if (await repos.IsUserNameTakenAsync(model.UserName)) - return Redirect(Url.GetNextWizardStep()); - - account.SetVerifiedEmail(model.EmailAddress); - await repos.CreateAsync(account); - - var user = new User(account.Id, account.UserName) - { - EmailAddress = account.Email - }; - var userRepos = new UserRepository(uow); - await userRepos.CreateAsync(user); - - var repos2 = new ApplicationRepository(uow); - var app = new Application(user.AccountId, "DemoApp") - { - ApplicationType = TypeOfApplication.DesktopApplication - }; - await repos2.CreateAsync(app); - - var tm = new ApplicationTeamMember(app.Id, account.Id, "System") + Application app; + using (var uow = new AdoNetUnitOfWork(con)) { - Roles = new[] { ApplicationRole.Admin, ApplicationRole.Member }, - UserName = account.UserName - }; - await repos2.CreateAsync(tm); - - uow.SaveChanges(); + var repos = new AccountRepository(uow); + if (await repos.IsUserNameTakenAsync(model.UserName)) + return Redirect(Url.GetNextWizardStep()); + + account.SetVerifiedEmail(model.EmailAddress); + await repos.CreateAsync(account); + + var user = new User(account.Id, account.UserName) { EmailAddress = account.Email }; + var userRepos = new UserRepository(uow); + await userRepos.CreateAsync(user); + + var repos2 = new ApplicationRepository(uow); + app = new Application(user.AccountId, "DemoApp") + { + ApplicationType = TypeOfApplication.DesktopApplication + }; + await repos2.CreateAsync(app); + + var tm = new ApplicationTeamMember(app.Id, account.Id, "System") + { + Roles = new[] { ApplicationRole.Admin, ApplicationRole.Member }, + UserName = account.UserName + }; + await repos2.CreateAsync(tm); + + uow.SaveChanges(); + } var claims = new List { diff --git a/src/Server/Coderr.Server.Web/Areas/Installation/Views/Shared/_Layout.cshtml b/src/Server/Coderr.Server.Web/Areas/Installation/Views/Shared/_Layout.cshtml index 040ba299..f6f1e5bb 100644 --- a/src/Server/Coderr.Server.Web/Areas/Installation/Views/Shared/_Layout.cshtml +++ b/src/Server/Coderr.Server.Web/Areas/Installation/Views/Shared/_Layout.cshtml @@ -9,10 +9,11 @@ html, body { height: 100%; } - + .validation-summary-errors ul li { color: red; } + .wrapper { position: absolute; top: 50%; @@ -22,7 +23,7 @@ } - + diff --git a/src/Server/Coderr.Server.Web/Boot/Cqs/CqsObjectMapper.cs b/src/Server/Coderr.Server.Web/Boot/Cqs/CqsObjectMapper.cs index 1e217821..a28b0115 100644 --- a/src/Server/Coderr.Server.Web/Boot/Cqs/CqsObjectMapper.cs +++ b/src/Server/Coderr.Server.Web/Boot/Cqs/CqsObjectMapper.cs @@ -29,7 +29,7 @@ public class CqsObjectMapper ContractResolver = new IncludeNonPublicMembersContractResolver(), NullValueHandling = NullValueHandling.Ignore, ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - Converters = new List {new StringEnumConverter()} + Converters = new List { new StringEnumConverter() } }; public bool IsEmpty => _cqsTypes.Count == 0; @@ -122,8 +122,7 @@ public void ScanAssembly(Assembly assembly) if (_cqsTypes.ContainsKey(type.Name)) throw new InvalidOperationException( - string.Format("Duplicate mappings for name '{0}'. '{1}' and '{2}'.", type.Name, type.FullName, - _cqsTypes[type.Name].FullName)); + $"Duplicate mappings for '{type.Name}': '{type.FullName}' and '{_cqsTypes[type.Name].FullName}'."); _cqsTypes.Add(type.Name, type); } diff --git a/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs b/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs index 5edd2aba..10a56810 100644 --- a/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs +++ b/src/Server/Coderr.Server.Web/Boot/Cqs/RegisterCqsServices.cs @@ -141,8 +141,7 @@ private QueueListener ConfigureQueueListener(ConfigurationContext context, strin private IMessageInvoker CreateMessageInvoker(IServiceProvider x) { - var provider = (ServiceProvider) x; - var invoker = new MessageInvoker(new HandlerScopeWrapper(provider)); + var invoker = new MessageInvoker(new HandlerScopeWrapper(x)); invoker.HandlerMissing += (sender, args) => { _log.Error("No handler for " + args.Message.Body.GetType()); diff --git a/src/Server/Coderr.Server.Web/Boot/Cqs/ScopeFactory.cs b/src/Server/Coderr.Server.Web/Boot/Cqs/ScopeFactory.cs index 223bd1aa..46de42be 100644 --- a/src/Server/Coderr.Server.Web/Boot/Cqs/ScopeFactory.cs +++ b/src/Server/Coderr.Server.Web/Boot/Cqs/ScopeFactory.cs @@ -6,19 +6,19 @@ namespace Coderr.Server.Web.Boot.Cqs { internal class ScopeFactory : IHandlerScopeFactory { - private readonly Func _serviceProviderAccesor; + private readonly Func _serviceProviderAccessor; private MicrosoftHandlerScopeFactory _scopeFactory; - public ScopeFactory(Func serviceProviderAccesor) + public ScopeFactory(Func serviceProviderAccessor) { - _serviceProviderAccesor = serviceProviderAccesor; + _serviceProviderAccessor = serviceProviderAccessor; } public IHandlerScope CreateScope() { if (_scopeFactory == null) { - var provider = _serviceProviderAccesor(); + var provider = _serviceProviderAccessor(); if (provider == null) throw new InvalidOperationException("container have not been setup properly yet."); _scopeFactory = new MicrosoftHandlerScopeFactory(provider); diff --git a/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs b/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs index a9f700ba..834a01d7 100644 --- a/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs +++ b/src/Server/Coderr.Server.Web/Boot/Modules/DbConnectionConfig.cs @@ -15,12 +15,9 @@ public class DbConnectionConfig : IAppModule public void Configure(ConfigurationContext context) { _config = context.Configuration; - context.Services.AddScoped(x => - { - var con = OpenConnection(); - var transaction = con.BeginTransaction(); - return new UnitOfWorkWithTransaction((SqlTransaction)transaction); - }); + context.Services.AddScoped(x => OpenConnection()); + context.Services.AddScoped(x => x.GetRequiredService().BeginTransaction()); + context.Services.AddScoped(x => new UnitOfWorkWithTransaction((SqlTransaction)x.GetRequiredService())); } public IDbConnection OpenConnection() diff --git a/src/Server/Coderr.Server.Web/ClientApp/boot.ts b/src/Server/Coderr.Server.Web/ClientApp/boot.ts index 4b3150bc..703074ef 100644 --- a/src/Server/Coderr.Server.Web/ClientApp/boot.ts +++ b/src/Server/Coderr.Server.Web/ClientApp/boot.ts @@ -53,6 +53,7 @@ Vue.filter("incidentState", } }); +Vue.component('context-navigator', require('./components/shared/incidents/ContextNavigator.vue.html').default); const routes = [ { diff --git a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/home/home.vue.html b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/home/home.vue.html index 3505bb1e..0276048b 100644 --- a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/home/home.vue.html +++ b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/home/home.vue.html @@ -1,13 +1,14 @@ diff --git a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/feedback.vue.html b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/feedback.vue.html index db7802c9..15329ddd 100644 --- a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/feedback.vue.html +++ b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/feedback.vue.html @@ -18,8 +18,8 @@

No feedback has been given.

-

This page can contain error descriptions written by your users when this error occurr.

-

Either write your own error page or activate one of your built in ones.

+

This page can contain error descriptions written by your users when this error occur.

+

Either write your own error page or activate one of our built in ones.

//Example for ASP.NET MVC5
 Err.Configuration.UserInteraction.AskForFeedback = true;
 Err.Configuration.DisplayErrorPages();
diff --git a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.css b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.css
index 6d98d35b..a1b50460 100644
--- a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.css
+++ b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.css
@@ -8,3 +8,10 @@
 .more {
     display: none;
 }
+
+.badge {
+    font-size: 0.8em;
+    background-color: #abdbe7;
+    color: #000;
+    padding: 2px;
+}
diff --git a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.vue.html b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.vue.html
index d974009a..038cf6a7 100644
--- a/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.vue.html
+++ b/src/Server/Coderr.Server.Web/ClientApp/components/analyze/incidents/incident.vue.html
@@ -1,112 +1,80 @@