More notification metrics fixes (#26889)

* Explicitly have the client tell the server when it should expect an ACK

* Don't count missing profile errors for your own posts, added comment

* Fix test

* Make postedAck a parameter in WebSocketClient

* Snapshot fixes

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Devin Binnie 2024-04-25 15:07:12 -04:00 committed by GitHub
parent 5f1a357845
commit 60c15c821f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 44 additions and 24 deletions

View File

@ -17,6 +17,7 @@ import (
const ( const (
connectionIDParam = "connection_id" connectionIDParam = "connection_id"
sequenceNumberParam = "sequence_number" sequenceNumberParam = "sequence_number"
postedAckParam = "posted_ack"
) )
func (api *API) InitWebSocket() { func (api *API) InitWebSocket() {
@ -48,6 +49,7 @@ func connectWebSocket(c *Context, w http.ResponseWriter, r *http.Request) {
TFunc: c.AppContext.T, TFunc: c.AppContext.T,
Locale: "", Locale: "",
Active: true, Active: true,
PostedAck: r.URL.Query().Get(postedAckParam) == "true",
} }
// The WebSocket upgrade request coming from mobile is missing the // The WebSocket upgrade request coming from mobile is missing the
// user agent so we need to fallback on the session's metadata. // user agent so we need to fallback on the session's metadata.

View File

@ -740,15 +740,19 @@ 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 // A user following a thread but had left the channel won't get a notification
// https://mattermost.atlassian.net/browse/MM-36769 // https://mattermost.atlassian.net/browse/MM-36769
if profileMap[uid] == nil { if profileMap[uid] == nil {
a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonMissingProfile) // This also sometimes happens when bots, which will never show up in the map, reply to threads
a.NotificationsLog().Error("Missing profile", // Their own post goes through this and they get "notified", which we don't need to count as an error if they can't
mlog.String("type", model.NotificationTypeWebsocket), if uid != post.UserId {
mlog.String("post_id", post.Id), a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeWebsocket, model.NotificationReasonMissingProfile)
mlog.String("status", model.NotificationStatusError), a.NotificationsLog().Error("Missing profile",
mlog.String("reason", model.NotificationReasonMissingProfile), mlog.String("type", model.NotificationTypeWebsocket),
mlog.String("sender_id", sender.Id), mlog.String("post_id", post.Id),
mlog.String("receiver_id", uid), mlog.String("status", model.NotificationStatusError),
) mlog.String("reason", model.NotificationReasonMissingProfile),
mlog.String("sender_id", sender.Id),
mlog.String("receiver_id", uid),
)
}
continue continue
} }
if a.IsCRTEnabledForUser(c, uid) { if a.IsCRTEnabledForUser(c, uid) {

View File

@ -68,6 +68,7 @@ type WebConnConfig struct {
Active bool Active bool
ReuseCount int ReuseCount int
OriginClient string OriginClient string
PostedAck bool
// These aren't necessary to be exported to api layer. // These aren't necessary to be exported to api layer.
sequence int sequence int
@ -89,6 +90,7 @@ type WebConn struct {
Locale string Locale string
Sequence int64 Sequence int64
UserId string UserId string
PostedAck bool
allChannelMembers map[string]string allChannelMembers map[string]string
lastAllChannelMembersTime int64 lastAllChannelMembersTime int64
@ -234,6 +236,7 @@ func (ps *PlatformService) NewWebConn(cfg *WebConnConfig, suite SuiteIFace, runn
UserId: cfg.Session.UserId, UserId: cfg.Session.UserId,
T: cfg.TFunc, T: cfg.TFunc,
Locale: cfg.Locale, Locale: cfg.Locale,
PostedAck: cfg.PostedAck,
reuseCount: cfg.ReuseCount, reuseCount: cfg.ReuseCount,
endWritePump: make(chan struct{}), endWritePump: make(chan struct{}),
pumpFinished: make(chan struct{}), pumpFinished: make(chan struct{}),

View File

@ -82,11 +82,9 @@ func usePostedAckHook(message *model.WebSocketEvent, postedUserId string, channe
} }
func (h *postedAckBroadcastHook) Process(msg *platform.HookedWebSocketEvent, webConn *platform.WebConn, args map[string]any) error { func (h *postedAckBroadcastHook) Process(msg *platform.HookedWebSocketEvent, webConn *platform.WebConn, args map[string]any) error {
// Don't ACK mobile app websocket events at this time, we may revisit this when we can add this to mobile app // Don't ACK unless we say to explicitly
if session := webConn.GetSession(); session != nil { if !webConn.PostedAck {
if session.IsMobile() { return nil
return nil
}
} }
postedUserId, err := getTypedArg[string](args, "posted_user_id") postedUserId, err := getTypedArg[string](args, "posted_user_id")

View File

@ -87,8 +87,9 @@ func TestPostedAckHook_Process(t *testing.T) {
hook := &postedAckBroadcastHook{} hook := &postedAckBroadcastHook{}
userID := model.NewId() userID := model.NewId()
webConn := &platform.WebConn{ webConn := &platform.WebConn{
UserId: userID, UserId: userID,
Platform: &platform.PlatformService{}, Platform: &platform.PlatformService{},
PostedAck: true,
} }
webConn.SetSession(&model.Session{}) webConn.SetSession(&model.Session{})
@ -140,12 +141,12 @@ func TestPostedAckHook_Process(t *testing.T) {
assert.True(t, msg.Event().GetData()["should_ack"].(bool)) assert.True(t, msg.Event().GetData()["should_ack"].(bool))
}) })
t.Run("should not ack for mobile app", func(t *testing.T) { t.Run("should not ack if posted ack is false", func(t *testing.T) {
mobileWebConn := &platform.WebConn{ mobileWebConn := &platform.WebConn{
UserId: userID, UserId: userID,
Platform: &platform.PlatformService{}, Platform: &platform.PlatformService{},
PostedAck: false,
} }
mobileWebConn.SetSession(&model.Session{Props: map[string]string{"isMobile": "true"}})
msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, "")) msg := platform.MakeHookedWebSocketEvent(model.NewWebSocketEvent(model.WebsocketEventPosted, "", "", "", nil, ""))
hook.Process(msg, mobileWebConn, map[string]any{ hook.Process(msg, mobileWebConn, map[string]any{

View File

@ -179,7 +179,7 @@ export function initialize() {
WebSocketClient.addMissedMessageListener(restart); WebSocketClient.addMissedMessageListener(restart);
WebSocketClient.addCloseListener(handleClose); WebSocketClient.addCloseListener(handleClose);
WebSocketClient.initialize(connUrl); WebSocketClient.initialize(connUrl, undefined, true);
} }
export function close() { export function close() {

View File

@ -111,6 +111,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
"messageListeners": Set {}, "messageListeners": Set {},
"missedEventCallback": null, "missedEventCallback": null,
"missedMessageListeners": Set {}, "missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null, "reconnectCallback": null,
"reconnectListeners": Set {}, "reconnectListeners": Set {},
"responseCallbacks": Object {}, "responseCallbacks": Object {},
@ -157,6 +158,7 @@ exports[`PostBodyAdditionalContent with a normal link Should render the plugin c
"messageListeners": Set {}, "messageListeners": Set {},
"missedEventCallback": null, "missedEventCallback": null,
"missedMessageListeners": Set {}, "missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null, "reconnectCallback": null,
"reconnectListeners": Set {}, "reconnectListeners": Set {},
"responseCallbacks": Object {}, "responseCallbacks": Object {},

View File

@ -115,6 +115,7 @@ exports[`plugins/Pluggable should match snapshot with extended component 1`] = `
"messageListeners": Set {}, "messageListeners": Set {},
"missedEventCallback": null, "missedEventCallback": null,
"missedMessageListeners": Set {}, "missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null, "reconnectCallback": null,
"reconnectListeners": Set {}, "reconnectListeners": Set {},
"responseCallbacks": Object {}, "responseCallbacks": Object {},
@ -248,6 +249,7 @@ exports[`plugins/Pluggable should match snapshot with extended component with pl
"messageListeners": Set {}, "messageListeners": Set {},
"missedEventCallback": null, "missedEventCallback": null,
"missedMessageListeners": Set {}, "missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null, "reconnectCallback": null,
"reconnectListeners": Set {}, "reconnectListeners": Set {},
"responseCallbacks": Object {}, "responseCallbacks": Object {},
@ -507,6 +509,7 @@ exports[`plugins/Pluggable should match snapshot with null pluggableId 1`] = `
"messageListeners": Set {}, "messageListeners": Set {},
"missedEventCallback": null, "missedEventCallback": null,
"missedMessageListeners": Set {}, "missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null, "reconnectCallback": null,
"reconnectListeners": Set {}, "reconnectListeners": Set {},
"responseCallbacks": Object {}, "responseCallbacks": Object {},
@ -641,6 +644,7 @@ exports[`plugins/Pluggable should match snapshot with valid pluggableId 1`] = `
"messageListeners": Set {}, "messageListeners": Set {},
"missedEventCallback": null, "missedEventCallback": null,
"missedMessageListeners": Set {}, "missedMessageListeners": Set {},
"postedAck": false,
"reconnectCallback": null, "reconnectCallback": null,
"reconnectListeners": Set {}, "reconnectListeners": Set {},
"responseCallbacks": Object {}, "responseCallbacks": Object {},

View File

@ -68,6 +68,7 @@ export default class WebSocketClient {
private closeListeners = new Set<CloseListener>(); private closeListeners = new Set<CloseListener>();
private connectionId: string | null; private connectionId: string | null;
private postedAck: boolean;
constructor() { constructor() {
this.conn = null; this.conn = null;
@ -77,12 +78,13 @@ export default class WebSocketClient {
this.connectFailCount = 0; this.connectFailCount = 0;
this.responseCallbacks = {}; this.responseCallbacks = {};
this.connectionId = ''; this.connectionId = '';
this.postedAck = false;
} }
// on connect, only send auth cookie and blank state. // on connect, only send auth cookie and blank state.
// on hello, get the connectionID and store it. // on hello, get the connectionID and store it.
// on reconnect, send cookie, connectionID, sequence number. // on reconnect, send cookie, connectionID, sequence number.
initialize(connectionUrl = this.connectionUrl, token?: string) { initialize(connectionUrl = this.connectionUrl, token?: string, postedAck?: boolean) {
if (this.conn) { if (this.conn) {
return; return;
} }
@ -96,10 +98,14 @@ export default class WebSocketClient {
console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console console.log('websocket connecting to ' + connectionUrl); //eslint-disable-line no-console
} }
if (typeof postedAck != 'undefined') {
this.postedAck = postedAck;
}
// Add connection id, and last_sequence_number to the query param. // Add connection id, and last_sequence_number to the query param.
// We cannot use a cookie because it will bleed across tabs. // We cannot use a cookie because it will bleed across tabs.
// We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request. // We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request.
this.conn = new WebSocket(`${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}`); this.conn = new WebSocket(`${connectionUrl}?connection_id=${this.connectionId}&sequence_number=${this.serverSequence}${this.postedAck ? '&posted_ack=true' : ''}`);
this.connectionUrl = connectionUrl; this.connectionUrl = connectionUrl;
this.conn.onopen = () => { this.conn.onopen = () => {
@ -148,7 +154,7 @@ export default class WebSocketClient {
setTimeout( setTimeout(
() => { () => {
this.initialize(connectionUrl, token); this.initialize(connectionUrl, token, postedAck);
}, },
retryTime, retryTime,
); );