chore: Track cloud signup conversions (#14852)

## Summary
- enqueue `cloud_signup` conversion tracking after website attribution
is stored on a new cloud account
- keep authenticated add-workspace requests out of
attribution/conversion tracking
- reuse the existing marketing conversion tracking service and config
already available on `develop`

## Notes
- This PR only wires the signup path.
- Billing/plan activation conversion tracking is intentionally left out
for a separate rollout.
- The conversion service still no-ops outside Chatwoot Cloud and when
stored attribution has no supported click identifier.
This commit is contained in:
Sojan Jose
2026-06-24 20:04:42 -07:00
committed by GitHub
parent cd1a214d98
commit 0849d2e070
5 changed files with 45 additions and 22 deletions
@@ -50,6 +50,7 @@ class Internal::Accounts::MarketingAttributionService
'stored_at' => Time.current.iso8601
}.compact
)
enqueue_signup_conversion
end
private
@@ -79,4 +80,8 @@ class Internal::Accounts::MarketingAttributionService
def internal_attributes_service
@internal_attributes_service ||= Internal::Accounts::InternalAttributesService.new(account)
end
def enqueue_signup_conversion
Internal::Accounts::MarketingConversionTrackingJob.perform_later(account.id, 'cloud_signup', account.created_at)
end
end
@@ -58,7 +58,7 @@ class Internal::Accounts::MarketingConversionTrackingService
def conversion_payload
payload = {
transactionId: "#{event_name}-account-#{account.id}",
eventTimestamp: (occurred_at.present? ? Time.zone.parse(occurred_at.to_s) : Time.current).iso8601,
eventTimestamp: event_timestamp.iso8601,
eventSource: 'WEB',
adIdentifiers: click_attributes
}
@@ -78,6 +78,10 @@ class Internal::Accounts::MarketingConversionTrackingService
end.to_h
end
def event_timestamp
occurred_at || Time.current
end
def attribution
marketing_attribution = account.internal_attributes['marketing_attribution'] || {}
[marketing_attribution['last_touch'], marketing_attribution['first_touch']].find do |touch|
@@ -28,19 +28,22 @@ RSpec.describe 'Enterprise Accounts API', type: :request do
allow(AccountBuilder).to receive(:new).and_return(account_builder)
allow(account_builder).to receive(:perform).and_return([user, account])
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
post api_v1_accounts_url,
params: {
account_name: 'test',
email: email,
user: nil,
locale: nil,
user_full_name: user_full_name,
password: 'Password1!'
},
headers: attribution_cookie_header,
as: :json
end
expect do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
post api_v1_accounts_url,
params: {
account_name: 'test',
email: email,
user: nil,
locale: nil,
user_full_name: user_full_name,
password: 'Password1!'
},
headers: attribution_cookie_header,
as: :json
end
end.to have_enqueued_job(Internal::Accounts::MarketingConversionTrackingJob)
.with(account.id, 'cloud_signup', account.created_at)
attribution = account.reload.internal_attributes['marketing_attribution']
expect(attribution['captured_from']).to eq('cookie')
@@ -51,13 +54,15 @@ RSpec.describe 'Enterprise Accounts API', type: :request do
it 'does not record marketing attribution for authenticated add-workspace requests' do
existing_user = create(:user, password: 'Password1!')
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
post api_v1_accounts_url,
params: { account_name: 'Second Account', email: existing_user.email,
user_full_name: existing_user.name, password: 'Password1!' },
headers: existing_user.create_new_auth_token.merge(attribution_cookie_header),
as: :json
end
expect do
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true' do
post api_v1_accounts_url,
params: { account_name: 'Second Account', email: existing_user.email,
user_full_name: existing_user.name, password: 'Password1!' },
headers: existing_user.create_new_auth_token.merge(attribution_cookie_header),
as: :json
end
end.not_to have_enqueued_job(Internal::Accounts::MarketingConversionTrackingJob)
account = Account.find(response.parsed_body.dig('data', 'account_id'))
expect(account.internal_attributes).not_to include('marketing_attribution')
@@ -32,6 +32,15 @@ RSpec.describe Internal::Accounts::MarketingAttributionService do
expect(attribution['last_touch']['source']).to eq('github')
end
it 'enqueues signup conversion tracking after storing attribution' do
cookies[described_class::LAST_TOUCH_COOKIE] = encoded_cookie('source' => 'github')
expect do
described_class.new(account: account, cookies: cookies).perform
end.to have_enqueued_job(Internal::Accounts::MarketingConversionTrackingJob)
.with(account.id, 'cloud_signup', account.created_at)
end
it 'does not store attribution outside Chatwoot Cloud' do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
cookies[described_class::LAST_TOUCH_COOKIE] = encoded_cookie('source' => 'reddit')
@@ -5,7 +5,7 @@ require 'rails_helper'
RSpec.describe Internal::Accounts::MarketingConversionTrackingService do
let(:account) { create(:account) }
let(:event_name) { 'cloud_signup' }
let(:occurred_at) { '2026-06-23T10:30:00Z' }
let(:occurred_at) { Time.zone.parse('2026-06-23T10:30:00Z') }
let(:private_key) { OpenSSL::PKey::RSA.new(2048).to_pem }
let(:credentials) do
instance_double(Google::Auth::ServiceAccountCredentials, fetch_access_token!: { 'access_token' => 'access-token' })