[MM-57066][MM-57329] Added metrics for all notification stopping points, consolidated categories between metrics and logging (#26799)

* [MM-57066] Add metric counters for notification events

* Some small changes

* Account for Metrics() sometimes being nil

* Fix test (again)

* Fix more tests

* A few changes from testing - added success counter

* Missed a mock

* Lint

* Add feature flag for notification monitoring
This commit is contained in:
Devin Binnie 2024-04-18 10:30:08 -04:00 committed by GitHub
parent 0ce5def8e2
commit 02e23a3275
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 572 additions and 241 deletions

View File

@ -641,13 +641,16 @@ func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
c.App.CountNotificationAck(model.NotificationTypePush)
err := c.App.SendAckToPushProxy(&ack)
if ack.IsIdLoaded {
if err != nil {
c.App.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonPushProxySendError)
c.App.NotificationsLog().Error("Notification ack not sent to push proxy",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonPushProxySendError),
mlog.String("ack_id", ack.Id),
mlog.String("push_type", ack.NotificationType),
mlog.String("post_id", ack.PostId),
@ -656,6 +659,8 @@ func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) {
mlog.Int("received_at", ack.ClientReceivedAt),
mlog.Err(err),
)
} else {
c.App.CountNotificationReason(model.NotificationStatusSuccess, model.NotificationTypePush, model.NotificationReason(""))
}
// Return post data only when PostId is passed.
if ack.PostId != "" && ack.NotificationType == model.PushTypeMessage {
@ -687,6 +692,7 @@ func pushNotificationAck(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
c.App.CountNotificationReason(model.NotificationStatusSuccess, model.NotificationTypePush, model.NotificationReason(""))
ReturnStatusOK(w)
}

View File

@ -513,6 +513,9 @@ type AppIface interface {
ConvertGroupMessageToChannel(c request.CTX, convertedByUserId string, gmConversionRequest *model.GroupMessageConversionRequestBody) (*model.Channel, *model.AppError)
CopyFileInfos(rctx request.CTX, userID string, fileIDs []string) ([]string, *model.AppError)
CopyWranglerPostlist(c request.CTX, wpl *model.WranglerPostList, targetChannel *model.Channel) (*model.Post, *model.AppError)
CountNotification(notificationType model.NotificationType)
CountNotificationAck(notificationType model.NotificationType)
CountNotificationReason(notificationStatus model.NotificationStatus, notificationType model.NotificationType, notificationReason model.NotificationReason)
CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError)
CreateChannelBookmark(c request.CTX, newBookmark *model.ChannelBookmark, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)

View File

@ -24,10 +24,11 @@ func (a *App) NotifySessionsExpired() error {
// Get all mobile sessions that expired within the last hour.
sessions, err := a.ch.srv.Store().Session().GetSessionsExpired(OneHourMillis, true, true)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Cannot get sessions expired",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(err),
)
return model.NewAppError("NotifySessionsExpired", "app.session.analytics_session_count.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
@ -46,10 +47,11 @@ func (a *App) NotifySessionsExpired() error {
errPush := a.sendToPushProxy(tmpMessage, session)
if errPush != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonPushProxySendError)
a.NotificationsLog().Error("Failed to send to push proxy",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonPushProxyError),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonPushProxySendError),
mlog.String("ack_id", tmpMessage.AckId),
mlog.String("push_type", tmpMessage.Type),
mlog.String("user_id", session.UserId),
@ -60,7 +62,7 @@ func (a *App) NotifySessionsExpired() error {
}
a.NotificationsLog().Trace("Notification sent to push proxy",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("ack_id", tmpMessage.AckId),
mlog.String("push_type", tmpMessage.Type),
mlog.String("user_id", session.UserId),

View File

@ -24,17 +24,17 @@ import (
func (a *App) canSendPushNotifications() bool {
if !*a.Config().EmailSettings.SendPushNotifications {
a.NotificationsLog().Debug("Push notifications are disabled - server config",
mlog.String("status", model.StatusBlocked),
mlog.String("reason", model.ReasonServerConfig),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", "push_disabled"),
)
return false
}
pushServer := *a.Config().EmailSettings.PushNotificationServer
if license := a.Srv().License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
a.NotificationsLog().Info("Push notifications are disabled - license missing",
mlog.String("status", model.StatusBlocked),
mlog.String("reason", model.ReasonServerConfig),
a.NotificationsLog().Warn("Push notifications are disabled - license missing",
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", "push_disabled_license"),
)
mlog.Warn("Push notifications have been disabled. Update your license or go to System Console > Environment > Push Notification Server to use a different server")
return false
@ -97,11 +97,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
pResult := <-pchan
if pResult.NErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Error fetching profiles",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(pResult.NErr),
)
return nil, pResult.NErr
@ -110,11 +111,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
cmnResult := <-cmnchan
if cmnResult.NErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Error fetching notify props",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(cmnResult.NErr),
)
return nil, cmnResult.NErr
@ -125,11 +127,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if tchan != nil {
tResult := <-tchan
if tResult.NErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Error fetching thread followers",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(tResult.NErr),
)
return nil, tResult.NErr
@ -143,11 +146,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if gchan != nil {
gResult := <-gchan
if gResult.NErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Error fetching group mentions",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(gResult.NErr),
)
return nil, gResult.NErr
@ -169,11 +173,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
group := groups[groupID]
anyUsersMentionedByGroup, err := a.insertGroupMentions(sender.Id, group, channel, profileMap, mentions)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Failed to populate group mentions",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(err),
)
return nil, err
@ -190,8 +195,8 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
a.NotificationsLog().Warn("Failed to send warning for out of channel mentions",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", "failed_to_send_out_of_channel"),
mlog.Err(err),
)
c.Logger().Error("Failed to send warning for out of channel mentions", mlog.String("user_id", sender.Id), mlog.String("post_id", post.Id), mlog.Err(err))
@ -368,7 +373,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if *a.Config().EmailSettings.SendEmailNotifications {
a.NotificationsLog().Trace("Begin sending email notifications",
mlog.String("type", model.TypeEmail),
mlog.String("type", model.NotificationTypeEmail),
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
)
@ -377,11 +382,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
for _, id := range emailRecipients {
if profileMap[id] == nil {
a.NotificationsLog().Warn("Missing profile",
mlog.String("type", model.TypeEmail),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeEmail, model.NotificationReasonMissingProfile)
a.NotificationsLog().Error("Missing profile",
mlog.String("type", model.NotificationTypeEmail),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonMissingProfile),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonMissingProfile),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
)
@ -390,11 +396,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
//If email verification is required and user email is not verified don't send email.
if *a.Config().EmailSettings.RequireEmailVerification && !profileMap[id].EmailVerified {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypeEmail, model.NotificationReasonEmailNotVerified)
a.NotificationsLog().Debug("Email not verified",
mlog.String("type", model.TypeEmail),
mlog.String("type", model.NotificationTypeEmail),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonUserConfig),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonEmailNotVerified),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
)
@ -408,11 +415,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(err))
}
if err := a.sendNotificationEmail(c, notification, profileMap[id], team, senderProfileImage); err != nil {
a.NotificationsLog().Warn("Error sending email notification",
mlog.String("type", model.TypeEmail),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeEmail, model.NotificationReasonEmailSendError)
a.NotificationsLog().Error("Error sending email notification",
mlog.String("type", model.NotificationTypeEmail),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonEmailSendError),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
mlog.Err(err),
@ -421,10 +429,10 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
} else {
a.NotificationsLog().Debug("Email disallowed by user",
mlog.String("type", model.TypeEmail),
mlog.String("type", model.NotificationTypeEmail),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonUserConfig),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", "email_disallowed_by_user"),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
)
@ -432,7 +440,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
a.NotificationsLog().Trace("Finished sending email notifications",
mlog.String("type", model.TypeEmail),
mlog.String("type", model.NotificationTypeEmail),
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
)
@ -440,11 +448,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
// Check for channel-wide mentions in channels that have too many members for those to work
if int64(len(profileMap)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypeAll, model.NotificationReasonTooManyUsersInChannel)
a.NotificationsLog().Debug("Too many users to notify - will send ephemeral message",
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonServerConfig),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonTooManyUsersInChannel),
)
T := i18n.GetUserTranslations(sender.Locale)
@ -488,18 +497,19 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if a.canSendPushNotifications() {
a.NotificationsLog().Trace("Begin sending push notifications",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
)
for _, id := range mentionedUsersList {
if profileMap[id] == nil {
a.NotificationsLog().Warn("Missing profile",
mlog.String("type", model.TypePush),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonMissingProfile)
a.NotificationsLog().Error("Missing profile",
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonMissingProfile),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonMissingProfile),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
)
@ -508,9 +518,9 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if notificationsForCRT.Push.Contains(id) {
a.NotificationsLog().Trace("Skipped direct push notification - will send as CRT notification",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("sender_id", sender.Id),
)
continue
@ -546,11 +556,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
for _, id := range allActivityPushUserIds {
if profileMap[id] == nil {
a.NotificationsLog().Warn("Missing profile",
mlog.String("type", model.TypePush),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonMissingProfile)
a.NotificationsLog().Error("Missing profile",
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonMissingProfile),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonMissingProfile),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
)
@ -559,9 +570,9 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if notificationsForCRT.Push.Contains(id) {
a.NotificationsLog().Trace("Skipped direct push notification - will send as CRT notification",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("sender_id", sender.Id),
)
continue
@ -589,11 +600,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
for _, id := range notificationsForCRT.Push {
if profileMap[id] == nil {
a.NotificationsLog().Warn("Missing profile",
mlog.String("type", model.TypePush),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonMissingProfile)
a.NotificationsLog().Error("Missing profile",
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonMissingProfile),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonMissingProfile),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", id),
)
@ -615,11 +627,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
model.CommentsNotifyCRT,
)
} else {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypePush, statusReason)
a.NotificationsLog().Debug("Notification not sent - status",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonUserConfig),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", statusReason),
mlog.String("status_reason", statusReason),
mlog.String("sender_id", post.UserId),
mlog.String("receiver_id", id),
@ -629,14 +642,14 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
a.NotificationsLog().Trace("Finished sending push notifications",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
)
}
a.NotificationsLog().Trace("Begin sending websocket notifications",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
)
@ -690,11 +703,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
published, err := a.publishWebsocketEventForPermalinkPost(c, post, message)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Couldn't send websocket notification for permalink post",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("sender_id", sender.Id),
mlog.Err(err),
)
@ -704,11 +718,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
removePermalinkMetadataFromPost(post)
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonParseError)
a.NotificationsLog().Error("JSON parse error",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonParseError),
mlog.String("sender_id", sender.Id),
mlog.Err(err),
)
@ -725,11 +740,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
// A user following a thread but had left the channel won't get a notification
// https://mattermost.atlassian.net/browse/MM-36769
if profileMap[uid] == nil {
a.NotificationsLog().Warn("Missing profile",
mlog.String("type", model.TypeWebsocket),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonMissingProfile)
a.NotificationsLog().Error("Missing profile",
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonMissingProfile),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonMissingProfile),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
)
@ -741,11 +757,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
if threadMembership == nil {
tm, err := a.Srv().Store().Thread().GetMembershipForUser(uid, post.RootId)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Missing thread membership",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
mlog.Err(err),
@ -753,11 +770,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
return nil, errors.Wrapf(err, "Missing thread membership for participant in notifications. user_id=%q thread_id=%q", uid, post.RootId)
}
if tm == nil {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypeWebsocket, model.NotificationReasonMissingThreadMembership)
a.NotificationsLog().Warn("Missing thread membership",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonMissingThreadMembership),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
)
@ -767,11 +785,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.IsPostPriorityEnabled())
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Missing thread",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
mlog.Err(err),
@ -800,11 +819,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
// should set unread mentions, and unread replies to 0
_, err = a.Srv().Store().Thread().MaintainMembership(uid, post.RootId, opts)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Failed to update thread membership",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
mlog.Err(err),
@ -819,11 +839,12 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
sanitizedPost, err := a.SanitizePostMetadataForUser(c, userThread.Post, uid)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonParseError)
a.NotificationsLog().Error("Failed to sanitize metadata",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonParseError),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
mlog.Err(err),
@ -847,7 +868,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
a.NotificationsLog().Trace("Finish sending websocket notifications",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("sender_id", sender.Id),
mlog.String("post_id", post.Id),
)
@ -1709,3 +1730,50 @@ func ShouldAckWebsocketNotification(channelType model.ChannelType, userNotificat
return false
}
func (a *App) CountNotification(notificationType model.NotificationType) {
if a.Metrics() == nil {
return
}
if !a.Config().FeatureFlags.NotificationMonitoring {
return
}
a.Metrics().IncrementNotificationCounter(notificationType)
}
func (a *App) CountNotificationAck(notificationType model.NotificationType) {
if a.Metrics() == nil {
return
}
if !a.Config().FeatureFlags.NotificationMonitoring {
return
}
a.Metrics().IncrementNotificationAckCounter(notificationType)
}
func (a *App) CountNotificationReason(
notificationStatus model.NotificationStatus,
notificationType model.NotificationType,
notificationReason model.NotificationReason,
) {
if a.Metrics() == nil {
return
}
if !a.Config().FeatureFlags.NotificationMonitoring {
return
}
switch notificationStatus {
case model.NotificationStatusSuccess:
a.Metrics().IncrementNotificationSuccessCounter(notificationType)
case model.NotificationStatusError:
a.Metrics().IncrementNotificationErrorCounter(notificationType, notificationReason)
case model.NotificationStatusNotSent:
a.Metrics().IncrementNotificationNotSentCounter(notificationType, notificationReason)
}
}

View File

@ -24,22 +24,12 @@ import (
)
type notificationType string
type notifyPropsReason string
type statusReason string
const (
notificationTypeClear notificationType = "clear"
notificationTypeMessage notificationType = "message"
notificationTypeUpdateBadge notificationType = "update_badge"
notificationTypeDummy notificationType = "dummy"
NotifyPropsReasonChannelMuted notifyPropsReason = "channel_muted"
NotifyPropsReasonSystemMessage notifyPropsReason = "system_message"
NotifyPropsReasonSetToNone notifyPropsReason = "notify_props_set_to_note"
NotifyPropsReasonSetToMention notifyPropsReason = "notify_props_set_to_mention_and_was_not_mentioned"
StatusReasonDNDOrOOO statusReason = "status_is_dnd_or_ooo"
StatusReasonIsActive statusReason = "user_is_active_on_channel"
)
type PushNotificationsHub struct {
@ -107,10 +97,12 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
if rejectionReason != "" {
// Notifications rejected by a plugin should not be considered errors
a.NotificationsLog().Info("Notification rejected by plugin",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", rejectionReason),
// This is likely normal operation so no need for metrics here
a.NotificationsLog().Debug("Notification rejected by plugin",
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonRejectedByPlugin),
mlog.String("rejection_reason", rejectionReason),
mlog.String("user_id", userID),
)
return nil
@ -118,10 +110,11 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
sessions, err := a.getMobileAppSessions(userID)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Failed to send mobile app sessions",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("user_id", userID),
mlog.Err(err),
)
@ -129,10 +122,11 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
}
if msg == nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonParseError)
a.NotificationsLog().Error("Failed to parse push notification",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonParseError),
mlog.String("user_id", userID),
)
return model.NewAppError(
@ -147,10 +141,11 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
for _, session := range sessions {
// Don't send notifications to this session if it's expired or we want to skip it
if session.IsExpired() || (skipSessionId != "" && skipSessionId == session.Id) {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypePush, model.NotificationReasonSessionExpired)
a.NotificationsLog().Debug("Session expired or skipped",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonUserStatus),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonSessionExpired),
mlog.String("user_id", session.UserId),
mlog.String("session_id", session.Id),
)
@ -164,10 +159,11 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
err := a.sendToPushProxy(tmpMessage, session)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonPushProxySendError)
a.NotificationsLog().Error("Failed to send to push proxy",
mlog.String("type", model.TypePush),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonPushProxyError),
mlog.String("type", model.NotificationTypePush),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", model.NotificationReasonPushProxySendError),
mlog.String("ack_id", tmpMessage.AckId),
mlog.String("push_type", tmpMessage.Type),
mlog.String("user_id", session.UserId),
@ -178,7 +174,7 @@ func (a *App) sendPushNotificationToAllSessions(msg *model.PushNotification, use
}
a.NotificationsLog().Trace("Notification sent to push proxy",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("ack_id", tmpMessage.AckId),
mlog.String("push_type", tmpMessage.Type),
mlog.String("user_id", session.UserId),
@ -477,7 +473,7 @@ func (a *App) sendToPushProxy(msg *model.PushNotification, session *model.Sessio
msg.ServerId = a.TelemetryId()
a.NotificationsLog().Trace("Notification will be sent",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("ack_id", msg.AckId),
mlog.String("push_type", msg.Type),
mlog.String("user_id", session.UserId),
@ -507,7 +503,7 @@ func (a *App) SendAckToPushProxy(ack *model.PushNotificationAck) error {
}
a.NotificationsLog().Trace("Notification successfully received",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("ack_id", ack.Id),
mlog.String("push_type", ack.NotificationType),
mlog.String("post_id", ack.PostId),
@ -553,12 +549,12 @@ func (a *App) getMobileAppSessions(userID string) ([]*model.Session, *model.AppE
func (a *App) ShouldSendPushNotification(user *model.User, channelNotifyProps model.StringMap, wasMentioned bool, status *model.Status, post *model.Post, isGM bool) bool {
if notifyPropsAllowedReason := DoesNotifyPropsAllowPushNotification(user, channelNotifyProps, post, wasMentioned, isGM); notifyPropsAllowedReason != "" {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypePush, notifyPropsAllowedReason)
a.NotificationsLog().Debug("Notification not sent - notify props",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonUserConfig),
mlog.String("notify_props_reason", notifyPropsAllowedReason),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", notifyPropsAllowedReason),
mlog.String("sender_id", post.UserId),
mlog.String("receiver_id", user.Id),
)
@ -566,12 +562,12 @@ func (a *App) ShouldSendPushNotification(user *model.User, channelNotifyProps mo
}
if statusAllowedReason := DoesStatusAllowPushNotification(user.NotifyProps, status, post.ChannelId); statusAllowedReason != "" {
a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypePush, statusAllowedReason)
a.NotificationsLog().Debug("Notification not sent - status",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusNotSent),
mlog.String("reason", model.ReasonUserConfig),
mlog.String("status_reason", statusAllowedReason),
mlog.String("status", model.NotificationStatusNotSent),
mlog.String("reason", statusAllowedReason),
mlog.String("sender_id", post.UserId),
mlog.String("receiver_id", user.Id),
mlog.String("receiver_status", status.Status),
@ -582,7 +578,7 @@ func (a *App) ShouldSendPushNotification(user *model.User, channelNotifyProps mo
return true
}
func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned, isGM bool) notifyPropsReason {
func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps model.StringMap, post *model.Post, wasMentioned, isGM bool) model.NotificationReason {
userNotifyProps := user.NotifyProps
userNotify := userNotifyProps[model.PushNotifyProp]
channelNotify, ok := channelNotifyProps[model.PushNotifyProp]
@ -600,19 +596,19 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m
// If the channel is muted do not send push notifications
if channelNotifyProps[model.MarkUnreadNotifyProp] == model.ChannelMarkUnreadMention {
return NotifyPropsReasonChannelMuted
return model.NotificationReasonChannelMuted
}
if post.IsSystemMessage() {
return NotifyPropsReasonSystemMessage
return model.NotificationReasonSystemMessage
}
if notify == model.ChannelNotifyNone {
return NotifyPropsReasonSetToNone
return model.NotificationReasonLevelSetToNone
}
if notify == model.ChannelNotifyMention && !wasMentioned {
return NotifyPropsReasonSetToMention
return model.NotificationReasonNotMentioned
}
if (notify == model.ChannelNotifyAll) &&
@ -623,10 +619,10 @@ func DoesNotifyPropsAllowPushNotification(user *model.User, channelNotifyProps m
return ""
}
func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelID string) statusReason {
func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *model.Status, channelID string) model.NotificationReason {
// If User status is DND or OOO return false right away
if status.Status == model.StatusDnd || status.Status == model.StatusOutOfOffice {
return StatusReasonDNDOrOOO
return model.NotificationReasonUserStatus
}
pushStatus, ok := userNotifyProps[model.PushStatusNotifyProp]
@ -642,7 +638,7 @@ func DoesStatusAllowPushNotification(userNotifyProps model.StringMap, status *mo
return ""
}
return StatusReasonIsActive
return model.NotificationReasonUserIsActive
}
func (a *App) BuildPushNotificationMessage(c request.CTX, contentsConfig string, post *model.Post, user *model.User, channel *model.Channel, channelName string, senderName string,
@ -689,11 +685,12 @@ func (a *App) SendTestPushNotification(deviceID string) string {
pushResponse, err := a.rawSendToPushProxy(msg)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonPushProxySendError)
a.NotificationsLog().Error("Failed to send test notification to push proxy",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("push_type", msg.Type),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonPushProxyError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonPushProxySendError),
mlog.String("device_id", msg.DeviceId),
mlog.Err(err),
)
@ -704,11 +701,12 @@ func (a *App) SendTestPushNotification(deviceID string) string {
case model.PushStatusRemove:
return "false"
case model.PushStatusFail:
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypePush, model.NotificationReasonPushProxyError)
a.NotificationsLog().Error("Push proxy failed to send test notification",
mlog.String("type", model.TypePush),
mlog.String("type", model.NotificationTypePush),
mlog.String("push_type", msg.Type),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonPushProxyError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonPushProxyError),
mlog.String("device_id", msg.DeviceId),
mlog.Err(errors.New(pushResponse[model.PushStatusErrorMsg])),
)

View File

@ -34,7 +34,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost bool
wasMentioned bool
isMuted bool
expected notifyPropsReason
expected model.NotificationReason
isGM bool
}{
{
@ -44,7 +44,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: true,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSystemMessage,
expected: model.NotificationReasonSystemMessage,
isGM: false,
},
{
@ -54,7 +54,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: true,
wasMentioned: true,
isMuted: false,
expected: NotifyPropsReasonSystemMessage,
expected: model.NotificationReasonSystemMessage,
isGM: false,
},
{
@ -84,7 +84,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToMention,
expected: model.NotificationReasonNotMentioned,
isGM: false,
},
{
@ -104,7 +104,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -114,7 +114,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: true,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -144,7 +144,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToMention,
expected: model.NotificationReasonNotMentioned,
isGM: false,
},
{
@ -164,7 +164,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -174,7 +174,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: true,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -244,7 +244,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToMention,
expected: model.NotificationReasonNotMentioned,
isGM: false,
},
{
@ -264,7 +264,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToMention,
expected: model.NotificationReasonNotMentioned,
isGM: false,
},
{
@ -284,7 +284,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToMention,
expected: model.NotificationReasonNotMentioned,
isGM: false,
},
{
@ -304,7 +304,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -314,7 +314,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: true,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -324,7 +324,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -334,7 +334,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: true,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -344,7 +344,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -354,7 +354,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: true,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: false,
},
{
@ -364,7 +364,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: true,
expected: NotifyPropsReasonChannelMuted,
expected: model.NotificationReasonChannelMuted,
isGM: false,
},
{
@ -374,7 +374,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToNone,
expected: model.NotificationReasonLevelSetToNone,
isGM: true,
},
{
@ -404,7 +404,7 @@ func TestDoesNotifyPropsAllowPushNotification(t *testing.T) {
withSystemPost: false,
wasMentioned: false,
isMuted: false,
expected: NotifyPropsReasonSetToMention,
expected: model.NotificationReasonNotMentioned,
isGM: true,
},
}
@ -447,7 +447,7 @@ func TestDoesStatusAllowPushNotification(t *testing.T) {
userNotifySetting string
status *model.Status
channelID string
expected statusReason
expected model.NotificationReason
}{
{
name: "WHEN props is ONLINE and user is offline with channel",
@ -489,21 +489,21 @@ func TestDoesStatusAllowPushNotification(t *testing.T) {
userNotifySetting: model.StatusOnline,
status: online,
channelID: "",
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is ONLINE and user is dnd with channel",
userNotifySetting: model.StatusOnline,
status: dnd,
channelID: channelID,
expected: StatusReasonDNDOrOOO,
expected: model.NotificationReasonUserStatus,
},
{
name: "WHEN props is ONLINE and user is dnd without channel",
userNotifySetting: model.StatusOnline,
status: dnd,
channelID: "",
expected: StatusReasonDNDOrOOO,
expected: model.NotificationReasonUserStatus,
},
{
name: "WHEN props is AWAY and user is offline with channel",
@ -538,28 +538,28 @@ func TestDoesStatusAllowPushNotification(t *testing.T) {
userNotifySetting: model.StatusAway,
status: online,
channelID: channelID,
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is AWAY and user is online without channel",
userNotifySetting: model.StatusAway,
status: online,
channelID: "",
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is AWAY and user is dnd with channel",
userNotifySetting: model.StatusAway,
status: dnd,
channelID: channelID,
expected: StatusReasonDNDOrOOO,
expected: model.NotificationReasonUserStatus,
},
{
name: "WHEN props is AWAY and user is dnd without channel",
userNotifySetting: model.StatusAway,
status: dnd,
channelID: "",
expected: StatusReasonDNDOrOOO,
expected: model.NotificationReasonUserStatus,
},
{
name: "WHEN props is OFFLINE and user is offline with channel",
@ -580,42 +580,42 @@ func TestDoesStatusAllowPushNotification(t *testing.T) {
userNotifySetting: model.StatusOffline,
status: away,
channelID: channelID,
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is OFFLINE and user is away without channel",
userNotifySetting: model.StatusOffline,
status: away,
channelID: "",
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is OFFLINE and user is online with channel",
userNotifySetting: model.StatusOffline,
status: online,
channelID: channelID,
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is OFFLINE and user is online without channel",
userNotifySetting: model.StatusOffline,
status: online,
channelID: "",
expected: StatusReasonIsActive,
expected: model.NotificationReasonUserIsActive,
},
{
name: "WHEN props is OFFLINE and user is dnd with channel",
userNotifySetting: model.StatusOffline,
status: dnd,
channelID: channelID,
expected: StatusReasonDNDOrOOO,
expected: model.NotificationReasonUserStatus,
},
{
name: "WHEN props is OFFLINE and user is dnd without channel",
userNotifySetting: model.StatusOffline,
status: dnd,
channelID: "",
expected: StatusReasonDNDOrOOO,
expected: model.NotificationReasonUserStatus,
},
}

View File

@ -1967,6 +1967,51 @@ func (a *OpenTracingAppLayer) CopyWranglerPostlist(c request.CTX, wpl *model.Wra
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) CountNotification(notificationType model.NotificationType) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CountNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.CountNotification(notificationType)
}
func (a *OpenTracingAppLayer) CountNotificationAck(notificationType model.NotificationType) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CountNotificationAck")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.CountNotificationAck(notificationType)
}
func (a *OpenTracingAppLayer) CountNotificationReason(notificationStatus model.NotificationStatus, notificationType model.NotificationType, notificationReason model.NotificationReason) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CountNotificationReason")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
a.app.CountNotificationReason(notificationStatus, notificationType, notificationReason)
}
func (a *OpenTracingAppLayer) CreateBot(rctx request.CTX, bot *model.Bot) (*model.Bot, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateBot")

View File

@ -357,11 +357,12 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
if rpost.RootId != "" {
if appErr := a.ResolvePersistentNotification(c, parentPostList.Posts[post.RootId], rpost.UserId); appErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonResolvePersistentNotificationError)
a.NotificationsLog().Error("Error resolving persistent notification",
mlog.String("sender_id", rpost.UserId),
mlog.String("post_id", post.RootId),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonResolvePersistentNotificationError),
mlog.Err(appErr),
)
return nil, appErr
@ -487,10 +488,11 @@ func (a *App) handlePostEvents(c request.CTX, post *model.Post, user *model.User
if channel.TeamId != "" {
t, err := a.Srv().Store().Team().Get(channel.TeamId)
if err != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Missing team",
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.Err(err),
)
return err
@ -781,11 +783,12 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
}
if !model.IsValidId(previewedPostID) {
a.NotificationsLog().Warn("Invalid post prop id for permalink post",
mlog.String("type", model.TypeWebsocket),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonParseError)
a.NotificationsLog().Error("Invalid post prop id for permalink post",
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonParseError),
mlog.String("prop_value", previewedPostID),
)
c.Logger().Warn("invalid post prop value", mlog.String("prop_key", model.PostPropsPreviewedPost), mlog.String("prop_value", previewedPostID))
@ -795,12 +798,14 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
previewedPost, err := a.GetSinglePost(previewedPostID, false)
if err != nil {
if err.StatusCode == http.StatusNotFound {
a.NotificationsLog().Warn("permalink post not found",
mlog.String("type", model.TypeWebsocket),
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("permalink post not found",
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonServerError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("referenced_post_id", previewedPostID),
mlog.Err(err),
)
c.Logger().Warn("permalinked post not found", mlog.String("referenced_post_id", previewedPostID))
return false, nil
@ -810,12 +815,29 @@ func (a *App) publishWebsocketEventForPermalinkPost(c request.CTX, post *model.P
userIDs, nErr := a.Srv().Store().Channel().GetAllChannelMemberIdsByChannelId(post.ChannelId)
if nErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Cannot get channel members",
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("referenced_post_id", previewedPostID),
mlog.Err(nErr),
)
return false, model.NewAppError("publishWebsocketEventForPermalinkPost", "app.channel.get_members.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
permalinkPreviewedChannel, err := a.GetChannel(c, previewedPost.ChannelId)
if err != nil {
if err.StatusCode == http.StatusNotFound {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonFetchError)
a.NotificationsLog().Error("Cannot get channel",
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("post_id", post.Id),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonFetchError),
mlog.String("referenced_post_id", previewedPostID),
)
c.Logger().Warn("channel containing permalinked post not found", mlog.String("referenced_channel_id", previewedPost.ChannelId))
return false, nil
}

View File

@ -42,11 +42,12 @@ func (a *App) SaveAcknowledgementForPost(c request.CTX, postID, userID string) (
}
if appErr := a.ResolvePersistentNotification(c, post, userID); appErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonResolvePersistentNotificationError)
a.NotificationsLog().Error("Error resolving persistent notification",
mlog.String("sender_id", userID),
mlog.String("post_id", post.RootId),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonResolvePersistentNotificationError),
mlog.Err(appErr),
)
return nil, appErr

View File

@ -67,11 +67,12 @@ func (a *App) SaveReactionForPost(c request.CTX, reaction *model.Reaction) (*mod
if post.RootId == "" {
if appErr := a.ResolvePersistentNotification(c, post, reaction.UserId); appErr != nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeAll, model.NotificationReasonResolvePersistentNotificationError)
a.NotificationsLog().Error("Error resolving persistent notification",
mlog.String("sender_id", reaction.UserId),
mlog.String("post_id", post.RootId),
mlog.String("status", model.StatusServerError),
mlog.String("reason", model.ReasonFetchError),
mlog.String("status", model.NotificationStatusError),
mlog.String("reason", model.NotificationReasonResolvePersistentNotificationError),
mlog.Err(appErr),
)
return nil, appErr

View File

@ -86,6 +86,7 @@ func (h *postedAckBroadcastHook) Process(msg *platform.HookedWebSocketEvent, web
// This works since we currently do have an order for broadcast hooks, but this probably should be reworked going forward
if msg.Get("followers") != nil || msg.Get("mentions") != nil {
msg.Add("should_ack", true)
incrementWebsocketCounter(webConn)
return nil
}
@ -107,6 +108,7 @@ func (h *postedAckBroadcastHook) Process(msg *platform.HookedWebSocketEvent, web
// Always ACK direct channels
if channelType == model.ChannelTypeDirect {
msg.Add("should_ack", true)
incrementWebsocketCounter(webConn)
return nil
}
@ -117,11 +119,24 @@ func (h *postedAckBroadcastHook) Process(msg *platform.HookedWebSocketEvent, web
if len(users) > 0 && slices.Contains(users, webConn.UserId) {
msg.Add("should_ack", true)
incrementWebsocketCounter(webConn)
}
return nil
}
func incrementWebsocketCounter(wc *platform.WebConn) {
if wc.Platform.Metrics() == nil {
return
}
if !wc.Platform.Config().FeatureFlags.NotificationMonitoring {
return
}
wc.Platform.Metrics().IncrementNotificationCounter(model.NotificationTypeWebsocket)
}
// getTypedArg returns a correctly typed hook argument with the given key, reinterpreting the type using JSON encoding
// if necessary. This is needed because broadcast hook args are JSON encoded in a multi-server environment, and any
// type information is lost because those types aren't known at decode time.

View File

@ -87,7 +87,8 @@ func TestPostedAckHook_Process(t *testing.T) {
hook := &postedAckBroadcastHook{}
userID := model.NewId()
webConn := &platform.WebConn{
UserId: userID,
UserId: userID,
Platform: &platform.PlatformService{},
}
t.Run("should ack if user is in the list of users to notify", func(t *testing.T) {

View File

@ -26,14 +26,38 @@ func ping(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
func (api *API) websocketNotificationAck(req *model.WebSocketRequest) (map[string]any, *model.AppError) {
// Log the ACKs if necessary
api.App.NotificationsLog().Debug("Websocket notification acknowledgment",
mlog.String("type", model.TypeWebsocket),
mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("user_id", req.Session.UserId),
mlog.Any("user_agent", req.Data["user_agent"]),
mlog.Any("post_id", req.Data["post_id"]),
mlog.Any("result", req.Data["result"]),
mlog.Any("status", req.Data["status"]),
mlog.Any("reason", req.Data["reason"]),
mlog.Any("data", req.Data["data"]),
)
// Count metrics for websocket acks
api.App.CountNotificationAck(model.NotificationTypeWebsocket)
status := req.Data["status"]
reason := req.Data["reason"]
if status == nil {
return nil, nil
}
notificationStatus := model.NotificationStatus(status.(string))
if reason == nil && notificationStatus != model.NotificationStatusSuccess {
return nil, nil
}
var notificationReason model.NotificationReason
if reason != nil {
notificationReason = model.NotificationReason(reason.(string))
}
api.App.CountNotificationReason(
notificationStatus,
model.NotificationTypeWebsocket,
notificationReason,
)
return nil, nil
}

View File

@ -95,4 +95,10 @@ type MetricsInterface interface {
SetReplicaLagAbsolute(node string, value float64)
SetReplicaLagTime(node string, value float64)
IncrementNotificationCounter(notificationType model.NotificationType)
IncrementNotificationAckCounter(notificationType model.NotificationType)
IncrementNotificationSuccessCounter(notificationType model.NotificationType)
IncrementNotificationErrorCounter(notificationType model.NotificationType, errorReason model.NotificationReason)
IncrementNotificationNotSentCounter(notificationType model.NotificationType, notSentReason model.NotificationReason)
}

View File

@ -163,6 +163,31 @@ func (_m *MetricsInterface) IncrementMemCacheMissCounterSession() {
_m.Called()
}
// IncrementNotificationAckCounter provides a mock function with given fields: notificationType
func (_m *MetricsInterface) IncrementNotificationAckCounter(notificationType model.NotificationType) {
_m.Called(notificationType)
}
// IncrementNotificationCounter provides a mock function with given fields: notificationType
func (_m *MetricsInterface) IncrementNotificationCounter(notificationType model.NotificationType) {
_m.Called(notificationType)
}
// IncrementNotificationErrorCounter provides a mock function with given fields: notificationType, errorReason
func (_m *MetricsInterface) IncrementNotificationErrorCounter(notificationType model.NotificationType, errorReason model.NotificationReason) {
_m.Called(notificationType, errorReason)
}
// IncrementNotificationNotSentCounter provides a mock function with given fields: notificationType, notSentReason
func (_m *MetricsInterface) IncrementNotificationNotSentCounter(notificationType model.NotificationType, notSentReason model.NotificationReason) {
_m.Called(notificationType, notSentReason)
}
// IncrementNotificationSuccessCounter provides a mock function with given fields: notificationType
func (_m *MetricsInterface) IncrementNotificationSuccessCounter(notificationType model.NotificationType) {
_m.Called(notificationType)
}
// IncrementPostBroadcast provides a mock function with given fields:
func (_m *MetricsInterface) IncrementPostBroadcast() {
_m.Called()

View File

@ -41,6 +41,7 @@ const (
MetricsSubsystemSharedChannels = "shared_channels"
MetricsSubsystemSystem = "system"
MetricsSubsystemJobs = "jobs"
MetricsSubsystemNotifications = "notifications"
MetricsCloudInstallationLabel = "installationId"
MetricsCloudDatabaseClusterLabel = "databaseClusterName"
MetricsCloudInstallationGroupLabel = "installationGroupId"
@ -201,6 +202,12 @@ type MetricsInterfaceImpl struct {
ServerStartTime prometheus.Gauge
JobsActive *prometheus.GaugeVec
NotificationTotalCounters *prometheus.CounterVec
NotificationAckCounters *prometheus.CounterVec
NotificationSuccessCounters *prometheus.CounterVec
NotificationErrorCounters *prometheus.CounterVec
NotificationNotSentCounters *prometheus.CounterVec
}
func init() {
@ -1041,6 +1048,67 @@ func New(ps *platform.PlatformService, driver, dataSource string) *MetricsInterf
[]string{"type"},
)
m.Registry.MustRegister(m.JobsActive)
m.NotificationTotalCounters = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemNotifications,
Name: "total",
Help: "Total number of notification events",
ConstLabels: additionalLabels,
},
[]string{"type"},
)
m.Registry.MustRegister(m.NotificationTotalCounters)
m.NotificationAckCounters = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemNotifications,
Name: "total_ack",
Help: "Total number of notification events acknowledged",
ConstLabels: additionalLabels,
},
[]string{"type"},
)
m.Registry.MustRegister(m.NotificationAckCounters)
m.NotificationSuccessCounters = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemNotifications,
Name: "success",
Help: "Total number of successfully sent notifications",
ConstLabels: additionalLabels,
},
[]string{"type"},
)
m.Registry.MustRegister(m.NotificationSuccessCounters)
m.NotificationErrorCounters = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemNotifications,
Name: "error",
Help: "Total number of errors that stop the notification flow",
ConstLabels: additionalLabels,
},
[]string{"type", "reason"},
)
m.Registry.MustRegister(m.NotificationErrorCounters)
m.NotificationNotSentCounters = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: MetricsNamespace,
Subsystem: MetricsSubsystemNotifications,
Name: "not_sent",
Help: "Total number of notifications the system deliberately did not send",
ConstLabels: additionalLabels,
},
[]string{"type", "reason"},
)
m.Registry.MustRegister(m.NotificationNotSentCounters)
return m
}
@ -1467,6 +1535,26 @@ func (mi *MetricsInterfaceImpl) SetReplicaLagTime(node string, value float64) {
mi.DbReplicaLagGaugeTime.With(prometheus.Labels{"node": node}).Set(value)
}
func (mi *MetricsInterfaceImpl) IncrementNotificationCounter(notificationType model.NotificationType) {
mi.NotificationTotalCounters.With(prometheus.Labels{"type": string(notificationType)}).Inc()
}
func (mi *MetricsInterfaceImpl) IncrementNotificationAckCounter(notificationType model.NotificationType) {
mi.NotificationAckCounters.With(prometheus.Labels{"type": string(notificationType)}).Inc()
}
func (mi *MetricsInterfaceImpl) IncrementNotificationSuccessCounter(notificationType model.NotificationType) {
mi.NotificationSuccessCounters.With(prometheus.Labels{"type": string(notificationType)}).Inc()
}
func (mi *MetricsInterfaceImpl) IncrementNotificationErrorCounter(notificationType model.NotificationType, errorReason model.NotificationReason) {
mi.NotificationErrorCounters.With(prometheus.Labels{"type": string(notificationType), "reason": string(errorReason)}).Inc()
}
func (mi *MetricsInterfaceImpl) IncrementNotificationNotSentCounter(notificationType model.NotificationType, notSentReason model.NotificationReason) {
mi.NotificationNotSentCounters.With(prometheus.Labels{"type": string(notificationType), "reason": string(notSentReason)}).Inc()
}
func (mi *MetricsInterfaceImpl) IncrementHTTPWebSockets(originClient string) {
mi.HTTPWebsocketsGauge.With(prometheus.Labels{"origin_client": originClient}).Inc()
}

View File

@ -55,6 +55,8 @@ type FeatureFlags struct {
ChannelBookmarks bool
WebSocketEventScope bool
NotificationMonitoring bool
}
func (f *FeatureFlags) SetDefaults() {
@ -77,6 +79,7 @@ func (f *FeatureFlags) SetDefaults() {
f.CloudDedicatedExportUI = false
f.ChannelBookmarks = false
f.WebSocketEventScope = false
f.NotificationMonitoring = true
}
// ToMap returns the feature flags as a map[string]string

View File

@ -3,20 +3,36 @@
package model
type NotificationStatus string
type NotificationType string
type NotificationReason string
const (
StatusBlocked = "blocked"
StatusServerError = "server_error"
StatusNotSent = "not_sent"
NotificationStatusSuccess NotificationStatus = "success"
NotificationStatusError NotificationStatus = "error"
NotificationStatusNotSent NotificationStatus = "not_sent"
TypeEmail = "email"
TypeWebsocket = "websocket"
TypePush = "push"
NotificationTypeAll NotificationType = "all"
NotificationTypeEmail NotificationType = "email"
NotificationTypeWebsocket NotificationType = "websocket"
NotificationTypePush NotificationType = "push"
ReasonServerConfig = "server_config"
ReasonUserConfig = "user_config"
ReasonUserStatus = "user_status"
ReasonFetchError = "error_fetching"
ReasonServerError = "server_error"
ReasonMissingProfile = "missing_profile"
ReasonPushProxyError = "push_proxy_error"
NotificationReasonFetchError NotificationReason = "fetch_error"
NotificationReasonParseError NotificationReason = "json_parse_error"
NotificationReasonPushProxyError NotificationReason = "push_proxy_error"
NotificationReasonPushProxySendError NotificationReason = "push_proxy_send_error"
NotificationReasonRejectedByPlugin NotificationReason = "rejected_by_plugin"
NotificationReasonSessionExpired NotificationReason = "session_expired"
NotificationReasonChannelMuted NotificationReason = "channel_muted"
NotificationReasonSystemMessage NotificationReason = "system_message"
NotificationReasonLevelSetToNone NotificationReason = "notify_level_none"
NotificationReasonNotMentioned NotificationReason = "not_mentioned"
NotificationReasonUserStatus NotificationReason = "user_status"
NotificationReasonUserIsActive NotificationReason = "user_is_active"
NotificationReasonMissingProfile NotificationReason = "missing_profile"
NotificationReasonEmailNotVerified NotificationReason = "email_not_verified"
NotificationReasonEmailSendError NotificationReason = "email_send_error"
NotificationReasonTooManyUsersInChannel NotificationReason = "too_many_users_in_channel"
NotificationReasonResolvePersistentNotificationError NotificationReason = "resolve_persistent_notification_error"
NotificationReasonMissingThreadMembership NotificationReason = "missing_thread_membership"
)

View File

@ -13,7 +13,7 @@
"@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39",
"@mattermost/desktop-api": "5.8.0-4",
"@mattermost/desktop-api": "5.8.0-5",
"@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16",

View File

@ -80,11 +80,11 @@ export function completePostReceive(post: Post, websocketMessageProps: NewPostMe
dispatch(setThreadRead(post));
}
const {result, reason, data} = await dispatch(sendDesktopNotification(post, websocketMessageProps));
const {status, reason, data} = await dispatch(sendDesktopNotification(post, websocketMessageProps));
// Only ACK for posts that require it
if (websocketMessageProps.should_ack) {
WebSocketClient.acknowledgePostedNotification(post.id, result, reason, data);
WebSocketClient.acknowledgePostedNotification(post.id, status, reason, data);
}
return {data: true};

View File

@ -57,11 +57,11 @@ export function sendDesktopNotification(post, msgProps) {
const currentUserId = getCurrentUserId(state);
if ((currentUserId === post.user_id && post.props.from_webhook !== 'true')) {
return {result: 'not_sent', reason: 'own_post'};
return {status: 'not_sent', reason: 'own_post'};
}
if (isSystemMessage(post) && !isUserAddedInChannel(post, currentUserId)) {
return {result: 'not_sent', reason: 'system_message'};
return {status: 'not_sent', reason: 'system_message'};
}
let userFromPost = getUser(state, post.user_id);
@ -92,15 +92,15 @@ export function sendDesktopNotification(post, msgProps) {
const isCrtReply = isCollapsedThreadsEnabled(state) && post.root_id !== '';
if (!member) {
return {result: 'not_sent', reason: 'no_member'};
return {status: 'error', reason: 'no_member'};
}
if (isChannelMuted(member)) {
return {result: 'not_sent', reason: 'channel_muted'};
return {status: 'not_sent', reason: 'channel_muted'};
}
if (userStatus === UserStatuses.DND || userStatus === UserStatuses.OUT_OF_OFFICE) {
return {result: 'not_sent', reason: 'user_status', data: userStatus};
return {status: 'not_sent', reason: 'user_status', data: userStatus};
}
const channelNotifyProp = member?.notify_props?.desktop || NotificationLevels.DEFAULT;
@ -115,7 +115,7 @@ export function sendDesktopNotification(post, msgProps) {
}
if (notifyLevel === NotificationLevels.NONE) {
return {result: 'not_sent', reason: 'notify_level', data: notifyLevel};
return {status: 'not_sent', reason: 'notify_level_none'};
} else if (channel?.type === 'G' && notifyLevel === NotificationLevels.MENTION) {
// Compose the whole text in the message, including interactive messages.
let text = post.message;
@ -189,13 +189,13 @@ export function sendDesktopNotification(post, msgProps) {
}
if (!isExplicitlyMentioned) {
return {result: 'not_sent', reason: 'not_explicitly_mentioned', data: mentionableText};
return {status: 'not_sent', reason: 'not_explicitly_mentioned', data: mentionableText};
}
} else if (notifyLevel === NotificationLevels.MENTION && mentions.indexOf(user.id) === -1 && msgProps.channel_type !== Constants.DM_CHANNEL) {
return {result: 'not_sent', reason: 'not_mentioned'};
return {status: 'not_sent', reason: 'not_mentioned'};
} else if (isCrtReply && notifyLevel === NotificationLevels.ALL && followers.indexOf(currentUserId) === -1) {
// if user is not following the thread don't notify
return {result: 'not_sent', reason: 'not_following_thread'};
return {status: 'not_sent', reason: 'not_following_thread'};
}
const config = getConfig(state);
@ -273,18 +273,18 @@ export function sendDesktopNotification(post, msgProps) {
const channelId = channel ? channel.id : null;
let notify = false;
let notifyResult = {result: 'not_sent', reason: 'unknown'};
let notifyResult = {status: 'not_sent', reason: 'unknown'};
if (state.views.browser.focused) {
notifyResult = {result: 'not_sent', reason: 'window_is_focused'};
notifyResult = {status: 'not_sent', reason: 'window_is_focused'};
if (isCrtReply) {
notify = !isThreadOpen(state, post.root_id);
if (!notify) {
notifyResult = {result: 'not_sent', reason: 'thread_is_open', data: post.root_id};
notifyResult = {status: 'not_sent', reason: 'thread_is_open', data: post.root_id};
}
} else {
notify = activeChannel && activeChannel.id !== channelId;
if (!notify) {
notifyResult = {result: 'not_sent', reason: 'channel_is_open', data: activeChannel?.id};
notifyResult = {status: 'not_sent', reason: 'channel_is_open', data: activeChannel?.id};
}
}
} else {
@ -305,7 +305,7 @@ export function sendDesktopNotification(post, msgProps) {
const hookResult = await dispatch(runDesktopNotificationHooks(post, msgProps, channel, teamId, args));
if (hookResult.error) {
dispatch(logError(hookResult.error));
return {result: 'error', reason: 'desktop_notification_hook', data: String(hookResult.error)};
return {status: 'error', reason: 'desktop_notification_hook', data: String(hookResult.error)};
}
let silent = false;
@ -323,7 +323,7 @@ export function sendDesktopNotification(post, msgProps) {
}
if (args.notify && !notify) {
notifyResult = {result: 'not_sent', reason: 'desktop_notification_hook', data: String(hookResult)};
notifyResult = {status: 'not_sent', reason: 'desktop_notification_hook', data: String(hookResult)};
}
return notifyResult;
@ -349,6 +349,6 @@ export const notifyMe = (title, body, channel, teamId, silent, soundName, url) =
});
} catch (error) {
dispatch(logError(error));
return {result: 'error', reason: 'notification_api', data: String(error)};
return {status: 'error', reason: 'notification_api', data: String(error)};
}
};

View File

@ -167,7 +167,7 @@ class DesktopAppAPI {
) => {
if (window.desktopAPI?.sendNotification) {
const result = await window.desktopAPI.sendNotification(title, body, channelId, teamId, url, silent, soundName);
return result ?? {result: 'unsupported', reason: 'desktop_app_unsupported'};
return result ?? {status: 'unsupported', reason: 'desktop_app_unsupported'};
}
// get the desktop app to trigger the notification
@ -186,7 +186,7 @@ class DesktopAppAPI {
},
window.location.origin,
);
return {result: 'unsupported', reason: 'desktop_app_unsupported'};
return {status: 'unsupported', reason: 'desktop_app_unsupported'};
};
doBrowserHistoryPush = (path: string) => {

View File

@ -53,7 +53,7 @@ export async function showNotification(
if (Notification.permission !== 'granted' && requestedNotificationPermission) {
// User didn't allow notifications
return {result: 'not_sent', reason: 'notifications_permission_previously_denied', data: Notification.permission, callback: () => {}};
return {status: 'not_sent', reason: 'notifications_permission_previously_denied', data: Notification.permission, callback: () => {}};
}
requestedNotificationPermission = true;
@ -68,7 +68,7 @@ export async function showNotification(
if (permission !== 'granted') {
// User has denied notification for the site
return {result: 'not_sent', reason: 'notifications_permission_denied', data: permission, callback: () => {}};
return {status: 'not_sent', reason: 'notifications_permission_denied', data: permission, callback: () => {}};
}
const notification = new Notification(title, {
@ -95,7 +95,7 @@ export async function showNotification(
}
return {
result: 'success',
status: 'success',
callback: () => {
notification.close();
},

View File

@ -62,7 +62,7 @@
"@mattermost/client": "*",
"@mattermost/compass-components": "^0.2.12",
"@mattermost/compass-icons": "0.1.39",
"@mattermost/desktop-api": "5.8.0-4",
"@mattermost/desktop-api": "5.8.0-5",
"@mattermost/types": "*",
"@mui/base": "5.0.0-alpha.127",
"@mui/material": "5.11.16",
@ -4290,9 +4290,9 @@
"link": true
},
"node_modules/@mattermost/desktop-api": {
"version": "5.8.0-4",
"resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.8.0-4.tgz",
"integrity": "sha512-oEbh3ByDgM432LrjO9JsRnIwqBIAkF6bJaQkHjYeQAYwkI4/s4CSQ4kZcfMiO9hE0MjfF1VeXnGTBv9wyWsbAw==",
"version": "5.8.0-5",
"resolved": "https://registry.npmjs.org/@mattermost/desktop-api/-/desktop-api-5.8.0-5.tgz",
"integrity": "sha512-YPtFRnduVFOXyK25GedJA+PkAKmFpLDKqfxvV/IIS+SMypv6BD17LJ8AkdBsguFFa6ZWLlZU40M5grKYBbjOuA==",
"peerDependencies": {
"typescript": "^4.3.0 || ^5.0.0"
},

View File

@ -412,11 +412,11 @@ export default class WebSocketClient {
this.sendMessage('user_update_active_status', data, callback);
}
acknowledgePostedNotification(postId: string, result: 'error' | 'not_sent' | 'unsupported' | 'success', reason?: string, postedData?: string) {
acknowledgePostedNotification(postId: string, status: string, reason?: string, postedData?: string) {
const data = {
post_id: postId,
user_agent: window.navigator.userAgent,
result,
status,
reason,
data: postedData,
};

View File

@ -221,3 +221,10 @@ export type PostInfo = {
team_display_name: string;
has_joined_team: boolean;
}
export type NotificationStatus = 'error' | 'not_sent' | 'unsupported' | 'success';
export type NotificationResult = {
status: NotificationStatus;
reason?: string;
data?: string;
}