fix(auth): record attribution for oauth signups (#14796)

Google OAuth signups now persist the same first-party attribution
cookies as email signups on Chatwoot Cloud.

This keeps attribution capture owned by the website and reuses the
existing Enterprise-only account attribution service. The OAuth callback
only records the already-shaped first-touch and last-touch cookie
payload after a new account is created.

## What changed

- Added Enterprise-only attribution persistence to the Google OAuth
signup account creation path.
- Reuses `Internal::Accounts::MarketingAttributionService`.
- Keeps attribution best-effort so failures do not interrupt OAuth
signup.
- Leaves existing email signup and SAML behavior unchanged.

## How to test

- Start from a Chatwoot Cloud-like setup with attribution cookies
present.
- Sign up using the Google OAuth button.
- Confirm the created account has
`internal_attributes['marketing_attribution']` with `first_touch` and
`last_touch`.
- Confirm existing Google OAuth login still redirects normally.

Validation run locally:

- `bundle exec rspec
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
- `bundle exec rspec
spec/controllers/devise/omniauth_callbacks_controller_spec.rb
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
- `bundle exec rubocop
enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb
spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb`
This commit is contained in:
Sojan Jose
2026-06-22 06:51:09 -07:00
committed by GitHub
parent 2b37fef0e0
commit e86222034e
2 changed files with 66 additions and 0 deletions
@@ -29,6 +29,19 @@ module Enterprise::DeviseOverrides::OmniauthCallbacksController
private
def create_account_for_user
super
record_marketing_attribution
end
def record_marketing_attribution
return if @account.blank?
Internal::Accounts::MarketingAttributionService.new(account: @account, cookies: cookies).perform
rescue StandardError => e
ChatwootExceptionTracker.new(e).capture_exception
end
def handle_saml_auth
account_id = extract_saml_account_id
relay_state = saml_relay_state
@@ -0,0 +1,53 @@
require 'rails_helper'
require 'base64'
RSpec.describe 'Enterprise Google OAuth attribution', type: :request do
let(:email_validation_service) { instance_double(Account::SignUpEmailValidationService) }
let(:email) { 'oauth-attribution@example.com' }
let(:account_builder) { double }
let(:account) { create(:account) }
let(:first_touch_cookie) { encoded_cookie('source' => 'reddit', 'source_type' => 'paid_social') }
let(:last_touch_cookie) { encoded_cookie('source' => 'github', 'source_type' => 'referral') }
before do
allow(ChatwootApp).to receive(:enterprise?).and_return(true)
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
allow(Account::SignUpEmailValidationService).to receive(:new).and_return(email_validation_service)
allow(email_validation_service).to receive(:perform).and_return(true)
allow(AccountBuilder).to receive(:new).and_return(account_builder)
allow(account_builder).to receive(:perform) do
[create(:user, email: email, account: account), account]
end
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
provider: 'google',
uid: '123545',
info: {
name: 'OAuth Attribution',
email: email,
image: 'https://example.com/image.jpg'
}
)
end
it 'records marketing attribution for Google OAuth signups' do
cookies[Internal::Accounts::MarketingAttributionService::FIRST_TOUCH_COOKIE] = first_touch_cookie
cookies[Internal::Accounts::MarketingAttributionService::LAST_TOUCH_COOKIE] = last_touch_cookie
with_modified_env ENABLE_ACCOUNT_SIGNUP: 'true', FRONTEND_URL: 'http://www.example.com' do
get '/omniauth/google_oauth2/callback'
follow_redirect!
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
def encoded_cookie(payload)
Base64.urlsafe_encode64(payload.to_json, padding: false)
end
end