Skip to content

Commit 1922249

Browse files
authored
Merge pull request #5503 from woocommerce/issue/5501-prototype-analytics-tracking
Analytics Tracking Prototype
2 parents 922b380 + 3b96f79 commit 1922249

11 files changed

+360
-29
lines changed

WooCommerce/Classes/Analytics/WooAnalyticsStat.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ public enum WooAnalyticsStat: String {
9595
case dashboardNewStatsAvailabilityBannerTryTapped = "dashboard_new_stats_availability_banner_try_tapped"
9696
case dashboardNewStatsRevertedBannerDismissTapped = "dashboard_new_stats_reverted_banner_dismiss_tapped"
9797
case dashboardNewStatsRevertedBannerLearnMoreTapped = "dashboard_new_stats_reverted_banner_learn_more_tapped"
98+
case usedAnalytics = "used_analytics"
9899

99100
// MARK: Site picker. Can be triggered by login epilogue or settings.
100101
//

WooCommerce/Classes/ViewRelated/Dashboard/MyStore/TopPerformerDataViewController.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ final class TopPerformerDataViewController: UIViewController {
4646
private let imageService: ImageService = ServiceLocator.imageService
4747
private let isMyStoreTabUpdatesEnabled: Bool
4848

49+
private let usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter
50+
4951
// MARK: - Computed Properties
5052

5153
private var topEarnerStats: TopEarnerStats? {
@@ -73,13 +75,15 @@ final class TopPerformerDataViewController: UIViewController {
7375
siteTimeZone: TimeZone,
7476
currentDate: Date,
7577
timeRange: StatsTimeRangeV4,
76-
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService) {
78+
featureFlagService: FeatureFlagService = ServiceLocator.featureFlagService,
79+
usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter) {
7780
self.siteID = siteID
7881
self.siteTimeZone = siteTimeZone
7982
self.currentDate = currentDate
8083
self.granularity = timeRange.topEarnerStatsGranularity
8184
self.timeRange = timeRange
8285
self.isMyStoreTabUpdatesEnabled = featureFlagService.isFeatureFlagEnabled(.myStoreTabUpdates)
86+
self.usageTracksEventEmitter = usageTracksEventEmitter
8387
super.init(nibName: type(of: self).nibName, bundle: nil)
8488
}
8589

@@ -263,6 +267,9 @@ extension TopPerformerDataViewController: UITableViewDelegate {
263267
guard let statsItem = statsItem(at: indexPath) else {
264268
return
265269
}
270+
271+
usageTracksEventEmitter.interacted()
272+
266273
presentProductDetails(statsItem: statsItem)
267274
}
268275

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/OldStoreStatsAndTopPerformersPeriodViewController.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ final class OldStoreStatsAndTopPerformersPeriodViewController: UIViewController
8181
// MARK: Child View Controllers
8282

8383
private lazy var storeStatsPeriodViewController: OldStoreStatsV4PeriodViewController = {
84-
return OldStoreStatsV4PeriodViewController(timeRange: timeRange, currentDate: currentDate)
84+
OldStoreStatsV4PeriodViewController(timeRange: timeRange,
85+
currentDate: currentDate,
86+
usageTracksEventEmitter: usageTracksEventEmitter)
8587
}()
8688

8789
private lazy var inAppFeedbackCardViewController = InAppFeedbackCardViewController()
@@ -94,7 +96,8 @@ final class OldStoreStatsAndTopPerformersPeriodViewController: UIViewController
9496
return TopPerformerDataViewController(siteID: siteID,
9597
siteTimeZone: siteTimezone,
9698
currentDate: currentDate,
97-
timeRange: timeRange)
99+
timeRange: timeRange,
100+
usageTracksEventEmitter: usageTracksEventEmitter)
98101
}()
99102

100103
// MARK: Internal Properties
@@ -105,6 +108,8 @@ final class OldStoreStatsAndTopPerformersPeriodViewController: UIViewController
105108

106109
private let viewModel: StoreStatsAndTopPerformersPeriodViewModel
107110

111+
private let usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter
112+
108113
private let siteID: Int64
109114

110115
/// Subscriptions that should be cancelled on `deinit`.
@@ -119,12 +124,14 @@ final class OldStoreStatsAndTopPerformersPeriodViewController: UIViewController
119124
init(siteID: Int64,
120125
timeRange: StatsTimeRangeV4,
121126
currentDate: Date,
122-
canDisplayInAppFeedbackCard: Bool) {
127+
canDisplayInAppFeedbackCard: Bool,
128+
usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter) {
123129
self.siteID = siteID
124130
self.timeRange = timeRange
125131
self.granularity = timeRange.intervalGranularity
126132
self.currentDate = currentDate
127133
self.viewModel = StoreStatsAndTopPerformersPeriodViewModel(canDisplayInAppFeedbackCard: canDisplayInAppFeedbackCard)
134+
self.usageTracksEventEmitter = usageTracksEventEmitter
128135

129136
super.init(nibName: nil, bundle: nil)
130137

@@ -167,6 +174,14 @@ extension OldStoreStatsAndTopPerformersPeriodViewController: UIScrollViewDelegat
167174
func scrollViewDidScroll(_ scrollView: UIScrollView) {
168175
scrollDelegate?.dashboardUIScrollViewDidScroll(scrollView)
169176
}
177+
178+
/// We're not using scrollViewDidScroll because that gets executed even while
179+
/// the app is being loaded for the first time.
180+
///
181+
/// Note: This also covers pull-to-refresh
182+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
183+
usageTracksEventEmitter.interacted()
184+
}
170185
}
171186

172187
// MARK: Public Interface

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/OldStoreStatsAndTopPerformersViewController.swift

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ final class OldStoreStatsAndTopPerformersViewController: ButtonBarPagerTabStripV
3333
private let siteID: Int64
3434
private var isSyncing = false
3535

36+
private let usageTracksEventEmitter = StoreStatsUsageTracksEventEmitter()
37+
3638
// MARK: - View Lifecycle
3739

3840
init(siteID: Int64) {
@@ -294,22 +296,30 @@ private extension OldStoreStatsAndTopPerformersViewController {
294296

295297
func configurePeriodViewControllers() {
296298
let currentDate = Date()
297-
let dayVC = OldStoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
298-
timeRange: .today,
299-
currentDate: currentDate,
300-
canDisplayInAppFeedbackCard: true)
301-
let weekVC = OldStoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
302-
timeRange: .thisWeek,
303-
currentDate: currentDate,
304-
canDisplayInAppFeedbackCard: false)
305-
let monthVC = OldStoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
306-
timeRange: .thisMonth,
307-
currentDate: currentDate,
308-
canDisplayInAppFeedbackCard: false)
309-
let yearVC = OldStoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
310-
timeRange: .thisYear,
311-
currentDate: currentDate,
312-
canDisplayInAppFeedbackCard: false)
299+
let dayVC = OldStoreStatsAndTopPerformersPeriodViewController(
300+
siteID: siteID,
301+
timeRange: .today,
302+
currentDate: currentDate,
303+
canDisplayInAppFeedbackCard: true,
304+
usageTracksEventEmitter: usageTracksEventEmitter)
305+
let weekVC = OldStoreStatsAndTopPerformersPeriodViewController(
306+
siteID: siteID,
307+
timeRange: .thisWeek,
308+
currentDate: currentDate,
309+
canDisplayInAppFeedbackCard: false,
310+
usageTracksEventEmitter: usageTracksEventEmitter)
311+
let monthVC = OldStoreStatsAndTopPerformersPeriodViewController(
312+
siteID: siteID,
313+
timeRange: .thisMonth,
314+
currentDate: currentDate,
315+
canDisplayInAppFeedbackCard: false,
316+
usageTracksEventEmitter: usageTracksEventEmitter)
317+
let yearVC = OldStoreStatsAndTopPerformersPeriodViewController(
318+
siteID: siteID,
319+
timeRange: .thisYear,
320+
currentDate: currentDate,
321+
canDisplayInAppFeedbackCard: false,
322+
usageTracksEventEmitter: usageTracksEventEmitter)
313323

314324
periodVCs.append(dayVC)
315325
periodVCs.append(weekVC)

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/OldStoreStatsV4PeriodViewController.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ final class OldStoreStatsV4PeriodViewController: UIViewController {
5050
}
5151
private var siteStatsItems: [SiteVisitStatsItem] = []
5252

53+
private let usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter
54+
5355
// MARK: - Subviews
5456

5557
@IBOutlet private weak var containerStackView: UIStackView!
@@ -150,10 +152,13 @@ final class OldStoreStatsV4PeriodViewController: UIViewController {
150152

151153
/// Designated Initializer
152154
///
153-
init(timeRange: StatsTimeRangeV4, currentDate: Date) {
155+
init(timeRange: StatsTimeRangeV4,
156+
currentDate: Date,
157+
usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter) {
154158
self.timeRange = timeRange
155159
self.granularity = timeRange.intervalGranularity
156160
self.currentDate = currentDate
161+
self.usageTracksEventEmitter = usageTracksEventEmitter
157162
super.init(nibName: type(of: self).nibName, bundle: nil)
158163

159164
// Make sure the ResultsControllers are ready to observe changes to the data even before the view loads
@@ -430,6 +435,7 @@ extension OldStoreStatsV4PeriodViewController: ChartViewDelegate {
430435
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
431436
let selectedIndex = Int(entry.x)
432437
updateUI(selectedBarIndex: selectedIndex)
438+
usageTracksEventEmitter.interacted()
433439
}
434440
}
435441

@@ -661,6 +667,7 @@ private extension OldStoreStatsV4PeriodViewController {
661667
isInitialLoad = false
662668
return
663669
}
670+
usageTracksEventEmitter.interacted()
664671
ServiceLocator.analytics.track(.dashboardMainStatsDate, withProperties: ["range": granularity.rawValue])
665672
isInitialLoad = false
666673
}

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsAndTopPerformersPeriodViewController.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ final class StoreStatsAndTopPerformersPeriodViewController: UIViewController {
7777
// MARK: Child View Controllers
7878

7979
private lazy var storeStatsPeriodViewController: StoreStatsV4PeriodViewController = {
80-
return StoreStatsV4PeriodViewController(siteID: siteID, timeRange: timeRange)
80+
StoreStatsV4PeriodViewController(siteID: siteID, timeRange: timeRange, usageTracksEventEmitter: usageTracksEventEmitter)
8181
}()
8282

8383
private lazy var inAppFeedbackCardViewController = InAppFeedbackCardViewController()
@@ -90,7 +90,8 @@ final class StoreStatsAndTopPerformersPeriodViewController: UIViewController {
9090
return TopPerformerDataViewController(siteID: siteID,
9191
siteTimeZone: siteTimezone,
9292
currentDate: currentDate,
93-
timeRange: timeRange)
93+
timeRange: timeRange,
94+
usageTracksEventEmitter: usageTracksEventEmitter)
9495
}()
9596

9697
// MARK: Internal Properties
@@ -103,6 +104,8 @@ final class StoreStatsAndTopPerformersPeriodViewController: UIViewController {
103104

104105
private let siteID: Int64
105106

107+
private let usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter
108+
106109
/// Subscriptions that should be cancelled on `deinit`.
107110
private var cancellables = [ObservationToken]()
108111

@@ -115,12 +118,14 @@ final class StoreStatsAndTopPerformersPeriodViewController: UIViewController {
115118
init(siteID: Int64,
116119
timeRange: StatsTimeRangeV4,
117120
currentDate: Date,
118-
canDisplayInAppFeedbackCard: Bool) {
121+
canDisplayInAppFeedbackCard: Bool,
122+
usageTracksEventEmitter: StoreStatsUsageTracksEventEmitter) {
119123
self.siteID = siteID
120124
self.timeRange = timeRange
121125
self.granularity = timeRange.intervalGranularity
122126
self.currentDate = currentDate
123127
self.viewModel = StoreStatsAndTopPerformersPeriodViewModel(canDisplayInAppFeedbackCard: canDisplayInAppFeedbackCard)
128+
self.usageTracksEventEmitter = usageTracksEventEmitter
124129

125130
super.init(nibName: nil, bundle: nil)
126131

@@ -163,6 +168,14 @@ extension StoreStatsAndTopPerformersPeriodViewController: UIScrollViewDelegate {
163168
func scrollViewDidScroll(_ scrollView: UIScrollView) {
164169
scrollDelegate?.dashboardUIScrollViewDidScroll(scrollView)
165170
}
171+
172+
/// We're not using scrollViewDidScroll because that gets executed even while
173+
/// the app is being loaded for the first time.
174+
///
175+
/// Note: This also covers pull-to-refresh
176+
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
177+
usageTracksEventEmitter.interacted()
178+
}
166179
}
167180

168181
// MARK: Public Interface

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsAndTopPerformersViewController.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ final class StoreStatsAndTopPerformersViewController: ButtonBarPagerTabStripView
3232
private var periodVCs = [StoreStatsAndTopPerformersPeriodViewController]()
3333
private let siteID: Int64
3434
private var isSyncing = false
35+
private let usageTracksEventEmitter = StoreStatsUsageTracksEventEmitter()
3536

3637
// MARK: - View Lifecycle
3738

@@ -297,19 +298,23 @@ private extension StoreStatsAndTopPerformersViewController {
297298
let dayVC = StoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
298299
timeRange: .today,
299300
currentDate: currentDate,
300-
canDisplayInAppFeedbackCard: true)
301+
canDisplayInAppFeedbackCard: true,
302+
usageTracksEventEmitter: usageTracksEventEmitter)
301303
let weekVC = StoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
302304
timeRange: .thisWeek,
303305
currentDate: currentDate,
304-
canDisplayInAppFeedbackCard: false)
306+
canDisplayInAppFeedbackCard: false,
307+
usageTracksEventEmitter: usageTracksEventEmitter)
305308
let monthVC = StoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
306309
timeRange: .thisMonth,
307310
currentDate: currentDate,
308-
canDisplayInAppFeedbackCard: false)
311+
canDisplayInAppFeedbackCard: false,
312+
usageTracksEventEmitter: usageTracksEventEmitter)
309313
let yearVC = StoreStatsAndTopPerformersPeriodViewController(siteID: siteID,
310314
timeRange: .thisYear,
311315
currentDate: currentDate,
312-
canDisplayInAppFeedbackCard: false)
316+
canDisplayInAppFeedbackCard: false,
317+
usageTracksEventEmitter: usageTracksEventEmitter)
313318

314319
periodVCs.append(dayVC)
315320
periodVCs.append(weekVC)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import Foundation
2+
3+
/// Note: If we ever change the algorithm in the future, we should probably consider renaming
4+
/// the Tracks event to avoid incorrect comparisons with old events.
5+
final class StoreStatsUsageTracksEventEmitter {
6+
7+
private let analytics: Analytics
8+
9+
/// The minimum amount of time (seconds) that the merchant have interacted with the
10+
/// Analytics UI before an event is triggered.
11+
private let minimumInteractionTime: TimeInterval = 10
12+
13+
/// The minimum number of Analytics UI interactions before an event is triggered.
14+
///
15+
/// The interactions captured are:
16+
///
17+
/// - Scrolling
18+
/// - Pull-to-refresh
19+
/// - Tapping on the bars in the chart
20+
/// - Changing the tab
21+
/// - Navigating to the My Store tab
22+
/// - Tapping on a product in the Top Performers list
23+
private let interactionsThreshold = 5
24+
25+
/// The maximum number of seconds in between interactions before we will consider the
26+
/// merchant to have been idle. If they were idle, the time and interactions counting
27+
/// will be reset.
28+
private let idleTimeThreshold: TimeInterval = 20
29+
30+
private var interactions = 0
31+
private var firstInteractionTime: Date? = nil
32+
private var lastInteractionTime: Date? = nil
33+
34+
init(analytics: Analytics = ServiceLocator.analytics) {
35+
self.analytics = analytics
36+
}
37+
38+
func interacted(at interactionTime: Date = Date()) {
39+
// Check if they were idle for some time.
40+
if let lastInteractionTime = lastInteractionTime,
41+
interactionTime.timeIntervalSince(lastInteractionTime) >= idleTimeThreshold {
42+
reset()
43+
}
44+
45+
guard let firstInteractionTime = firstInteractionTime else {
46+
interactions = 1
47+
self.firstInteractionTime = interactionTime
48+
self.lastInteractionTime = interactionTime
49+
50+
return
51+
}
52+
53+
interactions += 1
54+
lastInteractionTime = interactionTime
55+
56+
if interactionTime.timeIntervalSince(firstInteractionTime) >= minimumInteractionTime &&
57+
interactions >= interactionsThreshold {
58+
59+
reset()
60+
analytics.track(.usedAnalytics)
61+
}
62+
}
63+
64+
private func reset() {
65+
interactions = 0
66+
firstInteractionTime = nil
67+
lastInteractionTime = nil
68+
}
69+
}

0 commit comments

Comments
 (0)