Skip to content

Commit 5783dd3

Browse files
authored
feat: add analytics tracking for social login failures (#22182)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** This PR adds analytics tracking for social login failures to help identify where and why failures occur during the OAuth login flow. The implementation tracks failures at three stages: 1. **Provider login failures** - When users cancel or encounter errors during the OAuth provider authentication step 2. **Token exchange failures** - When errors occur during the authorization code to JWT token exchange 3. **Seedless authentication failures** - When errors occur during the seedless authentication step Each failure event includes contextual metadata: - `account_type` - The type of account being created (e.g., `default_google`, `default_apple`) - `is_rehydration` - Whether this is a rehydration flow (existing user returning) - `failure_type` - Whether the failure was due to user cancellation or an error - `error_category` - The specific stage where the failure occurred ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/SL-257 ## **Manual testing steps** 1. Start a social login flow (Google or Apple) 2. Cancel the OAuth provider authentication dialog 3. Verify that a `SocialLoginFailed` event is tracked with `failure_type: 'user_cancelled'` and `error_category: 'provider_login'` 4. Attempt a social login and simulate a network error during token exchange 5. Verify that a `SocialLoginFailed` event is tracked with `failure_type: 'error'` and `error_category: 'get_auth_tokens'` 6. Attempt a social login and simulate an error during seedless authentication 7. Verify that a `SocialLoginFailed` event is tracked with `failure_type: 'error'` and `error_category: 'seedless_auth'` ## **Screenshots/Recordings** <!-- Not applicable - this is an analytics-only change with no UI changes --> ### **Before** <!-- N/A --> ### **After** <!-- N/A --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds SOCIAL_LOGIN_FAILED tracking with rehydration context across provider login, token exchange, and seedless auth; updates OAuth login API and tests accordingly. > > - **Analytics**: > - Add `SOCIAL_LOGIN_FAILED` event in `core/Analytics/MetaMetrics.events.ts` and export mapping. > - **OAuth** (`core/OAuthService/OAuthService.ts`): > - Add `userClickedRehydration` to local state and `#trackSocialLoginFailure` to emit `SOCIAL_LOGIN_FAILED` with `account_type`, `is_rehydration`, `failure_type`, and `error_category`. > - Invoke tracking on failures in `provider_login`, `get_auth_tokens`, and `seedless_auth` stages; validate `id_token` earlier. > - Change `handleOAuthLogin(loginHandler, userClickedRehydration)` signature and wire usage. > - **Onboarding** (`components/Views/Onboarding/index.js`): > - Pass rehydration flag to `handleOAuthLogin` (`!createWallet`). > - **Tests**: > - Update unit tests in `OAuthService.test.ts` and `Onboarding/index.test.tsx` to reflect new `handleOAuthLogin` param and event behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 37e8b08. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent af57390 commit 5783dd3

File tree

5 files changed

+81
-10
lines changed

5 files changed

+81
-10
lines changed

app/components/Views/Onboarding/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,7 @@ class Onboarding extends PureComponent {
625625
const loginHandler = createLoginHandler(Platform.OS, provider);
626626
const result = await OAuthLoginService.handleOAuthLogin(
627627
loginHandler,
628+
!createWallet,
628629
).catch((error) => {
629630
this.props.unsetLoading();
630631
this.handleLoginError(error, provider);

app/components/Views/Onboarding/index.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,7 @@ describe('Onboarding', () => {
744744
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'google');
745745
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
746746
'mockGoogleHandler',
747+
false,
747748
);
748749
expect(mockNavigate).toHaveBeenCalledWith(
749750
Routes.ONBOARDING.SOCIAL_LOGIN_SUCCESS_NEW_USER,
@@ -794,6 +795,7 @@ describe('Onboarding', () => {
794795
expect(mockCreateLoginHandler).toHaveBeenCalledWith('android', 'google');
795796
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
796797
'mockGoogleHandler',
798+
false,
797799
);
798800
// On Android, should navigate directly to ChoosePassword, not SocialLoginSuccessNewUser
799801
expect(mockNavigate).toHaveBeenCalledWith(
@@ -847,6 +849,7 @@ describe('Onboarding', () => {
847849
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'apple');
848850
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
849851
'mockAppleHandler',
852+
false,
850853
);
851854
// On iOS with Apple login, should navigate to SocialLoginSuccessNewUser
852855
expect(mockNavigate).toHaveBeenCalledWith(
@@ -897,6 +900,7 @@ describe('Onboarding', () => {
897900
expect(mockCreateLoginHandler).toHaveBeenCalledWith('ios', 'apple');
898901
expect(mockOAuthService.handleOAuthLogin).toHaveBeenCalledWith(
899902
'mockAppleHandler',
903+
true,
900904
);
901905
expect(mockNavigate).toHaveBeenCalledWith(
902906
Routes.ONBOARDING.SOCIAL_LOGIN_SUCCESS_EXISTING_USER,

app/core/Analytics/MetaMetrics.events.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ enum EVENT_NAME {
138138
WALLET_SETUP_FAILURE = 'Wallet Setup Failure',
139139
WALLET_SETUP_COMPLETED = 'Wallet Setup Completed',
140140
SOCIAL_LOGIN_COMPLETED = 'Social Login Completed',
141+
SOCIAL_LOGIN_FAILED = 'Social Login Failed',
141142
ACCOUNT_ALREADY_EXISTS_PAGE_VIEWED = 'Account Already Exists Page Viewed',
142143
ACCOUNT_NOT_FOUND_PAGE_VIEWED = 'Account Not Found Page Viewed',
143144
REHYDRATION_PASSWORD_ATTEMPTED = 'Rehydration Password Attempted',
@@ -756,6 +757,7 @@ const events = {
756757
WALLET_SETUP_FAILURE: generateOpt(EVENT_NAME.WALLET_SETUP_FAILURE),
757758
WALLET_SETUP_COMPLETED: generateOpt(EVENT_NAME.WALLET_SETUP_COMPLETED),
758759
SOCIAL_LOGIN_COMPLETED: generateOpt(EVENT_NAME.SOCIAL_LOGIN_COMPLETED),
760+
SOCIAL_LOGIN_FAILED: generateOpt(EVENT_NAME.SOCIAL_LOGIN_FAILED),
759761
ACCOUNT_ALREADY_EXISTS_PAGE_VIEWED: generateOpt(
760762
EVENT_NAME.ACCOUNT_ALREADY_EXISTS_PAGE_VIEWED,
761763
),

app/core/OAuthService/OAuthService.test.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ describe('OAuth login service', () => {
131131

132132
it('return a type success', async () => {
133133
const loginHandler = mockCreateLoginHandler();
134-
const result = (await OAuthLoginService.handleOAuthLogin(loginHandler)) as {
134+
const result = (await OAuthLoginService.handleOAuthLogin(
135+
loginHandler,
136+
false,
137+
)) as {
135138
type: string;
136139
existingUser: boolean;
137140
};
@@ -154,7 +157,10 @@ describe('OAuth login service', () => {
154157
.spyOn(Engine.context.SeedlessOnboardingController, 'authenticate')
155158
.mockImplementation(mockAuthenticate);
156159

157-
const result = await OAuthLoginService.handleOAuthLogin(loginHandler);
160+
const result = await OAuthLoginService.handleOAuthLogin(
161+
loginHandler,
162+
false,
163+
);
158164
expect(result).toBeDefined();
159165

160166
expect(mockLoginHandlerResponse).toHaveBeenCalledTimes(1);
@@ -172,7 +178,7 @@ describe('OAuth login service', () => {
172178
.mockImplementation(mockAuthenticate);
173179

174180
await expectOAuthError(
175-
OAuthLoginService.handleOAuthLogin(loginHandler),
181+
OAuthLoginService.handleOAuthLogin(loginHandler, false),
176182
OAuthErrorType.LoginError,
177183
);
178184

@@ -188,7 +194,7 @@ describe('OAuth login service', () => {
188194
const loginHandler = mockCreateLoginHandler();
189195

190196
await expectOAuthError(
191-
OAuthLoginService.handleOAuthLogin(loginHandler),
197+
OAuthLoginService.handleOAuthLogin(loginHandler, false),
192198
OAuthErrorType.AuthServerError,
193199
);
194200

@@ -209,7 +215,7 @@ describe('OAuth login service', () => {
209215
});
210216

211217
await expectOAuthError(
212-
OAuthLoginService.handleOAuthLogin(loginHandler),
218+
OAuthLoginService.handleOAuthLogin(loginHandler, false),
213219
OAuthErrorType.UserDismissed,
214220
);
215221

@@ -225,7 +231,7 @@ describe('OAuth login service', () => {
225231
});
226232

227233
await expectOAuthError(
228-
OAuthLoginService.handleOAuthLogin(loginHandler),
234+
OAuthLoginService.handleOAuthLogin(loginHandler, false),
229235
OAuthErrorType.LoginError,
230236
);
231237

app/core/OAuthService/OAuthService.ts

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
import { OAuthError, OAuthErrorType } from './error';
2121
import { BaseLoginHandler } from './OAuthLoginHandlers/baseHandler';
2222
import { Platform } from 'react-native';
23+
import { MetaMetrics } from '../Analytics';
24+
import { MetricsEventBuilder } from '../Analytics/MetricsEventBuilder';
25+
import { MetaMetricsEvents } from '../Analytics/MetaMetrics.events';
2326

2427
export interface MarketingOptInRequest {
2528
opt_in_status: boolean;
@@ -48,6 +51,7 @@ interface OAuthServiceLocalState {
4851
loginInProgress: boolean;
4952
oauthLoginSuccess: boolean;
5053
oauthLoginError: string | null;
54+
userClickedRehydration?: boolean;
5155
}
5256
export class OAuthService {
5357
public localState: OAuthServiceLocalState;
@@ -140,8 +144,44 @@ export class OAuthService {
140144
}
141145
};
142146

147+
#trackSocialLoginFailure = ({
148+
authConnection,
149+
errorCategory,
150+
error,
151+
}: {
152+
authConnection: AuthConnection;
153+
errorCategory: 'provider_login' | 'get_auth_tokens' | 'seedless_auth';
154+
error: unknown;
155+
}) => {
156+
const isUserCancelled =
157+
error instanceof OAuthError &&
158+
(error.code === OAuthErrorType.UserCancelled ||
159+
error.code === OAuthErrorType.UserDismissed);
160+
161+
let userClickedRehydration: 'true' | 'false' | 'unknown' = 'unknown';
162+
if (this.localState.userClickedRehydration !== undefined) {
163+
userClickedRehydration = this.localState.userClickedRehydration
164+
? 'true'
165+
: 'false';
166+
}
167+
168+
MetaMetrics.getInstance().trackEvent(
169+
MetricsEventBuilder.createEventBuilder(
170+
MetaMetricsEvents.SOCIAL_LOGIN_FAILED,
171+
)
172+
.addProperties({
173+
account_type: `default_${authConnection}`,
174+
is_rehydration: userClickedRehydration,
175+
failure_type: isUserCancelled ? 'user_cancelled' : 'error',
176+
error_category: errorCategory,
177+
})
178+
.build(),
179+
);
180+
};
181+
143182
handleOAuthLogin = async (
144183
loginHandler: BaseLoginHandler,
184+
userClickedRehydration: boolean,
145185
): Promise<HandleOAuthLoginResult> => {
146186
const web3AuthNetwork = this.config.web3AuthNetwork;
147187

@@ -151,6 +191,7 @@ export class OAuthService {
151191
OAuthErrorType.LoginInProgress,
152192
);
153193
}
194+
this.updateLocalState({ userClickedRehydration });
154195
this.#dispatchLogin();
155196

156197
try {
@@ -174,6 +215,12 @@ export class OAuthService {
174215
});
175216
endTrace({ name: TraceName.OnboardingOAuthProviderLoginError });
176217

218+
this.#trackSocialLoginFailure({
219+
authConnection: loginHandler.authConnection,
220+
errorCategory: 'provider_login',
221+
error,
222+
});
223+
177224
throw error;
178225
} finally {
179226
endTrace({
@@ -196,6 +243,9 @@ export class OAuthService {
196243
{ ...result, web3AuthNetwork },
197244
this.config.authServerUrl,
198245
);
246+
if (!data.id_token) {
247+
throw new OAuthError('No token found', OAuthErrorType.LoginError);
248+
}
199249
getAuthTokensSuccess = true;
200250
} catch (error) {
201251
const errorMessage =
@@ -210,6 +260,12 @@ export class OAuthService {
210260
name: TraceName.OnboardingOAuthBYOAServerGetAuthTokensError,
211261
});
212262

263+
this.#trackSocialLoginFailure({
264+
authConnection,
265+
errorCategory: 'get_auth_tokens',
266+
error,
267+
});
268+
213269
throw error;
214270
} finally {
215271
endTrace({
@@ -218,10 +274,6 @@ export class OAuthService {
218274
});
219275
}
220276

221-
if (!data.id_token) {
222-
throw new OAuthError('No token found', OAuthErrorType.LoginError);
223-
}
224-
225277
const jwtPayload = JSON.parse(
226278
loginHandler.decodeIdToken(data.id_token),
227279
) as Partial<OAuthUserInfo>;
@@ -257,6 +309,12 @@ export class OAuthService {
257309
name: TraceName.OnboardingOAuthSeedlessAuthenticateError,
258310
});
259311

312+
this.#trackSocialLoginFailure({
313+
authConnection,
314+
errorCategory: 'seedless_auth',
315+
error,
316+
});
317+
260318
throw error;
261319
} finally {
262320
endTrace({

0 commit comments

Comments
 (0)