Skip to content

[PM-38743] Honor a partially supplied event date range - #8265

Merged
AlexRubik merged 8 commits into
mainfrom
dirt/pm-38743/honor-partial-event-date-range
Sep 10, 2026
Merged

[PM-38743] Honor a partially supplied event date range#8265
AlexRubik merged 8 commits into
mainfrom
dirt/pm-38743/honor-partial-event-date-range

Conversation

@AlexRubik

@AlexRubik AlexRubik commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

PM-38743

📔 Objective

Send only start or only end to an event log endpoint and both dates get ignored. You silently get the last 30 days instead, with no error.

The check responsible treats "one date given" the same as "no dates given":

if (!end.HasValue || !start.HasValue)

It lives in ApiHelpers.GetDateRange (11 endpoints) and in a copy inside EventFilterRequestModel.ToDateRange (GET /public/events). This fixes the shared one and points the copy at it, so all 12 endpoints are fixed at once.

Each date is now handled on its own:

Request Range used
neither last 30 days
start only start through now
end only the 30 days before end
both unchanged

Callers sending one date now get different results. That is intended, but it hits all 12 endpoints, including GET /public/events (external callers) and GET /sm/events/service-accounts/{id} (Secrets Manager owns it, so a reviewer from that team would help).

Three notes for reading the diff:

  • ToDateRange still copies the resolved dates back onto the model. Looks redundant, isn't: EventDiagnosticLogger reads them afterwards to log what was filtered. A test covers it.
  • start alone more than 367 days back now returns 400 instead of 200. That is the existing range cap, which only applies now the date is used.
  • A missing end resolves to now, not the end of the current day (review feedback). That also tightens the no filter default, which has run through end of day for years. Same rows in practice, since event dates are stamped server side.

🤖 Testing

  • Full Api.Test suite: 2038 pass, 0 fail, including the 22 existing event tests, unchanged.
  • GetDateRange had no tests despite 11 endpoints using it. Added 13, covering every branch and the DateTime min/max edges.
  • Confirmed the new tests fail without the fix.
  • To see the bug on main: ?start=<2 hours ago> returns the same count as no query string at all. After this change it returns fewer.

GetDateRange discarded both bounds whenever either was missing, so
?start= or ?end= alone silently returned the default last 30 days across
all eleven callers. Resolve each bound independently instead: an absent
start anchors 30 days before the supplied end, an absent end runs to the
end of the current day. The inverted-range swap and the 367-day cap now
apply to every case rather than only to fully supplied ranges.

Standardizes the cap message on the more descriptive of the two variants
that existed, which is the one the Public API already returned.

[PM-38743]
EventFilterRequestModel.ToDateRange was a copy of ApiHelpers.GetDateRange
that had already diverged on its exception message. Delegate to the
shared helper so the Public API picks up the partial-range fix and there
is one implementation to maintain.

ToDateRange still writes the resolved bounds back onto the model:
EventDiagnosticLogger reads Start and End after this call to log the
query's effective filters.

[PM-38743]
The endpoint's Swagger remarks described only the no-filter default and
implied that a partial range fell back to it. Describe what each single
bound now resolves to, and state the 367-day cap that was already
enforced but never documented.

[PM-38743]
GetDateRange served eleven endpoints with no unit coverage, which is how
the partial-range bug shipped. Cover every branch: no bounds, start only,
end only, both, inverted, over the 367-day cap, and start-only past the
cap.

Adds EventFilterRequestModelTests to pin the write-back that
EventDiagnosticLogger depends on.

[PM-38743]
@AlexRubik AlexRubik added the ai-review Request a Claude code review label Aug 27, 2026
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-reviewed after bfaaa0531, which corrects the Start/End property docs to match the DateTime.UtcNow default introduced in dacecc7ad. The one open finding from the previous round — the End doc still claiming "end of the current day" while the resolved default had moved to UtcNow — is resolved, and no new findings replace it. Re-traced each branch of GetDateRange (per-bound inference, the ThirtyDaysBefore clamp, the swap, the 367-day cap), the write-back in ToDateRange that EventDiagnosticLogger.LogAggregateData reads, and all 12 call sites; the 13 helper tests and 2 model tests match the behavior the code implements, including the MinValue + 30d boundary and the MaxValue swap that resolves to a 400 rather than overflowing.

Code Review Details

No findings.

Notes that did not rise to findings:

  • A clamped DateTime.MinValue start is safe in both storage paths. Dapper binds @StartDate explicitly as DbType.DateTime2 with scale 7 (Infrastructure.Dapper/Dirt/Repositories/EventRepository.cs:197) against DATETIME2(7) stored-procedure parameters, so there is no SqlDateTime range problem; the EF path uses a LINQ range comparison. That value is also already reachable on main by supplying both bounds near MinValue, so this PR adds a route to existing behavior rather than a new failure mode.
  • The behavior change reaches all 12 entry points, not just the Public API, and a start-only request older than 367 days now returns 400 instead of 200. Both are called out explicitly in the description with a request for a Secrets Manager reviewer, so they are surfaced product decisions rather than defects.
  • With only start supplied and dated in the future, the swap makes the supplied value the end bound. ApiHelpers remarks cover this ("An inverted range is swapped") and a test pins it, though the Public API controller remark reads as if start is always the lower bound.
  • The ApiHelpers cap message changed from "Range too large." to "Date range must be < 367 days.", unifying the two paths. No remaining references to the old string in this repo; low impact unless an external consumer matches on it.
  • No checked-in OpenAPI artifacts exist in the repo, so the XML doc corrections need no regenerated spec committed alongside them.
  • Two unresolved human threads remain on ApiHelpers.cs: the future-dated end question, which dacecc7ad answers in code by resolving a missing end to UtcNow, and a preference for rejecting inverted ranges with 400 rather than swapping them. The latter is a design decision on pre-existing behavior and is left to the participants.
  • CI was mixed at review time — Lint, Analyze (csharp), Run tests, and Review reported pending, the rest passing — so nothing here is asserted about the build.

Comment thread src/Api/Utilities/ApiHelpers.cs Outdated
Inferring the start bound as end.AddDays(-30) threw
ArgumentOutOfRangeException for an end bound within 30 days of
DateTime.MinValue. ExceptionHandlerFilterAttribute has no branch for
that type, so it fell through to the catch-all and returned 500 with an
error-level log entry. Before this branch existed the end-only case
discarded the supplied value without doing arithmetic on it, so the same
request returned 200, which made this a new failure mode on all twelve
callers.

Clamp the inferred bound instead. GET /public/events?end=0001-01-10 now
resolves to MinValue through the supplied end and returns 200 with an
empty result set.

[PM-38743]
@AlexRubik AlexRubik added the t:bugfix Change Type - Bugfix label Sep 3, 2026
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.04%. Comparing base (4c664a9) to head (bfaaa05).
⚠️ Report is 7 commits behind head on main.

❗ There is a different number of reports uploaded between BASE (4c664a9) and HEAD (bfaaa05). Click for more details.

HEAD has 2 uploads less than BASE
Flag BASE (4c664a9) HEAD (bfaaa05)
3 1
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8265      +/-   ##
==========================================
- Coverage   69.56%   64.04%   -5.52%     
==========================================
  Files        2471     2473       +2     
  Lines      105932   106005      +73     
  Branches     9601     9613      +12     
==========================================
- Hits        73689    67896    -5793     
- Misses      29779    35750    +5971     
+ Partials     2464     2359     -105     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@AlexRubik
AlexRubik marked this pull request as ready for review September 7, 2026 21:59
@AlexRubik
AlexRubik requested a review from a team as a code owner September 7, 2026 21:59
@AlexRubik
AlexRubik requested a review from Banrion September 7, 2026 21:59
Comment thread src/Api/Utilities/ApiHelpers.cs Outdated
Comment on lines +98 to 101
if (start.Value > end.Value)
{
var newEnd = start;
start = end;
end = newEnd;
(start, end) = (end, start);
}

@lastbestdev lastbestdev Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This functionality is interesting to me. I know it was existing so perhaps we leave it alone, but I would personally prefer our API to return a 400 Bad Request to the caller when incorrect date parameters are provided.

This makes it clear that the consumer of the API is responsible for providing a sensible date range, and doesn't let them build bad integrations with our API that mix up the start/end params

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree in principle but I think this might be out of scope/need a separate ticket because main is accounting for this backward date correction and so it could break existing code that is supplying the backwards dates. So everyone's code that is supplying bad dates would then break with no warning. What do you think? Do you think it's enough of an edge case to just implement it in this PR and maybe a couple people learn the hard way? @lastbestdev

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree this bit is out of scope, and it would be something to discuss with the team if we should remove it. To your point, doing this should include the due diligence of checking our clients references to the endpoints providing date range parameters and ensuring they don't pass the start/end backwards

An absent end bound resolved to the last millisecond of the current
day, so every unfiltered or start-only event query reached into the
future. Event dates are stamped server side from DateTime.UtcNow, so
nothing legitimately lands there and the extra window only absorbs
clock skew between app servers.

Resolve to DateTime.UtcNow instead. This also tightens the no-filter
default, which has returned through end of day since before this
branch existed.

Addresses review feedback:
- default the missing end bound to UtcNow

[PM-38743]
Comment thread src/Api/Dirt/Public/Models/EventFilterRequestModel.cs Outdated
The previous commit resolved a missing end bound to DateTime.UtcNow and
updated the remarks on GetDateRange and the events controller, but left
the EventFilterRequestModel property docs describing the end-of-day
behavior they replaced. Api.csproj emits a DocumentationFile and
AddSwaggerGen includes every emitted XML, so this model's summaries are
published as the start and end query parameter descriptions on
GET /public/events. The spec therefore contradicted itself.

Also tightens the start summary, which described only the case where an
end bound is supplied. With neither supplied the start resolves to 30
days before today's date rather than 30 days before the resolved end.

[PM-38743]

@lastbestdev lastbestdev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One last nit, not critical

{
end = DateTime.UtcNow.Date.AddDays(1).AddMilliseconds(-1);
start = DateTime.UtcNow.Date.AddDays(-30);
start = end.HasValue ? ThirtyDaysBefore(end.Value) : DateTime.UtcNow.Date.AddDays(-30);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: you could move the assignment of end to above this check, and then this line could be simplified to:

Suggested change
start = end.HasValue ? ThirtyDaysBefore(end.Value) : DateTime.UtcNow.Date.AddDays(-30);
start = ThirtyDaysBefore(end.Value);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This wouldn't keep the same value, since .Date evaluates to the very beginning datetime for the date it is read from, but that should be no issue. It would be a true 30 days clock time back instead.

@AlexRubik
AlexRubik merged commit 19ebcf3 into main Sep 10, 2026
72 checks passed
@AlexRubik
AlexRubik deleted the dirt/pm-38743/honor-partial-event-date-range branch September 10, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:bugfix Change Type - Bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants