Skip to content

Commit

Permalink
feat: add mau summary api
Browse files Browse the repository at this point in the history
  • Loading branch information
masaaania committed Oct 26, 2023
1 parent 6b43b12 commit 78f10e0
Show file tree
Hide file tree
Showing 11 changed files with 1,109 additions and 96 deletions.
133 changes: 133 additions & 0 deletions pkg/eventcounter/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type eventCounterService struct {
accountClient accountclient.Client
eventStorage v2ecstorage.EventStorage
mysqlExperimentResultStorage v2ecstorage.ExperimentResultStorage
mysqlMAUSummaryStorage v2ecstorage.MAUSummaryStorage
userCountStorage v2ecstorage.UserCountStorage
metrics metrics.Registerer
evaluationCountCacher cachev3.EventCounterCache
Expand All @@ -98,6 +99,7 @@ func NewEventCounterService(
accountClient: a,
eventStorage: v2ecstorage.NewEventStorage(b, bigQueryDataSet, l),
mysqlExperimentResultStorage: v2ecstorage.NewExperimentResultStorage(mc),
mysqlMAUSummaryStorage: v2ecstorage.NewMAUSummaryStorage(mc),
userCountStorage: v2ecstorage.NewUserCountStorage(mc),
metrics: r,
evaluationCountCacher: cachev3.NewEventCountCache(redis),
Expand Down Expand Up @@ -1051,6 +1053,86 @@ func (s *eventCounterService) GetMAUCount(
}, nil
}

func (s *eventCounterService) SummarizeMAUCounts(
ctx context.Context,
req *ecproto.SummarizeMAUCountsRequest,
) (*ecproto.SummarizeMAUCountsResponse, error) {
localizer := locale.NewLocalizer(ctx)
_, err := s.checkAdminRole(ctx, localizer)
if err != nil {
return nil, err
}
if req.YearMonth == "" {
dt, err := statusMAUYearMonthRequired.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalizeWithTemplate(locale.RequiredFieldTemplate, "year_month"),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
summaries := make([]*ecproto.MAUSummary, 0)
// Get the mau counts grouped by sourceID and environmentID.
groupBySourceID, err := s.userCountStorage.GetMAUCountsGroupBySourceID(ctx, req.YearMonth)
if err != nil {
s.logger.Error(
"Failed to get the mau counts by sourceID",
log.FieldsFromImcomingContext(ctx).AddFields(
zap.Error(err),
zap.String("yearMonth", req.YearMonth),
)...,
)
dt, err := statusInternal.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.InternalServerError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
summaries = append(summaries, groupBySourceID...)
// Get the mau counts grouped by environmentID.
groupByEnvID, err := s.userCountStorage.GetMAUCounts(ctx, req.YearMonth)
if err != nil {
s.logger.Error(
"Failed to get the mau counts",
log.FieldsFromImcomingContext(ctx).AddFields(
zap.Error(err),
zap.String("yearMonth", req.YearMonth),
)...,
)
dt, err := statusInternal.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.InternalServerError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
summaries = append(summaries, groupByEnvID...)
s.logger.Debug("SummarizeMAUCounts result", zap.Any("summaries", summaries))
for _, summary := range summaries {
summary.IsFinished = req.IsFinishDay
summary.CreatedAt = time.Now().Unix()
summary.UpdatedAt = time.Now().Unix()
err := s.mysqlMAUSummaryStorage.UpsertMAUSummary(ctx, summary)
if err != nil {
s.logger.Error(
"Failed to upsert the mau summary",
log.FieldsFromImcomingContext(ctx).AddFields(
zap.Error(err),
zap.Any("summary", summary),
)...,
)
return nil, err
}
}
return &ecproto.SummarizeMAUCountsResponse{}, nil
}

func (s *eventCounterService) getExperimentResultMySQL(
ctx context.Context,
id, environmentNamespace string,
Expand Down Expand Up @@ -1378,3 +1460,54 @@ func (s *eventCounterService) checkRole(
}
return editor, nil
}

func (s *eventCounterService) checkAdminRole(
ctx context.Context,
localizer locale.Localizer,
) (*eventproto.Editor, error) {
editor, err := role.CheckAdminRole(ctx)
if err != nil {
switch status.Code(err) {
case codes.Unauthenticated:
s.logger.Info(
"Unauthenticated",
log.FieldsFromImcomingContext(ctx).AddFields(zap.Error(err))...,
)
dt, err := statusUnauthenticated.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.UnauthenticatedError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
case codes.PermissionDenied:
s.logger.Info(
"Permission denied",
log.FieldsFromImcomingContext(ctx).AddFields(zap.Error(err))...,
)
dt, err := statusPermissionDenied.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.PermissionDenied),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
default:
s.logger.Error(
"Failed to check role",
log.FieldsFromImcomingContext(ctx).AddFields(zap.Error(err))...,
)
dt, err := statusInternal.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: localizer.MustLocalize(locale.InternalServerError),
})
if err != nil {
return nil, statusInternal.Err()
}
return nil, dt.Err()
}
}
return editor, nil
}
101 changes: 101 additions & 0 deletions pkg/eventcounter/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,106 @@ func TestGetMAUCount(t *testing.T) {
}
}

func TestSummarizeMAUCounts(t *testing.T) {
t.Parallel()
mockController := gomock.NewController(t)
defer mockController.Finish()
ctx := createContextWithToken(t, accountproto.Account_VIEWER)
ctx = metadata.NewIncomingContext(ctx, metadata.MD{
"accept-language": []string{"ja"},
})
localizer := locale.NewLocalizer(ctx)
createError := func(status *gstatus.Status, msg string) error {
st, err := status.WithDetails(&errdetails.LocalizedMessage{
Locale: localizer.GetLocale(),
Message: msg,
})
require.NoError(t, err)
return st.Err()
}
input := &ecproto.SummarizeMAUCountsRequest{
YearMonth: "201212",
IsFinishDay: false,
}
inputForGrouping := &ecproto.SummarizeMAUCountsRequest{
YearMonth: "201212",
IsFinishDay: true,
}
patterns := []struct {
desc string
setup func(*eventCounterService)
input *ecproto.SummarizeMAUCountsRequest
expected *ecproto.SummarizeMAUCountsResponse
expectedErr error
}{
{
desc: "error: mau year month is required",
input: &ecproto.SummarizeMAUCountsRequest{},
expected: nil,
expectedErr: createError(
statusMAUYearMonthRequired,
localizer.MustLocalizeWithTemplate(locale.RequiredFieldTemplate, "year_month"),
),
},
{
desc: "err: internal",
setup: func(s *eventCounterService) {
s.userCountStorage.(*v2ecsmock.MockUserCountStorage).EXPECT().GetMAUCountsGroupBySourceID(
ctx, input.YearMonth,
).Return([]*ecproto.MAUSummary{}, nil)
s.userCountStorage.(*v2ecsmock.MockUserCountStorage).EXPECT().GetMAUCounts(
ctx, input.YearMonth,
).Return(nil, errors.New("internal"))
},
input: input,
expected: nil,
expectedErr: createError(statusInternal, localizer.MustLocalize(locale.InternalServerError)),
},
{
desc: "success get mau counts",
setup: func(s *eventCounterService) {
s.userCountStorage.(*v2ecsmock.MockUserCountStorage).EXPECT().GetMAUCountsGroupBySourceID(
ctx, input.YearMonth,
).Return([]*ecproto.MAUSummary{}, nil)
s.userCountStorage.(*v2ecsmock.MockUserCountStorage).EXPECT().GetMAUCounts(
ctx, input.YearMonth,
).Return([]*ecproto.MAUSummary{}, nil)
},
input: input,
expected: &ecproto.SummarizeMAUCountsResponse{},
expectedErr: nil,
},
{
desc: "success upsert mau summary",
setup: func(s *eventCounterService) {
s.userCountStorage.(*v2ecsmock.MockUserCountStorage).EXPECT().GetMAUCountsGroupBySourceID(
ctx, input.YearMonth,
).Return([]*ecproto.MAUSummary{{Yearmonth: input.YearMonth}}, nil)
s.userCountStorage.(*v2ecsmock.MockUserCountStorage).EXPECT().GetMAUCounts(
ctx, input.YearMonth,
).Return([]*ecproto.MAUSummary{}, nil)
s.mysqlMAUSummaryStorage.(*v2ecsmock.MockMAUSummaryStorage).EXPECT().UpsertMAUSummary(
ctx, gomock.Any(),
).Return(nil)
},
input: inputForGrouping,
expected: &ecproto.SummarizeMAUCountsResponse{},
expectedErr: nil,
},
}
for _, p := range patterns {
t.Run(p.desc, func(t *testing.T) {
gs := newEventCounterService(t, mockController)
if p.setup != nil {
p.setup(gs)
}
actual, err := gs.SummarizeMAUCounts(ctx, p.input)
assert.Equal(t, p.expected, actual, "%s", p.desc)
assert.Equal(t, p.expectedErr, err, "%s", p.desc)
})
}
}

func TestGetStartTime(t *testing.T) {
t.Parallel()
patterns := []struct {
Expand Down Expand Up @@ -1660,6 +1760,7 @@ func newEventCounterService(t *testing.T, mockController *gomock.Controller) *ev
featureClient: featureclientmock.NewMockClient(mockController),
accountClient: accountClientMock,
mysqlExperimentResultStorage: v2ecsmock.NewMockExperimentResultStorage(mockController),
mysqlMAUSummaryStorage: v2ecsmock.NewMockMAUSummaryStorage(mockController),
userCountStorage: v2ecsmock.NewMockUserCountStorage(mockController),
evaluationCountCacher: eccachemock.NewMockEventCounterCache(mockController),
eventStorage: v2ecsmock.NewMockEventStorage(mockController),
Expand Down
20 changes: 20 additions & 0 deletions pkg/eventcounter/client/mock/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions pkg/eventcounter/storage/v2/mau_summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2023 The Bucketeer Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:generate mockgen -source=$GOFILE -package=mock -destination=./mock/$GOFILE
package v2

import (
"context"

"github.com/bucketeer-io/bucketeer/pkg/storage/v2/mysql"
"github.com/bucketeer-io/bucketeer/proto/eventcounter"
)

type MAUSummaryStorage interface {
UpsertMAUSummary(
ctx context.Context,
mauSummary *eventcounter.MAUSummary,
) error
}

type mauSummaryStorage struct {
qe mysql.QueryExecer
}

func NewMAUSummaryStorage(qe mysql.QueryExecer) MAUSummaryStorage {
return &mauSummaryStorage{qe: qe}
}

func (s *mauSummaryStorage) UpsertMAUSummary(
ctx context.Context,
m *eventcounter.MAUSummary,
) error {
query := `
INSERT INTO mau_summary (
yearmonth,
source_id,
user_count,
request_count,
evaluation_count,
goal_count,
is_all,
is_finished,
created_at,
updated_at,
environment_id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
) ON DUPLICATE KEY UPDATE
user_count = VALUES(user_count),
request_count = VALUES(request_count),
evaluation_count = VALUES(evaluation_count),
goal_count = VALUES(goal_count),
updated_at = VALUES(updated_at)
`
_, err := s.qe.ExecContext(
ctx,
query,
m.Yearmonth,
m.SourceId,
m.UserCount,
m.RequestCount,
m.EvaluationCount,
m.GoalCount,
m.IsAll,
m.IsFinished,
m.CreatedAt,
m.UpdatedAt,
m.EnvironmentId,
)
if err != nil {
return err
}
return nil
}

0 comments on commit 78f10e0

Please sign in to comment.