Skip to content

Commit

Permalink
feat: collecting community message metrics with test
Browse files Browse the repository at this point in the history
  • Loading branch information
MishkaRogachev committed Jul 26, 2023
1 parent d3ab3e1 commit 35196cc
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 30 deletions.
21 changes: 6 additions & 15 deletions protocol/messenger_community_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,16 @@ import (
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
Entries map[uint64]uint `json:"entries"`
}

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

func (m *Messenger) collectCommunityMessagesMetrics(request *requests.CommunityMetricsRequest) (*CommunityMetricsResponse, error) {
Expand All @@ -46,11 +38,10 @@ func (m *Messenger) collectCommunityMessagesMetrics(request *requests.CommunityM
return nil, err
}

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

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

response := &CommunityMetricsResponse{
Expand Down
184 changes: 180 additions & 4 deletions protocol/messenger_community_metrics_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package protocol

import (
"fmt"
"testing"

"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/communities"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"github.com/stretchr/testify/suite"
)
Expand All @@ -15,16 +21,186 @@ type MessengerCommunityMetricsSuite struct {
MessengerBaseTestSuite
}

func (s *MessengerCommunityMetricsSuite) TestCollectCommunityMessageMetrics() {
func (s *MessengerCommunityMetricsSuite) prepareCommunity() *communities.Community {
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_NO_MEMBERSHIP,
Name: "status",
Color: "#ffffff",
Description: "status community description",
}
response, err := s.m.CreateCommunity(description, true)

s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)

return response.Communities()[0]
}

func (s *MessengerCommunityMetricsSuite) generateMessages(chatID string, communityID string, timestamps []uint64) {
var messages []*common.Message
for i, timestamp := range timestamps {
message := &common.Message{
ChatMessage: protobuf.ChatMessage{
ChatId: chatID,
Text: fmt.Sprintf("Test message %d", i),
MessageType: protobuf.MessageType_ONE_TO_ONE,
// NOTE: should we filter content types for messages metrics
Clock: timestamp,
Timestamp: timestamp,
},
WhisperTimestamp: timestamp,
From: common.PubkeyToHex(&s.m.identity.PublicKey),
LocalChatID: chatID,
CommunityID: communityID,
ID: types.EncodeHex(crypto.Keccak256([]byte(fmt.Sprintf("%s%s%d", chatID, communityID, timestamp)))),
}

err := message.PrepareContent(common.PubkeyToHex(&s.m.identity.PublicKey))
s.Require().NoError(err)

messages = append(messages, message)
}
err := s.m.persistence.SaveMessages(messages)
s.Require().NoError(err)
}

func (s *MessengerCommunityMetricsSuite) TestCollectCommunityMessagesMetricsEmpty() {
community := s.prepareCommunity()

request := &requests.CommunityMetricsRequest{
CommunityID: []byte("0x654321"),
CommunityID: community.ID(),
Type: requests.CommunityMetricsRequestMessages,
StartTimestamp: 1690279200,
EndTimestamp: 1690282800, // one hour
MaxCount: 10,
StepTimestamp: 100,
}

// Expect empty metrics
resp, err := s.m.CollectCommunityMetrics(request)
s.Require().NoError(err)
s.Require().NotNil(resp)

// Entries count should be empty
s.Require().Len(resp.Entries, 0)
}

func (s *MessengerCommunityMetricsSuite) TestCollectCommunityMessagesMetricsOneChat() {
community := s.prepareCommunity()

s.Require().Len(community.ChatIDs(), 1)
chatId := community.ChatIDs()[0]

s.generateMessages(chatId, string(community.ID()), []uint64{
// out ouf range messages in the begining
1690162000,
1690371999,
// 1st column, 3 message
1690372000,
1690372100,
1690372200,
// 2nd column, 2 messages
1690372700,
1690372800,
// 3rd column, 1 message
1690373000,
// out ouf range messages in the end
1690373100,
1690374000,
1690383000,
})

// Request metrics
request := &requests.CommunityMetricsRequest{
CommunityID: community.ID(),
Type: requests.CommunityMetricsRequestMessages,
StartTimestamp: 1690372000,
EndTimestamp: 1690373000, // one hour
StepTimestamp: 300,
}

resp, err := s.m.CollectCommunityMetrics(request)
s.Require().NoError(err)
s.Require().NotNil(resp)

// floor(1000 / 300) == 3
s.Require().Len(resp.Entries, 3)

s.Require().Equal(resp.Entries[1690372300], uint(3))
// No entries for 1690372600
s.Require().Equal(resp.Entries[1690372900], uint(2))
s.Require().Equal(resp.Entries[1690373000], uint(1))
}

func (s *MessengerCommunityMetricsSuite) TestCollectCommunityMessagesMetricsMultipleChats() {
community := s.prepareCommunity()

s.Require().Len(community.ChatIDs(), 1)
chatIds := community.ChatIDs()

// Create another chat
chat := &protobuf.CommunityChat{
Permissions: &protobuf.CommunityPermissions{
Access: protobuf.CommunityPermissions_NO_MEMBERSHIP,
},
Identity: &protobuf.ChatIdentity{
DisplayName: "status",
Emoji: "👍",
Description: "status community chat",
},
}
response, err := s.m.CreateCommunityChat(community.ID(), chat)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
s.Require().Len(response.Chats(), 1)

chatIds = append(chatIds, response.Chats()[0].ID)

s.generateMessages(chatIds[0], string(community.ID()), []uint64{
// out ouf range messages in the begining
1690162000,
// 1st column, 1 message
1690372200,
// 2nd column, 1 message
1690372800,
// 3rd column, 1 message
1690373000,
// out ouf range messages in the end
1690373100,
})

s.generateMessages(chatIds[1], string(community.ID()), []uint64{
// out ouf range messages in the begining
1690152000,
// 1st column, 2 messages
1690372000,
1690372100,
// 2nd column, 1 message
1690372700,
// 3rd column empty
// out ouf range messages in the end
1690373100,
})

// Request metrics
request := &requests.CommunityMetricsRequest{
CommunityID: community.ID(),
Type: requests.CommunityMetricsRequestMessages,
StartTimestamp: 1690372000,
EndTimestamp: 1690373000, // one hour
StepTimestamp: 300,
}
// Send contact request

resp, err := s.m.CollectCommunityMetrics(request)
s.Require().NoError(err)
s.Require().NotNil(resp)

// floor(1000 / 300) == 3
s.Require().Len(resp.Entries, 3)

s.Require().Equal(resp.Entries[1690372300], uint(3))
// No entries for 1690372600
s.Require().Equal(resp.Entries[1690372900], uint(2))
s.Require().Equal(resp.Entries[1690373000], uint(1))
}
9 changes: 5 additions & 4 deletions protocol/persistence_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import (

func (db sqlitePersistence) fetchMessagesTimestampsForPeriod(tx *sql.Tx, chatID string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) {
rows, err := tx.Query(`
SELECT timestamp FROM user_messages
SELECT whisper_timestamp FROM user_messages
WHERE local_chat_id = ? AND
timestamp >= ? AND
timestamp <= ?`,
whisper_timestamp >= ? AND
whisper_timestamp <= ?`,
chatID,
startTimestamp,
endTimestamp)
endTimestamp,
)
if err != nil {
return []uint64{}, err
}
Expand Down
1 change: 0 additions & 1 deletion protocol/persistence_metrics_test.go

This file was deleted.

12 changes: 6 additions & 6 deletions protocol/requests/community_metrics_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

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")
var ErrInvalidTimestampInterval = errors.New("community metrics request invalid time interval")
var ErrInvalidTimestampStep = errors.New("community metrics request invalid time step")

type CommunityMetricsRequestType uint

Expand All @@ -23,7 +23,7 @@ type CommunityMetricsRequest struct {
Type CommunityMetricsRequestType `json:"type"`
StartTimestamp uint64 `json:"startTimestamp"`
EndTimestamp uint64 `json:"endTimestamp"`
MaxCount uint `json:"maxCount"`
StepTimestamp uint64 `json:"stepTimestamp"`
}

func (r *CommunityMetricsRequest) Validate() error {
Expand All @@ -32,11 +32,11 @@ func (r *CommunityMetricsRequest) Validate() error {
}

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

if r.MaxCount < 1 {
return ErrInvalidMaxCount
if r.StepTimestamp < 1 || r.StepTimestamp > (r.EndTimestamp-r.StartTimestamp) {
return ErrInvalidTimestampStep
}
return nil
}

0 comments on commit 35196cc

Please sign in to comment.