Skip to content

Commit

Permalink
feat(insights): automatic hits view events tracking (#276)
Browse files Browse the repository at this point in the history
  • Loading branch information
VladislavFitz committed May 2, 2023
1 parent 09c9fa3 commit 501bb00
Show file tree
Hide file tree
Showing 24 changed files with 194 additions and 13 deletions.
1 change: 1 addition & 0 deletions Examples/Examples/Demo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ struct Demo: Codable, DemoProtocol {
case codexMultipleIndex = "codex_multiple_index"
case codexCategoriesHits = "codex_categories_hits"
}
// swiftlint:enable type_name
}
1 change: 1 addition & 0 deletions Examples/Examples/DemoViewControllerFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,4 @@ class DemoViewControllerFactory: ViewControllerFactory {
}
}
}
// swiftlint:enable cyclomatic_complexity
1 change: 1 addition & 0 deletions Examples/Examples/ShowcaseDemo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ struct ShowcaseDemo: Codable, DemoProtocol {
case queryRuleCustomData = "query_rule_custom_data"
case relevantSort = "dynamic_sort"
}
// swiftlint:enable type_name
}
2 changes: 1 addition & 1 deletion Examples/Examples/ShowcaseDemoViewControllerFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ class ShowcaseDemoViewControllerFactory: ViewControllerFactory {
return viewController
}

// swiftlint:disable function_body_length cyclomatic_complexity
func swiftuiShowcaseViewController(for demoID: ShowcaseDemo.ID) -> UIViewController? {
let viewController: UIViewController

Expand Down Expand Up @@ -212,4 +211,5 @@ class ShowcaseDemoViewControllerFactory: ViewControllerFactory {

return viewController
}
// swiftlint:enable function_body_length cyclomatic_complexity
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Vladislav Fitc on 29/07/2020.
// Copyright © 2020 Algolia. All rights reserved.
//
// swiftlint:disable file_length line_length unused_optional_binding
// swiftlint:disable file_length line_length unused_optional_binding orphaned_doc_comment

import Foundation
import InstantSearch
Expand Down Expand Up @@ -435,3 +435,4 @@ extension GettingStartedGuide.StepSeven.ViewController: StatsTextController {
You can have a look at our examples to see more complex examples of applications built with `InstantSearch`.
You can head to our components page to see other components that you could use.
*/
// swiftlint:enable file_length line_length unused_optional_binding orphaned_doc_comment
Original file line number Diff line number Diff line change
Expand Up @@ -128,5 +128,6 @@ extension MultiIndex {
}
}
}
// swiftlint:enable unused_optional_binding
}
}
1 change: 1 addition & 0 deletions Examples/Shared/UIColor+Convenience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension UIColor {
blue: CGFloat(b) / 255,
alpha: CGFloat(a) / 255)
}
// swiftlint:enable identifier_name

static let algoliaCyan = UIColor(hexString: "5468FF")
}
Expand Down
2 changes: 2 additions & 0 deletions Examples/Shared/View/Product/ProductRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct ProductRow: View {
static let watch = Self(showDescription: false, imageWidth: 60, horizontalSpacing: 7, verticalSpacing: 5)
// swiftlint:disable identifier_name
static let tv = Self(showDescription: true, imageWidth: 200, horizontalSpacing: 30, verticalSpacing: 10)
// swiftlint:enable identifier_name
}

var body: some View {
Expand Down Expand Up @@ -110,3 +111,4 @@ struct ProductRow_Previews: PreviewProvider {
)
}
}
// swiftlint:enable line_length
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ class FacetListPersistentSelectionDemoController {
searcher.search()
}
}
// swiftlint:enable type_name
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,4 @@ private extension FilterNumericComparisonDemoViewController {
priceStepperValueLabel.text = demoController.priceConnector.interactor.item.flatMap { "\($0)" }
}
}
// swiftlint:enable type_name
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
}
}
#endif
// swiftlint:enable weak_delegate
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ public extension FilterMapInteractor {

@available(*, deprecated, renamed: "FilterMapInteractorControllerConnection")
public typealias SelectableFilterInteractorControllerConnection = FilterMapInteractorControllerConnection
// swiftlint:enable type_name
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ public extension FilterMapInteractor {

@available(*, deprecated, renamed: "FilterMapInteractorFilterStateConnection")
public typealias SelectableFilterInteractorFilterStateConnection = FilterMapInteractorFilterStateConnection
// swiftlint:enable type_name
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ public extension FilterMapInteractor {

@available(*, deprecated, renamed: "FilterMapInteractorSearcherConnection")
public typealias SelectableFilterInteractorSearcherConnection = FilterMapInteractorSearcherConnection
// swiftlint:enable type_name
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ extension FilterGroup.Or: CustomStringConvertible {
return "{ \(name ?? "_"): \(filters) }"
}
}
// swiftlint:enable type_name
1 change: 1 addition & 0 deletions Sources/InstantSearchCore/FilterState/FilterGroupID.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,4 @@ extension FilterGroup.ID: CustomStringConvertible {
}
}
}
// swiftlint:enable type_name
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ public extension HierarchicalInteractor {
return connection
}
}
// swiftlint:enable large_tuple
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ public extension Boundable {
return connection
}
}
// swiftlint:enable generic_type_name
24 changes: 24 additions & 0 deletions Sources/InstantSearchCore/Searcher/Hits/HitsSearcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
//

import AlgoliaSearchClient
#if !InstantSearchCocoaPods
import InstantSearchInsights
#endif
import Foundation

@available(*, deprecated, renamed: "HitsSearcher")
Expand Down Expand Up @@ -105,6 +108,19 @@ public final class HitsSearcher: IndexSearcher<AlgoliaSearchService> {
}
}

/// The hitsTracker property in the HitsSearcher class simplifies the tracking of Insights events for the search performed by the searcher.
///
/// It is an instance of the HitsTracker class that provides methods for tracking clicks, conversions, and views for search results.
/// By default, it uses the eventName property of the HitsSearcher instance as the event name to track, but custom event names can also be provided by passing them as parameters to the tracking methods.
public lazy var eventTracker: HitsTracker = {
let client = service.client
let insights = Insights.register(appId: client.applicationID,
apiKey: client.apiKey)
return HitsTracker(eventName: "hits event",
searcher: self,
insights: insights)
}()

/**
- Parameters:
- appID: Application ID
Expand Down Expand Up @@ -142,6 +158,10 @@ public final class HitsSearcher: IndexSearcher<AlgoliaSearchService> {
super.init(service: service, initialRequest: request)
Telemetry.shared.trace(type: .hitsSearcher,
parameters: .client)
onResults.subscribe(with: self) { searcher, response in
searcher.eventTracker.trackView(for: response.hits,
eventName: "Hits Viewed")
}
}

/**
Expand All @@ -157,6 +177,10 @@ public final class HitsSearcher: IndexSearcher<AlgoliaSearchService> {
query: indexQueryState.query,
requestOptions: requestOptions)
}

deinit {
onResults.cancelSubscription(for: self)
}
}

extension HitsSearcher: MultiSearchComponent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ protocol HitsAfterSearchTrackable {
}

extension Insights: HitsAfterSearchTrackable {}
// swiftlint:enable function_parameter_count
102 changes: 91 additions & 11 deletions Sources/InstantSearchCore/Tracker/HitsTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,33 @@

import Foundation
#if !InstantSearchCocoaPods
import InstantSearchInsights
import InstantSearchInsights
#endif

/// The HitsTracker class allows to track user interactions with search results using Algolia Insights.
/// It implements the InsightsTracker protocol and provides methods for tracking clicks, conversions, and views for search results.
public class HitsTracker: InsightsTracker {

/// The name of the event to track.
public let eventName: EventName

public var isEnabled: Bool

/// A `TrackableSearcher` object that provides search results to be tracked.
internal let searcher: TrackableSearcher

/// A `HitsAfterSearchTrackable` object that tracks search result interactions.
internal let tracker: HitsAfterSearchTrackable

/// An optional identifier for the search query.
internal var queryID: QueryID?

/// Initializes a new instance of the `HitsTracker` class with the specified event name, `TrackableSearcher`, and `Insights` object.
///
/// - Parameters:
/// - eventName: The name of the event to track.
/// - searcher: A `TrackableSearcher` object that provides search results to be tracked.
/// - insights: An `Insights` object.
public required convenience init(eventName: EventName,
searcher: TrackableSearcher,
insights: Insights) {
Expand All @@ -25,13 +43,19 @@ public class HitsTracker: InsightsTracker {
tracker: insights)
}

/// Initializes a new instance of the `HitsTracker` class with the specified event name, `TrackableSearcher`, and `HitsAfterSearchTrackable` object.
///
/// - Parameters:
/// - eventName: The name of the event to track.
/// - searcher: A `TrackableSearcher` object that provides search results to be tracked.
/// - tracker: A `HitsAfterSearchTrackable` object that tracks search result interactions.
init(eventName: EventName,
searcher: TrackableSearcher,
tracker: HitsAfterSearchTrackable) {
self.eventName = eventName
self.searcher = searcher
self.tracker = tracker

self.isEnabled = true
searcher.setClickAnalyticsOn(true)
searcher.subscribeForQueryIDChange(self)
}
Expand All @@ -49,34 +73,90 @@ public class HitsTracker: InsightsTracker {
// MARK: - Hits tracking methods

public extension HitsTracker {

/// Tracks a click event for the specified search result at the specified position.
///
/// - Parameters:
/// - hit: The search result for which to track the click event.
/// - position: The position of the search result in the search results list.
/// - eventName: An optional custom event name.
func trackClick<Record: Codable>(for hit: Hit<Record>,
position: Int,
eventName customEventName: EventName? = nil) {
eventName: EventName? = nil) {
trackClick(for: [hit],
positions: [position],
eventName: eventName)
}

/// Tracks a click event for the specified search results at the specified positions.
///
/// - Parameters:
/// - hits: The search results for which to track the click event.
/// - positions: The positions of the search results in the search results list.
/// - eventName: An optional custom event name.
func trackClick<Record: Codable>(for hits: [Hit<Record>],
positions: [Int],
eventName: EventName? = nil) {
guard isEnabled else { return }
guard let queryID = queryID else { return }
tracker.clickedAfterSearch(eventName: customEventName ?? eventName,
tracker.clickedAfterSearch(eventName: eventName ?? self.eventName,
indexName: searcher.indexName,
objectIDsWithPositions: [(hit.objectID, position)],
objectIDsWithPositions: Array(zip(hits.map(\.objectID), positions)),
queryID: queryID,
timestamp: .none,
userToken: .none)
}

/// Tracks a conversion event for the specified search result.
///
/// - Parameters:
/// - hit: The search result for which to track the conversion event.
/// - eventName: An optional custom event name.
func trackConvert<Record: Codable>(for hit: Hit<Record>,
eventName customEventName: EventName? = nil) {
eventName: EventName? = nil) {
trackConvert(for: [hit],
eventName: eventName)
}

/// Tracks a conversion event for the specified search results.
///
/// - Parameters:
/// - hits: The search results for which to track the conversion event.
/// - eventName: An optional custom event name.
func trackConvert<Record: Codable>(for hits: [Hit<Record>],
eventName: EventName? = nil) {
guard isEnabled else { return }
guard let queryID = queryID else { return }
tracker.convertedAfterSearch(eventName: customEventName ?? eventName,
tracker.convertedAfterSearch(eventName: eventName ?? self.eventName,
indexName: searcher.indexName,
objectIDs: [hit.objectID],
objectIDs: hits.map(\.objectID),
queryID: queryID,
timestamp: .none,
userToken: .none)
}

/// Tracks a view event for the specified search result.
///
/// - Parameters:
/// - hit: The search result for which to track the view event.
/// - eventName: An optional custom event name.
func trackView<Record: Codable>(for hit: Hit<Record>,
eventName customEventName: EventName? = nil) {
tracker.viewed(eventName: customEventName ?? eventName,
eventName: EventName? = nil) {
trackView(for: [hit],
eventName: eventName)
}

/// Tracks a view event for the specified search results.
///
/// - Parameters:
/// - hits: The search results for which to track the view event.
/// - eventName: An optional custom event name.
func trackView<Record: Codable>(for hits: [Hit<Record>],
eventName: EventName? = nil) {
guard isEnabled else { return }
tracker.viewed(eventName: eventName ?? self.eventName,
indexName: searcher.indexName,
objectIDs: [hit.objectID],
objectIDs: hits.map(\.objectID),
timestamp: .none,
userToken: .none)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/InstantSearchInsights/Logic/EventTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,4 @@ class EventTracker: EventTrackable {
logger.error("\(error.localizedDescription)")
}
}
// swiftlint:enable function_parameter_count
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,4 @@ protocol EventTrackable {
timestamp: Date?,
filters: [String])
}
// swiftlint:enable function_parameter_count

0 comments on commit 501bb00

Please sign in to comment.