Skip to content

Commit 8601ae5

Browse files
authored
Feature: New Stats (#24677)
* Update chart to take more horizontal space * Improve more menu alignment * More breezing room * Show top 5 * Simplify TopListItemView requirements * Add previews for top list items * Add cache for mock data * Simplify mock data generation * Set min height for bars * Add initial implementation * Rework await Task.detached(priority: .userInitiated) { * Add zoom * Refactor * Fix reloading in dark mode * Improve colors * Add initial hover support * Extract interactive-map-template * Improve tooltip view * Refactor * Clear map selection when you stop hovering * lintfix * Add TopListCard previews * Improve TopListLocationRowView design * Cleanup isNavigationDisabled * Remove map dependency * Fix animation in TrafficTabView * Disable zoom in InteractiveMapView * Improve sizing of TopListItemViews * More design changes * Implement chart animation on first load * Fix LineChartView sometimes showing zero in the middle * Fix clipping of AM label * Simplify selection on map * Fix an issue with SVGWebView not reloading if killed * Fix retain cycles * Extract CountriesMapData * Remove isNavigationDisabled * Use gradient in LineChartView * Use gradient in BarChartView * Use gradient for Top List also * More compelling gradients * Add significant data points, add pulsating anmation * Add significant data point to bar chart also * use rounded in badges * Extract SignificantPointAnnotation * Rework ARchive * Add tap animation for TopListItemView * Add context menus * Add context menus for items * Remove isComparisonPeriodEnabled * Remove isNavigationDisabled * Remove odd looking background when tapping Top List items * Implement pulsating for overlay too * Rework shapes * Fix showmore lyout * Nicer badges * Nicer video display * Add support for external links * Add more performance reserveSpace * Add initial TopListScreenView * Tap on card to open entire screen * Rename TopListViewModel * Add TopListChartData.Metrics * Rename TopListResponse * Rename TopListItemProtocol * Rename TopListItem * Rename TopListData * Add secondaryBackground * Add more eleborate background * Simplify background * Add date range control * Add CSV export * Optimize CSV export * Fix sorting for top list items to ensure stable ordering * use ShareLink * Darker empty row * Fix navigation from top list * lintfix * Fix export – use CSV extensio * Fix items not loading in mock service * Remove Calendar.current usages * Update unit tests * Smaller heading in ListScreenView * Remove broken LazyVStack and simplify interactive-map-template * Fix selection for country map * PRevent ScrollOffsetModifier to firing when no changes are made * Show only 5 recent years on a trend ma[ * Fix StatsDateRangeFormatter ntot showing last year * Update StatsDateRangeFormatterTests tests to be independant of year * Load aatars with higher quality * Cleanup StatsServiceRemoteV2 extensions * Fix TopList being tappable on mock data * Smaller ChartValuesSummaryView * Add documentation links * Add reporting time zone to the bottom of the tab * Nicer empty state view * Slight chart card redesign * Fix compilation error on earlier Xcode versions * Better ChartCard header * Update StandaloneChartCard * Center top list screen header vertically * Add initial StatsViewModel * Remove unused comparisonRangeText * Add initial ChartCardCustomizationView * Rework where config is created * Add initial delete widget support * Add support for deleting charts * Rename delete * Rework how we save configuration * Move seletedMetric to ViewModel * Save ChartType persistently * Improve gradients * Cleanup * Fix significantPointAnnotations in line chart * Rework ChartCardCustomizationView * Add TopListCardCustomizationView * Refactor TopListCardCustomizationView * Remove StatsViewModel from TopListCard * Improve chart editig * Improve add card style * Implement Move Card feature * Simplify EditCardMenuContent * Remove unused strings * Update menus org * SwiftLint * Cleanup * Better PlaceholderRowView * Better empty rows * Lighter bars in dark mode * Lighter bars in dark mode * Better contrast on a map * Fix map selection * Better loading state for avatars * Reorg * Cleanup * Fix StatsPeriodUnit crash * Fix navigateToTopListScreen * Fix navigation title not appearing instantly * Dsable sharing for now * Cleanup * Store preset persistently * Add reset all settings for convenience * Minor iPad improvemnets * Add caching * Add new menus to enable/disable stats * Rework how we show the new stats * Add SafeAreaHostingController to workaround safe area issue * Add extra top padding * Add a way to show mock data * Cleanup StatsHostingViewController * Add configureModernStyle to FilterTabBar * Shorten interval for editing * Update l10n * Add delay when showing tip * Fix an issue with interpolation when there is only one data point * Increase listHeaderView spacing * Add l10n * Fix long button names * Fix TopListCard in AuthorStatsView * Improve formatting in date range control * Add CardGradientBackground * Fix an issue with filter not passed to TopListScreen * Fix minimum size for subscriber rows * Fix an issue with authors posts showing wrong data on show more * Improve mock data * Show debug options in Stats in the debug menu * Simplify filter tab bar * Fix y domain on BarChartView * Add subscriber details * Reverse raw data * use regular context for date formatting * Update historic data * Initial iPad support * Remove insets from the CardModifier itself * Add a reusable method to set horizontal inset * Cleanup * Move range controls to hte top on iPad * More improvements on iPad * Fix how trailingItemGroups is communicated back the to main VC * Cleanup buttons * Show more cards on iPad * Improve design on PostStatsView * Save StandaloneChart type persistently; update PostStatsView on iPad * Add empty state * Update AuthorStatsView * SHow date navigation at the top * Update ChartDataListView * Update ExternalLinkStatsView * Update referrers * TopListScreenView to support iPad * Update TopListScreenView * Reduce trailing in TopListItemView * Fix realtime view * More spacing for metrics on iPad * Improve ChartValueTooltipView * Pink for likes * Improve menu design * Update ChartDataListView design * More improvemnets * Slightly rounder bars * Update ChartCard again * Change last5 to last3 years and use monthly granularity * Update tess * Improve average badge design * Cleanup * Fix an issue with selection line going beyond the chart range * Extract ChartAverageLine * Improve tab over view anmations * Remove fatalError * Add StatsEvent and StatsTracker * Add additional events for customization * Add mapping to WPAnalyticsEvent * Add new_stats key to .statsAccessed * Cleanup urlErrorType * Do no track CancellationError * Improve heatmap colors for dark mode * Improve annotations * Add dynamic type support * Improve line chart scaling * Add voice over support * Fix an issue with new stats menu not shownig up on iphone * Less visible TopList empty views * Fix tab size in overview * Fix insets on top list * Fix disabling of navigation button in top bar * Add a bit of top inset * Fix tests build * Remove animations from add chart button * Lower h spacing on ipad * Remove animations from ChartValuesSummaryView * Selection tooltip no longer moves with content * Remove delay when showing details * Use black for tint * Update articles * Fix 642: invalid hourly data * Update time zone design * Update WordPressKit to point to the latest commit * Revert "Fix minimum size for subscriber rows" This reverts commit 33ce879. * Replace fatalError with assertionFailure
1 parent 4adf1f7 commit 8601ae5

187 files changed

Lines changed: 23502 additions & 53 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Modules/Package.resolved

Lines changed: 2 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ let package = Package(
1111
.library(name: "AsyncImageKit", targets: ["AsyncImageKit"]),
1212
.library(name: "DesignSystem", targets: ["DesignSystem"]),
1313
.library(name: "FormattableContentKit", targets: ["FormattableContentKit"]),
14+
.library(name: "JetpackStats", targets: ["JetpackStats"]),
1415
.library(name: "JetpackStatsWidgetsCore", targets: ["JetpackStatsWidgetsCore"]),
1516
.library(name: "NotificationServiceExtensionCore", targets: ["NotificationServiceExtensionCore"]),
1617
.library(name: "ShareExtensionCore", targets: ["ShareExtensionCore"]),
@@ -49,7 +50,7 @@ let package = Package(
4950
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"),
5051
.package(
5152
url: "https://github.com/wordpress-mobile/WordPressKit-iOS",
52-
revision: "2b7d4f6acf2641b671c66b20873f5935f22210ed"
53+
revision: "440d94e3a3d6f9f39035a371984e088a2fb42a32"
5354
),
5455
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
5556
// We can't use wordpress-rs branches nor commits here. Only tags work.
@@ -93,6 +94,14 @@ let package = Package(
9394
// Set to v5 to avoid @Sendable warnings and errors
9495
swiftSettings: [.swiftLanguageMode(.v5)]
9596
),
97+
.target(
98+
name: "JetpackStats",
99+
dependencies: [
100+
"WordPressUI",
101+
.product(name: "WordPressKit", package: "WordPressKit-iOS"),
102+
],
103+
resources: [.process("Resources")]
104+
),
96105
.target(name: "JetpackStatsWidgetsCore", swiftSettings: [.swiftLanguageMode(.v5)]),
97106
.target(
98107
name: "ShareExtensionCore",
@@ -171,6 +180,7 @@ let package = Package(
171180
dependencies: ["AsyncImageKit", "WordPressUI", "WordPressShared"],
172181
resources: [.process("Resources")]
173182
),
183+
.testTarget(name: "JetpackStatsTests", dependencies: ["JetpackStats"]),
174184
.testTarget(name: "JetpackStatsWidgetsCoreTests", dependencies: [.target(name: "JetpackStatsWidgetsCore")], swiftSettings: [.swiftLanguageMode(.v5)]),
175185
.testTarget(name: "DesignSystemTests", dependencies: [.target(name: "DesignSystem")], swiftSettings: [.swiftLanguageMode(.v5)]),
176186
.testTarget(
@@ -276,6 +286,7 @@ enum XcodeSupport {
276286
"DesignSystem",
277287
"BuildSettingsKit",
278288
"FormattableContentKit",
289+
"JetpackStats",
279290
"JetpackStatsWidgetsCore",
280291
"NotificationServiceExtensionCore",
281292
"SFHFKeychainUtils",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
/// A no-op implementation of StatsTracker for testing and development
4+
final class MockStatsTracker: StatsTracker, Sendable {
5+
static let shared = MockStatsTracker()
6+
7+
private init() {}
8+
9+
func send(_ event: StatsEvent, properties: [String: String]) {
10+
#if DEBUG
11+
// In debug builds, print events to console for debugging
12+
debugPrint("[StatsTracker] Event: \(event) \(properties)")
13+
#endif
14+
}
15+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import Foundation
2+
3+
/// Analytics events for tracking user interactions within the Stats module
4+
///
5+
/// IMPORTANT: Do not include personally identifiable information (PII) in analytics events.
6+
/// This includes but is not limited to:
7+
/// - User IDs, author IDs, or any unique identifiers
8+
/// - Email addresses
9+
/// - URLs that might contain sensitive information
10+
/// - Post IDs or content identifiers
11+
///
12+
/// Instead, track only:
13+
/// - Event types and categories
14+
/// - Navigation sources
15+
/// - UI states and configurations
16+
/// - Aggregated metrics
17+
public enum StatsEvent {
18+
// MARK: - Screen View Events
19+
20+
/// Main stats screen shown
21+
case statsMainScreenShown
22+
23+
/// Traffic tab shown
24+
case trafficTabShown
25+
26+
/// Realtime tab shown
27+
case realtimeTabShown
28+
29+
/// Subscribers tab shown
30+
case subscribersTabShown
31+
32+
/// Post details screen shown
33+
case postDetailsScreenShown
34+
35+
/// Author stats screen shown
36+
case authorStatsScreenShown
37+
38+
/// Archive stats screen shown
39+
case archiveStatsScreenShown
40+
41+
/// External link stats screen shown
42+
case externalLinkStatsScreenShown
43+
44+
/// Referrer stats screen shown
45+
case referrerStatsScreenShown
46+
47+
// MARK: - Date Range Events
48+
49+
/// Date range preset selected
50+
/// - Parameters:
51+
/// - "selected_preset": The preset selected (e.g., "last_7_days", "last_28_days", "last_90_days", "last_365_days")
52+
case dateRangePresetSelected
53+
54+
/// Custom date range selected
55+
/// - Parameters:
56+
/// - "start_date": Start date in ISO format
57+
/// - "end_date": End date in ISO format
58+
case customDateRangeSelected
59+
60+
// MARK: - Card Events
61+
62+
/// Card shown on screen
63+
/// - Parameters:
64+
/// - "card_type": Type of card (e.g., "chart", "top_list")
65+
/// - "configuration": Card configuration details (e.g., metrics, item type)
66+
case cardShown
67+
68+
/// Card added to dashboard
69+
/// - Parameters:
70+
/// - "card_type": Type of card (e.g., "chart", "top_list")
71+
case cardAdded
72+
73+
/// Card removed from dashboard
74+
/// - Parameters:
75+
/// - "card_type": Type of card
76+
case cardRemoved
77+
78+
// MARK: - Chart Events
79+
80+
/// Chart type changed
81+
/// - Parameters:
82+
/// - "from_type": Previous chart type (e.g., "line", "bar")
83+
/// - "to_type": New chart type
84+
case chartTypeChanged
85+
86+
/// Chart metric selected
87+
/// - Parameters:
88+
/// - "metric": The metric selected (e.g., "visitors", "views", "likes")
89+
case chartMetricSelected
90+
91+
// MARK: - List Events
92+
93+
/// Top list item tapped
94+
/// - Parameters:
95+
/// - "item_type": Type of item (e.g., "posts_and_pages", "authors", "locations", "referrers")
96+
/// - "metric": The metric being sorted by
97+
case topListItemTapped
98+
99+
// MARK: - Navigation Events
100+
101+
/// Stats tab selected
102+
/// - Parameters:
103+
/// - "tab_name": Name of the tab selected
104+
/// - "previous_tab": Name of the previous tab
105+
case statsTabSelected
106+
107+
// MARK: - Error Events
108+
109+
/// Error encountered
110+
/// - Parameters:
111+
/// - "error_type": Type of error (e.g., "network", "parsing", "permission")
112+
/// - "error_code": Specific error code if available
113+
/// - "screen": Where the error occurred
114+
case errorEncountered
115+
}
116+
117+
// MARK: - StatsTracker Protocol
118+
119+
/// Protocol for tracking analytics events in the Stats module
120+
public protocol StatsTracker: Sendable {
121+
/// Send an analytics event
122+
/// - Parameters:
123+
/// - event: The event to track
124+
/// - properties: Additional properties for the event
125+
func send(_ event: StatsEvent, properties: [String: String])
126+
}
127+
128+
// MARK: - StatsTracker Convenience
129+
130+
extension StatsTracker {
131+
/// Convenience method to send events without properties
132+
func send(_ event: StatsEvent) {
133+
send(event, properties: [:])
134+
}
135+
}
136+
137+
// MARK: - Private Extensions
138+
139+
extension DateIntervalPreset {
140+
/// Analytics tracking name for the preset
141+
var analyticsName: String {
142+
switch self {
143+
case .today: "today"
144+
case .thisWeek: "this_week"
145+
case .thisMonth: "this_month"
146+
case .thisQuarter: "this_quarter"
147+
case .thisYear: "this_year"
148+
case .last7Days: "last_7_days"
149+
case .last28Days: "last_28_days"
150+
case .last30Days: "last_30_days"
151+
case .last90Days: "last_90_days"
152+
case .last6Months: "last_6_months"
153+
case .last12Months: "last_12_months"
154+
case .last3Years: "last_3_years"
155+
case .last10Years: "last_10_years"
156+
}
157+
}
158+
}
159+
160+
extension TopListItemType {
161+
/// Analytics tracking name for the item type
162+
var analyticsName: String {
163+
switch self {
164+
case .postsAndPages: "posts_and_pages"
165+
case .authors: "authors"
166+
case .referrers: "referrers"
167+
case .locations: "locations"
168+
case .videos: "videos"
169+
case .externalLinks: "external_links"
170+
case .searchTerms: "search_terms"
171+
case .fileDownloads: "file_downloads"
172+
case .archive: "archive"
173+
}
174+
}
175+
}
176+
177+
extension SiteMetric {
178+
/// Analytics tracking name for the metric
179+
var analyticsName: String {
180+
switch self {
181+
case .views: "views"
182+
case .visitors: "visitors"
183+
case .likes: "likes"
184+
case .comments: "comments"
185+
case .posts: "posts"
186+
case .timeOnSite: "time_on_site"
187+
case .bounceRate: "bounce_rate"
188+
case .downloads: "downloads"
189+
}
190+
}
191+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Foundation
2+
3+
extension StatsTracker {
4+
/// Convenience method to track errors with automatic type detection
5+
/// - Parameters:
6+
/// - error: The error to track
7+
/// - screen: The screen where the error occurred
8+
func trackError(_ error: Error, screen: String) {
9+
let errorType: String
10+
let errorCode = (error as NSError).code
11+
12+
// Determine error type based on the error instance
13+
switch error {
14+
case let urlError as URLError:
15+
errorType = urlErrorType(urlError)
16+
case is DecodingError:
17+
errorType = "parsing"
18+
case is CancellationError:
19+
return
20+
default:
21+
// Check for common error domains
22+
let nsError = error as NSError
23+
switch nsError.domain {
24+
case NSCocoaErrorDomain:
25+
errorType = "cocoa_\(errorCode)"
26+
case NSURLErrorDomain:
27+
errorType = "url_\(errorCode)"
28+
default:
29+
errorType = "unknown"
30+
}
31+
}
32+
33+
send(.errorEncountered, properties: [
34+
"error_type": errorType,
35+
"error_code": "\(errorCode)",
36+
"screen": screen
37+
])
38+
}
39+
40+
/// Determine specific network error type
41+
private func urlErrorType(_ error: URLError) -> String {
42+
switch error.code {
43+
case .notConnectedToInternet: "network_offline"
44+
case .timedOut: "network_timeout"
45+
case .cannotFindHost, .cannotConnectToHost: "network_host_unreachable"
46+
case .networkConnectionLost: "network_connection_lost"
47+
case .dnsLookupFailed: "network_dns_failed"
48+
case .httpTooManyRedirects: "network_too_many_redirects"
49+
case .resourceUnavailable: "network_resource_unavailable"
50+
case .dataNotAllowed: "network_data_not_allowed"
51+
case .secureConnectionFailed: "network_ssl_failed"
52+
default: "other"
53+
}
54+
}
55+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
enum MoveDirection {
4+
case up
5+
case down
6+
case top
7+
case bottom
8+
}
9+
10+
@MainActor
11+
protocol CardConfigurationDelegate: AnyObject {
12+
func saveConfiguration(for card: any TrafficCardViewModel)
13+
func deleteCard(_ card: any TrafficCardViewModel)
14+
func moveCard(_ card: any TrafficCardViewModel, direction: MoveDirection)
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
3+
@MainActor
4+
protocol TrafficCardViewModel: AnyObject {
5+
var id: UUID { get }
6+
var dateRange: StatsDateRange { get set }
7+
var isEditing: Bool { get set }
8+
var configurationDelegate: CardConfigurationDelegate? { get set }
9+
}

0 commit comments

Comments
 (0)