Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add leaderboard endpoint #15

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package queries
package utils

import (
"bytes"
"fmt"
"lib/constants"
"lib/utils"
"net/http"
"os"
"strings"
"time"
)

func PostHogQuery(query string, output any) error {
func SendPostHogQuery(query string, output any) error {
path := strings.Replace(constants.QueryPath, ":project_id", os.Getenv("POSTHOG_PROJECT_ID"), -1)
request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", os.Getenv("POSTHOG_API_URL"), path), bytes.NewReader([]byte(query)))
if err != nil {
Expand All @@ -31,7 +30,7 @@ func PostHogQuery(query string, output any) error {
return err
}

err = utils.ParseResponseBody(response.Body, &output)
err = ParseResponseBody(response.Body, &output)
if err != nil {
return err
}
Expand Down
14 changes: 14 additions & 0 deletions packages/v1/leaderboard/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module leaderboard

go 1.20

require lib v0.0.0

require (
github.com/fatih/color v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.21.0 // indirect
)

replace lib v0.0.0 => ../../../lib
11 changes: 11 additions & 0 deletions packages/v1/leaderboard/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3 changes: 3 additions & 0 deletions packages/v1/leaderboard/internal/constants/limits.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package constants

const ResultLimit = 1000
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package queries

import (
"fmt"
_constants "leaderboard/internal/constants"
_types "leaderboard/internal/types"
"lib/types"
"lib/utils"
)

// FetchLeaderboardResults Fetches a page of leaderboard results. The page is zero-indexed.
func FetchLeaderboardResults(page int, logger *utils.Logger) ([]_types.LeaderboardResult, error) {
var response types.PostHogQueryResponse
var results []_types.LeaderboardResult

query := fmt.Sprintf(`{"query":{"kind":"HogQLQuery","query":"select person.pdi.distinct_id as \"account\", count(distinct dateTrunc('day', events.timestamp) || events.event) as \"total\" from events where events.properties.genesisHash = 'IXnoWtviVVJW5LGivNFc0Dq14V3kqaXuK2u5OQrdVZo=' group by \"account\" order by \"total\" desc limit %d offset %d * %d"}}`, _constants.ResultLimit, page, _constants.ResultLimit)

if err := utils.SendPostHogQuery(query, &response); err != nil {
logger.Error(err)

return nil, err
}

for _, value := range response.Results {
results = append(results, _types.LeaderboardResult{
Account: value[0].(string),
Total: int(value[1].(float64)),
})
}

// if the results are nil, return an empty array
if results == nil {
return []_types.LeaderboardResult{}, nil
}

return results, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package queries

import (
"fmt"
"lib/types"
"lib/utils"
)

// FetchTotalAccountsCompleted Fetches the total number of accounts that have completed at least one event.
func FetchTotalAccountsCompleted(logger *utils.Logger) (int, error) {
var response types.PostHogQueryResponse

query := fmt.Sprintf(`{"query":{"kind":"HogQLQuery","query":"select count(distinct person.pdi.distinct_id) as \"total\" from events where events.properties.genesisHash = 'IXnoWtviVVJW5LGivNFc0Dq14V3kqaXuK2u5OQrdVZo='"}}`)
if err := utils.SendPostHogQuery(query, &response); err != nil {
logger.Error(err)

return 0, err
}

return int(response.Results[0][0].(float64)), nil
}
10 changes: 10 additions & 0 deletions packages/v1/leaderboard/internal/types/leaderboardresult.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package types

// LeaderboardResult
// @Description The account and the total daily quests the account has completed.
type LeaderboardResult struct {
// The base 32 encoded address of the account
Account string `json:"account" example:"TESTK4BURRDGVVHAX2FBY7CPRC2RTTVRRN4C2TVDCHRCXNTFGL3TVSDROE"`
// The total number of daily quests completed
Total int `json:"total" example:"22"`
}
12 changes: 12 additions & 0 deletions packages/v1/leaderboard/internal/types/paginationmetadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package types

type PaginationMetadata struct {
// The current page of results
Page int `json:"page" example:"0"`
// The total amount of pages
PageCount int `json:"pageCount" example:"3"`
// The amount of results per page
PageSize int `json:"pageSize" example:"1000"`
// The total number of results
Total int `json:"total" example:"1024"`
}
8 changes: 8 additions & 0 deletions packages/v1/leaderboard/internal/types/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package types

import "lib/types"

type Request struct {
Http types.Http `json:"http,omitempty"`
Page string `json:"page,omitempty"`
}
7 changes: 7 additions & 0 deletions packages/v1/leaderboard/internal/types/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package types

type Response struct {
Body ResponseBody `json:"body,omitempty"`
Headers ResponseHeaders `json:"headers,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
}
9 changes: 9 additions & 0 deletions packages/v1/leaderboard/internal/types/responsebody.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package types

type ResponseBody struct {
Error interface{} `json:"error,omitempty" swaggertype:"object"`
// The pagination metadata
Metadata PaginationMetadata `json:"metadata"`
// The leaderboard results; the completed quests for each account
Results []LeaderboardResult `json:"results"`
}
6 changes: 6 additions & 0 deletions packages/v1/leaderboard/internal/types/responseheaders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

type ResponseHeaders struct {
CacheControl string `json:"Cache-Control"`
ContentType string `json:"Content-Type"`
}
95 changes: 95 additions & 0 deletions packages/v1/leaderboard/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"fmt"
_constants "leaderboard/internal/constants"
_queries "leaderboard/internal/queries"
_types "leaderboard/internal/types"
"lib/constants"
"lib/errors"
"lib/utils"
"math"
"net/http"
"strconv"
)

// Main godoc
// @Summary Quests Leaderboard
// @Description Gets the quests' leaderboard for all accounts.
// @Produce json
// @Param page query string false "the page of results, the first page starts at 0" Example(0)
// @Success 200 {object} _types.ResponseBody
// @Failure 405 "If it is not a GET request"
// @Failure 500
// @Header all {string} Cache-Control "public, max-age=3600"
// @Router /v1/leaderboard [get]
func Main(request _types.Request) *_types.Response {
var err error
var page = 0

logger := utils.NewLogger()
headers := _types.ResponseHeaders{
CacheControl: fmt.Sprintf("public, max-age=%d", constants.HourInSeconds),
ContentType: "application/json",
}

// only accept get requests
if request.Http.Method != http.MethodGet {
return &_types.Response{
Headers: headers,
StatusCode: http.StatusMethodNotAllowed,
}
}

if request.Page != "" {
if page, err = strconv.Atoi(request.Page); err != nil {
logger.Debug(fmt.Sprintf("failed to parse page \"%s\" to integer", request.Page))
}
}

logger.Debug("fetching total number of accounts that have completed at least one event")

total, err := _queries.FetchTotalAccountsCompleted(logger)
if err != nil {
return &_types.Response{
Body: _types.ResponseBody{
Error: errors.NewPostHogError("failed to fetch the total amount of accounts that have completed an event from posthog", err),
},
Headers: headers,
StatusCode: http.StatusInternalServerError,
}
}

logger.Debug(fmt.Sprintf("received the total amount of accounts that have completed an event: %d", total))

pageCount := int(math.Ceil(float64(total / _constants.ResultLimit))) // pages are zero-based

logger.Debug(fmt.Sprintf("fetching total daily event for each account from page %d of %d from posthog", page, pageCount))

results, err := _queries.FetchLeaderboardResults(page, logger)
if err != nil {
return &_types.Response{
Body: _types.ResponseBody{
Error: errors.NewPostHogError(fmt.Sprintf("failed to fetch the total number of completed events for page %d of %d from posthog", page, pageCount), err),
},
Headers: headers,
StatusCode: http.StatusInternalServerError,
}
}

logger.Debug(fmt.Sprintf("received total number of completed events for page %d of %d from posthog", page, pageCount))

return &_types.Response{
Body: _types.ResponseBody{
Metadata: _types.PaginationMetadata{
Page: page,
PageCount: pageCount,
PageSize: _constants.ResultLimit,
Total: total,
},
Results: results,
},
Headers: headers,
StatusCode: http.StatusOK,
}
}
39 changes: 39 additions & 0 deletions packages/v1/leaderboard/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
_types "leaderboard/internal/types"
"lib/types"
"log"
"net/http"
"os"
"testing"
)

func TestMain(m *testing.M) {
err := os.Setenv("ENVIRONMENT", "test")
if err != nil {
log.Fatalf("failed to set \"ENVIRONMENT\" variable to \"test\"")
}

// run tests
code := m.Run()

os.Exit(code)
}

func TestIncorrectHTTPMethod(t *testing.T) {
// arrange
request := _types.Request{
Http: types.Http{
Method: http.MethodPost,
},
}

// act
response := Main(request)

// assert
if response.StatusCode != http.StatusMethodNotAllowed {
t.Errorf("expected \"statusCode\" to be \"%d\", got %d", http.StatusMethodNotAllowed, response.StatusCode)
}
}
5 changes: 3 additions & 2 deletions packages/v1/quests/internal/queries/fetchdailyevents.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package queries

import (
"fmt"
"lib/types"
"lib/utils"
_types "quests/internal/types"
)

func FetchDailyEvents(account string, logger *utils.Logger) ([]_types.DailyEvent, error) {
var response _types.PostHogQueryResponse
var response types.PostHogQueryResponse
var result []_types.DailyEvent

query := fmt.Sprintf(`{"query":{"kind":"HogQLQuery","query":"select events.event as \"event-name\", count() > 0 as \"completed\" from events where person.pdi.distinct_id = '%s' and events.properties.genesisHash = 'IXnoWtviVVJW5LGivNFc0Dq14V3kqaXuK2u5OQrdVZo=' and events.timestamp >= today() group by \"event-name\" order by \"event-name\""}}`, account)
err := PostHogQuery(query, &response)
err := utils.SendPostHogQuery(query, &response)
if err != nil {
logger.Error(err)

Expand Down
6 changes: 3 additions & 3 deletions packages/v1/quests/internal/queries/fetcheventreferences.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package queries

import (
"lib/types"
"lib/utils"
_types "quests/internal/types"
)

func FetchEventReferences(logger *utils.Logger) ([]string, error) {
var response _types.PostHogQueryResponse
var response types.PostHogQueryResponse
var result []string

err := PostHogQuery(`{"query":{"kind":"HogQLQuery","query":"select distinct events.event from events"}}`, &response)
err := utils.SendPostHogQuery(`{"query":{"kind":"HogQLQuery","query":"select distinct events.event from events"}}`, &response)
if err != nil {
logger.Error(err)

Expand Down
5 changes: 3 additions & 2 deletions packages/v1/quests/internal/queries/fetchtotaldailyevents.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ package queries

import (
"fmt"
"lib/types"
"lib/utils"
_types "quests/internal/types"
)

func FetchTotalDailyEvents(account string, logger *utils.Logger) ([]_types.TotalDailyEvent, error) {
var response _types.PostHogQueryResponse
var response types.PostHogQueryResponse
var result []_types.TotalDailyEvent

query := fmt.Sprintf(`{"query":{"kind":"HogQLQuery","query":"select events.event as \"event-name\", count(distinct dateTrunc('day', events.timestamp)) AS \"total\" from events where person.pdi.distinct_id = '%s' and events.properties.genesisHash = 'IXnoWtviVVJW5LGivNFc0Dq14V3kqaXuK2u5OQrdVZo=' group by \"event-name\" order by \"event-name\""}}`, account)
err := PostHogQuery(query, &response)
err := utils.SendPostHogQuery(query, &response)
if err != nil {
logger.Error(err)

Expand Down
6 changes: 3 additions & 3 deletions packages/v1/quests/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"log"
"net/http"
"os"
internaltypes "quests/internal/types"
_types "quests/internal/types"
"testing"
)

Expand All @@ -27,7 +27,7 @@ func TestMain(m *testing.M) {

func TestIncorrectHTTPMethod(t *testing.T) {
// arrange
request := internaltypes.Request{
request := _types.Request{
Account: account,
Http: types.Http{
Method: http.MethodPost,
Expand All @@ -45,7 +45,7 @@ func TestIncorrectHTTPMethod(t *testing.T) {

func TestInvalidAccount(t *testing.T) {
// arrange
request := internaltypes.Request{
request := _types.Request{
Account: "not a valid account",
Http: types.Http{
Method: http.MethodGet,
Expand Down
Loading