diff --git a/backend/cmd/analytics/main.go b/backend/cmd/analytics/main.go index e184a6d0b..98e5ae0bf 100644 --- a/backend/cmd/analytics/main.go +++ b/backend/cmd/analytics/main.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/mux" "net/http" "openreplay/backend/internal/http/server" + "openreplay/backend/pkg/analytics" "openreplay/backend/pkg/analytics/api" "openreplay/backend/pkg/common" "openreplay/backend/pkg/common/api/auth" @@ -30,8 +31,7 @@ func main() { } defer pgConn.Close() - builder := common.NewServiceBuilder(log) - services, err := builder. + services, err := analytics.NewServiceBuilder(log). WithDatabase(pgConn). WithJWTSecret(cfg.JWTSecret, cfg.JWTSpotSecret). Build() @@ -77,10 +77,10 @@ func main() { limiterMiddleware := middleware.RateLimit(common.NewUserRateLimiter(10, 30, 1*time.Minute, 5*time.Minute)) router, err := api.NewRouter(cfg, log, services) - router.GetRouter().Use(middleware.CORS(cfg.UseAccessControlHeaders)) - router.GetRouter().Use(authMiddleware) - router.GetRouter().Use(limiterMiddleware) - router.GetRouter().Use(middleware.Action()) + router.Use(middleware.CORS(cfg.UseAccessControlHeaders)) + router.Use(authMiddleware) + router.Use(limiterMiddleware) + router.Use(middleware.Action()) if err != nil { log.Fatal(ctx, "failed while creating router: %s", err) diff --git a/backend/pkg/analytics/api/handler.go b/backend/pkg/analytics/api/handler.go index 4cd0c5eeb..7064b500e 100644 --- a/backend/pkg/analytics/api/handler.go +++ b/backend/pkg/analytics/api/handler.go @@ -1,11 +1,9 @@ package api import ( - "context" "encoding/json" "fmt" "github.com/gorilla/mux" - "io" "net/http" "strconv" "time" @@ -20,7 +18,7 @@ func (e *Router) createDashboard(w http.ResponseWriter, r *http.Request) { startTime := time.Now() bodySize := 0 - bodyBytes, err := e.readBody(w, r, e.cfg.JsonSizeLimit) + bodyBytes, err := e.ReadBody(w, r, e.cfg.JsonSizeLimit) if err != nil { e.ResponseWithError(r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize) return @@ -98,7 +96,7 @@ func (e *Router) updateDashboard(w http.ResponseWriter, r *http.Request) { return } - bodyBytes, err := e.readBody(w, r, e.cfg.JsonSizeLimit) + bodyBytes, err := e.ReadBody(w, r, e.cfg.JsonSizeLimit) if err != nil { e.ResponseWithError(r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize) return @@ -160,7 +158,7 @@ func (e *Router) addCardToDashboard(w http.ResponseWriter, r *http.Request) { return } - bodyBytes, err := e.readBody(w, r, e.cfg.JsonSizeLimit) + bodyBytes, err := e.ReadBody(w, r, e.cfg.JsonSizeLimit) if err != nil { e.ResponseWithError(r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize) return @@ -190,7 +188,7 @@ func (e *Router) createMetricAndAddToDashboard(w http.ResponseWriter, r *http.Re return } - bodyBytes, err := e.readBody(w, r, e.cfg.JsonSizeLimit) + bodyBytes, err := e.ReadBody(w, r, e.cfg.JsonSizeLimit) if err != nil { e.ResponseWithError(r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize) return @@ -220,7 +218,7 @@ func (e *Router) updateWidgetInDashboard(w http.ResponseWriter, r *http.Request) return } - bodyBytes, err := e.readBody(w, r, e.cfg.JsonSizeLimit) + bodyBytes, err := e.ReadBody(w, r, e.cfg.JsonSizeLimit) if err != nil { e.ResponseWithError(r.Context(), w, http.StatusRequestEntityTooLarge, err, startTime, r.URL.Path, bodySize) return @@ -264,59 +262,3 @@ func getDashboardId(r *http.Request) (int, error) { } return id, nil } - -func recordMetrics(requestStart time.Time, url string, code, bodySize int) { - // TODO: Implement this -} - -func (e *Router) readBody(w http.ResponseWriter, r *http.Request, limit int64) ([]byte, error) { - body := http.MaxBytesReader(w, r.Body, limit) - bodyBytes, err := io.ReadAll(body) - - // Close body - if closeErr := body.Close(); closeErr != nil { - e.log.Warn(r.Context(), "error while closing request body: %s", closeErr) - } - if err != nil { - return nil, err - } - return bodyBytes, nil -} - -func (e *Router) ResponseOK(ctx context.Context, w http.ResponseWriter, requestStart time.Time, url string, bodySize int) { - w.WriteHeader(http.StatusOK) - e.log.Info(ctx, "response ok") - recordMetrics(requestStart, url, http.StatusOK, bodySize) -} - -func (e *Router) ResponseWithJSON(ctx context.Context, w http.ResponseWriter, res interface{}, requestStart time.Time, url string, bodySize int) { - e.log.Info(ctx, "response ok") - body, err := json.Marshal(res) - if err != nil { - e.log.Error(ctx, "can't marshal response: %s", err) - } - w.Header().Set("Content-Type", "application/json") - _, err = w.Write(body) - if err != nil { - return - } - recordMetrics(requestStart, url, http.StatusOK, bodySize) -} - -type response struct { - Error string `json:"error"` -} - -func (e *Router) ResponseWithError(ctx context.Context, w http.ResponseWriter, code int, err error, requestStart time.Time, url string, bodySize int) { - e.log.Error(ctx, "response error, code: %d, error: %s", code, err) - body, err := json.Marshal(&response{err.Error()}) - if err != nil { - e.log.Error(ctx, "can't marshal response: %s", err) - } - w.WriteHeader(code) - _, err = w.Write(body) - if err != nil { - return - } - recordMetrics(requestStart, url, code, bodySize) -} diff --git a/backend/pkg/analytics/api/router.go b/backend/pkg/analytics/api/router.go index c2ba94427..2b098dbf9 100644 --- a/backend/pkg/analytics/api/router.go +++ b/backend/pkg/analytics/api/router.go @@ -2,22 +2,16 @@ package api import ( "fmt" - "net/http" analyticsConfig "openreplay/backend/internal/config/analytics" "openreplay/backend/pkg/common" + "openreplay/backend/pkg/common/api" "openreplay/backend/pkg/logger" - "sync" - - "github.com/gorilla/mux" ) type Router struct { - log logger.Logger - cfg *analyticsConfig.Config - router *mux.Router - mutex *sync.RWMutex - services *common.ServicesBuilder - limiter *common.UserRateLimiter + *api.Router + cfg *analyticsConfig.Config + limiter *common.UserRateLimiter } func NewRouter(cfg *analyticsConfig.Config, log logger.Logger, services *common.ServicesBuilder) (*Router, error) { @@ -29,65 +23,26 @@ func NewRouter(cfg *analyticsConfig.Config, log logger.Logger, services *common. case log == nil: return nil, fmt.Errorf("logger is empty") } + e := &Router{ - log: log, - cfg: cfg, - mutex: &sync.RWMutex{}, - services: services, + Router: api.NewRouter(log, services), + cfg: cfg, + limiter: common.NewUserRateLimiter(10, 30, 1, 5), } e.init() return e, nil } func (e *Router) init() { - e.router = mux.NewRouter() - e.router.HandleFunc("/", e.ping) - - e.router.HandleFunc("/{projectId}/dashboards", e.createDashboard).Methods("POST", "OPTIONS") - e.router.HandleFunc("/v1/spots/{id}/uploaded", e.spotTest).Methods("POST", "OPTIONS") - e.router.HandleFunc("/{projectId}/dashboards", e.getDashboards).Methods("GET", "OPTIONS") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}", e.getDashboard).Methods("GET") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}", e.updateDashboard).Methods("PUT") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}", e.deleteDashboard).Methods("DELETE") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}/pin", e.pinDashboard).Methods("GET") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}/cards", e.addCardToDashboard).Methods("POST") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}/metrics", e.createMetricAndAddToDashboard).Methods("POST") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}", e.updateWidgetInDashboard).Methods("PUT") - e.router.HandleFunc("/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}", e.removeWidgetFromDashboard).Methods("DELETE") - //e.router.HandleFunc("/{projectId}/cards/try", e.tryCard).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/try/sessions", e.tryCardSessions).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/try/issues", e.tryCardIssues).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards", e.getCards).Methods("GET") - //e.router.HandleFunc("/{projectId}/cards", e.createCard).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/search", e.searchCards).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}", e.getCard).Methods("GET") - //e.router.HandleFunc("/{projectId}/cards/{cardId}/sessions", e.getCardSessions).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}/issues", e.getCardFunnelIssues).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}/issues/{issueId}/sessions", e.getMetricFunnelIssueSessions).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}/errors", e.getCardErrorsList).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}/chart", e.getCardChart).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}", e.updateCard).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}/status", e.updateCardState).Methods("POST") - //e.router.HandleFunc("/{projectId}/cards/{cardId}", e.deleteCard).Methods("DELETE") -} - -func (e *Router) ping(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) -} - -func (e *Router) GetHandler() http.Handler { - return e.router -} - -func (e *Router) GetRouter() *mux.Router { - return e.router -} - -type CurrentContext struct { - UserID int `json:"user_id"` -} - -func (e *Router) getCurrentContext(r *http.Request) *CurrentContext { - // retrieving user info from headers or tokens - return &CurrentContext{UserID: 1} + e.AddRoute("/{projectId}/dashboards", e.createDashboard, "POST") + e.AddRoute("/v1/spots/{id}/uploaded", e.spotTest, "POST") + e.AddRoute("/{projectId}/dashboards", e.getDashboards, "GET") + e.AddRoute("/{projectId}/dashboards/{dashboardId}", e.getDashboard, "GET") + e.AddRoute("/{projectId}/dashboards/{dashboardId}", e.updateDashboard, "PUT") + e.AddRoute("/{projectId}/dashboards/{dashboardId}", e.deleteDashboard, "DELETE") + e.AddRoute("/{projectId}/dashboards/{dashboardId}/pin", e.pinDashboard, "GET") + e.AddRoute("/{projectId}/dashboards/{dashboardId}/cards", e.addCardToDashboard, "POST") + e.AddRoute("/{projectId}/dashboards/{dashboardId}/metrics", e.createMetricAndAddToDashboard, "POST") + e.AddRoute("/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}", e.updateWidgetInDashboard, "PUT") + e.AddRoute("/{projectId}/dashboards/{dashboardId}/widgets/{widgetId}", e.removeWidgetFromDashboard, "DELETE") } diff --git a/backend/pkg/analytics/builder.go b/backend/pkg/analytics/builder.go index 472287ac9..b19a1376f 100644 --- a/backend/pkg/analytics/builder.go +++ b/backend/pkg/analytics/builder.go @@ -1 +1,25 @@ package analytics + +import ( + "openreplay/backend/pkg/common" + "openreplay/backend/pkg/logger" +) + +type ServiceBuilder struct { + *common.ServicesBuilder +} + +func NewServiceBuilder(log logger.Logger) *ServiceBuilder { + return &ServiceBuilder{ + ServicesBuilder: common.NewServiceBuilder(log), + } +} + +func (sb *ServiceBuilder) Build() (*ServiceBuilder, error) { + // Build common services + if _, err := sb.ServicesBuilder.Build(); err != nil { + return nil, err + } + + return sb, nil +} diff --git a/backend/pkg/common/api/router.go b/backend/pkg/common/api/router.go index c496bf5a8..1797cffa8 100644 --- a/backend/pkg/common/api/router.go +++ b/backend/pkg/common/api/router.go @@ -1,18 +1,119 @@ package api import ( + "context" + "encoding/json" "github.com/gorilla/mux" - analyticsConfig "openreplay/backend/internal/config/analytics" + "io" + "net/http" "openreplay/backend/pkg/common" "openreplay/backend/pkg/logger" "sync" + "time" ) type Router struct { log logger.Logger - cfg *analyticsConfig.Config router *mux.Router mutex *sync.RWMutex services *common.ServicesBuilder - limiter *common.UserRateLimiter +} + +func NewRouter(log logger.Logger, services *common.ServicesBuilder) *Router { + return &Router{ + router: mux.NewRouter(), + log: log, + mutex: &sync.RWMutex{}, + services: services, + } +} + +// Get return log, router, mutex, services +func (e *Router) Get() (logger.Logger, *mux.Router, *sync.RWMutex, *common.ServicesBuilder) { + return e.log, e.router, e.mutex, e.services +} + +func (e *Router) ping(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func (e *Router) GetHandler() http.Handler { + return e.router +} + +func (e *Router) GetRouter() *mux.Router { + return e.router +} + +func (e *Router) AddRoute(path string, handler http.HandlerFunc, method string) { + e.router.HandleFunc(path, handler).Methods(method) +} + +func (e *Router) Use(middleware func(http.Handler) http.Handler) { + e.router.Use(middleware) +} + +type CurrentContext struct { + UserID int `json:"user_id"` +} + +func (e *Router) getCurrentContext(r *http.Request) *CurrentContext { + // retrieving user info from headers or tokens + return &CurrentContext{UserID: 1} +} + +func recordMetrics(requestStart time.Time, url string, code, bodySize int) { + // TODO: Implement this +} + +func (e *Router) ReadBody(w http.ResponseWriter, r *http.Request, limit int64) ([]byte, error) { + body := http.MaxBytesReader(w, r.Body, limit) + bodyBytes, err := io.ReadAll(body) + + // Close body + if closeErr := body.Close(); closeErr != nil { + e.log.Warn(r.Context(), "error while closing request body: %s", closeErr) + } + if err != nil { + return nil, err + } + return bodyBytes, nil +} + +func (e *Router) ResponseOK(ctx context.Context, w http.ResponseWriter, requestStart time.Time, url string, bodySize int) { + w.WriteHeader(http.StatusOK) + e.log.Info(ctx, "response ok") + recordMetrics(requestStart, url, http.StatusOK, bodySize) +} + +func (e *Router) ResponseWithJSON(ctx context.Context, w http.ResponseWriter, res interface{}, requestStart time.Time, url string, bodySize int) { + e.log.Info(ctx, "response ok") + body, err := json.Marshal(res) + if err != nil { + e.log.Error(ctx, "can't marshal response: %s", err) + } + w.Header().Set("Content-Type", "application/json") + _, err = w.Write(body) + if err != nil { + return + } + recordMetrics(requestStart, url, http.StatusOK, bodySize) +} + +type response struct { + Error string `json:"error"` +} + +func (e *Router) ResponseWithError(ctx context.Context, w http.ResponseWriter, code int, err error, requestStart time.Time, url string, bodySize int) { + e.log.Error(ctx, "response error, code: %d, error: %s", code, err) + body, err := json.Marshal(&response{err.Error()}) + if err != nil { + e.log.Error(ctx, "can't marshal response: %s", err) + } + w.WriteHeader(code) + _, err = w.Write(body) + if err != nil { + return + } + recordMetrics(requestStart, url, code, bodySize) }