Skip to content

Reduce noisy change events - #2438

Open
ejsmith wants to merge 7 commits into
mainfrom
issue/reduce-noisy-change-events
Open

Reduce noisy change events#2438
ejsmith wants to merge 7 commits into
mainfrom
issue/reduce-noisy-change-events

Conversation

@ejsmith

@ejsmith ejsmith commented Aug 5, 2026

Copy link
Copy Markdown
Member

What changed

  • Stop routine organization/project usage persistence, usage repair, and usage-only maintenance from publishing entity-change notifications.
  • Keep meaningful maintenance notifications for plan, bot-list, configuration-version, usage-repair overage, and hourly-throttle-clear transitions.
  • Handle persistent-event configuration state locally in Svelte, including events received before project data loads.
  • Refresh organization state on targeted PlanOverage messages without invalidating unrelated organization queries.
  • Refresh organization usage state at UTC month rollover in both clients, independent of new ingestion.
  • Poll organization and project usage pages every five minutes while those pages are active.
  • Evaluate current overage state against the live effective limit when temporary bonuses expire.
  • Clear the legacy Angular organization cache on PlanOverage.
  • Add focused backend and frontend regression coverage.

Why

Routine usage counters and persistent-event traffic were causing repeated organization and project reloads while the app was otherwise idle. The remaining notifications and refreshes are limited to state transitions and views that users need to see.

Verification

  • dotnet build tests/Exceptionless.Tests/Exceptionless.Tests.csproj --no-restore --disable-build-servers — passed with 0 warnings and 0 errors
  • Full .NET test assembly — 2,686 passed / 2 skipped
  • UpdateEventUsageMigrationTests — 2 passed
  • OrganizationEndpointTests — 103 passed
  • UsageServiceTests — 22 passed
  • npm run validate — passed with 0 errors and 0 warnings
  • npm run test:unit — 36 files / 406 tests passed
  • Svelte npm run build — passed
  • Legacy Angular npm run build — passed

Breaking changes

None.

@ejsmith
ejsmith marked this pull request as ready for review August 5, 2026 01:06

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b319fb57d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), usage.Total, TimeSpan.FromHours(8));
await _organizationRepository.SaveAsync(organization);
// Usage counters and last-event timestamps are operational updates, not user-facing entity changes.
await _organizationRepository.SaveAsync(organization, o => o.Notifications(false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate organization state on plan overage

When this usage save is the one that crosses the monthly or hourly limit, the backend can still emit PlanOverage, but the Svelte client does not refetch organization data for that message: src/Exceptionless.Web/ClientApp/src/routes/(app)/+layout.svelte only invalidates queries inside isEntityChangedType, and PlanOverage is not a *Changed entity message. Before this change, the accompanying OrganizationChanged from this SaveAsync refreshed is_over_monthly_limit / is_throttled; with Notifications(false), connected users can remain on stale organization state and miss the overage banner until another organization change or a reload. Please keep an organization invalidation for overage transitions or add a PlanOverage handler in the client.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in f8449f0: the Svelte websocket handler now recognizes PlanOverage and invalidates only the affected organization, organization stats, and organization list queries. The endpoint also evaluates pending usage so the refetch observes the transition. Covered by the targeted frontend query-invalidation test and OrganizationEndpointTests.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8449f0ae4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), usage.Total, TimeSpan.FromHours(8));
await _organizationRepository.SaveAsync(organization);
// Usage counters and last-event timestamps are operational updates, not user-facing entity changes.
await _organizationRepository.SaveAsync(organization, o => o.Notifications(false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep clearing throttled state visible to clients

When an organization hits the hourly bucket limit, the new PlanOverage handler refreshes the Svelte organization query to is_throttled: true, but this save is also where the completed bucket's throttled cache key is removed; with repository notifications disabled here, the previous OrganizationChanged refetch that cleared the hourly overage banner is no longer emitted. In an active session that does not remount or focus-refetch after the 5-minute throttle window, users can keep seeing the destructive hourly overage banner even though GetUsageAsync would now return IsThrottled = false; please keep a clear-side invalidation/notification for this state transition.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 15e27df: hourly throttle transitions now write a per-bucket marker before PlanOverage is published. Saving that completed bucket emits one OrganizationChanged while routine usage saves remain silent. Covered by SavePendingUsageAsync_HourlyThrottleClears_PublishesOrganizationChangedMessage; the full UsageServiceTests suite passes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15e27df966

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), usage.Total, TimeSpan.FromHours(8));
await _organizationRepository.SaveAsync(organization);
// Routine usage updates stay silent, but clients need one refresh when an hourly throttle clears.
await _organizationRepository.SaveAsync(organization, o => o.Notifications(hourlyThrottleTransition?.Value ?? false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear legacy org cache on plan overage

When an organization crosses the monthly limit from pending usage, this save no longer emits OrganizationChanged, leaving PlanOverage as the only websocket signal. The legacy Angular banner listens for PlanOverage (src/Exceptionless.Web/ClientApp.angular/components/organization-notifications/organization-notifications-directive.tpl.html:4), but its vm.get() path calls organizationService.getAll() with the default cached Restangular client (src/Exceptionless.Web/ClientApp.angular/components/organization/organization-service.js:94-99), and that cache is only cleared on OrganizationChanged/ProjectChanged (src/Exceptionless.Web/ClientApp.angular/components/organization/organization-service.js:11-12). The fresh evidence is this unchanged Angular cache path, so Angular users with a warm organization cache keep seeing stale is_over_monthly_limit until a later cache clear or reload; please either clear the Angular organization cache on PlanOverage or keep an organization invalidation for monthly overage transitions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 9bd0f02: the legacy organization cache is now cleared on PlanOverage, so its notification refresh reads current overage state.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bd0f02c0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), usage.Total, TimeSpan.FromHours(8));
await _organizationRepository.SaveAsync(organization);
// Routine usage updates stay silent, but clients need one refresh when an hourly throttle clears.
await _organizationRepository.SaveAsync(organization, o => o.Notifications(hourlyThrottleTransition?.Value ?? false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh organizations when monthly overage clears

When an organization is already over its monthly limit and the calendar rolls into a new month, the first usage save for the new month is a routine usage-only save with hourlyThrottleTransition false. Because this now calls Notifications(false), no OrganizationChanged is published, and there is no PlanOverage message for a clearing transition, so clients with cached is_over_monthly_limit: true can keep showing the monthly overage banner until a reload/focus refetch. Please keep a targeted organization notification or equivalent invalidation when monthly overage transitions from true to false.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in bf75993: the first usage save of a new month detects an over-limit previous-month record and emits one OrganizationChanged. Later buckets remain silent. The regression test failed before the fix with Expected 1, Actual 0; the full UsageServiceTests suite now passes 21 of 21.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bf759932be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

currentUsage.Discarded = realTimeUsage.CurrentUsage.Discarded;
currentUsage.TooBig = realTimeUsage.CurrentUsage.TooBig;
currentUsage.Deleted = realTimeUsage.CurrentUsage.Deleted;
viewOrganization.IsOverMonthlyLimit = currentUsage.Limit >= 0 && currentUsage.Total >= currentUsage.Limit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute overage against the live plan limit

When an organization's temporary event bonus expires without another usage bucket being saved, realTimeUsage.CurrentUsage.Limit still contains the bonus-inflated limit written by BillingManager.ApplyBonus, while GetMaxEventsPerMonthWithBonus already returns the lower effective limit. This assignment therefore overrides the mapper's previously correct IsOverMonthlyLimit value and can leave the API response and overage banner false even when the persisted-plus-pending total now exceeds the live limit; compare the total against viewOrganization.GetMaxEventsPerMonthWithBonus(timeProvider) instead.

AGENTS.md reference: AGENTS.md:L67-L67

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2aa10d3: real-time usage totals are now compared against the organization view model live effective monthly limit, so an expired bonus cannot keep the overage state false. The focused regression failed before the fix with Expected True, Actual False; OrganizationEndpointTests now pass 103 of 103 and the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2aa10d3966

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

var previousMonthUtc = currentMonthUtc.AddMonths(-1);
bool hasCurrentMonthUsage = organization.Usage.Any(u => u.Date.Year == currentMonthUtc.Year && u.Date.Month == currentMonthUtc.Month);
var previousMonthUsage = organization.Usage.FirstOrDefault(u => u.Date.Year == previousMonthUtc.Year && u.Date.Month == previousMonthUtc.Month);
bool monthlyOverageCleared = !hasCurrentMonthUsage && previousMonthUsage is { Limit: > 0 } && previousMonthUsage.Total >= previousMonthUsage.Limit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Detect month rollover against the live limit

Fresh evidence is the bonus-expiration case: if the previous usage record retains a bonus-inflated Limit of 110, its total is 50, and the bonus expires so the live limit becomes 10, the API now correctly shows the organization as over limit, but this comparison remains false at rollover. Consequently the first new-month save suppresses OrganizationChanged, leaving connected clients' monthly-overage banner visible after the usage period has reset. Detect the prior overage using the effective live limit or persist an explicit transition marker rather than relying on the historical usage limit.

AGENTS.md reference: AGENTS.md:L72-L75

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 638388c: month-rollover detection now compares the previous month total with the organization’s live effective monthly limit, matching the API behavior after a bonus expires. The regression reproduced the issue before the fix (Expected 1, Actual 0); the full UsageServiceTests suite now passes 22 of 22 and the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 638388c232

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), usage.Total, TimeSpan.FromHours(8));
await _organizationRepository.SaveAsync(organization);
// Routine usage updates stay silent, but clients need one refresh when an overage clears.
await _organizationRepository.SaveAsync(organization, o => o.Notifications((hourlyThrottleTransition?.Value ?? false) || monthlyOverageCleared));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Publish the expired-bonus overage transition

When a bonus expires while usage:limits:{id} still contains the bonus-inclusive value (cached for one day), GetEventsLeftAsync and IncrementTotalAsync continue using that stale limit and publish no PlanOverage, even if the live base limit has now been exceeded. This save recalculates effectiveMonthlyLimit, but notifications remain disabled because this is neither an hourly transition nor an overage-clear transition; the API-side recomputation only helps after a client already refetches. The fresh evidence beyond the resolved API issue is the one-day limit cache combined with this silent save, which leaves connected clients showing is_over_monthly_limit: false after the next usage bucket is persisted. Emit a targeted notification when the live monthly overage begins, or expire the cached limit when the bonus expires.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 22e8d0f: when a persisted bonus-inclusive limit becomes a lower live limit after that bonus expires, saving the bucket now publishes one monthly PlanOverage after the updated usage is persisted. The condition is scoped to the expired-bonus transition, so routine saves and plan changes do not duplicate it. The regression primes the stale limit cache and failed before the fix (Expected 1, Actual 0); all 23 UsageServiceTests now pass and the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22e8d0f470

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

usage.Deleted += bucketDeleted?.Value ?? 0;
bool expiredBonusChangedLimit = organization.BonusEventsPerMonth > 0
&& organization.BonusExpiration <= _timeProvider.GetUtcNow().UtcDateTime
&& recordedMonthlyLimit == organization.MaxEventsPerMonth + organization.BonusEventsPerMonth;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle bonus expiry without a persisted usage record

When the bonus-inclusive usage:limits cache was primed while the bonus was active but the organization has no persisted usage entry for the current month, events accepted after expiry create usage with the live base limit, so recordedMonthlyLimit already equals that base limit and this condition remains false. Because IncrementTotalAsync compared the same events against the stale bonus-inclusive cache, neither PlanOverage nor OrganizationChanged is published even when the bucket crosses the live limit, leaving connected clients unaware until another refresh. Fresh evidence beyond the resolved expired-bonus comment is the missing-current-month-record path; detect the transition independently of the historical usage limit and cover that legacy-record case.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 9b95689: expired-bonus transition detection now uses the stale cached ingestion limit rather than requiring a persisted current-month usage record. When that cache is bonus-inflated, the save removes it, persists usage against the live limit, and publishes one monthly PlanOverage if the organization newly becomes over limit. Coverage now includes both persisted and missing usage records and verifies enforcement returns zero events left afterward; all 24 UsageServiceTests pass.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up in 8b3a46c broadens the same transition guard to a legacy current-month record that already stores the live limit but was ingested against the stale bonus-inclusive cache. The cache is inspected only when the bucket moves from under its recorded limit to over the live expired-bonus limit. All three record shapes are covered; UsageServiceTests pass 25 of 25.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b3a46c600

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +119 to +120
var cachedMonthlyLimit = await _cache.GetAsync<int>($"usage:limits:{organizationId}");
expiredBonusChangedLimit = cachedMonthlyLimit.HasValue && cachedMonthlyLimit.Value > effectiveMonthlyLimit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain the stale limit until detecting the overage transition

When the bonus-inclusive usage:limits entry expires after IncrementTotalAsync accepted events but before the completed bucket is saved, this lookup returns no value and expiredBonusChangedLimit remains false. The save then persists usage above the live post-bonus limit with notifications disabled, and later increments generally cannot publish PlanOverage because the total is already beyond the crossing point. This narrow end-of-cache-lifetime window therefore leaves connected clients unaware of the monthly overage; preserve the ingestion limit used by the bucket in a transition marker rather than relying on the one-day cache entry still existing at save time.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 2392825: successful monthly PlanOverage publication now records a per-bucket transition marker. The usage save consumes that marker when evaluating an expired-bonus crossing, so it publishes only when ingestion did not already publish, independent of whether the one-day limit cache still exists. The save also clears the stale limit cache. Regressions cover cache expiry between increment/save and duplicate suppression; all 27 UsageServiceTests pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 239282518b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

message.change_type === ChangeType.Removed ||
message.organization_id !== currentOrganizationId.current ||
!message.project_id ||
!projectNeedsConfiguration ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record configuration events before project data loads

When a PersistentEventChanged message arrives while the projects query is still loading, projects is empty, so this guard discards the message instead of recording its project_id. If the initial query then returns the project with is_configured: false before the asynchronous SetProjectIsConfiguredWorkItem finishes, the setup notification remains visible until that job publishes ProjectChanged (or indefinitely if the job is delayed or fails), whereas the previous handler recorded the event regardless of query timing. Preserve valid IDs even when the project has not loaded yet.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 910499a: valid PersistentEventChanged project IDs are now recorded immediately for the current organization, without requiring the projects query to have loaded first. Once project data arrives, the local configured-ID set suppresses the stale setup notification without refetching organization or project data. Added a focused early-event regression; Svelte validation is clean and all 404 frontend unit tests pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 910499ab6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

await _organizationRepository.SaveAsync(organization);
await _organizationRepository.SaveAsync(organization, o => o.Notifications(false));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve overage transitions during usage migration

When this repeatable migration repairs a current-month usage total from below the live limit to at or above it, Notifications(false) removes the OrganizationChanged signal that previously refreshed connected clients, while this path publishes no replacement PlanOverage. Users with warm Svelte or Angular organization caches therefore continue seeing the organization as under limit until an unrelated refresh; suppress routine migration notifications, but emit a targeted signal when the repaired current-month total crosses the effective limit.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ee08a86: the repeatable usage repair migration now detects only a current-month transition from below the live effective monthly limit to at or above it, saves the repaired usage silently, then publishes one monthly PlanOverage. Repeat runs remain silent because the persisted total is already repaired. The regression reproduced the issue before the fix (Expected 1, Actual 0), runs the migration twice to prove one-shot behavior, and both UpdateEventUsageMigrationTests plus all 27 UsageServiceTests pass; the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee08a8612b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +97 to +99
bool hasCurrentMonthUsage = organization.Usage.Any(u => u.Date.Year == currentMonthUtc.Year && u.Date.Month == currentMonthUtc.Month);
var previousMonthUsage = organization.Usage.FirstOrDefault(u => u.Date.Year == previousMonthUtc.Year && u.Date.Month == previousMonthUtc.Month);
bool monthlyOverageCleared = !hasCurrentMonthUsage && effectiveMonthlyLimit > 0 && previousMonthUsage?.Total >= effectiveMonthlyLimit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Notify clears with preexisting current-month usage

When a below-limit current-month usage record already exists without a client notification—for example, UpdateEventUsage now creates the record from the current-month histogram and saves it with Notifications(false)—this presence check makes monthlyOverageCleared false. If connected clients still cache the previous month's overage state, this save and every later routine usage save remain silent, so the banner can persist indefinitely. Detect the transition from the previous month's overage independently of whether the current-month record exists, or persist an explicit clear marker.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed at the source in 740bba9: when UpdateEventUsage itself creates the first current-month usage record, it now detects an over-limit previous month followed by a repaired current-month total below the live effective limit and enables exactly one OrganizationChanged notification for that save. This preserves the existing one-shot behavior in UsageService and avoids turning every later routine usage save into another clear notification. The regression failed before the fix (Expected 1, Actual 0), runs the repeatable migration twice to verify the count stays at one, and all 3 migration tests plus all 27 UsageServiceTests pass; the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 740bba9b8f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

bool monthlyOverageCleared = !hadCurrentMonthUsage
&& effectiveMonthlyLimit > 0
&& previousMonthUsage?.Total >= effectiveMonthlyLimit
&& currentMonthUsage?.Total < effectiveMonthlyLimit;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat missing current-month usage as zero

When this repeatable migration runs after a month rollover for an organization that was previously over limit but has no events or usage record in the new month, currentMonthUsage is null, so the nullable comparison evaluates false and the save suppresses OrganizationChanged. Connected clients can therefore retain the previous month's overage banner until an unrelated refetch, even though the new month's effective usage is zero; handle a missing current-month record as zero when detecting this clearing transition.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 29f3c50: missing current-month usage is now evaluated as zero for the rollover clear transition. When that clears an over-limit previous month, the migration persists a zero-usage current-month record before saving with one OrganizationChanged notification; that record is the durable marker that keeps subsequent repeatable runs silent. The focused regression failed before the fix (Expected 1, Actual 0), runs the migration twice, and verifies one notification plus the persisted zero record. All 4 migration tests and all 27 UsageServiceTests pass; the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29f3c506fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

await _cache.SetAsync(GetTotalCacheKey(utcNow, organizationId), usage.Total, TimeSpan.FromHours(8));
await _organizationRepository.SaveAsync(organization);
// Routine usage updates stay silent, but clients need one refresh when an overage clears.
await _organizationRepository.SaveAsync(organization, o => o.Notifications((hourlyThrottleTransition?.Value ?? false) || monthlyOverageCleared));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve clears for pre-marker throttle buckets

During a rolling deployment, a previous-version worker can set the hourly throttle key and publish PlanOverage without writing the newly introduced throttled-transition marker; if a new-version worker later persists that completed bucket, this condition disables OrganizationChanged while also removing the throttle key. A connected client that handled the overage then retains is_throttled: true until another refresh. Derive the transition from the completed bucket counters when the marker is missing, or otherwise provide compatibility for buckets created before this marker existed.

AGENTS.md reference: AGENTS.md:L72-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 068d20a: completed buckets now prefer the explicit hourly transition marker, but when it is absent they derive the legacy rolling-deployment transition from the persisted bucket total and the effective throttle threshold. The threshold is calculated at the completed bucket timestamp so month-boundary buckets retain the old worker’s semantics. The regression covers both marker-present and marker-missing cases; the missing case failed before the fix (Expected 1, Actual 0). All 28 UsageServiceTests and all 4 migration tests pass, and the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 068d20af96

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +152 to +153
if (monthlyOverageStarted)
await _messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organization.Id });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain the bucket until overage publication succeeds

When the expired-bonus path reaches this call and PublishAsync fails transiently, the organization has already been saved above the live limit and the bucket counters, organization set, and transition marker have already been removed. Retrying SavePendingUsageAsync therefore cannot revisit this bucket, while later buckets see wasOverRecordedMonthlyLimit as true and will not publish the missing transition, so connected clients and notification workers can permanently miss the monthly overage. Keep durable transition state until publication succeeds or arrange a retryable/outbox-style publication.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 39e026f: organization buckets now remain discoverable until all organizations and required transitions complete. Monthly overage publication uses a per-bucket pending marker that survives counter cleanup and is removed only after PublishAsync succeeds, so a retry republishes without reapplying removed counters. The regression injects a publisher that fails once; before the fix the second save made only one attempt (Expected: 2, Actual: 1), and now it retries successfully. All 29 UsageServiceTests and all 4 migration tests pass; the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39e026f75d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 138 to 142
// Routine usage updates stay silent, but clients need one refresh when an overage clears.
await _organizationRepository.SaveAsync(organization, o => o.Notifications(hourlyThrottleCleared || monthlyOverageCleared));

await _cache.RemoveAllAsync(new[] {
GetBucketTotalCacheKey(bucketUtc, organizationId),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make retained bucket retries idempotent

When SaveAsync succeeds but the following RemoveAllAsync fails, the organization-set key is now deliberately retained, so the next SavePendingUsageAsync rereads the same counters and executes the additive usage updates again. A transient cache failure after the Elasticsearch write can therefore permanently double-count the entire five-minute bucket in both monthly and hourly usage, potentially triggering false throttling or overage; record per-organization completion or otherwise make replay idempotent before retaining the bucket for retries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 555028a: each organization bucket now writes a pending persistence state containing the intended monthly and hourly totals before saving. If a retained bucket is retried, the service compares that state with the persisted organization and skips the additive mutations and notification save when the write already landed; the marker is removed only after cleanup and required publication complete. The regression injects a one-time RemoveAllAsync failure after the repository save, retries the bucket, and verifies both monthly and hourly totals remain 1 rather than being doubled. All 30 UsageServiceTests pass and the build is clean.

Comment on lines +113 to +115
await _organizationRepository.SaveAsync(organization, o => o.Notifications(monthlyOverageCleared));
if (monthlyOverageStarted)
await _messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organization.Id });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve migration overage publication for retries

When this PlanOverage publication fails transiently, the preceding save has already persisted the repaired total at or above the limit and the exception is only logged by the surrounding catch. On the next repeatable migration run, the total no longer crosses the threshold, so monthlyOverageStarted remains false and the message is never retried, permanently skipping both connected-client invalidation and the overage notification worker; persist a pending transition until publication succeeds.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 555028a: UpdateEventUsage now writes a month-scoped pending overage-publication marker before saving the repaired organization, reads it on later repeatable runs, and removes it only after PlanOverage publishes successfully. A stale pending marker is discarded if the organization is no longer over the live effective limit. The regression uses a publisher that fails once, runs the migration twice, and verifies two publication attempts while the repaired total remains correct. All 5 migration tests pass and the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 555028a8b3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +126 to +127
if (monthlyOverageStarted)
await _cache.SetAsync(monthlyOveragePublicationPendingKey, true, TimeSpan.FromHours(8));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep migration retry markers until the next run

When PlanOverage publication fails, the surrounding catch retains only this cache marker, but it expires after eight hours. MigrationJob is non-continuous and invokes the migration manager only once at startup (src/Exceptionless.Core/Jobs/Elastic/MigrationJob.cs:11,35), so the next production run may occur on a restart long after the marker has expired; because the repaired usage is already at the limit, that run no longer detects a crossing and never retries the message. The fresh evidence beyond the earlier publication-retry finding is the combination of this finite TTL and the migration's non-continuous schedule; retain the pending transition durably or arrange an immediate scheduled retry.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 31cfa5f: the month-scoped migration publication marker no longer expires. It remains in the shared cache until a later startup run either publishes PlanOverage successfully or determines that the organization is no longer over its live effective limit. The failure-injection test now verifies the marker exists with no expiration after the failed run and is removed after the successful retry. All 5 migration tests pass and the build is clean.

Comment on lines +107 to +109
bool usageAlreadyPersisted = usagePersistencePending is { HasValue: true }
&& usagePersistencePending.Value.IsApplied(usage, hourlyUsage);
if (!usageAlreadyPersisted)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry clear notifications after the usage write succeeds

When a clear-side SaveAsync persists the organization but its OrganizationChanged publication fails transiently, the persistence marker remains and the retained bucket is retried. The freshly loaded usage then satisfies IsApplied, so this branch skips the only notification-producing save; cleanup subsequently removes the marker and bucket, leaving connected clients stuck with is_throttled or monthly-overage state until an unrelated refresh. Track clear-notification delivery separately and retry it even when the aggregate write is already applied.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 31cfa5f: clear delivery now has its own per-bucket pending marker written before the notification-producing save. If the aggregate write landed but OrganizationChanged publication failed, the persistence marker still prevents counter replay while the clear marker causes a notification-only retry; it is removed only after that save/publish completes. The regression fails the first BeforePublishEntityChanged attempt, retries successfully, verifies two delivery attempts, and confirms monthly/hourly totals were applied exactly once. All 31 UsageServiceTests pass and the build is clean.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 31cfa5fb08

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// This repeatable migration runs only at startup, so retain the retry marker until a later run publishes or invalidates it.
await _cache.SetAsync(monthlyOveragePublicationPendingKey, true);

await _organizationRepository.SaveAsync(organization, o => o.Notifications(monthlyOverageCleared));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry migration clear notifications after persistence

When this migration creates the first under-limit current-month usage record after an over-limit month, a transient EntityChanged publication failure from this notification-enabled SaveAsync is caught after Elasticsearch has persisted the record. On the next startup, hadCurrentMonthUsage is therefore true, so monthlyOverageCleared becomes false and the notification is never retried, leaving connected clients cached in the previous overage state. Unlike the normal usage path, this migration needs a pending clear-notification marker that is removed only after publication succeeds.

AGENTS.md reference: AGENTS.md:L74-L74

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cb541f7: the migration now writes a non-expiring, month-scoped clear-notification marker before the notification-enabled save, reads it on later startup runs, and removes it only after OrganizationChanged publication succeeds. The regression fails the first entity-change publication after persistence, verifies the marker remains without an expiration, then verifies the next run retries and clears it. While isolating that test, I also fixed the zero-event aggregation path so an absent histogram is treated as zero instead of returning before rollover-clear detection. Both zero-event tests now pass independently; all 6 migration tests, all 31 UsageServiceTests, and the build are green.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb541f786a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +516 to +517
await _messagePublisher.PublishAsync(new PlanOverage { OrganizationId = organizationId });
await _cache.SetAsync(GetMonthlyOverageTransitionKey(utcNow, organizationId), true, TimeSpan.FromHours(8));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist monthly publication before attempting it

When an ordinary monthly-limit crossing reaches this block and PublishAsync fails transiently, the transition marker is never written; subsequent increments no longer satisfy the exact crossing condition, and the bucket save neither finds a pending publication nor emits the now-suppressed OrganizationChanged. Unlike the resolved expired-bonus save path, this normal IncrementTotalAsync path therefore permanently loses the PlanOverage notification and leaves connected clients stale until an unrelated refresh, so record a retryable publication marker before attempting the publish.

Useful? React with 👍 / 👎.

@ejsmith
ejsmith force-pushed the issue/reduce-noisy-change-events branch from cb541f7 to 8895405 Compare August 6, 2026 04:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e96f54f36

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

_usageMonth = usageMonth;
$rootScope.$broadcast("OrganizationChanged");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass a payload to the rollover notification

At UTC month rollover, this broadcasts OrganizationChanged without event data, but components/filter/filter-service.js:262-264 unconditionally dereferences organizationChanged.id. Every active legacy client therefore raises a TypeError during the synthetic monthly refresh; broadcast a compatible payload such as an empty object or update the listener to tolerate missing data.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Core 75% 67% 9986
Exceptionless.AppHost 38% 40% 139
Exceptionless.Insulation 37% 35% 286
Exceptionless.Web 85% 69% 7024
Summary 78% (23456 / 30259) 67% (11010 / 16510) 17435

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant