Skip to content

Commit bae9ae8

Browse files
authored
Fix non-sending feedback event for the query search result (#347)
1 parent 2b69d67 commit bae9ae8

File tree

9 files changed

+131
-31
lines changed

9 files changed

+131
-31
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ Guide: https://keepachangelog.com/en/1.0.0/
1010

1111
## 2.7.0-rc.1
1212

13-
- [Core] Update dependencies
13+
- [Core] Update dependencies.
14+
- [Tech] Support sending feedback events to Telemetry.
1415

1516
**MapboxCommon**: v24.9.0-rc.1
1617
**MapboxCoreSearch**: v2.7.0-rc.1

MapboxSearch.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@
151151
2C705F062A137CEB00B8B773 /* SearchNavigationProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C705F052A137CEB00B8B773 /* SearchNavigationProfile.swift */; };
152152
2C7FEBFA2CE78E6300B7ED22 /* PointAnnotation+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7FEBF92CE78E6300B7ED22 /* PointAnnotation+Search.swift */; };
153153
2C7FEBFC2CE7A62C00B7ED22 /* MapView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7FEBFB2CE7A62C00B7ED22 /* MapView+Search.swift */; };
154+
2C88C6532D0C62ED00F46FBF /* EventsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C88C6522D0C62ED00F46FBF /* EventsServiceProtocol.swift */; };
154155
2CA1E22129F09CD200A533CF /* PlaceAutocomplete.Suggestion+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1E22029F09CD200A533CF /* PlaceAutocomplete.Suggestion+Tests.swift */; };
155156
2CA1E22329F0A47600A533CF /* PlaceAutocompleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1E22229F0A47600A533CF /* PlaceAutocompleteTests.swift */; };
156157
2CD6C03C29F1982100D865D1 /* EventsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD6C03B29F1982100D865D1 /* EventsManagerTests.swift */; };
@@ -666,6 +667,7 @@
666667
2C705F052A137CEB00B8B773 /* SearchNavigationProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchNavigationProfile.swift; sourceTree = "<group>"; };
667668
2C7FEBF92CE78E6300B7ED22 /* PointAnnotation+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PointAnnotation+Search.swift"; sourceTree = "<group>"; };
668669
2C7FEBFB2CE7A62C00B7ED22 /* MapView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MapView+Search.swift"; sourceTree = "<group>"; };
670+
2C88C6522D0C62ED00F46FBF /* EventsServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsServiceProtocol.swift; sourceTree = "<group>"; };
669671
2CA1E22029F09CD200A533CF /* PlaceAutocomplete.Suggestion+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaceAutocomplete.Suggestion+Tests.swift"; sourceTree = "<group>"; };
670672
2CA1E22229F0A47600A533CF /* PlaceAutocompleteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceAutocompleteTests.swift; sourceTree = "<group>"; };
671673
2CD6C03B29F1982100D865D1 /* EventsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsManagerTests.swift; sourceTree = "<group>"; };
@@ -1721,6 +1723,7 @@
17211723
isa = PBXGroup;
17221724
children = (
17231725
F97FA23F25C95ACD0085A311 /* EventAppMetadata.swift */,
1726+
2C88C6522D0C62ED00F46FBF /* EventsServiceProtocol.swift */,
17241727
);
17251728
path = Telemetry;
17261729
sourceTree = "<group>";
@@ -2687,6 +2690,7 @@
26872690
FEEDD2F82508DFE400DC0A98 /* RecordsProviderInteractorNativeCore.swift in Sources */,
26882691
F97E9A84268C7BBE00F6353D /* DefaultStringInterpolation+Extensions.swift in Sources */,
26892692
14FA657F295355BC00056E5B /* AdministrativeUnits.swift in Sources */,
2693+
2C88C6532D0C62ED00F46FBF /* EventsServiceProtocol.swift in Sources */,
26902694
F9274FF227394AE600708F37 /* TileRegionError.swift in Sources */,
26912695
FE1064F525B9A1C9007837BC /* NavigationOptions.swift in Sources */,
26922696
148DE668285755500085684D /* AddressAutofill+Suggestion.swift in Sources */,

Sources/Demo/MapRootController.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class MapRootController: UIViewController {
8989

9090
@discardableResult
9191
private func present(result: SearchResult) -> Bool {
92-
let detailController = ResultDetailViewController(result: result)
92+
let detailController = ResultDetailViewController(result: result, searchEngine: searchController.searchEngine)
9393
present(detailController, animated: true)
9494
return true
9595
}

Sources/Demo/PlaceAutocompleteMainViewController.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ final class PlaceAutocompleteMainViewController: UIViewController {
2424

2525
extension PlaceAutocompleteMainViewController: UISearchResultsUpdating {
2626
func updateSearchResults(for searchController: UISearchController) {
27-
guard
28-
let text = searchController.searchBar.text
27+
guard let text = searchController.searchBar.text,
28+
!text.isEmpty
2929
else {
3030
cachedSuggestions = []
3131

Sources/Demo/ResultDetailViewController.swift

+40-3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import UIKit
77
class ResultDetailViewController: UIViewController {
88
private var tableView = UITableView()
99
private var mapView: MapView
10+
private var feedbackButton: UIButton
1011

1112
var result: SearchResult
13+
var searchEngine: SearchEngine
1214
private var resultComponents: [(name: String, value: String)] = []
1315

14-
init(result: SearchResult) {
16+
init(result: SearchResult, searchEngine: SearchEngine) {
1517
self.result = result
18+
self.searchEngine = searchEngine
1619
self.resultComponents = result.toComponents()
1720

1821
let inset: CGFloat = 8
@@ -25,8 +28,36 @@ class ResultDetailViewController: UIViewController {
2528
zoom: 15.5
2629
))
2730
)
31+
self.feedbackButton = UIButton()
32+
feedbackButton.setTitle("Send feedback", for: .normal)
33+
feedbackButton.backgroundColor = .lightGray
2834

2935
super.init(nibName: nil, bundle: nil)
36+
37+
feedbackButton.addTarget(self, action: #selector(showFeedbackAlert), for: .touchUpInside)
38+
}
39+
40+
@objc func showFeedbackAlert() {
41+
let alert = UIAlertController(
42+
title: "Submit Feedback?",
43+
message: nil,
44+
preferredStyle: .alert
45+
)
46+
let okAction = UIAlertAction(title: "OK", style: .default) { [weak self] _ in
47+
self?.sendFeedback()
48+
alert.dismiss(animated: true)
49+
}
50+
alert.addAction(okAction)
51+
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in
52+
alert.dismiss(animated: true)
53+
}
54+
alert.addAction(cancelAction)
55+
present(alert, animated: true, completion: nil)
56+
}
57+
58+
func sendFeedback() {
59+
let feedbackEvent = FeedbackEvent(record: result, reason: FeedbackEvent.Reason.name.rawValue, text: nil)
60+
try? searchEngine.feedbackManager.sendEvent(feedbackEvent)
3061
}
3162

3263
@available(*, unavailable)
@@ -39,8 +70,9 @@ class ResultDetailViewController: UIViewController {
3970

4071
tableView.dataSource = self
4172
tableView.allowsSelection = false
73+
view.backgroundColor = .systemBackground
4274

43-
for child in [tableView, mapView] {
75+
for child in [tableView, feedbackButton, mapView] {
4476
child.translatesAutoresizingMaskIntoConstraints = false
4577
view.addSubview(child)
4678
}
@@ -51,7 +83,12 @@ class ResultDetailViewController: UIViewController {
5183
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
5284
mapView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.4),
5385

54-
tableView.topAnchor.constraint(equalTo: mapView.bottomAnchor),
86+
feedbackButton.heightAnchor.constraint(equalToConstant: 44),
87+
feedbackButton.leadingAnchor.constraint(equalTo: view.leadingAnchor),
88+
feedbackButton.trailingAnchor.constraint(equalTo: view.trailingAnchor),
89+
feedbackButton.topAnchor.constraint(equalTo: mapView.bottomAnchor, constant: 20),
90+
91+
tableView.topAnchor.constraint(equalTo: feedbackButton.bottomAnchor, constant: 20),
5592
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
5693
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
5794
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
import MapboxCommon_Private
3+
4+
protocol EventsServiceProtocol {
5+
func sendEvent(for event: Event, callback: EventsServiceResponseCallback?)
6+
}
7+
8+
extension EventsService: EventsServiceProtocol { }

Sources/MapboxSearch/PublicAPI/Telemetry/EventsManager.swift

+38-16
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import MapboxCommon_Private
23

34
/// Report search error or any other to the Mapbox telemetry.
45
///
@@ -19,13 +20,39 @@ public class EventsManager: NSObject {
1920
}
2021
}
2122

22-
/// - Parameter json: EventTemplate from CoreSearchEngine
23-
func sendEvent(json: String) {
24-
// TODO: Analytics
23+
private let eventsService: EventsServiceProtocol
24+
25+
override convenience init() {
26+
let options = EventsServerOptions(
27+
sdkInformation: SdkInformation.defaultInfo,
28+
deferredDeliveryServiceOptions: nil
29+
)
30+
let eventsService = EventsService.getOrCreate(for: options)
31+
self.init(eventsService: eventsService)
32+
}
33+
34+
init(eventsService: EventsServiceProtocol) {
35+
self.eventsService = eventsService
36+
super.init()
2537
}
2638

2739
func sendEvent(_ event: Events, attributes: [String: Any], autoFlush: Bool) {
28-
// TODO: Analytics
40+
var commonEventAttributes = attributes
41+
commonEventAttributes["event"] = event.rawValue
42+
43+
// This unhandled parameter must be removed to match the event scheme.
44+
commonEventAttributes.removeValue(forKey: "mapboxId")
45+
46+
let commonEvent = Event(priority: autoFlush ? .immediate : .queued,
47+
attributes: commonEventAttributes,
48+
deferredOptions: nil)
49+
eventsService.sendEvent(for: commonEvent) { expected in
50+
if expected.isError() {
51+
_Logger.searchSDK.error("Failed to send the event \(event.rawValue) due to error: \(expected.error.message)")
52+
} else if expected.isValue() {
53+
_Logger.searchSDK.debug("Sent the event \(event.rawValue)")
54+
}
55+
}
2956
}
3057

3158
/// Report an error to Mapbox Search SDK.
@@ -40,28 +67,23 @@ public class EventsManager: NSObject {
4067
// TODO: Analytics
4168
}
4269

43-
/// json string from the core side populate the whole json suitable for the server
44-
/// Unfortunately, telemetry SDK doesn't support such kind of event
45-
/// Thats why we adopt the raw json to the telemetry-specific API:
46-
/// eventName + set of event attributes
47-
/// We also have a set of default attributes created automatically: `created` and `userAgent`
48-
/// Telemetry SDK is capable to set them on their own
70+
/// json string from the core side populate the whole json suitable for the server.
71+
/// All the events, exept for the feedback event, are sent from the core side.
4972
/// - Parameter eventTemplate: feedbackEventTemplate from CoreSearchEngine
50-
func prepareEventTemplate(_ eventTemplate: String) throws -> (name: String, attributes: [String: Any]) {
73+
func prepareEventTemplate(_ eventTemplate: String) throws -> [String: Any] {
5174
guard let jsonData = eventTemplate.data(using: .utf8),
52-
var jsonObject = try? JSONSerialization.jsonObject(
75+
let jsonObject = try? JSONSerialization.jsonObject(
5376
with: jsonData,
5477
options: [.mutableContainers]
5578
) as? [String: Any],
56-
let eventName = jsonObject.removeValue(forKey: "event") as? String
79+
let eventName = jsonObject["event"] as? String,
80+
!eventName.isEmpty
5781
else {
5882
reportError(SearchError.incorrectEventTemplate)
5983
throw SearchError.incorrectEventTemplate
6084
}
61-
jsonObject.removeValue(forKey: "created")
62-
jsonObject.removeValue(forKey: "userAgent")
6385

64-
return (eventName, jsonObject)
86+
return jsonObject
6587
}
6688

6789
func prepareEventTemplate(for event: String) -> [String: Any] {

Sources/MapboxSearch/PublicAPI/Telemetry/FeedbackManager.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class FeedbackManager {
2222
attributes["queryString"] = attributePlaceholder
2323
attributes["resultId"] = feedbackAttributes.id
2424
attributes["selectedItemName"] = feedbackAttributes.name
25+
attributes["sessionIdentifier"] = attributePlaceholder
2526

2627
if let coordinate = feedbackAttributes.coordinate {
2728
attributes["resultCoordinates"] = [coordinate.longitude, coordinate.latitude]
@@ -65,7 +66,9 @@ public class FeedbackManager {
6566
attributes["proximity"] = [proximity.value.longitude, proximity.value.latitude]
6667
}
6768

68-
attributes["responseUuid"] = response.responseUUID
69+
if !response.responseUUID.isEmpty {
70+
attributes["responseUuid"] = response.responseUUID
71+
}
6972

7073
// Result parameters
7174
// Mandatory field set -1 if no data available
@@ -185,12 +188,9 @@ public class FeedbackManager {
185188
try delegate.engine.makeFeedbackEvent(
186189
request: response.request,
187190
result: nil,
188-
callback: { [eventsManager] eventTemplateName in
191+
callback: { [eventsManager] eventJson in
189192
do {
190-
let attributes = try eventsManager.prepareEventTemplate(
191-
eventTemplateName
192-
).attributes
193-
193+
let attributes = try eventsManager.prepareEventTemplate(eventJson)
194194
completion(attributes)
195195
} catch {
196196
_Logger.searchSDK.error(
@@ -208,7 +208,7 @@ public class FeedbackManager {
208208
do {
209209
let attributes = try eventsManager.prepareEventTemplate(
210210
eventTemplateName
211-
).attributes
211+
)
212212

213213
completion(attributes)
214214
} catch {

Tests/MapboxSearchTests/Telemetry/EventsManagerTests.swift

+29-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
@testable import MapboxSearch
2+
import MapboxCommon_Private
23
import XCTest
34

5+
final class EventsServiceMock: EventsServiceProtocol {
6+
var events: [Event] = []
7+
8+
func sendEvent(for event: Event, callback: EventsServiceResponseCallback?) {
9+
events.append(event)
10+
callback?(.init(value: NSNull()))
11+
}
12+
}
13+
414
final class EventsManagerTests: XCTestCase {
515
var eventsManager: EventsManager!
16+
var eventsService: EventsServiceMock!
617

718
override func setUp() {
819
super.setUp()
920

10-
eventsManager = EventsManager()
21+
eventsService = EventsServiceMock()
22+
eventsManager = EventsManager(eventsService: eventsService)
1123
}
1224

1325
func testUserAgent() {
@@ -18,4 +30,20 @@ final class EventsManagerTests: XCTestCase {
1830
let event = eventsManager.prepareEventTemplate(for: "templateEvent")
1931
XCTAssertEqual(event["userAgent"] as? String, "search-sdk-ios/\(mapboxSearchSDKVersion)")
2032
}
33+
34+
func testSendEvent() {
35+
let attributes = ["a": "b", "mapboxId": "value"]
36+
let expectedAttributes = ["a": "b", "event": "search.feedback"]
37+
38+
eventsManager.sendEvent(.feedback, attributes: attributes, autoFlush: true)
39+
let sentEvent1 = eventsService.events[0]
40+
XCTAssertEqual(sentEvent1.priority, .immediate)
41+
XCTAssertEqual(sentEvent1.attributes as? [String: String], expectedAttributes)
42+
XCTAssertNil(sentEvent1.deferredOptions)
43+
44+
eventsManager.sendEvent(.feedback, attributes: attributes, autoFlush: false)
45+
let sentEvent2 = eventsService.events[1]
46+
XCTAssertEqual(sentEvent2.priority, .queued)
47+
48+
}
2149
}

0 commit comments

Comments
 (0)