-
Notifications
You must be signed in to change notification settings - Fork 243
feat(usage): add flexible time windows and include current month in usage analytics #1501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| import type { Granularity, UsageRecord } from '../types/usage'; | ||
| import type { Granularity, UsagePeriod, UsageRecord } from '../types/usage'; | ||
| import { DEFAULT_PERIOD_BY_GRANULARITY, PERIOD_CONFIG } from '../types/usage'; | ||
|
|
||
| export { DEFAULT_PERIOD_BY_GRANULARITY, PERIOD_CONFIG }; | ||
|
|
||
| export function isValidIsoDateString(s: string): boolean { | ||
| if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) { | ||
|
|
@@ -9,24 +12,67 @@ export function isValidIsoDateString(s: string): boolean { | |
| return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d; | ||
| } | ||
|
|
||
| export const lookbackPeriods = { | ||
| hour: 24, | ||
| day: 15, | ||
| month: 6, | ||
| }; | ||
| export function getPeriodStartDate(period: UsagePeriod, now: Date = new Date()): Date { | ||
| const config = PERIOD_CONFIG[period] ?? PERIOD_CONFIG['15d']; | ||
| const date = new Date(now); | ||
|
|
||
| export function getLookbackTimestamp(granularity: Granularity): number { | ||
| const now = Date.now(); | ||
| const periods = lookbackPeriods[granularity]; | ||
| if (config.granularity === 'month') { | ||
| return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() - (config.count - 1), 1, 0, 0, 0, 0)); | ||
| } | ||
|
|
||
| switch (granularity) { | ||
| case 'hour': | ||
| return now - periods * 60 * 60 * 1000; | ||
| case 'day': | ||
| return now - periods * 24 * 60 * 60 * 1000; | ||
| case 'month': | ||
| return now - periods * 30 * 24 * 60 * 60 * 1000; | ||
| if (config.granularity === 'hour') { | ||
| date.setUTCHours(date.getUTCHours() - (config.count - 1), 0, 0, 0); | ||
| return date; | ||
| } | ||
|
|
||
| date.setUTCDate(date.getUTCDate() - (config.count - 1)); | ||
| date.setUTCHours(0, 0, 0, 0); | ||
| return date; | ||
| } | ||
|
|
||
| export function resolvePeriodAndGranularity(options?: { period?: UsagePeriod; granularity?: Granularity }): { | ||
| period: UsagePeriod; | ||
| granularity: Granularity; | ||
| count: number; | ||
| startDate: Date; | ||
| } { | ||
| const now = new Date(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a usage query crosses a UTC boundary, separate Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Addressed in b304103 |
||
| const period = | ||
| options?.period && PERIOD_CONFIG[options.period] | ||
| ? options.period | ||
| : options?.granularity && DEFAULT_PERIOD_BY_GRANULARITY[options.granularity] | ||
| ? DEFAULT_PERIOD_BY_GRANULARITY[options.granularity] | ||
| : '15d'; | ||
|
|
||
| const defaultGranularity = PERIOD_CONFIG[period].granularity; | ||
| const granularity = options?.granularity ?? defaultGranularity; | ||
| const startDate = getPeriodStartDate(period, now); | ||
|
|
||
| let count: number; | ||
| if (granularity === defaultGranularity) { | ||
| count = PERIOD_CONFIG[period].count; | ||
| } else if (granularity === 'month') { | ||
| count = | ||
| (now.getUTCFullYear() - startDate.getUTCFullYear()) * 12 + | ||
| (now.getUTCMonth() - startDate.getUTCMonth()) + | ||
| 1; | ||
| } else if (granularity === 'day') { | ||
| count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (24 * 60 * 60 * 1000)) + 1); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: When a period is overridden to a finer bucket, Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Addressed in b304103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Prompt for AI agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✅ Addressed in b304103 |
||
| } else { | ||
| count = Math.max(1, Math.round((now.getTime() - startDate.getTime()) / (60 * 60 * 1000)) + 1); | ||
| } | ||
|
|
||
| return { | ||
| period, | ||
| granularity, | ||
| count, | ||
| startDate, | ||
| }; | ||
| } | ||
|
|
||
| export function getLookbackTimestamp(granularity?: Granularity, period?: UsagePeriod): number { | ||
| const resolved = resolvePeriodAndGranularity({ period, granularity }); | ||
| return resolved.startDate.getTime(); | ||
| } | ||
|
|
||
| export function formatDate(date: Date, granularity: Granularity): string { | ||
|
|
@@ -45,15 +91,15 @@ export function formatDate(date: Date, granularity: Granularity): string { | |
| } | ||
| } | ||
|
|
||
| export function generateDateSeries(granularity: Granularity): string[] { | ||
| export function generateDateSeries(granularity?: Granularity, period?: UsagePeriod): string[] { | ||
| const resolved = resolvePeriodAndGranularity({ period, granularity }); | ||
| const dates: string[] = []; | ||
| const periods = lookbackPeriods[granularity]; | ||
| const now = new Date(); | ||
|
|
||
| for (let i = periods - 1; i >= 0; i--) { | ||
| for (let i = resolved.count - 1; i >= 0; i--) { | ||
| const date = new Date(now); | ||
|
|
||
| switch (granularity) { | ||
| switch (resolved.granularity) { | ||
| case 'hour': | ||
| date.setUTCHours(date.getUTCHours() - i, 0, 0, 0); | ||
| break; | ||
|
|
@@ -67,7 +113,7 @@ export function generateDateSeries(granularity: Granularity): string[] { | |
| break; | ||
| } | ||
|
|
||
| dates.push(formatDate(date, granularity)); | ||
| dates.push(formatDate(date, resolved.granularity)); | ||
| } | ||
|
|
||
| return dates; | ||
|
|
@@ -97,9 +143,14 @@ export function formatCurrentDate(timezone?: string): string { | |
| return tz === 'UTC' ? `${formatted} (UTC)` : `${formatted} (${tz})`; | ||
| } | ||
|
|
||
| export function fillMissingDates(records: UsageRecord[], granularity: Granularity): UsageRecord[] { | ||
| export function fillMissingDates( | ||
| records: UsageRecord[], | ||
| granularity?: Granularity, | ||
| period?: UsagePeriod, | ||
| ): UsageRecord[] { | ||
| const resolved = resolvePeriodAndGranularity({ period, granularity }); | ||
| const dateSet = new Map(records.map((r) => [r.date, r])); | ||
| const allDates = generateDateSeries(granularity); | ||
| const allDates = generateDateSeries(resolved.granularity, resolved.period); | ||
|
|
||
| return allDates.map( | ||
| (date) => | ||
|
|
||
There was a problem hiding this comment.
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 withgranularity: '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 lettingfillMissingDatesrecompute it with rounded elapsed time.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅ Addressed in b304103