Skip to content
Draft
3 changes: 3 additions & 0 deletions src/Exceptionless.Core/Models/EventSummaryModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
public record EventSummaryModel : SummaryData
{
public DateTimeOffset Date { get; set; }
public string ProjectId { get; set; } = null!;
public string? ProjectName { get; set; }
public IReadOnlyCollection<string> Tags { get; set; } = [];
public string? Type { get; set; }
public string? Version { get; set; }
}
3 changes: 3 additions & 0 deletions src/Exceptionless.Core/Models/StackSummaryModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace Exceptionless.Core.Models;
[DebuggerDisplay("Id: {Id}, Status: {Status}, Title: {Title}, First: {FirstOccurrence}, Last: {LastOccurrence}")]
public record StackSummaryModel : SummaryData
{
public string ProjectId { get; init; } = null!;
public string? ProjectName { get; init; }
public IReadOnlyCollection<string> Tags { get; init; } = [];
public required string Title { get; init; }
public StackStatus Status { get; init; }
public DateTime FirstOccurrence { get; init; }
Expand Down
10 changes: 10 additions & 0 deletions src/Exceptionless.Web/Api/Handlers/EventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,8 @@ private async Task<Result<PagedResult<object>>> GetInternalAsync(AppFilter sf, T
{
case "summary":
events = await GetEventsInternalAsync(sf, ti, filter, sort, page, limit, before, after, includeTotal, httpContext.Request);
var projects = await projectRepository.GetByIdsAsync(events.Documents.Select(e => e.ProjectId).Distinct().ToArray(), o => o.Cache());
var projectNames = projects.ToDictionary(p => p.Id, p => p.Name);
var summaries = events.Documents.Select(e =>
{
var summaryData = formattingPluginManager.GetEventSummaryData(e);
Expand All @@ -772,6 +774,9 @@ private async Task<Result<PagedResult<object>>> GetInternalAsync(AppFilter sf, T
Id = summaryData.Id,
TemplateKey = summaryData.TemplateKey,
Date = e.Date,
ProjectId = e.ProjectId,
ProjectName = projectNames.GetValueOrDefault(e.ProjectId),
Tags = e.Tags?.OfType<string>().Order(StringComparer.OrdinalIgnoreCase).ToArray() ?? [],
Type = e.Type,
Version = e.GetVersion(),
Data = summaryData.Data
Expand Down Expand Up @@ -917,6 +922,8 @@ private async Task<ICollection<StackSummaryModel>> GetStackSummariesAsync(List<S
if (stacks.Count == 0)
return new List<StackSummaryModel>(0);

var projects = await projectRepository.GetByIdsAsync(stacks.Select(s => s.ProjectId).Distinct().ToArray(), o => o.Cache());
var projectNames = projects.ToDictionary(p => p.Id, p => p.Name);
var totalUsers = await GetUserCountByProjectIdsAsync(stacks, sf, ti.Range.UtcStart, ti.Range.UtcEnd);
return stacks.Join(stackTerms, s => s.Id, tk => tk.Key, (stack, term) =>
{
Expand All @@ -926,6 +933,9 @@ private async Task<ICollection<StackSummaryModel>> GetStackSummariesAsync(List<S
Id = data.Id,
TemplateKey = data.TemplateKey,
Data = data.Data,
ProjectId = stack.ProjectId,
ProjectName = projectNames.GetValueOrDefault(stack.ProjectId),
Tags = stack.Tags?.OfType<string>().Order(StringComparer.OrdinalIgnoreCase).ToArray() ?? [],
Title = stack.Title,
Status = stack.Status,
FirstOccurrence = term.Aggregations.Min<DateTime>("min_date")?.Value ?? stack.FirstOccurrence,
Expand Down
5 changes: 5 additions & 0 deletions src/Exceptionless.Web/Api/Handlers/StackHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ private async Task<ICollection<StackSummaryModel>> GetStackSummariesAsync(IColle
if (stacks.Count == 0)
return new List<StackSummaryModel>(0);

var projects = await projectRepository.GetByIdsAsync(stacks.Select(s => s.ProjectId).Distinct().ToArray(), o => o.Cache());
var projectNames = projects.ToDictionary(p => p.Id, p => p.Name);
var totalUsers = await GetUserCountByProjectIdsAsync(stacks, sf, ti.Range.UtcStart, ti.Range.UtcEnd);
return stacks.Join(stackTerms, s => s.Id, tk => tk.Key, (stack, term) =>
{
Expand All @@ -473,6 +475,9 @@ private async Task<ICollection<StackSummaryModel>> GetStackSummariesAsync(IColle
Id = data.Id,
TemplateKey = data.TemplateKey,
Data = data.Data,
ProjectId = stack.ProjectId,
ProjectName = projectNames.GetValueOrDefault(stack.ProjectId),
Tags = stack.Tags?.OfType<string>().Order(StringComparer.OrdinalIgnoreCase).ToArray() ?? [],
Title = stack.Title,
Status = stack.Status,
FirstOccurrence = term.Aggregations.Min<DateTime>("min_date")?.Value ?? stack.FirstOccurrence,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,36 @@
return 'yellow';
}

if (level === 'error') {
if (level === 'error' || level === 'fatal') {
return 'destructive';
}

return 'default';
}

function getDarkThemeClasses(level: LogLevel | null): string {
if (level === 'error' || level === 'fatal') {
return 'dark:border-red-400/25 dark:bg-red-400/15 dark:text-red-300';
}

if (level === 'info') {
return 'dark:border-green-400/25 dark:bg-green-400/15 dark:text-green-200';
}

if (level === 'warn') {
return 'dark:border-yellow-400/25 dark:bg-yellow-400/15 dark:text-yellow-200';
}

return 'dark:border-white/20 dark:bg-zinc-300/15 dark:text-zinc-300';
}

const normalizedLogLevel = $derived(getLogLevel(level));
const darkThemeClasses = $derived(getDarkThemeClasses(normalizedLogLevel));
const variant = $derived(getLogLevelVariant(normalizedLogLevel));
</script>

{#if normalizedLogLevel}
<Badge class="w-14 justify-center px-0 text-center" {variant}>
<Badge class={['w-12 justify-center rounded-md border border-current/20 px-0 text-center', darkThemeClasses]} {variant}>
{normalizedLogLevel}
</Badge>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';

import LogLevel from './log-level.svelte';

describe('LogLevel', () => {
it.each([
['debug', ['dark:border-white/20', 'dark:bg-zinc-300/15', 'dark:text-zinc-300']],
['error', ['dark:border-red-400/25', 'dark:bg-red-400/15', 'dark:text-red-300']],
['fatal', ['dark:border-red-400/25', 'dark:bg-red-400/15', 'dark:text-red-300']],
['info', ['dark:border-green-400/25', 'dark:bg-green-400/15', 'dark:text-green-200']],
['warn', ['dark:border-yellow-400/25', 'dark:bg-yellow-400/15', 'dark:text-yellow-200']]
])('renders %s as a compact tag with restrained dark-theme colors', (level, darkThemeClasses) => {
render(LogLevel, { level });

const badge = screen.getByText(level);
expect(badge.classList).toContain('w-12');
expect(badge.classList).toContain('rounded-md');
expect(badge.classList).toContain('border');
expect(badge.classList).toContain('border-current/20');
darkThemeClasses.forEach((className) => expect(badge.classList).toContain(className));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export interface EventSummaryData {
export interface EventSummaryModel<T extends SummaryTemplateKeys> extends SummaryModel<T> {
/** @format date-time */
date: string;
project_id: string;
project_name?: string;
tags: string[];
type?: string;
version?: string;
}
Expand Down Expand Up @@ -95,7 +98,10 @@ export interface StackSummaryModel<T extends SummaryTemplateKeys> extends Summar
first_occurrence: string;
/** @format date-time */
last_occurrence: string;
project_id: string;
project_name?: string;
status: StackStatus;
tags: string[];
title: string;
/** @format int64 */
total: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import TagList from '$features/shared/components/tag-list.svelte';

interface Props {
onTagClick?: (tag: string) => Promise<void> | void;
tags?: null | string[];
}

let { onTagClick, tags }: Props = $props();
</script>

<TagList class="max-w-48 flex-nowrap" maxVisible={2} {onTagClick} {tags} />
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { render, screen } from '@testing-library/svelte';
import { describe, expect, it } from 'vitest';

import EventTagsSummaryCell from './event-tags-summary-cell.svelte';

describe('EventTagsSummaryCell', () => {
it('shows two tags and summarizes the remaining tags', () => {
render(EventTagsSummaryCell, { tags: ['api', 'production', 'critical', 'customer'] });

expect(screen.getByText('api')).toBeTruthy();
expect(screen.getByText('production')).toBeTruthy();
expect(screen.getByText('+2')).toBeTruthy();
expect(screen.queryByText('critical')).toBeNull();
expect(screen.getByLabelText('Tags: api, production, critical, customer').getAttribute('title')).toBe('api, production, critical, customer');
});

it('shows an empty value when there are no tags', () => {
render(EventTagsSummaryCell, { tags: [] });

expect(screen.getByText('—')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest';

import type { EventSummaryModel, StackSummaryModel, SummaryTemplateKeys } from '../summary';

import { defaultEventColumnVisibility, defaultStackColumnVisibility, getColumns } from './options.svelte';

describe('event table columns', () => {
it('offers project and tags as hidden optional columns', () => {
const columns = getColumns<EventSummaryModel<SummaryTemplateKeys>>();
const columnIds = columns.map((column) => column.id);

expect(columnIds).toContain('project');
expect(columnIds).toContain('tags');
expect(defaultEventColumnVisibility.project).toBe(false);
expect(defaultEventColumnVisibility.tags).toBe(false);
});

it('offers project and tags as hidden optional stack columns', () => {
const columns = getColumns<StackSummaryModel<SummaryTemplateKeys>>('stack_frequent');
const columnIds = columns.map((column) => column.id);

expect(columnIds).toContain('project');
expect(columnIds).toContain('tags');
expect(defaultStackColumnVisibility.project).toBe(false);
expect(defaultStackColumnVisibility.tags).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type { EventSummaryModel, StackSummaryModel, SummaryModel, SummaryTemplat

import LogLevel from '../log-level.svelte';
import Summary from '../summary/summary.svelte';
import EventTagsSummaryCell from './event-tags-summary-cell.svelte';
import EventsUserIdentitySummaryCell from './events-user-identity-summary-cell.svelte';
import StackStatusCell from './stack-status-cell.svelte';
import StackUsersSummaryCell from './stack-users-summary-cell.svelte';
Expand All @@ -20,14 +21,21 @@ export const defaultEventColumnVisibility: ColumnVisibilityState = {
level: false,
message: false,
name: false,
project: false,
source: false,
tags: false,
type: false,
version: false
};

export const defaultStackColumnVisibility: ColumnVisibilityState = {
project: false,
tags: false
};

export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKeys>>(
mode: GetEventsMode = 'summary',
options?: { showType?: boolean }
options?: { onTagClick?: (tag: string) => Promise<void> | void; showType?: boolean }
): ColumnDef<StockFeatures, TSummaryModel, unknown>[] {
const showType = options?.showType ?? true;
const columns: ColumnDef<StockFeatures, TSummaryModel, unknown>[] = [
Expand Down Expand Up @@ -61,6 +69,16 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
meta: {
class: 'w-full'
}
},
{
accessorFn: (row) => getProject(row),
cell: (prop) => formatTextColumn(prop.getValue()),
enableSorting: false,
header: 'Project',
id: 'project',
meta: {
class: 'w-40 min-w-40 max-w-40'
}
}
];

Expand All @@ -84,6 +102,16 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
class: 'w-36'
}
},
{
accessorKey: nameof<EventSummaryModel<SummaryTemplateKeys>>('tags'),
cell: (prop) => renderComponent(EventTagsSummaryCell, { onTagClick: options?.onTagClick, tags: prop.getValue<string[]>() }),
enableSorting: false,
header: 'Tags',
id: 'tags',
meta: {
class: 'w-52 min-w-52 max-w-52'
}
},
{
accessorFn: (row) => getSummaryDataValue(row, 'Message'),
cell: (prop) => formatTextColumn(prop.getValue()),
Expand Down Expand Up @@ -153,6 +181,16 @@ export function getColumns<TSummaryModel extends SummaryModel<SummaryTemplateKey
);
} else {
columns.push(
{
accessorKey: nameof<StackSummaryModel<SummaryTemplateKeys>>('tags'),
cell: (prop) => renderComponent(EventTagsSummaryCell, { onTagClick: options?.onTagClick, tags: prop.getValue<string[]>() }),
enableSorting: false,
header: 'Tags',
id: 'tags',
meta: {
class: 'w-52 min-w-52 max-w-52'
}
},
{
accessorKey: nameof<StackSummaryModel<SummaryTemplateKeys>>('status'),
cell: (prop) => renderComponent(StackStatusCell, { value: prop.getValue<StackStatus>() }),
Expand Down Expand Up @@ -212,6 +250,11 @@ function formatTextColumn(value: unknown): string {
return typeof value === 'string' && value.length > 0 ? value : '—';
}

function getProject<TSummaryModel extends SummaryModel<SummaryTemplateKeys>>(summary: TSummaryModel): string | undefined {
const eventSummary = summary as Partial<Pick<EventSummaryModel<SummaryTemplateKeys>, 'project_id' | 'project_name'>> & TSummaryModel;
return eventSummary.project_name ?? eventSummary.project_id;
}

function getSource<TSummaryModel extends SummaryModel<SummaryTemplateKeys>>(summary: TSummaryModel): string | undefined {
return getSummaryDataValue(summary, 'SourceShortName') ?? getSummaryDataValue(summary, 'Source');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
getStackTrace,
hasErrorOrSimpleError
} from '$features/events/persistent-event';
import TagList from '$features/shared/components/tag-list.svelte';
import ExternalLink from '@lucide/svelte/icons/external-link';
import Filter from '@lucide/svelte/icons/filter';
import Email from '@lucide/svelte/icons/mail';

import type { PersistentEvent } from '../../models/index';
Expand Down Expand Up @@ -163,12 +163,7 @@
<Table.Head class="w-40 font-semibold whitespace-nowrap">Tags</Table.Head>
<Table.Cell class="w-4 pr-0"></Table.Cell>
<Table.Cell class="flex flex-wrap items-center justify-start gap-2 overflow-auto">
{#each event.tags as tag (tag)}
<EventsFacetedFilter.TagTrigger changed={filterChanged} value={[tag]}>
<Filter class="size-3" />
{tag}
</EventsFacetedFilter.TagTrigger>
{/each}
<TagList onTagClick={(tag) => filterChanged(new EventsFacetedFilter.TagFilter([tag]))} tags={event.tags} />
</Table.Cell>
</Table.Row>
{/if}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface ErrorInfo extends InnerErrorInfo {
}

export interface IErrorData extends Record<string, unknown> {
'@ext'?: Record<string, unknown>;
'@ext'?: Record<string, unknown> | string;
'@target'?: ITargetErrorData;
}

Expand All @@ -48,7 +48,7 @@ export interface IRequestInfoInfoData extends Record<string, unknown> {
}

export interface ISimpleErrorInfoData extends Record<string, unknown> {
'@ext'?: Record<string, unknown>;
'@ext'?: Record<string, unknown> | string;
}

export interface ITargetErrorData extends Record<string, string | undefined> {
Expand Down
Loading
Loading