From c70a57407d4722e878beeccc9ad756fbb2d75810 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Wed, 17 Jun 2026 00:05:29 +0530 Subject: [PATCH] fix: label Chatwoot Mobile sessions instead of 'Unknown Device' (#14753) ## Description Sessions created by logins from the Chatwoot Mobile app currently render as "Unknown Device" in the dashboard sessions UI. The mobile app's HTTP layer sends: - Android: `User-Agent: okhttp/4.9.2` - iOS: `User-Agent: Chatwoot/ CFNetwork/ Darwin/` Neither pattern is classifiable by the `browser` gem, so \`browser_name\`, \`platform_name\`, and \`device_name\` all end up "Unknown". This change adds a backend-only fallback in \`UserSessionTrackingService\`. When \`Browser.new(ua)\` returns "Unknown Browser" and the UA matches a known Chatwoot Mobile pattern, the labels are overridden to \`Chatwoot Mobile\` + \`Android\` / \`iPhone\`. The Vue sessions list (\`ActiveSessions.vue\`) then renders "Chatwoot Mobile on Android" or "Chatwoot Mobile on iPhone" with the smartphone icon. This is the immediate floor. A follow-up will add structured \`X-Chatwoot-*\` headers from the mobile app so we can render full version + device model. Fixes INF-75. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? - Added specs ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --- app/services/user_session_tracking_service.rb | 26 ++++++- .../user_session_tracking_service_spec.rb | 69 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/app/services/user_session_tracking_service.rb b/app/services/user_session_tracking_service.rb index 28f272a18..d7e6f6efb 100644 --- a/app/services/user_session_tracking_service.rb +++ b/app/services/user_session_tracking_service.rb @@ -1,4 +1,11 @@ class UserSessionTrackingService + # CFNetwork UAs cannot distinguish iPhone from iPad; both get labelled iPhone here. + LEGACY_MOBILE_UAS = [ + { match: %r{\Aokhttp/}, platform: 'Android', device: 'Android' }, + { match: %r{\AChatwoot/.*CFNetwork.*Darwin}, platform: 'iPhone', device: 'iPhone' } + ].freeze + private_constant :LEGACY_MOBILE_UAS + def initialize(user:, request:, client_id:) @user = user @request = request @@ -26,7 +33,7 @@ class UserSessionTrackingService def session_attributes browser = Browser.new(@request.user_agent) - { + attrs = { ip_address: @request.remote_ip, user_agent: @request.user_agent, browser_name: browser.name, @@ -35,5 +42,22 @@ class UserSessionTrackingService platform_name: browser.platform.name, platform_version: browser.platform.version } + + patch_for_legacy_mobile(attrs) + end + + def patch_for_legacy_mobile(attrs) + return attrs unless attrs[:browser_name] == 'Unknown Browser' + + hit = LEGACY_MOBILE_UAS.find { |m| @request.user_agent.to_s.match?(m[:match]) } + return attrs unless hit + + attrs.merge( + browser_name: 'Chatwoot Mobile', + browser_version: nil, + platform_name: hit[:platform], + platform_version: nil, + device_name: hit[:device] + ) end end diff --git a/spec/services/user_session_tracking_service_spec.rb b/spec/services/user_session_tracking_service_spec.rb index 71b6d5924..2b98b95bf 100644 --- a/spec/services/user_session_tracking_service_spec.rb +++ b/spec/services/user_session_tracking_service_spec.rb @@ -47,6 +47,75 @@ RSpec.describe UserSessionTrackingService do expect(existing.reload.ip_address).to eq('8.8.8.8') expect(existing.last_activity_at).to be_within(1.second).of(Time.current) end + + context 'with a Chatwoot Mobile legacy User-Agent' do + let(:request) do + instance_double( + ActionDispatch::Request, + user_agent: ua, + remote_ip: '8.8.8.8' + ) + end + + context 'when the UA is okhttp (Android Chatwoot Mobile)' do + let(:ua) { 'okhttp/4.9.2' } + + it 'labels the session as Chatwoot Mobile on Android', :aggregate_failures do + service.create_or_update! + + session = user.user_sessions.last + expect(session.browser_name).to eq('Chatwoot Mobile') + expect(session.browser_version).to be_nil + expect(session.platform_name).to eq('Android') + expect(session.platform_version).to be_nil + expect(session.device_name).to eq('Android') + expect(session.user_agent).to eq(ua) + end + end + + context 'when the UA is CFNetwork (iOS Chatwoot Mobile)' do + let(:ua) { 'Chatwoot/3759 CFNetwork/3886.100.1 Darwin/27.0.0' } + + it 'labels the session as Chatwoot Mobile on iPhone', :aggregate_failures do + service.create_or_update! + + session = user.user_sessions.last + expect(session.browser_name).to eq('Chatwoot Mobile') + expect(session.browser_version).to be_nil + expect(session.platform_name).to eq('iPhone') + expect(session.platform_version).to be_nil + expect(session.device_name).to eq('iPhone') + expect(session.user_agent).to eq(ua) + end + end + + context 'when the UA is a real browser (Firefox on Linux)' do + let(:ua) { 'Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0' } + + it 'does not override the Browser-derived metadata', :aggregate_failures do + service.create_or_update! + + session = user.user_sessions.last + expect(session.browser_name).to eq('Firefox') + expect(session.platform_name).to eq('Generic Linux') + expect(session.device_name).not_to eq('Android') + expect(session.device_name).not_to eq('iPhone') + end + end + + context 'when the UA is unknown but does not match any mobile pattern' do + let(:ua) { 'curl/8.4.0' } + + it 'leaves the Unknown labels untouched', :aggregate_failures do + service.create_or_update! + + session = user.user_sessions.last + expect(session.browser_name).to eq('Unknown Browser') + expect(session.platform_name).to eq('Unknown') + expect(session.device_name).to eq('Unknown') + end + end + end end describe '#update_activity!' do