Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Conversation

@kean
Copy link
Contributor

@kean kean commented Jul 18, 2025

New Features

  • Email Opens Stats: Added support for fetching email opens statistics with new StatsEmailOpensData model
  • Archive Stats: Introduced StatsArchiveTimeIntervalData for generalized archive endpoint parsing
  • Hourly Stats Support: Added StatsPeriodUnit.hour to enable hourly statistics data
  • Site Metrics: Added StatsSiteMetricsResponse for improved site metrics handling

API Enhancements

  • Extended StatsServiceRemoteV2.getData with startDate, timeZone, and fields parameters
  • Added summarize parameter support for stats requests
  • Enhanced StatsPostDetails to include all available data fields

WordPressKit.zip

@dangermattic
Copy link
Collaborator

dangermattic commented Jul 18, 2025

2 Warnings
⚠️ Package.swift was changed without updating its corresponding Package.resolved. Please resolve the Swift packages as appropriate to your project setup (e.g. in Xcode or by running swift package resolve).
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.

Generated by 🚫 Danger

@kean kean force-pushed the task/new-stats branch from 4fa9491 to 84eb859 Compare July 19, 2025 19:18
@kean kean force-pushed the task/new-stats branch from 84eb859 to 9139c2f Compare July 20, 2025 14:39
@kean kean force-pushed the task/new-stats branch from f29697a to 99ebbc8 Compare July 21, 2025 17:57
@kean kean force-pushed the task/new-stats branch from 99ebbc8 to 392d2d3 Compare July 21, 2025 18:21
@kean kean force-pushed the task/new-stats branch from 9c20c99 to 32f9410 Compare July 22, 2025 19:17
@kean kean requested a review from crazytonyli July 30, 2025 22:58
uniqueOpens = try container.decodeIfPresent(Int.self, forKey: .uniqueOpens)
totalOpens = try container.decodeIfPresent(Int.self, forKey: .totalOpens)
opensRate = try container.decodeIfPresent(Double.self, forKey: .opensRate)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this function? It's the same as the default derived implementation, if I'm not mistaken?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No really - removing.

}

extension StatsEmailOpensData {
public init?(jsonDictionary: [String: AnyObject]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw instead of silencing the error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I auto-generated this code with the rest of the file as a reference. It is consistent with the rest of StatsServiceRemoteV2 and how it's used. I'd suggest keeping it as it's throw-away code anyway.

public let totalSends: Int?
public let uniqueOpens: Int?
public let totalOpens: Int?
public let opensRate: Double?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all these properties be null? Will the response ever be null, like no total_sends or total_send: null in the response?

Copy link
Contributor Author

@kean kean Aug 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd change it, but it's a throw-away code, so I'm not sure it's worth reworking it and the usage. I'm not sure I have any energy left at this point 😪


static var hourlyDateFormatter: DateFormatter {
let df = DateFormatter()
df.locale = Locale(identifier: "en_US_POS")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the id supposed to be en_US_POSIX?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. This code is unused – the new stats don't request hourly data for subscribers and there is no hourly data for subscribers. The existing formatters in this screen use en_US_POS. I didn't want to make changes to the existing code in the scope of this change.

let dateFormatter = makeDateFormatter(for: period)

self.data = data.compactMap { data in
guard let periodDate = dateFormatter.date(from: data[periodIndex] as? String ?? "") else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick:

Suggested change
guard let periodDate = dateFormatter.date(from: data[periodIndex] as? String ?? "") else {
guard let date = data[periodIndex] as? String, let periodDate = dateFormatter.date(from: date) else {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied.


let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'd need to set the timeZone property to parse GMT date strings that do not have a time zone in them, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an existing issue across all existing decodes in this service. The date is actually returned in the site time zone. The decoders don't know what is the time zone of the site. The response also doesn't contain it. So it end's up creating a wrong timestamp in your local time zone.

I added code to map it in StatsService https://github.com/wordpress-mobile/WordPress-iOS/blob/b82374372485a73d0a3f49bc5cbc3886a3179767/Modules/Sources/JetpackStats/Services/StatsService.swift#L355, but I think I made a mistake at one of the layers. I opened a ticket and will debug it later on Monday: https://linear.app/a8c/issue/CMM-642/dates-are-in-the-wrong-timezone. The fix needs to be on the app level. I want to keep the WPKit behavior as is so it doesn't break the legacy screens.

var staticProperties = ["period": period.stringValue,
"unit": unit?.stringValue ?? period.stringValue,
"date": periodDataQueryDateFormatter.string(from: endingOn)] as [String: AnyObject]
"date": dateFormatter.string(from: endingOn)] as [String: AnyObject]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: I think you can get rid of all the as statements below if you declare the staticProperties as [String: Any].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it support Any, but I just didn't want to change it since it's the existing code. There was already too many changes, and I wanted to keep the changes to the existing code minimum.

// MARK: - Email Opens

public extension StatsServiceRemoteV2 {
func getEmailOpens(for postID: Int, completion: @escaping ((StatsEmailOpensData?, Error?) -> Void)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about using async functions instead of calls for the new API calls? I presume the callers are written in Swift too, which may prefer async functions?

posts: fields.firstIndex(of: Metric.posts.rawValue)
)

let dateFormatter = makeDateFormatter(for: period)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is this comment above: "/// Periods date in the site timezone."

It's impossible to parse a date string if we don't know what the site timezone is, right? So, it's incorrect to use any DateFormatter instance. I think the correct solution is either passing the site timezone from the app1 to the parsing function here, or returning a plain string and letting the app parse it.

Footnotes

  1. Or, making another HTTP request to get site timezone, which does feel like wasteful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll revisit this in the scope of #843 (comment). The app knows the time zone, so it will need to perform the conversion.

In reality, StatsServiceRemoteV2 simply uses incorrect types to represent dates. If It doesn't know what time zone the dates component belong to, it should never returned Date (timestamp). It ends up always creating wrong time stamps (in your app current time zone instead of the site time zone). Unfortunately, it is what it is.

@crazytonyli
Copy link
Contributor

There are merge conflicts and failing unit tests.

@kean
Copy link
Contributor Author

kean commented Aug 4, 2025

I merged trunk, recompiled the framework, and the tests are now successful. Looks like it was related to the SwiftLint changes.

@kean kean requested a review from crazytonyli August 4, 2025 17:12
@kean kean merged commit 7d8c756 into trunk Aug 5, 2025
7 checks passed
@kean kean deleted the task/new-stats branch August 5, 2025 18:29
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants