[PM-38743] Honor a partially supplied event date range - #8265
Conversation
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]
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE Re-reviewed after Code Review DetailsNo findings. Notes that did not rise to findings:
|
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]
Codecov Report✅ All modified and coverable lines are covered by tests.
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. 🚀 New features to boost your workflow:
|
| if (start.Value > end.Value) | ||
| { | ||
| var newEnd = start; | ||
| start = end; | ||
| end = newEnd; | ||
| (start, end) = (end, start); | ||
| } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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]
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
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Nit: you could move the assignment of end to above this check, and then this line could be simplified to:
| start = end.HasValue ? ThirtyDaysBefore(end.Value) : DateTime.UtcNow.Date.AddDays(-30); | |
| start = ThirtyDaysBefore(end.Value); |
There was a problem hiding this comment.
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.
🎟️ Tracking
PM-38743
📔 Objective
Send only
startor onlyendto 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":
It lives in
ApiHelpers.GetDateRange(11 endpoints) and in a copy insideEventFilterRequestModel.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:
startonlystartthrough nowendonlyendCallers sending one date now get different results. That is intended, but it hits all 12 endpoints, including
GET /public/events(external callers) andGET /sm/events/service-accounts/{id}(Secrets Manager owns it, so a reviewer from that team would help).Three notes for reading the diff:
ToDateRangestill copies the resolved dates back onto the model. Looks redundant, isn't:EventDiagnosticLoggerreads them afterwards to log what was filtered. A test covers it.startalone 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.endresolves 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
Api.Testsuite: 2038 pass, 0 fail, including the 22 existing event tests, unchanged.GetDateRangehad no tests despite 11 endpoints using it. Added 13, covering every branch and theDateTimemin/max edges.main:?start=<2 hours ago>returns the same count as no query string at all. After this change it returns fewer.