diff --git a/src/Exceptionless.Core/Models/EventSummaryModel.cs b/src/Exceptionless.Core/Models/EventSummaryModel.cs index 28c9ec591..b38dc61ed 100644 --- a/src/Exceptionless.Core/Models/EventSummaryModel.cs +++ b/src/Exceptionless.Core/Models/EventSummaryModel.cs @@ -4,4 +4,5 @@ public record EventSummaryModel : SummaryData { public DateTimeOffset Date { get; set; } public string? Type { get; set; } + public string? Version { get; set; } } diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/summary/index.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/summary/index.ts index e9c0a88fd..ae25ea353 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/summary/index.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/summary/index.ts @@ -62,6 +62,7 @@ export interface EventSummaryModel extends Summar /** @format date-time */ date: string; type?: string; + version?: string; } export interface StackErrorSummaryData { diff --git a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts index 657e40b28..a1d39b5f9 100644 --- a/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts +++ b/src/Exceptionless.Web/ClientApp/src/lib/features/events/components/table/options.svelte.ts @@ -21,7 +21,8 @@ export const defaultEventColumnVisibility: ColumnVisibilityState = { message: false, name: false, source: false, - type: false + type: false, + version: false }; export function getColumns>( @@ -102,6 +103,16 @@ export function getColumns>('version'), + cell: (prop) => formatTextColumn(prop.getValue()), + enableSorting: false, + header: 'Version', + id: 'version', + meta: { + class: 'w-32' + } + }, { accessorFn: (row) => getSummaryDataValue(row, 'Type'), cell: (prop) => formatTextColumn(prop.getValue()), diff --git a/src/Exceptionless.Web/Controllers/EventController.cs b/src/Exceptionless.Web/Controllers/EventController.cs index d36e6c715..e221009ec 100644 --- a/src/Exceptionless.Web/Controllers/EventController.cs +++ b/src/Exceptionless.Web/Controllers/EventController.cs @@ -320,6 +320,7 @@ private async Task>> GetInternalAsync( TemplateKey = summaryData.TemplateKey, Date = e.Date, Type = e.Type, + Version = e.GetVersion(), Data = summaryData.Data }; }).ToList(), events.HasMore && !NextPageExceedsSkipLimit(page, limit), page, includeTotal ? events.Total : null, events.Hits.FirstOrDefault()?.GetSortToken(_serializer), events.Hits.LastOrDefault()?.GetSortToken(_serializer)); diff --git a/src/Exceptionless.Web/Models/SavedView/NewSavedView.cs b/src/Exceptionless.Web/Models/SavedView/NewSavedView.cs index 8f4ba40ff..567dfc7db 100644 --- a/src/Exceptionless.Web/Models/SavedView/NewSavedView.cs +++ b/src/Exceptionless.Web/Models/SavedView/NewSavedView.cs @@ -15,9 +15,9 @@ public record NewSavedView : IOwnedByOrganization, IValidatableObject public static readonly IReadOnlyDictionary> ValidColumnIds = new Dictionary> { - ["events"] = new HashSet { "summary", "user", "date", "message", "type", "exception_type", "source", "name", "level" }, + ["events"] = new HashSet { "summary", "user", "date", "message", "type", "version", "exception_type", "source", "name", "level" }, ["stacks"] = new HashSet { "summary", "status", "users", "events", "first", "last" }, - ["stream"] = new HashSet { "summary", "user", "date", "message", "type", "exception_type", "source", "name", "level" } + ["stream"] = new HashSet { "summary", "user", "date", "message", "type", "version", "exception_type", "source", "name", "level" } }; /// Union of all valid column IDs across all views. diff --git a/tests/Exceptionless.Tests/Controllers/ExceptionlessMcpToolsTests.cs b/tests/Exceptionless.Tests/Controllers/ExceptionlessMcpToolsTests.cs index 39612950b..ffff3b790 100644 --- a/tests/Exceptionless.Tests/Controllers/ExceptionlessMcpToolsTests.cs +++ b/tests/Exceptionless.Tests/Controllers/ExceptionlessMcpToolsTests.cs @@ -300,10 +300,11 @@ public async Task CountEventsAsync_GroupByVersionAndInterval_ReturnsGroupedTrend { try { - TimeProvider.SetUtcNow(new DateTimeOffset(2026, 6, 25, 12, 0, 0, TimeSpan.Zero)); + var now = TimeProvider.GetUtcNow(); + TimeProvider.SetUtcNow(now); const string referenceId = "mcp-count-events-version-trend"; - await CreateDataAsync(d => d.Event().TestProject().ReferenceId(referenceId).Version("1.0.2").Date(new DateTimeOffset(2026, 6, 24, 12, 0, 0, TimeSpan.Zero)).Message("MCP count event 1.0.2 trend")); - await CreateDataAsync(d => d.Event().TestProject().ReferenceId(referenceId).Version("1.0.3").Date(new DateTimeOffset(2026, 6, 25, 12, 0, 0, TimeSpan.Zero)).Message("MCP count event 1.0.3 trend")); + await CreateDataAsync(d => d.Event().TestProject().ReferenceId(referenceId).Version("1.0.2").Date(now.AddDays(-1)).Message("MCP count event 1.0.2 trend")); + await CreateDataAsync(d => d.Event().TestProject().ReferenceId(referenceId).Version("1.0.3").Date(now.AddHours(-1)).Message("MCP count event 1.0.3 trend")); await RefreshDataAsync(); var tools = await CreateToolsAsync(AuthorizationRoles.McpRead, AuthorizationRoles.EventsRead); diff --git a/tests/Exceptionless.Tests/Controllers/SavedViewControllerTests.cs b/tests/Exceptionless.Tests/Controllers/SavedViewControllerTests.cs index 85462c496..746024206 100644 --- a/tests/Exceptionless.Tests/Controllers/SavedViewControllerTests.cs +++ b/tests/Exceptionless.Tests/Controllers/SavedViewControllerTests.cs @@ -1781,6 +1781,28 @@ public Task PostAsync_ValidColumnKeys_Succeeds() ); } + [Theory] + [InlineData("events")] + [InlineData("stream")] + public Task PostAsync_VersionColumnForEventViews_Succeeds(string viewType) + { + // Arrange & Act & Assert + return SendRequestAsync(r => r + .Post() + .AsGlobalAdminUser() + .AppendPaths("organizations", SampleDataService.TEST_ORG_ID, "saved-views") + .Content(new NewSavedView + { + OrganizationId = SampleDataService.TEST_ORG_ID, + Name = $"Valid {viewType} Version Column", + ViewType = viewType, + Columns = new Dictionary { ["summary"] = true, ["version"] = false }, + ColumnOrder = ["summary", "version"] + }) + .StatusCodeShouldBeCreated() + ); + } + [Fact] public Task PostAsync_InvalidColumnOrderKey_ReturnsUnprocessableEntity() {