Skip to content
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: 2 additions & 0 deletions api-description/apidocs.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3008,6 +3008,7 @@ paths:
- DOMAIN_EVENT_TAG
- DOMAIN_EVENT_CODEREF
- DOMAIN_EVENT_TEAM
- DOMAIN_EVENT_DEMO_ORGANIZATION
- FEATURE_STALE
- EXPERIMENT_RUNNING
- MAU_COUNT
Expand Down Expand Up @@ -3519,6 +3520,7 @@ definitions:
- DOMAIN_EVENT_TAG
- DOMAIN_EVENT_CODEREF
- DOMAIN_EVENT_TEAM
- DOMAIN_EVENT_DEMO_ORGANIZATION
- FEATURE_STALE
- EXPERIMENT_RUNNING
- MAU_COUNT
Expand Down
2 changes: 2 additions & 0 deletions api-description/web-api.swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7971,6 +7971,7 @@ paths:
- DOMAIN_EVENT_TAG
- DOMAIN_EVENT_CODEREF
- DOMAIN_EVENT_TEAM
- DOMAIN_EVENT_DEMO_ORGANIZATION
- FEATURE_STALE
- EXPERIMENT_RUNNING
- MAU_COUNT
Expand Down Expand Up @@ -8699,6 +8700,7 @@ definitions:
- DOMAIN_EVENT_TAG
- DOMAIN_EVENT_CODEREF
- DOMAIN_EVENT_TEAM
- DOMAIN_EVENT_DEMO_ORGANIZATION
- FEATURE_STALE
- EXPERIMENT_RUNNING
- MAU_COUNT
Expand Down
14 changes: 14 additions & 0 deletions manifests/bucketeer/charts/subscriber/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ subscribers:
pullerMaxOutstandingBytes: 100000000
maxMps: 1000
workerNum: 5
demoOrganizationCreationNotifier:
pubSubType: google
project:
topic:
subscription:
pullerNumGoroutines: 5
pullerMaxOutstandingMessages: 1000
pullerMaxOutstandingBytes: 1000000000
maxMps: 50
workerNum: 1

onDemandSubscribers:
evaluationCountEventDWHPersister:
Expand Down Expand Up @@ -241,6 +251,10 @@ processors:
userEventPersister:
flushSize: 200
flushInterval: 5
demoOrganizationCreationNotifier:
notifier:
slack:
webhookURL: ""

onDemandProcessors:
evaluationCountEventDWHPersister:
Expand Down
17 changes: 17 additions & 0 deletions manifests/bucketeer/values.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,19 @@ subscriber:
pullerMaxOutstandingBytes: 1000000000
maxMps: 50
workerNum: 1
demoOrganizationCreationNotifier:
pubSubType: ${global.pubsub.type}
redisAddr: ${global.pubsub.redis.addr}
redisPoolSize: ${global.pubsub.redis.poolSize}
redisMinIdle: ${global.pubsub.redis.minIdle}
project: ${global.pubsub.project}
topic: domain
subscription: demo-organization-creation-notifier
pullerNumGoroutines: 5
pullerMaxOutstandingMessages: 1000
pullerMaxOutstandingBytes: 1000000000
maxMps: 50
workerNum: 1

onDemandSubscribers:
evaluationCountEventDWHPersister:
Expand Down Expand Up @@ -498,6 +511,10 @@ subscriber:
userEventPersister:
flushSize: 10
flushInterval: 5
demoOrganizationCreationNotifier:
notifier:
slack:
webhookURL: ""

onDemandProcessors:
evaluationCountEventDWHPersister:
Expand Down
32 changes: 32 additions & 0 deletions pkg/notification/sender/notifier/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,10 +180,42 @@ func (n *slackNotifier) createAttachment(
return n.createExperimentRunningAttachment(notification.ExperimentRunningNotification)
case sender.Notification_MauCount:
return n.createMAUCountAttachment(notification.MauCountNotification)
case sender.Notification_DemoOrganizationCreation:
return n.createDemoOrganizationCreationAttachment(notification.DemoOrganizationCreationNotification)
}
return nil, ErrUnknownNotification
}

func (n *slackNotifier) createDemoOrganizationCreationAttachment(
notification *senderproto.DemoOrganizationCreationNotification,
) (*slack.Attachment, error) {
url, err := domainevent.URL(
domainproto.Event_ORGANIZATION,
n.webURL,
"",
notification.OrganizationId,
)
if err != nil {
n.logger.Error("Failed to create URL for demo organization",
zap.Error(err),
zap.String("organizationId", notification.OrganizationId),
)
return nil, err
}

attachment := &slack.Attachment{
Color: "#36a64f",
AuthorName: notification.OwnerEmail,
Text: "A new demo organization has been created.\n\n" +
"Organization ID: " + notification.OrganizationId + "\n" +
"Organization Name: " + notification.OrganizationName + "\n" +
"Owner Email: " + notification.OwnerEmail + "\n" +
"URL: " + fmt.Sprintf(linkTemplate, url, notification.OrganizationName),
}

return attachment, nil
}

func (n *slackNotifier) createDomainEventAttachment(
notification *senderproto.DomainEventNotification,
localizer locale.Localizer,
Expand Down
11 changes: 11 additions & 0 deletions pkg/subscriber/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,17 @@ func (s *server) registerPubSubProcessorMap(
processor.UserEventPersisterName,
userEventPersister,
)

demoOrganizationCreationNotifier := processor.NewDemoOrganizationCreationNotifier(
processorsConfigMap[processor.DemoOrganizationCreationNotifierName],
*s.webURL,
logger,
)
processors.RegisterProcessor(
processor.DemoOrganizationCreationNotifierName,
demoOrganizationCreationNotifier,
)

redisCache := cachev3.NewRedisCache(persistentRedisClient)
evaluationCountEventPersister, err := processor.NewEvaluationCountEventPersister(
ctx,
Expand Down
186 changes: 186 additions & 0 deletions pkg/subscriber/processor/demo_organization_creation_notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright 2025 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.

package processor

import (
"context"
"encoding/json"
"fmt"
"time"

"go.uber.org/zap"
"google.golang.org/protobuf/proto"

"github.com/bucketeer-io/bucketeer/pkg/notification/sender/notifier"
"github.com/bucketeer-io/bucketeer/pkg/pubsub/puller"
"github.com/bucketeer-io/bucketeer/pkg/pubsub/puller/codes"
"github.com/bucketeer-io/bucketeer/pkg/subscriber"
domainevent "github.com/bucketeer-io/bucketeer/proto/event/domain"
domaineventproto "github.com/bucketeer-io/bucketeer/proto/event/domain"
notificationproto "github.com/bucketeer-io/bucketeer/proto/notification"
senderproto "github.com/bucketeer-io/bucketeer/proto/notification/sender"
)

type DemoOrganizationCreationNotifierConfig struct {
Notifier NotifierConfig `json:"notifier"`
}

type NotifierConfig struct {
Slack SlackNotifierConfig `json:"slack"`
}

type SlackNotifierConfig struct {
WebHookURL string `json:"webHookURL"`
}

type demoOrganizationCreationNotifier struct {
slackNotifier notifier.Notifier
demoOrganizationCreationNotifierConfig DemoOrganizationCreationNotifierConfig
logger *zap.Logger
}

func NewDemoOrganizationCreationNotifier(
config interface{},
webURL string,
logger *zap.Logger,
) subscriber.PubSubProcessor {
jsonConfigMap, ok := config.(map[string]interface{})
if !ok {
logger.Error("demoOrganizationCreationNotifier: invalid config type, expected map[string]interface{}")
return nil
}
configBytes, err := json.Marshal(jsonConfigMap)
if err != nil {
logger.Error("demoOrganizationCreationNotifier: failed to marshal config", zap.Error(err))
return nil
}
var notifierConfig DemoOrganizationCreationNotifierConfig
if err := json.Unmarshal(configBytes, &notifierConfig); err != nil {
logger.Error("demoOrganizationCreationNotifier: failed to unmarshal config", zap.Error(err))
return nil
}
slackNotifier := notifier.NewSlackNotifier(webURL)

return &demoOrganizationCreationNotifier{
slackNotifier: slackNotifier,
demoOrganizationCreationNotifierConfig: notifierConfig,
logger: logger,
}
}

func (d demoOrganizationCreationNotifier) Process(ctx context.Context, msgChan <-chan *puller.Message) error {
for {
select {
case msg, ok := <-msgChan:
if !ok {
d.logger.Error("demoOrganizationCreationNotifier: message channel closed")
return nil
}
subscriberReceivedCounter.WithLabelValues(subscriberDemoOrganizationEvent).Inc()
d.handleMessage(msg)
case <-ctx.Done():
d.logger.Debug("subscriber context done, stopped processing messages")
return nil
}
}
}

func (d demoOrganizationCreationNotifier) handleMessage(msg *puller.Message) {
if id := msg.Attributes["id"]; id == "" {
msg.Ack()
subscriberHandledCounter.WithLabelValues(subscriberDemoOrganizationEvent, codes.MissingID.String()).Inc()
return
}
domainEvent, err := d.unmarshalMessage(msg)
if err != nil {
d.logger.Error("Failed to unmarshal message",
zap.Error(err),
zap.String("msgID", msg.ID),
zap.String("attributes", fmt.Sprintf("%+v", msg.Attributes)),
)
subscriberHandledCounter.WithLabelValues(subscriberDemoOrganizationEvent, codes.BadMessage.String()).Inc()
msg.Ack()
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if domainEvent.Type != domainevent.Event_DEMO_ORGANIZATION_CREATED {
subscriberHandledCounter.WithLabelValues(subscriberDemoOrganizationEvent, codes.OK.String()).Inc()
msg.Ack()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, is there no need for logs or subscriberHandledCounter.Inc()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think we should ignore events that aren't relevant

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think that unrelated events should be ignored.
In that case, isn't it necessary to collect logs or call subscriberHandledCounter.Inc()?
When I check the code for other errors or irregular cases, I see that logs are collected and subscriberHandledCounter.Inc() is called.
https://github.com/bucketeer-io/bucketeer/blob/notify-demo-org-create/pkg/subscriber/processor/demo_organization_creation_notifier.go#L107-L116

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that error case means that something already went wrong with the message and we don't know if that message is relevant or not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscriberHandledCounter.Inc() should be called whenever the processing fails or not.
It means that the processor tried to handle the messages.
We also have another metric that reports what happened.

E.g.
It could OK, MissingID, etc

subscriberHandledCounter.WithLabelValues(subscriberAuditLog, codes.MissingID.String()).Inc()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, I've added the subscriberHandledCounter.Inc() for this

return
}

var organizationCreatedEvent domaineventproto.OrganizationCreatedEvent
if err := domainEvent.Data.UnmarshalTo(&organizationCreatedEvent); err != nil {
d.logger.Error("Failed to unmarshal OrganizationCreatedEvent",
zap.String("event id", domainEvent.Id),
zap.Error(err),
)
subscriberHandledCounter.WithLabelValues(
subscriberDemoOrganizationEvent,
codes.NonRepeatableError.String(),
).Inc()
msg.Ack()
return
}

recipient := &notificationproto.Recipient{
Type: notificationproto.Recipient_SlackChannel,
Language: notificationproto.Recipient_ENGLISH,
SlackChannelRecipient: &notificationproto.SlackChannelRecipient{
WebhookUrl: d.demoOrganizationCreationNotifierConfig.Notifier.Slack.WebHookURL,
},
}

err = d.slackNotifier.Notify(ctx, &senderproto.Notification{
Type: senderproto.Notification_DemoOrganizationCreation,
DemoOrganizationCreationNotification: &senderproto.DemoOrganizationCreationNotification{
OwnerEmail: organizationCreatedEvent.OwnerEmail,
OrganizationId: organizationCreatedEvent.Id,
OrganizationName: organizationCreatedEvent.Name,
},
}, recipient, recipient.Language)
if err != nil {
d.logger.Error("Failed to send notification",
zap.Error(err),
zap.String("event id", domainEvent.Id),
zap.String("webhookURL", d.demoOrganizationCreationNotifierConfig.Notifier.Slack.WebHookURL),
zap.String("organizationId", organizationCreatedEvent.Id),
)
subscriberHandledCounter.WithLabelValues(
subscriberDemoOrganizationEvent,
codes.NonRepeatableError.String(),
).Inc()
msg.Ack()
return
}

subscriberHandledCounter.WithLabelValues(
subscriberDemoOrganizationEvent,
codes.OK.String(),
).Inc()
msg.Ack()
}

func (d demoOrganizationCreationNotifier) unmarshalMessage(msg *puller.Message) (*domainevent.Event, error) {
event := &domaineventproto.Event{}
err := proto.Unmarshal(msg.Data, event)
if err != nil {
d.logger.Error("Failed to unmarshal message", zap.Error(err), zap.String("msgID", msg.ID))
return nil, err
}
return event, nil
}
23 changes: 12 additions & 11 deletions pkg/subscriber/processor/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@ import (
)

const (
subscriberAuditLog = "AuditLog"
subscriberDomainEvent = "DomainEvent"
subscriberEvaluationCount = "EvaluationCount"
subscriberEvaluationEventDWH = "EvaluationEventDWH"
subscriberEvaluationEventOPS = "EvaluationEventOPS"
subscriberGoalEventDWH = "GoalEventDWH"
subscriberGoalEventOPS = "GoalEventOPS"
subscriberMetricsEvent = "MetricsEvent"
subscriberPushSender = "PushSender"
subscriberSegmentUser = "SegmentUser"
subscriberUserEvent = "UserEvent"
subscriberAuditLog = "AuditLog"
subscriberDomainEvent = "DomainEvent"
subscriberEvaluationCount = "EvaluationCount"
subscriberEvaluationEventDWH = "EvaluationEventDWH"
subscriberEvaluationEventOPS = "EvaluationEventOPS"
subscriberGoalEventDWH = "GoalEventDWH"
subscriberGoalEventOPS = "GoalEventOPS"
subscriberMetricsEvent = "MetricsEvent"
subscriberPushSender = "PushSender"
subscriberSegmentUser = "SegmentUser"
subscriberUserEvent = "UserEvent"
subscriberDemoOrganizationEvent = "DemoOrganizationEvent"
)

const (
Expand Down
1 change: 1 addition & 0 deletions pkg/subscriber/processor/processors.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
PushSenderName = "pushSender"
SegmentUserPersisterName = "segmentUserPersister"
UserEventPersisterName = "userEventPersister"
DemoOrganizationCreationNotifierName = "demoOrganizationCreationNotifier"
)

var (
Expand Down
Loading