Skip to content

Commit

Permalink
feat: proposal for collecting community metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
MishkaRogachev committed Jul 25, 2023
1 parent 14483da commit d3ab3e1
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 0 deletions.
76 changes: 76 additions & 0 deletions protocol/messenger_community_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package protocol

import (
"errors"

"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/requests"
)

type CommunityMetricsResponse struct {
Type requests.CommunityMetricsRequestType `json:"type"`
CommunityID types.HexBytes `json:"communityId"`
Entries map[uint64]int32 `json:"entries"`
}

func initRangesMap(start uint64, end uint64, step uint64) map[uint64]int32 {
result := map[uint64]int32{}
for timestamp := start; timestamp <= end; timestamp += step {
result[timestamp] = 0
}
return result
}

func floorToRange(value uint64, start uint64, end uint64, step uint64) uint64 {
for timestamp := start; timestamp <= end; timestamp += step {
if value <= timestamp {
return timestamp
}
}
return 0
}

func (m *Messenger) collectCommunityMessagesMetrics(request *requests.CommunityMetricsRequest) (*CommunityMetricsResponse, error) {
community, err := m.GetCommunityByID(request.CommunityID)
if err != nil {
return nil, err
}

if community == nil {
return nil, errors.New("no community found")
}

// TODO: timestamp summary should be stored in special table, not calculated here
timestamps, err := m.persistence.FetchMessageTimestampsForChatsByPeriod(community.ChatIDs(), request.StartTimestamp, request.EndTimestamp)
if err != nil {
return nil, err
}

timestampStep := (request.EndTimestamp - request.StartTimestamp) / uint64(request.MaxCount)
entries := initRangesMap(request.StartTimestamp, request.EndTimestamp, timestampStep)

for _, timestamp := range timestamps {
entries[floorToRange(timestamp, request.StartTimestamp, request.EndTimestamp, timestampStep)] += 1
}

response := &CommunityMetricsResponse{
Type: request.Type,
CommunityID: request.CommunityID,
Entries: entries,
}

return response, nil
}

func (m *Messenger) CollectCommunityMetrics(request *requests.CommunityMetricsRequest) (*CommunityMetricsResponse, error) {
if err := request.Validate(); err != nil {
return nil, err
}

switch request.Type {
case requests.CommunityMetricsRequestMessages:
return m.collectCommunityMessagesMetrics(request)
default:
return nil, errors.New("metrics is not implemented yet")
}
}
30 changes: 30 additions & 0 deletions protocol/messenger_community_metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package protocol

import (
"testing"

"github.com/status-im/status-go/protocol/requests"
"github.com/stretchr/testify/suite"
)

func TestMessengerCommunityMetricsSuite(t *testing.T) {
suite.Run(t, new(MessengerCommunityMetricsSuite))
}

type MessengerCommunityMetricsSuite struct {
MessengerBaseTestSuite
}

func (s *MessengerCommunityMetricsSuite) TestCollectCommunityMessageMetrics() {
request := &requests.CommunityMetricsRequest{
CommunityID: []byte("0x654321"),
Type: requests.CommunityMetricsRequestMessages,
StartTimestamp: 1690279200,
EndTimestamp: 1690282800, // one hour
MaxCount: 10,
}
// Send contact request
resp, err := s.m.CollectCommunityMetrics(request)
s.Require().NoError(err)
s.Require().NotNil(resp)
}
75 changes: 75 additions & 0 deletions protocol/persistence_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package protocol

import (
"context"
"database/sql"
)

func (db sqlitePersistence) fetchMessagesTimestampsForPeriod(tx *sql.Tx, chatID string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) {
rows, err := tx.Query(`
SELECT timestamp FROM user_messages
WHERE local_chat_id = ? AND
timestamp >= ? AND
timestamp <= ?`,
chatID,
startTimestamp,
endTimestamp)
if err != nil {
return []uint64{}, err
}
defer rows.Close()

var timestamps []uint64
for rows.Next() {
var timestamp uint64
err := rows.Scan(&timestamp)
if err != nil {
return nil, err
}
timestamps = append(timestamps, timestamp)
}

return timestamps, nil
}

func (db sqlitePersistence) FetchMessageTimestampsForChatByPeriod(chatID string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return []uint64{}, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()

return db.fetchMessagesTimestampsForPeriod(tx, chatID, startTimestamp, endTimestamp)
}

func (db sqlitePersistence) FetchMessageTimestampsForChatsByPeriod(chatIDs []string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return []uint64{}, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()

var timestamps []uint64
for _, chatID := range chatIDs {
chatTimestamps, err := db.fetchMessagesTimestampsForPeriod(tx, chatID, startTimestamp, endTimestamp)
if err != nil {
return []uint64{}, err
}
timestamps = append(timestamps, chatTimestamps...)
}
return timestamps, nil
}
1 change: 1 addition & 0 deletions protocol/persistence_metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package protocol
42 changes: 42 additions & 0 deletions protocol/requests/community_metrics_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package requests

import (
"errors"

"github.com/status-im/status-go/eth-node/types"
)

var ErrNoCommunityId = errors.New("community metrics request has no community id")
var ErrInvalidTimeInterval = errors.New("community metrics request invalid time interval")
var ErrInvalidMaxCount = errors.New("community metrics request max count should be gratear than zero")

type CommunityMetricsRequestType uint

const (
CommunityMetricsRequestMessages CommunityMetricsRequestType = iota + 1
CommunityMetricsRequestMembers
CommunityMetricsRequestControlNodeUptime
)

type CommunityMetricsRequest struct {
CommunityID types.HexBytes `json:"communityId"`
Type CommunityMetricsRequestType `json:"type"`
StartTimestamp uint64 `json:"startTimestamp"`
EndTimestamp uint64 `json:"endTimestamp"`
MaxCount uint `json:"maxCount"`
}

func (r *CommunityMetricsRequest) Validate() error {
if len(r.CommunityID) == 0 {
return ErrNoCommunityId
}

if r.StartTimestamp == 0 || r.EndTimestamp == 0 || r.StartTimestamp >= r.EndTimestamp {
return ErrInvalidTimeInterval
}

if r.MaxCount < 1 {
return ErrInvalidMaxCount
}
return nil
}
4 changes: 4 additions & 0 deletions services/ext/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,10 @@ func (api *PublicAPI) CheckAllCommunityChannelsPermissions(request *requests.Che
return api.service.messenger.CheckAllCommunityChannelsPermissions(request)
}

func (api *PublicAPI) CollectCommunityMetrics(request *requests.CommunityMetricsRequest) (*protocol.CommunityMetricsResponse, error) {
return api.service.messenger.CollectCommunityMetrics(request)
}

func (api *PublicAPI) ShareCommunityURLWithChatKey(communityID types.HexBytes) (string, error) {
return api.service.messenger.ShareCommunityURLWithChatKey(communityID)
}
Expand Down

0 comments on commit d3ab3e1

Please sign in to comment.