Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ Notes:

1. Running `Exceptionless.AppHost` starts the app and required infrastructure together.
2. Backend tests bootstrap required infrastructure automatically.
3. Use Aspire CLI 13.6.0 or newer (`aspire update --self` to update). The AppHost uses the CLI bundle for the dashboard and resource orchestrator; when no CLI is installed, the SDK downloads the matching version through DNX.
3. Use Aspire CLI 13.6.5 or newer (`aspire update --self` to update). The AppHost uses the CLI bundle for the dashboard and resource orchestrator; when no CLI is installed, the SDK downloads the matching version through DNX.
4. Try `dotnet build -mt` for multithreaded builds. Performance depends on the workload and machine.

## UI Development

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ public static bool UpdateSessionStart(this PersistentEvent ev, DateTime lastActi
if (duration < 0)
duration = 0;

decimal newDuration = (decimal)(lastActivityUtc - ev.Date.UtcDateTime).TotalSeconds;
// Avoid losing tick precision through TotalSeconds' double representation.
decimal newDuration = (lastActivityUtc - ev.Date.UtcDateTime).Ticks / (decimal)TimeSpan.TicksPerSecond;
if (duration >= newDuration)
lastActivityUtc = ev.Date.UtcDateTime.AddSeconds((double)duration);
else
Expand Down
6 changes: 6 additions & 0 deletions src/Exceptionless.Web/ClientApp/src/lib/generated/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,12 @@ export interface User {
id: string;
/** The organizations that the user has access to. */
organization_ids: string[];
/**
* Session revocation generation, rotated when a password changes or resets, or an external login is removed or transferred away.
* Remains null until rotation; active users with a null generation accept only matching legacy null token generations.
* This value does not describe the password hashing format.
*/
authentication_version?: null | string;
password?: null | string;
salt?: null | string;
password_reset_token?: null | string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,7 @@ export const UserSchema = object({
.length(24, "Id must be exactly 24 characters")
.regex(/^[a-fA-F0-9]{24}$/, "Id has invalid format"),
organization_ids: array(string()),
authentication_version: string().nullable().optional(),
password: string().min(1, "Password is required").nullable().optional(),
salt: string().min(1, "Salt is required").nullable().optional(),
password_reset_token: string()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi;

namespace Exceptionless.Web.Utility.OpenApi;

Expand All @@ -11,8 +10,6 @@ public static IServiceCollection AddExceptionlessOpenApi(this IServiceCollection
{
services.AddOpenApi(options =>
{
// ASP.NET Core 11 defaults to OpenAPI 3.2; keep publishing 3.1 for existing client generators.
options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1;
options.CreateSchemaReferenceId = SchemaReferenceIdHelper.CreateSchemaReferenceId;
options.AddDocumentTransformer<AggregateDocumentTransformer>();
options.AddDocumentTransformer<XmlDocumentationDocumentTransformer>();
Expand Down
4 changes: 2 additions & 2 deletions tests/Exceptionless.Tests/Api/Data/openapi.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"openapi": "3.1.2",
"openapi": "3.2.0",
"info": {
"title": "Exceptionless API",
"termsOfService": "https://exceptionless.com/terms/",
Expand Down Expand Up @@ -15224,4 +15224,4 @@
"name": "WebHook"
}
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,9 @@ public async Task AddUserAsync_ExistingUserWithoutMembership_AddsOrganizationMem
var existingUser = await _userRepository.GetByEmailAddressAsync(SampleDataService.FREE_USER_EMAIL);
Assert.NotNull(existingUser);
Assert.DoesNotContain(SampleDataService.TEST_ORG_ID, existingUser.OrganizationIds);
existingUser.AuthenticationVersion = "private-authentication-version";
existingUser.PasswordResetToken = "private-password-reset-token";
await _userRepository.SaveAsync(existingUser, o => o.ImmediateConsistency().Cache());

// Act
var user = await SendRequestAsAsync<User>(r => r
Expand All @@ -1116,6 +1119,11 @@ public async Task AddUserAsync_ExistingUserWithoutMembership_AddsOrganizationMem
// Assert
Assert.NotNull(user);
Assert.Equal(SampleDataService.FREE_USER_EMAIL, user.EmailAddress);
Assert.Null(user.AuthenticationVersion);
Assert.Null(user.Password);
Assert.Null(user.Salt);
Assert.Null(user.PasswordResetToken);
Assert.Null(user.VerifyEmailAddressToken);

existingUser = await _userRepository.GetByEmailAddressAsync(SampleDataService.FREE_USER_EMAIL);
Assert.NotNull(existingUser);
Expand Down
1 change: 1 addition & 0 deletions tests/Exceptionless.Tests/Api/OpenApiSnapshotTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public async Task GetOpenApiJson_Default_ContainsExpectedRoutesOperationsAndResp
var paths = document.RootElement.GetProperty("paths");

// Assert
Assert.Equal("3.2.0", document.RootElement.GetProperty("openapi").GetString());
Assert.True(paths.TryGetProperty("/api/v2/auth/login", out var loginPath));
Assert.True(loginPath.TryGetProperty("post", out var loginPost));
Assert.True(loginPost.TryGetProperty("requestBody", out _));
Expand Down
9 changes: 8 additions & 1 deletion tests/Exceptionless.Tests/Assistant/AssistantServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,7 @@ public async Task StreamAsync_ProviderThrows_RecordsFailureCategory(string failu
[InlineData("{\"usage\":{\"prompt_tokens\":\"private invalid token count\"}}")]
public async Task StreamAsync_InvalidProviderShape_RecordsProviderAndTurnFailure(string payload)
{
// Arrange
var options = AppOptions.ReadFromConfiguration(new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { ["BaseURL"] = "https://localhost", ["Assistant:ApiKey"] = "test-key" })
.Build());
Expand All @@ -1701,17 +1702,23 @@ public async Task StreamAsync_InvalidProviderShape_RecordsProviderAndTurnFailure
using var response = new MemoryStream();
context.Response.Body = response;

// Act
await AssistantEndpoints.WriteResponseAsync(context,
service.StreamAsync(new AssistantChatRequest([new AssistantChatMessage("user", "private question")]),
"user-id", CreatePlanOptions(), diagnostics, TestContext.Current.CancellationToken),
usageService, "organization-id", diagnostics, TestContext.Current.CancellationToken);

// Assert
var providerEntry = Assert.Single(logger.Entries, entry => entry.Properties.ContainsKey("ProviderOutcome"));
var turnEntry = Assert.Single(logger.Entries, entry => entry.Properties.ContainsKey("Outcome"));
Assert.Equal("invalid_provider_response", providerEntry.Properties["ProviderOutcome"]);
Assert.Equal("invalid_provider_response", turnEntry.Properties["FailureReason"]);
Assert.Equal("failed", turnEntry.Properties["Outcome"]);
Assert.All(logger.Entries, entry => Assert.DoesNotContain("private", entry.Message));
Assert.All(logger.Entries, entry =>
{
Assert.DoesNotContain("private question", entry.Message);
Assert.DoesNotContain("private invalid token count", entry.Message);
});
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Models;
using Foundatio.Xunit;
using Xunit;

namespace Exceptionless.Tests.Extensions;

public sealed class PersistentEventExtensionsTests(ITestOutputHelper output) : TestWithLoggingBase(output)
{
[Fact]
public void UpdateSessionStart_FractionalSeconds_PreservesTickPrecision()
{
// Arrange
var start = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
var session = new PersistentEvent { Type = Event.KnownTypes.Session, Date = start };
var lastActivity = start.UtcDateTime.AddTicks(1_234_567_891);

// Act
bool updated = session.UpdateSessionStart(lastActivity);

// Assert
Assert.True(updated);
Assert.Equal(123.4567891m, session.Value);
}
}
Loading