Skip to content

Commit

Permalink
feat: add project analytics api
Browse files Browse the repository at this point in the history
  • Loading branch information
K-A-I-L-A-S-H committed Oct 15, 2023
1 parent 7a4f4a4 commit e38fb03
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 6 deletions.
12 changes: 12 additions & 0 deletions services/dashboard-backend/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ func RegisterHandlers(mux *chi.Mux) {
subRouter.Patch("/", HandleUpdateProject)
})

router.Route(ProjectAnalyticsEndpoint, func(subRouter chi.Router) {
subRouter.Use(middleware.PathParameterMiddleware("projectId"))
subRouter.Use(
middleware.AuthorizationMiddleware(
middleware.MethodPermissionMap{
http.MethodGet: allPermissions,
},
),
)
subRouter.Get("/", HandleRetrieveProjectAnalytics)
})

router.Route(ProjectUserListEndpoint, func(subRouter chi.Router) {
subRouter.Use(middleware.PathParameterMiddleware("projectId"))
subRouter.Use(
Expand Down
3 changes: 2 additions & 1 deletion services/dashboard-backend/internal/api/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package api

const (
ProjectsListEndpoint = "/projects"
ProjectAnalyticsEndpoint = "/projects/{projectId}/analytics"
ProjectDetailEndpoint = "/projects/{projectId}"
ProjectUserListEndpoint = "/projects/{projectId}/users"
ProjectUserDetailEndpoint = "/projects/{projectId}/users/{userId}"
ApplicationAnalyticsEndpoint = "/projects/{projectId}/applications/{applicationId}/analytics"
ApplicationsListEndpoint = "/projects/{projectId}/applications"
ApplicationAnalyticsEndpoint = "/projects/{projectId}/applications/{applicationId}/analytics"
ApplicationDetailEndpoint = "/projects/{projectId}/applications/{applicationId}"
ApplicationTokensListEndpoint = "/projects/{projectId}/applications/{applicationId}/tokens" //nolint: gosec
ApplicationTokenDetailEndpoint = "/projects/{projectId}/applications/{applicationId}/tokens/{tokenId}" //nolint: gosec
Expand Down
27 changes: 26 additions & 1 deletion services/dashboard-backend/internal/api/projects.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package api

import (
"net/http"
"time"

"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/dto"
"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/middleware"
"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/repositories"
"github.com/basemind-ai/monorepo/shared/go/apierror"
"github.com/basemind-ai/monorepo/shared/go/db"
"github.com/basemind-ai/monorepo/shared/go/serialization"
"github.com/basemind-ai/monorepo/shared/go/timeutils"
"github.com/jackc/pgx/v5/pgtype"
"github.com/rs/zerolog/log"
"net/http"
)

// HandleCreateProject - creates a new project and sets the user as an ADMIN.
Expand Down Expand Up @@ -148,3 +151,25 @@ func HandleDeleteProject(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusNoContent)
}

func HandleRetrieveProjectAnalytics(w http.ResponseWriter, r *http.Request) {
projectID := r.Context().Value(middleware.ProjectIDContextKey).(pgtype.UUID)

toDate := timeutils.ParseDate(r.URL.Query().Get("toDate"), time.Now())
fromDate := timeutils.ParseDate(r.URL.Query().Get("fromDate"), timeutils.GetFirstDayOfMonth())

projectAnalytics, err := repositories.GetProjectAnalyticsByDateRange(
r.Context(),
projectID,
fromDate,
toDate,
)
if err != nil {
log.Error().Err(err).Msg("failed to retrieve project analytics")
apierror.InternalServerError().Render(w, r)
return
}

w.WriteHeader(http.StatusOK)
serialization.RenderJSONResponse(w, http.StatusOK, projectAnalytics)
}
124 changes: 121 additions & 3 deletions services/dashboard-backend/internal/api/projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ package api_test
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"

"github.com/basemind-ai/monorepo/e2e/factories"
"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/api"
"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/dto"
"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/repositories"
"github.com/basemind-ai/monorepo/shared/go/db"
"github.com/basemind-ai/monorepo/shared/go/serialization"
"github.com/stretchr/testify/assert"
"net/http"
"strings"
"testing"
)

func TestProjectsAPI(t *testing.T) {
Expand Down Expand Up @@ -281,4 +284,119 @@ func TestProjectsAPI(t *testing.T) {
},
)
})

t.Run(fmt.Sprintf("GET: %s", api.ProjectAnalyticsEndpoint), func(t *testing.T) {
invalidUUID := "invalid"
projectID := createProject(t)
createUserProject(t, userAccount.FirebaseID, projectID, db.AccessPermissionTypeADMIN)

applicationID := createApplication(t, projectID)
createPromptRequestRecord(t, applicationID)

fromDate := time.Now().AddDate(0, 0, -1)
toDate := fromDate.AddDate(0, 0, 2)

t.Run("retrieves project analytics", func(t *testing.T) {
response, requestErr := testClient.Get(
context.TODO(),
fmt.Sprintf(
"/v1%s",
strings.ReplaceAll(
api.ProjectAnalyticsEndpoint,
"{projectId}",
projectID,
),
),
)
assert.NoError(t, requestErr)
assert.Equal(t, http.StatusOK, response.StatusCode)

projectUUID, _ := db.StringToUUID(projectID)
promptReqAnalytics, _ := repositories.GetProjectAnalyticsByDateRange(
context.TODO(),
*projectUUID,
fromDate,
toDate,
)

responseAnalytics := dto.ProjectAnalyticsDTO{}
deserializationErr := serialization.DeserializeJSON(
response.Body,
&responseAnalytics,
)

assert.NoError(t, deserializationErr)
assert.Equal(t, promptReqAnalytics.TotalAPICalls, responseAnalytics.TotalAPICalls)
assert.Equal(t, promptReqAnalytics.ModelsCost, responseAnalytics.ModelsCost)
})

for _, permission := range []db.AccessPermissionType{
db.AccessPermissionTypeMEMBER, db.AccessPermissionTypeADMIN,
} {
t.Run(
fmt.Sprintf(
"responds with status 200 OK if the user has %s permission",
permission,
),
func(t *testing.T) {
newUserAccount, _ := factories.CreateUserAccount(context.TODO())
newProjectID := createProject(t)
createUserProject(t, newUserAccount.FirebaseID, newProjectID, permission)

newTestClient := createTestClient(t, newUserAccount)

response, requestErr := newTestClient.Get(
context.TODO(),
fmt.Sprintf(
"/v1%s",
strings.ReplaceAll(
api.ProjectAnalyticsEndpoint,
"{projectId}",
newProjectID,
),
),
)
assert.NoError(t, requestErr)
assert.Equal(t, http.StatusOK, response.StatusCode)
},
)
}

t.Run(
"responds with status 403 FORBIDDEN if the user does not have projects access",
func(t *testing.T) {
newProjectID := createProject(t)

response, requestErr := testClient.Get(
context.TODO(),
fmt.Sprintf(
"/v1%s",
strings.ReplaceAll(
api.ProjectAnalyticsEndpoint,
"{projectId}",
newProjectID,
),
),
)
assert.NoError(t, requestErr)
assert.Equal(t, http.StatusForbidden, response.StatusCode)
},
)

t.Run("responds with status 400 BAD REQUEST if projectID is invalid", func(t *testing.T) {
response, requestErr := testClient.Get(
context.TODO(),
fmt.Sprintf(
"/v1%s",
strings.ReplaceAll(
api.ProjectAnalyticsEndpoint,
"{projectId}",
invalidUUID,
),
),
)
assert.NoError(t, requestErr)
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
})
})
}
5 changes: 5 additions & 0 deletions services/dashboard-backend/internal/dto/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,8 @@ type ApplicationAnalyticsDTO struct {
TotalRequests int64 `json:"totalRequests"`
ProjectedCost float64 `json:"projectedCost"`
}

type ProjectAnalyticsDTO struct {
TotalAPICalls int64 `json:"totalAPICalls"`
ModelsCost float64 `json:"modelsCost"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package repositories
import (
"context"
"fmt"

"github.com/basemind-ai/monorepo/shared/go/db"
"github.com/basemind-ai/monorepo/shared/go/rediscache"
"github.com/jackc/pgx/v5/pgtype"
Expand Down
72 changes: 72 additions & 0 deletions services/dashboard-backend/internal/repositories/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package repositories
import (
"context"
"fmt"
"time"

"github.com/basemind-ai/monorepo/services/dashboard-backend/internal/dto"
"github.com/basemind-ai/monorepo/shared/go/db"
"github.com/basemind-ai/monorepo/shared/go/tokenutils"
"github.com/jackc/pgx/v5/pgtype"
"github.com/rs/zerolog/log"
)
Expand Down Expand Up @@ -114,3 +117,72 @@ func DeleteProject(ctx context.Context, projectID pgtype.UUID) error {

return nil
}

func GetTotalAPICountByDateRange(
ctx context.Context,
projectID pgtype.UUID,
fromDate, toDate time.Time,
) (int64, error) {
reqParam := db.RetrieveTotalPromptAPICallsParams{
ProjectID: projectID,
FromDate: pgtype.Timestamptz{Time: fromDate, Valid: true},
ToDate: pgtype.Timestamptz{Time: toDate, Valid: true},
}

totalAPICalls, dbErr := db.GetQueries().RetrieveTotalPromptAPICalls(ctx, reqParam)
if dbErr != nil {
return -1, dbErr
}

return totalAPICalls, nil
}

func GetTokenConsumedByProjectByDateRange(
ctx context.Context,
projectID pgtype.UUID,
fromDate, toDate time.Time,
) (map[db.ModelType]int64, error) {
reqParam := db.RetrieveTotalTokensConsumedParams{
ProjectID: projectID,
FromDate: pgtype.Timestamptz{Time: fromDate, Valid: true},
ToDate: pgtype.Timestamptz{Time: toDate, Valid: true},
}

tokensConsumed, dbErr := db.GetQueries().RetrieveTotalTokensConsumed(ctx, reqParam)
if dbErr != nil {
return nil, dbErr
}

projectTokenCntMap := make(map[db.ModelType]int64)
for _, record := range tokensConsumed {
projectTokenCntMap[record.ModelType] += record.TotalTokens
}

return projectTokenCntMap, nil
}

func GetProjectAnalyticsByDateRange(
ctx context.Context,
projectID pgtype.UUID,
fromDate, toDate time.Time,
) (dto.ProjectAnalyticsDTO, error) {
totalApiCalls, dbErr := GetTotalAPICountByDateRange(ctx, projectID, fromDate, toDate)
if dbErr != nil {
return dto.ProjectAnalyticsDTO{}, dbErr
}

projectTokenCntMap, dbErr := GetTokenConsumedByProjectByDateRange(ctx, projectID, fromDate, toDate)
if dbErr != nil {
return dto.ProjectAnalyticsDTO{}, dbErr
}

var modelCost float64
for model, tokenCnt := range projectTokenCntMap {
modelCost += tokenutils.GetCostByModelType(tokenCnt, model)
}

return dto.ProjectAnalyticsDTO{
TotalAPICalls: totalApiCalls,
ModelsCost: modelCost,
}, nil
}
Loading

0 comments on commit e38fb03

Please sign in to comment.