[GH-ISSUE #3910] Bug: DMARC alignment_stats period week is shifted due to mixing isocalendar() and %W #1932

Closed
opened 2026-02-27 11:19:57 +03:00 by kerem · 0 comments
Owner

Originally created by @jg-shinji-ueda on GitHub (Feb 9, 2026).
Original GitHub issue: https://github.com/modoboa/modoboa/issues/3910

Impacted versions

  • OS Type: Debian/Ubuntu
  • OS Version: Ubuntu 24.04
  • Database Type: PostgreSQL
  • Database version: 16+257build1.1
  • Modoboa: 2.6.5
  • installer used: Yes
  • Webserver: Nginx
  • TIME_ZONE (Django): Asia/Tokyo

Steps to reproduce

  1. Import DMARC aggregate reports into Modoboa (Modoboa DMARC app).
  2. Open DMARC alignment stats in UI and note it behaves like a week selector (UI shows a specific week range).
  3. Call the API for the same domain and different period values.

Example requests (replace host/domain IDs as needed):

  1. Verify in DB that DMARC reports exist only up to early Feb 2026 UTC (sample output below).

Current behavior

period=YYYY-WW appears to represent a week, but the computed week range is inconsistent / shifted by one week.

Evidence: DB data range (Reports exist only through 2026-02-07 UTC)

Report range example:

  • Report count: 17
  • start_date min: 2026-01-25 00:00:00+00:00
  • start_date max: 2026-02-07 00:00:00+00:00
  • policy_domain: example.com (17)

Daily counts example:

Report by day:

  • 2026-01-25 1
  • 2026-01-26 2
  • 2026-01-27 1
  • 2026-01-28 1
  • 2026-01-29 1
  • 2026-01-30 1
  • 2026-01-31 1
  • 2026-02-01 1
  • 2026-02-02 3
  • 2026-02-03 1
  • 2026-02-04 1
  • 2026-02-05 1
  • 2026-02-06 1
  • 2026-02-07 1

Record by report.start_date day:

  • 2026-01-25 4
  • 2026-01-26 6
  • 2026-01-27 4
  • 2026-01-28 5
  • 2026-01-29 5
  • 2026-01-30 4
  • 2026-01-31 1
  • 2026-02-01 6
  • 2026-02-02 7
  • 2026-02-03 5
  • 2026-02-04 3
  • 2026-02-05 5
  • 2026-02-06 2
  • 2026-02-07 3

Evidence: the UI week shown for "2026-06"

When requesting period=2026-6, the UI indicates the week range:
2026/2/2 - 2026/2/8 (week view)

However, the backend interpretation can shift depending on ISO week vs %W week numbering.

Evidence: actual report count in the "UI week range"

For the week window 2026-02-02 .. 2026-02-08 (UTC range [2026-02-02, 2026-02-09)):

  • Report count: 7
  • policy_domain in that window: example.com (7)

For the next week window 2026-02-09 .. 2026-02-15 (UTC range [2026-02-09, 2026-02-16)):

  • Report count: 0

So a one-week shift explains why one period value returns data and the next returns empty.

Root cause analysis (code-level)

In Modoboa 2.6.5, alignment_stats calls:

  • modoboa/dmarc/api/v2/viewsets.py -> lib.get_aligment_stats(domain, period)
  • modoboa/dmarc/lib.py:
    • get_aligment_stats(domain, period=None) sets default period using:
      timezone.now().isocalendar() (ISO week)
    • week_range(year, weeknumber) parses year-week with:
      datetime.strptime(..., "%Y-%W-%w") (%W week numbering)

This mixes ISO week numbering and %W week numbering. The same YYYY-WW can point to different date ranges.

Concrete example (Python):

  • ISO week:

    • date.fromisocalendar(2026, 5, 1) -> 2026-01-26 (Mon)
    • ISO week 5 => 2026-01-26 .. 2026-02-01
  • %W week:

    • datetime.strptime("2026-5-1", "%Y-%W-%w") -> 2026-02-02 (Mon)
    • %W week 5 => 2026-02-02 .. 2026-02-08

Therefore, depending on which definition is used, the "week number" shifts by one week around this boundary.

Expected behavior

The meaning of period=YYYY-WW should be clearly defined (ISO week or %W week numbering) and handled consistently end-to-end.

Proposal: backward compatible fix (A + period_mode)

To clarify period definition while keeping backward compatibility:

  • Add a query parameter period_mode:
    • period_mode=iso: interpret period as ISO-8601 week number
    • period_mode=w (or strftime): interpret period as %W week number (legacy)
  • Keep default behavior compatible with current implementation (choose whichever the project considers "current").
  • When period is omitted (default previous week), compute the "previous week" using the SAME mode as week_range uses, to avoid mixing isocalendar() with %W.

Alternative possible approach:

  • Fix frontend to generate period values that match backend semantics.
    However, because backend currently mixes ISO and %W, backend contract definition should be clarified first.

Note: We investigated this behavior with the help of ChatGPT and summarized the findings here to speed up triage and reproduction.

Originally created by @jg-shinji-ueda on GitHub (Feb 9, 2026). Original GitHub issue: https://github.com/modoboa/modoboa/issues/3910 # Impacted versions * OS Type: Debian/Ubuntu * OS Version: Ubuntu 24.04 * Database Type: PostgreSQL * Database version: 16+257build1.1 * Modoboa: 2.6.5 * installer used: Yes * Webserver: Nginx * TIME_ZONE (Django): Asia/Tokyo # Steps to reproduce 1. Import DMARC aggregate reports into Modoboa (Modoboa DMARC app). 2. Open DMARC alignment stats in UI and note it behaves like a week selector (UI shows a specific week range). 3. Call the API for the same domain and different `period` values. Example requests (replace host/domain IDs as needed): - Request: https://mail.example.com/api/v2/domains/2/dmarc/alignment_stats/?period=2026-5 Response: non-empty stats - Request: https://mail.example.com/api/v2/domains/2/dmarc/alignment_stats/?period=2026-6 Response: {"aligned":{},"trusted":{},"forwarded":{},"failed":{}} - Request: https://mail.example.com/api/v2/domains/2/dmarc/alignment_stats/?period=2026-7 Response: {"aligned":{},"trusted":{},"forwarded":{},"failed":{}} 4. Verify in DB that DMARC reports exist only up to early Feb 2026 UTC (sample output below). # Current behavior `period=YYYY-WW` appears to represent a week, but the computed week range is inconsistent / shifted by one week. ## Evidence: DB data range (Reports exist only through 2026-02-07 UTC) Report range example: - Report count: 17 - start_date min: 2026-01-25 00:00:00+00:00 - start_date max: 2026-02-07 00:00:00+00:00 - policy_domain: example.com (17) Daily counts example: Report by day: - 2026-01-25 1 - 2026-01-26 2 - 2026-01-27 1 - 2026-01-28 1 - 2026-01-29 1 - 2026-01-30 1 - 2026-01-31 1 - 2026-02-01 1 - 2026-02-02 3 - 2026-02-03 1 - 2026-02-04 1 - 2026-02-05 1 - 2026-02-06 1 - 2026-02-07 1 Record by report.start_date day: - 2026-01-25 4 - 2026-01-26 6 - 2026-01-27 4 - 2026-01-28 5 - 2026-01-29 5 - 2026-01-30 4 - 2026-01-31 1 - 2026-02-01 6 - 2026-02-02 7 - 2026-02-03 5 - 2026-02-04 3 - 2026-02-05 5 - 2026-02-06 2 - 2026-02-07 3 ## Evidence: the UI week shown for "2026-06" When requesting `period=2026-6`, the UI indicates the week range: 2026/2/2 - 2026/2/8 (week view) However, the backend interpretation can shift depending on ISO week vs `%W` week numbering. ## Evidence: actual report count in the "UI week range" For the week window 2026-02-02 .. 2026-02-08 (UTC range [2026-02-02, 2026-02-09)): - Report count: 7 - policy_domain in that window: example.com (7) For the next week window 2026-02-09 .. 2026-02-15 (UTC range [2026-02-09, 2026-02-16)): - Report count: 0 So a one-week shift explains why one `period` value returns data and the next returns empty. ## Root cause analysis (code-level) In Modoboa 2.6.5, `alignment_stats` calls: - modoboa/dmarc/api/v2/viewsets.py -> lib.get_aligment_stats(domain, period) - modoboa/dmarc/lib.py: - get_aligment_stats(domain, period=None) sets default period using: timezone.now().isocalendar() (ISO week) - week_range(year, weeknumber) parses year-week with: datetime.strptime(..., "%Y-%W-%w") (%W week numbering) This mixes ISO week numbering and `%W` week numbering. The same `YYYY-WW` can point to different date ranges. Concrete example (Python): - ISO week: - date.fromisocalendar(2026, 5, 1) -> 2026-01-26 (Mon) - ISO week 5 => 2026-01-26 .. 2026-02-01 - %W week: - datetime.strptime("2026-5-1", "%Y-%W-%w") -> 2026-02-02 (Mon) - %W week 5 => 2026-02-02 .. 2026-02-08 Therefore, depending on which definition is used, the "week number" shifts by one week around this boundary. # Expected behavior The meaning of `period=YYYY-WW` should be clearly defined (ISO week or `%W` week numbering) and handled consistently end-to-end. # Proposal: backward compatible fix (A + period_mode) To clarify period definition while keeping backward compatibility: - Add a query parameter `period_mode`: - period_mode=iso: interpret `period` as ISO-8601 week number - period_mode=w (or strftime): interpret `period` as `%W` week number (legacy) - Keep default behavior compatible with current implementation (choose whichever the project considers "current"). - When `period` is omitted (default previous week), compute the "previous week" using the SAME mode as week_range uses, to avoid mixing isocalendar() with %W. Alternative possible approach: - Fix frontend to generate `period` values that match backend semantics. However, because backend currently mixes ISO and %W, backend contract definition should be clarified first. Note: We investigated this behavior with the help of ChatGPT and summarized the findings here to speed up triage and reproduction.
kerem closed this issue 2026-02-27 11:19:57 +03:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/modoboa-modoboa#1932
No description provided.