From 11826e2a21e6f4490f63d28b2a4d627ef90a0a8b Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Mon, 9 Mar 2026 18:23:44 +0530 Subject: [PATCH] perf: reduce presence update frequency and fix background tab throttling (#13726) ## Description Reduces the frequency of update_presence WebSocket calls from the live chat widget and fixes agents appearing offline when the dashboard is in a background tab. ## Fixes # (issue) https://github.com/chatwoot/chatwoot/issues/13720 ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- .../shared/helpers/BaseActionCableConnector.js | 9 +++++++-- app/javascript/widget/helpers/actionCable.js | 4 +++- app/services/internal/remove_stale_redis_keys_service.rb | 2 +- lib/online_status_tracker.rb | 7 +++++-- spec/lib/online_status_tracker_spec.rb | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/javascript/shared/helpers/BaseActionCableConnector.js b/app/javascript/shared/helpers/BaseActionCableConnector.js index 3eb61a80a..06f529dde 100644 --- a/app/javascript/shared/helpers/BaseActionCableConnector.js +++ b/app/javascript/shared/helpers/BaseActionCableConnector.js @@ -6,7 +6,12 @@ const RECONNECT_INTERVAL = 1000; class BaseActionCableConnector { static isDisconnected = false; - constructor(app, pubsubToken, websocketHost = '') { + constructor( + app, + pubsubToken, + websocketHost = '', + presenceInterval = PRESENCE_INTERVAL + ) { const websocketURL = websocketHost ? `${websocketHost}/cable` : undefined; this.consumer = createConsumer(websocketURL); @@ -37,7 +42,7 @@ class BaseActionCableConnector { setTimeout(() => { this.subscription.updatePresence(); this.triggerPresenceInterval(); - }, PRESENCE_INTERVAL); + }, presenceInterval); }; this.triggerPresenceInterval(); } diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js index 4e18d0c70..60c379ed8 100644 --- a/app/javascript/widget/helpers/actionCable.js +++ b/app/javascript/widget/helpers/actionCable.js @@ -13,9 +13,11 @@ const isMessageInActiveConversation = (getters, message) => { return activeConversationId && conversationId !== activeConversationId; }; +const WIDGET_PRESENCE_INTERVAL = 60000; + class ActionCableConnector extends BaseActionCableConnector { constructor(app, pubsubToken) { - super(app, pubsubToken); + super(app, pubsubToken, '', WIDGET_PRESENCE_INTERVAL); this.events = { 'message.created': this.onMessageCreated, 'message.updated': this.onMessageUpdated, diff --git a/app/services/internal/remove_stale_redis_keys_service.rb b/app/services/internal/remove_stale_redis_keys_service.rb index 553cc6c6a..609fcb4a6 100644 --- a/app/services/internal/remove_stale_redis_keys_service.rb +++ b/app/services/internal/remove_stale_redis_keys_service.rb @@ -3,7 +3,7 @@ class Internal::RemoveStaleRedisKeysService def perform Rails.logger.info "Removing redis stale keys for account #{@account_id}" - range_start = (Time.zone.now - OnlineStatusTracker::PRESENCE_DURATION).to_i + range_start = (Time.zone.now - OnlineStatusTracker::CONTACT_PRESENCE_DURATION).to_i # exclusive minimum score is specified by prefixing ( # we are clearing old records because this could clogg up the sorted set ::Redis::Alfred.zremrangebyscore( diff --git a/lib/online_status_tracker.rb b/lib/online_status_tracker.rb index bc2ed1dbc..20b379c02 100644 --- a/lib/online_status_tracker.rb +++ b/lib/online_status_tracker.rb @@ -1,6 +1,8 @@ class OnlineStatusTracker # NOTE: You can customise the environment variable to keep your agents/contacts as online for longer PRESENCE_DURATION = ENV.fetch('PRESENCE_DURATION', 20).to_i.seconds + # Widget pings every 60s, so contacts need a longer presence window + CONTACT_PRESENCE_DURATION = ENV.fetch('CONTACT_PRESENCE_DURATION', 90).to_i.seconds # presence : sorted set with timestamp as the score & object id as value @@ -11,7 +13,8 @@ class OnlineStatusTracker def self.get_presence(account_id, obj_type, obj_id) connected_time = ::Redis::Alfred.zscore(presence_key(account_id, obj_type), obj_id) - connected_time && connected_time > (Time.zone.now - PRESENCE_DURATION).to_i + duration = obj_type == 'Contact' ? CONTACT_PRESENCE_DURATION : PRESENCE_DURATION + connected_time && connected_time > (Time.zone.now - duration).to_i end def self.presence_key(account_id, type) @@ -39,7 +42,7 @@ class OnlineStatusTracker end def self.get_available_contact_ids(account_id) - range_start = (Time.zone.now - PRESENCE_DURATION).to_i + range_start = (Time.zone.now - CONTACT_PRESENCE_DURATION).to_i # exclusive minimum score is specified by prefixing ( # we are clearing old records because this could clogg up the sorted set ::Redis::Alfred.zremrangebyscore(presence_key(account_id, 'Contact'), '-inf', "(#{range_start}") diff --git a/spec/lib/online_status_tracker_spec.rb b/spec/lib/online_status_tracker_spec.rb index d88298485..70820d95e 100644 --- a/spec/lib/online_status_tracker_spec.rb +++ b/spec/lib/online_status_tracker_spec.rb @@ -42,7 +42,7 @@ describe OnlineStatusTracker do described_class.update_presence(account.id, 'Contact', online_contact.id) # creating a stale record for offline contact presence Redis::Alfred.zadd(format(Redis::Alfred::ONLINE_PRESENCE_CONTACTS, account_id: account.id), - (Time.zone.now - (OnlineStatusTracker::PRESENCE_DURATION + 20)).to_i, offline_contact.id) + (Time.zone.now - (OnlineStatusTracker::CONTACT_PRESENCE_DURATION + 20)).to_i, offline_contact.id) end it 'returns only the online contact ids with presence' do