mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-04 21:02:35 +08:00
Some checks failed
Frontend Lint & Test / test (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
We're getting many customer reports saying "I'm not getting notifications." We can't always identify the root cause since there are multiple points of failure. Added a **Push Diagnostics** tool in Super Admin to help us investigate mobile/web push issues. Here's how it works: - Look up a user by email/ID → see all their registered subscriptions with device info (iOS/Android, brand, model), token freshness, and last-updated time - Send a customizable test push and read the raw FCM/web-push/relay response to see if the customer is receiving push notifications—if not, it will show proper errors. - Delete broken subscriptions so the mobile app re-registers on next launch <img width="3816" height="1974" alt="CleanShot 2026-04-20 at 12 56 56@2x" src="https://github.com/user-attachments/assets/08ecab6f-7ec3-44b3-a114-5e6eb8cf0879" /> Fixes https://linear.app/chatwoot/issue/CW-6892/push-diagnostics-tool --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
5.2 KiB
Ruby
161 lines
5.2 KiB
Ruby
class Notification::PushTestService
|
|
pattr_initialize [:user!, :subscription_ids!, :title, :body]
|
|
|
|
DEFAULT_TITLE = '%<installation_name>s notification test'.freeze
|
|
DEFAULT_BODY = 'This is a test from our team to check notification delivery on your device. No action needed.'.freeze
|
|
|
|
def self.default_title
|
|
format(DEFAULT_TITLE, installation_name: GlobalConfigService.load('INSTALLATION_NAME', 'Chatwoot'))
|
|
end
|
|
|
|
def self.default_body
|
|
DEFAULT_BODY
|
|
end
|
|
|
|
def perform
|
|
selected_subscriptions.map { |subscription| test_send(subscription) }
|
|
end
|
|
|
|
private
|
|
|
|
def resolved_title
|
|
title.presence || self.class.default_title
|
|
end
|
|
|
|
def resolved_body
|
|
body.presence || self.class.default_body
|
|
end
|
|
|
|
def selected_subscriptions
|
|
user.notification_subscriptions.where(id: subscription_ids).order(:id)
|
|
end
|
|
|
|
def test_send(subscription)
|
|
if subscription.browser_push?
|
|
test_browser_push(subscription)
|
|
elsif subscription.fcm?
|
|
test_fcm(subscription)
|
|
else
|
|
result(subscription, subscription.subscription_type.to_s, :skipped, 'Unknown subscription type')
|
|
end
|
|
end
|
|
|
|
def test_browser_push(subscription)
|
|
return result(subscription, 'browser_push', :skipped, 'VAPID keys not configured') unless VapidService.public_key
|
|
|
|
WebPush.payload_send(**browser_push_payload(subscription))
|
|
result(subscription, 'browser_push', :success, 'Web push accepted by endpoint')
|
|
rescue StandardError => e
|
|
result(subscription, 'browser_push', :failure, "#{e.class.name}: #{e.message}")
|
|
end
|
|
|
|
def test_fcm(subscription)
|
|
if firebase_credentials_present?
|
|
test_fcm_direct(subscription)
|
|
elsif chatwoot_hub_enabled?
|
|
test_fcm_via_hub(subscription)
|
|
else
|
|
result(subscription, 'fcm', :skipped, 'No Firebase credentials and push relay disabled')
|
|
end
|
|
end
|
|
|
|
def test_fcm_direct(subscription)
|
|
fcm_service = Notification::FcmService.new(
|
|
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil),
|
|
GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
|
|
)
|
|
response = fcm_service.fcm_client.send_v1(fcm_options(subscription))
|
|
status_code = response[:status_code].to_i
|
|
status = status_code.between?(200, 299) ? :success : :failure
|
|
result(subscription, 'fcm', status, "HTTP #{status_code} — #{response[:body]}")
|
|
rescue StandardError => e
|
|
result(subscription, 'fcm', :failure, "#{e.class.name}: #{e.message}")
|
|
end
|
|
|
|
def test_fcm_via_hub(subscription)
|
|
response = ChatwootHub.send_push_with_response(fcm_options(subscription))
|
|
result(subscription, 'fcm_via_hub', :success, "HTTP #{response.code} — #{response.body}")
|
|
rescue RestClient::ExceptionWithResponse => e
|
|
result(subscription, 'fcm_via_hub', :failure, "HTTP #{e.response&.code} — #{e.response&.body}")
|
|
rescue StandardError => e
|
|
result(subscription, 'fcm_via_hub', :failure, "#{e.class.name}: #{e.message}")
|
|
end
|
|
|
|
def firebase_credentials_present?
|
|
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil) && GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
|
|
end
|
|
|
|
def chatwoot_hub_enabled?
|
|
ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
|
|
end
|
|
|
|
def browser_push_payload(subscription)
|
|
{
|
|
message: JSON.generate(
|
|
title: resolved_title,
|
|
tag: "super_admin_test_#{Time.zone.now.to_i}",
|
|
url: ENV.fetch('FRONTEND_URL', 'https://app.chatwoot.com')
|
|
),
|
|
endpoint: subscription.subscription_attributes['endpoint'],
|
|
p256dh: subscription.subscription_attributes['p256dh'],
|
|
auth: subscription.subscription_attributes['auth'],
|
|
vapid: {
|
|
subject: ENV.fetch('FRONTEND_URL', 'https://app.chatwoot.com'),
|
|
public_key: VapidService.public_key,
|
|
private_key: VapidService.private_key
|
|
},
|
|
ssl_timeout: 5,
|
|
open_timeout: 5,
|
|
read_timeout: 5
|
|
}
|
|
end
|
|
|
|
def fcm_options(subscription)
|
|
{
|
|
'token': subscription.subscription_attributes['push_token'],
|
|
'data': { payload: { data: { notification: { type: 'test' } } }.to_json },
|
|
'notification': { title: resolved_title, body: resolved_body },
|
|
'android': { priority: 'high' },
|
|
'apns': { payload: { aps: { sound: 'default', category: Time.zone.now.to_i.to_s } } },
|
|
'fcm_options': { analytics_label: 'SuperAdminTest' }
|
|
}
|
|
end
|
|
|
|
def result(subscription, type, status, message)
|
|
attrs = subscription.subscription_attributes || {}
|
|
{
|
|
id: subscription.id,
|
|
type: type.to_s,
|
|
device: device_label(subscription, attrs),
|
|
token_tail: token_tail(subscription, attrs),
|
|
status: status,
|
|
message: message
|
|
}
|
|
end
|
|
|
|
def device_label(subscription, attrs)
|
|
if subscription.browser_push?
|
|
endpoint_host(attrs['endpoint'].to_s)
|
|
else
|
|
attrs['device_id'].present? ? "…#{attrs['device_id'].to_s.last(6)}" : '—'
|
|
end
|
|
end
|
|
|
|
def endpoint_host(endpoint)
|
|
return '—' if endpoint.blank?
|
|
|
|
URI.parse(endpoint).host.presence || endpoint
|
|
rescue URI::InvalidURIError
|
|
endpoint
|
|
end
|
|
|
|
def token_tail(subscription, attrs)
|
|
if subscription.browser_push?
|
|
endpoint = attrs['endpoint'].to_s
|
|
endpoint.present? ? "…#{endpoint.last(6)}" : '—'
|
|
else
|
|
attrs['push_token'].present? ? "…#{attrs['push_token'].to_s.last(6)}" : '—'
|
|
end
|
|
end
|
|
end
|