Skip to content

Commit a013530

Browse files
authored
feat: implement slack notifier for demo organization creation event (#2041)
1 parent ce84f59 commit a013530

18 files changed

Lines changed: 906 additions & 132 deletions

File tree

api-description/apidocs.swagger.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3008,6 +3008,7 @@ paths:
30083008
- DOMAIN_EVENT_TAG
30093009
- DOMAIN_EVENT_CODEREF
30103010
- DOMAIN_EVENT_TEAM
3011+
- DOMAIN_EVENT_DEMO_ORGANIZATION
30113012
- FEATURE_STALE
30123013
- EXPERIMENT_RUNNING
30133014
- MAU_COUNT
@@ -3519,6 +3520,7 @@ definitions:
35193520
- DOMAIN_EVENT_TAG
35203521
- DOMAIN_EVENT_CODEREF
35213522
- DOMAIN_EVENT_TEAM
3523+
- DOMAIN_EVENT_DEMO_ORGANIZATION
35223524
- FEATURE_STALE
35233525
- EXPERIMENT_RUNNING
35243526
- MAU_COUNT

api-description/web-api.swagger.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7971,6 +7971,7 @@ paths:
79717971
- DOMAIN_EVENT_TAG
79727972
- DOMAIN_EVENT_CODEREF
79737973
- DOMAIN_EVENT_TEAM
7974+
- DOMAIN_EVENT_DEMO_ORGANIZATION
79747975
- FEATURE_STALE
79757976
- EXPERIMENT_RUNNING
79767977
- MAU_COUNT
@@ -8699,6 +8700,7 @@ definitions:
86998700
- DOMAIN_EVENT_TAG
87008701
- DOMAIN_EVENT_CODEREF
87018702
- DOMAIN_EVENT_TEAM
8703+
- DOMAIN_EVENT_DEMO_ORGANIZATION
87028704
- FEATURE_STALE
87038705
- EXPERIMENT_RUNNING
87048706
- MAU_COUNT

manifests/bucketeer/charts/subscriber/values.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ subscribers:
173173
pullerMaxOutstandingBytes: 100000000
174174
maxMps: 1000
175175
workerNum: 5
176+
demoOrganizationCreationNotifier:
177+
pubSubType: google
178+
project:
179+
topic:
180+
subscription:
181+
pullerNumGoroutines: 5
182+
pullerMaxOutstandingMessages: 1000
183+
pullerMaxOutstandingBytes: 1000000000
184+
maxMps: 50
185+
workerNum: 1
176186

177187
onDemandSubscribers:
178188
evaluationCountEventDWHPersister:
@@ -241,6 +251,10 @@ processors:
241251
userEventPersister:
242252
flushSize: 200
243253
flushInterval: 5
254+
demoOrganizationCreationNotifier:
255+
notifier:
256+
slack:
257+
webhookURL: ""
244258

245259
onDemandProcessors:
246260
evaluationCountEventDWHPersister:

manifests/bucketeer/values.dev.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,19 @@ subscriber:
418418
pullerMaxOutstandingBytes: 1000000000
419419
maxMps: 50
420420
workerNum: 1
421+
demoOrganizationCreationNotifier:
422+
pubSubType: ${global.pubsub.type}
423+
redisAddr: ${global.pubsub.redis.addr}
424+
redisPoolSize: ${global.pubsub.redis.poolSize}
425+
redisMinIdle: ${global.pubsub.redis.minIdle}
426+
project: ${global.pubsub.project}
427+
topic: domain
428+
subscription: demo-organization-creation-notifier
429+
pullerNumGoroutines: 5
430+
pullerMaxOutstandingMessages: 1000
431+
pullerMaxOutstandingBytes: 1000000000
432+
maxMps: 50
433+
workerNum: 1
421434

422435
onDemandSubscribers:
423436
evaluationCountEventDWHPersister:
@@ -498,6 +511,10 @@ subscriber:
498511
userEventPersister:
499512
flushSize: 10
500513
flushInterval: 5
514+
demoOrganizationCreationNotifier:
515+
notifier:
516+
slack:
517+
webhookURL: ""
501518

502519
onDemandProcessors:
503520
evaluationCountEventDWHPersister:

pkg/notification/sender/notifier/slack.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,42 @@ func (n *slackNotifier) createAttachment(
180180
return n.createExperimentRunningAttachment(notification.ExperimentRunningNotification)
181181
case sender.Notification_MauCount:
182182
return n.createMAUCountAttachment(notification.MauCountNotification)
183+
case sender.Notification_DemoOrganizationCreation:
184+
return n.createDemoOrganizationCreationAttachment(notification.DemoOrganizationCreationNotification)
183185
}
184186
return nil, ErrUnknownNotification
185187
}
186188

189+
func (n *slackNotifier) createDemoOrganizationCreationAttachment(
190+
notification *senderproto.DemoOrganizationCreationNotification,
191+
) (*slack.Attachment, error) {
192+
url, err := domainevent.URL(
193+
domainproto.Event_ORGANIZATION,
194+
n.webURL,
195+
"",
196+
notification.OrganizationId,
197+
)
198+
if err != nil {
199+
n.logger.Error("Failed to create URL for demo organization",
200+
zap.Error(err),
201+
zap.String("organizationId", notification.OrganizationId),
202+
)
203+
return nil, err
204+
}
205+
206+
attachment := &slack.Attachment{
207+
Color: "#36a64f",
208+
AuthorName: notification.OwnerEmail,
209+
Text: "A new demo organization has been created.\n\n" +
210+
"Organization ID: " + notification.OrganizationId + "\n" +
211+
"Organization Name: " + notification.OrganizationName + "\n" +
212+
"Owner Email: " + notification.OwnerEmail + "\n" +
213+
"URL: " + fmt.Sprintf(linkTemplate, url, notification.OrganizationName),
214+
}
215+
216+
return attachment, nil
217+
}
218+
187219
func (n *slackNotifier) createDomainEventAttachment(
188220
notification *senderproto.DomainEventNotification,
189221
localizer locale.Localizer,

pkg/subscriber/cmd/server/server.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,17 @@ func (s *server) registerPubSubProcessorMap(
532532
processor.UserEventPersisterName,
533533
userEventPersister,
534534
)
535+
536+
demoOrganizationCreationNotifier := processor.NewDemoOrganizationCreationNotifier(
537+
processorsConfigMap[processor.DemoOrganizationCreationNotifierName],
538+
*s.webURL,
539+
logger,
540+
)
541+
processors.RegisterProcessor(
542+
processor.DemoOrganizationCreationNotifierName,
543+
demoOrganizationCreationNotifier,
544+
)
545+
535546
redisCache := cachev3.NewRedisCache(persistentRedisClient)
536547
evaluationCountEventPersister, err := processor.NewEvaluationCountEventPersister(
537548
ctx,
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright 2025 The Bucketeer Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package processor
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
"time"
22+
23+
"go.uber.org/zap"
24+
"google.golang.org/protobuf/proto"
25+
26+
"github.com/bucketeer-io/bucketeer/pkg/notification/sender/notifier"
27+
"github.com/bucketeer-io/bucketeer/pkg/pubsub/puller"
28+
"github.com/bucketeer-io/bucketeer/pkg/pubsub/puller/codes"
29+
"github.com/bucketeer-io/bucketeer/pkg/subscriber"
30+
domainevent "github.com/bucketeer-io/bucketeer/proto/event/domain"
31+
domaineventproto "github.com/bucketeer-io/bucketeer/proto/event/domain"
32+
notificationproto "github.com/bucketeer-io/bucketeer/proto/notification"
33+
senderproto "github.com/bucketeer-io/bucketeer/proto/notification/sender"
34+
)
35+
36+
type DemoOrganizationCreationNotifierConfig struct {
37+
Notifier NotifierConfig `json:"notifier"`
38+
}
39+
40+
type NotifierConfig struct {
41+
Slack SlackNotifierConfig `json:"slack"`
42+
}
43+
44+
type SlackNotifierConfig struct {
45+
WebHookURL string `json:"webHookURL"`
46+
}
47+
48+
type demoOrganizationCreationNotifier struct {
49+
slackNotifier notifier.Notifier
50+
demoOrganizationCreationNotifierConfig DemoOrganizationCreationNotifierConfig
51+
logger *zap.Logger
52+
}
53+
54+
func NewDemoOrganizationCreationNotifier(
55+
config interface{},
56+
webURL string,
57+
logger *zap.Logger,
58+
) subscriber.PubSubProcessor {
59+
jsonConfigMap, ok := config.(map[string]interface{})
60+
if !ok {
61+
logger.Error("demoOrganizationCreationNotifier: invalid config type, expected map[string]interface{}")
62+
return nil
63+
}
64+
configBytes, err := json.Marshal(jsonConfigMap)
65+
if err != nil {
66+
logger.Error("demoOrganizationCreationNotifier: failed to marshal config", zap.Error(err))
67+
return nil
68+
}
69+
var notifierConfig DemoOrganizationCreationNotifierConfig
70+
if err := json.Unmarshal(configBytes, &notifierConfig); err != nil {
71+
logger.Error("demoOrganizationCreationNotifier: failed to unmarshal config", zap.Error(err))
72+
return nil
73+
}
74+
slackNotifier := notifier.NewSlackNotifier(webURL)
75+
76+
return &demoOrganizationCreationNotifier{
77+
slackNotifier: slackNotifier,
78+
demoOrganizationCreationNotifierConfig: notifierConfig,
79+
logger: logger,
80+
}
81+
}
82+
83+
func (d demoOrganizationCreationNotifier) Process(ctx context.Context, msgChan <-chan *puller.Message) error {
84+
for {
85+
select {
86+
case msg, ok := <-msgChan:
87+
if !ok {
88+
d.logger.Error("demoOrganizationCreationNotifier: message channel closed")
89+
return nil
90+
}
91+
subscriberReceivedCounter.WithLabelValues(subscriberDemoOrganizationEvent).Inc()
92+
d.handleMessage(msg)
93+
case <-ctx.Done():
94+
d.logger.Debug("subscriber context done, stopped processing messages")
95+
return nil
96+
}
97+
}
98+
}
99+
100+
func (d demoOrganizationCreationNotifier) handleMessage(msg *puller.Message) {
101+
if id := msg.Attributes["id"]; id == "" {
102+
msg.Ack()
103+
subscriberHandledCounter.WithLabelValues(subscriberDemoOrganizationEvent, codes.MissingID.String()).Inc()
104+
return
105+
}
106+
domainEvent, err := d.unmarshalMessage(msg)
107+
if err != nil {
108+
d.logger.Error("Failed to unmarshal message",
109+
zap.Error(err),
110+
zap.String("msgID", msg.ID),
111+
zap.String("attributes", fmt.Sprintf("%+v", msg.Attributes)),
112+
)
113+
subscriberHandledCounter.WithLabelValues(subscriberDemoOrganizationEvent, codes.BadMessage.String()).Inc()
114+
msg.Ack()
115+
return
116+
}
117+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
118+
defer cancel()
119+
120+
if domainEvent.Type != domainevent.Event_DEMO_ORGANIZATION_CREATED {
121+
subscriberHandledCounter.WithLabelValues(subscriberDemoOrganizationEvent, codes.OK.String()).Inc()
122+
msg.Ack()
123+
return
124+
}
125+
126+
var organizationCreatedEvent domaineventproto.OrganizationCreatedEvent
127+
if err := domainEvent.Data.UnmarshalTo(&organizationCreatedEvent); err != nil {
128+
d.logger.Error("Failed to unmarshal OrganizationCreatedEvent",
129+
zap.String("event id", domainEvent.Id),
130+
zap.Error(err),
131+
)
132+
subscriberHandledCounter.WithLabelValues(
133+
subscriberDemoOrganizationEvent,
134+
codes.NonRepeatableError.String(),
135+
).Inc()
136+
msg.Ack()
137+
return
138+
}
139+
140+
recipient := &notificationproto.Recipient{
141+
Type: notificationproto.Recipient_SlackChannel,
142+
Language: notificationproto.Recipient_ENGLISH,
143+
SlackChannelRecipient: &notificationproto.SlackChannelRecipient{
144+
WebhookUrl: d.demoOrganizationCreationNotifierConfig.Notifier.Slack.WebHookURL,
145+
},
146+
}
147+
148+
err = d.slackNotifier.Notify(ctx, &senderproto.Notification{
149+
Type: senderproto.Notification_DemoOrganizationCreation,
150+
DemoOrganizationCreationNotification: &senderproto.DemoOrganizationCreationNotification{
151+
OwnerEmail: organizationCreatedEvent.OwnerEmail,
152+
OrganizationId: organizationCreatedEvent.Id,
153+
OrganizationName: organizationCreatedEvent.Name,
154+
},
155+
}, recipient, recipient.Language)
156+
if err != nil {
157+
d.logger.Error("Failed to send notification",
158+
zap.Error(err),
159+
zap.String("event id", domainEvent.Id),
160+
zap.String("webhookURL", d.demoOrganizationCreationNotifierConfig.Notifier.Slack.WebHookURL),
161+
zap.String("organizationId", organizationCreatedEvent.Id),
162+
)
163+
subscriberHandledCounter.WithLabelValues(
164+
subscriberDemoOrganizationEvent,
165+
codes.NonRepeatableError.String(),
166+
).Inc()
167+
msg.Ack()
168+
return
169+
}
170+
171+
subscriberHandledCounter.WithLabelValues(
172+
subscriberDemoOrganizationEvent,
173+
codes.OK.String(),
174+
).Inc()
175+
msg.Ack()
176+
}
177+
178+
func (d demoOrganizationCreationNotifier) unmarshalMessage(msg *puller.Message) (*domainevent.Event, error) {
179+
event := &domaineventproto.Event{}
180+
err := proto.Unmarshal(msg.Data, event)
181+
if err != nil {
182+
d.logger.Error("Failed to unmarshal message", zap.Error(err), zap.String("msgID", msg.ID))
183+
return nil, err
184+
}
185+
return event, nil
186+
}

pkg/subscriber/processor/metrics.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ import (
2121
)
2222

2323
const (
24-
subscriberAuditLog = "AuditLog"
25-
subscriberDomainEvent = "DomainEvent"
26-
subscriberEvaluationCount = "EvaluationCount"
27-
subscriberEvaluationEventDWH = "EvaluationEventDWH"
28-
subscriberEvaluationEventOPS = "EvaluationEventOPS"
29-
subscriberGoalEventDWH = "GoalEventDWH"
30-
subscriberGoalEventOPS = "GoalEventOPS"
31-
subscriberMetricsEvent = "MetricsEvent"
32-
subscriberPushSender = "PushSender"
33-
subscriberSegmentUser = "SegmentUser"
34-
subscriberUserEvent = "UserEvent"
24+
subscriberAuditLog = "AuditLog"
25+
subscriberDomainEvent = "DomainEvent"
26+
subscriberEvaluationCount = "EvaluationCount"
27+
subscriberEvaluationEventDWH = "EvaluationEventDWH"
28+
subscriberEvaluationEventOPS = "EvaluationEventOPS"
29+
subscriberGoalEventDWH = "GoalEventDWH"
30+
subscriberGoalEventOPS = "GoalEventOPS"
31+
subscriberMetricsEvent = "MetricsEvent"
32+
subscriberPushSender = "PushSender"
33+
subscriberSegmentUser = "SegmentUser"
34+
subscriberUserEvent = "UserEvent"
35+
subscriberDemoOrganizationEvent = "DemoOrganizationEvent"
3536
)
3637

3738
const (

pkg/subscriber/processor/processors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const (
3333
PushSenderName = "pushSender"
3434
SegmentUserPersisterName = "segmentUserPersister"
3535
UserEventPersisterName = "userEventPersister"
36+
DemoOrganizationCreationNotifierName = "demoOrganizationCreationNotifier"
3637
)
3738

3839
var (

0 commit comments

Comments
 (0)