diff --git a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNode.swift b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNode.swift index ec29c3cfed42..a769216245ac 100644 --- a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNode.swift +++ b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentNode.swift @@ -25,6 +25,8 @@ final class MapContentNode: Identifiable { self.context = context } + var childrenIsEmpty: Bool { children.isEmpty } + func withChildrenNodes(_ closure: (() -> MapContentNode) -> Void) { var idx = 0 diff --git a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift index 9f8534d2ae6b..18ad18a62d3d 100644 --- a/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift +++ b/Sources/MapboxMaps/ContentBuilders/Tree/MapContentReconciler.swift @@ -22,6 +22,7 @@ final class MapContentReconciler { private let context: MapContentNodeContext private var root: MapContentNode private var loadingToken: AnyCancelable? + private var sendTelemetryOncePerStyle = Once() init(styleManager: StyleManagerProtocol, sourceManager: StyleSourceManagerProtocol, styleIsLoaded: Signal) { self.context = MapContentNodeContext( @@ -44,13 +45,23 @@ final class MapContentReconciler { defer { trace?.end() } context.update(mapContent: content, root: root) + triggerTelemetryIfNeeded(for: root) } private func reloadStyle(with content: any MapContent) { let trace = OSLog.platform.beginInterval("MapContent update on style reload") defer { trace?.end() } + sendTelemetryOncePerStyle.reset() context.reload(mapContent: content, root: root) + triggerTelemetryIfNeeded(for: root) + } + + /// Increment telemetry counter once per StyleDSL usage on the single style + func triggerTelemetryIfNeeded(for node: MapContentNode) { + guard sendTelemetryOncePerStyle.continueOnce(), !node.childrenIsEmpty else { return } + Log.info(forMessage: "triggerTelemetryIfNeeded") + sendTelemetry(\.styleDSL) } } diff --git a/Sources/MapboxMaps/Foundation/CoreAliases.swift b/Sources/MapboxMaps/Foundation/CoreAliases.swift index 9c9e34cbb3b5..73d5eb5634dd 100644 --- a/Sources/MapboxMaps/Foundation/CoreAliases.swift +++ b/Sources/MapboxMaps/Foundation/CoreAliases.swift @@ -1,4 +1,5 @@ @_implementationOnly import MapboxCoreMaps_Private +@_implementationOnly import MapboxCommon_Private typealias CoreCameraOptions = MapboxCoreMaps_Private.CameraOptions typealias CoreCameraState = MapboxCoreMaps_Private.CameraState @@ -32,3 +33,4 @@ typealias CoreObservable = MapboxCoreMaps_Private.Observable typealias CoreViewAnnotationPositionsUpdateListener = MapboxCoreMaps_Private.ViewAnnotationPositionsUpdateListener typealias CoreMapSnapshotter = MapboxCoreMaps_Private.MapSnapshotter typealias CorePerformanceSamplerOptions = MapboxCoreMaps_Private.PerformanceSamplerOptions +typealias TelemetryCounter = MapboxCommon_Private.FeatureTelemetryCounter diff --git a/Sources/MapboxMaps/Foundation/Extensions/UIWindow+ParentScene.swift b/Sources/MapboxMaps/Foundation/Extensions/UIWindow+ParentScene.swift index 4dc6b397f959..10ae366de23d 100644 --- a/Sources/MapboxMaps/Foundation/Extensions/UIWindow+ParentScene.swift +++ b/Sources/MapboxMaps/Foundation/Extensions/UIWindow+ParentScene.swift @@ -3,10 +3,10 @@ import CarPlay #endif import UIKit -@available(iOS 13.0, *) extension UIWindow { /// The `UIScene` containing this window. + @available(iOS 13.0, *) internal var parentScene: UIScene? { #if canImport(CarPlay) switch self { @@ -17,6 +17,14 @@ extension UIWindow { } #else return windowScene +#endif + } + + var isCarPlay: Bool { +#if canImport(CarPlay) + return self is CPWindow +#else + return false #endif } } diff --git a/Sources/MapboxMaps/Foundation/MapView.swift b/Sources/MapboxMaps/Foundation/MapView.swift index 45b2d08e6fdf..ba80ac9344c3 100644 --- a/Sources/MapboxMaps/Foundation/MapView.swift +++ b/Sources/MapboxMaps/Foundation/MapView.swift @@ -631,6 +631,8 @@ open class MapView: UIView, SizeTrackingLayerDelegate { } } + private(set) var didMoveToCarPlayWindow = false + open override func didMoveToWindow() { super.didMoveToWindow() @@ -642,6 +644,11 @@ open class MapView: UIView, SizeTrackingLayerDelegate { return } + if window.isCarPlay, !didMoveToCarPlayWindow { + didMoveToCarPlayWindow = true + sendTelemetry(\.carPlay) + } + displayLink = dependencyProvider.makeDisplayLink( window: window, target: ForwardingDisplayLinkTarget { [weak self] in diff --git a/Sources/MapboxMaps/Foundation/TelemetryCounter.swift b/Sources/MapboxMaps/Foundation/TelemetryCounter.swift new file mode 100644 index 000000000000..a86d46c40221 --- /dev/null +++ b/Sources/MapboxMaps/Foundation/TelemetryCounter.swift @@ -0,0 +1,56 @@ +import Foundation + +extension TelemetryCounter { + fileprivate static let sdkPrefix = "maps-mobile" + fileprivate static let swiftUI = TelemetryCounter.create(name: "map", category: "swift-ui") + fileprivate static let viewportCameraState = TelemetryCounter.viewport(name: "state/camera") + fileprivate static let viewportFollowState = TelemetryCounter.viewport(name: "state/follow-puck") + fileprivate static let viewportOverviewState = TelemetryCounter.viewport(name: "state/overview") + fileprivate static let viewportTransition = TelemetryCounter.viewport(name: "transition") + fileprivate static let styleDSL = TelemetryCounter.create(name: "dsl", category: "style") + fileprivate static let carPlay = TelemetryCounter.create(forName: sdkPrefix + "/carplay") + + private static func viewport(name: String) -> TelemetryCounter { + .create(name: name, category: "viewport") + } + + private static func create(name: String, category: String) -> TelemetryCounter { + .create(forName: [sdkPrefix, category, name].joined(separator: "/")) + } +} + +/// Default scope for telemetry events +/// This scope and all posible future scopes should be singleton to get rid of spawning several equal counters +/// Also singleton allows the usage of KeyPath in sendTelemetry (static members on metatype not allowed in KeyPath) +struct TelemetryEvents { + let swiftUI = TelemetryEvent(counter: .swiftUI) + let viewportCameraState = TelemetryEvent(counter: .viewportCameraState) + let viewportFollowState = TelemetryEvent(counter: .viewportFollowState) + let viewportOverviewState = TelemetryEvent(counter: .viewportOverviewState) + let viewportTransition = TelemetryEvent(counter: .viewportTransition) + let styleDSL = TelemetryEvent(counter: .styleDSL) + let carPlay = TelemetryEvent(counter: .carPlay) + + static let shared = TelemetryEvents() + + fileprivate init() {} +} + +/// Abstraction over the actiual telemetry implementation, which hides all the implementtion details +/// All it's properties should be fileprivate to keep implementation inside the file +struct TelemetryEvent { + fileprivate let counter: TelemetryCounter +} + +/// Interface for sending telemetry using the default events scope +/// Allows to send events in type-safe manner while keeping implementstion details hidden from client code +func sendTelemetry(_ eventName: KeyPath) { + sendTelemetry(eventName: eventName, category: TelemetryEvents.shared) +} + +/// Interface for sending telemetry using the custom scope +/// Allows to send events in type-safe manner while keeping implementstion details hidden from client code and specify custom scope to group event when their number will grow +func sendTelemetry(eventName: KeyPath, category: T) { + let event = category[keyPath: eventName] + event.counter.increment() +} diff --git a/Sources/MapboxMaps/SwiftUI/Map.swift b/Sources/MapboxMaps/SwiftUI/Map.swift index 815924ffca0b..d1e95fbd9fd8 100644 --- a/Sources/MapboxMaps/SwiftUI/Map.swift +++ b/Sources/MapboxMaps/SwiftUI/Map.swift @@ -101,7 +101,7 @@ public struct Map: UIViewControllerRepresentable { public func makeCoordinator() -> Coordinator { let urlOpener = ClosureURLOpener() - + sendTelemetry(\.swiftUI) let mapView = MapView(frame: .zero, urlOpener: urlOpener) let viewController = MapViewController(mapView: mapView) diff --git a/Sources/MapboxMaps/Viewport/ViewportManager.swift b/Sources/MapboxMaps/Viewport/ViewportManager.swift index f862f6c644a9..23ed44cdd8f4 100644 --- a/Sources/MapboxMaps/Viewport/ViewportManager.swift +++ b/Sources/MapboxMaps/Viewport/ViewportManager.swift @@ -102,6 +102,7 @@ public final class ViewportManager { public func transition(to toState: ViewportState, transition: ViewportTransition? = nil, completion: ((_ success: Bool) -> Void)? = nil) { + sendTelemetry(\.viewportTransition) impl.transition(to: toState, transition: transition, completion: completion) } @@ -124,7 +125,8 @@ public final class ViewportManager { @_documentation(visibility: public) @_spi(Experimental) public func makeCameraViewportState(camera: CameraOptions) -> ViewportState { - CameraViewportState(cameraOptions: Signal(just: camera), mapboxMap: mapboxMap, safeAreaPadding: impl.safeAreaPadding) + sendTelemetry(\.viewportCameraState) + return CameraViewportState(cameraOptions: Signal(just: camera), mapboxMap: mapboxMap, safeAreaPadding: impl.safeAreaPadding) } func makeDefaultStyleViewportState(padding: UIEdgeInsets) -> ViewportState { @@ -138,7 +140,8 @@ public final class ViewportManager { /// with the default value specified for all parameters. /// - Returns: The newly-created ``FollowPuckViewportState``. public func makeFollowPuckViewportState(options: FollowPuckViewportStateOptions = .init()) -> FollowPuckViewportState { - FollowPuckViewportState( + sendTelemetry(\.viewportFollowState) + return FollowPuckViewportState( options: options, mapboxMap: mapboxMap, onPuckRender: onPuckRender, @@ -149,6 +152,7 @@ public final class ViewportManager { /// - Parameter options: configuration options used when creating ``OverviewViewportState``. /// - Returns: The newly-created ``OverviewViewportState``. public func makeOverviewViewportState(options: OverviewViewportStateOptions) -> OverviewViewportState { + sendTelemetry(\.viewportOverviewState) return OverviewViewportState( options: options, mapboxMap: mapboxMap,