fix: report last_access silently drops records on longer time ranges - #68
Open
isss802 wants to merge 1 commit into
Open
fix: report last_access silently drops records on longer time ranges#68isss802 wants to merge 1 commit into
isss802 wants to merge 1 commit into
Conversation
The application-reports/ops/query endpoint caps the number of records returned per call (250 documented; 500 observed as of 2026-08) without any error or truncation marker. last_access() only subdivided the time range when a response reached LIMIT_ACCESS_REPORT (5000) records, which the server-side cap makes unreachable, so busy ranges were silently truncated to their newest records. - Split when a response reaches SPLIT_THRESHOLD (250, the documented limit maximum) instead of 5000. The limit request parameter is unchanged. - Verify the effective cap at runtime: a response in [VERIFY_THRESHOLD, SPLIT_THRESHOLD) that is larger than any verified response is checked once against its two halves, compared by (uid, ts); if the halves contain records the parent response did not, the threshold is lowered to the observed cap. Verification is skipped near the current time where newly arriving events would look like a cap. - Clamp the number of sub ranges so no sub range goes below MIN_RANGE_DURATION (60s); when a range at that duration still hits the cap, its remainder is unreachable: accept what the server returned, count it, and print a summary WARNING that results may be INCOMPLETE instead of failing silently. - max(1, ...) on the oversplit path is defensive only; it is not reachable with num_sub_ranges == 1 today. Verified on a demo tenant, identical 3-month window: before 2 users / 500 records / 1 API call, after 7 users / 3,023 records / 34 API calls, matching an independent aggregation of the raw access logs. Short ranges and -a filtering behave as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
report last_accessreturns incomplete results for longer time ranges, with no error, warning or truncation marker. Users who were active in the requested window are simply missing from the report.Impact
This command is typically used for access reviews and license cleanups, where a missing user means "this person did not use EAA" — so silent truncation turns into a wrong deactivation decision. The failure is invisible: the command exits 0 and prints a plausible-looking report.
How to tell if you are affected
Look at the footer of any run over a busy time range:
500 records processedon a range that certainly has more activity means the response was capped. In my tenant every sufficiently busy range returned exactly 500 records, whatever the range length.Root cause
The
application-reports/ops/queryendpoint caps the number of records it returns per call. The API reference documents alimitmaximum of 250; the effective cap I observe is 500 (as of August 2026). Either way, nothing in the response indicates that truncation happened.last_access()subdivides the time range only when a response reachesLIMIT_ACCESS_REPORT(5,000) records:Because the server never returns 5,000 records, that condition is always true, the subdivision never triggers, and each range is silently reduced to its newest ~500 records.
Reproduction on a tenant with a few thousand events over 3 months: a single 3-month run reports 2 users / 500 records. Splitting the same window into three one-month runs returns 500 records each and surfaces users the single run missed. The raw access logs (
akamai eaa log access) show 7 unique userids for that window.Fix
SPLIT_THRESHOLD(250) replacesLIMIT_ACCESS_REPORTin the subdivision test. Thelimitrequest parameter is unchanged.[100, SPLIT_THRESHOLD)that is larger than any previously verified response is checked once against its two halves (compared by(uid, ts), the only fields this report aggregates). If the halves contain records the parent response did not, that response was capped, and the threshold drops to the observed cap. Verification is skipped for ranges ending near the current time, where events arriving between the two calls would look like a cap.MIN_RANGE_DURATION(60s). If a 60s range still hits the cap, its remainder is genuinely unreachable through this endpoint: the command keeps what the server returned and prints a summaryWARNING: ... Results may be INCOMPLETE.rather than pretending the report is complete.Why 250 and not the observed 500? 250 is the value the API reference documents, so it is the safer default: assuming a cap that is too low only costs extra API calls, while assuming one that is too high loses records silently — the exact failure being fixed here. Step 2 exists because the effective cap is undocumented and may differ per tenant or change over time; hardcoding any single number would reintroduce the same class of bug.
Cost: correctness here means more API calls — 1 → 34 for the 3-month window below, roughly one call per 125–250 records. The documented rate limit for this API is 25 requests/minute, so large tenants over long windows will want retry handling; I have that as a follow-up rather than mixing it into this fix.
Verification (demo tenant, identical 3-month window)
The "after" user set and timestamps match an independent aggregation of the raw access logs exactly. Short ranges (default 1 week: 160 records, 1 call) and
-afiltering are unaffected.I also ran the existing suite (
pytest test.py) against a live tenant onmainand on this branch: the set of failing tests is identical (pre-existing failures intest_useraccess_log_*andtest_connector_allowlist_*), andtest_report_last_accesspasses on both.