From e86222034e39b9be4837fea0c058ad9a6a27aa72 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 22 Jun 2026 06:51:09 -0700 Subject: [PATCH] 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` --- .../omniauth_callbacks_controller.rb | 13 +++++ .../google_oauth_attribution_spec.rb | 53 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb diff --git a/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb b/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb index 3e2713dd7..0b1328c0d 100644 --- a/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb +++ b/enterprise/app/controllers/enterprise/devise_overrides/omniauth_callbacks_controller.rb @@ -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 diff --git a/spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb b/spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb new file mode 100644 index 000000000..948e9af96 --- /dev/null +++ b/spec/enterprise/controllers/enterprise/devise_overrides/google_oauth_attribution_spec.rb @@ -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