Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/Exceptionless.Core/Jobs/EventNotificationsJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ protected override async Task<JobResult> ProcessQueueEntryAsync(QueueEntryContex
{
if (shouldLog) _logger.LogTrace("Loaded project: name={ProjectName}", project.Name);

// after the first 2 occurrences, don't send a notification for the same stack more then once every 30 minutes
// after the first 2 occurrences, don't send a notification for the same stack more then once every 15 minutes
var lastTimeSentUtc = await _cache.GetAsync<DateTime>(String.Concat("notify:stack-throttle:", ev.StackId), DateTime.MinValue);
if (wi is { TotalOccurrences: > 2, IsRegression: false } && lastTimeSentUtc != DateTime.MinValue && lastTimeSentUtc > _timeProvider.GetUtcNow().UtcDateTime.AddMinutes(-30))
if (wi is { TotalOccurrences: > 2, IsRegression: false } && lastTimeSentUtc != DateTime.MinValue && lastTimeSentUtc > _timeProvider.GetUtcNow().UtcDateTime.AddMinutes(-15))
{
if (shouldLog) _logger.LogInformation("Skipping message because of stack throttling: last sent={LastSentUtc} occurrences={TotalOccurrences}", lastTimeSentUtc, wi.TotalOccurrences);
return JobResult.Success;
Expand Down
4 changes: 2 additions & 2 deletions src/Exceptionless.Core/Mail/Mailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,8 @@ public async Task SendOrganizationNoticeAsync(User user, Organization organizati
: $"[{organization.Name}] Events are currently being throttled.";
string upgradeUrl = _appUrls.OrganizationUpgrade(organization.Id);
string learnMoreUrl = isOverMonthlyLimit
? "https://github.com/exceptionless/Exceptionless/wiki/Frequently-Asked-Questions#q-what-happens-if-the-organization-plan-limit-is-reached"
: "https://github.com/exceptionless/Exceptionless/wiki/Frequently-Asked-Questions#q-why-is-my-organization-throttled";
? "https://exceptionless.com/docs/FAQ/#what-happens-if-the-organization-plan-limit-is-reached"
: "https://exceptionless.com/docs/FAQ/#why-is-my-organization-throttled";
var message = new OrganizationNoticeEmail(
subject,
organization.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public override async Task ProcessBatchAsync(ICollection<EventContext> contexts)
var version = _semanticVersionParser.Parse(versionGroup.Key, versionCache) ?? _semanticVersionParser.Default;
if (version < fixedInVersion)
{
foreach (var ctx in stackGroup.Where(s => s.Organization.HasPremiumFeatures))
foreach (var ctx in versionGroup.Where(s => s.Organization.HasPremiumFeatures))
{
_logger.LogDebug("Discarding fixed stack event: Version {Version} is older than fixed in version {FixedInVersion}", version, fixedInVersion);
ctx.IsDiscarded = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
@if (Model.Blocked != 0)
{
<EmailCallout BackgroundColor="#f4dede" BorderColor="#bb423f">
<p style="@EmailStyles.Paragraph"><strong>@Model.Blocked events</strong> were discarded due to throttling. <a href="@Model.UpgradeUrl" style="@EmailStyles.Link">Upgrade now</a> to increase your limits. <a href="https://github.com/exceptionless/Exceptionless/wiki/Frequently-Asked-Questions#q-why-is-my-organization-throttled" style="@EmailStyles.Link">Click here to learn more about throttling.</a></p>
<p style="@EmailStyles.Paragraph"><strong>@Model.Blocked events</strong> were discarded due to throttling. <a href="@Model.UpgradeUrl" style="@EmailStyles.Link">Upgrade now</a> to increase your limits. <a href="https://exceptionless.com/docs/FAQ/#why-is-my-organization-throttled" style="@EmailStyles.Link">Click here to learn more about throttling.</a></p>
<EmailButton Url="@Model.UpgradeUrl" BackgroundColor="#bb423f">Upgrade Plan</EmailButton>
</EmailCallout>
}
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Web/Api/Handlers/TokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ private async Task<Result<ViewToken>> CreateTokenImplAsync(NewToken value)
var model = await AddModelAsync(mapped);
var viewModel = mapper.MapToViewToken(model);
AfterResultMap([viewModel]);
string location = linkGenerator.GetUriByName(HttpContext, "GetTokenById", new { id = model.Id })
string location = linkGenerator.GetUriByName(HttpContext, "GetTokenById", new { id = model.Id }, options: new LinkOptions { LowercaseUrls = false })
?? throw new InvalidOperationException("Unable to generate token location.");
return Result<ViewToken>.Created(viewModel, location);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Web/Assistant/AssistantSystemPrompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static string Create(string context) => $"""

PERIOD RULE: for which errors happened in a period, or the top, most frequent, or most common errors in one, call count_events once per project with groupBy stack, the time, and filter type:error. It counts the events inside the period, ranks stacks by how often they occurred, names each with its title, gives its webUrl, and includes first_occurrence, so an error is new when that falls inside the period. search_stacks matches stacks by their last occurrence and sorts by all-time totals, so a stack that happened in a past period and again since would be missed; use search_stacks only to find stacks by status, tag, title, or other stack fields.

CLIENT SETUP RULE: for any question about configuring, installing, connecting, or sending first events from a client, call get_project_setup once. Use the current project by default. When there is no current project and the user identifies one by name, pass that exact projectName to get_project_setup so it resolves the project within the current organization. If no project is identified, call list_projects once, ask which project they want to configure, and call suggest_followups with up to three of those projects as prompts that name the project, such as "How do I configure a client for Billing API?". Do not call get_stack for setup, and call list_projects only when no project is identified. Use the returned Client Setup webUrl verbatim and only describe the clients and support status returned by that tool. Never invent packages or advertise Python, Java, Ruby, or PHP clients. Treat JavaScript / Node.js as legacy; describe React Native and Expo as supported only when returned by the tool.
CLIENT SETUP RULE: for any question about configuring, installing, connecting, or sending first events from a client, call get_project_setup once. Use the current project by default. When there is no current project and the user identifies one by name, pass that exact projectName to get_project_setup so it resolves the project within the current organization. If no project is identified, call list_projects once, ask which project they want to configure, and call suggest_followups with up to three of those projects as prompts that name the project, such as "How do I configure a client for Billing API?". Do not call get_stack for setup, and call list_projects only when no project is identified. Use the returned Client Setup webUrl verbatim and only describe the clients and support status returned by that tool. Never invent packages or advertise Python, Java, Ruby, or PHP clients. Describe JavaScript / Node.js, React Native, and Expo as supported only when returned by the tool.

After using tools, always provide a complete final answer in the same response. Never end by merely saying what you will inspect or do next. If the available tools cannot retrieve something, clearly state that limitation and give the most useful answer supported by the available data.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@
</AlertDialog.Title>
<AlertDialog.Description>
{#if stackTitle}<strong>{stackTitle}</strong><br />{/if}
Are you sure you want to discard all current
Are you sure you want to discard future events for
{#if count === 1}
stack events
this stack?
{:else}
<Number value={count} /> stacks events
<Number value={count} /> stacks?
{/if}
and discard any future events?
Existing events are kept. To remove them, delete the
{#if count === 1}stack{:else}stacks{/if}
instead.
</AlertDialog.Description>
</AlertDialog.Header>
All future occurrences will be discarded and will not count against your event limit.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@
try {
await updateSlackNotificationSettingsResponse.mutateAsync(settings);
toastId = toast.success('Successfully updated Slack notification settings.');
} catch {
} catch (error) {
if (showBillingDialogOnUpgradeProblem(error, organization.current, () => updateSlackNotificationSettings(settings))) {
return;
}

toastId = toast.error('Error updating Slack notification settings. Please try again.');
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Exceptionless.Web/Mcp/ExceptionlessMcpTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public async Task<McpResponse<McpProjectSetupResult>> GetProjectSetupAsync(
new McpProjectSetupClient(".NET", "current"),
new McpProjectSetupClient("React Native", "current"),
new McpProjectSetupClient("Expo", "current"),
new McpProjectSetupClient("JavaScript / Node.js", "legacy")
new McpProjectSetupClient("JavaScript / Node.js", "current")
],
["Use the project's Client Setup page for the server URL, API key, and current installation instructions."]));
}
Expand Down
5 changes: 5 additions & 0 deletions tests/Exceptionless.Tests/Api/Endpoints/TokenEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public async Task PostAsync_NewToken_ReturnsAbsoluteLocation()
Assert.True(response.Headers.Location.IsAbsoluteUri);
Assert.Equal("localhost", response.Headers.Location.Host);
Assert.StartsWith("/api/v2/tokens/", response.Headers.Location.AbsolutePath);

// Token ids are case sensitive, so the location must preserve their case.
var viewToken = await response.DeserializeAsync<ViewToken>();
Assert.NotNull(viewToken);
Assert.EndsWith("/" + viewToken.Id, response.Headers.Location.AbsolutePath, StringComparison.Ordinal);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ [new AssistantChatMessage("user", "Say hello")],
Assert.Contains("pass that exact projectName to get_project_setup", handler.RequestBody);
Assert.Contains("Do not call get_stack for setup, and call list_projects only when no project is identified", handler.RequestBody);
Assert.Contains("Never invent packages or advertise Python, Java, Ruby, or PHP clients", handler.RequestBody);
Assert.Contains("describe React Native and Expo as supported only when returned by the tool", handler.RequestBody);
Assert.Contains("Describe JavaScript / Node.js, React Native, and Expo as supported only when returned by the tool", handler.RequestBody);
Assert.Contains("do not call list_projects, call each needed project-scoped tool only once", handler.RequestBody);
Assert.Contains("Defaults to the current page project id when omitted", handler.RequestBody);
Assert.Contains("This tool has no stack id filter", handler.RequestBody);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ public async Task GetProjectSetupAsync_Project_ReturnsOnlyVerifiedClientSupport(
client =>
{
Assert.Equal("JavaScript / Node.js", client.Name);
Assert.Equal("legacy", client.Status);
Assert.Equal("current", client.Status);
});
Assert.DoesNotContain(setup.Clients, client => client.Name is "Python" or "Java" or "Ruby" or "PHP");
}
Expand Down
37 changes: 37 additions & 0 deletions tests/Exceptionless.Tests/Pipeline/EventPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,43 @@ public async Task EnsureVersionedRegressionAsync()
Assert.Equal("1.0.1-rc3", regressedEvent.GetVersion());
}

[Fact]
public async Task EnsureOnlyOlderVersionEventsAreDiscardedWhenBatchRegressesAsync()
{
// Arrange
var utcNow = DateTime.UtcNow;
var organization = _organizationData.GenerateSampleOrganization(_billingManager, _plans);
organization.HasPremiumFeatures = true;
var project = _projectData.GenerateSampleProject();
var ev = _eventData.GenerateEvent(projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow);
var context = new EventContext(ev, organization, project);
await _pipeline.RunAsync(context);
await RefreshDataAsync();

ev = await _eventRepository.GetByIdAsync(ev.Id);
Assert.NotNull(ev);

var stack = await _stackRepository.GetByIdAsync(ev.StackId);
Assert.NotNull(stack);
stack.MarkFixed(new SemanticVersion(1, 0, 1), TimeProvider);
await _stackRepository.SaveAsync(stack, o => o.Cache());

var olderContext = new EventContext(_eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(-2), semver: "1.0.0"), organization, project);
var newerContext = new EventContext(_eventData.GenerateEvent(stackId: ev.StackId, projectId: TestConstants.ProjectId, organizationId: TestConstants.OrganizationId, occurrenceDate: utcNow.AddMinutes(-1), semver: "1.0.2"), organization, project);
await RefreshDataAsync();

// Act
await _pipeline.RunAsync([olderContext, newerContext]);

// Assert
Assert.False(olderContext.HasError, olderContext.ErrorMessage);
Assert.False(newerContext.HasError, newerContext.ErrorMessage);
Assert.True(olderContext.IsDiscarded);
Assert.False(newerContext.IsDiscarded);
Assert.True(newerContext.IsRegression);
Assert.True(newerContext.IsProcessed);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
Loading