Skip to content

Commit

Permalink
feat(analytics): other iOS GA updates (#652)
Browse files Browse the repository at this point in the history
* feat(analytics): log searches

* feat(analytics): log vehicle taps, separate legacy from new stop details

* feat(analytics): log session/user variables

* fix iOS tests

* use the regular LocationDataManager for HomeMapView

avoids creating a new one every time ContentView renders and thereby spamming GA with redundant location permission updates

* set distance filter to 1m instead of 100m
  • Loading branch information
boringcactus authored Jan 15, 2025
1 parent 32f2fe7 commit 5a3ea0a
Show file tree
Hide file tree
Showing 23 changed files with 221 additions and 38 deletions.
16 changes: 16 additions & 0 deletions iosApp/iosApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
8CA606A92CC02FBC0019C448 /* ViewInspectorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA606A82CC02FBC0019C448 /* ViewInspectorExtensions.swift */; };
8CA7CDA02D359357008EE7D2 /* DestinationRowAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CD9F2D359353008EE7D2 /* DestinationRowAnalytics.swift */; };
8CA7CDAC2D3722C8008EE7D2 /* CurrentAppVersionRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CDAB2D3722C3008EE7D2 /* CurrentAppVersionRepository.swift */; };
8CA7CDA42D35DD98008EE7D2 /* SearchAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CDA32D35DD94008EE7D2 /* SearchAnalytics.swift */; };
8CA7CDA62D36C960008EE7D2 /* StopTripDetailsAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CDA52D36C95C008EE7D2 /* StopTripDetailsAnalytics.swift */; };
8CA7CDA82D36CB14008EE7D2 /* MapAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CDA72D36CB11008EE7D2 /* MapAnalytics.swift */; };
8CA7CDAA2D36E214008EE7D2 /* SessionAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CDA92D36E20F008EE7D2 /* SessionAnalytics.swift */; };
8CB28DB92C2CC5AD0036258E /* MapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB28DB82C2CC5AD0036258E /* MapViewModel.swift */; };
8CB823D62BC5E85C002C87E0 /* SheetNavigationStackEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB823D52BC5E85C002C87E0 /* SheetNavigationStackEntryTests.swift */; };
8CB823D92BC5EDD2002C87E0 /* StopDetailsRouteViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CB823D82BC5EDD2002C87E0 /* StopDetailsRouteViewTests.swift */; };
Expand Down Expand Up @@ -400,6 +404,10 @@
8CA606A82CC02FBC0019C448 /* ViewInspectorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewInspectorExtensions.swift; sourceTree = "<group>"; };
8CA7CD9F2D359353008EE7D2 /* DestinationRowAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationRowAnalytics.swift; sourceTree = "<group>"; };
8CA7CDAB2D3722C3008EE7D2 /* CurrentAppVersionRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentAppVersionRepository.swift; sourceTree = "<group>"; };
8CA7CDA32D35DD94008EE7D2 /* SearchAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAnalytics.swift; sourceTree = "<group>"; };
8CA7CDA52D36C95C008EE7D2 /* StopTripDetailsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopTripDetailsAnalytics.swift; sourceTree = "<group>"; };
8CA7CDA72D36CB11008EE7D2 /* MapAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAnalytics.swift; sourceTree = "<group>"; };
8CA7CDA92D36E20F008EE7D2 /* SessionAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionAnalytics.swift; sourceTree = "<group>"; };
8CB28DB82C2CC5AD0036258E /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = "<group>"; };
8CB823D52BC5E85C002C87E0 /* SheetNavigationStackEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetNavigationStackEntryTests.swift; sourceTree = "<group>"; };
8CB823D82BC5EDD2002C87E0 /* StopDetailsRouteViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopDetailsRouteViewTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1168,9 +1176,13 @@
9A52B3392C6E7C7D0028EEAB /* AlertDetailsAnalytics.swift */,
ED24EADD2C1A986900A7BE4D /* AnalyticsProvider.swift */,
8CA7CD9F2D359353008EE7D2 /* DestinationRowAnalytics.swift */,
8CA7CDA72D36CB11008EE7D2 /* MapAnalytics.swift */,
ED24EAD72C1A941E00A7BE4D /* NearbyTransitAnalytics.swift */,
EDE92FA52C3DD675007AD2F6 /* ScreenTracker.swift */,
8CA7CDA32D35DD94008EE7D2 /* SearchAnalytics.swift */,
8CA7CDA92D36E20F008EE7D2 /* SessionAnalytics.swift */,
ED24EADB2C1A95A900A7BE4D /* StopDetailsAnalytics.swift */,
8CA7CDA52D36C95C008EE7D2 /* StopTripDetailsAnalytics.swift */,
8C2752EE2C6D5CA900F0B0A5 /* TripDetailsAnalytics.swift */,
);
path = Analytics;
Expand Down Expand Up @@ -1553,7 +1565,9 @@
9AE98DAB2CF4CCAE00EE80AA /* StopDetailsViewModel.swift in Sources */,
8C2752EF2C6D5CA900F0B0A5 /* TripDetailsAnalytics.swift in Sources */,
9A52A2B32CC3035F00CC01D6 /* WithRealtimeIndicator.swift in Sources */,
8CA7CDA82D36CB14008EE7D2 /* MapAnalytics.swift in Sources */,
9A5B275C2BB237DE009A6FC6 /* RecenterButton.swift in Sources */,
8CA7CDA62D36C960008EE7D2 /* StopTripDetailsAnalytics.swift in Sources */,
6E99CBB72B9892C80047E78D /* SocketProvider.swift in Sources */,
ED5C93F42C496FB90086D017 /* TripDetailsHeader.swift in Sources */,
8C1587042C76524600AB5036 /* AppVariantExtension.swift in Sources */,
Expand All @@ -1578,6 +1592,7 @@
6EE76D1B2BF532010051D608 /* RouteIcon.swift in Sources */,
ED24EADE2C1A986900A7BE4D /* AnalyticsProvider.swift in Sources */,
6EE7457E2B965ADE0052227E /* Socket.swift in Sources */,
8CA7CDAA2D36E214008EE7D2 /* SessionAnalytics.swift in Sources */,
9A5B27582BB22BF9009A6FC6 /* MapLayerManager.swift in Sources */,
9A03F3662BA9E68500DA40DC /* Debouncer.swift in Sources */,
8CE014102BBDB8DC00918FAE /* StopDetailsRoutesView.swift in Sources */,
Expand Down Expand Up @@ -1699,6 +1714,7 @@
9A18DEAD2D07DDC800DA0A3B /* RouteExtension.swift in Sources */,
6E973DA52C17384C00CBF341 /* SheetHeader.swift in Sources */,
9A9E7DD32C2203BE000DA1FD /* TransitCard.swift in Sources */,
8CA7CDA42D35DD98008EE7D2 /* SearchAnalytics.swift in Sources */,
9AF29DFA2CF548E5005AA4A3 /* StopDetailsUnfilteredView.swift in Sources */,
8C6A48402BC09A2E0032A554 /* StopDetailsFilteredRouteView.swift in Sources */,
9A6ACA2D2CD00F8200299AF5 /* MoreItem.swift in Sources */,
Expand Down
27 changes: 27 additions & 0 deletions iosApp/iosApp/Analytics/MapAnalytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// MapAnalytics.swift
// iosApp
//
// Created by Horn, Melody on 2025-01-14.
// Copyright © 2025 MBTA. All rights reserved.
//

protocol MapAnalytics {
func tappedOnStop(stopId: String)
func tappedVehicle(routeId: String)
}

extension AnalyticsProvider: MapAnalytics {
func tappedOnStop(stopId: String) {
logEvent(
"tapped_on_stop",
parameters: [
"stop_id": stopId,
]
)
}

func tappedVehicle(routeId: String) {
logEvent("tapped_vehicle", parameters: ["route_id": routeId])
}
}
10 changes: 0 additions & 10 deletions iosApp/iosApp/Analytics/NearbyTransitAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import Foundation
protocol NearbyTransitAnalytics: DestinationRowAnalytics {
func toggledPinnedRoute(pinned: Bool, routeId: String)
func refetchedNearbyTransit()
func tappedOnStop(stopId: String)
}

extension AnalyticsProvider: NearbyTransitAnalytics {
Expand All @@ -28,13 +27,4 @@ extension AnalyticsProvider: NearbyTransitAnalytics {
func refetchedNearbyTransit() {
logEvent("refetched_nearby_transit")
}

func tappedOnStop(stopId: String) {
logEvent(
"tapped_on_stop",
parameters: [
"stop_id": stopId,
]
)
}
}
19 changes: 19 additions & 0 deletions iosApp/iosApp/Analytics/SearchAnalytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// SearchAnalytics.swift
// iosApp
//
// Created by Horn, Melody on 2025-01-13.
// Copyright © 2025 MBTA. All rights reserved.
//

import FirebaseAnalytics

protocol SearchAnalytics {
func performedSearch(query: String)
}

extension AnalyticsProvider: SearchAnalytics {
func performedSearch(query: String) {
logEvent("search", parameters: ["query": query])
}
}
59 changes: 59 additions & 0 deletions iosApp/iosApp/Analytics/SessionAnalytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// SessionAnalytics.swift
// iosApp
//
// Created by Horn, Melody on 2025-01-14.
// Copyright © 2025 MBTA. All rights reserved.
//

import CoreLocation
import FirebaseAnalytics
import SwiftUI

protocol SessionAnalytics {
func recordSession(colorScheme: ColorScheme)
func recordSession(voiceOver: Bool)
func recordSession(hideMaps: Bool)
func recordSession(locationAccess: CLAuthorizationStatus, locationAccuracy: CLAccuracyAuthorization)
}

extension AnalyticsProvider: SessionAnalytics {
func recordSession(colorScheme: ColorScheme) {
let colorScheme = switch colorScheme {
case .light: "light"
case .dark: "dark"
@unknown default: "unknown"
}
Analytics.setUserProperty(colorScheme, forName: "color_scheme")
}

func recordSession(voiceOver: Bool) {
let voiceOver = switch voiceOver {
case true: "true"
case false: "false"
}
Analytics.setUserProperty(voiceOver, forName: "screen_reader_on")
}

func recordSession(hideMaps: Bool) {
let hideMaps = switch hideMaps {
case true: "true"
case false: "false"
}
Analytics.setUserProperty(hideMaps, forName: "hide_maps_on")
}

func recordSession(locationAccess: CLAuthorizationStatus, locationAccuracy: CLAccuracyAuthorization) {
let locationAllowed = switch locationAccess {
case .authorizedAlways, .authorizedWhenInUse: true
case .notDetermined, .denied, .restricted: false
@unknown default: false
}
let locationAccess = switch (locationAllowed, locationAccuracy) {
case (true, .fullAccuracy): "precise"
case (true, _): "approximate"
case (false, _): "off"
}
Analytics.setUserProperty(locationAccess, forName: "location_access")
}
}
18 changes: 4 additions & 14 deletions iosApp/iosApp/Analytics/StopDetailsAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import FirebaseAnalytics
import Foundation

protocol StopDetailsAnalytics: DestinationRowAnalytics {
func tappedAlertDetails(routeId: String, stopId: String, alertId: String)
func tappedRouteFilter(routeId: String, stopId: String)
func toggledPinnedRouteAtStop(pinned: Bool, routeId: String)
func tappedAlertDetailsLegacy(routeId: String, stopId: String, alertId: String)
func tappedRouteFilterLegacy(routeId: String, stopId: String)
}

extension AnalyticsProvider: StopDetailsAnalytics {
func tappedAlertDetails(routeId: String, stopId: String, alertId: String) {
func tappedAlertDetailsLegacy(routeId: String, stopId: String, alertId: String) {
logEvent(
"tapped_alert_details",
parameters: [
Expand All @@ -27,7 +26,7 @@ extension AnalyticsProvider: StopDetailsAnalytics {
)
}

func tappedRouteFilter(routeId: String, stopId: String) {
func tappedRouteFilterLegacy(routeId: String, stopId: String) {
logEvent(
"tapped_route_filter",
parameters: [
Expand All @@ -36,13 +35,4 @@ extension AnalyticsProvider: StopDetailsAnalytics {
]
)
}

func toggledPinnedRouteAtStop(pinned: Bool, routeId: String) {
logEvent(
pinned ? "pin_route" : "unpin_route",
parameters: [
"route_id": routeId,
]
)
}
}
45 changes: 45 additions & 0 deletions iosApp/iosApp/Analytics/StopTripDetailsAnalytics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// StopTripDetailsAnalytics.swift
// iosApp
//
// Created by Horn, Melody on 2025-01-14.
// Copyright © 2025 MBTA. All rights reserved.
//

protocol StopTripDetailsAnalytics: DestinationRowAnalytics {
func tappedAlertDetails(routeId: String, stopId: String, alertId: String)
func tappedRouteFilter(routeId: String, stopId: String)
func toggledPinnedRouteAtStop(pinned: Bool, routeId: String)
}

extension AnalyticsProvider: StopTripDetailsAnalytics {
func tappedAlertDetails(routeId: String, stopId: String, alertId: String) {
logEvent(
"tapped_alert_details",
parameters: [
"route_id": routeId,
"stop_id": stopId,
"alertId": alertId,
]
)
}

func tappedRouteFilter(routeId: String, stopId: String) {
logEvent(
"tapped_route_filter",
parameters: [
"route_id": routeId,
"stop_id": stopId,
]
)
}

func toggledPinnedRouteAtStop(pinned: Bool, routeId: String) {
logEvent(
pinned ? "pin_route" : "unpin_route",
parameters: [
"route_id": routeId,
]
)
}
}
17 changes: 17 additions & 0 deletions iosApp/iosApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import shared
import SwiftPhoenixClient
import SwiftUI

// swiftlint:disable:next type_body_length
struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
@Environment(\.colorScheme) var colorScheme
@Environment(\.accessibilityVoiceOverEnabled) var voiceOver

let platform = Platform_iosKt.getPlatform().name
@StateObject var searchObserver = TextFieldObserver()
Expand All @@ -25,6 +28,7 @@ struct ContentView: View {

let transition: AnyTransition = .asymmetric(insertion: .push(from: .bottom), removal: .opacity)
var screenTracker: ScreenTracker = AnalyticsProvider.shared
let analytics: SessionAnalytics = AnalyticsProvider.shared

let inspection = Inspection<Self>()

Expand Down Expand Up @@ -54,6 +58,9 @@ struct ContentView: View {
Task { await contentVM.loadFeaturePromos() }
Task { await contentVM.loadOnboardingScreens() }
Task { await nearbyVM.loadDebugSetting() }
analytics.recordSession(colorScheme: colorScheme)
analytics.recordSession(voiceOver: voiceOver)
analytics.recordSession(hideMaps: contentVM.hideMaps)
}
.task {
// We can't set stale caches in ResponseCache on init because of our Koin setup,
Expand All @@ -74,6 +81,15 @@ struct ContentView: View {
socketProvider.socket.detach()
}
}
.onChange(of: colorScheme) { _ in
analytics.recordSession(colorScheme: colorScheme)
}
.onChange(of: voiceOver) { _ in
analytics.recordSession(voiceOver: voiceOver)
}
.onChange(of: contentVM.hideMaps) { _ in
analytics.recordSession(hideMaps: contentVM.hideMaps)
}
.onChange(of: contentVM.configResponse) { response in
switch onEnum(of: response) {
case let .ok(response): contentVM.configureMapboxToken(token: response.data.mapboxPublicToken)
Expand Down Expand Up @@ -200,6 +216,7 @@ struct ContentView: View {
mapVM: mapVM,
nearbyVM: nearbyVM,
viewportProvider: viewportProvider,
locationDataManager: locationDataManager,
sheetHeight: $sheetHeight
)
}
Expand Down
12 changes: 10 additions & 2 deletions iosApp/iosApp/LocationDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,30 @@ public class LocationDataManager: NSObject, LocationFetcherDelegate, ObservableO
let subscribeToLocations: Bool
@Published public var currentLocation: CLLocation?
@Published public var authorizationStatus: CLAuthorizationStatus?
let analytics: SessionAnalytics

public init(
init(
locationFetcher: LocationFetcher = CLLocationManager(),
settingsRepository: ISettingsRepository = RepositoryDI().settings,
subscribeToLocations: Bool = true,
distanceFilter: Double = kCLDistanceFilterNone
distanceFilter: Double = kCLDistanceFilterNone,
analytics: SessionAnalytics = AnalyticsProvider.shared
) {
self.locationFetcher = locationFetcher
self.locationFetcher.distanceFilter = distanceFilter
self.settingsRepository = settingsRepository
self.subscribeToLocations = subscribeToLocations
self.analytics = analytics
super.init()
self.locationFetcher.locationFetcherDelegate = self
}

public func locationFetcherDidChangeAuthorization(_ fetcher: LocationFetcher) {
authorizationStatus = fetcher.authorizationStatus
analytics.recordSession(
locationAccess: fetcher.authorizationStatus,
locationAccuracy: fetcher.accuracyAuthorization
)
if subscribeToLocations,
fetcher.authorizationStatus == .authorizedWhenInUse || fetcher.authorizationStatus == .authorizedAlways {
fetcher.startUpdatingLocation()
Expand Down Expand Up @@ -63,6 +70,7 @@ extension LocationDataManager: CLLocationManagerDelegate {
public protocol LocationFetcher: AnyObject {
var locationFetcherDelegate: LocationFetcherDelegate? { get set }
var authorizationStatus: CLAuthorizationStatus { get }
var accuracyAuthorization: CLAccuracyAuthorization { get }
var distanceFilter: CLLocationDistance { get set }
func startUpdatingLocation()
func requestWhenInUseAuthorization()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ struct LegacyStopDetailsView: View {
guard let departures else { return }
guard let patterns = departures.routes.first(where: { patterns in patterns.routeIdentifier == filterId })
else { return }
analytics.tappedRouteFilter(routeId: patterns.routeIdentifier, stopId: stop.id)
analytics.tappedRouteFilterLegacy(routeId: patterns.routeIdentifier, stopId: stop.id)
let defaultDirectionId = patterns.patterns.flatMap { headsign in
// RealtimePatterns.patterns is a List<RoutePattern?> but that gets bridged as [Any] for some reason
headsign.patterns.compactMap { pattern in (pattern as? RoutePattern)?.directionId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ struct StopDetailsFilteredRouteView: View {
line: patternsByStop.line,
routes: patternsByStop.routes
))
analytics.tappedAlertDetails(
analytics.tappedAlertDetailsLegacy(
routeId: patternsByStop.routeIdentifier,
stopId: patternsByStop.stop.id,
alertId: alert.id
Expand All @@ -199,7 +199,7 @@ struct StopDetailsFilteredRouteView: View {
line: patternsByStop.line,
routes: patternsByStop.routes
))
analytics.tappedAlertDetails(
analytics.tappedAlertDetailsLegacy(
routeId: patternsByStop.routeIdentifier,
stopId: patternsByStop.stop.id,
alertId: alert.id
Expand Down
Loading

0 comments on commit 5a3ea0a

Please sign in to comment.