feat(account): persist signup attribution (#14761)

This keeps Chatwoot-side attribution persistence intentionally small and
Enterprise-only.

The website owns attribution capture, normalization, and source
classification. Chatwoot Cloud only reads the already-shaped first-party
attribution cookies during the current web signup account creation path
and stores the decoded payload in internal account metadata. Self-hosted
installs remain unchanged in this repo.

## What changed

- Added Enterprise-only attribution persistence to the current Cloud web
signup account creation path.
- Stores attribution only when `ChatwootApp.chatwoot_cloud?` is true.
- Reads the existing first-touch and last-touch attribution cookies.
- Saves only the documented scalar attribution fields under account
internal metadata.
- Preserves raw attribution values and leaves escaping to display
boundaries.
- Bounds stored attribution values to the website field-size limit.
- Keeps OSS controller code unchanged.
- Keeps signup attribution request coverage in Enterprise specs.
- Avoids backend attribution derivation, referrer parsing, or fallback
classification.
- Skips authenticated add-workspace flows so additional workspaces are
not counted as signup attribution.
- Does not hook unused account creation paths or OmniAuth account
creation.

## How to test

- `bundle exec rubocop
spec/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts_controller_spec.rb
enterprise/app/controllers/enterprise/api/v1/accounts_settings.rb
enterprise/app/services/internal/accounts/marketing_attribution_service.rb
spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb`
- `bundle exec rspec spec/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/controllers/api/v1/accounts_controller_spec.rb
spec/enterprise/services/internal/accounts/marketing_attribution_service_spec.rb`
- On a cloud-like setup, create an account through the current web
signup path with attribution cookies and confirm account internal
metadata is populated.
- On a non-cloud setup or authenticated add-workspace flow, confirm
account-create behavior is unchanged and no attribution is stored.
This commit is contained in:
Sojan Jose
2026-06-19 11:01:12 -07:00
committed by GitHub
parent b02e732dd1
commit dfd656a7d9
5 changed files with 325 additions and 1 deletions
@@ -0,0 +1,66 @@
require 'rails_helper'
require 'base64'
RSpec.describe 'Enterprise Accounts API', type: :request do
describe 'POST /api/v1/accounts' do
let(:email) { Faker::Internet.email }
let(:user_full_name) { Faker::Name.name_with_middle }
let(:first_touch_cookie) { Base64.urlsafe_encode64({ source: 'reddit', source_type: 'paid_social' }.to_json, padding: false) }
let(:last_touch_cookie) { Base64.urlsafe_encode64({ source: 'github', source_type: 'referral' }.to_json, padding: false) }
let(:attribution_cookie_header) do
{
'Cookie' => [
"#{Internal::Accounts::MarketingAttributionService::FIRST_TOUCH_COOKIE}=#{first_touch_cookie}",
"#{Internal::Accounts::MarketingAttributionService::LAST_TOUCH_COOKIE}=#{last_touch_cookie}"
].join('; ')
}
end
before do
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
end
it 'records marketing attribution for unauthenticated signup requests' do
account_builder = double
account = create(:account)
user = create(:user, email: email, account: account, name: user_full_name)
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
attribution = account.reload.internal_attributes['marketing_attribution']
expect(attribution['captured_from']).to eq('cookie')
expect(attribution['first_touch']).to include('source' => 'reddit', 'source_type' => 'paid_social')
expect(attribution['last_touch']).to include('source' => 'github', 'source_type' => 'referral')
end
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
account = Account.find(response.parsed_body.dig('data', 'account_id'))
expect(account.internal_attributes).not_to include('marketing_attribution')
end
end
end