|
32 | 32 | private var allSurveys: [PostHogSurvey]? |
33 | 33 |
|
34 | 34 | private var eventsToSurveysLock = NSLock() |
35 | | - private var eventsToSurveys: [String: [String]] = [:] |
| 35 | + private var eventsToSurveys: [String: [(surveyId: String, condition: PostHogEventCondition)]] = [:] |
36 | 36 |
|
37 | 37 | private var seenSurveyKeysLock = NSLock() |
38 | 38 | private var seenSurveyKeys: [AnyHashable: Any]? |
|
145 | 145 |
|
146 | 146 | // TODO: Decouple PostHogSDK and use registration handlers instead |
147 | 147 | /// Called from PostHogSDK instance when an event is captured |
148 | | - func onEvent(event: String) { |
149 | | - let activatedSurveys = eventsToSurveysLock.withLock { eventsToSurveys[event] } ?? [] |
150 | | - guard !activatedSurveys.isEmpty else { return } |
| 148 | + func onEvent(event: String, properties: [String: Any]) { |
| 149 | + let candidates = eventsToSurveysLock.withLock { eventsToSurveys[event] } ?? [] |
| 150 | + guard !candidates.isEmpty else { return } |
| 151 | + |
| 152 | + let matchingSurveyIds = candidates |
| 153 | + .filter { matchPropertyFilters($0.condition.propertyFilters, eventProperties: properties) } |
| 154 | + .map(\.surveyId) |
| 155 | + |
| 156 | + guard !matchingSurveyIds.isEmpty else { return } |
151 | 157 |
|
152 | 158 | eventActivatedSurveysLock.withLock { |
153 | | - for survey in activatedSurveys { |
| 159 | + for survey in matchingSurveyIds { |
154 | 160 | eventActivatedSurveys.insert(survey) |
155 | 161 | } |
156 | 162 | } |
|
214 | 220 | private func decodeAndSetSurveys(remoteConfig: [String: Any]?, callback: @escaping SurveyCallback) { |
215 | 221 | let loadedSurveys: [PostHogSurvey] = decodeSurveys(from: remoteConfig ?? [:]) |
216 | 222 |
|
217 | | - let eventMap = loadedSurveys.reduce(into: [String: [String]]()) { result, current in |
218 | | - if let surveyEvents = current.conditions?.events?.values.map(\.name) { |
219 | | - for event in surveyEvents { |
220 | | - result[event, default: []].append(current.id) |
| 223 | + let eventMap = loadedSurveys.reduce(into: [String: [(surveyId: String, condition: PostHogEventCondition)]]()) { result, current in |
| 224 | + if let surveyEvents = current.conditions?.events?.values { |
| 225 | + for eventCondition in surveyEvents { |
| 226 | + result[eventCondition.name, default: []].append( |
| 227 | + (surveyId: current.id, condition: eventCondition) |
| 228 | + ) |
221 | 229 | } |
222 | 230 | } |
223 | 231 | } |
|
389 | 397 | storage?.setString(forKey: .lastSeenSurveyDate, contents: toISO8601String(date)) |
390 | 398 | } |
391 | 399 |
|
| 400 | + /// Checks if the given event properties satisfy all property filters. |
| 401 | + /// Returns true if propertyFilters is nil or empty (no filters = match all). |
| 402 | + private func matchPropertyFilters( |
| 403 | + _ propertyFilters: [String: PostHogPropertyFilter]?, |
| 404 | + eventProperties: [String: Any] |
| 405 | + ) -> Bool { |
| 406 | + guard let propertyFilters, !propertyFilters.isEmpty else { |
| 407 | + return true |
| 408 | + } |
| 409 | + return propertyFilters.allSatisfy { propertyName, filter in |
| 410 | + guard let eventValue = eventProperties[propertyName] else { |
| 411 | + return false |
| 412 | + } |
| 413 | + let eventValueString = String(describing: eventValue) |
| 414 | + return filter.matchOperator.matches(targets: filter.values, value: eventValueString) |
| 415 | + } |
| 416 | + } |
| 417 | + |
392 | 418 | /// Checks if a survey has been previously activated by an associated event |
393 | 419 | private func isSurveyEventActivated(survey: PostHogSurvey) -> Bool { |
394 | 420 | eventActivatedSurveysLock.withLock { |
|
831 | 857 | private extension PostHogSurveyMatchType { |
832 | 858 | func matches(targets: [String], value: String) -> Bool { |
833 | 859 | switch self { |
834 | | - // any of the targets contain the value (matched lowercase) |
| 860 | + // value contains any of the targets (case-insensitive) |
835 | 861 | case .iContains: |
836 | 862 | targets.contains { target in |
837 | | - target.lowercased().contains(value.lowercased()) |
| 863 | + value.lowercased().contains(target.lowercased()) |
838 | 864 | } |
839 | | - // *none* of the targets contain the value (matched lowercase) |
| 865 | + // value contains *none* of the targets (case-insensitive) |
840 | 866 | case .notIContains: |
841 | 867 | targets.allSatisfy { target in |
842 | | - !target.lowercased().contains(value.lowercased()) |
| 868 | + !value.lowercased().contains(target.lowercased()) |
843 | 869 | } |
844 | | - // any of the targets match with regex |
| 870 | + // value matches any of the targets as a regex pattern |
845 | 871 | case .regex: |
846 | 872 | targets.contains { target in |
847 | | - target.range(of: value, options: .regularExpression) != nil |
| 873 | + value.range(of: target, options: .regularExpression) != nil |
848 | 874 | } |
849 | | - // *none* if the targets match with regex |
| 875 | + // value matches *none* of the targets as a regex pattern |
850 | 876 | case .notRegex: |
851 | 877 | targets.allSatisfy { target in |
852 | | - target.range(of: value, options: .regularExpression) == nil |
| 878 | + value.range(of: target, options: .regularExpression) == nil |
853 | 879 | } |
854 | 880 | // any of the targets is an exact match |
855 | 881 | case .exact: |
|
861 | 887 | targets.allSatisfy { target in |
862 | 888 | target != value |
863 | 889 | } |
| 890 | + // any of the targets is numerically less than the value (value > target) |
| 891 | + case .gt: |
| 892 | + targets.contains { target in |
| 893 | + if let targetNum = Double(target), let valueNum = Double(value) { |
| 894 | + return valueNum > targetNum |
| 895 | + } |
| 896 | + return false |
| 897 | + } |
| 898 | + // any of the targets is numerically greater than the value (value < target) |
| 899 | + case .lt: |
| 900 | + targets.contains { target in |
| 901 | + if let targetNum = Double(target), let valueNum = Double(value) { |
| 902 | + return valueNum < targetNum |
| 903 | + } |
| 904 | + return false |
| 905 | + } |
864 | 906 | case .unknown: |
865 | 907 | false |
866 | 908 | } |
|
955 | 997 | getNewResponseKey(for: questionId) |
956 | 998 | } |
957 | 999 |
|
| 1000 | + func testMatchPropertyFilters( |
| 1001 | + _ propertyFilters: [String: PostHogPropertyFilter]?, |
| 1002 | + eventProperties: [String: Any] |
| 1003 | + ) -> Bool { |
| 1004 | + matchPropertyFilters(propertyFilters, eventProperties: eventProperties) |
| 1005 | + } |
| 1006 | + |
| 1007 | + func testSetEventsToSurveys(_ map: [String: [(surveyId: String, condition: PostHogEventCondition)]]) { |
| 1008 | + eventsToSurveysLock.withLock { |
| 1009 | + eventsToSurveys = map |
| 1010 | + } |
| 1011 | + } |
| 1012 | + |
| 1013 | + func testIsEventActivated(surveyId: String) -> Bool { |
| 1014 | + eventActivatedSurveysLock.withLock { |
| 1015 | + eventActivatedSurveys.contains(surveyId) |
| 1016 | + } |
| 1017 | + } |
| 1018 | + |
958 | 1019 | static func clearInstalls() { |
959 | 1020 | integrationInstalledLock.withLock { |
960 | 1021 | integrationInstalled = false |
|
0 commit comments