forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSearchAnalytics.swift
More file actions
103 lines (90 loc) · 3.55 KB
/
Copy pathSearchAnalytics.swift
File metadata and controls
103 lines (90 loc) · 3.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import Foundation
/// Bounded search surfaces for desktop product analytics.
///
/// Raw query text, titles, paths, and prompts must never appear in PostHog
/// properties. These raw values are the only legal `search_surface` dimensions.
enum SearchSurface: String, CaseIterable, Sendable {
case activity
case home
case conversations
case memories
case rewind
case apps
case tasks
case brainMap = "brain_map"
}
/// Content-free search analytics. Call sites pass the committed query only so
/// this type can derive length/word-count; the query string itself never becomes
/// a PostHog property.
@MainActor
enum SearchAnalytics {
static let queryEnteredEvent = "Search Query Entered"
static let barFocusedEvent = "Search Bar Focused"
static let resultOpenedEvent = "Search Result Opened"
static let conversationOpenedFromSearchEvent = "Conversation Opened From Search"
static let allowedQueryEnteredKeys: Set<String> = [
"search_surface", "query_length", "query_word_count", "results_count", "had_results",
]
static let forbiddenPropertyKeys: Set<String> = [
"query", "search_query", "search_term", "q", "text", "prompt", "title",
]
private static var debounceCoordinators: [SearchSurface: DebouncedSearchCoordinator] = [:]
static func wordCount(of query: String) -> Int {
let trimmed = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return 0 }
return trimmed.split { $0.isWhitespace || $0.isNewline }.count
}
static func queryEnteredProperties(
surface: SearchSurface,
query: String,
resultsCount: Int
) -> [String: Any] {
[
"search_surface": surface.rawValue,
"query_length": query.count,
"query_word_count": wordCount(of: query),
"results_count": resultsCount,
"had_results": resultsCount > 0,
]
}
static func barFocusedProperties(surface: SearchSurface) -> [String: Any] {
["search_surface": surface.rawValue]
}
static func resultOpenedProperties(surface: SearchSurface, resultIndex: Int?) -> [String: Any] {
var properties: [String: Any] = ["search_surface": surface.rawValue]
if let resultIndex {
properties["result_index"] = resultIndex
}
return properties
}
static func queryEntered(surface: SearchSurface, query: String, resultsCount: Int) {
let normalized = DebouncedSearchCoordinator.normalized(query)
guard !normalized.isEmpty else { return }
AnalyticsManager.shared.searchQueryEntered(
surface: surface, query: normalized, resultsCount: resultsCount)
}
/// Debounce live-filter surfaces so keystrokes do not flood PostHog.
/// `resultsCount` is read at commit time so async filters can settle.
static func scheduleQueryEntered(
surface: SearchSurface,
query: String,
resultsCount: @escaping @MainActor () -> Int
) {
let coordinator = debounceCoordinators[surface] ?? DebouncedSearchCoordinator()
debounceCoordinators[surface] = coordinator
coordinator.submit(query) { submitted in
guard !submitted.isEmpty else { return }
queryEntered(surface: surface, query: submitted, resultsCount: resultsCount())
}
}
static func barFocused(surface: SearchSurface) {
AnalyticsManager.shared.searchBarFocused(surface: surface)
}
static func resultOpened(surface: SearchSurface, resultIndex: Int? = nil, searchIsActive: Bool) {
guard searchIsActive else { return }
AnalyticsManager.shared.searchResultOpened(surface: surface, resultIndex: resultIndex)
}
static func resetDebounceCoordinatorsForTests() {
debounceCoordinators.removeAll()
}
}