mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Merge pull request #5591 from vector-im/doug/5590_ftue_analytics
Add support for UserProperties to analytics and capture FTUE use case selection.
This commit is contained in:
commit
4a0a67ff73
13 changed files with 201 additions and 33 deletions
|
@ -171,6 +171,14 @@ import AnalyticsEvents
|
||||||
// The following methods are exposed for compatibility with Objective-C as
|
// The following methods are exposed for compatibility with Objective-C as
|
||||||
// the `capture` method and the generated events cannot be bridged from Swift.
|
// the `capture` method and the generated events cannot be bridged from Swift.
|
||||||
extension Analytics {
|
extension Analytics {
|
||||||
|
/// Updates any user properties to help with creating cohorts.
|
||||||
|
///
|
||||||
|
/// Only non-nil properties will be updated when calling this method.
|
||||||
|
func updateUserProperties(ftueUseCase: UserSessionProperties.UseCase? = nil) {
|
||||||
|
let userProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: ftueUseCase?.analyticsName, numSpaces: nil)
|
||||||
|
client.updateUserProperties(userProperties)
|
||||||
|
}
|
||||||
|
|
||||||
/// Track the presentation of a screen
|
/// Track the presentation of a screen
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - screen: The screen that was shown.
|
/// - screen: The screen that was shown.
|
||||||
|
@ -186,20 +194,21 @@ extension Analytics {
|
||||||
trackScreen(screen, duration: nil)
|
trackScreen(screen, duration: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track an element that has been tapped
|
/// Track an element that has been interacted with
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - tap: The element that was tapped
|
/// - uiElement: The element that was interacted with
|
||||||
|
/// - interactionType: The way in with the element was interacted with
|
||||||
/// - index: The index of the element, if it's in a list of elements
|
/// - index: The index of the element, if it's in a list of elements
|
||||||
func trackTap(_ tap: AnalyticsUIElement, index: Int?) {
|
func trackInteraction(_ uiElement: AnalyticsUIElement, interactionType: AnalyticsEvent.Interaction.InteractionType, index: Int?) {
|
||||||
let event = AnalyticsEvent.Click(index: index, name: tap.elementName)
|
let event = AnalyticsEvent.Interaction(index: index, interactionType: interactionType, name: uiElement.name)
|
||||||
client.capture(event)
|
client.capture(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track an element that has been tapped without including an index
|
/// Track an element that has been tapped without including an index
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - tap: The element that was tapped
|
/// - uiElement: The element that was tapped
|
||||||
func trackTap(_ tap: AnalyticsUIElement) {
|
func trackInteraction(_ uiElement: AnalyticsUIElement) {
|
||||||
trackTap(tap, index: nil)
|
trackInteraction(uiElement, interactionType: .Touch, index: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track an E2EE error that occurred
|
/// Track an E2EE error that occurred
|
||||||
|
|
|
@ -45,4 +45,12 @@ protocol AnalyticsClientProtocol {
|
||||||
/// Capture the supplied analytics screen event.
|
/// Capture the supplied analytics screen event.
|
||||||
/// - Parameter event: The screen event to capture.
|
/// - Parameter event: The screen event to capture.
|
||||||
func screen(_ event: AnalyticsScreenProtocol)
|
func screen(_ event: AnalyticsScreenProtocol)
|
||||||
|
|
||||||
|
/// Updates any user properties to help with creating cohorts.
|
||||||
|
/// - Parameter userProperties: The user properties to be updated.
|
||||||
|
///
|
||||||
|
/// Only non-nil properties will be updated when calling this method. There might
|
||||||
|
/// be a delay when updating user properties as these are cached to be included
|
||||||
|
/// as part of the next event that gets captured.
|
||||||
|
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,10 @@ import Foundation
|
||||||
import AnalyticsEvents
|
import AnalyticsEvents
|
||||||
|
|
||||||
@objc enum AnalyticsScreen: Int {
|
@objc enum AnalyticsScreen: Int {
|
||||||
|
case welcome
|
||||||
|
case login
|
||||||
|
case register
|
||||||
|
case forgotPassword
|
||||||
case sidebar
|
case sidebar
|
||||||
case home
|
case home
|
||||||
case favourites
|
case favourites
|
||||||
|
@ -51,6 +55,14 @@ import AnalyticsEvents
|
||||||
/// The screen name reported to the AnalyticsEvent.
|
/// The screen name reported to the AnalyticsEvent.
|
||||||
var screenName: AnalyticsEvent.Screen.ScreenName {
|
var screenName: AnalyticsEvent.Screen.ScreenName {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .welcome:
|
||||||
|
return .Welcome
|
||||||
|
case .login:
|
||||||
|
return .Login
|
||||||
|
case .register:
|
||||||
|
return .Register
|
||||||
|
case .forgotPassword:
|
||||||
|
return .ForgotPassword
|
||||||
case .sidebar:
|
case .sidebar:
|
||||||
return .MobileSidebar
|
return .MobileSidebar
|
||||||
case .home:
|
case .home:
|
||||||
|
|
|
@ -16,17 +16,17 @@
|
||||||
|
|
||||||
import AnalyticsEvents
|
import AnalyticsEvents
|
||||||
|
|
||||||
/// A tappable UI element that can be track in Analytics.
|
/// A tappable UI element that can be tracked in Analytics.
|
||||||
@objc enum AnalyticsUIElement: Int {
|
@objc enum AnalyticsUIElement: Int {
|
||||||
case sendMessageButton
|
case removeMe
|
||||||
|
|
||||||
/// The element name reported to the AnalyticsEvent.
|
/// The element name reported to the AnalyticsEvent.
|
||||||
var elementName: AnalyticsEvent.Click.Name {
|
var name: AnalyticsEvent.Interaction.Name {
|
||||||
switch self {
|
switch self {
|
||||||
// Note: This is a test element that doesn't need to be captured.
|
// Note: This is a test element that doesn't need to be captured.
|
||||||
// It will likely be removed when the AnalyticsEvent.Click is updated.
|
// It can be removed when some elements are added that relate to mobile.
|
||||||
case .sendMessageButton:
|
case .removeMe:
|
||||||
return .SendMessageButton
|
return .WebRoomSettingsLeaveButton
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// Copyright 2021 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import AnalyticsEvents
|
||||||
|
|
||||||
|
extension UserSessionProperties.UseCase {
|
||||||
|
var analyticsName: AnalyticsEvent.UserProperties.FtueUseCaseSelection {
|
||||||
|
switch self {
|
||||||
|
case .personalMessaging:
|
||||||
|
return .PersonalMessaging
|
||||||
|
case .workMessaging:
|
||||||
|
return .WorkMessaging
|
||||||
|
case .communityMessaging:
|
||||||
|
return .CommunityMessaging
|
||||||
|
case .skipped:
|
||||||
|
return .Skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,9 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||||
/// The PHGPostHog object used to report events.
|
/// The PHGPostHog object used to report events.
|
||||||
private var postHog: PHGPostHog?
|
private var postHog: PHGPostHog?
|
||||||
|
|
||||||
|
/// Any user properties to be included with the next captured event.
|
||||||
|
private(set) var pendingUserProperties: AnalyticsEvent.UserProperties?
|
||||||
|
|
||||||
var isRunning: Bool { postHog?.enabled ?? false }
|
var isRunning: Bool { postHog?.enabled ?? false }
|
||||||
|
|
||||||
func start() {
|
func start() {
|
||||||
|
@ -36,11 +39,18 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
func identify(id: String) {
|
func identify(id: String) {
|
||||||
postHog?.identify(id)
|
if let userProperties = pendingUserProperties {
|
||||||
|
// As user properties overwrite old ones, compactMap the dictionary to avoid resetting any missing properties
|
||||||
|
postHog?.identify(id, properties: userProperties.properties.compactMapValues { $0 })
|
||||||
|
pendingUserProperties = nil
|
||||||
|
} else {
|
||||||
|
postHog?.identify(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
postHog?.reset()
|
postHog?.reset()
|
||||||
|
pendingUserProperties = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func stop() {
|
func stop() {
|
||||||
|
@ -55,11 +65,38 @@ class PostHogAnalyticsClient: AnalyticsClientProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
func capture(_ event: AnalyticsEventProtocol) {
|
func capture(_ event: AnalyticsEventProtocol) {
|
||||||
postHog?.capture(event.eventName, properties: event.properties)
|
postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties))
|
||||||
}
|
}
|
||||||
|
|
||||||
func screen(_ event: AnalyticsScreenProtocol) {
|
func screen(_ event: AnalyticsScreenProtocol) {
|
||||||
postHog?.screen(event.screenName.rawValue, properties: event.properties)
|
postHog?.screen(event.screenName.rawValue, properties: attachUserProperties(to: event.properties))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateUserProperties(_ userProperties: AnalyticsEvent.UserProperties) {
|
||||||
|
guard let pendingUserProperties = pendingUserProperties else {
|
||||||
|
pendingUserProperties = userProperties
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the updated user properties with the existing ones
|
||||||
|
self.pendingUserProperties = AnalyticsEvent.UserProperties(ftueUseCaseSelection: userProperties.ftueUseCaseSelection ?? pendingUserProperties.ftueUseCaseSelection,
|
||||||
|
numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
/// Given a dictionary containing properties from an event, this method will return those properties
|
||||||
|
/// with any pending user properties included under the `$set` key.
|
||||||
|
/// - Parameter properties: A dictionary of properties from an event.
|
||||||
|
/// - Returns: The `properties` dictionary with any user properties included.
|
||||||
|
private func attachUserProperties(to properties: [String: Any]) -> [String: Any] {
|
||||||
|
guard isRunning, let userProperties = pendingUserProperties else { return properties }
|
||||||
|
|
||||||
|
var properties = properties
|
||||||
|
|
||||||
|
// As user properties overwrite old ones via $set, compactMap the dictionary to avoid resetting any missing properties
|
||||||
|
properties["$set"] = userProperties.properties.compactMapValues { $0 }
|
||||||
|
pendingUserProperties = nil
|
||||||
|
return properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,10 +147,10 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
/// Show the use case screen for new users.
|
/// Show the use case screen for new users.
|
||||||
private func showUseCaseSelectionScreen() {
|
private func showUseCaseSelectionScreen() {
|
||||||
let coordinator = OnboardingUseCaseCoordinator()
|
let coordinator = OnboardingUseCaseSelectionCoordinator()
|
||||||
coordinator.completion = { [weak self, weak coordinator] result in
|
coordinator.completion = { [weak self, weak coordinator] result in
|
||||||
guard let self = self, let coordinator = coordinator else { return }
|
guard let self = self, let coordinator = coordinator else { return }
|
||||||
self.useCaseCoordinator(coordinator, didCompleteWith: result)
|
self.useCaseSelectionCoordinator(coordinator, didCompleteWith: result)
|
||||||
}
|
}
|
||||||
|
|
||||||
coordinator.start()
|
coordinator.start()
|
||||||
|
@ -166,7 +166,7 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Displays the next view in the flow after the use case screen.
|
/// Displays the next view in the flow after the use case screen.
|
||||||
private func useCaseCoordinator(_ coordinator: OnboardingUseCaseCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
|
private func useCaseSelectionCoordinator(_ coordinator: OnboardingUseCaseSelectionCoordinator, didCompleteWith result: OnboardingUseCaseViewModelResult) {
|
||||||
useCaseResult = result
|
useCaseResult = result
|
||||||
showAuthenticationScreen()
|
showAuthenticationScreen()
|
||||||
}
|
}
|
||||||
|
@ -222,12 +222,15 @@ final class OnboardingCoordinator: NSObject, OnboardingCoordinatorProtocol {
|
||||||
completion?()
|
completion?()
|
||||||
isShowingAuthentication = false
|
isShowingAuthentication = false
|
||||||
|
|
||||||
// Handle the chosen use case if appropriate
|
// Handle the chosen use case where applicable
|
||||||
if authenticationType == MXKAuthenticationTypeRegister,
|
if authenticationType == MXKAuthenticationTypeRegister,
|
||||||
let useCaseResult = useCaseResult,
|
let useCase = useCaseResult?.userSessionPropertyValue,
|
||||||
let userSession = UserSessionsService.shared.mainUserSession {
|
let userSession = UserSessionsService.shared.mainUserSession {
|
||||||
// Store the value in the user's session
|
// Store the value in the user's session
|
||||||
userSession.userProperties.useCase = useCaseResult.userSessionPropertyValue
|
userSession.userProperties.useCase = useCase
|
||||||
|
|
||||||
|
// Update the analytics user properties with the use case
|
||||||
|
Analytics.shared.updateUserProperties(ftueUseCase: useCase)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import Foundation
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
enum MockAppScreens {
|
enum MockAppScreens {
|
||||||
static let appScreens: [MockScreenState.Type] = [
|
static let appScreens: [MockScreenState.Type] = [
|
||||||
MockOnboardingUseCaseScreenState.self,
|
MockOnboardingUseCaseSelectionScreenState.self,
|
||||||
MockOnboardingSplashScreenScreenState.self,
|
MockOnboardingSplashScreenScreenState.self,
|
||||||
MockLocationSharingScreenState.self,
|
MockLocationSharingScreenState.self,
|
||||||
MockAnalyticsPromptScreenState.self,
|
MockAnalyticsPromptScreenState.self,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
|
final class OnboardingUseCaseSelectionCoordinator: Coordinator, Presentable {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
init() {
|
init() {
|
||||||
let viewModel = OnboardingUseCaseViewModel()
|
let viewModel = OnboardingUseCaseViewModel()
|
||||||
let view = OnboardingUseCase(viewModel: viewModel.context)
|
let view = OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
|
||||||
onboardingUseCaseViewModel = viewModel
|
onboardingUseCaseViewModel = viewModel
|
||||||
|
|
||||||
let hostingController = VectorHostingController(rootView: view)
|
let hostingController = VectorHostingController(rootView: view)
|
||||||
|
@ -47,9 +47,9 @@ final class OnboardingUseCaseCoordinator: Coordinator, Presentable {
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
func start() {
|
func start() {
|
||||||
MXLog.debug("[OnboardingUseCaseCoordinator] did start.")
|
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] did start.")
|
||||||
onboardingUseCaseViewModel.completion = { [weak self] result in
|
onboardingUseCaseViewModel.completion = { [weak self] result in
|
||||||
MXLog.debug("[OnboardingUseCaseCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
|
MXLog.debug("[OnboardingUseCaseSelectionCoordinator] OnboardingUseCaseViewModel did complete with result: \(result).")
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.completion?(result)
|
self.completion?(result)
|
||||||
}
|
}
|
|
@ -20,7 +20,7 @@ import SwiftUI
|
||||||
/// Using an enum for the screen allows you define the different state cases with
|
/// Using an enum for the screen allows you define the different state cases with
|
||||||
/// the relevant associated data for each case.
|
/// the relevant associated data for each case.
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
enum MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
|
enum MockOnboardingUseCaseSelectionScreenState: MockScreenState, CaseIterable {
|
||||||
// A case for each state you want to represent
|
// A case for each state you want to represent
|
||||||
// with specific, minimal associated data that will allow you
|
// with specific, minimal associated data that will allow you
|
||||||
// mock that screen.
|
// mock that screen.
|
||||||
|
@ -28,11 +28,11 @@ enum MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
|
||||||
|
|
||||||
/// The associated screen
|
/// The associated screen
|
||||||
var screenType: Any.Type {
|
var screenType: Any.Type {
|
||||||
OnboardingUseCase.self
|
OnboardingUseCaseSelectionScreen.self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of screen state definitions
|
/// A list of screen state definitions
|
||||||
static var allCases: [MockOnboardingUseCaseScreenState] {
|
static var allCases: [MockOnboardingUseCaseSelectionScreenState] {
|
||||||
// Each of the presence statuses
|
// Each of the presence statuses
|
||||||
[.default]
|
[.default]
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ enum MockOnboardingUseCaseScreenState: MockScreenState, CaseIterable {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[self, viewModel],
|
[self, viewModel],
|
||||||
AnyView(OnboardingUseCase(viewModel: viewModel.context)
|
AnyView(OnboardingUseCaseSelectionScreen(viewModel: viewModel.context)
|
||||||
.addDependency(MockAvatarService.example))
|
.addDependency(MockAvatarService.example))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ import SwiftUI
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
/// The screen shown to a new user to select their use case for the app.
|
/// The screen shown to a new user to select their use case for the app.
|
||||||
struct OnboardingUseCase: View {
|
struct OnboardingUseCaseSelectionScreen: View {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ struct OnboardingUseCase: View {
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
struct OnboardingUseCase_Previews: PreviewProvider {
|
struct OnboardingUseCase_Previews: PreviewProvider {
|
||||||
static let stateRenderer = MockOnboardingUseCaseScreenState.stateRenderer
|
static let stateRenderer = MockOnboardingUseCaseSelectionScreenState.stateRenderer
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
stateRenderer.screenGroup()
|
stateRenderer.screenGroup()
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import XCTest
|
import XCTest
|
||||||
@testable import Riot
|
@testable import Riot
|
||||||
|
import AnalyticsEvents
|
||||||
|
|
||||||
class AnalyticsTests: XCTestCase {
|
class AnalyticsTests: XCTestCase {
|
||||||
func testAnalyticsPromptNewUser() {
|
func testAnalyticsPromptNewUser() {
|
||||||
|
@ -70,4 +71,68 @@ class AnalyticsTests: XCTestCase {
|
||||||
// Then no prompt should be shown.
|
// Then no prompt should be shown.
|
||||||
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
XCTAssertFalse(showPrompt, "A prompt should not be shown any more.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testAddingUserProperties() {
|
||||||
|
// Given a client with no user properties set
|
||||||
|
let client = PostHogAnalyticsClient()
|
||||||
|
XCTAssertNil(client.pendingUserProperties, "No user properties should have been set yet.")
|
||||||
|
|
||||||
|
// When updating the user properties
|
||||||
|
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: 5))
|
||||||
|
|
||||||
|
// Then the properties should be cached
|
||||||
|
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should match.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMergingUserProperties() {
|
||||||
|
// Given a client with a cached use case user properties
|
||||||
|
let client = PostHogAnalyticsClient()
|
||||||
|
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: nil))
|
||||||
|
|
||||||
|
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||||
|
XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.")
|
||||||
|
|
||||||
|
// When updating the number of spaced
|
||||||
|
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, numSpaces: 5))
|
||||||
|
|
||||||
|
// Then the new properties should be updated and the existing properties should remain unchanged
|
||||||
|
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection shouldn't have changed.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.numSpaces, 5, "The number of spaces should have been updated.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSendingUserProperties() {
|
||||||
|
// Given a client with user properties set
|
||||||
|
let client = PostHogAnalyticsClient()
|
||||||
|
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: nil))
|
||||||
|
client.start()
|
||||||
|
|
||||||
|
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||||
|
|
||||||
|
// When sending an event (tests run under Debug configuration so this is sent to the development instance)
|
||||||
|
client.screen(AnalyticsEvent.Screen(durationMs: nil, screenName: .Home))
|
||||||
|
|
||||||
|
// Then the properties should be cleared
|
||||||
|
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSendingUserPropertiesWithIdentify() {
|
||||||
|
// Given a client with user properties set
|
||||||
|
let client = PostHogAnalyticsClient()
|
||||||
|
client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: .PersonalMessaging, numSpaces: nil))
|
||||||
|
client.start()
|
||||||
|
|
||||||
|
XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.")
|
||||||
|
XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.")
|
||||||
|
|
||||||
|
// When calling identify (tests run under Debug configuration so this is sent to the development instance)
|
||||||
|
client.identify(id: UUID().uuidString)
|
||||||
|
|
||||||
|
// Then the properties should be cleared
|
||||||
|
XCTAssertNil(client.pendingUserProperties, "The user properties should be cleared.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
1
changelog.d/5590.change
Normal file
1
changelog.d/5590.change
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add support for UserProperties to analytics and capture FTUE use case selection.
|
Loading…
Reference in a new issue