feat(reports): align first response distribution range
This commit is contained in:
@@ -566,7 +566,7 @@ func (h *AnalyticsHandler) FirstResponseTimeDistribution(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
since, until, ok := parseDateRange(c)
|
||||
since, until, ok := parseOptionalReportDateRange(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -581,6 +581,25 @@ func (h *AnalyticsHandler) FirstResponseTimeDistribution(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func parseOptionalReportDateRange(c *gin.Context) (since, until time.Time, ok bool) {
|
||||
sinceRaw := c.Query("since")
|
||||
untilRaw := c.Query("until")
|
||||
if sinceRaw == "" || untilRaw == "" {
|
||||
return time.Time{}, time.Time{}, true
|
||||
}
|
||||
since, err := parseChatwootReportTime(sinceRaw)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format")
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
until, err = parseChatwootReportTime(untilRaw)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format")
|
||||
return time.Time{}, time.Time{}, false
|
||||
}
|
||||
return since, until, true
|
||||
}
|
||||
|
||||
// OutgoingMessagesCount returns outgoing message count metrics.
|
||||
// GET /api/v1/accounts/:account_id/reports/outgoing_messages_count
|
||||
// Reference: Chatwoot reports#outgoing_messages_count
|
||||
|
||||
@@ -74,6 +74,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
|
||||
accounts.GET("/reports/conversation_traffic", s.handler.ConversationTraffic)
|
||||
accounts.GET("/reports/conversations", s.handler.Conversations)
|
||||
accounts.GET("/reports/conversations_summary", s.handler.ConversationsSummary)
|
||||
accounts.GET("/reports/first_response_time_distribution", s.handler.FirstResponseTimeDistribution)
|
||||
accounts.GET("/reports/inbox_label_matrix", s.handler.InboxLabelMatrix)
|
||||
accounts.GET("/reports/outgoing_messages_count", s.handler.OutgoingMessagesCount)
|
||||
s.router = r
|
||||
@@ -514,6 +515,21 @@ func (s *AnalyticsHandlerTestSuite) TestOutgoingMessagesCount_InvalidGroupByRetu
|
||||
s.Empty(w.Body.String())
|
||||
}
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestFirstResponseTimeDistribution_AllowsMissingRange() {
|
||||
inbox := model.Inbox{AccountID: s.accountID, Name: "Web", ChannelType: "web_widget", ChannelID: 1, Enabled: true}
|
||||
s.Require().NoError(s.db.Create(&inbox).Error)
|
||||
s.Require().NoError(s.db.Create(&model.ReportingEvent{AccountID: s.accountID, Name: model.MetricNameFirstResponse, Value: 4000, InboxID: &inbox.ID}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reports/first_response_time_distribution", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
var payload map[string]map[string]int64
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
|
||||
s.Equal(int64(1), payload["web_widget"]["1-4h"])
|
||||
}
|
||||
|
||||
// ========== Nil service guard ==========
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestNilService() {
|
||||
|
||||
@@ -237,6 +237,24 @@ func TestAnalyticsOutgoingMessagesCountLabelUsesAccountLabelName(t *testing.T) {
|
||||
assert.Equal(t, int64(1), rows[0]["outgoing_messages_count"])
|
||||
}
|
||||
|
||||
func TestAnalyticsFirstResponseTimeDistributionAllowsMissingRange(t *testing.T) {
|
||||
db, svc, account, inbox, _, user, _ := setupAnalyticsP513Test(t)
|
||||
old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
recent := time.Date(2026, 6, 9, 0, 0, 0, 0, time.UTC)
|
||||
require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: old}, AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 120, InboxID: &inbox.ID, UserID: &user.ID}).Error)
|
||||
require.NoError(t, db.Create(&model.ReportingEvent{Base: model.Base{CreatedAt: recent}, AccountID: account.ID, Name: model.MetricNameFirstResponse, Value: 90000, InboxID: &inbox.ID, UserID: &user.ID}).Error)
|
||||
|
||||
allDistribution, err := svc.GetFirstResponseTimeDistribution(context.Background(), account.ID, time.Time{}, time.Time{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), allDistribution.(map[string]map[string]int64)["web_widget"]["0-1h"])
|
||||
assert.Equal(t, int64(1), allDistribution.(map[string]map[string]int64)["web_widget"]["24h+"])
|
||||
|
||||
rangedDistribution, err := svc.GetFirstResponseTimeDistribution(context.Background(), account.ID, recent.Add(-time.Hour), recent.Add(time.Hour))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(0), rangedDistribution.(map[string]map[string]int64)["web_widget"]["0-1h"])
|
||||
assert.Equal(t, int64(1), rangedDistribution.(map[string]map[string]int64)["web_widget"]["24h+"])
|
||||
}
|
||||
|
||||
func TestAnalyticsTimeseriesAndRollupWorker(t *testing.T) {
|
||||
db, svc, account, inbox, contact, user, _ := setupAnalyticsP513Test(t)
|
||||
since := time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -853,7 +853,11 @@ func (s *AnalyticsService) firstResponseTimeDistribution(ctx context.Context, ac
|
||||
return nil, err
|
||||
}
|
||||
var events []model.ReportingEvent
|
||||
if err := db.WithContext(ctx).Where("account_id = ? AND name = ? AND created_at >= ? AND created_at < ?", accountID, model.MetricNameFirstResponse, since, until).Find(&events).Error; err != nil {
|
||||
eventQ := db.WithContext(ctx).Where("account_id = ? AND name = ?", accountID, model.MetricNameFirstResponse)
|
||||
if !since.IsZero() && !until.IsZero() {
|
||||
eventQ = eventQ.Where("created_at >= ? AND created_at < ?", since, until)
|
||||
}
|
||||
if err := eventQ.Find(&events).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inboxIDs := make([]uint, 0, len(events))
|
||||
|
||||
Reference in New Issue
Block a user