public async Task CanCreateUpdateRemoveAsync() { await ResetAsync(); await _repository.RemoveAllAsync(); await _client.RefreshAsync(); Assert.Equal(0, await _repository.CountAsync()); var stack = StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId); Assert.Null(stack.Id); await _repository.AddAsync(stack); Assert.NotNull(stack.Id); await _client.RefreshAsync(); stack = await _repository.GetByIdAsync(stack.Id); Assert.NotNull(stack); stack.Description = "New Description"; await _repository.SaveAsync(stack); await _repository.RemoveAsync(stack.Id); }
public async Task WillMarkAutoSessionHeartbeatStackHidden() { var firstEventDate = SystemClock.OffsetNow.Subtract(TimeSpan.FromMinutes(5)); var events = new List <PersistentEvent> { GenerateEvent(firstEventDate.AddSeconds(10), "*****@*****.**", Event.KnownTypes.SessionHeartbeat) }; var contexts = await _pipeline.RunAsync(events); Assert.False(contexts.Any(c => c.HasError)); Assert.Equal(0, contexts.Count(c => c.IsCancelled)); Assert.Equal(1, contexts.Count(c => c.IsProcessed)); await _configuration.Client.RefreshAsync(); var results = await _eventRepository.GetAllAsync(new SortingOptions().WithField(Fields.Date)); Assert.Equal(2, results.Total); Assert.Equal(1, results.Documents.Count(e => e.IsSessionStart())); var sessionHeartbeat = results.Documents.Single(e => e.IsSessionHeartbeat()); Assert.NotNull(sessionHeartbeat); Assert.True(sessionHeartbeat.IsHidden); var stack = await _stackRepository.GetByIdAsync(sessionHeartbeat.StackId); Assert.NotNull(stack); Assert.True(stack.IsHidden); stack = await _stackRepository.GetByIdAsync(results.Documents.First(e => !e.IsSessionHeartbeat()).StackId); Assert.NotNull(stack); Assert.False(stack.IsHidden); }
public async Task SyncStackTagsAsync() { await ResetAsync(); const string Tag1 = "Tag One"; const string Tag2 = "Tag Two"; const string Tag2_Lowercase = "tag two"; PersistentEvent ev = EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.Now); ev.Tags.Add(Tag1); var pipeline = IoC.GetInstance <EventPipeline>(); await pipeline.RunAsync(ev); await _client.RefreshAsync(); ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); Assert.NotNull(ev.StackId); var stack = await _stackRepository.GetByIdAsync(ev.StackId, true); Assert.Equal(new TagSet { Tag1 }, stack.Tags); ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.Now); ev.Tags.Add(Tag2); await pipeline.RunAsync(ev); await _client.RefreshAsync(); stack = await _stackRepository.GetByIdAsync(ev.StackId, true); Assert.Equal(new TagSet { Tag1, Tag2 }, stack.Tags); ev = EventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, generateTags: false, occurrenceDate: DateTime.Now); ev.Tags.Add(Tag2_Lowercase); await pipeline.RunAsync(ev); await _client.RefreshAsync(); stack = await _stackRepository.GetByIdAsync(ev.StackId, true); Assert.Equal(new TagSet { Tag1, Tag2 }, stack.Tags); }
public async Task IncrementUsage_OnlyChangeCache() { var stack = await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId), o => o.ImmediateConsistency()); // Assert stack state in elasticsearch before increment usage Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); // Assert state in cache before increment usage Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); Assert.Equal(DateTime.MinValue, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(0, await _cache.GetAsync <long>(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); var occurrenceSet = await _cache.GetListAsync <(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); Assert.True(occurrenceSet.IsNull || !occurrenceSet.HasValue || occurrenceSet.Value.Count == 0); var firstUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); await RefreshDataAsync(); await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, firstUtcNow, firstUtcNow, 1); // Assert stack state has no change after increment usage stack = await _stackRepository.GetByIdAsync(TestConstants.StackId); Assert.Equal(0, stack.TotalOccurrences); Assert.True(stack.FirstOccurrence <= SystemClock.UtcNow); Assert.True(stack.LastOccurrence <= SystemClock.UtcNow); // Assert state in cache has been changed after increment usage Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(1, await _cache.GetAsync <long>(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); occurrenceSet = await _cache.GetListAsync <(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); Assert.Single(occurrenceSet.Value); var secondUtcNow = SystemClock.UtcNow.Floor(TimeSpan.FromMilliseconds(1)); await RefreshDataAsync(); await _stackService.IncrementStackUsageAsync(TestConstants.OrganizationId, TestConstants.ProjectId, stack.Id, secondUtcNow, secondUtcNow, 2); // Assert state in cache has been changed after increment usage again Assert.Equal(firstUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMinDateCacheKey(stack.Id))); Assert.Equal(secondUtcNow, await _cache.GetUnixTimeMillisecondsAsync(_stackService.GetStackOccurrenceMaxDateCacheKey(stack.Id))); Assert.Equal(3, await _cache.GetAsync <long>(_stackService.GetStackOccurrenceCountCacheKey(stack.Id), 0)); occurrenceSet = await _cache.GetListAsync <(string OrganizationId, string ProjectId, string StackId)>(_stackService.GetStackOccurrenceSetCacheKey()); Assert.Single(occurrenceSet.Value); }
public async Task WillSetStackDuplicateSignature() { var stack = StackData.GenerateStack(); stack.DuplicateSignature = null; stack = await _repository.AddAsync(stack, o => o.ImmediateConsistency()); Assert.NotEmpty(stack.ProjectId); Assert.NotEmpty(stack.SignatureHash); Assert.Null(stack.DuplicateSignature); var migration = GetService <SetStackDuplicateSignature>(); var context = new MigrationContext(GetService <ILock>(), _logger, CancellationToken.None); await migration.RunAsync(context); string expectedDuplicateSignature = $"{stack.ProjectId}:{stack.SignatureHash}"; var actualStack = await _repository.GetByIdAsync(stack.Id); Assert.NotEmpty(actualStack.ProjectId); Assert.NotEmpty(actualStack.SignatureHash); Assert.Equal($"{actualStack.ProjectId}:{actualStack.SignatureHash}", actualStack.DuplicateSignature); var results = await _repository.FindAsync(q => q.ElasticFilter(Query <Stack> .Term(s => s.DuplicateSignature, expectedDuplicateSignature))); Assert.Single(results.Documents); }
public async Task CanMarkAsRegressedAsync() { var stack = await _repository.AddAsync(StackData.GenerateStack(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1)), o => o.ImmediateConsistency()); Assert.NotNull(stack); Assert.False(stack.Status == Core.Models.StackStatus.Regressed); Assert.NotNull(stack.DateFixed); await _repository.MarkAsRegressedAsync(stack.Id); stack = await _repository.GetByIdAsync(stack.Id); Assert.NotNull(stack); Assert.True(stack.Status == Core.Models.StackStatus.Regressed); Assert.NotNull(stack.DateFixed); }
public async Task CanSearchByNonPremiumFields() { var ev = await SubmitErrorEventAsync(); Assert.NotNull(ev.StackId); var stack = await _stackRepository.GetByIdAsync(ev.StackId); Assert.NotNull(stack); Assert.False(stack.IsFixed()); var result = await SendRequestAsAsync <IReadOnlyCollection <Stack> >(r => r .AsGlobalAdminUser() .AppendPath("stacks") .QueryString("f", "status:fixed") .StatusCodeShouldBeOk()); Assert.NotNull(result); Assert.Equal(1, result.Count); }
public async Task CanCleanupSoftDeletedOrganization() { var organization = OrganizationData.GenerateSampleOrganization(_billingManager, _plans); organization.IsDeleted = true; await _organizationRepository.AddAsync(organization, o => o.ImmediateConsistency()); var project = await _projectRepository.AddAsync(ProjectData.GenerateSampleProject(), o => o.ImmediateConsistency()); var stack = await _stackRepository.AddAsync(StackData.GenerateSampleStack(), o => o.ImmediateConsistency()); var persistentEvent = await _eventRepository.AddAsync(EventData.GenerateEvent(organization.Id, project.Id, stack.Id), o => o.ImmediateConsistency()); await _job.RunAsync(); Assert.Null(await _organizationRepository.GetByIdAsync(organization.Id, o => o.IncludeSoftDeletes())); Assert.Null(await _projectRepository.GetByIdAsync(project.Id, o => o.IncludeSoftDeletes())); Assert.Null(await _stackRepository.GetByIdAsync(stack.Id, o => o.IncludeSoftDeletes())); Assert.Null(await _eventRepository.GetByIdAsync(persistentEvent.Id, o => o.IncludeSoftDeletes())); }
public async Task WillMergeDuplicatedStacks() { var utcNow = SystemClock.UtcNow; var originalStack = StackData.GenerateStack(); originalStack.Id = ObjectId.GenerateNewId().ToString(); originalStack.TotalOccurrences = 100; var duplicateStack = originalStack.DeepClone(); duplicateStack.Id = ObjectId.GenerateNewId().ToString(); duplicateStack.Status = StackStatus.Fixed; duplicateStack.TotalOccurrences = 10; duplicateStack.LastOccurrence = originalStack.LastOccurrence.AddMinutes(1); duplicateStack.SnoozeUntilUtc = originalStack.SnoozeUntilUtc = null; duplicateStack.DateFixed = duplicateStack.LastOccurrence.AddMinutes(1); duplicateStack.Tags.Add("stack2"); duplicateStack.References.Add("stack2"); duplicateStack.OccurrencesAreCritical = true; originalStack = await _stackRepository.AddAsync(originalStack, o => o.ImmediateConsistency()); duplicateStack = await _stackRepository.AddAsync(duplicateStack, o => o.ImmediateConsistency()); await _eventRepository.AddAsync(EventData.GenerateEvents(count: 100, stackId: originalStack.Id), o => o.ImmediateConsistency()); await _eventRepository.AddAsync(EventData.GenerateEvents(count: 10, stackId: duplicateStack.Id), o => o.ImmediateConsistency()); var results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query <Stack> .Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Equal(2, results.Total); var migration = GetService <FixDuplicateStacks>(); var context = new MigrationContext(GetService <ILock>(), _logger, CancellationToken.None); await migration.RunAsync(context); await RefreshDataAsync(); results = await _stackRepository.FindAsync(q => q.ElasticFilter(Query <Stack> .Term(s => s.DuplicateSignature, originalStack.DuplicateSignature))); Assert.Single(results.Documents); var updatedOriginalStack = await _stackRepository.GetByIdAsync(originalStack.Id, o => o.IncludeSoftDeletes()); Assert.False(updatedOriginalStack.IsDeleted); var updatedDuplicateStack = await _stackRepository.GetByIdAsync(duplicateStack.Id, o => o.IncludeSoftDeletes()); Assert.True(updatedDuplicateStack.IsDeleted); Assert.Equal(originalStack.CreatedUtc, updatedOriginalStack.CreatedUtc); Assert.Equal(110, updatedOriginalStack.TotalOccurrences); Assert.Equal(StackStatus.Fixed, updatedOriginalStack.Status); Assert.Equal(duplicateStack.LastOccurrence, updatedOriginalStack.LastOccurrence); Assert.Null(updatedOriginalStack.SnoozeUntilUtc); Assert.Equal(duplicateStack.DateFixed, updatedOriginalStack.DateFixed); Assert.Equal(originalStack.Tags.Count + 1, updatedOriginalStack.Tags.Count); Assert.Contains("stack2", updatedOriginalStack.Tags); Assert.Equal(originalStack.References.Count + 1, updatedOriginalStack.References.Count); Assert.Contains("stack2", updatedOriginalStack.References); Assert.True(updatedOriginalStack.OccurrencesAreCritical); }
public async Task <IHttpActionResult> GetByStackAsync(string stackId, string filter = null, string time = null, string offset = null) { if (String.IsNullOrEmpty(stackId)) { return(NotFound()); } Stack stack = await _stackRepository.GetByIdAsync(stackId); if (stack == null || !CanAccessOrganization(stack.OrganizationId)) { return(NotFound()); } return(await GetInternalAsync(String.Concat("stack:", stackId), filter, time, offset)); }
public async Task <IHttpActionResult> GetByStackAsync(string stackId, string filter = null, string sort = null, string time = null, string offset = null, string mode = null, int page = 1, int limit = 10) { if (String.IsNullOrEmpty(stackId)) { return(NotFound()); } var stack = await _stackRepository.GetByIdAsync(stackId, true); if (stack == null || !CanAccessOrganization(stack.OrganizationId)) { return(NotFound()); } return(await GetInternalAsync(String.Concat("stack:", stackId), filter, sort, time, offset, mode, page, limit)); }
protected async Task <IReadOnlyCollection <Organization> > GetSelectedOrganizationsAsync(IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IStackRepository stackRepository, string filter = null) { var associatedOrganizationIds = GetAssociatedOrganizationIds(); if (associatedOrganizationIds.Count == 0) { return(EmptyOrganizations); } if (!String.IsNullOrEmpty(filter)) { var scope = GetFilterScopeVisitor.Run(filter); if (scope.IsScopable) { Organization organization = null; if (scope.OrganizationId != null) { organization = await organizationRepository.GetByIdAsync(scope.OrganizationId, o => o.Cache()); } else if (scope.ProjectId != null) { var project = await projectRepository.GetByIdAsync(scope.ProjectId, o => o.Cache()); if (project != null) { organization = await organizationRepository.GetByIdAsync(project.OrganizationId, o => o.Cache()); } } else if (scope.StackId != null) { var stack = await stackRepository.GetByIdAsync(scope.StackId, o => o.Cache()); if (stack != null) { organization = await organizationRepository.GetByIdAsync(stack.OrganizationId, o => o.Cache()); } } if (organization != null) { if (associatedOrganizationIds.Contains(organization.Id) || Request.IsGlobalAdmin()) { return new[] { organization } }
public override async Task <object> CreateFromEventAsync(WebHookDataContext ctx) { if (ctx.Event == null) { throw new ArgumentException("Event cannot be null."); } if (ctx.Project == null) { ctx.Project = await _projectRepository.GetByIdAsync(ctx.Event.ProjectId, o => o.Cache()).AnyContext(); } if (ctx.Project == null) { throw new ArgumentException("Project not found."); } if (ctx.Organization == null) { ctx.Organization = await _organizationRepository.GetByIdAsync(ctx.Event.OrganizationId, o => o.Cache()).AnyContext(); } if (ctx.Organization == null) { throw new ArgumentException("Organization not found."); } if (ctx.Stack == null) { ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId).AnyContext(); } if (ctx.Stack == null) { throw new ArgumentException("Stack not found."); } return(null); }
public async Task CanMarkAsRegressedAsync() { await _repository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1))); var stack = await _repository.GetByIdAsync(TestConstants.StackId); Assert.NotNull(stack); Assert.False(stack.IsRegressed); Assert.NotNull(stack.DateFixed); await RefreshDataAsync(); await _repository.MarkAsRegressedAsync(TestConstants.StackId); stack = await _repository.GetByIdAsync(TestConstants.StackId); Assert.NotNull(stack); Assert.True(stack.IsRegressed); Assert.NotNull(stack.DateFixed); }
public override async Task ProcessBatchAsync(ICollection <EventContext> contexts) { var stacks = new Dictionary <string, Tuple <bool, Stack> >(); foreach (var ctx in contexts) { if (String.IsNullOrEmpty(ctx.Event.StackId)) { // only add default signature info if no other signature info has been added if (ctx.StackSignatureData.Count == 0) { ctx.StackSignatureData.AddItemIfNotEmpty("Type", ctx.Event.Type); ctx.StackSignatureData.AddItemIfNotEmpty("Source", ctx.Event.Source); } string signatureHash = ctx.StackSignatureData.Values.ToSHA1(); ctx.SignatureHash = signatureHash; if (stacks.TryGetValue(signatureHash, out var value)) { ctx.Stack = value.Item2; } else { ctx.Stack = await _stackRepository.GetStackBySignatureHashAsync(ctx.Event.ProjectId, signatureHash).AnyContext(); if (ctx.Stack != null) { stacks.Add(signatureHash, Tuple.Create(false, ctx.Stack)); } } if (ctx.Stack == null) { _logger.LogTrace("Creating new event stack."); ctx.IsNew = true; string title = _formattingPluginManager.GetStackTitle(ctx.Event); var stack = new Stack { OrganizationId = ctx.Event.OrganizationId, ProjectId = ctx.Event.ProjectId, SignatureInfo = new SettingsDictionary(ctx.StackSignatureData), SignatureHash = signatureHash, DuplicateSignature = ctx.Event.ProjectId + ":" + signatureHash, Title = title?.Truncate(1000), Tags = ctx.Event.Tags ?? new TagSet(), Type = ctx.Event.Type, TotalOccurrences = 1, FirstOccurrence = ctx.Event.Date.UtcDateTime, LastOccurrence = ctx.Event.Date.UtcDateTime }; if (ctx.Event.Type == Event.KnownTypes.Session) { stack.Status = StackStatus.Ignored; } ctx.Stack = stack; stacks.Add(signatureHash, Tuple.Create(true, ctx.Stack)); } } else { ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId, o => o.Cache()).AnyContext(); if (ctx.Stack == null || ctx.Stack.ProjectId != ctx.Event.ProjectId) { ctx.SetError("Invalid StackId."); continue; } ctx.SignatureHash = ctx.Stack.SignatureHash; if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) { stacks.Add(ctx.Stack.SignatureHash, Tuple.Create(false, ctx.Stack)); } else { stacks[ctx.Stack.SignatureHash] = Tuple.Create(false, ctx.Stack); } if (ctx.Stack.Status == StackStatus.Discarded) { ctx.IsDiscarded = true; ctx.IsCancelled = true; } } if (!ctx.IsNew && ctx.Event.Tags != null && ctx.Event.Tags.Count > 0) { if (ctx.Stack.Tags == null) { ctx.Stack.Tags = new TagSet(); } var newTags = ctx.Event.Tags.Where(t => !ctx.Stack.Tags.Contains(t)).ToList(); if (newTags.Count > 0 || ctx.Stack.Tags.Count > 50 || ctx.Stack.Tags.Any(t => t.Length > 100)) { ctx.Stack.Tags.AddRange(newTags); ctx.Stack.Tags.RemoveExcessTags(); // make sure the stack gets saved if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) { stacks.Add(ctx.Stack.SignatureHash, Tuple.Create(true, ctx.Stack)); } else { stacks[ctx.Stack.SignatureHash] = Tuple.Create(true, stacks[ctx.Stack.SignatureHash].Item2); } } } ctx.Event.IsFirstOccurrence = ctx.IsNew; } var stacksToAdd = stacks.Where(kvp => kvp.Value.Item1 && String.IsNullOrEmpty(kvp.Value.Item2.Id)).Select(kvp => kvp.Value.Item2).ToList(); if (stacksToAdd.Count > 0) { await _stackRepository.AddAsync(stacksToAdd, o => o.Cache().Notifications(stacksToAdd.Count == 1)).AnyContext(); if (stacksToAdd.Count > 1) { await _publisher.PublishAsync(new EntityChanged { ChangeType = ChangeType.Added, Type = StackTypeName, Data = { { ExtendedEntityChanged.KnownKeys.OrganizationId, contexts.First().Organization.Id }, { ExtendedEntityChanged.KnownKeys.ProjectId, contexts.First().Project.Id } } }).AnyContext(); } } var stacksToSave = stacks.Where(kvp => kvp.Value.Item1 && !String.IsNullOrEmpty(kvp.Value.Item2.Id)).Select(kvp => kvp.Value.Item2).ToList(); if (stacksToSave.Count > 0) { await _stackRepository.SaveAsync(stacksToSave, o => o.Cache().Notifications(false)).AnyContext(); // notification will get sent later in the update stats step } // Set stack ids after they have been saved and created contexts.ForEach(ctx => { ctx.Event.StackId = ctx.Stack?.Id; }); }
protected override async Task <JobResult> ProcessQueueEntryAsync(JobQueueEntryContext <EventNotificationWorkItem> context) { var eventModel = await _eventRepository.GetByIdAsync(context.QueueEntry.Value.EventId).AnyContext(); if (eventModel == null) { return(JobResult.FailedWithMessage($"Could not load event: {context.QueueEntry.Value.EventId}")); } var eventNotification = new EventNotification(context.QueueEntry.Value, eventModel); bool shouldLog = eventNotification.Event.ProjectId != Settings.Current.InternalProjectId; int emailsSent = 0; Logger.Trace().Message("Process notification: project={0} event={1} stack={2}", eventNotification.Event.ProjectId, eventNotification.Event.Id, eventNotification.Event.StackId).WriteIf(shouldLog); var project = await _projectRepository.GetByIdAsync(eventNotification.Event.ProjectId, true).AnyContext(); if (project == null) { return(JobResult.FailedWithMessage($"Could not load project: {eventNotification.Event.ProjectId}.")); } Logger.Trace().Message($"Loaded project: name={project.Name}").WriteIf(shouldLog); var organization = await _organizationRepository.GetByIdAsync(project.OrganizationId, true).AnyContext(); if (organization == null) { return(JobResult.FailedWithMessage($"Could not load organization: {project.OrganizationId}")); } Logger.Trace().Message($"Loaded organization: {organization.Name}").WriteIf(shouldLog); var stack = await _stackRepository.GetByIdAsync(eventNotification.Event.StackId).AnyContext(); if (stack == null) { return(JobResult.FailedWithMessage($"Could not load stack: {eventNotification.Event.StackId}")); } if (!organization.HasPremiumFeatures) { Logger.Info().Message("Skipping \"{0}\" because organization \"{1}\" does not have premium features.", eventNotification.Event.Id, eventNotification.Event.OrganizationId).WriteIf(shouldLog); return(JobResult.Success); } if (stack.DisableNotifications || stack.IsHidden) { Logger.Info().Message("Skipping \"{0}\" because stack \"{1}\" notifications are disabled or stack is hidden.", eventNotification.Event.Id, eventNotification.Event.StackId).WriteIf(shouldLog); return(JobResult.Success); } if (context.CancellationToken.IsCancellationRequested) { return(JobResult.Cancelled); } Logger.Trace().Message("Loaded stack: title={0}", stack.Title).WriteIf(shouldLog); int totalOccurrences = stack.TotalOccurrences; // after the first 2 occurrences, don't send a notification for the same stack more then once every 30 minutes var lastTimeSentUtc = await _cacheClient.GetAsync <DateTime>(String.Concat("notify:stack-throttle:", eventNotification.Event.StackId), DateTime.MinValue).AnyContext(); if (totalOccurrences > 2 && !eventNotification.IsRegression && lastTimeSentUtc != DateTime.MinValue && lastTimeSentUtc > DateTime.UtcNow.AddMinutes(-30)) { Logger.Info().Message("Skipping message because of stack throttling: last sent={0} occurrences={1}", lastTimeSentUtc, totalOccurrences).WriteIf(shouldLog); return(JobResult.Success); } // don't send more than 10 notifications for a given project every 30 minutes var projectTimeWindow = TimeSpan.FromMinutes(30); string cacheKey = String.Concat("notify:project-throttle:", eventNotification.Event.ProjectId, "-", DateTime.UtcNow.Floor(projectTimeWindow).Ticks); long notificationCount = await _cacheClient.IncrementAsync(cacheKey, 1, projectTimeWindow).AnyContext(); if (notificationCount > 10 && !eventNotification.IsRegression) { Logger.Info().Project(eventNotification.Event.ProjectId).Message("Skipping message because of project throttling: count={0}", notificationCount).WriteIf(shouldLog); return(JobResult.Success); } if (context.CancellationToken.IsCancellationRequested) { return(JobResult.Cancelled); } foreach (var kv in project.NotificationSettings) { var settings = kv.Value; Logger.Trace().Message("Processing notification: user={0}", kv.Key).WriteIf(shouldLog); var user = await _userRepository.GetByIdAsync(kv.Key).AnyContext(); if (String.IsNullOrEmpty(user?.EmailAddress)) { Logger.Error().Message("Could not load user {0} or blank email address {1}.", kv.Key, user != null ? user.EmailAddress : "").Write(); continue; } if (!user.IsEmailAddressVerified) { Logger.Info().Message("User {0} with email address {1} has not been verified.", kv.Key, user != null ? user.EmailAddress : "").WriteIf(shouldLog); continue; } if (!user.EmailNotificationsEnabled) { Logger.Info().Message("User {0} with email address {1} has email notifications disabled.", kv.Key, user != null ? user.EmailAddress : "").WriteIf(shouldLog); continue; } if (!user.OrganizationIds.Contains(project.OrganizationId)) { Logger.Error().Message("Unauthorized user: project={0} user={1} organization={2} event={3}", project.Id, kv.Key, project.OrganizationId, eventNotification.Event.Id).Write(); continue; } Logger.Trace().Message("Loaded user: email={0}", user.EmailAddress).WriteIf(shouldLog); bool shouldReportNewError = settings.ReportNewErrors && eventNotification.IsNew && eventNotification.Event.IsError(); bool shouldReportCriticalError = settings.ReportCriticalErrors && eventNotification.IsCritical && eventNotification.Event.IsError(); bool shouldReportRegression = settings.ReportEventRegressions && eventNotification.IsRegression; bool shouldReportNewEvent = settings.ReportNewEvents && eventNotification.IsNew; bool shouldReportCriticalEvent = settings.ReportCriticalEvents && eventNotification.IsCritical; bool shouldReport = shouldReportNewError || shouldReportCriticalError || shouldReportRegression || shouldReportNewEvent || shouldReportCriticalEvent; Logger.Trace().Message("Settings: newerror={0} criticalerror={1} regression={2} new={3} critical={4}", settings.ReportNewErrors, settings.ReportCriticalErrors, settings.ReportEventRegressions, settings.ReportNewEvents, settings.ReportCriticalEvents).WriteIf(shouldLog); Logger.Trace().Message("Should process: newerror={0} criticalerror={1} regression={2} new={3} critical={4}", shouldReportNewError, shouldReportCriticalError, shouldReportRegression, shouldReportNewEvent, shouldReportCriticalEvent).WriteIf(shouldLog); var request = eventNotification.Event.GetRequestInfo(); // check for known bots if the user has elected to not report them if (shouldReport && !String.IsNullOrEmpty(request?.UserAgent)) { var botPatterns = project.Configuration.Settings.ContainsKey(SettingsDictionary.KnownKeys.UserAgentBotPatterns) ? project.Configuration.Settings.GetStringCollection(SettingsDictionary.KnownKeys.UserAgentBotPatterns).ToList() : new List <string>(); var info = await _parser.ParseAsync(request.UserAgent, eventNotification.Event.ProjectId).AnyContext(); if (info != null && info.Device.IsSpider || request.UserAgent.AnyWildcardMatches(botPatterns)) { shouldReport = false; Logger.Info().Message("Skipping because event is from a bot \"{0}\".", request.UserAgent).WriteIf(shouldLog); } } if (!shouldReport) { continue; } var model = new EventNotificationModel(eventNotification) { ProjectName = project.Name, TotalOccurrences = totalOccurrences }; // don't send notifications in non-production mode to email addresses that are not on the outbound email list. if (Settings.Current.WebsiteMode != WebsiteMode.Production && !Settings.Current.AllowedOutboundAddresses.Contains(v => user.EmailAddress.ToLowerInvariant().Contains(v))) { Logger.Info().Message("Skipping because email is not on the outbound list and not in production mode.").WriteIf(shouldLog); continue; } Logger.Trace().Message("Sending email to {0}...", user.EmailAddress).Write(); await _mailer.SendNoticeAsync(user.EmailAddress, model).AnyContext(); emailsSent++; Logger.Trace().Message("Done sending email.").WriteIf(shouldLog); } // if we sent any emails, mark the last time a notification for this stack was sent. if (emailsSent > 0) { await _cacheClient.SetAsync(String.Concat("notify:stack-throttle:", eventNotification.Event.StackId), DateTime.UtcNow, DateTime.UtcNow.AddMinutes(15)).AnyContext(); Logger.Info().Message("Notifications sent: event={0} stack={1} count={2}", eventNotification.Event.Id, eventNotification.Event.StackId, emailsSent).WriteIf(shouldLog); } return(JobResult.Success); }
public async Task CanMarkFixed(string version) { var ev = await SubmitErrorEventAsync(); Assert.NotNull(ev.StackId); Assert.False(ev.IsFixed); var stack = await _stackRepository.GetByIdAsync(ev.StackId); Assert.NotNull(stack); Assert.False(stack.IsFixed()); await _workItemQueue.DeleteQueueAsync(); var workIds = await SendRequestAsAsync <WorkInProgressResult>(r => r .Post() .AsGlobalAdminUser() .AppendPath($"stacks/{stack.Id}/mark-fixed") .QueryStringIf(() => !String.IsNullOrEmpty(version), "version", version) .StatusCodeShouldBeAccepted()); Assert.Single(workIds.Workers); var stats = await _workItemQueue.GetQueueStatsAsync(); Assert.Equal(1, stats.Enqueued); Assert.Equal(0, stats.Completed); var job = GetService <WorkItemJob>(); await job.RunAsync(); await RefreshDataAsync(); stats = await _workItemQueue.GetQueueStatsAsync(); Assert.Equal(1, stats.Completed); stack = await _stackRepository.GetByIdAsync(ev.StackId); Assert.NotNull(stack); Assert.True(stack.IsFixed()); ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); Assert.True(ev.IsFixed); workIds = await SendRequestAsAsync <WorkInProgressResult>(r => r .Delete() .AsGlobalAdminUser() .AppendPath($"stacks/{stack.Id}/mark-fixed") .QueryStringIf(() => !String.IsNullOrEmpty(version), "version", version) .StatusCodeShouldBeAccepted()); Assert.Single(workIds.Workers); stack = await _stackRepository.GetByIdAsync(ev.StackId); Assert.NotNull(stack); Assert.False(stack.IsFixed()); await job.RunAsync(); ev = await _eventRepository.GetByIdAsync(ev.Id); Assert.NotNull(ev); Assert.False(ev.IsFixed); }
public async Task CanRunJobWithDiscardedEventUsage() { Log.MinimumLevel = LogLevel.Debug; var organization = await _organizationRepository.GetByIdAsync(TestConstants.OrganizationId); var usage = await _usageService.GetUsageAsync(organization); Assert.Equal(0, usage.MonthlyTotal); usage = await _usageService.GetUsageAsync(organization); Assert.Equal(0, usage.MonthlyTotal); Assert.Equal(0, usage.MonthlyBlocked); var ev = GenerateEvent(type: Event.KnownTypes.Log, source: "test", userIdentity: "test1"); Assert.NotNull(await EnqueueEventPostAsync(ev)); var result = await _job.RunAsync(); Assert.True(result.IsSuccess); await RefreshDataAsync(); var events = await _eventRepository.GetAllAsync(); Assert.Equal(2, events.Total); var logEvent = events.Documents.Single(e => String.Equals(e.Type, Event.KnownTypes.Log)); Assert.NotNull(logEvent); var sessionEvent = events.Documents.Single(e => String.Equals(e.Type, Event.KnownTypes.Session)); Assert.NotNull(sessionEvent); usage = await _usageService.GetUsageAsync(organization); Assert.Equal(1, usage.MonthlyTotal); Assert.Equal(0, usage.MonthlyBlocked); // Mark the stack as discarded var logStack = await _stackRepository.GetByIdAsync(logEvent.StackId); logStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(logStack, o => o.ImmediateConsistency()); var sessionStack = await _stackRepository.GetByIdAsync(sessionEvent.StackId); sessionStack.Status = StackStatus.Discarded; await _stackRepository.SaveAsync(sessionStack, o => o.ImmediateConsistency()); // Verify job processed discarded events. Assert.NotNull(await EnqueueEventPostAsync(new List <PersistentEvent> { GenerateEvent(type: Event.KnownTypes.Session, sessionId: "abcdefghi"), GenerateEvent(type: Event.KnownTypes.Log, source: "test", sessionId: "abcdefghi"), GenerateEvent(type: Event.KnownTypes.Log, source: "test", userIdentity: "test3") })); result = await _job.RunAsync(); Assert.True(result.IsSuccess); await RefreshDataAsync(); events = await _eventRepository.GetAllAsync(); Assert.Equal(3, events.Total); usage = await _usageService.GetUsageAsync(organization); Assert.Equal(1, usage.MonthlyTotal); Assert.Equal(0, usage.MonthlyBlocked); }
public override async Task ProcessBatchAsync(ICollection <EventContext> contexts) { var stacks = new Dictionary <string, Tuple <bool, Stack> >(); foreach (var ctx in contexts) { if (String.IsNullOrEmpty(ctx.Event.StackId)) { // only add default signature info if no other signature info has been added if (ctx.StackSignatureData.Count == 0) { ctx.StackSignatureData.AddItemIfNotEmpty("Type", ctx.Event.Type); ctx.StackSignatureData.AddItemIfNotEmpty("Source", ctx.Event.Source); } string signatureHash = ctx.StackSignatureData.Values.ToSHA1(); ctx.SignatureHash = signatureHash; Tuple <bool, Stack> value; if (stacks.TryGetValue(signatureHash, out value)) { ctx.Stack = value.Item2; } else { ctx.Stack = await _stackRepository.GetStackBySignatureHashAsync(ctx.Event.ProjectId, signatureHash).AnyContext(); if (ctx.Stack != null) { stacks.Add(signatureHash, Tuple.Create(false, ctx.Stack)); } } if (ctx.Stack == null) { Logger.Trace().Message("Creating new event stack.").Write(); ctx.IsNew = true; string title = _formattingPluginManager.GetStackTitle(ctx.Event); var stack = new Stack { OrganizationId = ctx.Event.OrganizationId, ProjectId = ctx.Event.ProjectId, SignatureInfo = new SettingsDictionary(ctx.StackSignatureData), SignatureHash = signatureHash, Title = title?.Truncate(1000), Tags = ctx.Event.Tags ?? new TagSet(), Type = ctx.Event.Type, TotalOccurrences = 1, FirstOccurrence = ctx.Event.Date.UtcDateTime, LastOccurrence = ctx.Event.Date.UtcDateTime }; ctx.Stack = stack; stacks.Add(signatureHash, Tuple.Create(true, ctx.Stack)); } } else { ctx.Stack = await _stackRepository.GetByIdAsync(ctx.Event.StackId, true).AnyContext(); if (ctx.Stack == null || ctx.Stack.ProjectId != ctx.Event.ProjectId) { ctx.SetError("Invalid StackId."); continue; } ctx.SignatureHash = ctx.Stack.SignatureHash; if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) { stacks.Add(ctx.Stack.SignatureHash, Tuple.Create(false, ctx.Stack)); } else { stacks[ctx.Stack.SignatureHash] = Tuple.Create(false, ctx.Stack); } } if (!ctx.IsNew && ctx.Event.Tags != null && ctx.Event.Tags.Count > 0) { if (ctx.Stack.Tags == null) { ctx.Stack.Tags = new TagSet(); } List <string> newTags = ctx.Event.Tags.Where(t => !ctx.Stack.Tags.Contains(t)).ToList(); if (newTags.Count > 0) { ctx.Stack.Tags.AddRange(newTags); // make sure the stack gets saved if (!stacks.ContainsKey(ctx.Stack.SignatureHash)) { stacks.Add(ctx.Stack.SignatureHash, Tuple.Create(true, ctx.Stack)); } else { stacks[ctx.Stack.SignatureHash] = Tuple.Create(true, stacks[ctx.Stack.SignatureHash].Item2); } } } ctx.Event.IsFirstOccurrence = ctx.IsNew; // sync the fixed and hidden flags to the error occurrence ctx.Event.IsFixed = ctx.Stack.DateFixed.HasValue; ctx.Event.IsHidden = ctx.Stack.IsHidden; } var stacksToAdd = stacks.Where(kvp => kvp.Value.Item1 && String.IsNullOrEmpty(kvp.Value.Item2.Id)).Select(kvp => kvp.Value.Item2).ToList(); if (stacksToAdd.Count > 0) { await _stackRepository.AddAsync(stacksToAdd, true, sendNotification : stacksToAdd.Count == 1).AnyContext(); if (stacksToAdd.Count > 1) { await _publisher.PublishAsync(new ExtendedEntityChanged { ChangeType = ChangeType.Added, Type = typeof(Stack).Name, OrganizationId = contexts.First().Organization.Id, ProjectId = contexts.First().Project.Id }).AnyContext(); } } var stacksToSave = stacks.Where(kvp => kvp.Value.Item1 && !String.IsNullOrEmpty(kvp.Value.Item2.Id)).Select(kvp => kvp.Value.Item2).ToList(); if (stacksToSave.Count > 0) { await _stackRepository.SaveAsync(stacksToSave, true, sendNotification : false).AnyContext(); // notification will get sent later in the update stats step } // Set stack ids after they have been saved and created contexts.ForEach(ctx => { ctx.Event.StackId = ctx.Stack?.Id; }); }