diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 95bbe00242..ca24bbac1d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -21,7 +21,7 @@ - + diff --git a/src/Exceptionless.Core/Billing/StripeEventHandler.cs b/src/Exceptionless.Core/Billing/StripeEventHandler.cs index 7d51e94c79..01dd40d2b7 100644 --- a/src/Exceptionless.Core/Billing/StripeEventHandler.cs +++ b/src/Exceptionless.Core/Billing/StripeEventHandler.cs @@ -153,6 +153,12 @@ private async Task InvoicePaymentSucceededAsync(Invoice invoice) return; } + if (String.IsNullOrEmpty(org.BillingChangedByUserId)) + { + _logger.LogError("No billing user set for organization: {OrganizationId}", org.Id); + return; + } + var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId); if (user is null) { @@ -172,6 +178,12 @@ private async Task InvoicePaymentFailedAsync(Invoice invoice) return; } + if (String.IsNullOrEmpty(org.BillingChangedByUserId)) + { + _logger.LogError("No billing user set for organization: {OrganizationId}", org.Id); + return; + } + var user = await _userRepository.GetByIdAsync(org.BillingChangedByUserId); if (user is null) { diff --git a/src/Exceptionless.Core/Configuration/CacheOptions.cs b/src/Exceptionless.Core/Configuration/CacheOptions.cs index a06c7c778b..3b4a75e76c 100644 --- a/src/Exceptionless.Core/Configuration/CacheOptions.cs +++ b/src/Exceptionless.Core/Configuration/CacheOptions.cs @@ -8,7 +8,7 @@ public class CacheOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = null!; + public Dictionary Data { get; internal set; } = null!; public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; @@ -26,7 +26,7 @@ public static CacheOptions ReadFromConfiguration(IConfiguration config, AppOptio string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); - options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); diff --git a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs index 88a2d019e3..aedb18d935 100644 --- a/src/Exceptionless.Core/Configuration/MessageBusOptions.cs +++ b/src/Exceptionless.Core/Configuration/MessageBusOptions.cs @@ -8,7 +8,7 @@ public class MessageBusOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = null!; + public Dictionary Data { get; internal set; } = null!; public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; @@ -29,7 +29,7 @@ public static MessageBusOptions ReadFromConfiguration(IConfiguration config, App string? providerConnectionString = !String.IsNullOrEmpty(options.Provider) ? config.GetConnectionString(options.Provider) : null; var providerOptions = providerConnectionString.ParseConnectionString(defaultKey: "server"); - options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + options.Data ??= new Dictionary(StringComparer.OrdinalIgnoreCase); options.Data.AddRange(providerOptions); options.ConnectionString = options.Data.BuildConnectionString(new HashSet { nameof(options.Provider) }); diff --git a/src/Exceptionless.Core/Configuration/MetricOptions.cs b/src/Exceptionless.Core/Configuration/MetricOptions.cs index 730ad17358..0087982776 100644 --- a/src/Exceptionless.Core/Configuration/MetricOptions.cs +++ b/src/Exceptionless.Core/Configuration/MetricOptions.cs @@ -8,7 +8,7 @@ public class MetricOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = null!; + public Dictionary Data { get; internal set; } = null!; public static MetricOptions ReadFromConfiguration(IConfiguration config) { diff --git a/src/Exceptionless.Core/Configuration/QueueOptions.cs b/src/Exceptionless.Core/Configuration/QueueOptions.cs index e8463f6651..62c1a9e4a3 100644 --- a/src/Exceptionless.Core/Configuration/QueueOptions.cs +++ b/src/Exceptionless.Core/Configuration/QueueOptions.cs @@ -8,7 +8,7 @@ public class QueueOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); + public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; diff --git a/src/Exceptionless.Core/Configuration/StorageOptions.cs b/src/Exceptionless.Core/Configuration/StorageOptions.cs index a6d28b8d27..085a459c47 100644 --- a/src/Exceptionless.Core/Configuration/StorageOptions.cs +++ b/src/Exceptionless.Core/Configuration/StorageOptions.cs @@ -8,7 +8,7 @@ public class StorageOptions { public string? ConnectionString { get; internal set; } public string? Provider { get; internal set; } - public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); + public Dictionary Data { get; internal set; } = new(StringComparer.OrdinalIgnoreCase); public string Scope { get; internal set; } = null!; public string ScopePrefix { get; internal set; } = null!; diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj index 66dd25afc1..c93f1b24d4 100644 --- a/src/Exceptionless.Core/Exceptionless.Core.csproj +++ b/src/Exceptionless.Core/Exceptionless.Core.csproj @@ -22,19 +22,19 @@ - - + + - - - + + + - + - + diff --git a/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs b/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs index d973b3840b..b78e5ca5f9 100644 --- a/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs +++ b/src/Exceptionless.Core/Extensions/DictionaryExtensions.cs @@ -141,12 +141,12 @@ public static int GetCollectionHashCode(this IDictionary return hashCode; } - public static T? GetValueOrDefault(this IDictionary source, string key, T? defaultValue = default) + public static T? GetValueOrDefault(this IDictionary source, string key, T? defaultValue = default) { if (!source.ContainsKey(key)) return defaultValue; - object data = source[key]; + object? data = source[key]; if (data is T variable) return variable; @@ -162,12 +162,12 @@ public static int GetCollectionHashCode(this IDictionary return defaultValue; } - public static string GetString(this IDictionary source, string name) + public static string GetString(this IDictionary source, string name) { return source.GetString(name, String.Empty); } - public static string GetString(this IDictionary source, string name, string @default) + public static string GetString(this IDictionary source, string name, string @default) { if (!source.TryGetValue(name, out string? value) || value is null) return @default; diff --git a/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs b/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs index 89d6d295ce..51ee21bce3 100644 --- a/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs +++ b/src/Exceptionless.Core/Extensions/EnumerableExtensions.cs @@ -7,7 +7,7 @@ public static class EnumerableExtensions { public static IReadOnlyCollection UnionOriginalAndModified(this IReadOnlyCollection> documents) where T : class, new() { - return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).Where(d => d is not null)).ToList(); + return documents.Select(d => d.Value).Union(documents.Select(d => d.Original).OfType()).ToList(); } public static bool Contains(this IEnumerable enumerable, Func function) diff --git a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs index 23e592c1ca..0218352aec 100644 --- a/src/Exceptionless.Core/Jobs/CleanupDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupDataJob.cs @@ -61,9 +61,9 @@ ILoggerFactory loggerFactory _cacheClient = cacheClient; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CleanupDataJob), TimeSpan.FromMinutes(15), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) @@ -91,7 +91,12 @@ private async Task MarkTokensSuspended(JobContext context) do { - long updatedCount = await _tokenRepository.PatchAllAsync(q => q.Organization(suspendedOrganizations.Hits.Select(o => o.Id)).FieldEquals(t => t.IsSuspended, false), new PartialPatch(new { is_suspended = true })); + // Foundatio Hits can contain null elements, so filter them before accessing properties. + var suspendedOrganizationIds = suspendedOrganizations.Hits + .Where(o => o is not null && o.Id is not null) + .Select(o => o!.Id!) + .ToList(); + long updatedCount = await _tokenRepository.PatchAllAsync(q => q.Organization(suspendedOrganizationIds).FieldEquals(t => t.IsSuspended, false), new PartialPatch(new { is_suspended = true })); if (updatedCount > 0) _logger.LogInformation("Marking {SuspendedTokenCount} tokens as suspended", updatedCount); } while (!context.CancellationToken.IsCancellationRequested && await suspendedOrganizations.NextPageAsync()); diff --git a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs index 5e09702161..795342dae8 100644 --- a/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs +++ b/src/Exceptionless.Core/Jobs/CleanupOrphanedDataJob.cs @@ -42,9 +42,9 @@ ILoggerFactory loggerFactory _lockProvider = lockProvider; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CleanupOrphanedDataJob), TimeSpan.FromHours(2), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs index c968f02623..73c50df988 100644 --- a/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs +++ b/src/Exceptionless.Core/Jobs/CloseInactiveSessionsJob.cs @@ -36,9 +36,9 @@ ILoggerFactory loggerFactory _jsonOptions = jsonOptions; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(CloseInactiveSessionsJob), TimeSpan.FromMinutes(15), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs index 8ccdd02eab..530ae6e2d7 100644 --- a/src/Exceptionless.Core/Jobs/DailySummaryJob.cs +++ b/src/Exceptionless.Core/Jobs/DailySummaryJob.cs @@ -51,9 +51,9 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromHours(1), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(DailySummaryJob), TimeSpan.FromHours(1), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs index 08d8001194..18a93b0bdb 100644 --- a/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs +++ b/src/Exceptionless.Core/Jobs/DownloadGeoIPDatabaseJob.cs @@ -30,9 +30,9 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromDays(1), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(DownloadGeoIPDatabaseJob), TimeSpan.FromHours(2), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs b/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs index e3938e2785..41a670598d 100644 --- a/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs +++ b/src/Exceptionless.Core/Jobs/Elastic/DataMigrationJob.cs @@ -141,7 +141,7 @@ protected override async Task RunInternalAsync(JobContext context) var status = taskStatus?.Task?.Status; if (taskStatus?.Task is null || status is null) { - _logger.LogWarning(taskStatus?.OriginalException, "Error getting task status for {TargetIndex} {TaskId}: {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus.GetErrorMessage()); + _logger.LogWarning(taskStatus?.OriginalException, "Error getting task status for {TargetIndex} {TaskId}: {Message}", workItem.TargetIndex, workItem.TaskId, taskStatus?.GetErrorMessage()); if (taskStatus?.ServerError?.Status == 429) await Task.Delay(TimeSpan.FromSeconds(1), _timeProvider); diff --git a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs index e51f0f0a27..0cc28b6a1f 100644 --- a/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventNotificationsJob.cs @@ -60,7 +60,8 @@ public EventNotificationsJob(IQueue queue, protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var wi = context.QueueEntry.Value; + var wi = context.QueueEntry.Value!; + var ev = await _eventRepository.GetByIdAsync(wi.EventId); if (ev is null) return JobResult.SuccessWithMessage($"Could not load event: {wi.EventId}"); diff --git a/src/Exceptionless.Core/Jobs/EventPostsJob.cs b/src/Exceptionless.Core/Jobs/EventPostsJob.cs index 605ee3ee6c..1807c3d6ba 100644 --- a/src/Exceptionless.Core/Jobs/EventPostsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventPostsJob.cs @@ -5,13 +5,13 @@ using Exceptionless.Core.Plugins.EventParser; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Foundatio.Repositories.Exceptions; using Exceptionless.Core.Services; using Exceptionless.Core.Validation; using FluentValidation; using Foundatio.Jobs; using Foundatio.Queues; using Foundatio.Repositories; +using Foundatio.Repositories.Exceptions; using Foundatio.Resilience; using Microsoft.Extensions.Logging; using Newtonsoft.Json; @@ -54,10 +54,11 @@ public EventPostsJob(IQueue queue, EventPostService eventPostService, protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { var entry = context.QueueEntry; - var ep = entry.Value; + var ep = entry.Value!; + using var _ = _logger.BeginScope(new ExceptionlessState().Organization(ep.OrganizationId).Project(ep.ProjectId)); - string payloadPath = Path.ChangeExtension(entry.Value.FilePath, ".payload"); + string payloadPath = Path.ChangeExtension(ep.FilePath, ".payload"); var payloadTask = AppDiagnostics.PostsMarkFileActiveTime.TimeAsync(() => _eventPostService.GetEventPostPayloadAsync(payloadPath)); var projectTask = _projectRepository.GetByIdAsync(ep.ProjectId, o => o.Cache()); var organizationTask = _organizationRepository.GetByIdAsync(ep.OrganizationId, o => o.Cache()); @@ -124,7 +125,10 @@ protected override async Task ProcessQueueEntryAsync(QueueEntryContex if (uncompressedData.Length > maxEventPostSize) { var org = await organizationTask; - await _usageService.IncrementTooBigAsync(org.Id, project.Id); + if (org is not null) + await _usageService.IncrementTooBigAsync(org.Id, project.Id); + else + _logger.LogWarning("Organization {OrganizationId} not found, skipping too-big usage increment for event post {EventPostId}", ep.OrganizationId, entry.Id); await CompleteEntryAsync(entry, ep, _timeProvider.GetUtcNow().UtcDateTime); return JobResult.FailedWithMessage($"Unable to process decompressed EventPost data '{payloadPath}' ({payload.Length} bytes compressed, {uncompressedData.Length} bytes): Maximum uncompressed event post size limit ({maxEventPostSize} bytes) reached."); } @@ -322,7 +326,7 @@ await _eventPostService.EnqueueAsync(new EventPost(false) if (!isInternalProject && _logger.IsEnabled(LogLevel.Critical)) { using (_logger.BeginScope(new ExceptionlessState().Property("Event", new { ev.Date, ev.StackId, ev.Type, ev.Source, ev.Message, ev.Value, ev.Geo, ev.ReferenceId, ev.Tags }))) - _logger.LogCritical(ex, "Error while requeuing event post {FilePath}: {Message}", queueEntry.Value.FilePath, ex.Message); + _logger.LogCritical(ex, "Error while requeuing event post {QueueEntryId} {FilePath}: {Message}", queueEntry.Id, queueEntry.Value!.FilePath, ex.Message); } AppDiagnostics.EventsRetryErrors.Add(1); @@ -335,12 +339,12 @@ private Task AbandonEntryAsync(IQueueEntry queueEntry) return AppDiagnostics.PostsAbandonTime.TimeAsync(queueEntry.AbandonAsync); } - private Task CompleteEntryAsync(IQueueEntry entry, EventPostInfo eventPostInfo, DateTime created) + private Task CompleteEntryAsync(IQueueEntry entry, EventPost eventPost, DateTime created) { return AppDiagnostics.PostsCompleteTime.TimeAsync(async () => { await entry.CompleteAsync(); - await _eventPostService.CompleteEventPostAsync(entry.Value.FilePath, eventPostInfo.ProjectId, created, entry.Value.ShouldArchive); + await _eventPostService.CompleteEventPostAsync(eventPost.FilePath, eventPost.ProjectId, created, eventPost.ShouldArchive); }); } diff --git a/src/Exceptionless.Core/Jobs/EventUsageJob.cs b/src/Exceptionless.Core/Jobs/EventUsageJob.cs index b324778206..99c1cff0f2 100644 --- a/src/Exceptionless.Core/Jobs/EventUsageJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUsageJob.cs @@ -24,9 +24,9 @@ ILoggerFactory loggerFactory _lockProvider = lockProvider; } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(EventUsageJob), TimeSpan.FromMinutes(4), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs index 8b4514b6b3..fc8a5949ad 100644 --- a/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs +++ b/src/Exceptionless.Core/Jobs/EventUserDescriptionsJob.cs @@ -1,9 +1,10 @@ -using Exceptionless.Core.Models.Data; +using Exceptionless.Core.Models; +using Exceptionless.Core.Models.Data; using Exceptionless.Core.Queues.Models; using Exceptionless.Core.Repositories; -using Foundatio.Repositories.Exceptions; using Foundatio.Jobs; using Foundatio.Queues; +using Foundatio.Repositories.Exceptions; using Foundatio.Repositories.Extensions; using Foundatio.Resilience; using Microsoft.Extensions.Logging; @@ -23,11 +24,13 @@ public EventUserDescriptionsJob(IQueue queue, IEventReposi protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var description = context.QueueEntry.Value!; + _logger.LogTrace("Processing user description: id={0}", context.QueueEntry.Id); try { - await ProcessUserDescriptionAsync(context.QueueEntry.Value); + await ProcessUserDescriptionAsync(description); _logger.LogInformation("Processed user description: id={Id}", context.QueueEntry.Id); } catch (DocumentNotFoundException ex) @@ -57,7 +60,10 @@ private async Task ProcessUserDescriptionAsync(EventUserDescription description) }; if (description.Data is not null && description.Data.Count > 0) + { + ev.Data ??= new DataDictionary(); ev.Data.AddRange(description.Data); + } ev.SetUserDescription(ud); diff --git a/src/Exceptionless.Core/Jobs/MailMessageJob.cs b/src/Exceptionless.Core/Jobs/MailMessageJob.cs index bc142a08c8..afbd055c3f 100644 --- a/src/Exceptionless.Core/Jobs/MailMessageJob.cs +++ b/src/Exceptionless.Core/Jobs/MailMessageJob.cs @@ -20,12 +20,14 @@ public MailMessageJob(IQueue queue, IMailSender mailSender, TimePro protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { + var message = context.QueueEntry.Value!; + _logger.LogTrace("Processing message {Id}", context.QueueEntry.Id); try { - await _mailSender.SendAsync(context.QueueEntry.Value); - _logger.LogInformation("Sent message: to={To} subject={Subject}", context.QueueEntry.Value.To, context.QueueEntry.Value.Subject); + await _mailSender.SendAsync(message); + _logger.LogInformation("Sent message: to={To} subject={Subject}", message.To, message.Subject); } catch (Exception ex) { diff --git a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs index 61fc3721ad..24bbbcd812 100644 --- a/src/Exceptionless.Core/Jobs/StackEventCountJob.cs +++ b/src/Exceptionless.Core/Jobs/StackEventCountJob.cs @@ -25,9 +25,9 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(5), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(StackEventCountJob), TimeSpan.FromSeconds(5), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/StackStatusJob.cs b/src/Exceptionless.Core/Jobs/StackStatusJob.cs index 9a1ef8897b..5462944a0f 100644 --- a/src/Exceptionless.Core/Jobs/StackStatusJob.cs +++ b/src/Exceptionless.Core/Jobs/StackStatusJob.cs @@ -27,9 +27,9 @@ ILoggerFactory loggerFactory _lockProvider = new ThrottlingLockProvider(cacheClient, 1, TimeSpan.FromSeconds(10), timeProvider, resiliencePolicyProvider, loggerFactory); } - protected override Task GetLockAsync(CancellationToken cancellationToken = default) + protected override Task GetLockAsync(CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(StackStatusJob), TimeSpan.FromSeconds(10), cancellationToken); } protected override async Task RunInternalAsync(JobContext context) diff --git a/src/Exceptionless.Core/Jobs/WebHooksJob.cs b/src/Exceptionless.Core/Jobs/WebHooksJob.cs index 9f616db157..65d84f49d8 100644 --- a/src/Exceptionless.Core/Jobs/WebHooksJob.cs +++ b/src/Exceptionless.Core/Jobs/WebHooksJob.cs @@ -55,7 +55,8 @@ public WebHooksJob(IQueue queue, IProjectRepository project protected override async Task ProcessQueueEntryAsync(QueueEntryContext context) { - var body = context.QueueEntry.Value; + var body = context.QueueEntry.Value!; + bool shouldLog = body.ProjectId != _appOptions.InternalProjectId; using (_logger.BeginScope(new ExceptionlessState().Organization(body.OrganizationId).Project(body.ProjectId))) { @@ -161,6 +162,12 @@ private async Task IsEnabledAsync(WebHookNotification body) switch (body.Type) { case WebHookType.General: + if (body.WebHookId is null) + { + _logger.LogWarning("WebHook notification is missing the web hook id. Organization: {OrganizationId}, Project: {ProjectId}, Url: {Url}", body.OrganizationId, body.ProjectId, body.Url); + return false; + } + var webHook = await _webHookRepository.GetByIdAsync(body.WebHookId, o => o.Cache()); return webHook?.IsEnabled ?? false; case WebHookType.Slack: diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs index a40ad6c746..30fc8af6fc 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/FixStackStatsWorkItemHandler.cs @@ -26,14 +26,15 @@ public FixStackStatsWorkItemHandler(IStackRepository stackRepository, IEventRepo _timeProvider = timeProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(FixStackStatsWorkItemHandler), TimeSpan.FromHours(1), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); + var wi = context.GetData()!; + var utcEnd = wi.UtcEnd ?? _timeProvider.GetUtcNow().UtcDateTime; Log.LogInformation("Starting stack stats repair for {UtcStart:O} to {UtcEnd:O}. OrganizationId={Organization}", wi.UtcStart, utcEnd, wi.OrganizationId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs index 30d95134e1..e4d93665d6 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationMaintenanceWorkItemHandler.cs @@ -24,7 +24,7 @@ public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizati _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(OrganizationMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } @@ -32,7 +32,8 @@ public OrganizationMaintenanceWorkItemHandler(IOrganizationRepository organizati public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; - var wi = context.GetData(); + var wi = context.GetData()!; + Log.LogInformation("Received upgrade organizations work item. Upgrade Plans: {UpgradePlans}", wi.UpgradePlans); var results = await _organizationRepository.GetAllAsync(o => o.PageLimit(LIMIT)); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs index 7542b7f134..840f2bad38 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/OrganizationNotificationWorkItemHandler.cs @@ -61,7 +61,8 @@ public OrganizationNotificationWorkItemHandler(IOrganizationRepository organizat public override Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); + var wi = context.GetData()!; + string cacheKey = $"{nameof(OrganizationNotificationWorkItemHandler)}:{wi.OrganizationId}"; return _lockProvider.TryUsingAsync(cacheKey, async () => @@ -73,7 +74,7 @@ public override Task HandleItemAsync(WorkItemContext context) if (wi.IsOverMonthlyLimit) await SendOverageNotificationsAsync(organization, wi.IsOverHourlyLimit, wi.IsOverMonthlyLimit); - }, TimeSpan.FromMinutes(15), new CancellationToken(true)); + }, TimeSpan.FromMinutes(15), context.CancellationToken); } private async Task SendOverageNotificationsAsync(Organization organization, bool isOverHourlyLimit, bool isOverMonthlyLimit) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs index 129e715c09..f00c98d601 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/ProjectMaintenanceWorkItemHandler.cs @@ -22,16 +22,17 @@ public ProjectMaintenanceWorkItemHandler(IProjectRepository projectRepository, I _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(ProjectMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; - var workItem = context.GetData(); + var workItem = context.GetData()!; + Log.LogInformation("Received upgrade projects work item. Update Default Bot List: {UpdateDefaultBotList} IncrementConfigurationVersion: {IncrementConfigurationVersion}", workItem.UpdateDefaultBotList, workItem.IncrementConfigurationVersion); var results = await _projectRepository.GetAllAsync(o => o.PageLimit(LIMIT)); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs index d5e879ca12..e4cfb8b7ff 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveBotEventsWorkItemHandler.cs @@ -17,7 +17,7 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ILockPro _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { var wi = (RemoveBotEventsWorkItem)workItem; string cacheKey = $"{nameof(RemoveBotEventsWorkItem)}:{wi.OrganizationId}:{wi.ProjectId}"; @@ -26,7 +26,8 @@ public RemoveBotEventsWorkItemHandler(IEventRepository eventRepository, ILockPro public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); + var wi = context.GetData()!; + using var _ = Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId).Tag("Delete").Tag("Bot")); Log.LogInformation("Received remove bot events work item OrganizationId={OrganizationId} ProjectId={ProjectId}, ClientIpAddress={ClientIpAddress}, UtcStartDate={UtcStartDate}, UtcEndDate={UtcEndDate}", wi.OrganizationId, wi.ProjectId, wi.ClientIpAddress, wi.UtcStartDate, wi.UtcEndDate); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs index 3b58191399..eaf9a74dd8 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/RemoveStacksWorkItemHandler.cs @@ -20,15 +20,16 @@ public RemoveStacksWorkItemHandler(IStackRepository stackRepository, ICacheClien _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { string cacheKey = $"{nameof(RemoveStacksWorkItem)}:{((RemoveStacksWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { - var wi = context.GetData(); + var wi = context.GetData()!; + using (Log.BeginScope(new ExceptionlessState().Organization(wi.OrganizationId).Project(wi.ProjectId))) { Log.LogInformation("Received remove stacks work item for project: {ProjectId}", wi.ProjectId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs index 9dd197b68e..a7b0204974 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetLocationFromGeoWorkItemHandler.cs @@ -24,20 +24,21 @@ public SetLocationFromGeoWorkItemHandler(ICacheClient cacheClient, IEventReposit _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { string cacheKey = $"{nameof(SetLocationFromGeoWorkItemHandler)}:{((SetLocationFromGeoWorkItem)workItem).EventId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); + var workItem = context.GetData()!; + var geo = workItem.Geo; - if (!GeoResult.TryParse(workItem.Geo, out var result) || result is null) + if (geo is null || !GeoResult.TryParse(geo, out var result) || result is null) return; - var location = await _cache.GetAsync(workItem.Geo, null); + var location = await _cache.GetAsync(geo, null); if (location is null) { try @@ -48,14 +49,14 @@ public override async Task HandleItemAsync(WorkItemContext context) } catch (Exception ex) { - Log.LogError(ex, "Error occurred looking up reverse geocode: {Geo}", workItem.Geo); + Log.LogError(ex, "Error occurred looking up reverse geocode: {Geo}", geo); } } if (location is null) return; - await _cache.SetAsync(workItem.Geo, location, TimeSpan.FromDays(3)); + await _cache.SetAsync(geo, location, TimeSpan.FromDays(3)); var ev = await _eventRepository.GetByIdAsync(workItem.EventId); if (ev is null) diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs index fa4606c15b..d4c40a8159 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/SetProjectIsConfiguredWorkItemHandler.cs @@ -21,15 +21,16 @@ public SetProjectIsConfiguredWorkItemHandler(IProjectRepository projectRepositor _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { string cacheKey = $"{nameof(SetProjectIsConfiguredWorkItemHandler)}:{((SetProjectIsConfiguredWorkItem)workItem).ProjectId}"; - return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(cacheKey, TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); + var workItem = context.GetData()!; + Log.LogInformation("Setting Is Configured for project: {ProjectId}", workItem.ProjectId); var project = await _projectRepository.GetByIdAsync(workItem.ProjectId); diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs index baf839c012..2c38fbf03b 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandler.cs @@ -30,14 +30,15 @@ public UpdateProjectNotificationSettingsWorkItemHandler( _timeProvider = timeProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { return _lockProvider.AcquireAsync(nameof(UpdateProjectNotificationSettingsWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { - var workItem = context.GetData(); + var workItem = context.GetData()!; + Log.LogInformation("Received update project notification settings work item. Organization={Organization}", workItem.OrganizationId); long totalNotificationSettingsRemoved = 0; diff --git a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs index 0f33252f80..ae2d97f89b 100644 --- a/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs +++ b/src/Exceptionless.Core/Jobs/WorkItemHandlers/UserMaintenanceWorkItemHandler.cs @@ -22,16 +22,17 @@ public UserMaintenanceWorkItemHandler(IUserRepository userRepository, ILockProvi _lockProvider = lockProvider; } - public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = new()) + public override Task GetWorkItemLockAsync(object workItem, CancellationToken cancellationToken = default) { - return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), new CancellationToken(true)); + return _lockProvider.AcquireAsync(nameof(UserMaintenanceWorkItemHandler), TimeSpan.FromMinutes(15), cancellationToken); } public override async Task HandleItemAsync(WorkItemContext context) { const int LIMIT = 100; - var workItem = context.GetData(); + var workItem = context.GetData()!; + Log.LogInformation("Received user maintenance work item. Normalize={Normalize} ResetVerifyEmailAddressToken={ResendVerifyEmailAddressEmails}", workItem.Normalize, workItem.ResetVerifyEmailAddressToken); var results = await _userRepository.GetAllAsync(o => o.PageLimit(LIMIT)); diff --git a/src/Exceptionless.Core/Mail/Mailer.cs b/src/Exceptionless.Core/Mail/Mailer.cs index 2c5e9403ba..b6498c1d45 100644 --- a/src/Exceptionless.Core/Mail/Mailer.cs +++ b/src/Exceptionless.Core/Mail/Mailer.cs @@ -303,10 +303,10 @@ private HandlebarsTemplate GetCompiledTemplate(string name) }); } - private Task QueueMessageAsync(MailMessage message, string metricsName) + private Task QueueMessageAsync(MailMessage message, string metricsName) { if (!CleanAddresses(message)) - return Task.FromResult(String.Empty); + return Task.FromResult(null); AppDiagnostics.Counter($"mailer.{metricsName}"); return _queue.EnqueueAsync(message); diff --git a/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs b/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs index 700b113429..47c7f055f3 100644 --- a/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs +++ b/src/Exceptionless.Core/Migrations/UpdateEventUsage.cs @@ -72,6 +72,9 @@ private async Task UpdateOrganizationsUsageAsync(MigrationContext context) { var result = await _eventRepository.CountAsync(q => q.Organization(organization.Id).AggregationsExpression("date:date~1M")); var dateAggs = result.Aggregations.DateHistogram("date_date"); + if (dateAggs?.Buckets is null) + continue; + foreach (var dateHistogramBucket in dateAggs.Buckets) { var usage = organization.GetUsage(dateHistogramBucket.Date, _timeProvider); @@ -123,6 +126,9 @@ private async Task UpdateProjectsUsageAsync(MigrationContext context, Organizati { var result = await _eventRepository.CountAsync(q => q.Organization(organization.Id).Project(project.Id).AggregationsExpression("date:date~1M")); var dateAggs = result.Aggregations.DateHistogram("date_date"); + if (dateAggs?.Buckets is null) + continue; + foreach (var dateHistogramBucket in dateAggs.Buckets) { var usage = project.GetUsage(dateHistogramBucket.Date); diff --git a/src/Exceptionless.Core/Models/Collections/DataDictionary.cs b/src/Exceptionless.Core/Models/Collections/DataDictionary.cs index 323a7f352f..f9c0a5d0b9 100644 --- a/src/Exceptionless.Core/Models/Collections/DataDictionary.cs +++ b/src/Exceptionless.Core/Models/Collections/DataDictionary.cs @@ -4,7 +4,9 @@ namespace Exceptionless.Core.Models; public class DataDictionary : Dictionary { - public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) { } + public DataDictionary() : base(StringComparer.OrdinalIgnoreCase) + { + } public DataDictionary(IEnumerable> values) : base(StringComparer.OrdinalIgnoreCase) { @@ -27,12 +29,12 @@ public DataDictionary(IEnumerable> values) : base( return TryGetValue(key, out object? value) ? value : defaultValueProvider(); } - public string GetString(string name) + public string? GetString(string name) { - return GetString(name, String.Empty); + return GetString(name, null); } - public string GetString(string name, string @default) + public string? GetString(string name, string? @default) { if (!TryGetValue(name, out object? value)) return @default; @@ -46,9 +48,12 @@ public string GetString(string name, string @default) { return value.ToType(); } - catch { } + catch + { + // Ignored + } } - return String.Empty; + return null; } } diff --git a/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs b/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs index 42349b6767..99d8d2e5bb 100644 --- a/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs +++ b/src/Exceptionless.Core/Models/Data/ManualStackingInfo.cs @@ -18,7 +18,7 @@ public ManualStackingInfo(string? title) : this() public ManualStackingInfo(string? title, IDictionary signatureData) : this(title) { if (signatureData is not null && signatureData.Count > 0) - SignatureData.AddRange(signatureData); + SignatureData!.AddRange(signatureData); } public ManualStackingInfo(IDictionary signatureData) : this(null, signatureData) { } diff --git a/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs b/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs index 3770ae2be3..9b3f09ca65 100644 --- a/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs +++ b/src/Exceptionless.Core/Plugins/EventProcessor/Default/60_LocationPlugin.cs @@ -45,6 +45,9 @@ public override Task EventBatchProcessedAsync(ICollection contexts private async Task GetGeoLocationFromCacheAsync(IGrouping geoGroup) { + if (geoGroup.Key is null) + return; + var location = await _cacheClient.GetAsync(geoGroup.Key, null); if (location is null) return; diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs index 725bd7672b..a5e16ca5de 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/EventIndex.cs @@ -321,7 +321,7 @@ internal static class EventIndexExtensions public static PropertiesDescriptor AddCopyToMappings(this PropertiesDescriptor descriptor) { return descriptor - .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER)) + .Text(f => f.Name(EventIndex.Alias.IpAddress).Analyzer(EventIndex.COMMA_WHITESPACE_ANALYZER).AddKeywordField()) .Text(f => f.Name(EventIndex.Alias.OperatingSystem).Analyzer(EventIndex.WHITESPACE_LOWERCASE_ANALYZER).AddKeywordField()) .Object(f => f.Name(EventIndex.Alias.Error).Properties(p1 => p1 .Keyword(f3 => f3.Name("code").IgnoreAbove(1024)) diff --git a/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs b/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs index aa9ad57b92..810b68e775 100644 --- a/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs +++ b/src/Exceptionless.Core/Repositories/Configuration/Indexes/StackIndex.cs @@ -70,7 +70,7 @@ public override TypeMappingDescriptor ConfigureIndexMapping(TypeMappingDe protected override void ConfigureQueryParser(ElasticQueryParserConfiguration config) { - string dateFixedFieldName = InferPropertyName(f => f.DateFixed); + string dateFixedFieldName = InferPropertyName(f => f.DateFixed!); config .SetDefaultFields(["id", Alias.Title, Alias.Description, Alias.Tags, Alias.References]) .AddVisitor(new StackDateFixedQueryVisitor(dateFixedFieldName)) diff --git a/src/Exceptionless.Core/Repositories/EventRepository.cs b/src/Exceptionless.Core/Repositories/EventRepository.cs index c28518a735..fdf183ccb0 100644 --- a/src/Exceptionless.Core/Repositories/EventRepository.cs +++ b/src/Exceptionless.Core/Repositories/EventRepository.cs @@ -22,7 +22,7 @@ public EventRepository(ExceptionlessElasticConfiguration configuration, AppOptio BatchNotifications = true; DefaultPipeline = "events-pipeline"; - AddDefaultExclude(e => e.Idx); + AddDefaultExclude(e => e.Idx!); // copy to fields AddDefaultExclude(EventIndex.Alias.IpAddress); AddDefaultExclude(EventIndex.Alias.OperatingSystem); diff --git a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs index 52fcdfca90..c7c2272cbc 100644 --- a/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs +++ b/src/Exceptionless.Core/Repositories/Interfaces/IEventRepository.cs @@ -17,9 +17,12 @@ public interface IEventRepository : IRepositoryOwnedByOrganizationAndProject GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter? systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) + public static async Task GetPreviousAndNextEventIdsAsync(this IEventRepository repository, string id, AppFilter? systemFilter = null, DateTime? utcStart = null, DateTime? utcEnd = null) { var ev = await repository.GetByIdAsync(id, o => o.Cache()); + if (ev is null) + return null; + return await repository.GetPreviousAndNextEventIdsAsync(ev, systemFilter, utcStart, utcEnd); } } diff --git a/src/Exceptionless.Core/Repositories/OrganizationRepository.cs b/src/Exceptionless.Core/Repositories/OrganizationRepository.cs index a1e9cb4e3a..9fe9006c1b 100644 --- a/src/Exceptionless.Core/Repositories/OrganizationRepository.cs +++ b/src/Exceptionless.Core/Repositories/OrganizationRepository.cs @@ -83,7 +83,7 @@ public Task> GetByCriteriaAsync(string? criteria, Comm query.SortDescending((Organization o) => o.Id); break; case OrganizationSortBy.Subscribed: - query.SortDescending((Organization o) => o.SubscribeDate); + query.SortDescending((Organization o) => o.SubscribeDate!); break; // case OrganizationSortBy.MostActive: // query.WithSortDescending((Organization o) => o.TotalEventCount); diff --git a/src/Exceptionless.Core/Repositories/ProjectRepository.cs b/src/Exceptionless.Core/Repositories/ProjectRepository.cs index 5d7ca4ea5e..30a9f89c5d 100644 --- a/src/Exceptionless.Core/Repositories/ProjectRepository.cs +++ b/src/Exceptionless.Core/Repositories/ProjectRepository.cs @@ -96,7 +96,7 @@ protected override async Task AddDocumentsToCacheAsync(ICollection(); foreach (var project in findHits.Select(hit => hit.Document).Where(d => !String.IsNullOrEmpty(d?.Id))) - cacheEntries.Add(ConfigCacheKey(project.Id), ToCachedProjectConfig(project)); + cacheEntries.Add(ConfigCacheKey(project!.Id), ToCachedProjectConfig(project)); // NOTE: We call SetAllAsync instead of AddDocumentsToCacheWithKeyAsync due to our repo method gets the value directly from cache. if (cacheEntries.Count > 0) diff --git a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs index 72637e5370..9b3bd30166 100644 --- a/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs +++ b/src/Exceptionless.Core/Repositories/Queries/EventStackFilterQuery.cs @@ -80,6 +80,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie // TODO: Handle search expressions as well string filter = ctx.Source.GetFilterExpression() ?? String.Empty; + //bool altInvertRequested = false; if (filter.StartsWith("@!")) { @@ -98,15 +99,15 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie var stackIds = new List(); long stackTotal = 0; - string stackFilterValue = stackFilter.Filter; + string? stackFilterValue = stackFilter?.Filter; bool isStackIdsNegated = false; //= stackFilter.HasStatusOpen && !altInvertRequested; if (isStackIdsNegated) - stackFilterValue = stackFilter.InvertedFilter; + stackFilterValue = stackFilter?.InvertedFilter; if (String.IsNullOrEmpty(stackFilterValue) && (!ctx.Source.ShouldEnforceEventStackFilter() || ctx.Options.GetSoftDeleteMode() != SoftDeleteQueryMode.ActiveOnly)) return; - _logger.LogTrace("Source: {Filter} Stack Filter: {StackFilter} Inverted Stack Filter: {InvertedStackFilter}", filter, stackFilter.Filter, stackFilter.InvertedFilter); + _logger.LogTrace("Source: {Filter} Stack Filter: {StackFilter} Inverted Stack Filter: {InvertedStackFilter}", filter, stackFilter?.Filter, stackFilter?.InvertedFilter); var systemFilterQuery = GetSystemFilterQuery(ctx, isStackIdsNegated); systemFilterQuery.FilterExpression(stackFilterValue); @@ -132,7 +133,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie _logger.LogTrace("Query: {Query} will be inverted due to id limit: {ResultCount}", stackFilterValue, stackTotal); isStackIdsNegated = !isStackIdsNegated; - stackFilterValue = isStackIdsNegated ? stackFilter.InvertedFilter : stackFilter.Filter; + stackFilterValue = isStackIdsNegated ? stackFilter?.InvertedFilter : stackFilter?.Filter; systemFilterQuery.FilterExpression(stackFilterValue); softDeleteMode = isStackIdsNegated ? SoftDeleteQueryMode.All : SoftDeleteQueryMode.ActiveOnly; systemFilterQuery.EventStackFilterInverted(isStackIdsNegated); @@ -160,7 +161,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie { do { - stackIds.AddRange(results.Hits.Select(h => h.Id)); + stackIds.AddRange(results.Hits.Select(h => h.Id).OfType()); } while (await results.NextPageAsync()); } @@ -180,7 +181,7 @@ public EventStackFilterQueryBuilder(IStackRepository stackRepository, ICacheClie } // Strips stack only fields and stack only special fields - string eventFilter = await _eventStackFilter.GetEventFilterAsync(filter, ctx); + string? eventFilter = await _eventStackFilter.GetEventFilterAsync(filter, ctx); ctx.Source.FilterExpression(eventFilter); } @@ -202,15 +203,36 @@ private IRepositoryQuery GetSystemFilterQuery(IQueryVisitorContext context, bool if (!systemFilterQuery.HasAppFilter()) systemFilterQuery.AppFilter(builderContext?.Source.GetAppFilter()); - foreach (var range in systemFilterQuery.GetDateRanges()) + /* + * NOTE: Cannot mutate init only field. + * foreach (var range in systemFilterQuery.GetDateRanges()) + { + if (range.Field == _inferredEventDateField || range.Field == "date") + { + range.Field = _inferredStackLastOccurrenceField; + if (isStackIdsNegated) // don't apply retention date filter on inverted stack queries + range.StartDate = null; + + range.EndDate = null; + } + } + */ + + var dateRanges = systemFilterQuery.GetDateRanges(); + var rangesToReplace = dateRanges + .Where(range => range.Field == _inferredEventDateField || range.Field == "date") + .ToList(); + + // Remove date ranges targeting the event date field and replace with + // stack last-occurrence ranges (was previously in-place mutation). + foreach (var range in rangesToReplace) { - if (range.Field == _inferredEventDateField || range.Field == "date") - { - range.Field = _inferredStackLastOccurrenceField; - if (isStackIdsNegated) // don't apply retention date filter on inverted stack queries - range.StartDate = null; - range.EndDate = null; - } + dateRanges.Remove(range); + systemFilterQuery.DateRange( + isStackIdsNegated ? null : range.StartDate, // don't apply retention date filter on inverted stack queries + null, + _inferredStackLastOccurrenceField, + range.TimeZone); } return systemFilterQuery; diff --git a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs index 48743e04c0..00940d2762 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Validation/AppQueryValidator.cs @@ -34,7 +34,7 @@ public async Task ValidateQueryAsync(string? query) if (String.IsNullOrWhiteSpace(query)) return new QueryProcessResult { IsValid = true }; - IQueryNode parsedResult; + IQueryNode? parsedResult; try { var context = new ElasticQueryVisitorContext { QueryType = QueryTypes.Query }; @@ -49,6 +49,9 @@ public async Task ValidateQueryAsync(string? query) return new QueryProcessResult { Message = ex.Message }; } + if (parsedResult is null) + return new QueryProcessResult { Message = "Failed to parse query." }; + return await ValidateQueryAsync(parsedResult); } @@ -68,7 +71,7 @@ public async Task ValidateAggregationsAsync(string? aggs) if (String.IsNullOrWhiteSpace(aggs)) return new QueryProcessResult { IsValid = true }; - IQueryNode parsedResult; + IQueryNode? parsedResult; try { var context = new ElasticQueryVisitorContext { QueryType = QueryTypes.Aggregation }; @@ -83,6 +86,9 @@ public async Task ValidateAggregationsAsync(string? aggs) return new QueryProcessResult { Message = ex.Message }; } + if (parsedResult is null) + return new QueryProcessResult { Message = "Failed to parse aggregation." }; + return await ValidateAggregationsAsync(parsedResult); } diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs index 0c4e7d6104..a359f69b26 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventFieldsQueryVisitor.cs @@ -10,22 +10,26 @@ public class EventFieldsQueryVisitor : ChainableQueryVisitor public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { var childTerms = new List(); - if (node.Left is TermNode { Field: null } leftTermNode) + if (node.Left is TermNode { Field: null, Term: not null } leftTermNode) childTerms.Add(leftTermNode.Term); if (node.Left is TermRangeNode { Field: null } leftTermRangeNode) { - childTerms.Add(leftTermRangeNode.Min); - childTerms.Add(leftTermRangeNode.Max); + if (leftTermRangeNode.Min is not null) + childTerms.Add(leftTermRangeNode.Min); + if (leftTermRangeNode.Max is not null) + childTerms.Add(leftTermRangeNode.Max); } - if (node.Right is TermNode { Field: null } rightTermNode) + if (node.Right is TermNode { Field: null, Term: not null } rightTermNode) childTerms.Add(rightTermNode.Term); if (node.Right is TermRangeNode { Field: null } rightTermRangeNode) { - childTerms.Add(rightTermRangeNode.Min); - childTerms.Add(rightTermRangeNode.Max); + if (rightTermRangeNode.Min is not null) + childTerms.Add(rightTermRangeNode.Min); + if (rightTermRangeNode.Max is not null) + childTerms.Add(rightTermRangeNode.Max); } node.Field = GetCustomFieldName(node.Field, childTerms.ToArray()) ?? node.Field; @@ -63,7 +67,7 @@ public override Task VisitAsync(MissingNode node, IQueryVisitorContext context) return Task.CompletedTask; } - private string? GetCustomFieldName(string field, string[] terms) + private string? GetCustomFieldName(string? field, string?[] terms) { if (String.IsNullOrEmpty(field)) return null; @@ -92,11 +96,11 @@ public override Task VisitAsync(MissingNode node, IQueryVisitorContext context) return field; } - private static string GetTermType(string[] terms) + private static string GetTermType(string?[] terms) { string termType = "s"; - var trimmedTerms = terms.Where(t => t is not null).Distinct().ToList(); + var trimmedTerms = terms.OfType().Distinct().ToList(); foreach (string term in trimmedTerms) { if (term.StartsWith("*")) @@ -119,12 +123,12 @@ private static string GetTermType(string[] terms) return termType; } - public static Task RunAsync(IQueryNode node, IQueryVisitorContext? context = null) + public static Task RunAsync(IQueryNode node, IQueryVisitorContext? context = null) { - return new EventFieldsQueryVisitor().AcceptAsync(node, context); + return new EventFieldsQueryVisitor().AcceptAsync(node, context ?? new QueryVisitorContext()); } - public static IQueryNode Run(IQueryNode node, IQueryVisitorContext? context = null) + public static IQueryNode? Run(IQueryNode node, IQueryVisitorContext? context = null) { return RunAsync(node, context).GetAwaiter().GetResult(); } diff --git a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs index f5ed92ac6f..7d06961364 100644 --- a/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs +++ b/src/Exceptionless.Core/Repositories/Queries/Visitors/EventStackFilterQueryVisitor.cs @@ -80,18 +80,24 @@ public EventStackFilter() _invertedStackQueryVisitor.AddVisitor(new CleanupQueryVisitor()); } - public async Task GetEventFilterAsync(string query, IQueryVisitorContext? context = null) + public async Task GetEventFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); var result = await _parser.ParseAsync(query, context); + if (result is null) + return null; + await _eventQueryVisitor.AcceptAsync(result, context); return result.ToString(); } - public async Task GetStackFilterAsync(string query, IQueryVisitorContext? context = null) + public async Task GetStackFilterAsync(string query, IQueryVisitorContext? context = null) { context ??= new ElasticQueryVisitorContext(); var result = await _parser.ParseAsync(query, context); + if (result is null) + return null; + var invertedResult = result.Clone(); result = await _stackQueryVisitor.AcceptAsync(result, context); @@ -99,8 +105,8 @@ public async Task GetStackFilterAsync(string query, IQueryVisitorCo return new StackFilter { - Filter = result.ToString(), - InvertedFilter = invertedResult.ToString(), + Filter = result?.ToString(), + InvertedFilter = invertedResult?.ToString(), HasStatus = context.GetBoolean(nameof(StackFilter.HasStatus)), HasStackIds = context.GetBoolean(nameof(StackFilter.HasStackIds)), HasStatusOpen = context.GetBoolean(nameof(StackFilter.HasStatusOpen)) @@ -197,7 +203,7 @@ public class StackFilterQueryVisitor : ChainableQueryVisitor return Task.FromResult(result); } - public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) + public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext context) { return node.AcceptAsync(this, context); } @@ -205,8 +211,8 @@ public override Task AcceptAsync(IQueryNode node, IQueryVisitorConte public record StackFilter { - public required string Filter { get; set; } - public required string InvertedFilter { get; set; } + public required string? Filter { get; set; } + public required string? InvertedFilter { get; set; } public required bool HasStatus { get; set; } public required bool HasStatusOpen { get; set; } public required bool HasStackIds { get; set; } diff --git a/src/Exceptionless.Core/Repositories/StackRepository.cs b/src/Exceptionless.Core/Repositories/StackRepository.cs index f85dc8c9ba..c7a539d713 100644 --- a/src/Exceptionless.Core/Repositories/StackRepository.cs +++ b/src/Exceptionless.Core/Repositories/StackRepository.cs @@ -6,6 +6,7 @@ using Foundatio.Repositories.Exceptions; using Foundatio.Repositories.Models; using Foundatio.Repositories.Options; +using Microsoft.Extensions.Logging; using Nest; namespace Exceptionless.Core.Repositories; @@ -74,7 +75,7 @@ Instant parseDate(def dt) { ctx._source.total_occurrences += params.count;"; - var operation = new ScriptPatch(script.TrimScript()) + var operation = new ScriptPatch(script.TrimScript()!) { Params = new Dictionary(4) { @@ -130,7 +131,7 @@ Instant parseDate(def dt) { ctx._source.updated_utc = params.updatedUtc; }"; - var operation = new ScriptPatch(script.TrimScript()) + var operation = new ScriptPatch(script.TrimScript()!) { Params = new Dictionary(4) { @@ -166,6 +167,12 @@ public Task> GetIdsByQueryAsync(RepositoryQueryDescriptor o.Cache()); } @@ -187,7 +194,7 @@ protected override async Task AddDocumentsToCacheAsync(ICollection>(); foreach (var hit in findHits.Where(d => !String.IsNullOrEmpty(d.Document?.SignatureHash))) - cacheEntries.Add(GetStackSignatureCacheKey(hit.Document), hit); + cacheEntries.Add(GetStackSignatureCacheKey(hit.Document!), hit); if (cacheEntries.Count > 0) await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()); diff --git a/src/Exceptionless.Core/Repositories/TokenRepository.cs b/src/Exceptionless.Core/Repositories/TokenRepository.cs index e76ea8bf22..b03efaa97e 100644 --- a/src/Exceptionless.Core/Repositories/TokenRepository.cs +++ b/src/Exceptionless.Core/Repositories/TokenRepository.cs @@ -53,10 +53,14 @@ public Task RemoveAllByUserIdAsync(string userId, CommandOptionsDescriptor protected override Task PublishChangeTypeMessageAsync(ChangeType changeType, Token? document, IDictionary? data = null, TimeSpan? delay = null) { - var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) { - { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type }, - { ExtendedEntityChanged.KnownKeys.UserId, document?.UserId } - }; + var items = new Foundatio.Utility.DataDictionary(data ?? new Dictionary()) + { + { ExtendedEntityChanged.KnownKeys.IsAuthenticationToken, TokenType.Authentication == document?.Type } + }; + + if (document?.UserId is not null) + items[ExtendedEntityChanged.KnownKeys.UserId] = document.UserId; + return PublishMessageAsync(CreateEntityChanged(changeType, document?.OrganizationId, document?.ProjectId ?? document?.DefaultProjectId, null, document?.Id, items), delay); } } diff --git a/src/Exceptionless.Core/Repositories/UserRepository.cs b/src/Exceptionless.Core/Repositories/UserRepository.cs index 7c9fbd122e..5f3c340ff1 100644 --- a/src/Exceptionless.Core/Repositories/UserRepository.cs +++ b/src/Exceptionless.Core/Repositories/UserRepository.cs @@ -86,7 +86,7 @@ protected override async Task AddDocumentsToCacheAsync(ICollection var cacheEntries = new Dictionary>(); foreach (var hit in findHits.Where(d => !String.IsNullOrEmpty(d.Document?.EmailAddress))) - cacheEntries.Add(EmailCacheKey(hit.Document.EmailAddress), hit); + cacheEntries.Add(EmailCacheKey(hit.Document!.EmailAddress), hit); if (cacheEntries.Count > 0) await AddDocumentsToCacheWithKeyAsync(cacheEntries, options.GetExpiresIn()); diff --git a/src/Exceptionless.Core/Repositories/WebHookRepository.cs b/src/Exceptionless.Core/Repositories/WebHookRepository.cs index 5d6cb74400..d3d46bdc35 100644 --- a/src/Exceptionless.Core/Repositories/WebHookRepository.cs +++ b/src/Exceptionless.Core/Repositories/WebHookRepository.cs @@ -4,6 +4,7 @@ using FluentValidation; using Foundatio.Repositories; using Foundatio.Repositories.Models; +using Microsoft.Extensions.Logging; using Nest; namespace Exceptionless.Core.Repositories; @@ -40,6 +41,12 @@ public override Task> GetByProjectIdAsync(string projectId, public async Task MarkDisabledAsync(string id) { var webHook = await GetByIdAsync(id); + if (webHook is null) + { + _logger.LogWarning("WebHook {WebHookId} not found when marking as disabled", id); + return; + } + if (!webHook.IsEnabled) return; diff --git a/src/Exceptionless.Core/Services/EventPostService.cs b/src/Exceptionless.Core/Services/EventPostService.cs index 0f990997e6..38a047a3cd 100644 --- a/src/Exceptionless.Core/Services/EventPostService.cs +++ b/src/Exceptionless.Core/Services/EventPostService.cs @@ -63,7 +63,7 @@ public EventPostService(IQueue queue, IFileStorage storage, if (String.IsNullOrEmpty(path)) return null; - byte[] data; + byte[]? data; try { data = await _storage.GetFileContentsRawAsync(path); diff --git a/src/Exceptionless.Core/Services/SlackService.cs b/src/Exceptionless.Core/Services/SlackService.cs index c41ec984c4..d2660411bf 100644 --- a/src/Exceptionless.Core/Services/SlackService.cs +++ b/src/Exceptionless.Core/Services/SlackService.cs @@ -46,6 +46,9 @@ public SlackService(IQueue webHookNotificationQueue, Format byte[] body = await response.Content.ReadAsByteArrayAsync(); var result = _serializer.Deserialize(body); + if (result is null) + throw new Exception("Failed to deserialize Slack OAuth response"); + if (!result.ok) { throw new Exception($"Error getting access token: {result.error ?? result.warning}, Response: {result}"); @@ -83,6 +86,9 @@ public async Task RevokeAccessTokenAsync(string token) byte[] body = await response.Content.ReadAsByteArrayAsync(); var result = _serializer.Deserialize(body); + if (result is null) + return false; + if (result.ok && result.revoked || String.Equals(result.error, "invalid_auth")) return true; diff --git a/src/Exceptionless.Core/Services/UsageService.cs b/src/Exceptionless.Core/Services/UsageService.cs index 6811675415..52c1a9aeb2 100644 --- a/src/Exceptionless.Core/Services/UsageService.cs +++ b/src/Exceptionless.Core/Services/UsageService.cs @@ -285,6 +285,9 @@ public async Task GetUsageAsync(string organizationId, string if (projectId is null) { var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); + if (organization is null) + throw new UsageServiceException($"Organization '{organizationId}' not found."); + organization.TrimUsage(_timeProvider); usage = new UsageInfoResponse @@ -297,6 +300,9 @@ public async Task GetUsageAsync(string organizationId, string else { var project = await _projectRepository.GetByIdAsync(projectId, o => o.Cache()); + if (project is null) + throw new UsageServiceException($"Project '{projectId}' not found."); + project.TrimUsage(_timeProvider); usage = new UsageInfoResponse @@ -354,6 +360,9 @@ public async Task GetEventsLeftAsync(string organizationId) if (context.Organization is null) context.Organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); + if (context.Organization is null) + throw new UsageServiceException($"Organization '{organizationId}' not found."); + currentTotal = context.Organization.GetCurrentUsage(_timeProvider).Total; await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), currentTotal, TimeSpan.FromHours(8)); } @@ -426,11 +435,14 @@ public async Task IncrementBlockedAsync(string organizationId, string? projectId await _cache.IncrementAsync(GetBucketBlockedCacheKey(utcNow, organizationId, projectId), eventCount, TimeSpan.FromHours(8)); await _cache.ListAddAsync(GetOrganizationSetKey(utcNow), organizationId, TimeSpan.FromHours(8)); - await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); + if (projectId is not null) + await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); AppDiagnostics.EventsBlocked.Add(eventCount); } + // projectId is intentionally non-nullable: discarded events are only counted after project resolution + // (unlike Blocked/TooBig which can occur before a project is identified). public async Task IncrementDiscardedAsync(string organizationId, string projectId, int eventCount = 1) { if (eventCount <= 0) @@ -455,7 +467,8 @@ public async Task IncrementTooBigAsync(string organizationId, string? projectId) await _cache.IncrementAsync(GetBucketTooBigCacheKey(utcNow, organizationId, projectId), 1, TimeSpan.FromHours(8)); await _cache.ListAddAsync(GetOrganizationSetKey(utcNow), organizationId, TimeSpan.FromHours(8)); - await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); + if (projectId is not null) + await _cache.ListAddAsync(GetProjectSetKey(utcNow), projectId, TimeSpan.FromHours(8)); AppDiagnostics.PostTooBig.Add(1); } diff --git a/src/Exceptionless.Core/Services/UsageServiceException.cs b/src/Exceptionless.Core/Services/UsageServiceException.cs new file mode 100644 index 0000000000..4d404c5961 --- /dev/null +++ b/src/Exceptionless.Core/Services/UsageServiceException.cs @@ -0,0 +1,3 @@ +namespace Exceptionless.Core.Services; + +public class UsageServiceException(string? message = null) : Exception(message); diff --git a/src/Exceptionless.Core/Utility/SampleDataService.cs b/src/Exceptionless.Core/Utility/SampleDataService.cs index f3e5059d96..98538baf23 100644 --- a/src/Exceptionless.Core/Utility/SampleDataService.cs +++ b/src/Exceptionless.Core/Utility/SampleDataService.cs @@ -237,6 +237,12 @@ public async Task CreateInternalOrganizationAndProjectAsync(string userId) return; var user = await _userRepository.GetByIdAsync(userId, o => o.Cache()); + if (user is null) + { + _logger.LogDebug("User {UserId} not found when creating sample data", userId); + return; + } + var organization = new Organization { Name = "Exceptionless" }; _billingManager.ApplyBillingPlan(organization, _billingPlans.UnlimitedPlan, user); organization = await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency().Cache()); diff --git a/src/Exceptionless.Insulation/Bootstrapper.cs b/src/Exceptionless.Insulation/Bootstrapper.cs index 2301172fc3..62f9ae7aa7 100644 --- a/src/Exceptionless.Insulation/Bootstrapper.cs +++ b/src/Exceptionless.Insulation/Bootstrapper.cs @@ -233,7 +233,7 @@ private static void RegisterStorage(IServiceCollection container, StorageOptions else if (String.Equals(options.Provider, "s3")) { container.ReplaceSingleton(s => new S3FileStorage(o => o - .ConnectionString(options.ConnectionString) + .ConnectionString(options.ConnectionString!) .Credentials(GetAWSCredentials(options.Data)) .Region(GetAWSRegionEndpoint(options.Data)) .Bucket(options.Data.GetString("bucket", $"{options.ScopePrefix}ex-events")) @@ -318,13 +318,13 @@ private static string GetQueueName(QueueOptions options) return String.Concat(options.ScopePrefix, typeof(T).Name); } - private static RegionEndpoint GetAWSRegionEndpoint(IDictionary data) + private static RegionEndpoint GetAWSRegionEndpoint(IDictionary data) { string region = data.GetString("region"); return RegionEndpoint.GetBySystemName(String.IsNullOrEmpty(region) ? "us-east-1" : region); } - private static AWSCredentials GetAWSCredentials(IDictionary data) + private static AWSCredentials GetAWSCredentials(IDictionary data) { string accessKey = data.GetString("accesskey"); string secretKey = data.GetString("secretkey"); diff --git a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj index c0072d5d74..ec9261c81b 100644 --- a/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj +++ b/src/Exceptionless.Insulation/Exceptionless.Insulation.csproj @@ -2,18 +2,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs b/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs index cdb575db9a..a75662f81e 100644 --- a/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs +++ b/src/Exceptionless.Insulation/Geo/MaxMindGeoIpService.cs @@ -104,8 +104,14 @@ public MaxMindGeoIpService(IFileStorage storage, TimeProvider timeProvider, ILog _logger.LogInformation("Loading GeoIP database"); try { - using (var stream = await _storage.GetFileStreamAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH, StreamMode.Read, cancellationToken)) - _database = new DatabaseReader(stream); + using var stream = await _storage.GetFileStreamAsync(DownloadGeoIPDatabaseJob.GEO_IP_DATABASE_PATH, StreamMode.Read, cancellationToken); + if (stream is null) + { + _logger.LogWarning("GeoIP database stream was null"); + return null; + } + + _database = new DatabaseReader(stream); } catch (Exception ex) { diff --git a/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs b/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs index 1c8fbd5b2c..7512a19c60 100644 --- a/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs +++ b/src/Exceptionless.Insulation/Mail/ExtensionsProtocolLogger.cs @@ -11,7 +11,7 @@ public class ExtensionsProtocolLogger : IProtocolLogger private readonly ILogger _logger; - public IAuthenticationSecretDetector AuthenticationSecretDetector { get; set; } = null!; + public IAuthenticationSecretDetector? AuthenticationSecretDetector { get; set; } = null!; public ExtensionsProtocolLogger(ILogger logger) { diff --git a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs index 6068fb60a5..39e11316fb 100644 --- a/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs +++ b/src/Exceptionless.Insulation/Mail/MailKitMailSender.cs @@ -52,7 +52,7 @@ public async Task SendAsync(MailMessage model) client.AuthenticationMechanisms.Remove("XOAUTH2"); string? user = _emailOptions.SmtpUser; - if (!String.IsNullOrEmpty(user)) + if (!String.IsNullOrEmpty(user) && !String.IsNullOrEmpty(_emailOptions.SmtpPassword)) { _logger.LogTrace("Authenticating {SmtpUser} to SMTP server", user); sw.Restart(); @@ -60,7 +60,7 @@ public async Task SendAsync(MailMessage model) _logger.LogTrace("Authenticated to SMTP server took {Duration:g}", sw.Elapsed); } - _logger.LogTrace("Sending message: to={To} subject={Subject}", message.Subject, message.To); + _logger.LogTrace("Sending message: to={To} subject={Subject}", message.To, message.Subject); sw.Restart(); await client.SendAsync(message); _logger.LogTrace("Sent Message took {Duration:g}", sw.Elapsed); diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index e19a21f68f..324749d7f4 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -250,7 +250,7 @@ private async Task> CountInternalAsync(AppFilter sf, T CountResult result; try { - result = await _repository.CountAsync(q => q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression(aggregations)); + result = await _repository.CountAsync(q => q.SystemFilter(query).FilterExpression(filter).EnforceEventStackFilter().AggregationsExpression(aggregations!)); } catch (Exception ex) { @@ -420,7 +420,7 @@ private Task> GetEventsInternalAsync(AppFilter sf, .Index(ti.Range.UtcStart, ti.Range.UtcEnd), o => page.HasValue ? o.PageNumber(page).PageLimit(limit) - : o.SearchBeforeToken(before).SearchAfterToken(after).PageLimit(limit)); + : o.SearchBeforeToken(before!).SearchAfterToken(after!).PageLimit(limit)); } /// @@ -1091,6 +1091,7 @@ private async Task GetSubmitEventAsync(string? projectId = null, i ev.Geo = geo?.ToString(); break; case "tags": + ev.Tags ??= []; ev.Tags.AddRange(kvp.Value.SelectMany(t => t?.Split([","], StringSplitOptions.RemoveEmptyEntries) ?? []).Distinct()); break; case "identity": @@ -1417,11 +1418,11 @@ private async Task> GetStackSummariesAsync(List("min_date").Value, - LastOccurrence = term.Aggregations.Max("max_date").Value, - Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), + FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? stack.FirstOccurrence, + LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? stack.LastOccurrence, + Total = (long)(term.Aggregations.Sum("sum_count")?.Value ?? term.Total.GetValueOrDefault()), - Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), + Users = term.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0, TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) }; @@ -1447,8 +1448,8 @@ private async Task> GetUserCountByProjectIdsAsync(ICo var countResult = await _repository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).EnforceEventStackFilter().AggregationsExpression("terms:(project_id cardinality:user)")); // Cache all projects that have more than 10 users for 5 minutes. - var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; - var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + var projectTerms = countResult.Aggregations.Terms("terms_project_id")?.Buckets ?? []; + var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0); await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); totals.AddRange(aggregations); diff --git a/src/Exceptionless.Web/Controllers/StackController.cs b/src/Exceptionless.Web/Controllers/StackController.cs index eef9d13441..638d9f0ebc 100644 --- a/src/Exceptionless.Web/Controllers/StackController.cs +++ b/src/Exceptionless.Web/Controllers/StackController.cs @@ -600,7 +600,8 @@ private async Task> GetStackSummariesAsync(IColle var systemFilter = new RepositoryQuery().AppFilter(eventSystemFilter).DateRange(ti.Range.UtcStart, ti.Range.UtcEnd, (PersistentEvent e) => e.Date).Index(ti.Range.UtcStart, ti.Range.UtcEnd); var stackTerms = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(String.Join(" OR ", stacks.Select(r => $"stack:{r.Id}"))).AggregationsExpression($"terms:(stack_id~{stacks.Count} cardinality:user sum:count~1 min:date max:date)")); - return await GetStackSummariesAsync(stacks, stackTerms.Aggregations.Terms("terms_stack_id").Buckets, eventSystemFilter, ti); + var buckets = stackTerms.Aggregations.Terms("terms_stack_id")?.Buckets ?? []; + return await GetStackSummariesAsync(stacks, buckets, eventSystemFilter, ti); } private async Task> GetStackSummariesAsync(ICollection stacks, IReadOnlyCollection> stackTerms, AppFilter sf, TimeInfo ti) @@ -619,11 +620,11 @@ private async Task> GetStackSummariesAsync(IColle Data = data.Data, Title = stack.Title, Status = stack.Status, - FirstOccurrence = term.Aggregations.Min("min_date").Value, - LastOccurrence = term.Aggregations.Max("max_date").Value, - Total = (long)(term.Aggregations.Sum("sum_count").Value ?? term.Total.GetValueOrDefault()), + FirstOccurrence = term.Aggregations.Min("min_date")?.Value ?? stack.FirstOccurrence, + LastOccurrence = term.Aggregations.Max("max_date")?.Value ?? stack.LastOccurrence, + Total = (long)(term.Aggregations.Sum("sum_count")?.Value ?? term.Total.GetValueOrDefault()), - Users = term.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault(), + Users = term.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0, TotalUsers = totalUsers.GetOrDefault(stack.ProjectId) }; @@ -649,8 +650,8 @@ private async Task> GetUserCountByProjectIdsAsync(ICo var countResult = await _eventRepository.CountAsync(q => q.SystemFilter(systemFilter).FilterExpression(projects.BuildFilter()).AggregationsExpression("terms:(project_id cardinality:user)")); // Cache all projects that have more than 10 users for 5 minutes. - var projectTerms = countResult.Aggregations.Terms("terms_project_id").Buckets; - var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); + var projectTerms = countResult.Aggregations.Terms("terms_project_id")?.Buckets ?? []; + var aggregations = projectTerms.ToDictionary(t => t.Key, t => t.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0); await scopedCacheClient.SetAllAsync(aggregations.Where(t => t.Value >= 10).ToDictionary(k => k.Key, v => v.Value), TimeSpan.FromMinutes(5)); totals.AddRange(aggregations); diff --git a/src/Exceptionless.Web/Exceptionless.Web.csproj b/src/Exceptionless.Web/Exceptionless.Web.csproj index 923bc98ac9..f1d0420267 100644 --- a/src/Exceptionless.Web/Exceptionless.Web.csproj +++ b/src/Exceptionless.Web/Exceptionless.Web.csproj @@ -15,12 +15,12 @@ - + - + diff --git a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs index 0625a05fb0..e9397869da 100644 --- a/src/Exceptionless.Web/Hubs/MessageBusBroker.cs +++ b/src/Exceptionless.Web/Hubs/MessageBusBroker.cs @@ -82,6 +82,12 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc return; } + if (entityChanged.Id is null) + { + _logger.LogTrace("Ignoring {UserTypeName} message: No user id", UserTypeName); + return; + } + var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(entityChanged.Id); _logger.LogTrace("Sending {UserTypeName} message to user: {UserId} (to {UserConnectionCount} connections)", UserTypeName, entityChanged.Id, userConnectionIds.Count); foreach (string connectionId in userConnectionIds) @@ -93,7 +99,7 @@ private async Task OnEntityChangedAsync(EntityChanged ec, CancellationToken canc // Only allow specific token messages to be sent down to the client. if (TokenTypeName == entityChanged.Type) { - string userId = entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.UserId); + string? userId = entityChanged.Data.GetValueOrDefault(ExtendedEntityChanged.KnownKeys.UserId); if (userId is not null) { var userConnectionIds = await _connectionMapping.GetUserIdConnectionsAsync(userId); diff --git a/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs b/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs index 33cf038162..82c0661bdc 100644 --- a/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs +++ b/src/Exceptionless.Web/Utility/GetFilterScopeVisitor.cs @@ -37,10 +37,10 @@ public override void Visit(TermNode node, IQueryVisitorContext context) } } - public override Task AcceptAsync(IQueryNode node, IQueryVisitorContext? context) + public override async Task AcceptAsync(IQueryNode node, IQueryVisitorContext? context) { - node.AcceptAsync(this, context); - return Task.FromResult(_scope); + await node.AcceptAsync(this, context ?? new QueryVisitorContext()); + return _scope; } public static FilterScope Run(string filter) diff --git a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs index b726115f52..384783aaed 100644 --- a/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs +++ b/src/Exceptionless.Web/Utility/Handlers/OverageMiddleware.cs @@ -96,7 +96,7 @@ public async Task Invoke(HttpContext context) { AppDiagnostics.PostsBlocked.Add(1); var organization = await _organizationRepository.GetByIdAsync(organizationId, o => o.Cache()); - if (organization.IsSuspended) + if (organization is { IsSuspended: true }) { context.Response.StatusCode = StatusCodes.Status402PaymentRequired; return; diff --git a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs index e7fc0526c1..9e51f1f7dd 100644 --- a/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/AuthControllerTests.cs @@ -1,3 +1,4 @@ +using System.IdentityModel.Tokens.Jwt; using Exceptionless.Core.Authorization; using Exceptionless.Core.Configuration; using Exceptionless.Core.Extensions; @@ -14,7 +15,6 @@ using Foundatio.Queues; using Foundatio.Repositories; using Microsoft.AspNetCore.Mvc; -using System.IdentityModel.Tokens.Jwt; using Xunit; using User = Exceptionless.Core.Models.User; @@ -349,7 +349,8 @@ public async Task CanSignupWhenAccountCreationEnabledWithValidTokenAsync() Assert.Equal(password.ToSaltedHash(user.Salt), user.Password); Assert.Contains(organization.Id, user.OrganizationIds); - organization = await _organizationRepository.GetByIdAsync(organization.Id); + organization = (await _organizationRepository.GetByIdAsync(organization.Id))!; + Assert.NotNull(organization); Assert.Empty(organization.Invites); var token = await _tokenRepository.GetByIdAsync(result.Token); @@ -748,6 +749,7 @@ public async Task CanChangePasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); + Assert.NotNull(token.UserId); var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -809,6 +811,7 @@ public async Task ChangePasswordShouldFailWithCurrentPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); + Assert.NotNull(token.UserId); var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -873,6 +876,7 @@ public async Task CanResetPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); + Assert.NotNull(token.UserId); var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -934,6 +938,7 @@ public async Task ResetPasswordShouldFailWithCurrentPasswordAsync() var token = await _tokenRepository.GetByIdAsync(result.Token); Assert.NotNull(token); + Assert.NotNull(token.UserId); var actualUser = await _userRepository.GetByIdAsync(token.UserId); Assert.NotNull(actualUser); Assert.Equal(email, actualUser.EmailAddress); @@ -992,6 +997,7 @@ public async Task CanLogoutUserAsync() // Verify that the token is valid var token = await _tokenRepository.GetByIdAsync(result.Token); + Assert.NotNull(token); Assert.Equal(TokenType.Authentication, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); @@ -1021,7 +1027,8 @@ await SendRequestAsync(r => r .StatusCodeShouldBeForbidden() ); - token = await _tokenRepository.GetByIdAsync(token.Id); + token = (await _tokenRepository.GetByIdAsync(token.Id))!; + Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); @@ -1126,7 +1133,8 @@ await SendRequestAsync(r => r .StatusCodeShouldBeForbidden() ); - token = await _tokenRepository.GetByIdAsync(token.Id); + token = (await _tokenRepository.GetByIdAsync(token.Id))!; + Assert.NotNull(token); Assert.Equal(TokenType.Access, token.Type); Assert.False(token.IsDisabled); Assert.False(token.IsSuspended); diff --git a/tests/Exceptionless.Tests/Controllers/Data/openapi.json b/tests/Exceptionless.Tests/Controllers/Data/openapi.json index 920ec6aefc..086c0a5990 100644 --- a/tests/Exceptionless.Tests/Controllers/Data/openapi.json +++ b/tests/Exceptionless.Tests/Controllers/Data/openapi.json @@ -7098,7 +7098,9 @@ }, "CountResult": { "required": [ - "total" + "total", + "aggregations", + "data" ], "type": "object", "properties": { diff --git a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs index 52cb4ccfe8..1ef4cccfaf 100644 --- a/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/EventControllerTests.cs @@ -127,6 +127,7 @@ await SendRequestAsync(r => r Assert.Equal(1, stats.Completed); ev = await _eventRepository.GetByIdAsync(ev.Id); + Assert.NotNull(ev); identity = ev.GetUserIdentity(jsonOptions); Assert.NotNull(identity); Assert.Equal("Test user", identity.Identity); @@ -783,8 +784,10 @@ await CreateDataAsync(d => Assert.NotNull(countResult); var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); - double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); + Assert.NotNull(dateAgg); + Assert.NotNull(dateAgg.Buckets); + double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); + double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); Assert.Equal(1, dateAggStackCount); Assert.Equal(1, dateAggEventCount); @@ -890,8 +893,10 @@ await CreateDataAsync(d => Assert.NotNull(countResult); var dateAgg = countResult.Aggregations.DateHistogram("date_date"); - double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack").Value.GetValueOrDefault()); - double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count").Value.GetValueOrDefault()); + Assert.NotNull(dateAgg); + Assert.NotNull(dateAgg.Buckets); + double dateAggStackCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_stack")?.Value.GetValueOrDefault() ?? 0); + double dateAggEventCount = dateAgg.Buckets.Sum(t => t.Aggregations.Cardinality("sum_count")?.Value.GetValueOrDefault() ?? 0); Assert.Equal(2, dateAggStackCount); Assert.Equal(2, dateAggEventCount); @@ -915,6 +920,7 @@ public async Task ShouldRespectEventUsageLimits() string organizationId = TestConstants.OrganizationId; var organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1061,6 +1067,7 @@ await SendRequestAsync(r => r Assert.Equal(JobResult.Success, await processUsageJob.RunAsync(TestCancellationToken)); organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); organizationUsage = organization.Usage.Single(); Assert.Equal(total, organizationUsage.Total); @@ -1079,6 +1086,7 @@ public async Task ShouldDiscardEventsForSuspendedOrganization() string organizationId = TestConstants.OrganizationId; var organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1164,6 +1172,7 @@ public async Task PlanChangeShouldAllowEventSubmission() string organizationId = TestConstants.OrganizationId; var organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1229,6 +1238,7 @@ await SendRequestAsync(r => r // Upgrade Plan organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.MediumPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1284,6 +1294,7 @@ await SendRequestAsync(r => r // Downgrade Plan and verify throttled organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); billingManager.ApplyBillingPlan(organization, plans.SmallPlan, _userData.GenerateSampleUser()); if (organization.BillingPrice > 0) { @@ -1317,6 +1328,7 @@ await SendRequestAsync(r => r Assert.Equal(JobResult.Success, await processUsageJob.RunAsync(TestCancellationToken)); organization = await _organizationRepository.GetByIdAsync(organizationId); + Assert.NotNull(organization); organizationUsage = organization.Usage.Single(); Assert.Equal(total, organizationUsage.Total); @@ -1837,7 +1849,8 @@ await SendRequestAsync(r => r private string ToPrettyJson(string json) { using var document = JsonDocument.Parse(json); - var prettyJsonOptions = new JsonSerializerOptions(_jsonSerializerOptions) { + var prettyJsonOptions = new JsonSerializerOptions(_jsonSerializerOptions) + { WriteIndented = true }; return JsonSerializer.Serialize(document.RootElement, prettyJsonOptions); diff --git a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs index 5d43c6cd88..0cac5bf1fe 100644 --- a/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/TokenControllerTests.cs @@ -264,6 +264,7 @@ public async Task SuspendingOrganizationWillDisableApiKey() var repository = GetService(); var tokenRecord = await repository.GetByIdAsync(token.Id, o => o.Cache()); + Assert.NotNull(tokenRecord); Assert.NotNull(tokenRecord.Id); Assert.False(tokenRecord.IsDisabled); diff --git a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj index f786d3d3d6..db7c525b64 100644 --- a/tests/Exceptionless.Tests/Exceptionless.Tests.csproj +++ b/tests/Exceptionless.Tests/Exceptionless.Tests.csproj @@ -7,14 +7,14 @@ - + - - + + diff --git a/tests/Exceptionless.Tests/IntegrationTestsBase.cs b/tests/Exceptionless.Tests/IntegrationTestsBase.cs index 46530e605f..395f8c42e0 100644 --- a/tests/Exceptionless.Tests/IntegrationTestsBase.cs +++ b/tests/Exceptionless.Tests/IntegrationTestsBase.cs @@ -170,7 +170,7 @@ await _configuration.Client.DeleteByQueryAsync(new DeleteByQueryRequest(indexes) _logger.LogTrace("Configured Indexes"); foreach (var index in _configuration.Indexes) - index.QueryParser.Configuration.MappingResolver.RefreshMapping(); + index.QueryParser.Configuration?.MappingResolver?.RefreshMapping(); var cacheClient = GetService(); await cacheClient.RemoveAllAsync(); diff --git a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs index 5dca853373..6cc00f7013 100644 --- a/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/CleanupDataJobTests.cs @@ -61,6 +61,7 @@ public async Task CanCleanupSuspendedTokens() await _job.RunAsync(TestCancellationToken); token = await _tokenRepository.GetByIdAsync(token.Id); + Assert.NotNull(token); Assert.True(token.IsSuspended); } diff --git a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs index f3c4bc8b5d..fb07d885e1 100644 --- a/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs +++ b/tests/Exceptionless.Tests/Jobs/EventPostJobTests.cs @@ -94,6 +94,7 @@ public async Task CanRunJob() public async Task CanRunJobWithDiscardedEventUsage() { var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId); + Assert.NotNull(organization); var usage = await _usageService.GetUsageAsync(organization.Id); Assert.Equal(0, usage.CurrentUsage.Total); @@ -121,10 +122,12 @@ public async Task CanRunJobWithDiscardedEventUsage() // Mark the stack as discarded var logStack = await _stackRepository.GetByIdAsync(logEvent.StackId); + Assert.NotNull(logStack); logStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(logStack, o => o.ImmediateConsistency()); var sessionStack = await _stackRepository.GetByIdAsync(sessionEvent.StackId); + Assert.NotNull(sessionStack); sessionStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(sessionStack, o => o.ImmediateConsistency()); diff --git a/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs b/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs index edd3d3ee27..62df528f39 100644 --- a/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs +++ b/tests/Exceptionless.Tests/Jobs/WorkItemHandlers/UpdateProjectNotificationSettingsWorkItemHandlerTests.cs @@ -217,7 +217,7 @@ public async Task RunUntilEmptyAsync_WithMoreThanOnePageOfProjects_CleansEveryPr await _workItemJob.RunUntilEmptyAsync(TestCancellationToken); // Assert - var refreshedProjects = await _projectRepository.GetByIdsAsync([firstProject.Id, ..additionalProjects.Select(project => project.Id)]); + var refreshedProjects = await _projectRepository.GetByIdsAsync([firstProject.Id, .. additionalProjects.Select(project => project.Id)]); Assert.Equal(56, refreshedProjects.Count); Assert.All(refreshedProjects, project => Assert.DoesNotContain(orphanedUserId, project.NotificationSettings.Keys)); } diff --git a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs index 7267bb590b..0636cefb97 100644 --- a/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/FixDuplicateStacksMigrationTests.cs @@ -31,7 +31,7 @@ public FixDuplicateStacksMigrationTests(ITestOutputHelper output, AppWebHostFact protected override void RegisterServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(new EmptyLock()); + services.AddSingleton(EmptyLock.Empty); base.RegisterServices(services); } @@ -71,8 +71,10 @@ public async Task WillMergeDuplicatedStacks() Assert.Single(results.Documents); var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedOriginalStack); Assert.False(updatedOriginalStack.IsDeleted); var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedDuplicateStack); Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); @@ -124,8 +126,10 @@ public async Task WillMergeToStackWithMostEvents() Assert.Single(results.Documents); var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedOriginalStack); Assert.True(updatedOriginalStack.IsDeleted); var updatedBiggerStack = await _stackRepository.GetByIdAsync(biggerStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedBiggerStack); Assert.False(updatedBiggerStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedBiggerStack.CreatedUtc); @@ -173,8 +177,10 @@ public async Task WillNotMergeDuplicatedDeletedStacks() Assert.Single(results.Documents); var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedOriginalStack); Assert.False(updatedOriginalStack.IsDeleted); var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); + Assert.NotNull(updatedDuplicateStack); Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); diff --git a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs index 5a5ea1cc70..7bca481c55 100644 --- a/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/SetStackDuplicateSignatureMigrationTests.cs @@ -25,7 +25,7 @@ public SetStackDuplicateSignatureMigrationTests(ITestOutputHelper output, AppWeb protected override void RegisterServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(new EmptyLock()); + services.AddSingleton(EmptyLock.Empty); base.RegisterServices(services); } @@ -46,6 +46,7 @@ public async Task WillSetStackDuplicateSignature() string expectedDuplicateSignature = $"{stack.ProjectId}:{stack.SignatureHash}"; var actualStack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(actualStack); Assert.NotEmpty(actualStack.ProjectId); Assert.NotEmpty(actualStack.SignatureHash); Assert.Equal($"{actualStack.ProjectId}:{actualStack.SignatureHash}", actualStack.DuplicateSignature); diff --git a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs index 6acdc33842..24a9c1aea9 100644 --- a/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs +++ b/tests/Exceptionless.Tests/Migrations/UpdateEventUsageMigrationTests.cs @@ -38,7 +38,7 @@ public UpdateEventUsageMigrationTests(ITestOutputHelper output, AppWebHostFactor protected override void RegisterServices(IServiceCollection services) { services.AddTransient(); - services.AddSingleton(new EmptyLock()); + services.AddSingleton(EmptyLock.Empty); base.RegisterServices(services); } @@ -66,6 +66,7 @@ public async Task ShouldPopulateUsageStats() int limit = organization.GetMaxEventsPerMonthWithBonus(TimeProvider); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); Assert.Equal(2, organization.Usage.Count); var previousMonthsUsage = organization.GetUsage(previousMonthUsageDate, TimeProvider); Assert.Equal(100, previousMonthsUsage.Total); @@ -75,6 +76,7 @@ public async Task ShouldPopulateUsageStats() Assert.Equal(limit, currentMonthsUsage.Limit); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); Assert.Equal(2, project.Usage.Count); previousMonthsUsage = project.GetUsage(previousMonthUsageDate); Assert.Equal(100, previousMonthsUsage.Total); diff --git a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs index beac5c5a1f..65164d881e 100644 --- a/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs +++ b/tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs @@ -599,6 +599,7 @@ public async Task SyncStackTagsAsync() Assert.NotNull(ev.StackId); var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(new[] { Tag1 }, stack.Tags.ToArray()); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -609,6 +610,7 @@ public async Task SyncStackTagsAsync() context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); Assert.False(context.HasError, context.ErrorMessage); stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -619,6 +621,7 @@ public async Task SyncStackTagsAsync() context = await _pipeline.RunAsync(ev, _organizationData.GenerateSampleOrganization(_billingManager, _plans), _projectData.GenerateSampleProject()); Assert.False(context.HasError, context.ErrorMessage); stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(new[] { Tag1, Tag2 }, stack.Tags.ToArray()); } @@ -641,9 +644,11 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.Empty(ev.Tags); var stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Empty(stack.Tags); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); + ev.Tags ??= []; ev.Tags.AddRange(Enumerable.Range(0, 100).Select(i => i.ToString())); await RefreshDataAsync(); @@ -657,6 +662,7 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.Equal(50, ev.Tags.Count); stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(50, stack.Tags.Count); ev = _eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.UtcNow); @@ -678,6 +684,7 @@ public async Task RemoveTagsExceedingLimitsWhileKeepingKnownTags() Assert.Contains(Event.KnownTags.Critical, ev.Tags); stack = await _stackRepository.GetByIdAsync(ev.StackId, o => o.Cache()); + Assert.NotNull(stack); Assert.Equal(50, stack.Tags.Count); Assert.DoesNotContain(new string('x', 150), stack.Tags); Assert.Contains(Event.KnownTags.Critical, stack.Tags); @@ -746,6 +753,7 @@ public async Task EnsureSingleRegressionAsync() Assert.NotNull(ev); var stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); stack.MarkFixed(null, TimeProvider); await _stackRepository.SaveAsync(stack, o => o.Cache()); @@ -819,6 +827,7 @@ public async Task EnsureVersionedRegressionAsync() Assert.NotNull(ev); var stack = await _stackRepository.GetByIdAsync(ev.StackId); + Assert.NotNull(stack); stack.MarkFixed(new SemanticVersion(1, 0, 1, ["rc2"]), TimeProvider); await _stackRepository.SaveAsync(stack, o => o.Cache()); @@ -982,7 +991,9 @@ public async Task WillHandleDiscardedStack() public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedStatus, bool expectedDiscard, string? stackFixedInVersion, string? eventSemanticVersion) { var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId, o => o.Cache()); + Assert.NotNull(organization); var project = await _projectRepository.GetByIdAsync(TestConstants.ProjectId, o => o.Cache()); + Assert.NotNull(project); var ev = _eventData.GenerateEvent(organizationId: organization.Id, projectId: project.Id, type: Event.KnownTypes.Log, source: "test", occurrenceDate: DateTimeOffset.Now); var context = await _pipeline.RunAsync(ev, organization, project); @@ -1018,6 +1029,7 @@ public async Task CanDiscardStackEventsBasedOnEventVersion(StackStatus expectedS public async Task WillNotDiscardStackEventsBasedOnEventVersionWithFreePlan(string stackFixedInVersion, string? eventSemanticVersion) { var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId3, o => o.Cache()); + Assert.NotNull(organization); var plans = GetService(); Assert.Equal(plans.FreePlan.Id, organization.PlanId); @@ -1136,7 +1148,8 @@ public async Task GeneratePerformanceDataAsync() foreach (var file in await storage.GetFileListAsync(Path.Combine("Exceptionless.Web", "storage", "q", "*"), cancellationToken: TestCancellationToken)) { - byte[] data = await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")); + byte[]? data = await storage.GetFileContentsRawAsync(Path.ChangeExtension(file.Path, ".payload")); + Assert.NotNull(data); var eventPostInfo = await storage.GetObjectAsync(file.Path, TestCancellationToken); if (!String.IsNullOrEmpty(eventPostInfo.ContentEncoding)) data = data.Decompress(eventPostInfo.ContentEncoding); diff --git a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs index c8e0cf9528..d13cdc6bc5 100644 --- a/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/EventRepositoryTests.cs @@ -117,10 +117,12 @@ public async Task GetPreviousEventIdInStackTestAsync() for (int i = 0; i < sortedIds.Count; i++) { _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); + var adjacentEvents = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1); + Assert.NotNull(adjacentEvents); if (i == 0) - Assert.Null((await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); + Assert.Null(adjacentEvents.Previous); else - Assert.Equal(sortedIds[i - 1].Item1, (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Previous); + Assert.Equal(sortedIds[i - 1].Item1, adjacentEvents.Previous); } } @@ -145,7 +147,9 @@ public async Task GetNextEventIdInStackTestAsync() for (int i = 0; i < sortedIds.Count; i++) { _logger.LogDebug("Current - {Id}: {Date}", sortedIds[i].Item1, sortedIds[i].Item2.ToLongTimeString()); - string? nextId = (await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1)).Next; + var adjacentEvents = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[i].Item1); + Assert.NotNull(adjacentEvents); + string? nextId = adjacentEvents.Next; if (i == sortedIds.Count - 1) Assert.Null(nextId); else @@ -163,6 +167,7 @@ public async Task CanGetPreviousAndNextEventIdWithFilterTestAsync() var sortedIds = _ids.OrderBy(t => t.Item2.Ticks).ThenBy(t => t.Item1).ToList(); var result = await _repository.GetPreviousAndNextEventIdsAsync(sortedIds[1].Item1); + Assert.NotNull(result); Assert.Equal(sortedIds[0].Item1, result.Previous); Assert.Equal(sortedIds[2].Item1, result.Next); } diff --git a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs index 5f8dd59dab..5cf55c0fbf 100644 --- a/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/ProjectRepositoryTests.cs @@ -40,6 +40,7 @@ public async Task IncrementNextSummaryEndOfDayTicksAsync() await RefreshDataAsync(); var updatedProject = await _repository.GetByIdAsync(project.Id); + Assert.NotNull(updatedProject); // TODO: Modified date isn't currently updated in the update scripts. //Assert.NotEqual(project.ModifiedUtc, updatedProject.ModifiedUtc); Assert.Equal(project.NextSummaryEndOfDayTicks + TimeSpan.TicksPerDay, updatedProject.NextSummaryEndOfDayTicks); @@ -142,7 +143,11 @@ public async Task CanRoundTripWithCaching() var actualCache = await _cache.GetAsync>>("Project:" + project.Id); Assert.True(actualCache.HasValue); - Assert.Equal(project.Name, actualCache.Value.Single().Document.Name); + Assert.NotNull(actualCache.Value); + var cachedDocs = actualCache.Value; + var cachedDoc = cachedDocs.Single(); + Assert.NotNull(cachedDoc.Document); + Assert.Equal(project.Name, cachedDoc.Document.Name); var actualCacheToken = actual.GetSlackToken(); Assert.Equal(token.AccessToken, actualCacheToken?.AccessToken); } diff --git a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs index bd765b8129..cad325c3a5 100644 --- a/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs +++ b/tests/Exceptionless.Tests/Repositories/StackRepositoryTests.cs @@ -40,7 +40,10 @@ public async Task CanGetSoftDeletedStack() await _repository.AddAsync(stack, o => o.ImmediateConsistency()); - var actual = _repository.GetByIdAsync(stack.Id, o => o.Cache("test")); + var actual = await _repository.GetByIdAsync(stack.Id, o => o.Cache("test")); + Assert.Null(actual); + + actual = await _repository.GetByIdAsync(stack.Id, o => o.Cache("test").IncludeSoftDeletes()); Assert.NotNull(actual); } @@ -147,6 +150,7 @@ public async Task CanIncrementEventCounterAsync() await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow, utcNow, 1); stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); Assert.Equal(1, stack.TotalOccurrences); Assert.Equal(utcNow, stack.FirstOccurrence); Assert.Equal(utcNow, stack.LastOccurrence); @@ -156,6 +160,7 @@ public async Task CanIncrementEventCounterAsync() await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.SubtractDays(1), utcNow.SubtractDays(1), 1); stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); Assert.Equal(2, stack.TotalOccurrences); Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); Assert.Equal(utcNow, stack.LastOccurrence); @@ -163,6 +168,7 @@ public async Task CanIncrementEventCounterAsync() await _repository.IncrementEventCounterAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, utcNow.AddDays(1), utcNow.AddDays(1), 1); stack = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(stack); Assert.Equal(3, stack.TotalOccurrences); Assert.Equal(utcNow.SubtractDays(1), stack.FirstOccurrence); Assert.Equal(utcNow.AddDays(1), stack.LastOccurrence); @@ -190,6 +196,7 @@ await _repository.SetEventCounterAsync( sendNotifications: false); var unchanged = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(unchanged); // Assert Assert.Equal(10, unchanged.TotalOccurrences); @@ -205,6 +212,7 @@ await _repository.SetEventCounterAsync( sendNotifications: false); var updated = await _repository.GetByIdAsync(stack.Id); + Assert.NotNull(updated); // Assert Assert.Equal(15, updated.TotalOccurrences); diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs index 756dca782f..7882f68340 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryTests.cs @@ -69,6 +69,10 @@ public async Task VerifyStackFilter(string filter, int expected, int? expectedIn var ctx = new ElasticQueryVisitorContext(); var stackFilter = await new EventStackFilter().GetStackFilterAsync(filter, ctx); + Assert.NotNull(stackFilter); + Assert.NotNull(stackFilter.Filter); + Assert.NotNull(stackFilter.InvertedFilter); + _logger.LogInformation("Finding Filter: {Filter}", stackFilter.Filter); var stacks = await _stackRepository.FindAsync(q => q.FilterExpression(stackFilter.Filter), o => o.SoftDeleteMode(SoftDeleteQueryMode.All).PageLimit(1000)); Assert.Equal(expected, stacks.Total); @@ -78,8 +82,8 @@ public async Task VerifyStackFilter(string filter, int expected, int? expectedIn long expectedInvert = expectedInverted ?? totalStacks - expected; Assert.Equal(expectedInvert, invertedStacks.Total); - var stackIds = new HashSet(stacks.Hits.Select(h => h.Id)); - var invertedStackIds = new HashSet(invertedStacks.Hits.Select(h => h.Id)); + var stackIds = new HashSet(stacks.Hits.Select(h => h.Id!)); + var invertedStackIds = new HashSet(invertedStacks.Hits.Select(h => h.Id!)); Assert.Empty(stackIds.Intersect(invertedStackIds)); } diff --git a/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs b/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs index ef07e44c4b..f49f760dd2 100644 --- a/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs +++ b/tests/Exceptionless.Tests/Search/EventStackFilterQueryVisitorTests.cs @@ -17,7 +17,7 @@ public async Task CanBuildStackFilter(FilterScenario scenario) var eventStackFilter = new EventStackFilter(); var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); - Assert.Equal(scenario.Stack, stackFilter.Filter.Trim()); + Assert.Equal(scenario.Stack, stackFilter?.Filter?.Trim()); } [Theory] @@ -28,7 +28,7 @@ public async Task CanBuildInvertedStackFilter(FilterScenario scenario) var eventStackFilter = new EventStackFilter(); var stackFilter = await eventStackFilter.GetStackFilterAsync(scenario.Source); - Assert.Equal(scenario.InvertedStack, stackFilter.InvertedFilter.Trim()); + Assert.Equal(scenario.InvertedStack, stackFilter?.InvertedFilter?.Trim()); } [Theory] @@ -39,7 +39,7 @@ public async Task CanBuildEventFilter(FilterScenario scenario) var eventStackFilter = new EventStackFilter(); string? stackFilter = await eventStackFilter.GetEventFilterAsync(scenario.Source); - Assert.Equal(scenario.Event, stackFilter.Trim()); + Assert.Equal(scenario.Event, stackFilter?.Trim()); } } diff --git a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs index c1cf61b5e9..5209f02d71 100644 --- a/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs +++ b/tests/Exceptionless.Tests/Search/PersistentEventQueryValidatorTests.cs @@ -54,7 +54,7 @@ public async Task CanProcessQueryAsync(string query, string expected, bool isVal { var context = new ElasticQueryVisitorContext { QueryType = QueryTypes.Query }; - IQueryNode result; + IQueryNode? result; try { result = await _parser.ParseAsync(query, context); @@ -68,8 +68,15 @@ public async Task CanProcessQueryAsync(string query, string expected, bool isVal return; } + if (result is null) + { + Assert.False(isValid, $"Expected query '{query}' to parse successfully."); + return; + } + // NOTE: we have to do this because we don't have access to the right query parser instance. result = await EventFieldsQueryVisitor.RunAsync(result, context); + Assert.NotNull(result); Assert.Equal(expected, await GenerateQueryVisitor.RunAsync(result, context)); var info = await _validator.ValidateQueryAsync(result); diff --git a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs index 9d9d6f51f5..ee344de170 100644 --- a/tests/Exceptionless.Tests/Serializer/SerializerTests.cs +++ b/tests/Exceptionless.Tests/Serializer/SerializerTests.cs @@ -39,7 +39,7 @@ public void CanDeserializeEventWithUnknownNamesAndProperties() Assert.Equal(8, ev.Data.Count); Assert.Equal("Hi", ev.Data.GetString("SomeString")); - Assert.False(ev.Data.GetBoolean("SomeBool")); + Assert.False(ev.Data!.GetBoolean("SomeBool")); Assert.Equal(1L, ev.Data["SomeNum"]); Assert.Equal(typeof(JObject), ev.Data["UnknownProp"]?.GetType()); Assert.Equal(typeof(JObject), ev.Data["UnknownSerializedProp"]?.GetType()); @@ -123,6 +123,7 @@ public void CanDeserializeWebHook() Assert.Equal("{\"id\":\"test\",\"event_types\":[\"NewError\"],\"is_enabled\":true,\"version\":\"v2\",\"created_utc\":\"0001-01-01T00:00:00\"}", json); var model = _serializer.Deserialize(json); + Assert.NotNull(model); Assert.Equal(hook.Id, model.Id); Assert.Equal(hook.EventTypes, model.EventTypes); Assert.Equal(hook.Version, model.Version); @@ -301,7 +302,7 @@ public void SerializeToString_PrimitiveTypes_RoundtripCorrectly() Assert.Equal("true", _serializer.SerializeToString(true)); Assert.True(_serializer.Deserialize("true")); - string roundtripped = _serializer.Deserialize(_serializer.SerializeToString("hello")); + string? roundtripped = _serializer.Deserialize(_serializer.SerializeToString("hello")); Assert.Equal("hello", roundtripped); } diff --git a/tests/Exceptionless.Tests/Services/StackServiceTests.cs b/tests/Exceptionless.Tests/Services/StackServiceTests.cs index a20a959201..adf63aa306 100644 --- a/tests/Exceptionless.Tests/Services/StackServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/StackServiceTests.cs @@ -48,6 +48,7 @@ public async Task IncrementUsage_OnlyChangeCache() // Assert stack state has no change after increment usage stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.NotNull(stack); Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack.LastOccurrence <= DateTime.UtcNow); @@ -86,6 +87,7 @@ public async Task IncrementUsageConcurrently() // Assert stack state has no change after increment usage stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.NotNull(stack); Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack.LastOccurrence <= DateTime.UtcNow); @@ -96,6 +98,7 @@ public async Task IncrementUsageConcurrently() Assert.Equal(100, await _cache.GetAsync(StackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); stack2 = await _stackRepository.GetByIdAsync(TestConstants.StackId2); + Assert.NotNull(stack2); Assert.Equal(0, stack2.TotalOccurrences); Assert.True(stack2.FirstOccurrence <= DateTime.UtcNow); Assert.True(stack2.LastOccurrence <= DateTime.UtcNow); @@ -138,6 +141,7 @@ public async Task CanSaveStackUsage() // Assert stack state after save stack usage stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); + Assert.NotNull(stack); Assert.Equal(10, stack.TotalOccurrences); Assert.Equal(minOccurrenceDate, stack.FirstOccurrence); Assert.Equal(maxOccurrenceDate, stack.LastOccurrence); diff --git a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs index fe37b06944..61b13936fc 100644 --- a/tests/Exceptionless.Tests/Services/UsageServiceTests.cs +++ b/tests/Exceptionless.Tests/Services/UsageServiceTests.cs @@ -69,6 +69,7 @@ await messageBus.SubscribeAsync(po => await _usageService.SavePendingUsageAsync(); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -77,6 +78,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.TooBig); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); Assert.Equal(eventsLeftInBucket, usage.Total); @@ -139,6 +141,7 @@ await messageBus.SubscribeAsync(po => await _usageService.SavePendingUsageAsync(); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); var overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1, overage.Blocked); @@ -151,6 +154,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.TooBig); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1, overage.Blocked); @@ -168,6 +172,7 @@ await messageBus.SubscribeAsync(po => await _usageService.SavePendingUsageAsync(); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); overage = organization.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1001, overage.Blocked); @@ -180,6 +185,7 @@ await messageBus.SubscribeAsync(po => Assert.Equal(0, usage.TooBig); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); overage = project.UsageHours.Single(); Assert.Equal(eventsLeftInBucket + 1, overage.Total); Assert.Equal(1001, overage.Blocked); @@ -204,6 +210,7 @@ public async Task CanIncrementBlockedAsync() await _usageService.SavePendingUsageAsync(); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -216,6 +223,7 @@ public async Task CanIncrementBlockedAsync() Assert.Equal(0, overage.TooBig); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -242,6 +250,7 @@ public async Task CanIncrementDiscardedAsync() await _usageService.SavePendingUsageAsync(); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -254,6 +263,7 @@ public async Task CanIncrementDiscardedAsync() Assert.Equal(0, overage.TooBig); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); @@ -280,6 +290,7 @@ public async Task CanIncrementTooBigAsync() await _usageService.SavePendingUsageAsync(); organization = await _organizationRepository.GetByIdAsync(organization.Id); + Assert.NotNull(organization); Assert.Single(organization.UsageHours); var usage = organization.Usage.Single(); Assert.Equal(organization.MaxEventsPerMonth, usage.Limit); @@ -288,6 +299,7 @@ public async Task CanIncrementTooBigAsync() Assert.Equal(1, usage.TooBig); project = await _projectRepository.GetByIdAsync(project.Id); + Assert.NotNull(project); Assert.Single(project.UsageHours); usage = project.Usage.Single(); Assert.Equal(0, usage.Total); diff --git a/tests/Exceptionless.Tests/Stats/AggregationTests.cs b/tests/Exceptionless.Tests/Stats/AggregationTests.cs index 9161d1bfdc..b7112dc313 100644 --- a/tests/Exceptionless.Tests/Stats/AggregationTests.cs +++ b/tests/Exceptionless.Tests/Stats/AggregationTests.cs @@ -51,8 +51,8 @@ public async Task CanGetCardinalityAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("cardinality:stack_id cardinality:id")); Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Cardinality("cardinality_stack_id").Value.GetValueOrDefault()); + Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Cardinality("cardinality_stack_id")?.Value.GetValueOrDefault() ?? 0); } [Fact] @@ -64,17 +64,21 @@ public async Task CanGetDateHistogramWithCardinalityAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("date:(date cardinality:id) cardinality:id")); Assert.Equal(eventCount, result.Total); - Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Total)); - Assert.Single(result.Aggregations.DateHistogram("date_date").Buckets.First().Aggregations); - Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); - Assert.Equal(eventCount, result.Aggregations.DateHistogram("date_date").Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault())); + var dateHistogram = result.Aggregations.DateHistogram("date_date"); + Assert.NotNull(dateHistogram); + Assert.NotNull(dateHistogram.Buckets); + Assert.NotEmpty(dateHistogram.Buckets); + Assert.Equal(eventCount, dateHistogram.Buckets.Sum(t => t.Total)); + Assert.Single(dateHistogram.Buckets.First().Aggregations); + Assert.Equal(eventCount, result.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(eventCount, dateHistogram.Buckets.Sum(t => t.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0)); var stacks = await _stackRepository.GetByOrganizationIdAsync(TestConstants.OrganizationId, o => o.PageLimit(100)); foreach (var stack in stacks.Documents) { var stackResult = await _eventRepository.CountAsync(q => q.FilterExpression($"stack:{stack.Id}").AggregationsExpression("cardinality:id")); Assert.Equal(stack.TotalOccurrences, stackResult.Total); - Assert.Equal(stack.TotalOccurrences, stackResult.Aggregations.Cardinality("cardinality_id").Value.GetValueOrDefault()); + Assert.Equal(stack.TotalOccurrences, stackResult.Aggregations.Cardinality("cardinality_id")?.Value.GetValueOrDefault() ?? 0); } } @@ -87,7 +91,10 @@ public async Task CanGetExcludedTermsAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression($"project:{TestConstants.ProjectId}").AggregationsExpression("terms:(is_first_occurrence @include:true)")); Assert.Equal(eventCount, result.Total); - Assert.Equal(await _stackRepository.CountAsync(), result.Aggregations.Terms("terms_is_first_occurrence").Buckets.First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); + + var termsAggregation = result.Aggregations.Terms("terms_is_first_occurrence"); + Assert.NotNull(termsAggregation?.Buckets); + Assert.Equal(await _stackRepository.CountAsync(), termsAggregation.Buckets.First(b => b.KeyAsString == Boolean.TrueString.ToLower()).Total.GetValueOrDefault()); } [Fact] @@ -104,11 +111,11 @@ public async Task CanGetNumericAggregationsAsync() Assert.Equal(values.Length, result.Total); Assert.Equal(5, result.Aggregations.Count); - Assert.Equal(50, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); - Assert.Equal(11, result.Aggregations.Cardinality("cardinality_value").Value.GetValueOrDefault()); - Assert.Equal(550, result.Aggregations.Sum("sum_value").Value.GetValueOrDefault()); - Assert.Equal(0, result.Aggregations.Min("min_value").Value.GetValueOrDefault()); - Assert.Equal(100, result.Aggregations.Max("max_value").Value.GetValueOrDefault()); + Assert.Equal(50, result.Aggregations.Average("avg_value")?.Value ?? 0); + Assert.Equal(11, result.Aggregations.Cardinality("cardinality_value")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(550, result.Aggregations.Sum("sum_value")?.Value ?? 0); + Assert.Equal(0, result.Aggregations.Min("min_value")?.Value ?? default); + Assert.Equal(100, result.Aggregations.Max("max_value")?.Value ?? default); } [Fact] @@ -120,10 +127,13 @@ public async Task CanGetTagTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:tags")); Assert.Equal(eventCount, result.Total); + + var termsAggregation = result.Aggregations.Terms("terms_tags"); + Assert.NotNull(termsAggregation?.Buckets); // each event can be in multiple tag buckets since an event can have up to 3 sample tags - Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); - Assert.InRange(result.Aggregations.Terms("terms_tags").Buckets.Count, 1, TestConstants.EventTags.Count); - foreach (var term in result.Aggregations.Terms("terms_tags").Buckets) + Assert.InRange(termsAggregation.Buckets.Sum(t => t.Total.GetValueOrDefault()), eventCount, eventCount * 3); + Assert.InRange(termsAggregation.Buckets.Count, 1, TestConstants.EventTags.Count); + foreach (var term in termsAggregation.Buckets) Assert.InRange(term.Total.GetValueOrDefault(), 1, eventCount); } @@ -137,7 +147,7 @@ public async Task CanGetVersionTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:version")); Assert.Equal(eventCount, result.Total); // NOTE: The events are created without a version. - Assert.Empty(result.Aggregations.Terms("terms_version").Buckets); + Assert.Empty(result.Aggregations.Terms("terms_version")?.Buckets ?? []); } [Fact] @@ -152,10 +162,14 @@ public async Task CanGetStackIdTermAggregationsAsync() Assert.Equal(eventCount, result.Total); var termsAggregation = result.Aggregations.Terms("terms_stack_id"); - Assert.Equal(eventCount, termsAggregation.Buckets.Sum(b1 => b1.Total.GetValueOrDefault()) + (long)termsAggregation.Data["SumOtherDocCount"]); + Assert.NotNull(termsAggregation?.Buckets); + Assert.NotNull(termsAggregation.Data); + Assert.Equal(eventCount, termsAggregation.Buckets.Sum(b1 => b1.Total.GetValueOrDefault()) + (long)(termsAggregation.Data["SumOtherDocCount"] ?? 0)); foreach (var term in termsAggregation.Buckets) { - Assert.Equal(1, term.Aggregations.Terms("terms_is_first_occurrence").Buckets.Sum(b => b.Total.GetValueOrDefault())); + var firstOccurrenceBuckets = term.Aggregations.Terms("terms_is_first_occurrence"); + Assert.NotNull(firstOccurrenceBuckets?.Buckets); + Assert.Equal(1, firstOccurrenceBuckets.Buckets.Sum(b => b.Total.GetValueOrDefault())); } } @@ -172,16 +186,19 @@ public async Task CanGetStackIdTermMinMaxAggregationsAsync() Assert.Equal(eventCount, result.Total); var termsAggregation = result.Aggregations.Terms("terms_stack_id"); + Assert.NotNull(termsAggregation); + Assert.NotNull(termsAggregation.Buckets); + Assert.NotEmpty(termsAggregation.Buckets); var largestStackBucket = termsAggregation.Buckets.First(); var events = await _eventRepository.FindAsync(q => q.FilterExpression($"stack:{largestStackBucket.Key}"), o => o.PageLimit(eventCount)); Assert.Equal(largestStackBucket.Total.GetValueOrDefault(), events.Total); var oldestEvent = events.Documents.OrderBy(e => e.Date).First(); - Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("min_date").Value.Floor(TimeSpan.FromMilliseconds(1))); + Assert.Equal(oldestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Min("min_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); var newestEvent = events.Documents.OrderByDescending(e => e.Date).First(); - Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), largestStackBucket.Aggregations.Min("max_date").Value.Floor(TimeSpan.FromMilliseconds(1))); + Assert.Equal(newestEvent.Date.UtcDateTime.Floor(TimeSpan.FromMilliseconds(1)), (largestStackBucket.Aggregations.Max("max_date")?.Value ?? default).Floor(TimeSpan.FromMilliseconds(1))); } [Fact] @@ -193,8 +210,11 @@ public async Task CanGetProjectTermAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.AggregationsExpression("terms:project_id")); Assert.Equal(eventCount, result.Total); - Assert.InRange(result.Aggregations.Terms("terms_project_id").Buckets.Count, 1, 3); // 3 sample projects - Assert.Equal(eventCount, result.Aggregations.Terms("terms_project_id").Buckets.Sum(t => t.Total.GetValueOrDefault())); + + var termsAggregation = result.Aggregations.Terms("terms_project_id"); + Assert.NotNull(termsAggregation?.Buckets); + Assert.InRange(termsAggregation.Buckets.Count, 1, 3); // 3 sample projects + Assert.Equal(eventCount, termsAggregation.Buckets.Sum(t => t.Total.GetValueOrDefault())); } [Fact] @@ -205,8 +225,8 @@ public async Task CanGetSessionAggregationsAsync() var result = await _eventRepository.CountAsync(q => q.FilterExpression("type:session").AggregationsExpression("avg:value cardinality:user")); Assert.Equal(3, result.Total); - Assert.Equal(3, result.Aggregations.Cardinality("cardinality_user").Value.GetValueOrDefault()); - Assert.Equal(3600.0 / result.Total, result.Aggregations.Average("avg_value").Value.GetValueOrDefault()); + Assert.Equal(3, result.Aggregations.Cardinality("cardinality_user")?.Value.GetValueOrDefault() ?? 0); + Assert.Equal(3600.0 / result.Total, result.Aggregations.Average("avg_value")?.Value ?? 0); } private async Task CreateDataAsync(int eventCount = 0, bool multipleProjects = true) diff --git a/tests/Exceptionless.Tests/Utility/DataBuilder.cs b/tests/Exceptionless.Tests/Utility/DataBuilder.cs index d5997b6dca..d2401a5362 100644 --- a/tests/Exceptionless.Tests/Utility/DataBuilder.cs +++ b/tests/Exceptionless.Tests/Utility/DataBuilder.cs @@ -188,6 +188,7 @@ public EventDataBuilder Source(string source) public EventDataBuilder Tag(params string[] tags) { + _event.Tags ??= []; _event.Tags.AddRange(tags); return this; } @@ -218,13 +219,14 @@ public EventDataBuilder RequestInfo(RequestInfo requestInfo) public EventDataBuilder RequestInfo(string json) { - _event.AddRequestInfo(_serializer.Deserialize(json)); + var requestInfo = _serializer.Deserialize(json) ?? throw new InvalidOperationException("Unable to deserialize request info."); + _event.AddRequestInfo(requestInfo); return this; } public EventDataBuilder RequestInfoSample(Action? requestMutator = null) { - var requestInfo = _serializer.Deserialize(_sampleRequestInfo); + var requestInfo = _serializer.Deserialize(_sampleRequestInfo) ?? throw new InvalidOperationException("Unable to deserialize sample request info."); requestMutator?.Invoke(requestInfo); _event.AddRequestInfo(requestInfo); diff --git a/tests/Exceptionless.Tests/Utility/EventData.cs b/tests/Exceptionless.Tests/Utility/EventData.cs index d3da36cead..c4f3e9f12a 100644 --- a/tests/Exceptionless.Tests/Utility/EventData.cs +++ b/tests/Exceptionless.Tests/Utility/EventData.cs @@ -222,6 +222,6 @@ public async Task CreateSearchDataAsync(bool updateDates = false) await _eventRepository.AddAsync(events, o => o.ImmediateConsistency()); } - _configuration.Events.QueryParser.Configuration.MappingResolver.RefreshMapping(); + _configuration.Events.QueryParser.Configuration?.MappingResolver?.RefreshMapping(); } }