Skip to content

fix: report last_access silently drops records on longer time ranges - #68

Open
isss802 wants to merge 1 commit into
akamai:mainfrom
isss802:fix/last-access-cap-truncation
Open

fix: report last_access silently drops records on longer time ranges#68
isss802 wants to merge 1 commit into
akamai:mainfrom
isss802:fix/last-access-cap-truncation

Conversation

@isss802

@isss802 isss802 commented Aug 20, 2026

Copy link
Copy Markdown

report last_access returns 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:

2 users accessed the application for this time range, 500 records processed
1 API calls issued.

500 records processed on 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/query endpoint caps the number of records it returns per call. The API reference documents a limit maximum 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 reaches LIMIT_ACCESS_REPORT (5,000) records:

LIMIT_ACCESS_REPORT = 5000  #: API limit, 5000 is the max
...
if len(sub_records) < ReportingAPI.LIMIT_ACCESS_REPORT:  # We got the right level of subsplitting, carry on

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

  1. Split on the documented limit. A new SPLIT_THRESHOLD (250) replaces LIMIT_ACCESS_REPORT in the subdivision test. The limit request parameter is unchanged.
  2. Verify the effective cap at runtime. A response in [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.
  3. Stop splitting at 60s, and say so. Sub ranges are clamped so none goes below 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 summary WARNING: ... 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)

users records API calls
before 2 500 1
after 7 3,023 34

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 -a filtering are unaffected.

I also ran the existing suite (pytest test.py) against a live tenant on main and on this branch: the set of failing tests is identical (pre-existing failures in test_useraccess_log_* and test_connector_allowlist_*), and test_report_last_access passes on both.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant