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 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);
        }
Esempio n. 3
0
        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);
        }
Esempio n. 4
0
        private async Task CreateDataAsync()
        {
            var baseDate            = SystemClock.UtcNow.SubtractHours(1);
            var occurrenceDateStart = baseDate.AddMinutes(-30);
            var occurrenceDateMid   = baseDate;
            var occurrenceDateEnd   = baseDate.AddMinutes(30);

            await _stackRepository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, organizationId: TestConstants.OrganizationId, projectId: TestConstants.ProjectId));

            var occurrenceDates = new List <DateTime> {
                occurrenceDateStart,
                occurrenceDateEnd,
                baseDate.AddMinutes(-10),
                baseDate.AddMinutes(-20),
                occurrenceDateMid,
                occurrenceDateMid,
                occurrenceDateMid,
                baseDate.AddMinutes(20),
                baseDate.AddMinutes(10),
                baseDate.AddSeconds(1),
                occurrenceDateEnd,
                occurrenceDateStart
            };

            foreach (var date in occurrenceDates)
            {
                var ev = await _repository.AddAsync(EventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, stackId: TestConstants.StackId, occurrenceDate: date));

                _ids.Add(Tuple.Create(ev.Id, date));
            }

            await RefreshDataAsync();
        }
Esempio n. 5
0
        public static async Task CreateSearchDataAsync(IStackRepository stackRepository, JsonSerializer serializer, bool updateDates = false)
        {
            string path = Path.Combine("..", "..", "..", "Search", "Data");

            foreach (string file in Directory.GetFiles(path, "stack*.json", SearchOption.AllDirectories))
            {
                if (file.EndsWith("summary.json"))
                {
                    continue;
                }

                using (var stream = new FileStream(file, FileMode.Open)) {
                    using (var streamReader = new StreamReader(stream)) {
                        var stack = serializer.Deserialize(streamReader, typeof(Stack)) as Stack;
                        Assert.NotNull(stack);

                        if (updateDates)
                        {
                            stack.CreatedUtc     = stack.FirstOccurrence = SystemClock.UtcNow.SubtractDays(1);
                            stack.LastOccurrence = SystemClock.UtcNow;
                        }

                        await stackRepository.AddAsync(stack, o => o.ImmediateConsistency());
                    }
                }
            }
        }
Esempio n. 6
0
        private async Task CreateStacksAsync()
        {
            _configuration.DeleteIndexes(_client);
            _configuration.ConfigureIndexes(_client);

            var serializer = IoC.GetInstance <JsonSerializer>();

            foreach (var file in Directory.GetFiles(@"..\..\Search\Data\", "stack*.json", SearchOption.AllDirectories))
            {
                if (file.EndsWith("summary.json"))
                {
                    continue;
                }

                using (var stream = new FileStream(file, FileMode.Open)) {
                    using (var streamReader = new StreamReader(stream)) {
                        var stack = serializer.Deserialize(streamReader, typeof(Stack)) as Stack;
                        Assert.NotNull(stack);
                        await _repository.AddAsync(stack);
                    }
                }
            }

            await _client.RefreshAsync();
        }
Esempio n. 7
0
        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 CanGetByStackHashAsync()
        {
            long count  = _cache.Count;
            long hits   = _cache.Hits;
            long misses = _cache.Misses;

            var stack = await _repository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: SystemClock.UtcNow.SubtractMonths(1)), o => o.Cache());

            Assert.NotNull(stack?.Id);
            Assert.Equal(count + 2, _cache.Count);
            Assert.Equal(hits, _cache.Hits);
            Assert.Equal(misses, _cache.Misses);

            var result = await _repository.GetStackBySignatureHashAsync(stack.ProjectId, stack.SignatureHash);

            Assert.Equal(stack.ToJson(), result.ToJson());
            Assert.Equal(count + 2, _cache.Count);
            Assert.Equal(hits + 1, _cache.Hits);
            Assert.Equal(misses, _cache.Misses);
        }
    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 MarkAsRegressedTestAsync()
        {
            await RemoveDataAsync();

            await _repository.AddAsync(StackData.GenerateStack(id: TestConstants.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, dateFixed: DateTime.Now.SubtractMonths(1)));

            await _client.RefreshAsync();

            var stack = await _repository.GetByIdAsync(TestConstants.StackId);

            Assert.NotNull(stack);
            Assert.False(stack.IsRegressed);
            Assert.NotNull(stack.DateFixed);

            await _repository.MarkAsRegressedAsync(TestConstants.StackId);

            await _client.RefreshAsync();

            stack = await _repository.GetByIdAsync(TestConstants.StackId);

            Assert.NotNull(stack);
            Assert.True(stack.IsRegressed);
            Assert.Null(stack.DateFixed);
        }
Esempio n. 11
0
        private async Task CreateDataAsync()
        {
            string path       = Path.Combine("..", "..", "..", "Search", "Data");
            var    serializer = GetService <JsonSerializer>();

            foreach (string file in Directory.GetFiles(path, "stack*.json", SearchOption.AllDirectories))
            {
                if (file.EndsWith("summary.json"))
                {
                    continue;
                }

                using (var stream = new FileStream(file, FileMode.Open)) {
                    using (var streamReader = new StreamReader(stream)) {
                        var stack = serializer.Deserialize(streamReader, typeof(Stack)) as Stack;
                        Assert.NotNull(stack);
                        await _repository.AddAsync(stack, o => o.ImmediateConsistency());
                    }
                }
            }
        }
Esempio n. 12
0
        private async Task CreateDataAsync(IServiceProvider serviceProvider)
        {
            string path       = Path.Combine("..", "..", "..", "Search", "Data");
            var    serializer = serviceProvider.GetService <JsonSerializer>();

            foreach (string file in Directory.GetFiles(path, "stack*.json", SearchOption.AllDirectories))
            {
                if (file.EndsWith("summary.json"))
                {
                    continue;
                }

                using (var stream = new FileStream(file, FileMode.Open)) {
                    using (var streamReader = new StreamReader(stream)) {
                        var stack = serializer.Deserialize(streamReader, typeof(Stack)) as Stack;
                        Assert.NotNull(stack);
                        await _repository.AddAsync(stack);
                    }
                }
            }

            await _configuration.Client.RefreshAsync(Indices.All);
        }
Esempio n. 13
0
        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;
            });
        }
        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;
            });
        }