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

MEP activate multiple #24

Merged
merged 5 commits into from
Oct 18, 2022
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
git.mills.io/prologic/bitcask v1.0.2
github.com/aws/aws-sdk-go v1.40.45
github.com/flagship-io/flagship-common v0.0.18-beta.1
github.com/flagship-io/flagship-proto v0.0.16
github.com/flagship-io/flagship-proto v0.0.18
github.com/go-kit/kit v0.12.0
github.com/go-redis/redis/v8 v8.11.4
github.com/sirupsen/logrus v1.8.1
Expand Down
7 changes: 2 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -162,12 +162,9 @@ github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGE
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/flagship-io/flagship-common v0.0.18-beta.1 h1:CBo4xCpGG4ui77GhGcOmejAEnCXfcUKHsbp5MVYaeqM=
github.com/flagship-io/flagship-common v0.0.18-beta.1/go.mod h1:9NsiiubOPN6ZD8T4VvrSSwOWTloHTkoWftvRnmr4W7Y=
github.com/flagship-io/flagship-proto v0.0.15 h1:2sK9DWtnTkUEBiGtjLdwhMaaGsXuU+QRmAOhxlwj7bQ=
github.com/flagship-io/flagship-proto v0.0.15/go.mod h1:Nv5epf8wbbAEx6sAnjPumFy+YQOClXyXlxZzUMUqidw=
github.com/flagship-io/flagship-proto v0.0.16-0.20220816134540-8b1aa2ad5a0a h1:4FmZvUtZ2KjDD7V7nSJFodiAH7RYKUnMd0EQvHCFEYU=
github.com/flagship-io/flagship-proto v0.0.16-0.20220816134540-8b1aa2ad5a0a/go.mod h1:Nv5epf8wbbAEx6sAnjPumFy+YQOClXyXlxZzUMUqidw=
github.com/flagship-io/flagship-proto v0.0.16 h1:Mz/ljPweSR44Ek0moW5/yHPDz5DZO4fPTp+zHzC9nzQ=
github.com/flagship-io/flagship-proto v0.0.16/go.mod h1:Nv5epf8wbbAEx6sAnjPumFy+YQOClXyXlxZzUMUqidw=
github.com/flagship-io/flagship-proto v0.0.18 h1:u1r97L4Qm6MyhmH2G2j621ZtzDFRnS/xgUDZTCSuH2k=
github.com/flagship-io/flagship-proto v0.0.18/go.mod h1:Nv5epf8wbbAEx6sAnjPumFy+YQOClXyXlxZzUMUqidw=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/franela/goblin v0.0.0-20210519012713-85d372ac71e2/go.mod h1:VzmDKDJVZI3aJmnRI9VjAn9nJ8qPPsN1fqzr9dqInIo=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
Expand Down
28 changes: 3 additions & 25 deletions internal/validation/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ func BuildErrorResponse(bodyError map[string]string) *ErrorResponse {
}
}

func CheckErrorBody(body *activate_request.ActivateRequest) *ErrorResponse {
func CheckErrorBody(envID string, body *activate_request.ActivateRequest) *ErrorResponse {
errorResponse := map[string]string{}
if body.Cid == "" {
errorResponse["cid"] = "Field is mandatory."
} else if envID != body.Cid {
errorResponse["cid"] = "Invalid cid."
}
if body.Vid == "" {
errorResponse["vid"] = "Field is mandatory."
Expand All @@ -35,27 +37,3 @@ func CheckErrorBody(body *activate_request.ActivateRequest) *ErrorResponse {
}
return BuildErrorResponse(errorResponse)
}

// CheckErrorBodyMultiple checks a multiple activation request
func CheckErrorBodyMultiple(body *activate_request.ActivateRequestMultiple) *ErrorResponse {
errorResponse := map[string]string{}
if body.EnvironmentId == "" {
errorResponse["environment_id"] = "Field is mandatory."
}

for _, a := range body.Activations {
if a.VariationId == "" {
errorResponse["variation_id"] = "Field is mandatory."
}
if a.VariationGroupId == "" {
errorResponse["variation_group_id"] = "Field is mandatory."
}
if body.VisitorId == "" && a.VisitorId == "" {
errorResponse["visitor_id"] = "Field is mandatory. It can be set globally or for each specific activation"
}
}
if len(errorResponse) == 0 {
return nil
}
return BuildErrorResponse(errorResponse)
}
37 changes: 7 additions & 30 deletions internal/validation/activate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,20 @@ func TestBuildErrorResponse(t *testing.T) {
}

func TestCheckErrorBody(t *testing.T) {
resp := CheckErrorBody(&activate_request.ActivateRequest{})

resp := CheckErrorBody("env_id", &activate_request.ActivateRequest{})
assert.Equal(t, "error", resp.Status)
assert.Equal(t, "Field is mandatory.", resp.Errors["cid"])
assert.Equal(t, "Field is mandatory.", resp.Errors["vid"])
assert.Equal(t, "Field is mandatory.", resp.Errors["caid"])
assert.Equal(t, "Field is mandatory.", resp.Errors["vaid"])

resp = CheckErrorBody(&activate_request.ActivateRequest{
resp = CheckErrorBody("fake", &activate_request.ActivateRequest{
Cid: "env_id",
})
assert.Equal(t, "Invalid cid.", resp.Errors["cid"])

resp = CheckErrorBody("env_id", &activate_request.ActivateRequest{
Cid: "env_id",
Vid: "visitor_id",
Caid: "campaign_id",
Expand All @@ -32,31 +37,3 @@ func TestCheckErrorBody(t *testing.T) {

assert.Nil(t, resp)
}

// CheckErrorBodyMultiple checks a multiple activation request
func TestCheckErrorBodyMultiple(t *testing.T) {
resp := CheckErrorBodyMultiple(&activate_request.ActivateRequestMultiple{
Activations: []*activate_request.ActivateRequestMultipleInner{
{},
},
})

assert.Equal(t, "error", resp.Status)
assert.Equal(t, "Field is mandatory.", resp.Errors["environment_id"])
assert.Equal(t, "Field is mandatory. It can be set globally or for each specific activation", resp.Errors["visitor_id"])
assert.Equal(t, "Field is mandatory.", resp.Errors["variation_id"])
assert.Equal(t, "Field is mandatory.", resp.Errors["variation_group_id"])

resp = CheckErrorBodyMultiple(&activate_request.ActivateRequestMultiple{
EnvironmentId: "env_id",
VisitorId: "vis_id",
Activations: []*activate_request.ActivateRequestMultipleInner{
{
VariationGroupId: "vg_id",
VariationId: "v_id",
},
},
})

assert.Nil(t, resp)
}
142 changes: 79 additions & 63 deletions pkg/handlers/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,95 +30,111 @@ import (
// @Router /activate [post]
func Activate(context *connectors.DecisionContext) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
activateRequest := &activate_request.ActivateRequest{}
var activateItems []*activate_request.ActivateRequest

data, err := io.ReadAll(req.Body)
if err != nil {
utils.WriteServerError(w, err)
return
}

// check body unique, if not check body multiple
activateRequest := &activate_request.ActivateRequest{}
if err := protojson.Unmarshal(data, activateRequest); err != nil {
utils.WriteClientError(w, http.StatusBadRequest, err.Error())
return
}
activateRequestBatch := &activate_request.ActivateRequestBatch{}
if err := protojson.Unmarshal(data, activateRequestBatch); err != nil {
utils.WriteClientError(w, http.StatusBadRequest, err.Error())
return
}

if bodyErr := validation.CheckErrorBody(activateRequest); bodyErr != nil {
data, _ := json.Marshal(bodyErr)
utils.WriteClientError(w, http.StatusBadRequest, string(data))
return
activateItems = activateRequestBatch.Batch
for _, activateItem := range activateItems {
activateItem.Cid = activateRequestBatch.Cid
}
} else {
activateItems = []*activate_request.ActivateRequest{activateRequest}
}

now := time.Now()
// error management & campaign activations
errorsLength := 0
errors := make(chan error)
campaignActivations := []*models.CampaignActivation{}

visitorID := activateRequest.Vid
// If anonymous id is defined
if activateRequest.Aid != nil {
visitorID = activateRequest.Aid.Value
}
for _, activateItem := range activateItems {
if bodyErr := validation.CheckErrorBody(context.EnvID, activateItem); bodyErr != nil {
data, _ := json.Marshal(bodyErr)
utils.WriteClientError(w, http.StatusBadRequest, string(data))
return
}

shouldPersistActivation := false
environment, err := context.EnvironmentLoader.LoadEnvironment(activateRequest.Cid, context.APIKey)
if err != nil {
log.Printf("Error when reading existing environment : %v", err)
} else {
shouldPersistActivation = environment.Common.CacheEnabled && environment.Common.SingleAssignment
}
now := time.Now()

assignments := map[string]*decision.VisitorCache{}
if shouldPersistActivation {
existingAssignments, err := context.AssignmentsManager.LoadAssignments(activateRequest.Cid, activateRequest.Vid)
if err != nil {
log.Printf("Error when reading existing assignments : %v", err)
visitorID := activateItem.Vid
// If anonymous id is defined
if activateItem.Aid != nil {
visitorID = activateItem.Aid.Value
}

var vgAssign *decision.VisitorCache
if existingAssignments != nil {
vgAssign = existingAssignments.Assignments[activateRequest.Caid]
shouldPersistActivation := false
environment, err := context.EnvironmentLoader.LoadEnvironment(activateItem.Cid, context.APIKey)
if err != nil {
log.Printf("Error when reading existing environment : %v", err)
} else {
shouldPersistActivation = environment.Common.CacheEnabled && environment.Common.SingleAssignment
}

assignments[activateRequest.Caid] = &decision.VisitorCache{
VariationID: activateRequest.Vaid,
Activated: true,
}
shouldPersistActivation = vgAssign == nil || !vgAssign.Activated || vgAssign.VariationID != activateRequest.Vaid
}
if shouldPersistActivation {
existingAssignments, err := context.AssignmentsManager.LoadAssignments(activateItem.Cid, activateItem.Vid)
if err != nil {
log.Printf("Error when reading existing assignments : %v", err)
}

chanLength := 1
if shouldPersistActivation {
chanLength = 2
}
var vgAssign *decision.VisitorCache
if existingAssignments != nil {
vgAssign = existingAssignments.Assignments[activateItem.Caid]
}

errors := make(chan error, chanLength)
shouldPersistActivation = vgAssign == nil || !vgAssign.Activated || vgAssign.VariationID != activateItem.Vaid
}

if shouldPersistActivation {
go func(errors chan error) {
if !context.AssignmentsManager.ShouldSaveAssignments(connectors.SaveAssignmentsContext{
AssignmentScope: connectors.Activation,
}) {
return
}
errors <- context.AssignmentsManager.SaveAssignments(context.EnvID, activateRequest.Vid, assignments, now)
}(errors)
if shouldPersistActivation {
errorsLength++
go func(activateItem *activate_request.ActivateRequest) {
if !context.AssignmentsManager.ShouldSaveAssignments(connectors.SaveAssignmentsContext{
AssignmentScope: connectors.Activation,
}) {
return
}
errors <- context.AssignmentsManager.SaveAssignments(context.EnvID, activateItem.Vid, map[string]*decision.VisitorCache{
activateItem.Caid: {
VariationID: activateItem.Vaid,
Activated: true,
},
}, now)
}(activateItem)
}

campaignActivations = append(campaignActivations, &models.CampaignActivation{
EnvID: activateItem.Cid,
VisitorID: visitorID,
CustomerID: activateItem.Vid,
CampaignID: activateItem.Caid,
VariationID: activateItem.Vaid,
Timestamp: now.UnixNano() / 1000000,
PersistActivate: shouldPersistActivation,
})
}

go func(errors chan error) {
errorsLength++
go func() {
errors <- context.HitsProcessor.TrackHits(
connectors.TrackingHits{
CampaignActivations: []*models.CampaignActivation{
{
EnvID: activateRequest.Cid,
VisitorID: visitorID,
CustomerID: activateRequest.Vid,
CampaignID: activateRequest.Caid,
VariationID: activateRequest.Vaid,
Timestamp: now.UnixNano() / 1000000,
PersistActivate: shouldPersistActivation,
},
},
CampaignActivations: campaignActivations,
})
}(errors)

for i := 0; i < chanLength; i++ {
}()

for i := 0; i < errorsLength; i++ {
err := <-errors
if err != nil {
utils.WriteServerError(w, err)
Expand Down
47 changes: 47 additions & 0 deletions pkg/handlers/activate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func TestActivate(t *testing.T) {
assert.Equal(t, 400, resp.StatusCode)
assert.Contains(t, string(bodyResp), "Field is mandatory")

// activate simple
body = `{
"cid": "env_id",
"aid": "anonymous_id",
Expand Down Expand Up @@ -104,4 +105,50 @@ func TestActivate(t *testing.T) {
assert.Equal(t, "visitor_id", hitProcessor.TrackedHits.CampaignActivations[0].CustomerID)
assert.Equal(t, "anonymous_id", hitProcessor.TrackedHits.CampaignActivations[0].VisitorID)
assert.True(t, hitProcessor.TrackedHits.CampaignActivations[0].PersistActivate)

// activate batch
body = `{
"cid": "env_id",
"batch": [
{
"aid": "anonymous_id",
"vid": "visitor_id",
"caid": "campaign_id",
"vaid": "variation_id"
},
{
"aid": "anonymous_id",
"vid": "visitor_id",
"caid": "campaign_id_2",
"vaid": "variation_id_2"
}
]
}`
w = httptest.NewRecorder()
req.Body = io.NopCloser(strings.NewReader(body))
Activate(context)(w, req)

resp = w.Result()
assert.Equal(t, 204, resp.StatusCode)
cacheVisitor, err = assignmentManager.LoadAssignments("env_id", "visitor_id")
assert.Nil(t, err)
assert.Len(t, cacheVisitor.Assignments, 2)
assert.Equal(t, "variation_id", cacheVisitor.Assignments["campaign_id"].VariationID)
assert.Equal(t, "variation_id_2", cacheVisitor.Assignments["campaign_id_2"].VariationID)
assert.True(t, cacheVisitor.Assignments["campaign_id"].Activated)
assert.True(t, cacheVisitor.Assignments["campaign_id_2"].Activated)

assert.Len(t, hitProcessor.TrackedHits.CampaignActivations, 2)
assert.Equal(t, "variation_id", hitProcessor.TrackedHits.CampaignActivations[0].VariationID)
assert.Equal(t, "campaign_id", hitProcessor.TrackedHits.CampaignActivations[0].CampaignID)
assert.Equal(t, "env_id", hitProcessor.TrackedHits.CampaignActivations[0].EnvID)
assert.Equal(t, "visitor_id", hitProcessor.TrackedHits.CampaignActivations[0].CustomerID)
assert.Equal(t, "anonymous_id", hitProcessor.TrackedHits.CampaignActivations[0].VisitorID)
assert.Equal(t, "variation_id_2", hitProcessor.TrackedHits.CampaignActivations[1].VariationID)
assert.Equal(t, "campaign_id_2", hitProcessor.TrackedHits.CampaignActivations[1].CampaignID)
assert.Equal(t, "env_id", hitProcessor.TrackedHits.CampaignActivations[1].EnvID)
assert.Equal(t, "visitor_id", hitProcessor.TrackedHits.CampaignActivations[1].CustomerID)
assert.Equal(t, "anonymous_id", hitProcessor.TrackedHits.CampaignActivations[1].VisitorID)
assert.False(t, hitProcessor.TrackedHits.CampaignActivations[0].PersistActivate)
assert.True(t, hitProcessor.TrackedHits.CampaignActivations[1].PersistActivate)
}