Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommend App: Add Tracks #17027

Merged
merged 7 commits into from
Aug 19, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ import Foundation
// When Likes list is scrolled
case likeListFetchedMore

// Recommend app to others
case recommendAppEngaged
case recommendAppContentFetchFailed

/// A String that represents the event
var value: String {
switch self {
Expand Down Expand Up @@ -481,6 +485,14 @@ import Foundation
// When Likes list is scrolled
case .likeListFetchedMore:
return "like_list_fetched_more"

// When the recommend app button is tapped
case .recommendAppEngaged:
return "recommend_app_engaged"

// When the content fetching for the recommend app failed
case .recommendAppContentFetchFailed:
return "recommend_app_content_fetch_failed"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ open class AboutViewController: UITableViewController {
return
}

sharePresenter.present(for: .wordpress, in: self)
sharePresenter.present(for: .wordpress, in: self, source: .about)
}

// MARK: - Nested Row Class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class MeViewController: UITableViewController {
func displayShareFlow() -> ImmuTableAction {
return { [unowned self] row in
self.tableView.deselectSelectedRowWithAnimation(true)
self.sharePresenter.present(for: .wordpress, in: self)
self.sharePresenter.present(for: .wordpress, in: self, source: .me)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ class ShareAppContentPresenter {

/// Fetches the content needed for sharing, and presents the share sheet through the provided `sender` instance.
///
func present(for appName: ShareAppName, in sender: UIViewController, completion: (() -> Void)? = nil) {
func present(for appName: ShareAppName, in sender: UIViewController, source: ShareAppEventSource, completion: (() -> Void)? = nil) {
if let content = cachedContent {
presentShareSheet(with: content, in: sender)
trackEngagement(source: source)
completion?()
return
}
Expand All @@ -63,9 +64,11 @@ class ShareAppContentPresenter {
case .success(let content):
self.cachedContent = content
self.presentShareSheet(with: content, in: sender)
self.trackEngagement(source: source)

case .failure:
self.showFailureNotice(in: sender)
self.trackContentFetchFailed()
}

self.isLoading = false
Expand All @@ -74,6 +77,16 @@ class ShareAppContentPresenter {
}
}

// MARK: - Tracking Source Definition

enum ShareAppEventSource: String {
// Feature engaged from the Me page.
case me

// Feature engaged form the About page.
case about
}

// MARK: - Delegate Definition

protocol ShareAppContentPresenterDelegate: AnyObject {
Expand Down Expand Up @@ -110,11 +123,22 @@ private extension ShareAppContentPresenter {
func showFailureNotice(in viewController: UIViewController) {
viewController.displayNotice(title: .failureNoticeText, message: nil)
}

// MARK: Tracking Helpers

func trackEngagement(source: ShareAppEventSource) {
WPAnalytics.track(.recommendAppEngaged, properties: [String.sourceParameterKey: source.rawValue])
}

func trackContentFetchFailed() {
WPAnalytics.track(.recommendAppContentFetchFailed)
}
}

// MARK: Localized Strings

private extension String {
static let sourceParameterKey = "source"
static let failureNoticeText = NSLocalizedString("Something went wrong. Please try again.",
comment: "Error message shown when user tries to share the app with others, "
+ "but failed due to unknown errors.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Foundation
class TestAnalyticsTracker: NSObject {
struct Tracked {
let stat: WPAnalyticsStat
let event: String
let properties: [AnyHashable: Any]

func value<T>(for propertyName: String) -> T? {
Expand All @@ -29,19 +30,19 @@ class TestAnalyticsTracker: NSObject {
return tracked.count
}

private static func track(_ stat: WPAnalyticsStat, with properties: [AnyHashable: Any]? = nil) {
let trackedStat = Tracked(stat: stat, properties: properties ?? [:])
private static func track(_ stat: WPAnalyticsStat, event: String = "", with properties: [AnyHashable: Any]? = nil) {
let trackedStat = Tracked(stat: stat, event: event, properties: properties ?? [:])
_tracked.append(trackedStat)
}
}

extension TestAnalyticsTracker: WPAnalyticsTracker {
func trackString(_ event: String) {

TestAnalyticsTracker.track(.noStat, event: event)
}

func trackString(_ event: String, withProperties properties: [AnyHashable: Any]!) {

TestAnalyticsTracker.track(.noStat, event: event, with: properties)
}

func track(_ stat: WPAnalyticsStat) {
Expand Down
74 changes: 66 additions & 8 deletions WordPress/WordPressTest/ShareAppContentPresenterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import OHHTTPStubs

final class ShareAppContentPresenterTests: XCTestCase {

private let timeout: TimeInterval = 0.1
private var contextManager: TestContextManager!
private var account: WPAccount!
private var presenter: ShareAppContentPresenter!
Expand All @@ -13,6 +14,7 @@ final class ShareAppContentPresenterTests: XCTestCase {
override func setUp() {
super.setUp()

TestAnalyticsTracker.setup()
contextManager = TestContextManager()
account = AccountBuilder(contextManager).build()
presenter = ShareAppContentPresenter(account: account)
Expand All @@ -23,6 +25,7 @@ final class ShareAppContentPresenterTests: XCTestCase {
override func tearDown() {
super.tearDown()
HTTPStubs.removeAllStubs()
TestAnalyticsTracker.tearDown()

contextManager = nil
account = nil
Expand All @@ -36,48 +39,103 @@ final class ShareAppContentPresenterTests: XCTestCase {
stubShareAppLinkResponse(success: true)

let expectation = expectation(description: "Present share sheet success")
presenter.present(for: .wordpress, in: viewController) {
presenter.present(for: .wordpress, in: viewController, source: .me) {
XCTAssertNotNil(self.viewController.viewControllerToPresent)
XCTAssertTrue(self.viewController.viewControllerToPresent! is UIActivityViewController)
XCTAssertEqual(self.viewController.stateHistory, [true, false])
expectation.fulfill()
}

waitForExpectations(timeout: 0.1, handler: nil)
wait(for: [expectation], timeout: timeout)
}

func test_present_givenFailedResponse_displaysFailureNotice() {
stubShareAppLinkResponse(success: false)

let expectation = expectation(description: "Present failure notice success")
presenter.present(for: .wordpress, in: viewController) {
presenter.present(for: .wordpress, in: viewController, source: .me) {
XCTAssertNotNil(self.viewController.noticeTitle)
expectation.fulfill()
}

waitForExpectations(timeout: 0.1, handler: nil)
wait(for: [expectation], timeout: timeout)
}

func test_present_givenCachedContent_immediatelyPresentsShareSheet() {
stubShareAppLinkResponse(success: true)

let firstExpectation = expectation(description: "Present share sheet success")
presenter.present(for: .wordpress, in: viewController) {
presenter.present(for: .wordpress, in: viewController, source: .me) {
firstExpectation.fulfill()
}
waitForExpectations(timeout: 0.1, handler: nil)
wait(for: [firstExpectation], timeout: timeout)
viewController.stateHistory = [] // reset state

// present the share sheet again.
let secondExpectation = expectation(description: "Second present share sheet success")
presenter.present(for: .wordpress, in: viewController) {
presenter.present(for: .wordpress, in: viewController, source: .me) {
XCTAssertNotNil(self.viewController.viewControllerToPresent)
XCTAssertTrue(self.viewController.viewControllerToPresent! is UIActivityViewController)
XCTAssertTrue(self.viewController.stateHistory.isEmpty) // state should still be empty since there should be no more loading state changes.
secondExpectation.fulfill()
}

waitForExpectations(timeout: 0.1, handler: nil)
wait(for: [secondExpectation], timeout: timeout)
}

// MARK: Tracking

func test_givenValidResponse_tracksEngagement() {
stubShareAppLinkResponse(success: true)

let expectation = expectation(description: "Present share sheet success")
presenter.present(for: .wordpress, in: viewController, source: .me) {
XCTAssertEqual(TestAnalyticsTracker.trackedEventsCount(), 1)

let tracked = TestAnalyticsTracker.tracked.first!
XCTAssertEqual(tracked.event, WPAnalyticsEvent.recommendAppEngaged.value)
XCTAssertEqual(tracked.properties["source"]! as! String, ShareAppEventSource.me.rawValue)
expectation.fulfill()
}

wait(for: [expectation], timeout: timeout)
}

func test_givenCachedContent_tracksEngagement() {
stubShareAppLinkResponse(success: true)
let expectedEvents: [WPAnalyticsEvent] = [.recommendAppEngaged, .recommendAppEngaged]
let expectedSources: [ShareAppEventSource] = [.about, .me]

let firstExpectation = expectation(description: "Present share sheet success")
presenter.present(for: .wordpress, in: viewController, source: .about) {
firstExpectation.fulfill()
}
wait(for: [firstExpectation], timeout: timeout)

// present the share sheet again. the second event should be fired even though cached content is returned.
let secondExpectation = expectation(description: "Second present share sheet success")
presenter.present(for: .wordpress, in: viewController, source: .me) {
XCTAssertEqual(TestAnalyticsTracker.trackedEventsCount(), 2)
XCTAssertEqual(TestAnalyticsTracker.tracked.map { $0.event }, expectedEvents.map { $0.value })
XCTAssertEqual(TestAnalyticsTracker.tracked.compactMap { $0.properties["source"] as? String }, expectedSources.map { $0.rawValue })
secondExpectation.fulfill()
}

wait(for: [secondExpectation], timeout: timeout)
}

func test_givenFailedResponse_tracksContentFetchFailure() {
stubShareAppLinkResponse(success: false)

let expectation = expectation(description: "Present failure notice success")
presenter.present(for: .wordpress, in: viewController, source: .me) {
XCTAssertEqual(TestAnalyticsTracker.trackedEventsCount(), 1)
let tracked = TestAnalyticsTracker.tracked.first!
XCTAssertEqual(tracked.event, WPAnalyticsEvent.recommendAppContentFetchFailed.value)
expectation.fulfill()
}

wait(for: [expectation], timeout: timeout)
}
}

Expand Down