Skip to content

Commit

Permalink
chore: check for feature domain event before sending notifications
Browse files Browse the repository at this point in the history
Signed-off-by: Alessandro Yuichi Okimoto <[email protected]>
  • Loading branch information
cre8ivejp committed Jan 22, 2025
1 parent 890be22 commit 8d2dff7
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 42 deletions.
76 changes: 76 additions & 0 deletions pkg/notification/sender/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ package sender

import (
"context"
"errors"

"go.uber.org/zap"
"google.golang.org/protobuf/encoding/protojson"

"github.com/bucketeer-io/bucketeer/pkg/metrics"
notificationclient "github.com/bucketeer-io/bucketeer/pkg/notification/client"
"github.com/bucketeer-io/bucketeer/pkg/notification/sender/notifier"
ftproto "github.com/bucketeer-io/bucketeer/proto/feature"
notificationproto "github.com/bucketeer-io/bucketeer/proto/notification"
senderproto "github.com/bucketeer-io/bucketeer/proto/notification/sender"
)
Expand All @@ -37,6 +40,9 @@ const (
)

var (
errFailedToUnmarshal = errors.New("sender: failed to unmarshal")
errFeatureFlagTagNotFound = errors.New("sender: feature flag tag not found")

defaultOptions = options{
logger: zap.NewNop(),
}
Expand Down Expand Up @@ -111,6 +117,20 @@ func (s *sender) Send(ctx context.Context, notificationEvent *senderproto.Notifi
}
var lastErr error
for _, subscription := range subscriptions {
// When a flag changes it must be checked before sending notifications
send, err := s.checkForFeatureDomainEvent(
subscription,
notificationEvent.SourceType,
notificationEvent.Notification.DomainEventNotification.EntityData,
)
if err != nil {
return err
}
// Check if the subcription tag is configured in the feature flag
// If not, we skip the notification
if !send {
continue
}
if err := s.send(
ctx,
notificationEvent.Notification,
Expand Down Expand Up @@ -201,3 +221,59 @@ func (s *sender) listEnabledAdminSubscriptions(
cursor = resp.Cursor
}
}

// When a flag changes it must be checked before sending notifications
func (s *sender) checkForFeatureDomainEvent(
sub *notificationproto.Subscription,
sourceType notificationproto.Subscription_SourceType,
entityData string,
) (bool, error) {
// Different domain event
if sourceType != notificationproto.Subscription_DOMAIN_EVENT_FEATURE {
s.logger.Debug(
"Skipping notification. Different domain event",
zap.String("environmentId", sub.EnvironmentId),
zap.String("subscriptionId", sub.Id),
zap.String("subscriptionName", sub.Name),
zap.Strings("subscriptionTags", sub.FeatureFlagTags),
zap.String("entityData", entityData),
)
return true, nil
}
// Unmarshal the JSON string into the Feature message
var feature ftproto.Feature
if err := protojson.Unmarshal([]byte(entityData), &feature); err != nil {
s.logger.Error("Failed to unmarshal feature message", zap.Error(err),
zap.String("environmentId", sub.EnvironmentId),
zap.String("subscriptionId", sub.Id),
zap.String("subscriptionName", sub.Name),
zap.String("entityData", entityData),
)
return false, errFailedToUnmarshal
}
// Check if the subcription tag is configured in the feature flag
// If not, we skip the notification
if containsTags(sub.FeatureFlagTags, feature.Tags) {
s.logger.Debug(
"Skipping notification. Flag doesn't contain any of the tags configured in the subscription",
zap.String("environmentId", sub.EnvironmentId),
zap.String("subscriptionId", sub.Id),
zap.String("subscriptionName", sub.Name),
zap.Strings("subscriptionTags", sub.FeatureFlagTags),
zap.String("entityData", entityData),
)
return true, nil
}
return false, errFeatureFlagTagNotFound
}

func containsTags(subTags []string, ftTags []string) bool {
for _, subTag := range subTags {
for _, ftTag := range ftTags {
if subTag == ftTag {
return true
}
}
}
return false
}
91 changes: 91 additions & 0 deletions pkg/notification/sender/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,97 @@ func TestListEnabledAdminSubscriptions(t *testing.T) {
}
}

func TestCheckForFeatureDomainEvent(t *testing.T) {
t.Parallel()
mockController := gomock.NewController(t)
defer mockController.Finish()

type inputTest struct {
subscription *notificationproto.Subscription
sourceType notificationproto.Subscription_SourceType
entityData string
}

patterns := []struct {
desc string
input inputTest
expected bool
expectedErr error
}{
{
desc: "err: failed to unmarshal",
input: inputTest{
subscription: &notificationproto.Subscription{
Id: "sub-id",
Name: "sub-name",
EnvironmentId: "env-id",
},
sourceType: notificationproto.Subscription_DOMAIN_EVENT_FEATURE,
entityData: "random-string",
},
expected: false,
expectedErr: errFailedToUnmarshal,
},
{
desc: "err: feature flag tag not found",
input: inputTest{
subscription: &notificationproto.Subscription{
Id: "sub-id",
Name: "sub-name",
EnvironmentId: "env-id",
FeatureFlagTags: []string{"web"},
},
sourceType: notificationproto.Subscription_DOMAIN_EVENT_FEATURE,
entityData: `{
"id": "feature-id-1",
"tags": ["android", "ios"]
}`,
},
expected: false,
expectedErr: errFeatureFlagTagNotFound,
},
{
desc: "success: feature flag tag found",
input: inputTest{
subscription: &notificationproto.Subscription{
Id: "sub-id",
Name: "sub-name",
EnvironmentId: "env-id",
FeatureFlagTags: []string{"ios"},
},
sourceType: notificationproto.Subscription_DOMAIN_EVENT_FEATURE,
entityData: `{
"id": "feature-id-1",
"tags": ["android", "ios"]
}`,
},
expected: true,
expectedErr: nil,
},
{
desc: "success: not a feature domain event",
input: inputTest{
sourceType: notificationproto.Subscription_DOMAIN_EVENT_ACCOUNT,
},
expected: true,
expectedErr: nil,
},
}

for _, p := range patterns {
t.Run(p.desc, func(t *testing.T) {
sender := createSender(t, mockController)
send, err := sender.checkForFeatureDomainEvent(
p.input.subscription,
p.input.sourceType,
p.input.entityData,
)
assert.Equal(t, p.expected, send)
assert.Equal(t, p.expectedErr, err)
})
}
}

func createSubscriptions(t *testing.T, size int) []*notificationproto.Subscription {
subscriptions := []*notificationproto.Subscription{}
for i := 0; i < size; i++ {
Expand Down
3 changes: 2 additions & 1 deletion pkg/subscriber/processor/domain_event_informer.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (d domainEventInformer) handleMessage(msg *puller.Message) {
msg.Ack()
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
environmentName := ""
// TODO: The environmentURLCode will be dynamic when the console v3 is ready.
Expand Down Expand Up @@ -156,6 +156,7 @@ func (d domainEventInformer) createNotificationEvent(
Editor: event.Editor,
EntityType: event.EntityType,
EntityId: event.EntityId,
EntityData: event.EntityData,
Type: event.Type,
},
},
Expand Down
90 changes: 50 additions & 40 deletions proto/notification/sender/notification.pb.go

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

1 change: 1 addition & 0 deletions proto/notification/sender/notification.proto
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ message DomainEventNotification {
reserved 6; // string environment_id = 6
string environment_name = 7;
string environment_url_code = 8;
string entity_data = 9;
}

message FeatureStaleNotification {
Expand Down
5 changes: 5 additions & 0 deletions proto/proto.lock
Original file line number Diff line number Diff line change
Expand Up @@ -31629,6 +31629,11 @@
"id": 8,
"name": "environment_url_code",
"type": "string"
},
{
"id": 9,
"name": "entity_data",
"type": "string"
}
],
"reserved_ids": [
Expand Down
4 changes: 4 additions & 0 deletions ui/web-v2/src/proto/notification/sender/notification_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export class DomainEventNotification extends jspb.Message {
getEnvironmentUrlCode(): string;
setEnvironmentUrlCode(value: string): void;

getEntityData(): string;
setEntityData(value: string): void;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): DomainEventNotification.AsObject;
static toObject(
Expand Down Expand Up @@ -124,6 +127,7 @@ export namespace DomainEventNotification {
type: proto_event_domain_event_pb.Event.TypeMap[keyof proto_event_domain_event_pb.Event.TypeMap];
environmentName: string;
environmentUrlCode: string;
entityData: string;
};
}

Expand Down
Loading

0 comments on commit 8d2dff7

Please sign in to comment.