Skip to content

Commit a8f3e9c

Browse files
authored
Merge pull request #8697 from woocommerce/feat/8453-tracks-login
REST API: Track login events
2 parents 3b55efa + f779f92 commit a8f3e9c

File tree

7 files changed

+130
-16
lines changed

7 files changed

+130
-16
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1896,3 +1896,64 @@ extension WooAnalyticsEvent {
18961896
}
18971897
}
18981898
}
1899+
1900+
// MARK: - REST API Login
1901+
//
1902+
extension WooAnalyticsEvent {
1903+
enum Login {
1904+
enum Key: String {
1905+
case step
1906+
case currentRoles = "current_roles"
1907+
case exists
1908+
case hasWordPress = "is_wordpress"
1909+
case isWPCom = "is_wp_com"
1910+
case isJetpackInstalled = "has_jetpack"
1911+
case isJetpackActive = "is_jetpack_active"
1912+
case isJetpackConnected = "is_jetpack_connected"
1913+
case urlAfterRedirects = "url_after_redirects"
1914+
}
1915+
1916+
enum LoginSiteCredentialStep: String {
1917+
case authentication
1918+
case applicationPasswordGeneration = "application_password_generation"
1919+
case wooStatus = "woo_status"
1920+
case userRole = "user_role"
1921+
}
1922+
1923+
/// Tracks when the user attempts to log in with insufficient roles.
1924+
///
1925+
static func insufficientRole(currentRoles: [String]) -> WooAnalyticsEvent {
1926+
let roles = String(currentRoles.sorted().joined(by: ","))
1927+
return WooAnalyticsEvent(statName: .loginInsufficientRole,
1928+
properties: [Key.currentRoles.rawValue: roles])
1929+
}
1930+
1931+
/// Tracks when the login with site credentials failed.
1932+
///
1933+
static func siteCredentialFailed(step: LoginSiteCredentialStep, error: Error?) -> WooAnalyticsEvent {
1934+
WooAnalyticsEvent(statName: .loginSiteCredentialsFailed,
1935+
properties: [Key.step.rawValue: step.rawValue],
1936+
error: error)
1937+
}
1938+
1939+
/// Tracks when site info is fetched during site address login.
1940+
///
1941+
static func siteInfoFetched(exists: Bool,
1942+
hasWordPress: Bool,
1943+
isWPCom: Bool,
1944+
isJetpackInstalled: Bool,
1945+
isJetpackActive: Bool,
1946+
isJetpackConnected: Bool,
1947+
urlAfterRedirects: String) -> WooAnalyticsEvent {
1948+
.init(statName: .loginSiteAddressSiteInfoFetched, properties: [
1949+
Key.exists.rawValue: exists,
1950+
Key.hasWordPress.rawValue: hasWordPress,
1951+
Key.isWPCom.rawValue: isWPCom,
1952+
Key.isJetpackInstalled.rawValue: isJetpackInstalled,
1953+
Key.isJetpackActive.rawValue: isJetpackActive,
1954+
Key.isJetpackConnected.rawValue: isJetpackConnected,
1955+
Key.urlAfterRedirects.rawValue: urlAfterRedirects
1956+
])
1957+
}
1958+
}
1959+
}

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ public enum WooAnalyticsStat: String {
8282
case loginInvalidEmailScreenViewed = "login_invalid_email_screen_viewed"
8383
case whatIsWPComOnInvalidEmailScreenTapped = "what_is_wordpress_com_on_invalid_email_screen"
8484
case createAccountOnInvalidEmailScreenTapped = "create_account_on_invalid_email_screen"
85+
case loginInsufficientRole = "login_insufficient_role"
86+
87+
// MARK: REST API login
88+
//
89+
case loginSiteAddressSiteInfoFetched = "login_site_address_site_info_fetched"
90+
case loginSiteCredentialsFailed = "login_site_credentials_login_failed"
8591

8692
// MARK: Site credentials
8793
//

WooCommerce/Classes/Authentication/AuthenticationManager.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,17 @@ extension AuthenticationManager: WordPressAuthenticatorDelegate {
318318
/// and can proceed to the self-hosted username and password view controller.
319319
///
320320
func shouldPresentUsernamePasswordController(for siteInfo: WordPressComSiteInfo?, onCompletion: @escaping (WordPressAuthenticatorResult) -> Void) {
321+
if let site = siteInfo {
322+
analytics.track(event: .Login.siteInfoFetched(
323+
exists: site.exists,
324+
hasWordPress: site.isWP,
325+
isWPCom: site.isWPCom,
326+
isJetpackInstalled: site.hasJetpack,
327+
isJetpackActive: site.isJetpackActive,
328+
isJetpackConnected: site.isJetpackConnected,
329+
urlAfterRedirects: site.url
330+
))
331+
}
321332

322333
/// WordPress must be present.
323334
guard let site = siteInfo, site.isWP else {

WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ private extension StorePickerViewController {
846846
}
847847
case .failure(let error):
848848
if case let RoleEligibilityError.insufficientRole(errorInfo) = error {
849+
ServiceLocator.analytics.track(event: .Login.insufficientRole(currentRoles: errorInfo.roles))
849850
delegate.showRoleErrorScreen(for: site.siteID, errorInfo: errorInfo) { [weak self] in
850851
self?.dismiss()
851852
}

WooCommerce/Classes/Authentication/PostSiteCredentialLoginChecker.swift

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ final class PostSiteCredentialLoginChecker {
1212
private let stores: StoresManager
1313
private let applicationPasswordUseCase: ApplicationPasswordUseCase
1414
private let roleEligibilityUseCase: RoleEligibilityUseCaseProtocol
15+
private let analytics: Analytics
1516

1617
init(applicationPasswordUseCase: ApplicationPasswordUseCase,
1718
roleEligibilityUseCase: RoleEligibilityUseCaseProtocol = RoleEligibilityUseCase(stores: ServiceLocator.stores),
18-
stores: StoresManager = ServiceLocator.stores) {
19+
stores: StoresManager = ServiceLocator.stores,
20+
analytics: Analytics = ServiceLocator.analytics) {
1921
self.applicationPasswordUseCase = applicationPasswordUseCase
2022
self.roleEligibilityUseCase = roleEligibilityUseCase
2123
self.stores = stores
24+
self.analytics = analytics
2225
}
2326

2427
/// Checks whether the user is eligible to use the app.
@@ -44,21 +47,25 @@ private extension PostSiteCredentialLoginChecker {
4447
do {
4548
let _ = try await useCase.generateNewPassword()
4649
onSuccess()
47-
} catch ApplicationPasswordUseCaseError.applicationPasswordsDisabled {
48-
// show application password disabled error
49-
let errorUI = applicationPasswordDisabledUI(for: siteURL)
50-
navigationController.show(errorUI, sender: nil)
51-
} catch ApplicationPasswordUseCaseError.unauthorizedRequest {
52-
showAlert(message: Localization.invalidLoginOrAdminURL, in: navigationController)
5350
} catch {
54-
DDLogError("⛔️ Error generating application password: \(error)")
55-
showAlert(
56-
message: Localization.applicationPasswordError,
57-
in: navigationController,
58-
onRetry: { [weak self] in
59-
self?.checkApplicationPassword(for: siteURL, with: useCase, in: navigationController, onSuccess: onSuccess)
60-
}
61-
)
51+
analytics.track(event: .Login.siteCredentialFailed(step: .applicationPasswordGeneration, error: error))
52+
switch error {
53+
case ApplicationPasswordUseCaseError.applicationPasswordsDisabled:
54+
// show application password disabled error
55+
let errorUI = applicationPasswordDisabledUI(for: siteURL)
56+
navigationController.show(errorUI, sender: nil)
57+
case ApplicationPasswordUseCaseError.unauthorizedRequest:
58+
showAlert(message: Localization.invalidLoginOrAdminURL, in: navigationController)
59+
default:
60+
DDLogError("⛔️ Error generating application password: \(error)")
61+
showAlert(
62+
message: Localization.applicationPasswordError,
63+
in: navigationController,
64+
onRetry: { [weak self] in
65+
self?.checkApplicationPassword(for: siteURL, with: useCase, in: navigationController, onSuccess: onSuccess)
66+
}
67+
)
68+
}
6269
}
6370
}
6471
}
@@ -72,7 +79,9 @@ private extension PostSiteCredentialLoginChecker {
7279
case .success:
7380
onSuccess()
7481
case .failure(let error):
82+
self?.analytics.track(event: .Login.siteCredentialFailed(step: .userRole, error: error))
7583
if case let RoleEligibilityError.insufficientRole(errorInfo) = error {
84+
self?.analytics.track(event: .Login.insufficientRole(currentRoles: errorInfo.roles))
7685
self?.showRoleErrorScreen(for: WooConstants.placeholderStoreID,
7786
errorInfo: errorInfo,
7887
in: navigationController,
@@ -119,9 +128,11 @@ private extension PostSiteCredentialLoginChecker {
119128
if site.isWooCommerceActive {
120129
onSuccess()
121130
} else {
131+
self?.analytics.track(event: .Login.siteCredentialFailed(step: .wooStatus, error: nil))
122132
self?.showAlert(message: Localization.noWooError, in: navigationController)
123133
}
124134
case .failure(let error):
135+
self?.analytics.track(event: .Login.siteCredentialFailed(step: .wooStatus, error: error))
125136
DDLogError("⛔️ Error checking Woo: \(error)")
126137
// show generic error
127138
self?.showAlert(message: Localization.wooCheckError, in: navigationController, onRetry: {

WooCommerce/Classes/ViewRelated/AppCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ private extension AppCoordinator {
276276

277277
// this needs to be wrapped within a navigation controller to properly show the right bar button for Help.
278278
setWindowRootViewControllerAndAnimateIfNeeded(WooNavigationController(rootViewController: errorViewController))
279+
ServiceLocator.analytics.track(event: .Login.insufficientRole(currentRoles: errorInfo.roles))
279280
}
280281

281282
/// Synchronously check if there's any `EligibilityErrorInfo` stored locally. If there is, then let's show the role error UI instead.

WooCommerce/WooCommerceTests/Authentication/AuthenticationManagerTests.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,10 +456,33 @@ final class AuthenticationManagerTests: XCTestCase {
456456
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["is_jetpack_active"] as? Bool))
457457
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["is_jetpack_connected"] as? Bool))
458458
}
459+
460+
func test_shouldPresentUsernamePasswordController_tracks_fetched_site_info() throws {
461+
// Given
462+
let navigationController = UINavigationController()
463+
let analyticsProvider = MockAnalyticsProvider()
464+
let analytics = WooAnalytics(analyticsProvider: analyticsProvider)
465+
466+
let siteInfo = siteInfo(exists: true, hasWordPress: true, isWordPressCom: true, hasJetpack: true, isJetpackActive: true, isJetpackConnected: true)
467+
let storage = MockStorageManager()
468+
let manager = AuthenticationManager(storageManager: storage, analytics: analytics)
469+
470+
// When
471+
manager.shouldPresentUsernamePasswordController(for: siteInfo) { _ in }
472+
473+
// Then
474+
XCTAssertEqual(analyticsProvider.receivedEvents, [WooAnalyticsStat.loginSiteAddressSiteInfoFetched.rawValue])
475+
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["is_wordpress"] as? Bool))
476+
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["is_wp_com"] as? Bool))
477+
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["has_jetpack"] as? Bool))
478+
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["is_jetpack_active"] as? Bool))
479+
XCTAssertTrue(try XCTUnwrap(analyticsProvider.receivedProperties.first?["is_jetpack_connected"] as? Bool))
480+
XCTAssertEqual(analyticsProvider.receivedProperties.first?["url_after_redirects"] as? String, siteInfo.url)
481+
}
459482
}
460483

461484
private extension AuthenticationManagerTests {
462-
func siteInfo(url: String = "",
485+
func siteInfo(url: String = "https://test.com",
463486
exists: Bool = false,
464487
hasWordPress: Bool = false,
465488
isWordPressCom: Bool = false,

0 commit comments

Comments
 (0)