Commit ac44ac1a by Dmitriy Stepanets

Added analytics to the shorts

parent 7b560730
......@@ -12,7 +12,7 @@
<key>OneWeatherNotificationServiceExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>59</integer>
<integer>61</integer>
</dict>
<key>PG (Playground) 1.xcscheme</key>
<dict>
......
......@@ -236,7 +236,7 @@ extension PushNotificationsManager: UNUserNotificationCenterDelegate {
if UIApplication.shared.applicationState == .active {
analytics(log: .ANALYTICS_PUSH_RECEIVED)
} else {
analytics(log: .ANALYTICS_PUSH_SELECTED, params: [.ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE: "background"])
analytics(log: .ANALYTICS_PUSH_SELECTED, params: [.ANALYTICS_KEY_SOURCE: "background"])
}
log.debug("Got a push notification: \(notification.request.content.userInfo)");
......
......@@ -82,7 +82,6 @@ class ShortsManager {
return
}
print("[ShortsManager] Mark short as viewed at index: \(sourceIndex)")
self.shorts[sourceIndex].markAsViewed()
}
}
......@@ -299,7 +299,6 @@ class ForecastTimePeriodView: UIView {
self.graphView.drawAdditionalGraph(with: [CGPoint]())
self.tintGraphAt(button: selectedButton)
}
print("[ForecastTimePeriod] Draw graph")
}
......
......@@ -8,6 +8,7 @@
import UIKit
import OneWeatherCore
import pop
import OneWeatherAnalytics
private enum ScrollDirection {
case swipeDown //toTop
......@@ -26,9 +27,19 @@ class ShortsViewController: UIViewController {
private var lastOffset: CGFloat = 0
private var itemIndexToScroll: Int?
private var swipeUpCounter = 0
private var source: String?
private var shortsViewTimeSpentSec = 0
private lazy var viewScheduler: Scheduler = {
let scheduler = Scheduler(with: "shorts_binge_view_time",
interval: 1,
onlyCountTimeWhenRunning: true,
action: {
self.shortsViewTimeSpentSec += 1
})
return scheduler
}()
deinit {
print("[ShortsViewController] deinit")
coordinator.viewControllerDidEnd(controller: self)
}
......@@ -54,26 +65,43 @@ class ShortsViewController: UIViewController {
super.viewWillAppear(animated)
guard !viewModel.shorts.isEmpty else { return }
viewScheduler.start()
if let indexToScroll = itemIndexToScroll {
source = "card_click"
tableView.scrollToRow(at: [0, indexToScroll], at: .top, animated: false)
lastOffset = tableView.contentOffset.y
updateView(basedOnDirection: .swipeUp, willShowRowAt: indexToScroll)
itemIndexToScroll = nil
viewModel.markAsViewed(atIndex: indexToScroll)
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_VIEW_SHORTS,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : viewModel.shorts[indexToScroll].id,
.ANALYTICS_KEY_SOURCE : "card_click"])
}
else {
source = "others"
lastOffset = 0
updateView(basedOnDirection: .swipeDown, willShowRowAt: 0)
tableView.scrollToRow(at: [0, 0], at: .top, animated: false)
viewModel.markAsViewed(atIndex: 0)
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_VIEW_SHORTS,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : viewModel.shorts[0].id,
.ANALYTICS_KEY_SOURCE : "others"])
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
source = nil
viewScheduler.stop()
viewScheduler.resetTimer()
shortsViewTimeSpentSec = 0
swipeUpCounter = 0
ShortsManager.shared.reordering()
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_EXIT_SHORTS_VIEW, params: nil)
}
private func scrollTo(newOffset:CGPoint, velocity:CGPoint) {
......@@ -145,6 +173,40 @@ class ShortsViewController: UIViewController {
}
private func logShortsAnalytics(source: String, forRowAtIndex index: Int) {
let shortsSource: String
if self.source != nil {
shortsSource = self.source!
self.source = nil
}
else {
shortsSource = source
}
var timeSpent: String
switch shortsViewTimeSpentSec {
case 0...2:
timeSpent = "<3s"
case 3...4:
timeSpent = "3-5s"
case 5...9:
timeSpent = "5-10s"
case 10...19:
timeSpent = "10-20s"
case 20...Int.max:
timeSpent = ">20s"
default:
timeSpent = ""
}
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_CARD_BINGE_VIEW,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : viewModel.shorts[index].id,
.ANALYTICS_KEY_SOURCE : shortsSource,
.ANALYTICS_KEY_SHORTS_TIME_SPENT : timeSpent])
viewScheduler.resetTimer()
shortsViewTimeSpentSec = 0
}
func set(indexToScroll: Int) {
itemIndexToScroll = indexToScroll
}
......@@ -251,13 +313,15 @@ extension ShortsViewController: UITableViewDelegate {
updateView(basedOnDirection: direction, willShowRowAt: nextRowIndexPath.row)
let offset = tableView.contentSize.height - tableView.frame.height
self.scrollTo(newOffset: .init(x: tableView.contentOffset.x, y: offset), velocity: velocity)
self.logShortsAnalytics(source: "swipe_up", forRowAtIndex: rowsCount - 1)
return
}
case .swipeDown:
if topRowIndexPath.row == 0 {
self.scrollTo(newOffset: .zero, velocity: velocity)
viewModel.markAsViewed(atIndex: 0)
updateView(basedOnDirection: direction, willShowRowAt: 0)
self.scrollTo(newOffset: .zero, velocity: velocity)
self.logShortsAnalytics(source: "swipe_down", forRowAtIndex: 0)
return
}
......@@ -268,6 +332,7 @@ extension ShortsViewController: UITableViewDelegate {
viewModel.markAsViewed(atIndex: nextRowIndexPath.row)
updateView(basedOnDirection: direction, willShowRowAt: nextRowIndexPath.row)
self.scrollTo(newOffset: nextRowRect.origin, velocity: velocity)
self.logShortsAnalytics(source: direction == .swipeDown ? "swipe_down" : "swipe_up", forRowAtIndex: nextRowIndexPath.row - 1)
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
......
......@@ -184,6 +184,8 @@ class TodayCellFactory: CellFactoryProtocol {
moonCell.updateMoonPosition()
case let adCell as AdCell:
adCell.adView?.start()
case cell as TodayShortsCell:
todayViewModel.logShortsSectionEvent()
default:
break
}
......
......@@ -7,6 +7,7 @@
import UIKit
import OneWeatherCore
import OneWeatherAnalytics
protocol ShortsViewDelegate: AnyObject {
func didSelectShort(at index:Int)
......@@ -87,8 +88,20 @@ extension ShortsView: UICollectionViewDelegate {
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let visiblePath = (collectionView.indexPathsForVisibleItems.sorted{$0.row < $1.row})
guard let lastVisibleRow = visiblePath.last?.row else { return }
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_CARD_VIEW,
params: [.ANALYTICS_KEY_SHORTS_CARD_POSITION : lastVisibleRow])
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.didSelectShort(at: indexPath.row)
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_CARD_CLICK,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : shorts[indexPath.row].id,
.ANALYTICS_KEY_SHORTS_CARD_POSITION : indexPath.row,
.ANALYTICS_KEY_SOURCE : "TODAY"])
}
}
......
......@@ -7,6 +7,7 @@
import UIKit
import OneWeatherCore
import OneWeatherAnalytics
class ShortsViewModel: ViewModelProtocol {
//Private
......@@ -30,12 +31,17 @@ class ShortsViewModel: ViewModelProtocol {
func like(item: ShortsItem) {
shortsManager.like(item: item)
self.delegate?.viewModelDidChange(model: self)
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_LIKE_BUTTON_CLICK,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : item.id])
}
func openMore(item: ShortsItem) {
guard let url = item.ctaURL else { return }
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [:])
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_READ_MORE_CLICK,
params: [.ANALYTICS_KEY_SHORTS_CARD_ID : item.id,
.ANALYTICS_KEY_SHORTS_READ_MORE_VIEW : "external browser"])
}
}
......
......@@ -20,9 +20,12 @@ class TodayViewModel: ViewModelProtocol {
public weak var delegate:TodayViewModelDelegate?
//Private
private let kAnalyticsThresholdSec = 2.5
private let log = Logger(componentName: "TodayViewModel")
private let locationManager: LocationManager! = LocationManager.shared
private var ccpaHelper = CCPAHelper.shared
private let shortsManager:ShortsManager
private var canSendAnalytics = true
private(set) var location:Location?
var shorts: [ShortsItem] {
return ShortsManager.shared.shorts
......@@ -47,6 +50,7 @@ class TodayViewModel: ViewModelProtocol {
NotificationCenter.default.addObserver(self, selector: #selector(handlePremiumStateChange), name: Notification.Name(rawValue: kEventInAppPurchasedCompleted), object: nil)
}
//MARK: Private
@objc
private func handlePremiumStateChange() {
onMain {
......@@ -55,13 +59,6 @@ class TodayViewModel: ViewModelProtocol {
}
}
public func updateWeather() {
locationManager.updateEverythingIfNeeded()
if shortsManager.shortsAvailable {
shortsManager.refreshShorts()
}
}
private func initializeAllAdsIfNeeded() {
onMain {
let adManager = AdManager.shared
......@@ -83,7 +80,19 @@ class TodayViewModel: ViewModelProtocol {
}
}
private var ccpaHelper = CCPAHelper.shared
private func onboardingFlowCompleted() {
self.initializeAllAdsIfNeeded()
PushNotificationsManager.shared.registerForRemoteNotifications()
analytics(log: .ANALYTICS_VIEW_TODAY)
}
//MARK: Public
public func updateWeather() {
locationManager.updateEverythingIfNeeded()
if shortsManager.shortsAvailable {
shortsManager.refreshShorts()
}
}
public func privacyPolicyHasBeenViewed() {
ccpaHelper.shownPrivacyNoticeBefore = true
......@@ -112,14 +121,17 @@ class TodayViewModel: ViewModelProtocol {
}
}
public func openShorts(atIndex index: Int) {
AppCoordinator.instance.openShorts(atIndex: index)
public func logShortsSectionEvent() {
guard canSendAnalytics else { return }
canSendAnalytics = false
AppAnalytics.shared.log(event: .ANALYTICS_SHORTS_SECTION_VIEW, params: nil)
DispatchQueue.main.asyncAfter(deadline: .now() + kAnalyticsThresholdSec) {
self.canSendAnalytics = true
}
}
private func onboardingFlowCompleted() {
self.initializeAllAdsIfNeeded()
PushNotificationsManager.shared.registerForRemoteNotifications()
analytics(log: .ANALYTICS_VIEW_TODAY)
public func openShorts(atIndex index: Int) {
AppCoordinator.instance.openShorts(atIndex: index)
}
}
......
......@@ -89,6 +89,14 @@ public enum AnalyticsEvent: String {
case ANALYTICS_A9_BID_RECEIVED = "A9_BID_RECEIVED"
case ANALYTICS_AD_CLICKED = "AD_CLICKED"
case ANALYTICS_APP_OPEN = "APP_OPEN"
case ANALYTICS_SHORTS_SECTION_VIEW = "SHORTS_SECTION_VIEW"
case ANALYTICS_SHORTS_CARD_VIEW = "SHORTS_CARD_VIEW"
case ANALYTICS_SHORTS_CARD_CLICK = "SHORTS_CARD_CLICK"
case ANALYTICS_SHORTS_CARD_BINGE_VIEW = "SHORTS_CARD_BINGE_VIEW"
case ANALYTICS_SHORTS_VIEW_SHORTS = "VIEW_SHORTS"
case ANALYTICS_SHORTS_READ_MORE_CLICK = "READ_MORE_CLICK"
case ANALYTICS_SHORTS_LIKE_BUTTON_CLICK = "LIKE_BUTTON_CLICK"
case ANALYTICS_SHORTS_EXIT_SHORTS_VIEW = "EXIT_SHORTS_VIEW"
/// FTUE Funnel: User has saved his first city after installing the app.
case ANALYTICS_USER_QUALIFIED = "USER_QUALIFIED"
......
......@@ -9,6 +9,7 @@ import Foundation
public enum AnalyticsParameter: String {
//TODO: rename to Swifty names. This is a legacy from the old app.
case ANALYTICS_KEY_SOURCE = "source"
case ANALYTICS_KEY_CCPA_TODAY_DISMISS_ACTION = "ACTION"
case ANALYTICS_KEY_CCPA_TODAY_CCPA_EXPT_3_VERSION = "CCPA_EXPT_3_VERSION"
case ANALYTICS_KEY_LAST_SEEN_CITY_CITY_ID = "cityId"
......@@ -18,7 +19,10 @@ public enum AnalyticsParameter: String {
case ANALYTICS_KEY_PLACEMENT_NAME = "placement_name"
case ANALYTICS_KEY_AD_UNIT_ID = "AD_PLACEMENT_ID"
case ANALYTICS_KEY_AD_ADAPTER = "AD_ADAPTER"
case ANALYTICS_KEY_PUSH_NOTIFICATION_SOURCE = "source"
case ANALYTICS_KEY_THEME_CHANGE_NAME = "themeName"
case ANALYTICS_KEY_FIST_OPEN_SOURCE = "Source"
case ANALYTICS_KEY_SHORTS_CARD_POSITION = "position"
case ANALYTICS_KEY_SHORTS_CARD_ID = "card_id"
case ANALYTICS_KEY_SHORTS_TIME_SPENT = "time_spent"
case ANALYTICS_KEY_SHORTS_READ_MORE_VIEW = "view"
}
......@@ -10,7 +10,11 @@ import AppsFlyerLib
internal struct AppsFlyerAnalyticsService: AnalyticsService {
public let name: String = "AppsFlyer"
let eventsWhitelist: Set<AnalyticsEvent>? = [.ANALYTICS_APP_OPEN, .ANALYTICS_USER_QUALIFIED, .ANALYTICS_D3_RETAINED, .ANALYTICS_FIRST_OPEN]
let eventsWhitelist: Set<AnalyticsEvent>? = [.ANALYTICS_APP_OPEN,
.ANALYTICS_USER_QUALIFIED,
.ANALYTICS_D3_RETAINED,
.ANALYTICS_FIRST_OPEN,
.ANALYTICS_SHORTS_CARD_BINGE_VIEW]
let attributesWhitelist: Set<AnalyticsAttribute>? = [] // block all
func log(event: AnalyticsEvent, params: [AnalyticsParameter : Any]?) {
......
import Foundation
import UIKit
extension Array {
mutating func move(from oldIndex: Index, to newIndex: Index) {
// Don't work for free and use swap when indices are next to each other - this
// won't rebuild array and will be super efficient.
if oldIndex == newIndex { return }
if abs(newIndex - oldIndex) == 1 { return self.swapAt(oldIndex, newIndex) }
self.insert(self.remove(at: oldIndex), at: newIndex)
}
}
struct Element {
let isViewed: Bool
let value: String
}
var arr:[Element] = [.init(isViewed: true, value: "a"),
.init(isViewed: true, value: "b"),
.init(isViewed: true, value: "c"),
.init(isViewed: false, value: "d"),
.init(isViewed: false, value: "e"),
.init(isViewed: false, value: "f"),
.init(isViewed: false, value: "g"),
.init(isViewed: false, value: "h"),
.init(isViewed: false, value: "i")]
var indexesToReorder = [Int]()
for (index, element) in arr.enumerated() {
if element.isViewed {
indexesToReorder.append(index)
}
}
arr = arr.filter{!$0.isViewed} + arr.filter{$0.isViewed}
//for index in indexesToReorder {
// arr.move(from: index, to: arr.count - 1)
//}
print(arr.map{$0.value})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment