Skip to content

Commit 288d41f

Browse files
Merge pull request #8732 from woocommerce/feat/8453-track-application-password
[REST API] Track application password events
2 parents 19b2742 + 470bc61 commit 288d41f

File tree

9 files changed

+369
-3
lines changed

9 files changed

+369
-3
lines changed

Networking/Networking/ApplicationPassword/RequestProcessor.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ final class RequestProcessor {
1010

1111
private let requestAuthenticator: RequestAuthenticator
1212

13-
init(requestAuthenticator: RequestAuthenticator) {
13+
private let notificationCenter: NotificationCenter
14+
15+
init(requestAuthenticator: RequestAuthenticator,
16+
notificationCenter: NotificationCenter = .default) {
1417
self.requestAuthenticator = requestAuthenticator
18+
self.notificationCenter = notificationCenter
1519
}
1620
}
1721

@@ -56,9 +60,17 @@ private extension RequestProcessor {
5660
do {
5761
let _ = try await requestAuthenticator.generateApplicationPassword()
5862
isAuthenticating = false
63+
64+
// Post a notification for tracking
65+
notificationCenter.post(name: .ApplicationPasswordsNewPasswordCreated, object: nil, userInfo: nil)
66+
5967
completeRequests(true)
6068
} catch {
6169
isAuthenticating = false
70+
71+
// Post a notification for tracking
72+
notificationCenter.post(name: .ApplicationPasswordsGenerationFailed, object: error, userInfo: nil)
73+
6274
completeRequests(false)
6375
}
6476
}
@@ -85,3 +97,15 @@ private extension RequestProcessor {
8597
requestsToRetry.removeAll()
8698
}
8799
}
100+
101+
// MARK: - Application Password Notifications
102+
//
103+
public extension NSNotification.Name {
104+
/// Posted whenever a new password was created when a regeneration is needed.
105+
///
106+
static let ApplicationPasswordsNewPasswordCreated = NSNotification.Name(rawValue: "ApplicationPasswordsNewPasswordCreated")
107+
108+
/// Posted when generating an application password fails
109+
///
110+
static let ApplicationPasswordsGenerationFailed = NSNotification.Name(rawValue: "ApplicationPasswordsGenerationFailed")
111+
}

Networking/NetworkingTests/ApplicationPassword/RequestProcessorTests.swift

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ final class RequestProcessorTests: XCTestCase {
88
private var mockRequestAuthenticator: MockRequestAuthenticator!
99
private var sut: RequestProcessor!
1010
private var sessionManager: Alamofire.SessionManager!
11+
private var mockNotificationCenter: MockNotificationCenter!
1112

1213
private let url = URL(string: "https://test.com/")!
1314

@@ -16,13 +17,16 @@ final class RequestProcessorTests: XCTestCase {
1617

1718
sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
1819
mockRequestAuthenticator = MockRequestAuthenticator()
19-
sut = RequestProcessor(requestAuthenticator: mockRequestAuthenticator)
20+
mockNotificationCenter = MockNotificationCenter()
21+
sut = RequestProcessor(requestAuthenticator: mockRequestAuthenticator,
22+
notificationCenter: mockNotificationCenter)
2023
}
2124

2225
override func tearDown() {
2326
sut = nil
2427
mockRequestAuthenticator = nil
2528
sessionManager = nil
29+
mockNotificationCenter = nil
2630

2731
super.tearDown()
2832
}
@@ -205,6 +209,68 @@ final class RequestProcessorTests: XCTestCase {
205209
// Then
206210
XCTAssertFalse(mockRequestAuthenticator.generateApplicationPasswordCalled)
207211
}
212+
213+
// MARK: Notification center
214+
//
215+
func test_notification_is_posted_when_application_password_generation_is_successful() throws {
216+
// Given
217+
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
218+
let request = try mockRequest()
219+
220+
// When
221+
let error = RequestAuthenticatorError.applicationPasswordNotAvailable
222+
waitFor { promise in
223+
self.sut.should(sessionManager, retry: request, with: error) { shouldRetry, timeDelay in
224+
promise(())
225+
}
226+
}
227+
228+
// Then
229+
waitUntil {
230+
self.mockNotificationCenter.notificationName == .ApplicationPasswordsNewPasswordCreated
231+
}
232+
}
233+
234+
func test_notification_is_posted_when_application_password_generation_fails() throws {
235+
// Given
236+
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
237+
let request = try mockRequest()
238+
mockRequestAuthenticator.mockErrorWhileGeneratingPassword = ApplicationPasswordUseCaseError.applicationPasswordsDisabled
239+
240+
// When
241+
let error = RequestAuthenticatorError.applicationPasswordNotAvailable
242+
waitFor { promise in
243+
self.sut.should(sessionManager, retry: request, with: error) { shouldRetry, timeDelay in
244+
promise(())
245+
}
246+
}
247+
248+
// Then
249+
waitUntil {
250+
self.mockNotificationCenter.notificationName == .ApplicationPasswordsGenerationFailed
251+
}
252+
}
253+
254+
func test_posted_notification_holds_expected_error_when_application_password_generation_fails() throws {
255+
// Given
256+
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
257+
let request = try mockRequest()
258+
let applicationPasswordGenerationError = ApplicationPasswordUseCaseError.applicationPasswordsDisabled
259+
mockRequestAuthenticator.mockErrorWhileGeneratingPassword = applicationPasswordGenerationError
260+
261+
// When
262+
let error = RequestAuthenticatorError.applicationPasswordNotAvailable
263+
waitFor { promise in
264+
self.sut.should(sessionManager, retry: request, with: error) { shouldRetry, timeDelay in
265+
promise(())
266+
}
267+
}
268+
269+
// Then
270+
waitUntil {
271+
(self.mockNotificationCenter.notificationObject as? ApplicationPasswordUseCaseError) == applicationPasswordGenerationError
272+
}
273+
}
208274
}
209275

210276
// MARK: Helpers
@@ -234,16 +300,31 @@ private class MockRequestAuthenticator: RequestAuthenticator {
234300

235301
var credentials: Networking.Credentials? = nil
236302

303+
var mockErrorWhileGeneratingPassword: Error?
304+
237305
func authenticate(_ urlRequest: URLRequest) throws -> URLRequest {
238306
authenticateCalled = true
239307
return urlRequest
240308
}
241309

242310
func generateApplicationPassword() async throws {
243311
generateApplicationPasswordCalled = true
312+
if let mockErrorWhileGeneratingPassword {
313+
throw mockErrorWhileGeneratingPassword
314+
}
244315
}
245316

246317
func shouldRetry(_ urlRequest: URLRequest) -> Bool {
247318
mockedShouldRetryValue ?? true
248319
}
249320
}
321+
322+
private class MockNotificationCenter: NotificationCenter {
323+
private(set) var notificationName: NSNotification.Name?
324+
private(set) var notificationObject: Any?
325+
326+
override func post(name aName: NSNotification.Name, object anObject: Any?, userInfo aUserInfo: [AnyHashable: Any]? = nil) {
327+
notificationName = aName
328+
notificationObject = anObject
329+
}
330+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import Foundation
2+
3+
final class TrackEventRequestNotificationHandler {
4+
5+
/// NotificationCenter Tokens for tracking Analytics event
6+
///
7+
private var trackingObservers: [NSObjectProtocol]?
8+
9+
/// NotificationCenter
10+
///
11+
private let notificationCenter: NotificationCenter
12+
13+
/// Analytics
14+
///
15+
private let analytics: Analytics
16+
17+
init(notificationCenter: NotificationCenter = .default,
18+
analytics: Analytics = ServiceLocator.analytics) {
19+
self.notificationCenter = notificationCenter
20+
self.analytics = analytics
21+
startListeningToNotifications()
22+
}
23+
}
24+
25+
private extension TrackEventRequestNotificationHandler {
26+
/// Starts listening for Notifications
27+
///
28+
func startListeningToNotifications() {
29+
let newPasswordCreatedObserver = notificationCenter.addObserver(forName: .ApplicationPasswordsNewPasswordCreated,
30+
object: nil,
31+
queue: .main) { [weak self] note in
32+
self?.trackApplicationPasswordsNewPasswordCreated(note: note)
33+
}
34+
35+
let passwordGenerationFailedObserver = notificationCenter.addObserver(forName: .ApplicationPasswordsGenerationFailed,
36+
object: nil,
37+
queue: .main) { [weak self] note in
38+
self?.trackApplicationPasswordsGenerationFailed(note: note)
39+
}
40+
41+
trackingObservers = [newPasswordCreatedObserver, passwordGenerationFailedObserver]
42+
}
43+
44+
/// Tracks an event when an application password is created
45+
///
46+
func trackApplicationPasswordsNewPasswordCreated(note: Notification) {
47+
analytics.track(event: .ApplicationPassword.applicationPasswordGeneratedSuccessfully(scenario: .regeneration))
48+
}
49+
50+
/// Tracks an event when generating an application password fails
51+
///
52+
func trackApplicationPasswordsGenerationFailed(note: Notification) {
53+
guard let error = note.object as? Error else {
54+
return
55+
}
56+
analytics.track(event: .ApplicationPassword.applicationPasswordGenerationFailed(scenario: .regeneration, error: error))
57+
}
58+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import enum Networking.ApplicationPasswordUseCaseError
2+
3+
extension WooAnalyticsEvent {
4+
enum ApplicationPassword {
5+
private enum Key {
6+
static let scenario = "scenario"
7+
static let cause = "cause"
8+
}
9+
10+
enum Scenario: String {
11+
case generation = "generation"
12+
case regeneration = "regeneration"
13+
}
14+
15+
private enum FailureCause: String {
16+
case authorizationFailed = "authorization_failed"
17+
case featureDisabled = "feature_disabled"
18+
case customLoginOrAdminUrl = "custom_login_or_admin_url"
19+
case other = "other"
20+
}
21+
22+
/// Tracks when generating application password succeeds
23+
///
24+
static func applicationPasswordGeneratedSuccessfully(scenario: Scenario) -> WooAnalyticsEvent {
25+
WooAnalyticsEvent(statName: .applicationPasswordsNewPasswordCreated,
26+
properties: [Key.scenario: scenario.rawValue])
27+
}
28+
29+
/// Tracks when generating application password fails
30+
///
31+
static func applicationPasswordGenerationFailed(scenario: Scenario,
32+
error: Error) -> WooAnalyticsEvent {
33+
let failureCause = { () -> FailureCause in
34+
switch error {
35+
case ApplicationPasswordUseCaseError.applicationPasswordsDisabled:
36+
return .featureDisabled
37+
case ApplicationPasswordUseCaseError.unauthorizedRequest:
38+
return .authorizationFailed
39+
case ApplicationPasswordUseCaseError.failedToConstructLoginOrAdminURLUsingSiteAddress:
40+
return .customLoginOrAdminUrl
41+
default:
42+
return .other
43+
}
44+
}()
45+
return WooAnalyticsEvent(statName: .applicationPasswordsGenerationFailed,
46+
properties: [Key.scenario: scenario.rawValue, Key.cause: failureCause.rawValue],
47+
error: error)
48+
}
49+
}
50+
}

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,10 @@ public enum WooAnalyticsStat: String {
817817

818818
// MARK: Widgets
819819
case widgetTapped = "widget_tapped"
820+
821+
// MARK: Application password Events
822+
case applicationPasswordsNewPasswordCreated = "application_passwords_new_password_created"
823+
case applicationPasswordsGenerationFailed = "application_passwords_generation_failed"
820824
}
821825

822826
public extension WooAnalyticsStat {

WooCommerce/Classes/Authentication/PostSiteCredentialLoginChecker.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ private extension PostSiteCredentialLoginChecker {
4646
Task { @MainActor in
4747
do {
4848
let _ = try await useCase.generateNewPassword()
49+
analytics.track(event: .ApplicationPassword.applicationPasswordGeneratedSuccessfully(scenario: .generation))
4950
onSuccess()
5051
} catch {
51-
analytics.track(event: .Login.siteCredentialFailed(step: .applicationPasswordGeneration, error: error))
52+
analytics.track(event: .ApplicationPassword.applicationPasswordGenerationFailed(scenario: .generation, error: error))
5253
switch error {
5354
case ApplicationPasswordUseCaseError.applicationPasswordsDisabled:
5455
// show application password disabled error

WooCommerce/Classes/Yosemite/AuthenticatedState.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class AuthenticatedState: StoresManagerState {
1919
///
2020
private var errorObserverToken: NSObjectProtocol?
2121

22+
/// For tracking events from Networking layer
23+
///
24+
private let trackEventRequestNotificationHandler: TrackEventRequestNotificationHandler
25+
2226
/// Designated Initializer
2327
///
2428
init(credentials: Credentials) {
@@ -99,6 +103,8 @@ class AuthenticatedState: StoresManagerState {
99103

100104
self.services = services
101105

106+
trackEventRequestNotificationHandler = TrackEventRequestNotificationHandler()
107+
102108
startListeningToNotifications()
103109
}
104110

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1966,6 +1966,9 @@
19661966
E1F52DC62668E03B00349D75 /* CardPresentModalBluetoothRequired.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1F52DC52668E03B00349D75 /* CardPresentModalBluetoothRequired.swift */; };
19671967
EE0EE7A628B7415200F6061E /* CustomHelpCenterContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0EE7A528B7415200F6061E /* CustomHelpCenterContent.swift */; };
19681968
EE0EE7A828B74EF300F6061E /* CustomHelpCenterContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0EE7A728B74EF300F6061E /* CustomHelpCenterContentTests.swift */; };
1969+
EE57C11D297AC27300BC31E7 /* TrackEventRequestNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57C11C297AC27300BC31E7 /* TrackEventRequestNotificationHandler.swift */; };
1970+
EE57C11F297E742200BC31E7 /* WooAnalyticsEvent+ApplicationPassword.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57C11E297E742200BC31E7 /* WooAnalyticsEvent+ApplicationPassword.swift */; };
1971+
EE57C121297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE57C120297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift */; };
19691972
EE81B1382865BB0B0032E0D4 /* ProductImagesProductIDUpdaterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE81B1372865BB0B0032E0D4 /* ProductImagesProductIDUpdaterTests.swift */; };
19701973
EE8DCA8028BF964700F23B23 /* MockAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE8DCA7F28BF964700F23B23 /* MockAuthentication.swift */; };
19711974
EEAA45FD293073FE0047D125 /* JetpackInstallStepTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEAA45FC293073FE0047D125 /* JetpackInstallStepTests.swift */; };
@@ -4059,6 +4062,9 @@
40594062
E1F52DC52668E03B00349D75 /* CardPresentModalBluetoothRequired.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalBluetoothRequired.swift; sourceTree = "<group>"; };
40604063
EE0EE7A528B7415200F6061E /* CustomHelpCenterContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHelpCenterContent.swift; sourceTree = "<group>"; };
40614064
EE0EE7A728B74EF300F6061E /* CustomHelpCenterContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHelpCenterContentTests.swift; sourceTree = "<group>"; };
4065+
EE57C11C297AC27300BC31E7 /* TrackEventRequestNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackEventRequestNotificationHandler.swift; sourceTree = "<group>"; };
4066+
EE57C11E297E742200BC31E7 /* WooAnalyticsEvent+ApplicationPassword.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WooAnalyticsEvent+ApplicationPassword.swift"; sourceTree = "<group>"; };
4067+
EE57C120297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackEventRequestNotificationHandlerTests.swift; sourceTree = "<group>"; };
40624068
EE81B1372865BB0B0032E0D4 /* ProductImagesProductIDUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductImagesProductIDUpdaterTests.swift; sourceTree = "<group>"; };
40634069
EE8DCA7F28BF964700F23B23 /* MockAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAuthentication.swift; sourceTree = "<group>"; };
40644070
EEAA45FC293073FE0047D125 /* JetpackInstallStepTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackInstallStepTests.swift; sourceTree = "<group>"; };
@@ -6800,12 +6806,14 @@
68006806
74213449210A323C00C13890 /* WooAnalyticsStat.swift */,
68016807
747AA0882107CEC60047A89B /* AnalyticsProvider.swift */,
68026808
747AA08A2107CF8D0047A89B /* TracksProvider.swift */,
6809+
EE57C11C297AC27300BC31E7 /* TrackEventRequestNotificationHandler.swift */,
68036810
5783FB3E25D7369F00B9984B /* WooAnalyticsEventPropertyType.swift */,
68046811
02C3FACD282A93020095440A /* WooAnalyticsEvent+Dashboard.swift */,
68056812
0247F511286F73EA009C177E /* WooAnalyticsEvent+ImageUpload.swift */,
68066813
02AC30CE2888EC8100146A25 /* WooAnalyticsEvent+LoginOnboarding.swift */,
68076814
53284FB62FF7F94F18F0D3FF /* WaitingTimeTracker.swift */,
68086815
0263E3BA290BB21800E5F88F /* WooAnalyticsEvent+StoreCreation.swift */,
6816+
EE57C11E297E742200BC31E7 /* WooAnalyticsEvent+ApplicationPassword.swift */,
68096817
);
68106818
path = Analytics;
68116819
sourceTree = "<group>";
@@ -7640,6 +7648,7 @@
76407648
children = (
76417649
B5DBF3C220E1484400B53AED /* StoresManagerTests.swift */,
76427650
E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */,
7651+
EE57C120297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift */,
76437652
);
76447653
path = Yosemite;
76457654
sourceTree = "<group>";
@@ -10148,6 +10157,7 @@
1014810157
buildActionMask = 2147483647;
1014910158
files = (
1015010159
B5F571A421BEC90D0010D1B8 /* NoteDetailsHeaderPlainTableViewCell.swift in Sources */,
10160+
EE57C11F297E742200BC31E7 /* WooAnalyticsEvent+ApplicationPassword.swift in Sources */,
1015110161
AEA3F91127BEC08800B9F555 /* PriceFieldFormatter.swift in Sources */,
1015210162
4592A54B24BF58DD00BC3DE0 /* ProductTagsViewController.swift in Sources */,
1015310163
D817585E22BB5E8700289CFE /* OrderEmailComposer.swift in Sources */,
@@ -10277,6 +10287,7 @@
1027710287
E1C47209267A1ECC00D06DA1 /* CrashLoggingStack.swift in Sources */,
1027810288
CCF87BBE279047BC00461C43 /* InfiniteScrollList.swift in Sources */,
1027910289
D85A3C5226C15DE200C0E026 /* InPersonPaymentsPluginNotSupportedVersionView.swift in Sources */,
10290+
EE57C11D297AC27300BC31E7 /* TrackEventRequestNotificationHandler.swift in Sources */,
1028010291
CE583A0B2107937F00D73C1C /* TextViewTableViewCell.swift in Sources */,
1028110292
45AE150224A23F03005AA948 /* ProductParentCategoriesViewController.swift in Sources */,
1028210293
B557652B20F681E800185843 /* StoreTableViewCell.swift in Sources */,
@@ -11660,6 +11671,7 @@
1166011671
020BE77523B4A7EC007FE54C /* AztecSourceCodeFormatBarCommandTests.swift in Sources */,
1166111672
57A5D8D92534FEBB00AA54D6 /* TotalRefundedCalculationUseCaseTests.swift in Sources */,
1166211673
B5718D6521B56B400026C9F0 /* PushNotificationsManagerTests.swift in Sources */,
11674+
EE57C121297E76E000BC31E7 /* TrackEventRequestNotificationHandlerTests.swift in Sources */,
1166311675
D8A8C4F32268288F001C72BF /* AddManualCustomTrackingViewModelTests.swift in Sources */,
1166411676
AEFF77A829786A2900667F7A /* PriceInputViewControllerTests.swift in Sources */,
1166511677
02C2756F24F5F5EE00286C04 /* ProductShippingSettingsViewModel+ProductVariationTests.swift in Sources */,

0 commit comments

Comments
 (0)