[verified] Remove Chatwoot Hub sync paths

This commit is contained in:
Kunthawat Greethong
2026-08-15 13:23:03 +07:00
parent 8ebb320bd4
commit 832a7fdae0
15 changed files with 43 additions and 290 deletions

View File

@@ -23,18 +23,11 @@ class Installation::OnboardingController < ApplicationController
private
def onboarding_params
params.permit(:subscribe_to_updates, user: [:name, :company, :email])
params.permit(user: [:name, :company, :email])
end
def finish_onboarding
::Redis::Alfred.delete(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
return if onboarding_params[:subscribe_to_updates].blank?
ChatwootHub.register_instance(
onboarding_params.dig(:user, :company),
onboarding_params.dig(:user, :name),
onboarding_params.dig(:user, :email)
)
end
def ensure_installation_onboarding

View File

@@ -1,10 +1,3 @@
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
def show; end
def refresh
Internal::CheckNewVersionsJob.perform_now
# rubocop:disable Rails/I18nLocaleTexts
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
# rubocop:enable Rails/I18nLocaleTexts
end
end

View File

@@ -2,18 +2,7 @@ class Internal::CheckNewVersionsJob < ApplicationJob
queue_as :scheduled_jobs
def perform
return unless Rails.env.production?
@instance_info = ChatwootHub.sync_with_hub
update_version_info
end
private
def update_version_info
return if @instance_info['version'].blank?
::Redis::Alfred.set(::Redis::Alfred::LATEST_CHATWOOT_VERSION, @instance_info['version'])
# Version checks are intentionally local-only in the private fork.
end
end

View File

@@ -2,25 +2,7 @@ class Internal::TriggerDailyScheduledItemsJob < ApplicationJob
queue_as :scheduled_jobs
def perform
# Schedule daily deferred jobs here so each installation can spread load
# across the day without changing its slot on deploys or restarts.
schedule_version_check
end
private
def schedule_version_check
return unless Rails.env.production?
Internal::CheckNewVersionsJob.set(wait_until: version_check_run_at).perform_later
end
def version_check_run_at
Time.current.utc.beginning_of_day + designated_minute.minutes
end
def designated_minute
@designated_minute ||= Digest::MD5.hexdigest(ChatwootHub.installation_identifier).hex % 1440
# Daily local scheduled work is extended by feature modules when present.
end
end

View File

@@ -59,13 +59,6 @@
</div>
</div>
<div class="text-sm text-slate-900">
<%= check_box_tag "subscribe_to_updates", 'true', true %>
<label class="ml-2" for="subscribe_to_updates">
Subscribe to release notes, newsletters & product feedback surveys.
</label>
</div>
<button type="submit" class="flex items-center w-full justify-center rounded-md bg-woot-500 py-3 px-3 text-base font-medium text-white shadow-sm hover:bg-woot-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-woot-500 cursor-pointer">
Finish Setup
</button>

View File

@@ -44,10 +44,6 @@
<div class="flex flex-col flex-grow gap-1">
<div class="flex items-center gap-2">
<h2 class="h-5 leading-5 text-n-slate-12 text-sm font-medium">Current plan</h2>
<a href="<%= refresh_super_admin_settings_url %>" class="inline-flex gap-1 text-xs font-medium items-center text-woot-500 hover:text-woot-700">
<svg width="16" height="16"><use xlink:href="#icon-refresh-line" /></svg>
<span>Refresh</span>
</a>
</div>
<p class="text-n-slate-11 m-0 text-sm"><%= SuperAdmin::FeaturesHelper.plan_details.html_safe %></p>
</div>

View File

@@ -730,9 +730,7 @@ Rails.application.routes.draw do
resources :platform_banners
resource :instance_status, only: [:show]
resource :settings, only: [:show] do
get :refresh, on: :collection
end
resource :settings, only: [:show]
# resources that doesn't appear in primary navigation in super admin
resources :account_users, only: [:new, :create, :show, :destroy]

View File

@@ -1,30 +1,2 @@
module Enterprise::Internal::CheckNewVersionsJob
def perform
super
update_plan_info
reconcile_premium_config_and_features
end
private
def update_plan_info
return if @instance_info.blank?
update_installation_config(key: 'INSTALLATION_PRICING_PLAN', value: @instance_info['plan'])
update_installation_config(key: 'INSTALLATION_PRICING_PLAN_QUANTITY', value: @instance_info['plan_quantity'])
update_installation_config(key: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN', value: @instance_info['chatwoot_support_website_token'])
update_installation_config(key: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH', value: @instance_info['chatwoot_support_identifier_hash'])
update_installation_config(key: 'CHATWOOT_SUPPORT_SCRIPT_URL', value: @instance_info['chatwoot_support_script_url'])
end
def update_installation_config(key:, value:)
config = InstallationConfig.find_or_initialize_by(name: key)
config.value = value
config.locked = true
config.save!
end
def reconcile_premium_config_and_features
Internal::ReconcilePlanConfigService.new.perform
end
end

View File

@@ -1,4 +1,3 @@
# TODO: lets use HTTParty instead of RestClient
class ChatwootHub
DEFAULT_BASE_URL = 'https://hub.2.chatwoot.com'.freeze
@@ -6,22 +5,6 @@ class ChatwootHub
DEFAULT_BASE_URL
end
def self.ping_url
"#{base_url}/ping"
end
def self.registration_url
"#{base_url}/instances"
end
def self.push_notification_url
"#{base_url}/send_push"
end
def self.events_url
"#{base_url}/events"
end
def self.billing_base_url
"#{base_url}/billing"
end
@@ -55,79 +38,6 @@ class ChatwootHub
support_identifier_hash: InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH')&.value
}
end
def self.instance_config
{
installation_identifier: installation_identifier,
installation_version: Chatwoot.config[:version],
installation_host: URI.parse(ENV.fetch('FRONTEND_URL', '')).host,
installation_env: ENV.fetch('INSTALLATION_ENV', ''),
edition: ENV.fetch('CW_EDITION', '')
}
end
def self.instance_metrics
{
accounts_count: fetch_count(Account),
users_count: fetch_count(User),
inboxes_count: fetch_count(Inbox),
conversations_count: fetch_count(Conversation),
incoming_messages_count: fetch_count(Message.incoming),
outgoing_messages_count: fetch_count(Message.outgoing),
additional_information: {}
}
end
def self.fetch_count(model)
model.last&.id || 0
end
def self.sync_with_hub
begin
info = instance_config
info = info.merge(instance_metrics) unless ENV['DISABLE_TELEMETRY']
response = RestClient.post(ping_url, info.to_json, { content_type: :json, accept: :json })
parsed_response = JSON.parse(response)
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
parsed_response
end
def self.register_instance(company_name, owner_name, owner_email)
info = { company_name: company_name, owner_name: owner_name, owner_email: owner_email, subscribed_to_mailers: true }
RestClient.post(registration_url, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
def self.send_push(fcm_options)
send_push_with_response(fcm_options)
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
def self.send_push_with_response(fcm_options)
info = { fcm_options: fcm_options }
RestClient.post(push_notification_url, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
end
def self.emit_event(event_name, event_data)
return if ENV['DISABLE_TELEMETRY']
info = { event_name: event_name, event_data: event_data }
RestClient.post(events_url, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
Rails.logger.error "Exception: #{e.message}"
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
end
ChatwootHub.singleton_class.prepend_mod_with('ChatwootHub')

View File

@@ -13,10 +13,11 @@ RSpec.describe 'Installation::Onboarding API', type: :request do
end
context 'when CHATWOOT_INSTALLATION_ONBOARDING redis key is set' do
it 'returns onboarding page' do
it 'returns onboarding page without a subscription checkbox' do
Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING, true)
get '/installation/onboarding'
expect(response).to have_http_status(:success)
expect(response.body).not_to include('subscribe_to_updates')
Redis::Alfred.delete(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
end
end
@@ -28,7 +29,6 @@ RSpec.describe 'Installation::Onboarding API', type: :request do
before do
allow(AccountBuilder).to receive(:new).and_return(account_builder)
allow(account_builder).to receive(:perform).and_return(true)
allow(ChatwootHub).to receive(:register_instance).and_return(true)
Redis::Alfred.set(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING, true)
end
@@ -42,14 +42,9 @@ RSpec.describe 'Installation::Onboarding API', type: :request do
expect(Redis::Alfred.get(Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)).to be_nil
end
it 'will not call register instance when checkboxes are unchecked' do
post '/installation/onboarding', params: { user: {} }
expect(ChatwootHub).not_to have_received(:register_instance)
end
it 'will call register instance when checkboxes are checked' do
it 'accepts the subscription parameter without remote registration' do
post '/installation/onboarding', params: { user: {}, subscribe_to_updates: 1 }
expect(ChatwootHub).to have_received(:register_instance)
expect(response).to have_http_status(:redirect)
end
end

View File

@@ -0,0 +1,13 @@
require 'rails_helper'
RSpec.describe SuperAdmin::SettingsController do
it 'does not expose a manual refresh action' do
expect(described_class.action_methods).not_to include('refresh')
end
it 'does not leave a manual refresh link in the settings view' do
template = Rails.root.join('app/views/super_admin/settings/show.html.erb').read
expect(template).not_to include('refresh_super_admin_settings_url')
end
end

View File

@@ -8,25 +8,19 @@ RSpec.describe Internal::CheckNewVersionsJob do
before do
allow(Internal::ReconcilePlanConfigService).to receive(:new).and_return(reconsile_premium_config_service)
allow(reconsile_premium_config_service).to receive(:perform)
allow(Rails.env).to receive(:production?).and_return(true)
end
it 'updates the plan info' do
data = { 'version' => '1.2.3', 'plan' => 'enterprise', 'plan_quantity' => 1, 'chatwoot_support_website_token' => '123',
'chatwoot_support_identifier_hash' => '123', 'chatwoot_support_script_url' => '123' }
allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
it 'does not reconcile enterprise configuration from a remote response' do
job
expect(InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN').value).to eq 'enterprise'
expect(InstallationConfig.find_by(name: 'INSTALLATION_PRICING_PLAN_QUANTITY').value).to eq 1
expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_WEBSITE_TOKEN').value).to eq '123'
expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_IDENTIFIER_HASH').value).to eq '123'
expect(InstallationConfig.find_by(name: 'CHATWOOT_SUPPORT_SCRIPT_URL').value).to eq '123'
expect(reconsile_premium_config_service).not_to have_received(:perform)
end
it 'calls Internal::ReconcilePlanConfigService' do
data = { 'version' => '1.2.3' }
allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
job
expect(reconsile_premium_config_service).to have_received(:perform)
it 'does not update installation configuration' do
expect { job }.not_to(
change do
InstallationConfig.where(name: %w[INSTALLATION_PRICING_PLAN INSTALLATION_PRICING_PLAN_QUANTITY]).pluck(:name, :serialized_value)
end
)
end
end

View File

@@ -3,12 +3,11 @@ require 'rails_helper'
RSpec.describe Internal::CheckNewVersionsJob do
subject(:job) { described_class.perform_now }
it 'updates the latest chatwoot version in redis' do
data = { 'version' => '1.2.3' }
allow(Rails.env).to receive(:production?).and_return(true)
allow(ChatwootHub).to receive(:sync_with_hub).and_return(data)
it 'does not write a remote version result' do
allow(Redis::Alfred).to receive(:set)
job
expect(ChatwootHub).to have_received(:sync_with_hub)
expect(Redis::Alfred.get(Redis::Alfred::LATEST_CHATWOOT_VERSION)).to eq data['version']
expect(Redis::Alfred).not_to have_received(:set)
end
end

View File

@@ -3,14 +3,8 @@ require 'rails_helper'
RSpec.describe Internal::TriggerDailyScheduledItemsJob do
subject(:perform_job) { described_class.perform_now }
let(:installation_id) { 'test-installation-id' }
let(:designated_minute) { Digest::MD5.hexdigest(installation_id).hex % 1440 }
let(:scheduled_time) { Time.current.utc.beginning_of_day + designated_minute.minutes }
let(:configured_job) { instance_double(ActiveJob::ConfiguredJob, perform_later: true) }
before do
allow(ChatwootHub).to receive(:installation_identifier).and_return(installation_id)
allow(Internal::CheckNewVersionsJob).to receive(:set).and_return(configured_job)
allow(Internal::CheckNewVersionsJob).to receive(:set)
end
it 'enqueues the job' do
@@ -18,20 +12,9 @@ RSpec.describe Internal::TriggerDailyScheduledItemsJob do
.on_queue('scheduled_jobs')
end
it 'schedules the version check at a stable minute in production' do
it 'does not enqueue the remote version check in production' do
allow(Rails.env).to receive(:production?).and_return(true)
travel_to Time.zone.parse('2026-03-17 08:00:00 UTC') do
perform_job
expect(Internal::CheckNewVersionsJob).to have_received(:set).with(wait_until: scheduled_time)
expect(configured_job).to have_received(:perform_later)
end
end
it 'does not schedule the version check outside production' do
allow(Rails.env).to receive(:production?).and_return(false)
perform_job
expect(Internal::CheckNewVersionsJob).not_to have_received(:set)

View File

@@ -1,6 +1,12 @@
require 'rails_helper'
describe ChatwootHub do
it 'does not expose remote sync, registration, or event APIs' do
expect(described_class).not_to respond_to(:sync_with_hub)
expect(described_class).not_to respond_to(:register_instance)
expect(described_class).not_to respond_to(:emit_event)
end
describe '.base_url' do
it 'uses the static hub url' do
expect(described_class::DEFAULT_BASE_URL).to eq('https://hub.2.chatwoot.com')
@@ -13,67 +19,4 @@ describe ChatwootHub do
expect(installation_identifier).not_to be_nil
expect(described_class.installation_identifier).to eq installation_identifier
end
context 'when fetching sync_with_hub' do
it 'get latest version from chatwoot hub' do
version = '1.1.1'
allow(RestClient).to receive(:post).and_return({ version: version }.to_json)
expect(described_class.sync_with_hub['version']).to eq version
expect(RestClient).to have_received(:post).with(described_class.ping_url, described_class.instance_config
.merge(described_class.instance_metrics).to_json, { content_type: :json, accept: :json })
end
it 'will not send instance metrics when telemetry is disabled' do
version = '1.1.1'
with_modified_env DISABLE_TELEMETRY: 'true' do
allow(RestClient).to receive(:post).and_return({ version: version }.to_json)
expect(described_class.sync_with_hub['version']).to eq version
expect(RestClient).to have_received(:post).with(described_class.ping_url,
described_class.instance_config.to_json, { content_type: :json, accept: :json })
end
end
it 'returns nil when chatwoot hub is down' do
allow(RestClient).to receive(:post).and_raise(ExceptionList::REST_CLIENT_EXCEPTIONS.sample)
expect(described_class.sync_with_hub).to be_nil
end
end
context 'when register instance' do
let(:company_name) { 'test' }
let(:owner_name) { 'test' }
let(:owner_email) { 'test@test.com' }
it 'sends info of registration' do
info = { company_name: company_name, owner_name: owner_name, owner_email: owner_email, subscribed_to_mailers: true }
allow(RestClient).to receive(:post)
described_class.register_instance(company_name, owner_name, owner_email)
expect(RestClient).to have_received(:post).with(described_class.registration_url,
info.merge(described_class.instance_config).to_json, { content_type: :json, accept: :json })
end
end
context 'when sending events' do
let(:event_name) { 'sample_event' }
let(:event_data) { { 'sample_data' => 'sample_data' } }
it 'will send instance events' do
info = { event_name: event_name, event_data: event_data }
allow(RestClient).to receive(:post)
described_class.emit_event(event_name, event_data)
expect(RestClient).to have_received(:post).with(described_class.events_url,
info.merge(described_class.instance_config).to_json, { content_type: :json, accept: :json })
end
it 'will not send instance events when telemetry is disabled' do
with_modified_env DISABLE_TELEMETRY: 'true' do
info = { event_name: event_name, event_data: event_data }
allow(RestClient).to receive(:post)
described_class.emit_event(event_name, event_data)
expect(RestClient).not_to have_received(:post)
.with(described_class.events_url,
info.merge(described_class.instance_config).to_json, { content_type: :json, accept: :json })
end
end
end
end