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

add support for analytics #698

Merged
merged 23 commits into from
Jun 12, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7528ef9
feat: add support for analytics
Dotunj Jun 2, 2022
8d53508
feat: add support for configuration resource
Dotunj Jun 3, 2022
c28d263
chore: remove unused import
Dotunj Jun 3, 2022
4f649de
chore: add cron for tracking analytcis
Dotunj Jun 3, 2022
aebe3bd
feat: add config repository test
Dotunj Jun 3, 2022
c49a198
chore: add configuration service test
Dotunj Jun 3, 2022
6a37f93
chore: add configuration integration test
Dotunj Jun 3, 2022
e2a3aab
feat: add api version to configuration response
Dotunj Jun 4, 2022
7a65bc0
chore: add analytics test
Dotunj Jun 5, 2022
d496843
chore: generate docs
Dotunj Jun 5, 2022
e4bfc61
feat: use separate tokens based on environment
Dotunj Jun 6, 2022
455fc93
chore: Merge branch 'main' of https://github.com/frain-dev/convoy int…
Dotunj Jun 6, 2022
7a22ac3
chore: Merge branch 'main' of https://github.com/frain-dev/convoy int…
Dotunj Jun 7, 2022
31ed27f
chore: add cron schedule
Dotunj Jun 7, 2022
47a1260
Merge branch 'main' of https://github.com/frain-dev/convoy into dotun…
Dotunj Jun 8, 2022
be98442
feat: add support for analytics source
Dotunj Jun 9, 2022
b9bb227
chore: remove repeated org repo
Dotunj Jun 9, 2022
5e01c08
Merge branch 'main' of https://github.com/frain-dev/convoy into dotun…
Dotunj Jun 9, 2022
2328575
Merge branch 'main' of https://github.com/frain-dev/convoy into dotun…
Dotunj Jun 11, 2022
280ce89
feat: add org name to event details
Dotunj Jun 11, 2022
605ba6e
Merge branch 'main' of https://github.com/frain-dev/convoy into dotun…
Dotunj Jun 11, 2022
4be8c78
Merge branch 'main' of https://github.com/frain-dev/convoy into dotun…
Dotunj Jun 11, 2022
ed10ae5
feat: add support for tracking host
Dotunj Jun 11, 2022
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
52 changes: 52 additions & 0 deletions analytics/active_group_analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package analytics

import (
"context"
"time"

"github.com/frain-dev/convoy/datastore"
log "github.com/sirupsen/logrus"
)

type ActiveGroupAnalytics struct {
groupRepo datastore.GroupRepository
eventRepo datastore.EventRepository
client AnalyticsClient
}

func newActiveGroupAnalytics(groupRepo datastore.GroupRepository, eventRepo datastore.EventRepository, client AnalyticsClient) *ActiveGroupAnalytics {
return &ActiveGroupAnalytics{groupRepo: groupRepo, eventRepo: eventRepo, client: client}
}

func (a *ActiveGroupAnalytics) Track() error {
groups, err := a.groupRepo.LoadGroups(context.Background(), &datastore.GroupFilter{})
if err != nil {
return err
}

count := 0
now := time.Now()
for _, group := range groups {
filter := datastore.SearchParams{
CreatedAtStart: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).Unix(),
CreatedAtEnd: time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999999999, time.UTC).Unix(),
}

events, _, err := a.eventRepo.LoadEventsPaged(context.Background(), group.UID, "", filter, datastore.Pageable{Sort: -1})
if err != nil {
log.WithError(err).Error("failed to load events paged")
continue
}

if len(events) > 0 {
count += 1
}
}

return a.client.Export(a.Name(), Event{"Count": count})

}

func (a *ActiveGroupAnalytics) Name() string {
return DailyActiveGroupCount
}
70 changes: 70 additions & 0 deletions analytics/active_group_analytics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package analytics

import (
"errors"
"testing"

"github.com/frain-dev/convoy/datastore"
"github.com/frain-dev/convoy/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

func provideActiveGroupAnalytics(ctrl *gomock.Controller) *ActiveGroupAnalytics {
groupRepo := mocks.NewMockGroupRepository(ctrl)
eventRepo := mocks.NewMockEventRepository(ctrl)
client := NewNoopAnalyticsClient()

return newActiveGroupAnalytics(groupRepo, eventRepo, client)
}

func Test_TrackActiveGroupAnalytics(t *testing.T) {

tests := []struct {
name string
dbFn func(ga *ActiveGroupAnalytics)
wantErr bool
}{
{
name: "should_track_active_group_analytics",
dbFn: func(ga *ActiveGroupAnalytics) {
groupRepo := ga.groupRepo.(*mocks.MockGroupRepository)
eventRepo := ga.eventRepo.(*mocks.MockEventRepository)

groupRepo.EXPECT().LoadGroups(gomock.Any(), gomock.Any()).Return([]*datastore.Group{{UID: "123456", Name: "test"}}, nil)
eventRepo.EXPECT().LoadEventsPaged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, datastore.PaginationData{}, nil)
},
},

{
name: "should_fail_to_track_active_group_analytics",
dbFn: func(ga *ActiveGroupAnalytics) {
groupRepo := ga.groupRepo.(*mocks.MockGroupRepository)
groupRepo.EXPECT().LoadGroups(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed"))
},
wantErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

ga := provideActiveGroupAnalytics(ctrl)

if tc.dbFn != nil {
tc.dbFn(ga)
}

err := ga.Track()

if tc.wantErr {
require.NotNil(t, err)
return
}

require.Nil(t, err)
})
}
}
154 changes: 154 additions & 0 deletions analytics/analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package analytics

import (
"context"
"encoding/base64"
"errors"

"github.com/dukex/mixpanel"
"github.com/frain-dev/convoy/config"
"github.com/frain-dev/convoy/datastore"
"github.com/google/uuid"
"github.com/hibiken/asynq"
log "github.com/sirupsen/logrus"
)

const (
DailyEventCount string = "Daily Event Count"
DailyOrganisationCount string = "Daily Organization Count"
DailyGroupCount string = "Daily Project Count"
DailyActiveGroupCount string = "Daily Active Project Count"
DailyUserCount string = "Daily User Count"
MixPanelDevToken string = "YTAwYWI1ZWE3OTE2MzQwOWEwMjk4ZTA1NTNkNDQ0M2M="
MixPanelProdToken string = "YWViNzUwYWRmYjM0YTZmZjJkMzg2YTYyYWVhY2M2NWI="
)

type Tracker interface {
Track() error
Name() string
}

type Event map[string]interface{}

type AnalyticsClient interface {
Export(eventName string, e Event) error
}

type analyticsMap map[string]Tracker

type Repo struct {
ConfigRepo datastore.ConfigurationRepository
EventRepo datastore.EventRepository
GroupRepo datastore.GroupRepository
OrgRepo datastore.OrganisationRepository
UserRepo datastore.UserRepository
}

type Analytics struct {
Repo *Repo
trackers analyticsMap
client AnalyticsClient
}

func newAnalytics(Repo *Repo, cfg config.Configuration) (*Analytics, error) {
client, err := NewMixPanelClient(cfg)
if err != nil {
return nil, err
}

a := &Analytics{Repo: Repo, client: client}

a.RegisterTrackers()
return a, nil
}

func TrackDailyAnalytics(Repo *Repo, cfg config.Configuration) func(context.Context, *asynq.Task) error {
return func(ctx context.Context, t *asynq.Task) error {
a, err := newAnalytics(Repo, cfg)
if err != nil {
log.Fatal(err)
}

a.trackDailyAnalytics()

return nil
}
}

func (a *Analytics) trackDailyAnalytics() {
config, err := a.Repo.ConfigRepo.LoadConfiguration(context.Background())
if err != nil {
if errors.Is(err, datastore.ErrConfigNotFound) {
return
}

log.WithError(err).Error("failed to track metrics")
}

if !config.IsAnalyticsEnabled {
return
}

for _, tracker := range a.trackers {
go func(tracker Tracker) {
err := tracker.Track()
if err != nil {
log.WithError(err).Error("failed to track metrics")
}
}(tracker)
}
}

func (a *Analytics) RegisterTrackers() {
a.trackers = analyticsMap{
DailyEventCount: newEventAnalytics(a.Repo.EventRepo, a.client),
DailyOrganisationCount: newOrganisationAnalytics(a.Repo.OrgRepo, a.client),
DailyGroupCount: newGroupAnalytics(a.Repo.GroupRepo, a.client),
DailyActiveGroupCount: newActiveGroupAnalytics(a.Repo.GroupRepo, a.Repo.EventRepo, a.client),
DailyUserCount: newUserAnalytics(a.Repo.UserRepo, a.client),
}

}

type MixPanelClient struct {
client mixpanel.Mixpanel
}

func NewMixPanelClient(cfg config.Configuration) (*MixPanelClient, error) {
token := MixPanelDevToken

if cfg.Environment == "prod" {
token = MixPanelProdToken
}

decoded, err := base64.StdEncoding.DecodeString(token)
if err != nil {
return nil, err
}

c := mixpanel.New(string(decoded), "")
return &MixPanelClient{client: c}, nil
}

func (m *MixPanelClient) Export(eventName string, e Event) error {
err := m.client.Track(uuid.NewString(), eventName, &mixpanel.Event{
IP: "0",
Timestamp: nil,
Properties: e,
})
if err != nil {
return err
}

return nil
}

type NoopAnalyticsClient struct{}

func NewNoopAnalyticsClient() *NoopAnalyticsClient {
return &NoopAnalyticsClient{}
}

func (n *NoopAnalyticsClient) Export(eventName string, e Event) error {
return nil
}
29 changes: 29 additions & 0 deletions analytics/event_analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package analytics

import (
"context"

"github.com/frain-dev/convoy/datastore"
)

type EventAnalytics struct {
eventRepo datastore.EventRepository
client AnalyticsClient
}

func newEventAnalytics(eventRepo datastore.EventRepository, client AnalyticsClient) *EventAnalytics {
return &EventAnalytics{eventRepo: eventRepo, client: client}
}

func (ea *EventAnalytics) Track() error {
_, pagination, err := ea.eventRepo.LoadEventsPaged(context.Background(), "", "", datastore.SearchParams{}, datastore.Pageable{Sort: -1})
if err != nil {
return err
}

return ea.client.Export(ea.Name(), Event{"Count": pagination.Total})
subomi marked this conversation as resolved.
Show resolved Hide resolved
}

func (ea *EventAnalytics) Name() string {
return DailyEventCount
}
66 changes: 66 additions & 0 deletions analytics/event_analytics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package analytics

import (
"errors"
"testing"

"github.com/frain-dev/convoy/datastore"
"github.com/frain-dev/convoy/mocks"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)

func provideEventAnalytics(ctrl *gomock.Controller) *EventAnalytics {
eventRepo := mocks.NewMockEventRepository(ctrl)
client := NewNoopAnalyticsClient()

return newEventAnalytics(eventRepo, client)
}

func Test_TrackEventAnalytics(t *testing.T) {

tests := []struct {
name string
dbFn func(ea *EventAnalytics)
wantErr bool
}{
{
name: "should_track_event_analytics",
dbFn: func(ea *EventAnalytics) {
eventRepo := ea.eventRepo.(*mocks.MockEventRepository)
eventRepo.EXPECT().LoadEventsPaged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, datastore.PaginationData{}, nil)
},
},

{
name: "should_fail_to_track_event_analytics",
dbFn: func(ea *EventAnalytics) {
eventRepo := ea.eventRepo.(*mocks.MockEventRepository)
eventRepo.EXPECT().LoadEventsPaged(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, datastore.PaginationData{}, errors.New("failed"))
},
wantErr: true,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

ea := provideEventAnalytics(ctrl)

if tc.dbFn != nil {
tc.dbFn(ea)
}

err := ea.Track()

if tc.wantErr {
require.NotNil(t, err)
return
}

require.Nil(t, err)
})
}
}
Loading