Skip to content

Commit b1644f7

Browse files
authored
Merge pull request #8410 from woocommerce/issue/8363-summary-stats-action-yosemite
[Analytics Hub] Add support for fetching site summary stats (Yosemite)
2 parents 4dd446e + a61614e commit b1644f7

File tree

8 files changed

+241
-14
lines changed

8 files changed

+241
-14
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@
552552
CC0786C7267BB10700BA9AC1 /* shipping-label-status-success.json in Resources */ = {isa = PBXBuildFile; fileRef = CC0786C6267BB10700BA9AC1 /* shipping-label-status-success.json */; };
553553
CC0786C9267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0786C8267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift */; };
554554
CC6A1FF5270E042200F6AF4A /* OrderMetaData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC6A1FF4270E042200F6AF4A /* OrderMetaData.swift */; };
555+
CC80E3F92948C8BC00D5FF45 /* site-visits-quarter.json in Resources */ = {isa = PBXBuildFile; fileRef = CC80E3F82948C8BC00D5FF45 /* site-visits-quarter.json */; };
555556
CC851D0625E51ADF00249E9C /* Decimal+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC851D0525E51ADF00249E9C /* Decimal+Extensions.swift */; };
556557
CC851D1425E52AB500249E9C /* Decimal+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC851D1325E52AB500249E9C /* Decimal+ExtensionsTests.swift */; };
557558
CC9A24A32641BCF2005DE56E /* ShippingLabelCreationEligibilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC9A24A22641BCF2005DE56E /* ShippingLabelCreationEligibilityResponse.swift */; };
@@ -1314,6 +1315,7 @@
13141315
CC0786C6267BB10700BA9AC1 /* shipping-label-status-success.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "shipping-label-status-success.json"; sourceTree = "<group>"; };
13151316
CC0786C8267BB32800BA9AC1 /* ShippingLabelStatusMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelStatusMapperTests.swift; sourceTree = "<group>"; };
13161317
CC6A1FF4270E042200F6AF4A /* OrderMetaData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderMetaData.swift; sourceTree = "<group>"; };
1318+
CC80E3F82948C8BC00D5FF45 /* site-visits-quarter.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-visits-quarter.json"; sourceTree = "<group>"; };
13171319
CC851D0525E51ADF00249E9C /* Decimal+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+Extensions.swift"; sourceTree = "<group>"; };
13181320
CC851D1325E52AB500249E9C /* Decimal+ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+ExtensionsTests.swift"; sourceTree = "<group>"; };
13191321
CC9A24A22641BCF2005DE56E /* ShippingLabelCreationEligibilityResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCreationEligibilityResponse.swift; sourceTree = "<group>"; };
@@ -2213,6 +2215,7 @@
22132215
74A1D25F211898F000931DFA /* site-visits-day.json */,
22142216
74A1D260211898F000931DFA /* site-visits-week.json */,
22152217
74A1D261211898F000931DFA /* site-visits-month.json */,
2218+
CC80E3F82948C8BC00D5FF45 /* site-visits-quarter.json */,
22162219
74A1D262211898F000931DFA /* site-visits-year.json */,
22172220
743BF8BD21191B63008A9D87 /* site-visits.json */,
22182221
74E30950216E8DCE00ABCE4C /* site-visits-alt.json */,
@@ -2942,6 +2945,7 @@
29422945
45AB8B2024AB3E1F00B5B36E /* product-tags-empty.json in Resources */,
29432946
0359EA2127AAE58C0048DE2D /* wcpay-charge-card-present.json in Resources */,
29442947
451274A625276C82009911FF /* product-variation.json in Resources */,
2948+
CC80E3F92948C8BC00D5FF45 /* site-visits-quarter.json in Resources */,
29452949
020D07C223D858BB00FD9580 /* media-upload.json in Resources */,
29462950
31A451D127863A2E00FE81AA /* stripe-account-rejected-other.json in Resources */,
29472951
74ABA1CA213F19FE00FFAD30 /* top-performers-year.json in Resources */,

Networking/Networking/Model/Stats/SiteSummaryStats.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Codegen
33

44
/// Represents site summary stats for a specific period.
55
///
6-
public struct SiteSummaryStats: Decodable, GeneratedCopiable, GeneratedFakeable {
6+
public struct SiteSummaryStats: Decodable, Equatable, GeneratedCopiable, GeneratedFakeable {
77
public let siteID: Int64
88
public let date: String
99
public let period: StatGranularity
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"date": "2022-12-09",
3+
"unit": "month",
4+
"fields": [
5+
"period",
6+
"views",
7+
"visitors"
8+
],
9+
"data": [
10+
[
11+
"2022-10-01",
12+
448,
13+
224
14+
],
15+
[
16+
"2022-11-01",
17+
14,
18+
7
19+
],
20+
[
21+
"2022-12-01",
22+
24,
23+
12
24+
]
25+
]
26+
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ private extension StoreStatsAndTopPerformersViewController {
483483
}
484484
}
485485

486-
func handleSiteVisitStatsStoreError(error: SiteVisitStatsStoreError) {
486+
func handleSiteStatsStoreError(error: SiteStatsStoreError) {
487487
switch error {
488488
case .noPermission:
489489
updateSiteVisitors(mode: .hidden)
@@ -501,8 +501,8 @@ private extension StoreStatsAndTopPerformersViewController {
501501

502502
private func handleSyncError(error: Error) {
503503
switch error {
504-
case let siteVisitStatsStoreError as SiteVisitStatsStoreError:
505-
handleSiteVisitStatsStoreError(error: siteVisitStatsStoreError)
504+
case let siteStatsStoreError as SiteStatsStoreError:
505+
handleSiteStatsStoreError(error: siteStatsStoreError)
506506
default:
507507
displaySyncingError()
508508
}

Yosemite/Yosemite/Actions/StatsActionV4.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,12 @@ public enum StatsActionV4: Action {
4848
forceRefresh: Bool,
4949
saveInStorage: Bool,
5050
onCompletion: (Result<TopEarnerStats, Error>) -> Void)
51+
52+
/// Retrieves the site summary stats for the provided site ID, period(s), and date, without saving them to the Storage layer.
53+
///
54+
case retrieveSiteSummaryStats(siteID: Int64,
55+
period: StatGranularity,
56+
quantity: Int,
57+
latestDateToInclude: Date,
58+
onCompletion: (Result<SiteSummaryStats, Error>) -> Void)
5159
}

Yosemite/Yosemite/Stores/StatsStoreV4.swift

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import WooFoundation
66
// MARK: - StatsStoreV4
77
//
88
public final class StatsStoreV4: Store {
9-
private let siteVisitStatsRemote: SiteStatsRemote
9+
private let siteStatsRemote: SiteStatsRemote
1010
private let leaderboardsRemote: LeaderboardsRemote
1111
private let orderStatsRemote: OrderStatsRemoteV4
1212
private let productsRemote: ProductsRemote
1313

1414
public override init(dispatcher: Dispatcher, storageManager: StorageManagerType, network: Network) {
15-
self.siteVisitStatsRemote = SiteStatsRemote(network: network)
15+
self.siteStatsRemote = SiteStatsRemote(network: network)
1616
self.leaderboardsRemote = LeaderboardsRemote(network: network)
1717
self.orderStatsRemote = OrderStatsRemoteV4(network: network)
1818
self.productsRemote = ProductsRemote(network: network)
@@ -90,6 +90,16 @@ public final class StatsStoreV4: Store {
9090
forceRefresh: forceRefresh,
9191
saveInStorage: saveInStorage,
9292
onCompletion: onCompletion)
93+
case .retrieveSiteSummaryStats(let siteID,
94+
let period,
95+
let quantity,
96+
let latestDateToInclude,
97+
let onCompletion):
98+
retrieveSiteSummaryStats(siteID: siteID,
99+
period: period,
100+
quantity: quantity,
101+
latestDateToInclude: latestDateToInclude,
102+
onCompletion: onCompletion)
93103
}
94104
}
95105
}
@@ -164,7 +174,7 @@ private extension StatsStoreV4 {
164174

165175
let quantity = timeRange.siteVisitStatsQuantity(date: latestDateToInclude, siteTimezone: siteTimezone)
166176

167-
siteVisitStatsRemote.loadSiteVisitorStats(for: siteID,
177+
siteStatsRemote.loadSiteVisitorStats(for: siteID,
168178
siteTimezone: siteTimezone,
169179
unit: timeRange.siteVisitStatsGranularity,
170180
latestDateToInclude: latestDateToInclude,
@@ -174,7 +184,50 @@ private extension StatsStoreV4 {
174184
self?.upsertStoredSiteVisitStats(readOnlyStats: siteVisitStats, timeRange: timeRange)
175185
onCompletion(.success(()))
176186
case .failure(let error):
177-
onCompletion(.failure(SiteVisitStatsStoreError(error: error)))
187+
onCompletion(.failure(SiteStatsStoreError(error: error)))
188+
}
189+
}
190+
}
191+
192+
/// Retrieves the site summary stats for the provided site ID, period(s), and date, without saving them to the Storage layer.
193+
///
194+
func retrieveSiteSummaryStats(siteID: Int64,
195+
period: StatGranularity,
196+
quantity: Int,
197+
latestDateToInclude: Date,
198+
onCompletion: @escaping (Result<SiteSummaryStats, Error>) -> Void) {
199+
if quantity == 1 {
200+
siteStatsRemote.loadSiteSummaryStats(for: siteID,
201+
period: period,
202+
includingDate: latestDateToInclude) { result in
203+
switch result {
204+
case .success(let siteSummaryStats):
205+
onCompletion(.success(siteSummaryStats))
206+
case .failure(let error):
207+
onCompletion(.failure(SiteStatsStoreError(error: error)))
208+
}
209+
}
210+
} else {
211+
// If we are not fetching stats for a single period, we need to summarize the stats manually.
212+
// The remote summary stats endpoint only retrieves visitor stats for a single period.
213+
// We should only do this for periods of a month or greater; otherwise the visitor total is inaccurate.
214+
// See: pe5uwI-5c-p2
215+
siteStatsRemote.loadSiteVisitorStats(for: siteID,
216+
unit: period,
217+
latestDateToInclude: latestDateToInclude,
218+
quantity: quantity) { result in
219+
switch result {
220+
case .success(let siteVisitStats):
221+
let totalViews = siteVisitStats.items?.map({ $0.views }).reduce(0, +) ?? 0
222+
let summaryStats = SiteSummaryStats(siteID: siteID,
223+
date: siteVisitStats.date,
224+
period: siteVisitStats.granularity,
225+
visitors: siteVisitStats.totalVisitors,
226+
views: totalViews)
227+
onCompletion(.success(summaryStats))
228+
case .failure(let error):
229+
onCompletion(.failure(SiteStatsStoreError(error: error)))
230+
}
178231
}
179232
}
180233
}
@@ -571,7 +624,7 @@ public enum StatsStoreV4Error: Error {
571624
/// - statsModuleDisabled: Jetpack site stats module is disabled for the site.
572625
/// - unknown: other error cases.
573626
///
574-
public enum SiteVisitStatsStoreError: Error {
627+
public enum SiteStatsStoreError: Error {
575628
case statsModuleDisabled
576629
case noPermission
577630
case unknown

Yosemite/YosemiteTests/Stores/SiteVisitStatsStoreErrorTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,28 @@ import XCTest
22
@testable import Yosemite
33
@testable import Networking
44

5-
class SiteVisitStatsStoreErrorTests: XCTestCase {
5+
class SiteStatsStoreErrorTests: XCTestCase {
66
func testNoPermissionError() {
77
let remoteError = DotcomError.noStatsPermission
8-
let error = SiteVisitStatsStoreError(error: remoteError)
8+
let error = SiteStatsStoreError(error: remoteError)
99
XCTAssertEqual(error, .noPermission)
1010
}
1111

1212
func testStatsModuleDisabledError() {
1313
let remoteError = DotcomError.statsModuleDisabled
14-
let error = SiteVisitStatsStoreError(error: remoteError)
14+
let error = SiteStatsStoreError(error: remoteError)
1515
XCTAssertEqual(error, .statsModuleDisabled)
1616
}
1717

1818
func testOtherDotcomError() {
1919
let remoteError = DotcomError.unknown(code: "invalid_blog", message: "This blog does not have Jetpack connected")
20-
let error = SiteVisitStatsStoreError(error: remoteError)
20+
let error = SiteStatsStoreError(error: remoteError)
2121
XCTAssertEqual(error, .unknown)
2222
}
2323

2424
func testNonDotcomRemoteError() {
2525
let remoteError = NSError(domain: "Woo", code: 404, userInfo: nil)
26-
let error = SiteVisitStatsStoreError(error: remoteError)
26+
let error = SiteStatsStoreError(error: remoteError)
2727
XCTAssertEqual(error, .unknown)
2828
}
2929
}

Yosemite/YosemiteTests/Stores/StatsStoreV4Tests.swift

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,124 @@ final class StatsStoreV4Tests: XCTestCase {
503503
let storageTopEarnerStats = viewStorage.loadTopEarnerStats(date: "2020", granularity: StatGranularity.year.rawValue)
504504
XCTAssertEqual(storageTopEarnerStats?.toReadOnly(), expectedTopEarnerStats)
505505
}
506+
507+
// MARK: - StatsStoreV4.retrieveSiteSummaryStats
508+
509+
/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` returns any retrieved SiteSummaryStats.
510+
///
511+
func test_retrieveSiteSummaryStats_returns_retrieved_stats() throws {
512+
// Given
513+
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
514+
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/summary/", filename: "site-summary-stats")
515+
516+
// When
517+
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
518+
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
519+
period: .day,
520+
quantity: 1,
521+
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in
522+
promise(result)
523+
}
524+
store.onAction(action)
525+
}
526+
527+
// Then
528+
XCTAssertTrue(result.isSuccess)
529+
let siteSummaryStats = try XCTUnwrap(result).get()
530+
XCTAssertEqual(siteSummaryStats, sampleSiteSummaryStats())
531+
}
532+
533+
/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` makes the expected alternate network request for multiple stats periods.
534+
///
535+
func test_retrieveSiteSummaryStats_makes_expected_network_request_for_multiple_periods() throws {
536+
// Given
537+
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
538+
539+
// When
540+
let _: Void = waitFor { promise in
541+
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
542+
period: .month,
543+
quantity: 3,
544+
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55")) { _ in
545+
promise(())
546+
}
547+
store.onAction(action)
548+
}
549+
550+
// Then
551+
let request = try XCTUnwrap(network.requestsForResponseData.first as? DotcomRequest)
552+
XCTAssertEqual(request.path, "sites/\(sampleSiteID)/stats/visits/")
553+
XCTAssertEqual(request.parameters?["date"] as? String, "2022-12-31")
554+
XCTAssertEqual(request.parameters?["unit"] as? String, "month")
555+
XCTAssertEqual(request.parameters?["quantity"] as? String, "3")
556+
}
557+
558+
/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` converts and returns SiteSummaryStats for multiple periods.
559+
///
560+
func test_retrieveSiteSummaryStats_returns_retrieved_quarter_stats() throws {
561+
// Given
562+
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
563+
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/visits/", filename: "site-visits-quarter")
564+
565+
// When
566+
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
567+
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
568+
period: .month,
569+
quantity: 3,
570+
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-31T17:06:55")) { result in
571+
promise(result)
572+
}
573+
store.onAction(action)
574+
}
575+
576+
// Then
577+
XCTAssertTrue(result.isSuccess)
578+
let siteSummaryStats = try XCTUnwrap(result).get()
579+
XCTAssertEqual(siteSummaryStats, sampleSiteSummaryStatsQuarter())
580+
}
581+
582+
/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` returns an error whenever there is an error response from the backend.
583+
///
584+
func test_retrieveSiteSummaryStats_returns_error_upon_response_error() {
585+
// Given
586+
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
587+
network.simulateResponse(requestUrlSuffix: "sites/\(sampleSiteID)/stats/summary/", filename: "generic_error")
588+
589+
// When
590+
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
591+
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
592+
period: .day,
593+
quantity: 1,
594+
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in
595+
promise(result)
596+
}
597+
store.onAction(action)
598+
}
599+
600+
// Then
601+
XCTAssertTrue(result.isFailure)
602+
}
603+
604+
/// Verifies that `StatsActionV4.retrieveSiteSummaryStats` returns an error whenever there is no backend response.
605+
///
606+
func test_retrieveSiteSummaryStats_returns_error_upon_empty_response() {
607+
// Given
608+
let store = StatsStoreV4(dispatcher: dispatcher, storageManager: storageManager, network: network)
609+
610+
// When
611+
let result: Result<SiteSummaryStats, Error> = waitFor { promise in
612+
let action = StatsActionV4.retrieveSiteSummaryStats(siteID: self.sampleSiteID,
613+
period: .day,
614+
quantity: 1,
615+
latestDateToInclude: DateFormatter.dateFromString(with: "2022-12-09T17:06:55")) { result in
616+
promise(result)
617+
}
618+
store.onAction(action)
619+
}
620+
621+
// Then
622+
XCTAssertTrue(result.isFailure)
623+
}
506624
}
507625

508626

@@ -683,4 +801,22 @@ private extension StatsStoreV4Tests {
683801
currency: "",
684802
imageUrl: "https://dulces.mystagingwebsite.com/wp-content/uploads/2020/07/img_7472-scaled.jpeg")
685803
}
804+
805+
// MARK: - Site Summary Stats Sample
806+
807+
func sampleSiteSummaryStats() -> Networking.SiteSummaryStats {
808+
return SiteSummaryStats(siteID: sampleSiteID,
809+
date: "2022-12-09",
810+
period: .day,
811+
visitors: 12,
812+
views: 123)
813+
}
814+
815+
func sampleSiteSummaryStatsQuarter() -> Networking.SiteSummaryStats {
816+
return SiteSummaryStats(siteID: sampleSiteID,
817+
date: "2022-12-09",
818+
period: .month,
819+
visitors: 243,
820+
views: 486)
821+
}
686822
}

0 commit comments

Comments
 (0)