feat(analytics): cards api endpoints
This commit is contained in:
parent
5640913e68
commit
0c0cac8fbe
5 changed files with 378 additions and 11 deletions
268
backend/pkg/analytics/api/card-handlers.go
Normal file
268
backend/pkg/analytics/api/card-handlers.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"openreplay/backend/pkg/server/api"
|
||||
"openreplay/backend/pkg/server/user"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// getCardId returns the ID from the request
|
||||
func getCardId(r *http.Request) (int64, error) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
if idStr == "" {
|
||||
return 0, fmt.Errorf("invalid Card ID")
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid Card ID")
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (e *handlersImpl) createCard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
bodyBytes, err := api.ReadBody(e.log, w, r, e.jsonSizeLimit)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
bodySize = len(bodyBytes)
|
||||
|
||||
req := &CardCreateRequest{}
|
||||
if err := json.Unmarshal(bodyBytes, req); err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
validate := validator.New()
|
||||
err = validate.Struct(req)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO save card to DB
|
||||
|
||||
resp := &CardGetResponse{
|
||||
Card: Card{
|
||||
CardID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: CardBase{
|
||||
Name: req.Name,
|
||||
IsPublic: req.IsPublic,
|
||||
Thumbnail: req.Thumbnail,
|
||||
MetricType: req.MetricType,
|
||||
MetricOf: req.MetricOf,
|
||||
Series: req.Series,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
currentUser := r.Context().Value("userData").(*user.User)
|
||||
e.log.Info(r.Context(), "User ID: ", currentUser.ID)
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
// getCard
|
||||
func (e *handlersImpl) getCard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
id, err := getCardId(r)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
thumbnail := "https://example.com/image.png"
|
||||
|
||||
// TODO get card from DB
|
||||
|
||||
resp := &CardGetResponse{
|
||||
Card: Card{
|
||||
CardID: id,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: CardBase{
|
||||
Name: "My Card",
|
||||
IsPublic: true,
|
||||
Thumbnail: &thumbnail,
|
||||
MetricType: "timeseries",
|
||||
MetricOf: "session_count",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
// get cards paginated
|
||||
func (e *handlersImpl) getCards(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
// TODO get cards from DB
|
||||
thumbnail := "https://example.com/image.png"
|
||||
|
||||
resp := &GetCardsResponse{
|
||||
Cards: []Card{
|
||||
{
|
||||
CardID: 1,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: CardBase{
|
||||
Name: "My Card",
|
||||
IsPublic: true,
|
||||
Thumbnail: &thumbnail,
|
||||
MetricType: "timeseries",
|
||||
MetricOf: "session_count",
|
||||
},
|
||||
},
|
||||
},
|
||||
Total: 10,
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
func (e *handlersImpl) updateCard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
id, err := getCardId(r)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
bodyBytes, err := api.ReadBody(e.log, w, r, e.jsonSizeLimit)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
bodySize = len(bodyBytes)
|
||||
|
||||
req := &CardUpdateRequest{}
|
||||
if err := json.Unmarshal(bodyBytes, req); err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
validate := validator.New()
|
||||
err = validate.Struct(req)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO update card in DB
|
||||
|
||||
resp := &CardGetResponse{
|
||||
Card: Card{
|
||||
CardID: id,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
DeletedAt: nil,
|
||||
EditedAt: nil,
|
||||
ProjectID: 1,
|
||||
UserID: 1,
|
||||
CardBase: CardBase{
|
||||
Name: req.Name,
|
||||
IsPublic: req.IsPublic,
|
||||
Thumbnail: req.Thumbnail,
|
||||
MetricType: req.MetricType,
|
||||
MetricOf: req.MetricOf,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
func (e *handlersImpl) deleteCard(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
_, err := getCardId(r)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO delete card from DB
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, nil, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
|
||||
func (e *handlersImpl) getCardChartData(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
bodyBytes, err := api.ReadBody(e.log, w, r, e.jsonSizeLimit)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
bodySize = len(bodyBytes)
|
||||
|
||||
req := &GetCardChartDataRequest{}
|
||||
if err := json.Unmarshal(bodyBytes, req); err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
validate := validator.New()
|
||||
err = validate.Struct(req)
|
||||
|
||||
// TODO get card chart data from ClickHouse
|
||||
jsonInput := `
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"timestamp": 1733934939000,
|
||||
"Series A": 100,
|
||||
"Series B": 200
|
||||
},
|
||||
{
|
||||
"timestamp": 1733935939000,
|
||||
"Series A": 150,
|
||||
"Series B": 250
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
var resp GetCardChartDataResponse
|
||||
err = json.Unmarshal([]byte(jsonInput), &resp)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusInternalServerError, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
}
|
||||
|
||||
e.responser.ResponseWithJSON(e.log, r.Context(), w, resp, startTime, r.URL.Path, bodySize)
|
||||
}
|
||||
92
backend/pkg/analytics/api/card.go
Normal file
92
backend/pkg/analytics/api/card.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CardBase Common fields for the Card entity
|
||||
type CardBase struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IsPublic bool `json:"isPublic" validate:"omitempty"`
|
||||
DefaultConfig map[string]any `json:"defaultConfig"`
|
||||
Thumbnail *string `json:"thumbnail" validate:"omitempty,url"`
|
||||
MetricType string `json:"metricType" validate:"required,oneof=timeseries table funnel"`
|
||||
MetricOf string `json:"metricOf" validate:"required,oneof=session_count user_count"`
|
||||
MetricFormat string `json:"metricFormat" validate:"required,oneof=default percentage"`
|
||||
ViewType string `json:"viewType" validate:"required,oneof=line_chart table_view"`
|
||||
MetricValue []string `json:"metricValue" validate:"omitempty"`
|
||||
SessionID *int64 `json:"sessionId" validate:"omitempty"`
|
||||
Series []CardSeries `json:"series" validate:"required,dive"`
|
||||
}
|
||||
|
||||
// Card Fields specific to database operations
|
||||
type Card struct {
|
||||
CardBase
|
||||
ProjectID int64 `json:"projectId" validate:"required"`
|
||||
UserID int64 `json:"userId" validate:"required"`
|
||||
CardID int64 `json:"cardId"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
||||
EditedAt *time.Time `json:"edited_at,omitempty"`
|
||||
}
|
||||
|
||||
type CardSeries struct {
|
||||
SeriesID int64 `json:"seriesId" validate:"omitempty"`
|
||||
MetricID int64 `json:"metricId" validate:"omitempty"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
CreatedAt time.Time `json:"createdAt" validate:"omitempty"`
|
||||
DeletedAt *time.Time `json:"deletedAt" validate:"omitempty"`
|
||||
Index int64 `json:"index" validate:"required"`
|
||||
Filter SeriesFilter `json:"filter"`
|
||||
}
|
||||
|
||||
type SeriesFilter struct {
|
||||
EventOrder string `json:"eventOrder" validate:"required,oneof=then or and"`
|
||||
Filters []FilterItem `json:"filters"`
|
||||
}
|
||||
|
||||
type FilterItem struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Operator string `json:"operator" validate:"required"`
|
||||
Source string `json:"source" validate:"required"`
|
||||
SourceOperator string `json:"sourceOperator" validate:"required"`
|
||||
Value []string `json:"value" validate:"required,dive,required"`
|
||||
IsEvent bool `json:"isEvent"`
|
||||
}
|
||||
|
||||
// CardCreateRequest Fields required for creating a card (from the frontend)
|
||||
type CardCreateRequest struct {
|
||||
CardBase
|
||||
}
|
||||
|
||||
type CardGetResponse struct {
|
||||
Card
|
||||
}
|
||||
|
||||
type CardUpdateRequest struct {
|
||||
CardBase
|
||||
}
|
||||
|
||||
type GetCardsResponse struct {
|
||||
Cards []Card `json:"cards"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type DataPoint struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Series map[string]int64 `json:"series"`
|
||||
}
|
||||
|
||||
type GetCardChartDataRequest struct {
|
||||
ProjectID int64 `json:"projectId" validate:"required"`
|
||||
MetricType string `json:"metricType" validate:"required,oneof=timeseries table funnel"`
|
||||
MetricOf string `json:"metricOf" validate:"required,oneof=session_count user_count"`
|
||||
MetricFormat string `json:"metricFormat" validate:"required,oneof=default percentage"`
|
||||
SessionID int64 `json:"sessionId" validate:"required"`
|
||||
Series []CardSeries `json:"series"`
|
||||
}
|
||||
|
||||
type GetCardChartDataResponse struct {
|
||||
Data []DataPoint `json:"data"`
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
|
@ -11,7 +11,7 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
func getId(r *http.Request) (int, error) {
|
||||
func getDashboardId(r *http.Request) (int, error) {
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
if idStr == "" {
|
||||
|
|
@ -64,7 +64,7 @@ func (e *handlersImpl) getDashboards(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
//id, err := getId(r)
|
||||
//id, err := getDashboardId(r)
|
||||
//if err != nil {
|
||||
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
// return
|
||||
|
|
@ -90,7 +90,7 @@ func (e *handlersImpl) getDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
id, err := getId(r)
|
||||
id, err := getDashboardId(r)
|
||||
if err != nil {
|
||||
e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
return
|
||||
|
|
@ -113,7 +113,7 @@ func (e *handlersImpl) updateDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
//id, err := getId(r)
|
||||
//id, err := getDashboardId(r)
|
||||
//if err != nil {
|
||||
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
// return
|
||||
|
|
@ -149,7 +149,7 @@ func (e *handlersImpl) deleteDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
//id, err := getId(r)
|
||||
//id, err := getDashboardId(r)
|
||||
//if err != nil {
|
||||
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
// return
|
||||
|
|
@ -163,7 +163,7 @@ func (e *handlersImpl) pinDashboard(w http.ResponseWriter, r *http.Request) {
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
//id, err := getId(r)
|
||||
//id, err := getDashboardId(r)
|
||||
//if err != nil {
|
||||
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
// return
|
||||
|
|
@ -179,7 +179,7 @@ func (e *handlersImpl) addCardToDashboard(w http.ResponseWriter, r *http.Request
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
//id, err := getId(r)
|
||||
//id, err := getDashboardId(r)
|
||||
//if err != nil {
|
||||
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
// return
|
||||
|
|
@ -195,7 +195,7 @@ func (e *handlersImpl) removeCardFromDashboard(w http.ResponseWriter, r *http.Re
|
|||
startTime := time.Now()
|
||||
bodySize := 0
|
||||
|
||||
//id, err := getId(r)
|
||||
//id, err := getDashboardId(r)
|
||||
//if err != nil {
|
||||
// e.responser.ResponseWithError(e.log, r.Context(), w, http.StatusBadRequest, err, startTime, r.URL.Path, bodySize)
|
||||
// return
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package models
|
||||
|
||||
import (
|
||||
config "openreplay/backend/internal/config/analytics"
|
||||
|
|
@ -25,6 +25,13 @@ func (e *handlersImpl) GetAll() []*api.Description {
|
|||
{"/v1/analytics/{projectId}/dashboards/{id}", e.getDashboard, "GET"},
|
||||
{"/v1/analytics/{projectId}/dashboards/{id}", e.updateDashboard, "PUT"},
|
||||
{"/v1/analytics/{projectId}/dashboards/{id}", e.deleteDashboard, "DELETE"},
|
||||
{"/v1/analytics/{projectId}/cards", e.createCard, "POST"},
|
||||
{"/v1/analytics/{projectId}/cards", e.getCards, "GET"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}", e.getCard, "GET"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}", e.updateCard, "PUT"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}", e.deleteCard, "DELETE"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}/chart", e.getCardChartData, "POST"},
|
||||
{"/v1/analytics/{projectId}/cards/{id}/try", e.getCardChartData, "POST"},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package api
|
||||
package models
|
||||
|
||||
type Dashboard struct {
|
||||
DashboardID int `json:"dashboard_id"`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue