From eb21f72b22a435ba71eed19219ee63be43aa4de9 Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Fri, 2 Sep 2022 10:09:02 +0100 Subject: [PATCH] #106: Begin adding the Analytics class from EI. No UI or SDK support to enable/disable yet and no tracking is implemented. --- ElementX.xcodeproj/project.pbxproj | 130 ++++++++++--- .../xcshareddata/swiftpm/Package.resolved | 18 ++ ElementX/Sources/BuildSettings.swift | 18 ++ ElementX/Sources/Other/ElementSettings.swift | 21 ++- .../Services/Analytics/Analytics.swift | 173 ++++++++++++++++++ .../Analytics/AnalyticsClientProtocol.swift | 56 ++++++ .../Analytics/AnalyticsConfiguration.swift | 29 +++ .../Services/Analytics/AnalyticsService.swift | 75 ++++++++ .../Analytics/AnalyticsSettings.swift | 50 +++++ .../Helpers/JoinedRoomSize+MemberCount.swift | 36 ++++ .../Helpers/UserProperties+Element.swift | 33 ++++ .../Analytics/PHGPostHogConfiguration.swift | 29 +++ .../Analytics/PostHogAnalyticsClient.swift | 104 +++++++++++ .../Sources/Services/Client/ClientProxy.swift | 12 +- .../Services/Client/ClientProxyProtocol.swift | 6 + .../Services/Client/MockClientProxy.swift | 8 + .../Services/Session/MockUserSession.swift | 2 +- ElementX/SupportingFiles/target.yml | 2 + UnitTests/Sources/AnalyticsTests.swift | 136 ++++++++++++++ UnitTests/SupportingFiles/target.yml | 2 - changelog.d/106.wip | 1 + project.yml | 6 + 22 files changed, 919 insertions(+), 28 deletions(-) create mode 100644 ElementX/Sources/Services/Analytics/Analytics.swift create mode 100644 ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift create mode 100644 ElementX/Sources/Services/Analytics/AnalyticsConfiguration.swift create mode 100644 ElementX/Sources/Services/Analytics/AnalyticsService.swift create mode 100644 ElementX/Sources/Services/Analytics/AnalyticsSettings.swift create mode 100644 ElementX/Sources/Services/Analytics/Helpers/JoinedRoomSize+MemberCount.swift create mode 100644 ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift create mode 100644 ElementX/Sources/Services/Analytics/PHGPostHogConfiguration.swift create mode 100644 ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift create mode 100644 UnitTests/Sources/AnalyticsTests.swift create mode 100644 changelog.d/106.wip diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index beda3b139..ff8650701 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -56,7 +56,6 @@ 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5A56C4F47C368EBE5C5E870 /* DesignKit */; }; 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */; }; 224A55EEAEECF5336B14A4A5 /* EmoteRoomMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE2DF459F1737A594667CC46 /* EmoteRoomMessage.swift */; }; - 226027BE23AF64FA61C7A4C0 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; }; 22DADD537401E79D66132134 /* NavigationRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4173A48FD8542CD4AD3645C /* NavigationRouter.swift */; }; 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; }; 23B2CD5A06B16055BDDD0994 /* ApplicationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */; }; @@ -94,13 +93,15 @@ 388FD50AC66E9E684DDFA9D8 /* ServerSelectionScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5D2C0950F8196232D88045C /* ServerSelectionScreen.swift */; }; 38C76D586404C1FDED095F3A /* LoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B01468022EC826CB2FD2C0 /* LoginModels.swift */; }; 3B770CB4DED51CC362C66D47 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */; }; - 3C549A0BF39F8A854D45D9FD /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; + 3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; }; + 3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */; }; 3D325A1147F6281C57BFCDF6 /* EventBrief.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4411C0DA0087A1CB143E96FA /* EventBrief.swift */; }; 3DA57CA0D609A6B37CA1DC2F /* BugReportService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6DC38E64A5ED3FDB201029A /* BugReportService.swift */; }; 3ED2725734568F6B8CC87544 /* AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5C6FBF97B6EED3D4FA5EFF /* AttributedStringBuilder.swift */; }; 3F2148F11164C7C5609984EB /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 19CD5B074D7DD44AF4C58BB6 /* SwiftState */; }; + 407DCE030E0F9B7C9861D38A /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; 418B4AEFD03DC7A6D2C9D5C8 /* EventBriefFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36322DD0D4E29D31B0945ADC /* EventBriefFactory.swift */; }; - 41DFDD212D1BE57CA50D783B /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 997C7385E1A07E061D7E2100 /* GZIP */; }; + 41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; 438FB9BC535BC95948AA5F34 /* SettingsViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B2F9D5C39A4494D19F33E38 /* SettingsViewModelProtocol.swift */; }; 43FD77998F33C32718C51450 /* TemplateCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */; }; 462813B93C39DF93B1249403 /* RoundedToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFABDF2E19D349DAAAC18C65 /* RoundedToastView.swift */; }; @@ -126,6 +127,7 @@ 5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; }; 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6235E1CE00A6D989D7DB6D47 /* RectangleToastView.swift */; }; 541374590CA7E8318BD480FD /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 187853A7E643995EE49FAD43 /* Localizable.stringsdict */; }; + 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */; }; 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9010EE0CC913D095887EF36E /* OIDCService.swift */; }; 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24F5530B2212862FA4BEFF2D /* HomeScreenViewModelProtocol.swift */; }; 59C41313AED7566C3AC51163 /* RoomSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A953B6C0C431DBF4DD00B4 /* RoomSummary.swift */; }; @@ -138,7 +140,7 @@ 5F5488FBC9CFEB6F433D74A4 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7109E709A7738E6BCC4553E6 /* Localizable.strings */; }; 60ED66E63A169E47489348A8 /* GZIP in Frameworks */ = {isa = PBXBuildFile; productRef = 2B788C81F6369D164ADEB917 /* GZIP */; }; 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A00C7A331B72C0F05C00392F /* RoomScreenViewModelProtocol.swift */; }; - 6298AB0906DDD3525CD78C6B /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; + 6298AB0906DDD3525CD78C6B /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 9573B94B1C86C6DF751AF3FD /* SwiftState */; }; 62BBF5BE7B905222F0477FF2 /* MediaSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8210612D17A39369480FC183 /* MediaSource.swift */; }; 63C9AF0FB8278AF1C0388A0C /* TemplateModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAB10E673916D2B8D21FD197 /* TemplateModels.swift */; }; 64FF5CB4E35971255872E1BB /* AuthenticationServiceProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F0CB536D1C3CC15AA740CC6 /* AuthenticationServiceProxyProtocol.swift */; }; @@ -154,7 +156,7 @@ 6AC1DC1EAD9F7568360DA1BA /* ServerSelectionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A30A1758E2B73EF38E7C42F8 /* ServerSelectionModels.swift */; }; 6D046D653DA28ADF1E6E59A4 /* BackgroundTaskServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE73D571D4F9C36DD45255A /* BackgroundTaskServiceProtocol.swift */; }; 6EA61FCA55D950BDE326A1A7 /* ImageAnonymizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A626D74BBE9F4A60763B45 /* ImageAnonymizer.swift */; }; - 6F2AB43A1EFAD8A97AF41A15 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; + 6F2AB43A1EFAD8A97AF41A15 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; 6FC10A00D268FCD48B631E37 /* ViewFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFF7BF82A950B91BC5469E91 /* ViewFrameReader.swift */; }; 7002C55A4C917F3715765127 /* MediaProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */; }; 706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FCD9116ADDE820E4E30F92 /* UIKitBackgroundTask.swift */; }; @@ -192,15 +194,17 @@ 87756CA950ED55870A1AAE8F /* ServerSelectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7D706FFF438CAF16F44D8C /* ServerSelectionCoordinator.swift */; }; 8775F46AE3234A5A5688C19D /* UserIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FAAA4A76CE4A1F3014D9 /* UserIndicator.swift */; }; 8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; }; + 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C687844F60BFF532D49A994C /* AnalyticsTests.swift */; }; 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5E9C044BEB7C70B1378E91 /* UserSession.swift */; }; 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F49CDE349C490D617332770 /* NoticeRoomTimelineItem.swift */; }; 8CC12086CBF91A7E10CDC205 /* HomeScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */; }; 8D9F646387DF656EF91EE4CB /* RoomMessageFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F37AB24AF5A006521D38D1 /* RoomMessageFactoryProtocol.swift */; }; + 8F2FAA98457750D9D664136F /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; 9219640F4D980CFC5FE855AD /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = 536E72DCBEEC4A1FE66CFDCE /* target.yml */; }; 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */; }; - 93BA4A81B6D893271101F9F0 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; + 93BA4A81B6D893271101F9F0 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */; }; 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */; }; 94D0F36A87E596A93C0C178A /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B5CF94E124616FD89424B73 /* SplashScreenViewModelTests.swift */; }; @@ -211,19 +215,21 @@ 989029A28C9E2F828AD6658A /* AppIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */; }; 992F5E750F5030C4BA2D0D03 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 01C4C7DB37597D7D8379511A /* Assets.xcassets */; }; 99ED42B8F8D6BFB1DBCF4C45 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 4346F63D53A346271577FD9C /* AppAuth */; }; - 9AC5F8142413862A9E3A2D98 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; + 9AC5F8142413862A9E3A2D98 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; }; 9B8DE1D424E37581C7D99CCC /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */; }; 9BD3A773186291560DF92B62 /* RoomTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F2402D738694F98729A441 /* RoomTimelineProvider.swift */; }; 9BE7A9CF6C593251D734B461 /* MockServerSelectionScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A20AE75FF4FF35B1FF6CA7 /* MockServerSelectionScreenState.swift */; }; 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */; }; 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56F01DD1BBD4450E18115916 /* LabelledActivityIndicatorView.swift */; }; 9CB5129C83F75921E5E28028 /* ToastViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */; }; - 9D2E03DB175A6AB14589076D /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */; }; + 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */; }; + 9D2E03DB175A6AB14589076D /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA4E1BEB4E9BC2467006E12B /* AppAuth */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9E8AE387FD03E4F1C1B8815A /* SessionVerificationStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06FCD42EEFEFC220F14EAC5 /* SessionVerificationStateMachine.swift */; }; A00DFC1DD3567B1EDC9F8D16 /* SplashScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325A2B3278875554DDEB8A9B /* SplashScreenUITests.swift */; }; A0A0D2A9564BDA3FDE2E360F /* FormattedBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */; }; A32517FB1CA0BBCE2BC75249 /* BugReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD6C07DA7D3FF193F7419F55 /* BugReportCoordinator.swift */; }; + A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73FC861755C6388F62B9280A /* Analytics.swift */; }; A4E885358D7DD5A072A06824 /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; }; A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A901D95158B02CA96C79C7F /* InfoPlist.swift */; }; @@ -262,11 +268,11 @@ C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; }; C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; }; - CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F87116470221880017CF522 /* BuildSettings.swift */; }; - CB137BFB3E083C33E398A6CB /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */; }; + CB137BFB3E083C33E398A6CB /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 5986E300FC849DEAB2EE7AEB /* Introspect */; }; CB326BAB54E9B68658909E36 /* Benchmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */; }; CB498F4E27AA0545DCEF0F6F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; }; CC736DA1AA8F8B9FD8785009 /* ScreenshotDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C4AF6E3885730CD560311C /* ScreenshotDetector.swift */; }; + CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */; }; CE1694C7BB93C3311524EF28 /* Untranslated.strings in Resources */ = {isa = PBXBuildFile; fileRef = D2F7194F440375338F8E2487 /* Untranslated.strings */; }; CE7A715947ABAB1DEB5C21D7 /* SplashScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F7A812F160E75B69A9181A2 /* SplashScreenCoordinator.swift */; }; CEB8FB1269DE20536608B957 /* LoginMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B41FABA2B0AEF4389986495 /* LoginMode.swift */; }; @@ -285,9 +291,11 @@ DD9B70DE54B24E0694A35D8A /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A63815AD6A5C306453342F2 /* ImageRoomTimelineItem.swift */; }; DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D6764D6976D235926FE5FC /* HomeScreenViewModel.swift */; }; + DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */; }; DFF7D6A6C26DDD40D00AE579 /* target.yml in Resources */ = {isa = PBXBuildFile; fileRef = F012CB5EE3F2B67359F6CC52 /* target.yml */; }; E0A4DCA633D174EB43AD599F /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; }; E1DF24D085572A55C9758A2D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E89E530A8E92EC44301CA1 /* Bundle.swift */; }; + E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */; }; E481C8FDCB6C089963C95344 /* DTCoreText in Frameworks */ = {isa = PBXBuildFile; productRef = 527578916BD388A09F5A8036 /* DTCoreText */; }; E5895C74615CBE8462FB840F /* SessionVerificationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCF86010A0A719A9A50EEC59 /* SessionVerificationCoordinator.swift */; }; E81EEC1675F2371D12A880A3 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61ADFB893DEF81E58DF3FAB9 /* MockRoomTimelineController.swift */; }; @@ -296,6 +304,7 @@ EA65360A0EC026DD83AC0CF5 /* AuthenticationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */; }; EBD6C79705B3DDB2F7E5F554 /* UserSessionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF1B52D0ABBA7091A991CAFE /* UserSessionStoreProtocol.swift */; }; EC280623A42904341363EAAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 886A0A498FA01E8EDD451D05 /* Sentry */; }; + EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */; }; EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F40F48279322E504153AB0D /* MockClientProxy.swift */; }; EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; }; EEC40663922856C65D1E0DF5 /* KeychainControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB9C37196A4C79F24CE80C6 /* KeychainControllerTests.swift */; }; @@ -305,8 +314,9 @@ F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4427F9E0571B4E6E048A2B /* KeychainController.swift */; }; F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; }; F56261126E368C831B3DE976 /* NavigationRouterType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */; }; - F656F92A63D3DC1978D79427 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = AA4E1BEB4E9BC2467006E12B /* AppAuth */; }; + F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */ = {isa = PBXBuildFile; productRef = 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */; }; F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; }; + F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; }; F78C57B197DA74735FEBB42C /* EventBriefFactoryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92B61C243325DC76D3086494 /* EventBriefFactoryProtocol.swift */; }; F99FB21EFC6D99D247FE7CBE /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D82E84F90358CC1118E6034B /* Introspect */; }; FA9C427FFB11B1AA2DCC5602 /* RoomProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */; }; @@ -381,6 +391,7 @@ 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 167521635A1CC27624FCEB7F /* ServerSelectionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModel.swift; sourceTree = ""; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = ""; }; + 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecorationTimelineItemProtocol.swift; sourceTree = ""; }; 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxy.swift; sourceTree = ""; }; 193FB285430D3956B6E61E4D /* UserIndicatorViewPresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorViewPresentable.swift; sourceTree = ""; }; @@ -462,6 +473,7 @@ 4990FDBDA96B88E214F92F48 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 49D2C8E66E83EA578A7F318A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 49EAD710A2C16EFF7C3EA16F /* Benchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmark.swift; sourceTree = ""; }; + 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettings.swift; sourceTree = ""; }; 4B40B7F6FCCE2D8C242492D9 /* ga */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ga; path = ga.lproj/Localizable.strings; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; 4C82DAE0B8EB28234E84E6CF /* ToastViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewState.swift; sourceTree = ""; }; @@ -480,6 +492,7 @@ 529513218340CC8419273165 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; 534A5C8FCDE2CBC50266B9F2 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = gl; path = gl.lproj/Localizable.stringsdict; sourceTree = ""; }; 536E72DCBEEC4A1FE66CFDCE /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; + 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 54E438DBCBDC7A41B95DDDD9 /* MXLogObjcWrapper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXLogObjcWrapper.m; sourceTree = ""; }; 55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = ""; }; 55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = ""; }; @@ -517,6 +530,7 @@ 68706A66BBA04268F7747A2F /* ActivityIndicatorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorPresenter.swift; sourceTree = ""; }; 6920A4869821BF72FFC58842 /* MockMediaProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaProvider.swift; sourceTree = ""; }; 6A152791A2F56BD193BFE986 /* MemberDetailsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberDetailsProvider.swift; sourceTree = ""; }; + 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserProperties+Element.swift"; sourceTree = ""; }; 6A901D95158B02CA96C79C7F /* InfoPlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlist.swift; sourceTree = ""; }; 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixEntityRegex.swift; sourceTree = ""; }; 6B73A8C3118EAC7BF3F3EE7A /* SplashScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -532,6 +546,7 @@ 71D52BAA5BADB06E5E8C295D /* Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; 72D03D36422177EF01905D20 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderProtocol.swift; sourceTree = ""; }; + 73FC861755C6388F62B9280A /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; 752DEC02D93AFF46BC13313A /* NavigationRouterType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRouterType.swift; sourceTree = ""; }; 799A3A11C434296ED28F87C8 /* iw */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = iw; path = iw.lproj/Localizable.strings; sourceTree = ""; }; 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportViewModelTests.swift; sourceTree = ""; }; @@ -610,6 +625,7 @@ A4756C5A8C8649AD6C10C615 /* MockUserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserSession.swift; sourceTree = ""; }; A64F0DB78E0AC23C91AD89EF /* mk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = mk; path = mk.lproj/Localizable.strings; sourceTree = ""; }; A65F140F9FE5E8D4DAEFF354 /* RoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxy.swift; sourceTree = ""; }; + A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A72232816DCE2B76D48E1367 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; A8903A9F615BBD0E6D7CD133 /* ApplicationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationProtocol.swift; sourceTree = ""; }; A8D1CC633517D695FEC54208 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; @@ -662,6 +678,7 @@ C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = ""; }; C483956FA3D665E3842E319A /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxy.swift; sourceTree = ""; }; + C687844F60BFF532D49A994C /* AnalyticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsTests.swift; sourceTree = ""; }; C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportUITests.swift; sourceTree = ""; }; C88508B6F7974CFABEC4B261 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; C888BCD78E2A55DCE364F160 /* MediaProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderProtocol.swift; sourceTree = ""; }; @@ -673,6 +690,7 @@ CBA95E52C4C6EE8769A63E57 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = ""; }; CBBCC6E74774E79B599625D0 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; CBDA364DFFC3AC71C4771251 /* NoticeRoomMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomMessage.swift; sourceTree = ""; }; + CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JoinedRoomSize+MemberCount.swift"; sourceTree = ""; }; CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CC7CCC6DE5FA623E31BA8546 /* RoomTimelineControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineControllerProtocol.swift; sourceTree = ""; }; CCA431E6EDD71F7067B5F9E7 /* UITestsRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsRootView.swift; sourceTree = ""; }; @@ -696,6 +714,7 @@ D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = ""; }; D6D094C15E8DB424F1C6FC94 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = ""; }; D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = ""; }; + D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = ""; }; DBD460ED7ED1E03B85DEA25C /* TemplateCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateCoordinator.swift; sourceTree = ""; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = ""; }; DCE978A6118C131D7F2A04B3 /* SplashScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashScreenModels.swift; sourceTree = ""; }; @@ -707,6 +726,7 @@ E0FCA0957FAA0E15A9F5579D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Untranslated.stringsdict; sourceTree = ""; }; E157152B11E347F735C3FD6E /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = tr; path = tr.lproj/Localizable.stringsdict; sourceTree = ""; }; E18CF12478983A5EB390FB26 /* MessageComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageComposer.swift; sourceTree = ""; }; + E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = ""; }; E3E29F98CF0E960689A410E3 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = ""; }; E45C57120F28F8D619150219 /* sr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sr; path = sr.lproj/Localizable.strings; sourceTree = ""; }; E4BB9A17AC512A7EF4B106E5 /* SessionVerificationModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationModels.swift; sourceTree = ""; }; @@ -786,15 +806,17 @@ files = ( 1A70A2199394B5EC660934A5 /* MatrixRustSDK in Frameworks */, 1F3232BD368DF430AB433907 /* DesignKit in Frameworks */, - F656F92A63D3DC1978D79427 /* AppAuth in Frameworks */, - 9D2E03DB175A6AB14589076D /* DTCoreText in Frameworks */, - 6F2AB43A1EFAD8A97AF41A15 /* KeychainAccess in Frameworks */, - 93BA4A81B6D893271101F9F0 /* Kingfisher in Frameworks */, - 9AC5F8142413862A9E3A2D98 /* Introspect in Frameworks */, - CB137BFB3E083C33E398A6CB /* SwiftyBeaver in Frameworks */, - 3C549A0BF39F8A854D45D9FD /* SwiftState in Frameworks */, - 41DFDD212D1BE57CA50D783B /* GZIP in Frameworks */, - 6298AB0906DDD3525CD78C6B /* Sentry in Frameworks */, + F656F92A63D3DC1978D79427 /* AnalyticsEvents in Frameworks */, + 9D2E03DB175A6AB14589076D /* AppAuth in Frameworks */, + 6F2AB43A1EFAD8A97AF41A15 /* DTCoreText in Frameworks */, + 93BA4A81B6D893271101F9F0 /* KeychainAccess in Frameworks */, + 9AC5F8142413862A9E3A2D98 /* Kingfisher in Frameworks */, + CB137BFB3E083C33E398A6CB /* Introspect in Frameworks */, + 3C549A0BF39F8A854D45D9FD /* PostHog in Frameworks */, + 41DFDD212D1BE57CA50D783B /* SwiftyBeaver in Frameworks */, + 6298AB0906DDD3525CD78C6B /* SwiftState in Frameworks */, + 407DCE030E0F9B7C9861D38A /* GZIP in Frameworks */, + 8F2FAA98457750D9D664136F /* Sentry in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -838,6 +860,7 @@ 0787F81684E503024BD0C051 /* Services */ = { isa = PBXGroup; children = ( + 4BF8D11D9ED15CFC373D0119 /* Analytics */, AAFDD509929A0CCF8BCE51EB /* Authentication */, EBBEB5471737E9D116DF4738 /* Background */, 0ED3F5C21537519389C07644 /* BugReport */, @@ -953,6 +976,15 @@ path = ServerSelection; sourceTree = ""; }; + 3A304097A59704AC9B869EC6 /* Helpers */ = { + isa = PBXGroup; + children = ( + CBF9AEA706926DD0DA2B954C /* JoinedRoomSize+MemberCount.swift */, + 6A6C4BE591FE5C38CE9C7EF3 /* UserProperties+Element.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 4009BE2E791C16AC6EE39A7E /* BugReport */ = { isa = PBXGroup; children = ( @@ -1049,6 +1081,21 @@ path = Tests; sourceTree = ""; }; + 4BF8D11D9ED15CFC373D0119 /* Analytics */ = { + isa = PBXGroup; + children = ( + 73FC861755C6388F62B9280A /* Analytics.swift */, + E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */, + D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */, + 5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */, + 4B362E695A7103C11F64B185 /* AnalyticsSettings.swift */, + A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */, + 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */, + 3A304097A59704AC9B869EC6 /* Helpers */, + ); + path = Analytics; + sourceTree = ""; + }; 4EC4EBBC4F6885775F198875 /* Sources */ = { isa = PBXGroup; children = ( @@ -1180,6 +1227,7 @@ 73CD9796729EB702B4DFA88C /* Sources */ = { isa = PBXGroup; children = ( + C687844F60BFF532D49A994C /* AnalyticsTests.swift */, AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */, 6DFCAA239095A116976E32C4 /* BackgroundTaskTests.swift */, EFFD3200F9960D4996159F10 /* BugReportServiceTests.swift */, @@ -1782,11 +1830,13 @@ packageProductDependencies = ( A678E40E917620059695F067 /* MatrixRustSDK */, A5A56C4F47C368EBE5C5E870 /* DesignKit */, + 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */, AA4E1BEB4E9BC2467006E12B /* AppAuth */, 531CE4334AC5CA8DFF6AEB84 /* DTCoreText */, 020597E28A4BC8E1BE8EDF6E /* KeychainAccess */, 0DD568A494247444A4B56031 /* Kingfisher */, 5986E300FC849DEAB2EE7AEB /* Introspect */, + 4278261E147DB2DE5CFB7FC5 /* PostHog */, FD43A50D9B75C9D6D30F006B /* SwiftyBeaver */, 9573B94B1C86C6DF751AF3FD /* SwiftState */, 997C7385E1A07E061D7E2100 /* GZIP */, @@ -1926,6 +1976,7 @@ ); mainGroup = 405B00F139AEE3994601B36A; packageReferences = ( + AC3475112CA40C2C6E78D1EB /* XCRemoteSwiftPackageReference "matrix-analytics-events" */, 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */, C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */, 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */, @@ -1933,6 +1984,7 @@ 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */, D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */, 80B898A3AD2AC63F3ABFC218 /* XCRemoteSwiftPackageReference "matrix-rust-components-swift" */, + 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */, A08925A9D5E3770DEB9D8509 /* XCRemoteSwiftPackageReference "sentry-cocoa" */, E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */, 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */, @@ -2082,11 +2134,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 890F0D453FE388756479AC97 /* AnalyticsTests.swift in Sources */, 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */, 0F9E38A75337D0146652ACAB /* BackgroundTaskTests.swift in Sources */, 7F61F9ACD5EC9E845EF3EFBF /* BugReportServiceTests.swift in Sources */, C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */, - CA1E41AE5CDCB8D801DE0830 /* BuildSettings.swift in Sources */, 9C45CE85325CD591DADBC4CA /* ElementXTests.swift in Sources */, F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */, 0B1F80C2BF7D223159FBA82C /* ImageAnonymizerTests.swift in Sources */, @@ -2102,7 +2154,6 @@ 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, 94E062D08E27B0387658E364 /* SplashScreenViewModelTests.swift in Sources */, 7AE1FFB132F2B84EB8A2AEBC /* TemplateViewModelTests.swift in Sources */, - 226027BE23AF64FA61C7A4C0 /* TimelineStyle.swift in Sources */, 1151DCC5EC2C6585826545EC /* UserIndicatorPresenterSpy.swift in Sources */, 4B8A2C45FF906ADBB1F5C3B4 /* UserIndicatorQueueTests.swift in Sources */, BEEC06EFD30BFCA02F0FD559 /* UserIndicatorTests.swift in Sources */, @@ -2117,6 +2168,11 @@ 4D23C56053013437C35E511E /* ActivityIndicatorPresenterType.swift in Sources */, FC6B7436C3A5B3D0565227D5 /* ActivityIndicatorView.swift in Sources */, A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */, + A371629728E597C5FCA3C2B2 /* Analytics.swift in Sources */, + F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */, + 54C774874BED4A8FAD1F22FE /* AnalyticsConfiguration.swift in Sources */, + 3C73442084BF8A6939F0F80B /* AnalyticsService.swift in Sources */, + EC4C31963E755EEC77BD778C /* AnalyticsSettings.swift in Sources */, A636D4900E0D98ED91536482 /* AppCoordinator.swift in Sources */, B3FDB1D9CF40777695DBBD1D /* AppCoordinatorStateMachine.swift in Sources */, 2FE4EEF780553B25A446BBFB /* AppDelegate.swift in Sources */, @@ -2174,6 +2230,7 @@ DDB80FD2753FEAAE43CC2AAE /* ImageRoomTimelineItem.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, A5C8F013ED9FB8AA6FEE18A7 /* InfoPlist.swift in Sources */, + E3CA565A4B9704F191B191F0 /* JoinedRoomSize+MemberCount.swift in Sources */, F4C3FEDB1B3A05376A1723A3 /* KeychainController.swift in Sources */, F2DD8661B5C0BA2BB526FA6C /* KeychainControllerProtocol.swift in Sources */, 9C9E48A627C7C166084E3F5B /* LabelledActivityIndicatorView.swift in Sources */, @@ -2217,7 +2274,9 @@ 8BBD3AA589DEE02A1B0923B2 /* NoticeRoomTimelineItem.swift in Sources */, 368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */, 563A05B43207D00A6B698211 /* OIDCService.swift in Sources */, + CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, 7D1DAAA364A9A29D554BD24E /* PlaceholderAvatarImage.swift in Sources */, + DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */, BF35062D06888FA80BD139FF /* Presentable.swift in Sources */, C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */, 53B9C2240C2F5533246EE230 /* RectangleToastView.swift in Sources */, @@ -2314,6 +2373,7 @@ 15D1F9C415D9C921643BA82E /* UserIndicatorRequest.swift in Sources */, C052A8CDC7A8E7A2D906674F /* UserIndicatorStore.swift in Sources */, 80E04BE80A89A78FBB4863BB /* UserIndicatorViewPresentable.swift in Sources */, + 9CCC77C31CB399661A034739 /* UserProperties+Element.swift in Sources */, 8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */, 978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */, 79A6E08ADE6E7C460A8A17A5 /* UserSessionStore.swift in Sources */, @@ -2934,6 +2994,14 @@ version = "1.0.13-alpha"; }; }; + 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/PostHog/posthog-ios"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.4.4; + }; + }; 9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; @@ -2950,6 +3018,14 @@ minimumVersion = 7.15.0; }; }; + AC3475112CA40C2C6E78D1EB /* XCRemoteSwiftPackageReference "matrix-analytics-events" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/matrix-org/matrix-analytics-events"; + requirement = { + branch = main; + kind = branch; + }; + }; C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Cocoanetics/DTCoreText"; @@ -3007,6 +3083,11 @@ package = E9C4F3A12AA1F65C13A8C8EB /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; productName = SnapshotTesting; }; + 2A3F7BCCB18C15B30CCA39A9 /* AnalyticsEvents */ = { + isa = XCSwiftPackageProductDependency; + package = AC3475112CA40C2C6E78D1EB /* XCRemoteSwiftPackageReference "matrix-analytics-events" */; + productName = AnalyticsEvents; + }; 2B43F2AF7456567FE37270A7 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = 61916C63E3F5BD900F08DA0C /* XCRemoteSwiftPackageReference "KeychainAccess" */; @@ -3027,6 +3108,11 @@ package = 6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */; productName = SwiftState; }; + 4278261E147DB2DE5CFB7FC5 /* PostHog */ = { + isa = XCSwiftPackageProductDependency; + package = 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */; + productName = PostHog; + }; 4346F63D53A346271577FD9C /* AppAuth */ = { isa = XCSwiftPackageProductDependency; package = 4CE94127E27181B8B72188F0 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */; diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e901af35d..f09e89c48 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -63,6 +63,15 @@ "version" : "7.2.4" } }, + { + "identity" : "matrix-analytics-events", + "kind" : "remoteSourceControl", + "location" : "https://github.com/matrix-org/matrix-analytics-events", + "state" : { + "branch" : "main", + "revision" : "53ad46ba1ea1ee8f21139dda3c351890846a202f" + } + }, { "identity" : "matrix-rust-components-swift", "kind" : "remoteSourceControl", @@ -72,6 +81,15 @@ "version" : "1.0.13-alpha" } }, + { + "identity" : "posthog-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/PostHog/posthog-ios", + "state" : { + "revision" : "d3177a24686ead147f5e4ff4d7e284ca4be33870", + "version" : "1.4.4" + } + }, { "identity" : "sentry-cocoa", "kind" : "remoteSourceControl", diff --git a/ElementX/Sources/BuildSettings.swift b/ElementX/Sources/BuildSettings.swift index c6b9f82c2..020363d9f 100644 --- a/ElementX/Sources/BuildSettings.swift +++ b/ElementX/Sources/BuildSettings.swift @@ -30,6 +30,24 @@ final class BuildSettings { static let bugReportUISIId = "element-auto-uisi" static let bugReportGHLabels = ["Element-X"] + + // MARK: - Analytics + + #if DEBUG + /// The configuration to use for analytics during development. Set `isEnabled` to false to disable analytics in debug builds. + /// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations. + static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"), + host: "https://posthog.element.dev", + apiKey: "phc_VtA1L35nw3aeAtHIx1ayrGdzGkss7k1xINeXcoIQzXN", + termsURL: URL(string: "https://element.io/cookie-policy")!) // swiftlint:disable:this force_unwrapping + #else + /// The configuration to use for analytics. Set `isEnabled` to false to disable analytics. + /// **Note:** Analytics are disabled by default for forks. If you are maintaining a fork, set custom configurations. + static let analyticsConfiguration = AnalyticsConfiguration(isEnabled: ElementInfoPlist.cfBundleIdentifier.starts(with: "io.element.elementx"), + host: "https://posthog.hss.element.io", + apiKey: "phc_Jzsm6DTm6V2705zeU5dcNvQDlonOR68XvX2sh1sEOHO", + termsURL: URL(string: "https://element.io/cookie-policy")!) // swiftlint:disable:this force_unwrapping + #endif // MARK: - Settings screen diff --git a/ElementX/Sources/Other/ElementSettings.swift b/ElementX/Sources/Other/ElementSettings.swift index 405de8a66..745214c1c 100644 --- a/ElementX/Sources/Other/ElementSettings.swift +++ b/ElementX/Sources/Other/ElementSettings.swift @@ -23,6 +23,8 @@ final class ElementSettings: ObservableObject { public enum UserDefaultsKeys: String { case timelineStyle + case enableAnalytics + case isIdentifiedForAnalytics } static let shared = ElementSettings() @@ -35,8 +37,25 @@ final class ElementSettings: ObservableObject { private init() { // no-op } + + // MARK: - Analytics + + /// Whether the user has already been shown the PostHog analytics prompt. + var hasSeenAnalyticsPrompt: Bool { + Self.store.object(forKey: UserDefaultsKeys.enableAnalytics.rawValue) != nil + } + + /// `true` when the user has opted in to send analytics. + @AppStorage(UserDefaultsKeys.enableAnalytics.rawValue, store: store) + var enableAnalytics = false + + /// Indicates if the device has already called identify for this session to PostHog. + /// This is separate to `enableAnalytics` as logging out leaves analytics + /// enabled, but requires the next account to be identified separately. + @AppStorage(UserDefaultsKeys.isIdentifiedForAnalytics.rawValue, store: store) + var isIdentifiedForAnalytics = false - // MARK: - + // MARK: - Room Screen @AppStorage(UserDefaultsKeys.timelineStyle.rawValue, store: store) var timelineStyle = BuildSettings.defaultRoomTimelineStyle diff --git a/ElementX/Sources/Services/Analytics/Analytics.swift b/ElementX/Sources/Services/Analytics/Analytics.swift new file mode 100644 index 000000000..9cf2f1a44 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/Analytics.swift @@ -0,0 +1,173 @@ +// +// 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 AnalyticsEvents +import PostHog + +/// A class responsible for managing a variety of analytics clients +/// and sending events through these clients. +/// +/// Events may include user activity, or app health data such as crashes, +/// non-fatal issues and performance. `Analytics` class serves as a façade +/// to all these use cases. +/// +/// ## Creating Analytics Events +/// +/// Events are managed in a shared repo for all Element clients https://github.com/matrix-org/matrix-analytics-events +/// To add a new event create a PR to that repo with the new/updated schema. Once merged +/// into `main`, update the AnalyticsEvents Swift package in `project.yml`. +/// +class Analytics { + // MARK: - Properties + + /// The singleton instance to be used within the Riot target. + static let shared = Analytics() + + /// The analytics client to send events with. + private var client: AnalyticsClientProtocol = PostHogAnalyticsClient() + + /// The monitoring client to track crashes, issues and performance +// private var monitoringClient = SentryMonitoringClient() + + /// The service used to interact with account data settings. + private var service: AnalyticsService? + + /// Whether or not the object is enabled and sending events to the server. + var isRunning: Bool { client.isRunning } + + /// Whether to show the user the analytics opt in prompt. + var shouldShowAnalyticsPrompt: Bool { + // Only show the prompt once, and when analytics are enabled in BuildSettings. + !ElementSettings.shared.hasSeenAnalyticsPrompt && BuildSettings.analyticsConfiguration.isEnabled + } + + // MARK: - Public + + /// Opts in to analytics tracking with the supplied session. + /// - Parameter session: An optional session to use to when reading/generating the analytics ID. + /// The session will be ignored if not running. + func optIn(with session: UserSessionProtocol) { + ElementSettings.shared.enableAnalytics = true + startIfEnabled() + + Task { await useAnalyticsSettings(from: session) } + } + + /// Stops analytics tracking and calls `reset` to clear any IDs and event queues. + func optOut() { + ElementSettings.shared.enableAnalytics = false + + // The order is important here. PostHog ignores the reset if stopped. + reset() + client.stop() +// monitoringClient.stop() + + MXLog.debug("Stopped.") + } + + /// Starts the analytics client if the user has opted in, otherwise does nothing. + func startIfEnabled() { + guard ElementSettings.shared.enableAnalytics, !isRunning else { return } + + client.start() +// monitoringClient.start() + + // Sanity check in case something went wrong. + guard client.isRunning else { return } + + MXLog.debug("Started.") + + // Catch and log crashes +// MXLogger.logCrashes(true) +// MXLogger.setBuildVersion(ElementInfoPlist.cfBundleShortVersionString) + } + + /// Use the analytics settings from the supplied session to configure analytics. + /// For now this is only used for (pseudonymous) identification. + /// - Parameter session: The session to read analytics settings from. + func useAnalyticsSettings(from session: UserSessionProtocol) async { + guard + ElementSettings.shared.enableAnalytics, + !ElementSettings.shared.isIdentifiedForAnalytics + else { return } + + let service = AnalyticsService(session: session) + self.service = service + + switch await service.settings() { + case .success(let settings): + identify(with: settings) + self.service = nil + case .failure: + MXLog.error("Failed to use analytics settings. Will continue to run without analytics ID.") + self.service = nil + } + } + + /// Resets the any IDs and event queues in the analytics client. This method should + /// be called on sign-out to maintain opt-in status, whilst ensuring the next + /// account used isn't associated with the previous one. + /// Note: **MUST** be called before stopping PostHog or the reset is ignored. + func reset() { + client.reset() +// monitoringClient.reset() + + MXLog.debug("Reset.") + ElementSettings.shared.isIdentifiedForAnalytics = false + + // Stop collecting crash logs +// MXLogger.logCrashes(false) + } + + /// Flushes the event queue in the analytics client, uploading all pending events. + /// Normally events are sent in batches. Call this method when you need an event + /// to be sent immediately. + func forceUpload() { + client.flush() + } + + // MARK: - Private + + /// Identify (pseudonymously) any future events with the ID from the analytics account data settings. + /// - Parameter settings: The settings to use for identification. The ID must be set *before* calling this method. + private func identify(with settings: AnalyticsSettings) { + guard let id = settings.id else { + MXLog.error("identify(with:) called before an ID has been generated.") + return + } + + client.identify(id: id) + MXLog.debug("Identified.") + ElementSettings.shared.isIdentifiedForAnalytics = true + } + + /// Capture an event in the `client`. + /// - Parameter event: The event to capture. + private func capture(event: AnalyticsEventProtocol) { + client.capture(event) + } +} + +// MARK: - Public tracking methods + +// The following methods are exposed for compatibility with Objective-C as +// the `capture` method and the generated events cannot be bridged from Swift. +extension Analytics { } + +// MARK: - MXAnalyticsDelegate + +// extension Analytics: MXAnalyticsDelegate { +// } diff --git a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift new file mode 100644 index 000000000..f601a1095 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift @@ -0,0 +1,56 @@ +// +// 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 AnalyticsEvents + +/// A protocol representing an analytics client. +protocol AnalyticsClientProtocol { + /// Whether the analytics client is currently reporting data or ignoring it. + var isRunning: Bool { get } + + /// Starts the analytics client reporting data. + func start() + + /// Associate the client with an ID. This is persisted until `reset` is called. + /// - Parameter id: The ID to associate with the user. + func identify(id: String) + + /// Reset all stored properties and any event queues on the client. Note that + /// the client will remain active, but in a fresh unidentified state. + func reset() + + /// Stop the analytics client reporting data. + func stop() + + /// Send any queued events immediately. + func flush() + + /// Capture the supplied analytics event. + /// - Parameter event: The event to capture. + func capture(_ event: AnalyticsEventProtocol) + + /// Capture the supplied analytics screen event. + /// - Parameter event: The screen event to capture. + 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) +} diff --git a/ElementX/Sources/Services/Analytics/AnalyticsConfiguration.swift b/ElementX/Sources/Services/Analytics/AnalyticsConfiguration.swift new file mode 100644 index 000000000..96a321ff1 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/AnalyticsConfiguration.swift @@ -0,0 +1,29 @@ +// +// Copyright 2022 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 + +/// A type that represents how to set up the analytics module in the app. +struct AnalyticsConfiguration { + /// Whether or not analytics should be enabled. + let isEnabled: Bool + /// The host to use for PostHog analytics. + let host: String + /// The public key for submitting analytics. + let apiKey: String + /// The URL to open with more information about analytics terms. + let termsURL: URL +} diff --git a/ElementX/Sources/Services/Analytics/AnalyticsService.swift b/ElementX/Sources/Services/Analytics/AnalyticsService.swift new file mode 100644 index 000000000..745ec3fb0 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/AnalyticsService.swift @@ -0,0 +1,75 @@ +// +// 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 + +enum AnalyticsServiceError: Error { + /// The session supplied to the service does not have a state of `MXSessionStateRunning`. + case sessionIsNotRunning + /// The service failed to get or update the analytics settings event from the user's account data. + case accountDataFailure +} + +/// A service responsible for handling the `im.vector.analytics` event from the user's account data. +class AnalyticsService { + let session: UserSessionProtocol + + /// Creates an analytics service with the supplied session. + /// - Parameter session: The session to use when reading analytics settings from account data. + init(session: UserSessionProtocol) { + self.session = session + } + + /// The analytics settings for the current user. Calling this method will check whether the settings already + /// contain an `id` property and if not, will add one to the account data before calling the completion. + /// - Parameter completion: A completion handler that will be called when the request completes. + /// + /// The request will fail if the service's session does not have the `MXSessionStateRunning` state. + func settings() async -> Result { + // Only use the session if it is running otherwise we could wipe out an existing analytics ID. + fatalWithoutUnreachableCodeWarning() +// guard session.state == .running else { +// MXLog.warning("Aborting attempt to read analytics settings. The session may not be up-to-date.") +// return .failure(.sessionIsNotRunning) +// } + + let result: Result = await session.clientProxy.accountDataEvent(type: AnalyticsSettings.eventType) + switch result { + case .failure: + return .failure(.accountDataFailure) + case .success(let settings): + // The id has already be set so we are done here. + if let settings = settings, settings.id != nil { + return .success(settings) + } + + let newSettings = AnalyticsSettings.new(currentEvent: settings) + switch await session.clientProxy.setAccountData(content: newSettings, type: AnalyticsSettings.eventType) { + case .failure: + MXLog.warning("Failed to update analytics settings.") + return .failure(.accountDataFailure) + case .success: + MXLog.debug("Successfully updated analytics settings in account data.") + return .success(newSettings) + } + } + } + + /// Silences a warning on some intentionally unreachable code. + func fatalWithoutUnreachableCodeWarning() { + fatalError("Missing running state detection.") + } +} diff --git a/ElementX/Sources/Services/Analytics/AnalyticsSettings.swift b/ElementX/Sources/Services/Analytics/AnalyticsSettings.swift new file mode 100644 index 000000000..96305223c --- /dev/null +++ b/ElementX/Sources/Services/Analytics/AnalyticsSettings.swift @@ -0,0 +1,50 @@ +// +// 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 + +/// An analytics settings event from the user's account data. +struct AnalyticsSettings: Codable { + static let eventType = "im.vector.analytics" + + /// A randomly generated analytics token for this user. + /// This is suggested to be a UUID string. + let id: String? + + /// Whether the user has opted in on web or not. This is unused on iOS but necessary + /// to store here so that it's value is preserved when updating the account data if we + /// generated an ID on iOS. + /// + /// `true` if opted in on web, `false` if opted out on web and `nil` if the web prompt is not yet seen. + private let webOptIn: Bool? + + enum CodingKeys: String, CodingKey { + case id + case webOptIn = "pseudonymousAnalyticsOptIn" + } +} + +extension AnalyticsSettings { + /// Generates a new AnalyticsSettings value (inc an ID if necessary) based upon an + /// existing value. This is the only way the type should be created so as to avoid wiping + /// out the `webOptIn` value that the user may already have set. + /// + /// **Note:** Please don't pass a `nil` literal to this method. + static func new(currentEvent: AnalyticsSettings?) -> AnalyticsSettings { + AnalyticsSettings(id: currentEvent?.id ?? UUID().uuidString, + webOptIn: currentEvent?.webOptIn) + } +} diff --git a/ElementX/Sources/Services/Analytics/Helpers/JoinedRoomSize+MemberCount.swift b/ElementX/Sources/Services/Analytics/Helpers/JoinedRoomSize+MemberCount.swift new file mode 100644 index 000000000..074f1e2d1 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/Helpers/JoinedRoomSize+MemberCount.swift @@ -0,0 +1,36 @@ +// +// 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 AnalyticsEvents + +extension AnalyticsEvent.JoinedRoom.RoomSize { + init?(memberCount: UInt) { + switch memberCount { + case 2: + self = .Two + case 3...10: + self = .ThreeToTen + case 11...100: + self = .ElevenToOneHundred + case 101...1000: + self = .OneHundredAndOneToAThousand + case 1001...: + self = .MoreThanAThousand + default: + return nil + } + } +} diff --git a/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift b/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift new file mode 100644 index 000000000..7d251083d --- /dev/null +++ b/ElementX/Sources/Services/Analytics/Helpers/UserProperties+Element.swift @@ -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 AnalyticsEvents +import Foundation + +public extension AnalyticsEvent.UserProperties { + // Initializer for Element. Strips all Web properties. + init(ftueUseCaseSelection: FtueUseCaseSelection?, numFavouriteRooms: Int?, numSpaces: Int?, allChatsActiveFilter: AllChatsActiveFilter?) { + self.init(WebMetaSpaceFavouritesEnabled: nil, + WebMetaSpaceHomeAllRooms: nil, + WebMetaSpaceHomeEnabled: nil, + WebMetaSpaceOrphansEnabled: nil, + WebMetaSpacePeopleEnabled: nil, + allChatsActiveFilter: nil, + ftueUseCaseSelection: ftueUseCaseSelection, + numFavouriteRooms: numFavouriteRooms, + numSpaces: numSpaces) + } +} diff --git a/ElementX/Sources/Services/Analytics/PHGPostHogConfiguration.swift b/ElementX/Sources/Services/Analytics/PHGPostHogConfiguration.swift new file mode 100644 index 000000000..e1f4ed442 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/PHGPostHogConfiguration.swift @@ -0,0 +1,29 @@ +// +// 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 PostHog + +extension PHGPostHogConfiguration { + static var standard: PHGPostHogConfiguration? { + let analyticsConfiguration = BuildSettings.analyticsConfiguration + guard analyticsConfiguration.isEnabled else { return nil } + + let postHogConfiguration = PHGPostHogConfiguration(apiKey: analyticsConfiguration.apiKey, host: analyticsConfiguration.host) + postHogConfiguration.shouldSendDeviceID = false + + return postHogConfiguration + } +} diff --git a/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift new file mode 100644 index 000000000..1f2beb190 --- /dev/null +++ b/ElementX/Sources/Services/Analytics/PostHogAnalyticsClient.swift @@ -0,0 +1,104 @@ +// +// 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 AnalyticsEvents +import PostHog + +/// An analytics client that reports events to a PostHog server. +class PostHogAnalyticsClient: AnalyticsClientProtocol { + /// The PHGPostHog object used to report events. + 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 } + + func start() { + // Only start if analytics have been configured in BuildSettings + guard let configuration = PHGPostHogConfiguration.standard else { return } + + if postHog == nil { + postHog = PHGPostHog(configuration: configuration) + } + + postHog?.enable() + } + + func identify(id: String) { + 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() { + postHog?.reset() + pendingUserProperties = nil + } + + func stop() { + postHog?.disable() + + // As of PostHog 1.4.4, setting the client to nil here doesn't release + // it. Keep it around to avoid having multiple instances if the user re-enables + } + + func flush() { + postHog?.flush() + } + + func capture(_ event: AnalyticsEventProtocol) { + postHog?.capture(event.eventName, properties: attachUserProperties(to: event.properties)) + } + + func screen(_ event: AnalyticsScreenProtocol) { + 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, + numFavouriteRooms: userProperties.numFavouriteRooms ?? pendingUserProperties.numFavouriteRooms, + numSpaces: userProperties.numSpaces ?? pendingUserProperties.numSpaces, + allChatsActiveFilter: userProperties.allChatsActiveFilter ?? pendingUserProperties.allChatsActiveFilter) + } + + // 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 + } +} diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index aeea5a734..c358a9ef8 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -74,7 +74,7 @@ class ClientProxy: ClientProxyProtocol { } func loadUserDisplayName() async -> Result { - await Task.detached { () -> Result in + await Task.detached { do { let displayName = try self.client.displayName() return .success(displayName) @@ -86,7 +86,7 @@ class ClientProxy: ClientProxyProtocol { } func loadUserAvatarURLString() async -> Result { - await Task.detached { () -> Result in + await Task.detached { do { let avatarURL = try self.client.avatarUrl() return .success(avatarURL) @@ -97,6 +97,14 @@ class ClientProxy: ClientProxyProtocol { .value } + func accountDataEvent(type: String) async -> Result where Content: Decodable { + .failure(.failedRetrievingAccountData) + } + + func setAccountData(content: Content, type: String) async -> Result { + .failure(.failedSettingAccountData) + } + func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource { MatrixRustSDK.mediaSourceFromUrl(url: urlString) } diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index bc3e0c025..742954a18 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -26,6 +26,8 @@ enum ClientProxyCallback { enum ClientProxyError: Error { case failedRetrievingAvatarURL case failedRetrievingDisplayName + case failedRetrievingAccountData + case failedSettingAccountData case failedRetrievingSessionVerificationController case failedLoadingMedia } @@ -41,6 +43,10 @@ protocol ClientProxyProtocol { func loadUserAvatarURLString() async -> Result + func accountDataEvent(type: String) async -> Result + + func setAccountData(content: Content, type: String) async -> Result + func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource func loadMediaContentForSource(_ source: MatrixRustSDK.MediaSource) throws -> Data diff --git a/ElementX/Sources/Services/Client/MockClientProxy.swift b/ElementX/Sources/Services/Client/MockClientProxy.swift index ad7acc685..085707070 100644 --- a/ElementX/Sources/Services/Client/MockClientProxy.swift +++ b/ElementX/Sources/Services/Client/MockClientProxy.swift @@ -32,6 +32,14 @@ struct MockClientProxy: ClientProxyProtocol { .failure(.failedRetrievingAvatarURL) } + func accountDataEvent(type: String) async -> Result where Content: Decodable { + .failure(.failedRetrievingAccountData) + } + + func setAccountData(content: Content, type: String) async -> Result where Content: Encodable { + .failure(.failedSettingAccountData) + } + func mediaSourceForURLString(_ urlString: String) -> MatrixRustSDK.MediaSource { MatrixRustSDK.mediaSourceFromUrl(url: urlString) } diff --git a/ElementX/Sources/Services/Session/MockUserSession.swift b/ElementX/Sources/Services/Session/MockUserSession.swift index aaa8cf013..3c56f7bb7 100644 --- a/ElementX/Sources/Services/Session/MockUserSession.swift +++ b/ElementX/Sources/Services/Session/MockUserSession.swift @@ -20,7 +20,7 @@ struct MockUserSession: UserSessionProtocol { let callbacks = PassthroughSubject() let sessionVerificationController: SessionVerificationControllerProxyProtocol? = nil - let userID = "@mock:usersession.com" + var userID: String { clientProxy.userIdentifier } let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol } diff --git a/ElementX/SupportingFiles/target.yml b/ElementX/SupportingFiles/target.yml index 4d5e6a891..c3c44b5bc 100644 --- a/ElementX/SupportingFiles/target.yml +++ b/ElementX/SupportingFiles/target.yml @@ -95,11 +95,13 @@ targets: dependencies: - package: MatrixRustSDK - package: DesignKit + - package: AnalyticsEvents - package: AppAuth - package: DTCoreText - package: KeychainAccess - package: Kingfisher - package: Introspect + - package: PostHog - package: SwiftyBeaver - package: SwiftState - package: GZIP diff --git a/UnitTests/Sources/AnalyticsTests.swift b/UnitTests/Sources/AnalyticsTests.swift new file mode 100644 index 000000000..9b8331438 --- /dev/null +++ b/UnitTests/Sources/AnalyticsTests.swift @@ -0,0 +1,136 @@ +// +// Copyright 2022 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 AnalyticsEvents +@testable import ElementX +import XCTest + +class AnalyticsTests: XCTestCase { + func testAnalyticsPromptNewUser() { + // Given a fresh install of the app (without PostHog analytics having been set). + ElementSettings.store.removeObject(forKey: ElementSettings.UserDefaultsKeys.enableAnalytics.rawValue) + + // When the user is prompted for analytics. + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + + // Then the prompt should be shown. + XCTAssertTrue(showPrompt, "A prompt should be shown for a new user.") + } + + func testAnalyticsPromptUserDeclinedPostHog() { + // Given an existing install of the app where the user previously declined PostHog + ElementSettings.shared.enableAnalytics = false + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + + // Then no prompt should be shown. + XCTAssertFalse(showPrompt, "A prompt should not be shown any more.") + } + + func testAnalyticsPromptUserAcceptedPostHog() { + // Given an existing install of the app where the user previously accepted PostHog + ElementSettings.shared.enableAnalytics = true + + // When the user is prompted for analytics + let showPrompt = Analytics.shared.shouldShowAnalyticsPrompt + + // Then no prompt should be shown. + 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, + numFavouriteRooms: 4, + numSpaces: 5, + allChatsActiveFilter: nil)) + + // 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?.numFavouriteRooms, 4, "The number of favorite rooms 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, + numFavouriteRooms: nil, + numSpaces: nil, + allChatsActiveFilter: nil)) + + XCTAssertNotNil(client.pendingUserProperties, "The user properties should be cached.") + XCTAssertEqual(client.pendingUserProperties?.ftueUseCaseSelection, .PersonalMessaging, "The use case selection should match.") + XCTAssertNil(client.pendingUserProperties?.numFavouriteRooms, "The number of favorite rooms should not be set.") + XCTAssertNil(client.pendingUserProperties?.numSpaces, "The number of spaces should not be set.") + + // When updating the number of spaced + client.updateUserProperties(AnalyticsEvent.UserProperties(ftueUseCaseSelection: nil, + numFavouriteRooms: 4, + numSpaces: 5, + allChatsActiveFilter: nil)) + + // 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?.numFavouriteRooms, 4, "The number of favorite rooms should have been updated.") + 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, + numFavouriteRooms: nil, + numSpaces: nil, + allChatsActiveFilter: 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.MobileScreen(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, + numFavouriteRooms: nil, + numSpaces: nil, + allChatsActiveFilter: 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.") + } +} diff --git a/UnitTests/SupportingFiles/target.yml b/UnitTests/SupportingFiles/target.yml index 34a6b39f1..b6d9a875c 100644 --- a/UnitTests/SupportingFiles/target.yml +++ b/UnitTests/SupportingFiles/target.yml @@ -49,5 +49,3 @@ targets: - path: ../SupportingFiles - path: ../../Tools/Scripts/Templates/SimpleScreenExample/Tests/Unit - path: ../Resources - - path: ../../ElementX/Sources/BuildSettings.swift - - path: ../../ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift diff --git a/changelog.d/106.wip b/changelog.d/106.wip new file mode 100644 index 000000000..d1bc84048 --- /dev/null +++ b/changelog.d/106.wip @@ -0,0 +1 @@ +Begin adding the same Analytics used in Element iOS. diff --git a/project.yml b/project.yml index 52442dc63..7f07c1a20 100644 --- a/project.yml +++ b/project.yml @@ -37,6 +37,9 @@ packages: # path: ../matrix-rust-components-swift DesignKit: path: ./ + AnalyticsEvents: + url: https://github.com/matrix-org/matrix-analytics-events + branch: main AppAuth: url: https://github.com/openid/AppAuth-iOS majorVersion: 1.5.0 @@ -52,6 +55,9 @@ packages: Introspect: url: https://github.com/siteline/SwiftUI-Introspect majorVersion: 0.1.4 + PostHog: + url: https://github.com/PostHog/posthog-ios + majorVersion: 1.4.4 SwiftyBeaver: url: https://github.com/SwiftyBeaver/SwiftyBeaver majorVersion: 1.9.5