feat(reports): add analytics timeseries rollups
This commit is contained in:
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -21,6 +22,41 @@ func NewAnalyticsHandler(svc *service.AnalyticsService) *AnalyticsHandler {
|
||||
return &AnalyticsHandler{svc: svc}
|
||||
}
|
||||
|
||||
// Index returns metric time-series for the overview charts.
|
||||
// GET /api/v2/accounts/:account_id/reports?metric=...&since=...&until=...
|
||||
func (h *AnalyticsHandler) Index(c *gin.Context) {
|
||||
accountID, ok := parseAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
since, until, ok := parseDateRange(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
metric := c.Query("metric")
|
||||
if metric == "" {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrBadRequest, "metric parameter is required")
|
||||
return
|
||||
}
|
||||
id := uint(0)
|
||||
if rawID := c.Query("id"); rawID != "" {
|
||||
parsed, err := strconv.ParseUint(rawID, 10, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
id = uint(parsed)
|
||||
}
|
||||
businessHours := c.Query("business_hours") == "true" || c.Query("business_hours") == "1"
|
||||
result, err := h.svc.GetTimeseries(c.Request.Context(), accountID, metric, since, until, c.DefaultQuery("type", "account"), id, c.Query("group_by"), businessHours)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Timeseries report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate report")
|
||||
return
|
||||
}
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
// parseAccountID extracts account_id from URL params.
|
||||
func parseAccountID(c *gin.Context) (uint, bool) {
|
||||
id := getAccountID(c)
|
||||
|
||||
@@ -37,6 +37,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
|
||||
s.db = db
|
||||
s.Require().NoError(db.AutoMigrate(
|
||||
&model.Account{},
|
||||
&model.Conversation{},
|
||||
&model.ReportingEvent{},
|
||||
&model.ReportingEventsRollup{},
|
||||
))
|
||||
@@ -54,6 +55,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
r := gin.New()
|
||||
accounts := r.Group("/api/v1/accounts/:account_id")
|
||||
accounts.GET("/reports", s.handler.Index)
|
||||
accounts.GET("/reports/summary", s.handler.Summary)
|
||||
accounts.GET("/reports/agents", s.handler.AgentMetrics)
|
||||
accounts.GET("/reports/inboxes", s.handler.InboxMetrics)
|
||||
@@ -70,6 +72,8 @@ func (s *AnalyticsHandlerTestSuite) TearDownSuite() {
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) SetupTest() {
|
||||
s.db.Exec("DELETE FROM reporting_events")
|
||||
s.db.Exec("DELETE FROM reporting_events_rollups")
|
||||
s.db.Exec("DELETE FROM conversations")
|
||||
}
|
||||
|
||||
// ========== parseAccountID / parseDateRange edge cases ==========
|
||||
@@ -149,6 +153,23 @@ func (s *AnalyticsHandlerTestSuite) TestSummary_WithData() {
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestIndex_TimeseriesWithData() {
|
||||
conv := model.Conversation{AccountID: s.accountID, InboxID: 1, ContactID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget"}
|
||||
s.Require().NoError(s.db.Create(&conv).Error)
|
||||
s.Require().NoError(s.db.Model(&conv).Updates(map[string]interface{}{"created_at": parseTime("2025-01-15T10:00:00Z")}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet,
|
||||
"/api/v1/accounts/1/reports?metric=conversations_count&since=2025-01-01T00:00:00Z&until=2025-02-01T00:00:00Z&type=account&group_by=day", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
var body map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
|
||||
data := body["data"].([]interface{})
|
||||
s.Len(data, 1)
|
||||
s.Equal(float64(1), data[0].(map[string]interface{})["value"])
|
||||
}
|
||||
|
||||
// ========== AgentMetrics ==========
|
||||
|
||||
func (s *AnalyticsHandlerTestSuite) TestAgentMetrics_InvalidAccountID() {
|
||||
@@ -251,4 +272,4 @@ func (s *AnalyticsHandlerTestSuite) TestNilService() {
|
||||
func parseTime(s string) time.Time {
|
||||
t, _ := time.Parse(time.RFC3339, s)
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user