Skip to content

feat(usage): add flexible time windows and include current month in usage analytics - #1501

Closed
jaiswalabhishek377 wants to merge 2 commits into
getnao:mainfrom
jaiswalabhishek377:feat/usage-flexible-time-windows
Closed

feat(usage): add flexible time windows and include current month in usage analytics#1501
jaiswalabhishek377 wants to merge 2 commits into
getnao:mainfrom
jaiswalabhishek377:feat/usage-flexible-time-windows

Conversation

@jaiswalabhishek377

@jaiswalabhishek377 jaiswalabhishek377 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #1482

Summary of Changes

1. Flexible Time Windows

  • Added Last 7 days, Last 30 days, Last 60 days, and Last 90 days options to the usage analytics filter alongside existing Last 24 hours, Last 15 days, and Last 6 months.
  • Decoupled period (time lookback window) from granularity (SQL aggregation bucket size).
  • Sane default granularities are derived automatically from the chosen period (24h -> hour, 7d..90d -> day, 6m -> month), while still supporting independent granularity overrides if needed.

2. Root Causes & Fix for Missing Current Month

  • Backend Lookback: Previously, getLookbackTimestamp('month') subtracted an approximate 180 days (now - 6 * 30 * 24 * 60 * 60 * 1000), causing boundary cutoffs.
    It now calculates the 1st day of the month 5 months ago at 00:00:00 UTC, covering the 5 preceding complete months plus the active current month (6 months total).
  • Client-Side Timezone Shift: Earlier format(new Date('YYYY-MM'), 'MMM yyyy') was parsing month strings as UTC midnight (2026-08-01T00:00:00Z). In timezones with negative UTC offsets (e.g. US/Canada UTC-4 to UTC-8), this timestamp resolved to July 31st local time, shifting all 6 month labels backward by 1 month and dropping August.
    fix-Added local date component parsing (formatChartXAxisLabel) to preserve the exact month string.

3. Backward Compatibility & Test Coverage

  • Existing URLs and bookmarks using ?granularity=... are seamlessly parsed and mapped to the corresponding period.
  • Added unit tests in @nao/backend verifying 30d, 60d, 90d, and 6m lookback query outputs.

Key Files Modified

  • apps/backend/src/types/usage.ts: Added usagePeriodSchema ('24h' | '7d' | '15d' | '30d' | '60d' | '90d' | '6m') and updated usageFilterSchema.
  • apps/backend/src/utils/date.ts: Added PERIOD_CONFIG, resolvePeriodAndGranularity, updated getLookbackTimestamp, generateDateSeries, and fillMissingDates.
  • apps/backend/src/queries/usage.queries.ts: Resolved period and granularity in getMessagesUsage and getTotalUsage.
  • apps/backend/tests/usage-queries.test.ts: Added unit tests for 30d, 60d, 90d, and 6m lookbacks.
  • apps/frontend/src/components/settings/usage-route-search.ts: Added period to search params, storage, and fallback validators.
  • apps/frontend/src/components/settings/usage-filters.tsx: Added new period options to dropdown.
  • apps/frontend/src/routes/_sidebar-layout.settings.usage.tsx: Passed period to queries and added timezone-safe formatChartXAxisLabel.

Testing & Verification

  • Automated Tests: Ran npm run test -w @nao/backend -- tests/usage-queries.test.ts — all 4 test suites passed.
  • Lint & Type Checks: Ran npm run lint across @nao/backend, @nao/frontend, and @nao/shared with 0 errors.
  • Manual Local Verification:
    • Tested all 7 options (Last 24 hours, Last 7 days, Last 15 days, Last 30 days, Last 60 days, Last 90 days, and Last 6 months) on localhost:3000.
    • Verified URL query search synchronization and filter persistence.
    • Verified that Last 6 months accurately renders the active current month (August) across different timezones without offset shifts.

Screenshots / Proof of Fix

Before:

  • Only 3 fixed options in dropdown:
bfusagedates
  • Last 6 Months missing active current month (i.e. August):
last6monthsbefore

After:

  • All 7 Flexible Time Windows:
newdropdown
  • Last 6 Months cleanly rendering active current month (i.e. August):
new6monthsafter
  • Newly added Last 7 30 60 and 90 Days Daily Views:
last7days last30daysafter last60days last90daysafter
  • The 24h and 15 Days:
last24hourafter last15daysafter

Tested Locally on the running frontend and backend.

Review in cubic

@github-actions

Copy link
Copy Markdown
Contributor

This PR was auto-closed. Only contributors approved with lgtm can open PRs. Open an issue first.

Maintainers review auto-closed issues daily. Issues that do not meet the quality bar in CONTRIBUTING.md will not be reopened or receive a reply.

If a maintainer replies lgtmi, your future issues will stay open. If a maintainer replies lgtm, your future issues and PRs will stay open.

See CONTRIBUTING.md.

@github-actions github-actions Bot closed this Aug 27, 2026
@jaiswalabhishek377

Copy link
Copy Markdown
Contributor Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@jaiswalabhishek377 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

5 issues found across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/backend/src/utils/date.ts">

<violation number="1" location="apps/backend/src/utils/date.ts:39">
P2: When a usage query crosses a UTC boundary, separate `new Date()` snapshots can make its SQL window and zero-filled series use different periods. Resolve the window once and pass that snapshot or resolved result through the query and date-series generation.</violation>

<violation number="2" location="apps/backend/src/utils/date.ts:60">
P2: When a period is overridden to a finer bucket, `Math.round` adds a bucket before the requested lookback after the current bucket is sufficiently far underway. For example, `6m` with daily granularity can render December 31 even though the query starts January 1. Compute the count from UTC bucket boundaries instead of rounded elapsed milliseconds.</violation>

<violation number="3" location="apps/backend/src/utils/date.ts:60">
P2: When a non-default granularity is selected, rounding elapsed time counts a partial current bucket as a full extra bucket. This makes `6m`/`day` render a date before its SQL lookback, and day-period/hour renders an extra hour after `:30`; derive counts from UTC bucket boundaries instead.</violation>
</file>

<file name="apps/frontend/src/routes/_sidebar-layout.settings.usage.tsx">

<violation number="1" location="apps/frontend/src/routes/_sidebar-layout.settings.usage.tsx:104">
P2: For `day` granularity, `value` is a UTC `YYYY-MM-DD` bucket, but `new Date(value)` parses it at UTC midnight. Users west of UTC therefore see every daily label one day early; parse date components as local values before formatting.</violation>
</file>

<file name="apps/backend/src/queries/usage.queries.ts">

<violation number="1" location="apps/backend/src/queries/usage.queries.ts:259">
P2: When `period: '6m'` is requested with `granularity: 'day'` after 12:00 UTC, this returns an extra leading zero day. Compute the series length from UTC calendar dates or pass the resolved start/count through instead of letting `fillMissingDates` recompute it with rounded elapsed time.</violation>
</file>

count: number;
startDate: Date;
} {
const now = new Date();

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.

P2: When a usage query crosses a UTC boundary, separate new Date() snapshots can make its SQL window and zero-filled series use different periods. Resolve the window once and pass that snapshot or resolved result through the query and date-series generation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/utils/date.ts, line 39:

<comment>When a usage query crosses a UTC boundary, separate `new Date()` snapshots can make its SQL window and zero-filled series use different periods. Resolve the window once and pass that snapshot or resolved result through the query and date-series generation.</comment>

<file context>
@@ -9,24 +12,67 @@ export function isValidIsoDateString(s: string): boolean {
+	count: number;
+	startDate: Date;
+} {
+	const now = new Date();
+	const period =
+		options?.period && PERIOD_CONFIG[options.period]
</file context>

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.

✅ Addressed in b304103

(now.getUTCMonth() - startDate.getUTCMonth()) +
1;
} else if (granularity === 'day') {
count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1);

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.

P2: When a period is overridden to a finer bucket, Math.round adds a bucket before the requested lookback after the current bucket is sufficiently far underway. For example, 6m with daily granularity can render December 31 even though the query starts January 1. Compute the count from UTC bucket boundaries instead of rounded elapsed milliseconds.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/utils/date.ts, line 60:

<comment>When a period is overridden to a finer bucket, `Math.round` adds a bucket before the requested lookback after the current bucket is sufficiently far underway. For example, `6m` with daily granularity can render December 31 even though the query starts January 1. Compute the count from UTC bucket boundaries instead of rounded elapsed milliseconds.</comment>

<file context>
@@ -9,24 +12,67 @@ export function isValidIsoDateString(s: string): boolean {
+			(now.getUTCMonth() - startDate.getUTCMonth()) +
+			1;
+	} else if (granularity === 'day') {
+		count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1);
+	} else {
+		count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (60 * 60 * 1000)) + 1);
</file context>

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.

✅ Addressed in b304103

const [y, m] = value.split('-').map(Number);
return format(new Date(y, m - 1, 1), dateFormats.month);
}
return format(new Date(value), dateFormats[granularity]);

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.

P2: For day granularity, value is a UTC YYYY-MM-DD bucket, but new Date(value) parses it at UTC midnight. Users west of UTC therefore see every daily label one day early; parse date components as local values before formatting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/frontend/src/routes/_sidebar-layout.settings.usage.tsx, line 104:

<comment>For `day` granularity, `value` is a UTC `YYYY-MM-DD` bucket, but `new Date(value)` parses it at UTC midnight. Users west of UTC therefore see every daily label one day early; parse date components as local values before formatting.</comment>

<file context>
@@ -95,6 +96,14 @@ function UsagePage() {
+		const [y, m] = value.split('-').map(Number);
+		return format(new Date(y, m - 1, 1), dateFormats.month);
+	}
+	return format(new Date(value), dateFormats[granularity]);
+}
+
</file context>
Suggested change
return format(new Date(value), dateFormats[granularity]);
if (granularity === 'day' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
const [y, m, d] = value.split('-').map(Number);
return format(new Date(y, m - 1, d), dateFormats.day);
}
return format(new Date(value), dateFormats[granularity]);

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.

✅ Addressed in b304103

.groupBy(({ date }) => date);

return fillMissingDates(rows.map(normalizeMessageUsageRow), granularity);
return fillMissingDates(rows.map(normalizeMessageUsageRow), granularity, period);

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.

P2: When period: '6m' is requested with granularity: 'day' after 12:00 UTC, this returns an extra leading zero day. Compute the series length from UTC calendar dates or pass the resolved start/count through instead of letting fillMissingDates recompute it with rounded elapsed time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/queries/usage.queries.ts, line 259:

<comment>When `period: '6m'` is requested with `granularity: 'day'` after 12:00 UTC, this returns an extra leading zero day. Compute the series length from UTC calendar dates or pass the resolved start/count through instead of letting `fillMissingDates` recompute it with rounded elapsed time.</comment>

<file context>
@@ -253,12 +256,16 @@ export const getMessagesUsage = async (projectId: string, filter: UsageFilter):
 		.groupBy(({ date }) => date);
 
-	return fillMissingDates(rows.map(normalizeMessageUsageRow), granularity);
+	return fillMissingDates(rows.map(normalizeMessageUsageRow), granularity, period);
 };
 
</file context>

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.

✅ Addressed in b304103

(now.getUTCMonth() - startDate.getUTCMonth()) +
1;
} else if (granularity === 'day') {
count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1);

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.

P2: When a non-default granularity is selected, rounding elapsed time counts a partial current bucket as a full extra bucket. This makes 6m/day render a date before its SQL lookback, and day-period/hour renders an extra hour after :30; derive counts from UTC bucket boundaries instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/utils/date.ts, line 60:

<comment>When a non-default granularity is selected, rounding elapsed time counts a partial current bucket as a full extra bucket. This makes `6m`/`day` render a date before its SQL lookback, and day-period/hour renders an extra hour after `:30`; derive counts from UTC bucket boundaries instead.</comment>

<file context>
@@ -9,24 +12,67 @@ export function isValidIsoDateString(s: string): boolean {
+			(now.getUTCMonth() - startDate.getUTCMonth()) +
+			1;
+	} else if (granularity === 'day') {
+		count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1);
+	} else {
+		count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (60 * 60 * 1000)) + 1);
</file context>

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.

✅ Addressed in b304103

@jaiswalabhishek377

Copy link
Copy Markdown
Contributor Author

Hi @Bl3f

I’ve resolved all of them in the latest commits on my branch (commit b304103) with 100% passing tests and lint checks.
Could you please reopen PR #1501 so the latest commits attach for review and I can again trigger cubic-ai-review if it looks good i have attached screenshots.

Branch: main...jaiswalabhishek377:nao:feat/usage-flexible-time-windows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feature] Flexible time windows in the usage analytics filter (30/60/90 days)

1 participant