mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(api): add trend analysis and extended periods to dashboard summary
- Add `include_trends` query parameter to enable trend calculation - Implement trend comparison logic (delta % and direction) against previous periods - Add support for `last7d`, `last30d`, and `all` time period options - Update `DashboardSummaryResponse` to include optional `trends` field - Add helper functions for custom time window aggregation - Add unit tests for trend calculation and period window logic - Update feature documentation with new parameters and response schemas
This commit is contained in:
@@ -72,15 +72,39 @@
|
||||
|
||||
| 参数 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `period` | string | 预设周期: `today`, `week`, `month` |
|
||||
| `period` | string | 预设周期: `today`, `week`, `month`, `last7d`, `last30d`, `all` |
|
||||
| `since` | int | 自定义起始时间 (Unix 秒) |
|
||||
| `until` | int | 自定义结束时间 (Unix 秒) |
|
||||
| `include_trends` | bool | 是否包含趋势数据 (与前一周期对比) |
|
||||
|
||||
> **已知限制** (待 fix-dashboard-summary-api 实现后解决):
|
||||
> - `period=week` 使用自然周起点 (周一),非滚动 7 天
|
||||
> - `period=month` 使用自然月起点 (1号),非滚动 30 天
|
||||
> - 趋势数据 (delta/trend) 当前不返回,前端需隐藏趋势指示器
|
||||
> - 建议使用 `since`/`until` 实现精确的滚动时间窗口
|
||||
**周期类型说明**:
|
||||
|
||||
| 周期值 | 时间窗口 | 趋势对比周期 |
|
||||
|--------|----------|--------------|
|
||||
| `today` | 今日 00:00 UTC → 当前 | 昨日 |
|
||||
| `week` | 本周一 00:00 UTC → 当前 | 上一自然周 |
|
||||
| `month` | 本月 1 日 00:00 UTC → 当前 | 上一自然月 |
|
||||
| `last7d` | 当前 - 7 天 → 当前 | 14 天前 → 7 天前 |
|
||||
| `last30d` | 当前 - 30 天 → 当前 | 60 天前 → 30 天前 |
|
||||
| `all` | 无时间过滤 | 不支持趋势 |
|
||||
|
||||
**趋势数据** (当 `include_trends=true` 时返回):
|
||||
|
||||
```json
|
||||
{
|
||||
"trends": {
|
||||
"requests": { "delta": 12.5, "direction": "up" },
|
||||
"tokens": { "delta": -1.1, "direction": "down" },
|
||||
"error_rate": { "delta": 0.0, "direction": "stable" },
|
||||
"latency": { "delta": -5.2, "direction": "down" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `delta` | 与前一周期相比的百分比变化 (四舍五入到 1 位小数) |
|
||||
| `direction` | `up` (增长 >0.5%), `down` (下降 <-0.5%), `stable`, `new` (无基准数据) |
|
||||
|
||||
### 3.4 告警摘要
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -42,6 +44,52 @@ func (h *DashboardHandler) logBaseQuery() *gorm.DB {
|
||||
return logBaseQuery(h.logDBConn(), h.logPartitioner)
|
||||
}
|
||||
|
||||
// TrendInfo contains trend calculation results
|
||||
type TrendInfo struct {
|
||||
Delta *float64 `json:"delta,omitempty"` // Percentage change from previous period (nil if no baseline)
|
||||
Direction string `json:"direction,omitempty"` // "up", "down", "stable", or "new" (no baseline)
|
||||
}
|
||||
|
||||
// CalculateTrend calculates the trend between current and previous values.
|
||||
// Returns TrendInfo with delta percentage and direction.
|
||||
func CalculateTrend(current, previous int64) TrendInfo {
|
||||
if previous == 0 {
|
||||
if current == 0 {
|
||||
return TrendInfo{Direction: "stable"}
|
||||
}
|
||||
return TrendInfo{Direction: "new"}
|
||||
}
|
||||
delta := float64(current-previous) / float64(previous) * 100
|
||||
// Round to 1 decimal place
|
||||
delta = math.Round(delta*10) / 10
|
||||
direction := "stable"
|
||||
if delta > 0.5 {
|
||||
direction = "up"
|
||||
} else if delta < -0.5 {
|
||||
direction = "down"
|
||||
}
|
||||
return TrendInfo{Delta: &delta, Direction: direction}
|
||||
}
|
||||
|
||||
// CalculateTrendFloat calculates the trend between current and previous float values.
|
||||
func CalculateTrendFloat(current, previous float64) TrendInfo {
|
||||
if previous == 0 {
|
||||
if current == 0 {
|
||||
return TrendInfo{Direction: "stable"}
|
||||
}
|
||||
return TrendInfo{Direction: "new"}
|
||||
}
|
||||
delta := (current - previous) / previous * 100
|
||||
delta = math.Round(delta*10) / 10
|
||||
direction := "stable"
|
||||
if delta > 0.5 {
|
||||
direction = "up"
|
||||
} else if delta < -0.5 {
|
||||
direction = "down"
|
||||
}
|
||||
return TrendInfo{Delta: &delta, Direction: direction}
|
||||
}
|
||||
|
||||
// RequestStats contains request-related statistics
|
||||
type RequestStats struct {
|
||||
Total int64 `json:"total"`
|
||||
@@ -83,6 +131,14 @@ type TopModelStat struct {
|
||||
Tokens int64 `json:"tokens"`
|
||||
}
|
||||
|
||||
// DashboardTrends contains trend data for dashboard metrics
|
||||
type DashboardTrends struct {
|
||||
Requests TrendInfo `json:"requests"`
|
||||
Tokens TrendInfo `json:"tokens"`
|
||||
ErrorRate TrendInfo `json:"error_rate"`
|
||||
Latency TrendInfo `json:"latency"`
|
||||
}
|
||||
|
||||
// DashboardSummaryResponse is the response for dashboard summary endpoint
|
||||
type DashboardSummaryResponse struct {
|
||||
Period string `json:"period,omitempty"`
|
||||
@@ -93,6 +149,7 @@ type DashboardSummaryResponse struct {
|
||||
Keys CountStats `json:"keys"`
|
||||
ProviderKeys ProviderKeyStats `json:"provider_keys"`
|
||||
TopModels []TopModelStat `json:"top_models"`
|
||||
Trends *DashboardTrends `json:"trends,omitempty"` // Only present when include_trends=true
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -102,9 +159,10 @@ type DashboardSummaryResponse struct {
|
||||
// @Tags admin
|
||||
// @Produce json
|
||||
// @Security AdminAuth
|
||||
// @Param period query string false "time period: today, week, month, all"
|
||||
// @Param since query int false "unix seconds"
|
||||
// @Param until query int false "unix seconds"
|
||||
// @Param period query string false "time period: today, week, month, last7d, last30d, all"
|
||||
// @Param since query int false "unix seconds"
|
||||
// @Param until query int false "unix seconds"
|
||||
// @Param include_trends query bool false "include trend data comparing to previous period"
|
||||
// @Success 200 {object} DashboardSummaryResponse
|
||||
// @Failure 400 {object} gin.H
|
||||
// @Failure 500 {object} gin.H
|
||||
@@ -116,6 +174,9 @@ func (h *DashboardHandler) GetSummary(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse include_trends parameter
|
||||
includeTrends := c.Query("include_trends") == "true"
|
||||
|
||||
// Build log query with time range
|
||||
logQuery := h.logBaseQuery()
|
||||
logQuery = applyStatsRange(logQuery, rng)
|
||||
@@ -235,6 +296,32 @@ func (h *DashboardHandler) GetSummary(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate trends if requested
|
||||
var trends *DashboardTrends
|
||||
if includeTrends && rng.Period != "" && rng.Period != "all" {
|
||||
prevStart, prevEnd := previousPeriodWindow(rng.Period)
|
||||
if !prevStart.IsZero() && !prevEnd.IsZero() {
|
||||
prevStats, err := h.aggregateFromLogRecords(prevStart, prevEnd)
|
||||
if err == nil {
|
||||
prevErrorRate := 0.0
|
||||
if prevStats.Requests > 0 {
|
||||
prevErrorRate = float64(prevStats.Failed) / float64(prevStats.Requests)
|
||||
}
|
||||
prevAvgLatency := 0.0
|
||||
if prevStats.Requests > 0 {
|
||||
prevAvgLatency = float64(prevStats.LatencySumMs) / float64(prevStats.Requests)
|
||||
}
|
||||
|
||||
trends = &DashboardTrends{
|
||||
Requests: CalculateTrend(totalRequests, prevStats.Requests),
|
||||
Tokens: CalculateTrend(ts.TokensIn+ts.TokensOut, prevStats.TokensIn+prevStats.TokensOut),
|
||||
ErrorRate: CalculateTrendFloat(errorRate, prevErrorRate),
|
||||
Latency: CalculateTrendFloat(ts.AvgLatency, prevAvgLatency),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, DashboardSummaryResponse{
|
||||
Period: rng.Period,
|
||||
Requests: RequestStats{
|
||||
@@ -266,6 +353,147 @@ func (h *DashboardHandler) GetSummary(c *gin.Context) {
|
||||
AutoDisabled: autoDisabledProviderKeys,
|
||||
},
|
||||
TopModels: topModelStats,
|
||||
Trends: trends,
|
||||
UpdatedAt: time.Now().UTC().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// aggregatedStats holds aggregated statistics from daily_stats or log_records
|
||||
type aggregatedStats struct {
|
||||
Requests int64
|
||||
Success int64
|
||||
Failed int64
|
||||
TokensIn int64
|
||||
TokensOut int64
|
||||
LatencySumMs int64
|
||||
}
|
||||
|
||||
// previousPeriodWindow calculates the comparison window for trend calculation
|
||||
func previousPeriodWindow(period string) (start, end time.Time) {
|
||||
now := time.Now().UTC()
|
||||
startOfDay := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
// Compare to yesterday
|
||||
yesterday := startOfDay.AddDate(0, 0, -1)
|
||||
return yesterday, startOfDay
|
||||
case "last7d":
|
||||
// Compare: last 7 days vs 14-7 days ago
|
||||
return now.AddDate(0, 0, -14), now.AddDate(0, 0, -7)
|
||||
case "last30d":
|
||||
// Compare: last 30 days vs 60-30 days ago
|
||||
return now.AddDate(0, 0, -60), now.AddDate(0, 0, -30)
|
||||
case "week":
|
||||
// Compare to previous calendar week
|
||||
weekday := int(startOfDay.Weekday())
|
||||
if weekday == 0 {
|
||||
weekday = 7
|
||||
}
|
||||
currentWeekStart := startOfDay.AddDate(0, 0, -(weekday - 1))
|
||||
prevWeekStart := currentWeekStart.AddDate(0, 0, -7)
|
||||
return prevWeekStart, currentWeekStart
|
||||
case "month":
|
||||
// Compare to previous calendar month
|
||||
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
|
||||
prevMonthStart := currentMonthStart.AddDate(0, -1, 0)
|
||||
return prevMonthStart, currentMonthStart
|
||||
default:
|
||||
return time.Time{}, time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateFromDailyStats sums statistics from daily_stats table for the given date range
|
||||
func (h *DashboardHandler) aggregateFromDailyStats(startDate, endDate string) (aggregatedStats, error) {
|
||||
var stats aggregatedStats
|
||||
err := h.db.Model(&model.DailyStat{}).
|
||||
Select(`
|
||||
COALESCE(SUM(requests), 0) as requests,
|
||||
COALESCE(SUM(success), 0) as success,
|
||||
COALESCE(SUM(failed), 0) as failed,
|
||||
COALESCE(SUM(tokens_in), 0) as tokens_in,
|
||||
COALESCE(SUM(tokens_out), 0) as tokens_out,
|
||||
COALESCE(SUM(latency_sum_ms), 0) as latency_sum_ms
|
||||
`).
|
||||
Where("date >= ? AND date <= ?", startDate, endDate).
|
||||
Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// aggregateFromLogRecords queries log_records directly for the given time range
|
||||
func (h *DashboardHandler) aggregateFromLogRecords(start, end time.Time) (aggregatedStats, error) {
|
||||
var stats struct {
|
||||
Requests int64
|
||||
Success int64
|
||||
Failed int64
|
||||
TokensIn int64
|
||||
TokensOut int64
|
||||
LatencySumMs int64
|
||||
}
|
||||
|
||||
err := h.logBaseQuery().
|
||||
Select(`
|
||||
COUNT(*) as requests,
|
||||
SUM(CASE WHEN status_code >= 200 AND status_code < 400 THEN 1 ELSE 0 END) as success,
|
||||
SUM(CASE WHEN status_code >= 400 OR status_code = 0 THEN 1 ELSE 0 END) as failed,
|
||||
COALESCE(SUM(tokens_in), 0) as tokens_in,
|
||||
COALESCE(SUM(tokens_out), 0) as tokens_out,
|
||||
COALESCE(SUM(latency_ms), 0) as latency_sum_ms
|
||||
`).
|
||||
Where("created_at >= ? AND created_at < ?", start, end).
|
||||
Scan(&stats).Error
|
||||
|
||||
return aggregatedStats{
|
||||
Requests: stats.Requests,
|
||||
Success: stats.Success,
|
||||
Failed: stats.Failed,
|
||||
TokensIn: stats.TokensIn,
|
||||
TokensOut: stats.TokensOut,
|
||||
LatencySumMs: stats.LatencySumMs,
|
||||
}, err
|
||||
}
|
||||
|
||||
// getTopModelsFromDailyStats aggregates top models from daily_stats JSON field
|
||||
func (h *DashboardHandler) getTopModelsFromDailyStats(startDate, endDate string) ([]TopModelStat, error) {
|
||||
var dailyStats []model.DailyStat
|
||||
if err := h.db.Where("date >= ? AND date <= ?", startDate, endDate).Find(&dailyStats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Aggregate top models from all days
|
||||
modelMap := make(map[string]TopModelStat)
|
||||
for _, ds := range dailyStats {
|
||||
var topModels []model.TopModelStat
|
||||
if err := json.Unmarshal([]byte(ds.TopModels), &topModels); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, tm := range topModels {
|
||||
existing := modelMap[tm.Model]
|
||||
existing.Model = tm.Model
|
||||
existing.Requests += tm.Requests
|
||||
existing.Tokens += tm.Tokens
|
||||
modelMap[tm.Model] = existing
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to slice and sort by requests
|
||||
result := make([]TopModelStat, 0, len(modelMap))
|
||||
for _, tm := range modelMap {
|
||||
result = append(result, tm)
|
||||
}
|
||||
|
||||
// Simple bubble sort for top 10 (small dataset)
|
||||
for i := 0; i < len(result)-1; i++ {
|
||||
for j := i + 1; j < len(result); j++ {
|
||||
if result[j].Requests > result[i].Requests {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) > 10 {
|
||||
result = result[:10]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
237
internal/api/dashboard_handler_test.go
Normal file
237
internal/api/dashboard_handler_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCalculateTrend(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current int64
|
||||
previous int64
|
||||
wantDelta *float64
|
||||
wantDirection string
|
||||
}{
|
||||
{
|
||||
name: "zero previous, zero current - stable",
|
||||
current: 0,
|
||||
previous: 0,
|
||||
wantDelta: nil,
|
||||
wantDirection: "stable",
|
||||
},
|
||||
{
|
||||
name: "zero previous, positive current - new",
|
||||
current: 100,
|
||||
previous: 0,
|
||||
wantDelta: nil,
|
||||
wantDirection: "new",
|
||||
},
|
||||
{
|
||||
name: "100% increase - up",
|
||||
current: 200,
|
||||
previous: 100,
|
||||
wantDelta: ptr(100.0),
|
||||
wantDirection: "up",
|
||||
},
|
||||
{
|
||||
name: "50% decrease - down",
|
||||
current: 50,
|
||||
previous: 100,
|
||||
wantDelta: ptr(-50.0),
|
||||
wantDirection: "down",
|
||||
},
|
||||
{
|
||||
name: "0.3% increase - stable",
|
||||
current: 1003,
|
||||
previous: 1000,
|
||||
wantDelta: ptr(0.3),
|
||||
wantDirection: "stable",
|
||||
},
|
||||
{
|
||||
name: "0.5% increase - stable (boundary)",
|
||||
current: 1005,
|
||||
previous: 1000,
|
||||
wantDelta: ptr(0.5),
|
||||
wantDirection: "stable",
|
||||
},
|
||||
{
|
||||
name: "0.6% increase - up",
|
||||
current: 1006,
|
||||
previous: 1000,
|
||||
wantDelta: ptr(0.6),
|
||||
wantDirection: "up",
|
||||
},
|
||||
{
|
||||
name: "0.6% decrease - down",
|
||||
current: 994,
|
||||
previous: 1000,
|
||||
wantDelta: ptr(-0.6),
|
||||
wantDirection: "down",
|
||||
},
|
||||
{
|
||||
name: "no change - stable",
|
||||
current: 100,
|
||||
previous: 100,
|
||||
wantDelta: ptr(0.0),
|
||||
wantDirection: "stable",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := CalculateTrend(tt.current, tt.previous)
|
||||
|
||||
if tt.wantDelta == nil {
|
||||
if got.Delta != nil {
|
||||
t.Errorf("expected nil delta, got %v", *got.Delta)
|
||||
}
|
||||
} else {
|
||||
if got.Delta == nil {
|
||||
t.Errorf("expected delta %v, got nil", *tt.wantDelta)
|
||||
} else if *got.Delta != *tt.wantDelta {
|
||||
t.Errorf("expected delta %v, got %v", *tt.wantDelta, *got.Delta)
|
||||
}
|
||||
}
|
||||
|
||||
if got.Direction != tt.wantDirection {
|
||||
t.Errorf("expected direction %q, got %q", tt.wantDirection, got.Direction)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateTrendFloat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
current float64
|
||||
previous float64
|
||||
wantDelta *float64
|
||||
wantDirection string
|
||||
}{
|
||||
{
|
||||
name: "zero previous, zero current - stable",
|
||||
current: 0,
|
||||
previous: 0,
|
||||
wantDelta: nil,
|
||||
wantDirection: "stable",
|
||||
},
|
||||
{
|
||||
name: "zero previous, positive current - new",
|
||||
current: 0.5,
|
||||
previous: 0,
|
||||
wantDelta: nil,
|
||||
wantDirection: "new",
|
||||
},
|
||||
{
|
||||
name: "50% increase - up",
|
||||
current: 0.15,
|
||||
previous: 0.10,
|
||||
wantDelta: ptr(50.0),
|
||||
wantDirection: "up",
|
||||
},
|
||||
{
|
||||
name: "33.3% decrease - down",
|
||||
current: 0.10,
|
||||
previous: 0.15,
|
||||
wantDelta: ptr(-33.3),
|
||||
wantDirection: "down",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := CalculateTrendFloat(tt.current, tt.previous)
|
||||
|
||||
if tt.wantDelta == nil {
|
||||
if got.Delta != nil {
|
||||
t.Errorf("expected nil delta, got %v", *got.Delta)
|
||||
}
|
||||
} else {
|
||||
if got.Delta == nil {
|
||||
t.Errorf("expected delta %v, got nil", *tt.wantDelta)
|
||||
} else if *got.Delta != *tt.wantDelta {
|
||||
t.Errorf("expected delta %v, got %v", *tt.wantDelta, *got.Delta)
|
||||
}
|
||||
}
|
||||
|
||||
if got.Direction != tt.wantDirection {
|
||||
t.Errorf("expected direction %q, got %q", tt.wantDirection, got.Direction)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviousPeriodWindow(t *testing.T) {
|
||||
// These tests verify the structure of the period calculation
|
||||
// Not testing exact timestamps since they depend on current time
|
||||
|
||||
tests := []struct {
|
||||
period string
|
||||
expectZero bool
|
||||
expectWindow bool
|
||||
}{
|
||||
{"today", false, true},
|
||||
{"last7d", false, true},
|
||||
{"last30d", false, true},
|
||||
{"week", false, true},
|
||||
{"month", false, true},
|
||||
{"all", true, false},
|
||||
{"invalid", true, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.period, func(t *testing.T) {
|
||||
start, end := previousPeriodWindow(tt.period)
|
||||
|
||||
if tt.expectZero {
|
||||
if !start.IsZero() || !end.IsZero() {
|
||||
t.Errorf("expected zero times for period %q", tt.period)
|
||||
}
|
||||
} else {
|
||||
if start.IsZero() || end.IsZero() {
|
||||
t.Errorf("expected non-zero times for period %q", tt.period)
|
||||
}
|
||||
if !start.Before(end) {
|
||||
t.Errorf("expected start < end for period %q", tt.period)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviousPeriodWindowToday(t *testing.T) {
|
||||
start, end := previousPeriodWindow("today")
|
||||
|
||||
// Previous period for "today" should be "yesterday"
|
||||
now := time.Now().UTC()
|
||||
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
|
||||
yesterday := startOfToday.AddDate(0, 0, -1)
|
||||
|
||||
if !start.Equal(yesterday) {
|
||||
t.Errorf("expected start to be yesterday (%v), got %v", yesterday, start)
|
||||
}
|
||||
if !end.Equal(startOfToday) {
|
||||
t.Errorf("expected end to be start of today (%v), got %v", startOfToday, end)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreviousPeriodWindowLast7d(t *testing.T) {
|
||||
start, end := previousPeriodWindow("last7d")
|
||||
|
||||
now := time.Now().UTC()
|
||||
expected14DaysAgo := now.AddDate(0, 0, -14)
|
||||
expected7DaysAgo := now.AddDate(0, 0, -7)
|
||||
|
||||
// Allow 1 second tolerance for test execution time
|
||||
if start.Sub(expected14DaysAgo).Abs() > time.Second {
|
||||
t.Errorf("expected start ~14 days ago, got %v (expected %v)", start, expected14DaysAgo)
|
||||
}
|
||||
if end.Sub(expected7DaysAgo).Abs() > time.Second {
|
||||
t.Errorf("expected end ~7 days ago, got %v (expected %v)", end, expected7DaysAgo)
|
||||
}
|
||||
}
|
||||
|
||||
func ptr(v float64) *float64 {
|
||||
return &v
|
||||
}
|
||||
Reference in New Issue
Block a user