diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index f5291926a..47a1fc733 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ 8CA1FB772BF813F500384658 /* TripDetailsStopListSplitViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA1FB762BF813F500384658 /* TripDetailsStopListSplitViewTests.swift */; }; 8CA485B82BDC679A00E84E1F /* VehicleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA485B72BDC679A00E84E1F /* VehicleExtension.swift */; }; 8CA606A92CC02FBC0019C448 /* ViewInspectorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA606A82CC02FBC0019C448 /* ViewInspectorExtensions.swift */; }; + 8CA7CDA02D359357008EE7D2 /* DestinationRowAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CA7CD9F2D359353008EE7D2 /* DestinationRowAnalytics.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 */; }; @@ -396,6 +397,7 @@ 8CA1FB762BF813F500384658 /* TripDetailsStopListSplitViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TripDetailsStopListSplitViewTests.swift; sourceTree = ""; }; 8CA485B72BDC679A00E84E1F /* VehicleExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleExtension.swift; sourceTree = ""; }; 8CA606A82CC02FBC0019C448 /* ViewInspectorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewInspectorExtensions.swift; sourceTree = ""; }; + 8CA7CD9F2D359353008EE7D2 /* DestinationRowAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestinationRowAnalytics.swift; sourceTree = ""; }; 8CB28DB82C2CC5AD0036258E /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = ""; }; 8CB823D52BC5E85C002C87E0 /* SheetNavigationStackEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetNavigationStackEntryTests.swift; sourceTree = ""; }; 8CB823D82BC5EDD2002C87E0 /* StopDetailsRouteViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopDetailsRouteViewTests.swift; sourceTree = ""; }; @@ -1162,6 +1164,7 @@ children = ( 9A52B3392C6E7C7D0028EEAB /* AlertDetailsAnalytics.swift */, ED24EADD2C1A986900A7BE4D /* AnalyticsProvider.swift */, + 8CA7CD9F2D359353008EE7D2 /* DestinationRowAnalytics.swift */, ED24EAD72C1A941E00A7BE4D /* NearbyTransitAnalytics.swift */, EDE92FA52C3DD675007AD2F6 /* ScreenTracker.swift */, ED24EADB2C1A95A900A7BE4D /* StopDetailsAnalytics.swift */, @@ -1642,6 +1645,7 @@ 6E35D4D02B72C7B700A2BF95 /* HomeMapView.swift in Sources */, 9ACE4FD02CE6707900FEB006 /* StopDetailsPage.swift in Sources */, 9A4DB77F2CA4A32800E8755B /* SearchOverlay.swift in Sources */, + 8CA7CDA02D359357008EE7D2 /* DestinationRowAnalytics.swift in Sources */, 8CC1BB402B59D1F6005386FE /* LocationDataManager.swift in Sources */, 9A6ACA2B2CD0096A00299AF5 /* MoreSectionView.swift in Sources */, 6E3C8D7E2C11FDA80059C28C /* ActionButton.swift in Sources */, diff --git a/iosApp/iosApp/Analytics/DestinationRowAnalytics.swift b/iosApp/iosApp/Analytics/DestinationRowAnalytics.swift new file mode 100644 index 000000000..89b2a1ed0 --- /dev/null +++ b/iosApp/iosApp/Analytics/DestinationRowAnalytics.swift @@ -0,0 +1,57 @@ +// +// DestinationRowAnalytics.swift +// iosApp +// +// Created by Horn, Melody on 2025-01-13. +// Copyright © 2025 MBTA. All rights reserved. +// + +import FirebaseAnalytics +import shared + +protocol DestinationRowAnalytics { + func tappedDeparture( + routeId: String, + stopId: String, + pinned: Bool, + alert: Bool, + routeType: RouteType, + noTrips: RealtimePatterns.NoTripsFormat? + ) +} + +extension AnalyticsProvider: DestinationRowAnalytics { + func tappedDeparture( + routeId: String, + stopId: String, + pinned: Bool, + alert: Bool, + routeType: RouteType, + noTrips: RealtimePatterns.NoTripsFormat? + ) { + let mode = switch routeType { + case .bus: "bus" + case .commuterRail: "commuter rail" + case .ferry: "ferry" + case .heavyRail: "subway" + case .lightRail: "subway" + } + let noTrips = switch onEnum(of: noTrips) { + case .noSchedulesToday: "no service today" + case .predictionsUnavailable: "predictions unavailable" + case .serviceEndedToday: "service ended" + case nil: "" + } + logEvent( + "tapped_departure", + parameters: [ + "route_id": routeId, + "stop_id": stopId, + "pinned": pinned ? "true" : "false", + "alert": alert ? "true" : "false", + "mode": mode, + "no_trips": noTrips, + ] + ) + } +} diff --git a/iosApp/iosApp/Analytics/NearbyTransitAnalytics.swift b/iosApp/iosApp/Analytics/NearbyTransitAnalytics.swift index 4e7962b24..3ff40effa 100644 --- a/iosApp/iosApp/Analytics/NearbyTransitAnalytics.swift +++ b/iosApp/iosApp/Analytics/NearbyTransitAnalytics.swift @@ -9,9 +9,8 @@ import FirebaseAnalytics import Foundation -protocol NearbyTransitAnalytics { +protocol NearbyTransitAnalytics: DestinationRowAnalytics { func toggledPinnedRoute(pinned: Bool, routeId: String) - func tappedDeparture(routeId: String, stopId: String, pinned: Bool, alert: Bool) func refetchedNearbyTransit() func tappedOnStop(stopId: String) } @@ -26,18 +25,6 @@ extension AnalyticsProvider: NearbyTransitAnalytics { ) } - func tappedDeparture(routeId: String, stopId: String, pinned: Bool, alert: Bool) { - logEvent( - "tapped_departure", - parameters: [ - "route_id": routeId, - "stop_id": stopId, - "pinned": pinned ? "true" : "false", - "alert": alert ? "true" : "false", - ] - ) - } - func refetchedNearbyTransit() { logEvent("refetched_nearby_transit") } diff --git a/iosApp/iosApp/Analytics/StopDetailsAnalytics.swift b/iosApp/iosApp/Analytics/StopDetailsAnalytics.swift index 94ee1c324..5f2d9de5e 100644 --- a/iosApp/iosApp/Analytics/StopDetailsAnalytics.swift +++ b/iosApp/iosApp/Analytics/StopDetailsAnalytics.swift @@ -9,26 +9,13 @@ import FirebaseAnalytics import Foundation -protocol StopDetailsAnalytics { +protocol StopDetailsAnalytics: DestinationRowAnalytics { func tappedAlertDetails(routeId: String, stopId: String, alertId: String) - func tappedDepartureRow(routeId: String, stopId: String, pinned: Bool, alert: Bool) func tappedRouteFilter(routeId: String, stopId: String) func toggledPinnedRouteAtStop(pinned: Bool, routeId: String) } extension AnalyticsProvider: StopDetailsAnalytics { - func tappedDepartureRow(routeId: String, stopId: String, pinned: Bool, alert: Bool) { - logEvent( - "tapped_departure", - parameters: [ - "route_id": routeId, - "stop_id": stopId, - "pinned": pinned ? "true" : "false", - "alert": alert ? "true" : "false", - ] - ) - } - func tappedAlertDetails(routeId: String, stopId: String, alertId: String) { logEvent( "tapped_alert_details", diff --git a/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsFilteredRouteView.swift b/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsFilteredRouteView.swift index 92abe9085..ee371bc8b 100644 --- a/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsFilteredRouteView.swift +++ b/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsFilteredRouteView.swift @@ -213,12 +213,19 @@ struct StopDetailsFilteredRouteView: View { ForEach(Array(rows.enumerated()), id: \.offset) { index, row in VStack(spacing: 0) { OptionalNavigationLink(value: row.navigationTarget, action: { entry in + let noTrips: RealtimePatterns + .NoTripsFormat? = switch onEnum(of: row.formatted) { + case let .noTrips(noTrips): noTrips.noTripsFormat + default: nil + } pushNavEntry(entry) - analytics.tappedDepartureRow( + analytics.tappedDeparture( routeId: patternsByStop.routeIdentifier, stopId: patternsByStop.stop.id, pinned: pinned, - alert: alerts.count > 0 + alert: alerts.count > 0, + routeType: patternsByStop.representativeRoute.type, + noTrips: noTrips ) }) { HeadsignRowView( diff --git a/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsRouteView.swift b/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsRouteView.swift index b9872952e..47da59e27 100644 --- a/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsRouteView.swift +++ b/iosApp/iosApp/Pages/LegacyStopDetails/StopDetailsRouteView.swift @@ -26,7 +26,9 @@ struct StopDetailsRouteView: View { condenseHeadsignPredictions: patternsByStop.routes.count > 1, now: now, context: .stopDetailsUnfiltered, - pushNavEntry: navAnalytics(routeId: line.id) + pushNavEntry: pushNavEntry, + analytics: analytics, + pinned: pinned ) } } else if let route = patternsByStop.routes.first { @@ -36,23 +38,13 @@ struct StopDetailsRouteView: View { condenseHeadsignPredictions: false, now: now, context: .stopDetailsUnfiltered, - pushNavEntry: navAnalytics(routeId: route.id) + pushNavEntry: pushNavEntry, + analytics: analytics, + pinned: pinned ) } } else { EmptyView() } } - - private func navAnalytics(routeId: String) -> (SheetNavigationStackEntry, Bool) -> Void { - { entry, alerting in - pushNavEntry(entry) - analytics.tappedDepartureRow( - routeId: routeId, - stopId: patternsByStop.stop.id, - pinned: pinned, - alert: alerting - ) - } - } } diff --git a/iosApp/iosApp/Pages/NearbyTransit/DestinationRowView.swift b/iosApp/iosApp/Pages/NearbyTransit/DestinationRowView.swift index 19d063b56..1bc1be65c 100644 --- a/iosApp/iosApp/Pages/NearbyTransit/DestinationRowView.swift +++ b/iosApp/iosApp/Pages/NearbyTransit/DestinationRowView.swift @@ -11,46 +11,109 @@ import SwiftUI struct DestinationRowView: View { let patterns: RealtimePatterns + let stop: Stop + let routeId: String let condenseHeadsignPredictions: Bool let now: Instant let context: TripInstantDisplay.Context + let pushNavEntry: (SheetNavigationStackEntry) -> Void + let analytics: DestinationRowAnalytics + let pinned: Bool + let routeType: RouteType init( patterns: RealtimePatterns, + stop: Stop, + routeId: String, now: Instant, context: TripInstantDisplay.Context, - condenseHeadsignPredictions: Bool = false + condenseHeadsignPredictions: Bool = false, + pushNavEntry: @escaping (SheetNavigationStackEntry) -> Void, + analytics: DestinationRowAnalytics, + pinned: Bool, + routeType: RouteType ) { self.patterns = patterns + self.stop = stop + self.routeId = routeId self.now = now self.context = context self.condenseHeadsignPredictions = condenseHeadsignPredictions + self.pushNavEntry = pushNavEntry + self.analytics = analytics + self.pinned = pinned + self.routeType = routeType } var body: some View { switch onEnum(of: patterns) { case let .byHeadsign(patternsByHeadsign): - HeadsignRowView( - headsign: patternsByHeadsign.headsign, - predictions: patternsByHeadsign.format( - now: now, - routeType: patternsByHeadsign.route.type, - count: condenseHeadsignPredictions ? 1 : 2, - context: context - ), - pillDecoration: patternsByHeadsign.line != nil ? - .onRow(route: patternsByHeadsign.route) : .none + let predictions = patternsByHeadsign.format( + now: now, + routeType: patternsByHeadsign.route.type, + count: condenseHeadsignPredictions ? 1 : 2, + context: context ) - case let .byDirection(patternsByDirection): - DirectionRowView( - direction: patternsByDirection.direction, - predictions: patternsByDirection.format( - now: now, - routeType: patternsByDirection.representativeRoute.type, - context: context + SheetNavigationLink( + value: .legacyStopDetails( + stop, + .init( + routeId: routeId, + directionId: patternsByHeadsign.directionId() + ) ), - pillDecoration: .onPrediction(routesByTrip: patternsByDirection.routesByTrip) + action: { entry in + pushNavEntry(entry) + analyticsTappedDeparture(predictions: predictions) + } + ) { + HeadsignRowView( + headsign: patternsByHeadsign.headsign, + predictions: predictions, + pillDecoration: patternsByHeadsign.line != nil ? + .onRow(route: patternsByHeadsign.route) : .none + ) + } + case let .byDirection(patternsByDirection): + let predictions = patternsByDirection.format( + now: now, + routeType: patternsByDirection.representativeRoute.type, + context: context ) + SheetNavigationLink( + value: .legacyStopDetails( + stop, + .init( + routeId: routeId, + directionId: patternsByDirection.directionId() + ) + ), + action: { entry in + pushNavEntry(entry) + analyticsTappedDeparture(predictions: predictions) + } + ) { + DirectionRowView( + direction: patternsByDirection.direction, + predictions: predictions, + pillDecoration: .onPrediction(routesByTrip: patternsByDirection.routesByTrip) + ) + } + } + } + + private func analyticsTappedDeparture(predictions: RealtimePatterns.Format) { + let noTrips: RealtimePatterns.NoTripsFormat? = switch onEnum(of: predictions) { + case let .noTrips(noTrips): noTrips.noTripsFormat + default: nil } + analytics.tappedDeparture( + routeId: routeId, + stopId: stop.id, + pinned: pinned, + alert: (patterns.alertsHere?.count ?? 0) > 0, + routeType: routeType, + noTrips: noTrips + ) } } diff --git a/iosApp/iosApp/Pages/NearbyTransit/NearbyStopView.swift b/iosApp/iosApp/Pages/NearbyTransit/NearbyStopView.swift index 7041258d1..ad9f6cd31 100644 --- a/iosApp/iosApp/Pages/NearbyTransit/NearbyStopView.swift +++ b/iosApp/iosApp/Pages/NearbyTransit/NearbyStopView.swift @@ -44,15 +44,9 @@ struct NearbyStopView: View { condenseHeadsignPredictions: condenseHeadsignPredictions, now: now, context: .nearbyTransit, - pushNavEntry: { entry, alertsHere in - pushNavEntry(entry) - analytics.tappedDeparture( - routeId: patternsAtStop.routeIdentifier, - stopId: patternsAtStop.stop.id, - pinned: pinned, - alert: alertsHere - ) - } + pushNavEntry: pushNavEntry, + analytics: analytics, + pinned: pinned ) } } diff --git a/iosApp/iosApp/Pages/NearbyTransit/StopDeparturesSummaryList.swift b/iosApp/iosApp/Pages/NearbyTransit/StopDeparturesSummaryList.swift index 32d4bc13d..d9f5b0ce8 100644 --- a/iosApp/iosApp/Pages/NearbyTransit/StopDeparturesSummaryList.swift +++ b/iosApp/iosApp/Pages/NearbyTransit/StopDeparturesSummaryList.swift @@ -15,7 +15,9 @@ struct StopDeparturesSummaryList: View { let condenseHeadsignPredictions: Bool let now: Instant let context: TripInstantDisplay.Context - let pushNavEntry: (SheetNavigationStackEntry, Bool) -> Void + let pushNavEntry: (SheetNavigationStackEntry) -> Void + let analytics: any DestinationRowAnalytics + let pinned: Bool var body: some View { ForEach( @@ -29,19 +31,14 @@ struct StopDeparturesSummaryList: View { } VStack(spacing: 0) { - SheetNavigationLink( - value: .legacyStopDetails( - patternsByStop.stop, - filterFor(patterns: patterns) - ), - action: { entry in pushNavEntry(entry, (patterns.alertsHere?.count ?? 0) > 0) } - ) { - DestinationRowView( - patterns: patterns, - now: now, context: context, - condenseHeadsignPredictions: condenseHeadsignPredictions - ) - } + DestinationRowView( + patterns: patterns, + stop: patternsByStop.stop, routeId: patternsByStop.routeIdentifier, + now: now, context: context, + condenseHeadsignPredictions: condenseHeadsignPredictions, + pushNavEntry: pushNavEntry, + analytics: analytics, pinned: pinned, routeType: patternsByStop.representativeRoute.type + ) .accessibilityInputLabels([inputLabel]) .padding(8) .frame(minHeight: 44) @@ -55,19 +52,4 @@ struct StopDeparturesSummaryList: View { .accessibilityElement(children: .contain) .accessibilityHint(Text("Open for more arrivals")) } - - func filterFor(patterns: RealtimePatterns) -> StopDetailsFilter { - switch onEnum(of: patterns) { - case let .byHeadsign(patternsByHeadsign): - .init( - routeId: patternsByStop.routeIdentifier, - directionId: patternsByHeadsign.directionId() - ) - case let .byDirection(patternsByDirection): - .init( - routeId: patternsByStop.routeIdentifier, - directionId: patternsByDirection.directionId() - ) - } - } } diff --git a/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredDepartureDetails.swift b/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredDepartureDetails.swift index 02e245720..1286a570d 100644 --- a/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredDepartureDetails.swift +++ b/iosApp/iosApp/Pages/StopDetails/StopDetailsFilteredDepartureDetails.swift @@ -168,11 +168,13 @@ struct StopDetailsFilteredDepartureDetails: View { stopSequence: upcoming.stopSequence, selectionLock: false ) - analytics.tappedDepartureRow( + analytics.tappedDeparture( routeId: patternsByStop.routeIdentifier, stopId: patternsByStop.stop.id, pinned: pinned, - alert: alerts.count > 0 + alert: alerts.count > 0, + routeType: patternsByStop.representativeRoute.type, + noTrips: nil ) view.scrollTo(tileData.id) } diff --git a/iosApp/iosAppTests/Views/NearbyTransitViewTests.swift b/iosApp/iosAppTests/Views/NearbyTransitViewTests.swift index d3d0656e0..e50b854ea 100644 --- a/iosApp/iosAppTests/Views/NearbyTransitViewTests.swift +++ b/iosApp/iosAppTests/Views/NearbyTransitViewTests.swift @@ -851,7 +851,7 @@ final class NearbyTransitViewTests: XCTestCase { pinned: false ) - try sut.inspect().find(DestinationRowView.self).parent().parent().parent().button().tap() + try sut.inspect().find(DestinationRowView.self).find(ViewType.Button.self).tap() wait(for: [stopEntryPushedExp], timeout: 2) }