From 2ec3ae1630727cb57f999dc95472f29e5fd7ccf1 Mon Sep 17 00:00:00 2001 From: Doug Date: Tue, 5 Mar 2024 10:19:33 +0000 Subject: [PATCH 01/21] Prepare for new sprint --- Config/AppVersion.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index b1d7c33ed..4cf4cbfbe 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.11.8 -CURRENT_PROJECT_VERSION = 1.11.8 +MARKETING_VERSION = 1.11.9 +CURRENT_PROJECT_VERSION = 1.11.9 From a1d59b32b7eaba3253215ea9cf935b473ba37d74 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 6 Mar 2024 11:18:00 +0100 Subject: [PATCH 02/21] Increase decryption failure grace period --- Riot/Modules/Analytics/DecryptionFailureTracker.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m index 4a749b71a..86b847f22 100644 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.m +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.m @@ -19,11 +19,11 @@ // Call `checkFailures` every `CHECK_INTERVAL` -#define CHECK_INTERVAL 2 +#define CHECK_INTERVAL 10 // Give events a chance to be decrypted by waiting `GRACE_PERIOD` before counting // and reporting them as failures -#define GRACE_PERIOD 4 +#define GRACE_PERIOD 30 // E2E failures analytics category. NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; From 5a85651e6b4cf0afe8213f3020cbf527c5a39534 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 19 Mar 2024 10:01:19 +0200 Subject: [PATCH 03/21] Add .xcprivacy info files to the main app, the notification service extension and the broadcast upload one --- .../SupportingFiles/PrivacyInfo.xcprivacy | 33 +++++++++++++++++++ Riot/SupportingFiles/PrivacyInfo.xcprivacy | 33 +++++++++++++++++++ RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy | 33 +++++++++++++++++++ 3 files changed, 99 insertions(+) create mode 100644 BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy create mode 100644 Riot/SupportingFiles/PrivacyInfo.xcprivacy create mode 100644 RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy diff --git a/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy b/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..04e918224 --- /dev/null +++ b/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy @@ -0,0 +1,33 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + 7D9E.1 + + + + + diff --git a/Riot/SupportingFiles/PrivacyInfo.xcprivacy b/Riot/SupportingFiles/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..04e918224 --- /dev/null +++ b/Riot/SupportingFiles/PrivacyInfo.xcprivacy @@ -0,0 +1,33 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + 7D9E.1 + + + + + diff --git a/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy b/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy new file mode 100644 index 000000000..04e918224 --- /dev/null +++ b/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy @@ -0,0 +1,33 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPITypeReasons + + 7D9E.1 + + + + + From 92be17d80548c2adf2da7f246b0389502eef2f18 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 19 Mar 2024 12:13:16 +0200 Subject: [PATCH 04/21] Add privacy accessed API declaration for system boot time --- .../SupportingFiles/PrivacyInfo.xcprivacy | 8 ++++++++ Riot/SupportingFiles/PrivacyInfo.xcprivacy | 8 ++++++++ RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy | 8 ++++++++ fastlane/Fastfile | 2 +- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy b/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy index 04e918224..500ae9aff 100644 --- a/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy +++ b/BroadcastUploadExtension/SupportingFiles/PrivacyInfo.xcprivacy @@ -28,6 +28,14 @@ 7D9E.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 3D61.1 + + diff --git a/Riot/SupportingFiles/PrivacyInfo.xcprivacy b/Riot/SupportingFiles/PrivacyInfo.xcprivacy index 04e918224..500ae9aff 100644 --- a/Riot/SupportingFiles/PrivacyInfo.xcprivacy +++ b/Riot/SupportingFiles/PrivacyInfo.xcprivacy @@ -28,6 +28,14 @@ 7D9E.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 3D61.1 + + diff --git a/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy b/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy index 04e918224..500ae9aff 100644 --- a/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy +++ b/RiotNSE/SupportingFiles/PrivacyInfo.xcprivacy @@ -28,6 +28,14 @@ 7D9E.1 + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 3D61.1 + + diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 90d3ff0d7..aea874310 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -21,7 +21,7 @@ platform :ios do before_all do # Ensure used Xcode version - xcversion(version: "14.2") + xcversion(version: "15.2") end #### Public #### From 4d3d82fa337fdbf3efa45d5a9a8908401ae3508e Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 19 Mar 2024 12:46:29 +0200 Subject: [PATCH 05/21] Switch the github actions to the macos-14 runner --- .github/workflows/ci-build.yml | 2 +- .github/workflows/ci-tests.yml | 2 +- .github/workflows/ci-ui-tests.yml | 2 +- .github/workflows/release-alpha.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e11eca47a..6cac438d5 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -15,7 +15,7 @@ env: jobs: build: name: Build - runs-on: macos-12 + runs-on: macos-14 # Concurrency group not needed as this workflow only runs on develop which we always want to test. diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index d5a9d105d..f78a5aba9 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -16,7 +16,7 @@ env: jobs: tests: name: Tests - runs-on: macos-12 + runs-on: macos-14 concurrency: # When running on develop, use the sha to allow all runs of this workflow to run concurrently. diff --git a/.github/workflows/ci-ui-tests.yml b/.github/workflows/ci-ui-tests.yml index 39c90d509..b13272947 100644 --- a/.github/workflows/ci-ui-tests.yml +++ b/.github/workflows/ci-ui-tests.yml @@ -12,7 +12,7 @@ env: jobs: tests: name: UI Tests - runs-on: macos-12 + runs-on: macos-14 concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. diff --git a/.github/workflows/release-alpha.yml b/.github/workflows/release-alpha.yml index 4f6a1b3e3..e610628b4 100644 --- a/.github/workflows/release-alpha.yml +++ b/.github/workflows/release-alpha.yml @@ -17,7 +17,7 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build') name: Release - runs-on: macos-12 + runs-on: macos-14 concurrency: # Only allow a single run of this workflow on each branch, automatically cancelling older runs. From c1036cb7bb795643305402a24d225bee75fbe7e9 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 19 Mar 2024 13:05:57 +0200 Subject: [PATCH 06/21] Use the latest version of the sonarcloud github action --- .github/workflows/sonarcloud.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index f46144c76..d902ca136 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -18,7 +18,7 @@ jobs: - name: Analyze with SonarCloud # You can pin the exact commit or the version. - uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 + uses: SonarSource/sonarcloud-github-action@master env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate the token on Sonarcloud.io, add it to the secrets of this repo From b1c6ac435d9462406c8dbc36b5ed5e5559fa7cb1 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 19 Mar 2024 13:29:27 +0200 Subject: [PATCH 07/21] Remove inherently broken tests --- .../NotificationSettingsViewModelTests.swift | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift index 95b5e08fa..b241dcfce 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Test/Unit/NotificationSettingsViewModelTests.swift @@ -41,37 +41,6 @@ final class NotificationSettingsViewModelTests: XCTestCase { XCTAssertEqual(viewModel.viewState.selectionState[.encrypted], false) } - func testUpdateOneToOneRuleAlsoUpdatesPollRules() async { - setupWithPollRules() - - await viewModel.update(ruleID: .oneToOneRoom, isChecked: false) - - XCTAssertEqual(viewModel.viewState.selectionState.count, 8) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], false) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], false) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], false) - - // unrelated poll rules stay the same - XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], true) - XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], true) - XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], true) - } - - func testUpdateMessageRuleAlsoUpdatesPollRules() async { - setupWithPollRules() - - await viewModel.update(ruleID: .allOtherMessages, isChecked: false) - XCTAssertEqual(viewModel.viewState.selectionState.count, 8) - XCTAssertEqual(viewModel.viewState.selectionState[.allOtherMessages], false) - XCTAssertEqual(viewModel.viewState.selectionState[.pollStart], false) - XCTAssertEqual(viewModel.viewState.selectionState[.pollEnd], false) - - // unrelated poll rules stay the same - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOneRoom], true) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollStart], true) - XCTAssertEqual(viewModel.viewState.selectionState[.oneToOnePollEnd], true) - } - func testMismatchingRulesAreHandled() async { setupWithPollRules() From 608f760be83f24de87afbcd37b8080ec8ba574f7 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Tue, 19 Mar 2024 15:15:11 +0200 Subject: [PATCH 08/21] Switch UI tests to iPhone 15, fix the broken ones --- Gemfile.lock | 92 ++++++++++--------- Podfile.lock | 2 +- .../Modules/Common/Test/UI/XCUIElement.swift | 28 ++++++ .../Test/UI/UserOtherSessionsUITests.swift | 10 +- .../View/UserOtherSessionsToolbar.swift | 48 +++++----- .../Test/UI/UserSessionOverviewUITests.swift | 2 +- fastlane/Fastfile | 2 +- 7 files changed, 106 insertions(+), 78 deletions(-) create mode 100644 RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift diff --git a/Gemfile.lock b/Gemfile.lock index 02bb36363..519e1f087 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,9 +7,11 @@ GIT GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (7.1.2) + activesupport (7.1.3.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -19,32 +21,32 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.859.0) - aws-sdk-core (3.188.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (1.899.0) + aws-sdk-core (3.191.4) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.73.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.140.0) - aws-sdk-core (~> 3, >= 3.188.0) + aws-sdk-s3 (1.146.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.7.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) - bigdecimal (3.1.4) + bigdecimal (3.1.7) claide (1.1.0) clamp (1.3.2) cocoapods (1.14.3) @@ -88,20 +90,19 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.2.3) connection_pool (2.4.1) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) - drb (2.2.0) - ruby2_keywords + drb (2.2.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.104.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -130,8 +131,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.217.0) + fastimage (2.3.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -150,6 +151,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -158,7 +160,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -172,7 +174,7 @@ GEM xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) fastlane-plugin-brew (0.1.1) - fastlane-plugin-sentry (1.16.0) + fastlane-plugin-sentry (1.20.0) os (~> 1.1, >= 1.1.4) fastlane-plugin-versioning (0.5.2) fastlane-plugin-xcodegen (1.1.0) @@ -181,9 +183,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.53.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -191,24 +193,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -222,29 +223,31 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.4) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.1) + json (2.7.1) + jwt (2.8.1) + base64 mini_magick (4.12.0) mini_mime (1.1.5) mini_portile2 (2.8.5) - minitest (5.20.0) + minitest (5.22.3) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.0) mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) - nokogiri (1.15.5) + nkf (0.2.0) + nokogiri (1.15.6) mini_portile2 (~> 2.8.2) racc (~> 1.4) - optparse (0.1.1) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) + plist (3.7.1) public_suffix (4.0.7) racc (1.7.3) rake (13.1.0) @@ -259,7 +262,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.18.0) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -278,7 +281,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) typhoeus (1.4.1) @@ -287,12 +290,11 @@ GEM concurrent-ruby (~> 1.0) uber (0.1.0) unicode-display_width (2.5.0) - webrick (1.8.1) word_wrap (1.0.0) xcode-install (2.8.1) claide (>= 0.9.1) fastlane (>= 2.1.0, < 3.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/Podfile.lock b/Podfile.lock index abe637e53..8ca318a66 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -210,4 +210,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: c87b532985dd755b373732f841e3bcfe616f4e4f -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift b/RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift new file mode 100644 index 000000000..db41a603e --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Test/UI/XCUIElement.swift @@ -0,0 +1,28 @@ +// +// Copyright 2024 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 XCTest + +extension XCUIElement { + func forceTap() { + if isHittable { + tap() + } else { + let coordinate: XCUICoordinate = coordinate(withNormalizedOffset: .init(dx: 0.5, dy: 0.5)) + coordinate.tap() + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift index 71a651659..3fcf2a8cc 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift @@ -56,7 +56,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenOtherSessionsMoreMenuButtonSelected_moreMenuIsCorrect() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() XCTAssertTrue(app.buttons["Select sessions"].exists) XCTAssertTrue(app.buttons["Sign out of 6 sessions"].exists) } @@ -64,7 +64,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenOtherSessionsSelectSessionsSelected_navBarContainsCorrectButtons() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() let signOutButton = app.buttons["Sign out"] XCTAssertTrue(signOutButton.exists) @@ -76,7 +76,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenOtherSessionsSelectAllSelected_navBarContainsCorrectButtons() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() app.buttons["Select All"].tap() XCTAssertTrue(app.buttons["Deselect All"].exists) @@ -85,7 +85,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenAllOtherSessionsAreSelected_navBarContainsCorrectButtons() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() for i in 0...MockUserOtherSessionsScreenState.all.allSessions().count - 1 { app.buttons["UserSessionListItem_\(i)"].tap() @@ -95,7 +95,7 @@ class UserOtherSessionsUITests: MockScreenTestCase { func test_whenChangingSessionSelection_signOutButtonChangesItState() { app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title) - app.buttons["More"].tap() + app.buttons["More"].forceTap() app.buttons["Select sessions"].tap() let signOutButton = app.buttons["Sign out"] XCTAssertTrue(signOutButton.exists) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift index ba844904e..39970240d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsToolbar.swift @@ -80,35 +80,33 @@ struct UserOtherSessionsToolbar: ToolbarContent { } private func optionsMenu() -> some View { - Button { } label: { - Menu { - if showDeviceLogout { // As you can only sign out the selected sessions, we don't allow selection when you're unable to sign out devices. - Button { - isEditModeEnabled = true - } label: { - Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle") - } - .disabled(sessionCount == 0) - } - + Menu { + if showDeviceLogout { // As you can only sign out the selected sessions, we don't allow selection when you're unable to sign out devices. Button { - isShowLocationEnabled.toggle() + isEditModeEnabled = true } label: { - Label(showLocationInfo: isShowLocationEnabled) + Label(VectorL10n.userOtherSessionMenuSelectSessions, systemImage: "checkmark.circle") } - - if sessionCount > 0, showDeviceLogout { - DestructiveButton { - onSignOut() - } label: { - Label(VectorL10n.userOtherSessionMenuSignOutSessions(String(sessionCount)), systemImage: "rectangle.portrait.and.arrow.forward.fill") - } - } - } label: { - Image(systemName: "ellipsis") - .padding(.horizontal, 4) - .padding(.vertical, 12) + .disabled(sessionCount == 0) } + + Button { + isShowLocationEnabled.toggle() + } label: { + Label(showLocationInfo: isShowLocationEnabled) + } + + if sessionCount > 0, showDeviceLogout { + DestructiveButton { + onSignOut() + } label: { + Label(VectorL10n.userOtherSessionMenuSignOutSessions(String(sessionCount)), systemImage: "rectangle.portrait.and.arrow.forward.fill") + } + } + } label: { + Image(systemName: "ellipsis") + .padding(.horizontal, 4) + .padding(.vertical, 12) } .accessibilityIdentifier("More") } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift index 643c28cbe..93938057c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/UI/UserSessionOverviewUITests.swift @@ -67,7 +67,7 @@ class UserSessionOverviewUITests: MockScreenTestCase { let navTitle = VectorL10n.userSessionOverviewSessionTitle let barButton = app.navigationBars[navTitle].buttons["Menu"] XCTAssertTrue(barButton.exists) - barButton.tap() + barButton.forceTap() XCTAssertTrue(app.buttons[VectorL10n.signOut].exists) XCTAssertTrue(app.buttons[VectorL10n.manageSessionRename].exists) } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index aea874310..51e23fe9d 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -197,7 +197,7 @@ platform :ios do run_tests( workspace: "Riot.xcworkspace", scheme: "RiotSwiftUITests", - device: "iPhone 14", + device: "iPhone 15", code_coverage: true, # Test result configuration result_bundle: true, From b6b765242ee30553d3ba600c6b590ad52388834a Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Mar 2024 15:13:05 +0100 Subject: [PATCH 09/21] Bump analytics event to v0.15.0 --- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- Riot/Modules/Analytics/Analytics.swift | 14 ++++++++++++-- project.yml | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2f19e2ddc..511ea29a7 100644 --- a/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-analytics-events", "state" : { - "revision" : "2f5fa5f1e2f6c6ae1a47c33d953a3ce289167eb0", - "version" : "0.5.0" + "revision" : "44d5a0e898a71f8abbbe12afe9d73e82d370a9a1", + "version" : "0.15.0" } }, { diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 1a30841b9..c04a291de 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -275,8 +275,16 @@ extension Analytics { let event = AnalyticsEvent.Error( context: context, cryptoModule: .Rust, + cryptoSDK: AnalyticsEvent.Error.CryptoSDK.Rust, domain: .E2EE, - name: reason.errorName + // XXX not yet supported. + eventLocalAgeMillis: nil, + isFederated: nil, + isMatrixDotOrg: nil, + name: reason.errorName, + timeToDecryptMillis: nil, + userTrustsOwnIdentity: nil, + wasVisibleToUser: nil ) capture(event: event) } @@ -355,7 +363,8 @@ extension Analytics: MXAnalyticsDelegate { func trackCallError(with reason: __MXCallHangupReason, video isVideo: Bool, numberOfParticipants: Int, incoming isIncoming: Bool) { let callEvent = AnalyticsEvent.CallError(isVideo: isVideo, numParticipants: numberOfParticipants, placed: !isIncoming) - let event = AnalyticsEvent.Error(context: nil, cryptoModule: nil, domain: .VOIP, name: reason.errorName) + let event = AnalyticsEvent.Error(context: nil, cryptoModule: nil, cryptoSDK: nil, domain: .VOIP, eventLocalAgeMillis: nil, + isFederated: nil, isMatrixDotOrg: nil, name: reason.errorName, timeToDecryptMillis: nil, userTrustsOwnIdentity: nil, wasVisibleToUser: nil) capture(event: callEvent) capture(event: event) } @@ -386,6 +395,7 @@ extension Analytics: MXAnalyticsDelegate { let event = AnalyticsEvent.Composer(inThread: inThread, isEditing: isEditing, isReply: isReply, + messageType: AnalyticsEvent.Composer.MessageType.Text, startsThread: startsThread) capture(event: event) } diff --git a/project.yml b/project.yml index 3d410864a..72c0ff8d4 100644 --- a/project.yml +++ b/project.yml @@ -45,7 +45,7 @@ include: packages: AnalyticsEvents: url: https://github.com/matrix-org/matrix-analytics-events - exactVersion: 0.5.0 + exactVersion: 0.15.0 Mapbox: url: https://github.com/maplibre/maplibre-gl-native-distribution minVersion: 5.12.2 From 6945e3fdd81ac6306c0bebe926e78b6baaffa300 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 27 Mar 2024 15:16:10 +0100 Subject: [PATCH 10/21] Update changelog --- changelog.d/7768.misc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelog.d/7768.misc diff --git a/changelog.d/7768.misc b/changelog.d/7768.misc new file mode 100644 index 000000000..042654b87 --- /dev/null +++ b/changelog.d/7768.misc @@ -0,0 +1,2 @@ + +Update matrix-analytics-events to version 0.15.0 From 31a2b123abf40205fbd75bc1af387ef8383ed82a Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 28 Mar 2024 21:52:06 +0100 Subject: [PATCH 11/21] code review --- Riot/Modules/Analytics/Analytics.swift | 4 ++-- changelog.d/7768.misc | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index c04a291de..123b09cd5 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -275,7 +275,7 @@ extension Analytics { let event = AnalyticsEvent.Error( context: context, cryptoModule: .Rust, - cryptoSDK: AnalyticsEvent.Error.CryptoSDK.Rust, + cryptoSDK: .Rust, domain: .E2EE, // XXX not yet supported. eventLocalAgeMillis: nil, @@ -395,7 +395,7 @@ extension Analytics: MXAnalyticsDelegate { let event = AnalyticsEvent.Composer(inThread: inThread, isEditing: isEditing, isReply: isReply, - messageType: AnalyticsEvent.Composer.MessageType.Text, + messageType: .Text, startsThread: startsThread) capture(event: event) } diff --git a/changelog.d/7768.misc b/changelog.d/7768.misc index 042654b87..053b12b37 100644 --- a/changelog.d/7768.misc +++ b/changelog.d/7768.misc @@ -1,2 +1 @@ - Update matrix-analytics-events to version 0.15.0 From 09af055fa6a4eb5aff0a183e19c836c1f24ba773 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 28 Mar 2024 22:38:57 +0100 Subject: [PATCH 12/21] rename changelog --- changelog.d/{7768.misc => pr-7768.misc} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{7768.misc => pr-7768.misc} (100%) diff --git a/changelog.d/7768.misc b/changelog.d/pr-7768.misc similarity index 100% rename from changelog.d/7768.misc rename to changelog.d/pr-7768.misc From 95fd61f1016d28996c74ac65a621749f118145ec Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 29 Mar 2024 11:03:28 +0100 Subject: [PATCH 13/21] convert DecryptionFailureTracker to swift + tests --- Riot/Modules/Analytics/Analytics.swift | 55 +++-- .../Modules/Analytics/DecryptionFailure.swift | 5 +- .../Analytics/DecryptionFailureTracker.h | 55 ----- .../Analytics/DecryptionFailureTracker.m | 174 ---------------- .../Analytics/DecryptionFailureTracker.swift | 138 ++++++++++++ Riot/Modules/Application/LegacyAppDelegate.m | 1 - Riot/Utils/EventFormatter.m | 1 - RiotTests/DecryptionFailureTrackerTests.swift | 196 ++++++++++++++++++ RiotTests/FakeUtils.swift | 109 ++++++++++ 9 files changed, 479 insertions(+), 255 deletions(-) delete mode 100644 Riot/Modules/Analytics/DecryptionFailureTracker.h delete mode 100644 Riot/Modules/Analytics/DecryptionFailureTracker.m create mode 100644 Riot/Modules/Analytics/DecryptionFailureTracker.swift create mode 100644 RiotTests/DecryptionFailureTrackerTests.swift create mode 100644 RiotTests/FakeUtils.swift diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 123b09cd5..849efd0b8 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -213,6 +213,38 @@ import AnalyticsEvents } } +@objc +protocol E2EAnalytics { + func trackE2EEError(_ reason: DecryptionFailureReason, context: String) +} + + +@objc extension Analytics: E2EAnalytics { + + /// Track an E2EE error that occurred + /// - Parameters: + /// - reason: The error that occurred. + /// - context: Additional context of the error that occured + func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { + let event = AnalyticsEvent.Error( + context: context, + cryptoModule: .Rust, + cryptoSDK: AnalyticsEvent.Error.CryptoSDK.Rust, + domain: .E2EE, + // XXX not yet supported. + eventLocalAgeMillis: nil, + isFederated: nil, + isMatrixDotOrg: nil, + name: reason.errorName, + timeToDecryptMillis: nil, + userTrustsOwnIdentity: nil, + wasVisibleToUser: nil + ) + capture(event: 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. @@ -266,28 +298,7 @@ extension Analytics { func trackInteraction(_ uiElement: AnalyticsUIElement) { trackInteraction(uiElement, interactionType: .Touch, index: nil) } - - /// Track an E2EE error that occurred - /// - Parameters: - /// - reason: The error that occurred. - /// - context: Additional context of the error that occured - func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { - let event = AnalyticsEvent.Error( - context: context, - cryptoModule: .Rust, - cryptoSDK: .Rust, - domain: .E2EE, - // XXX not yet supported. - eventLocalAgeMillis: nil, - isFederated: nil, - isMatrixDotOrg: nil, - name: reason.errorName, - timeToDecryptMillis: nil, - userTrustsOwnIdentity: nil, - wasVisibleToUser: nil - ) - capture(event: event) - } + /// Track when a user becomes unauthenticated without pressing the `sign out` button. /// - Parameters: diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index 1c991db88..b5804d258 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -38,15 +38,16 @@ import AnalyticsEvents /// The id of the event that was unabled to decrypt. let failedEventId: String /// The time the failure has been reported. - let ts: TimeInterval = Date().timeIntervalSince1970 + let ts: TimeInterval /// Decryption failure reason. let reason: DecryptionFailureReason /// Additional context of failure let context: String - init(failedEventId: String, reason: DecryptionFailureReason, context: String) { + init(failedEventId: String, reason: DecryptionFailureReason, context: String, ts: TimeInterval) { self.failedEventId = failedEventId self.reason = reason self.context = context + self.ts = ts } } diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.h b/Riot/Modules/Analytics/DecryptionFailureTracker.h deleted file mode 100644 index b8f9ca467..000000000 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright 2018 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 - -@class DecryptionFailureTracker; - -@class Analytics; -@import MatrixSDK; - -@interface DecryptionFailureTracker : NSObject - -/** - Returns the shared tracker. - - @return the shared tracker. - */ -+ (instancetype)sharedInstance; - -/** - The delegate object to receive analytics events. - */ -@property (nonatomic, weak) Analytics *delegate; - -/** - Report an event unable to decrypt. - - This error can be momentary. The DecryptionFailureTracker will check if it gets - fixed. Else, it will generate a failure (@see `trackFailures`). - - @param event the event. - @param roomState the room state when the event was received. - @param userId my user id. - */ -- (void)reportUnableToDecryptErrorForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState myUser:(NSString*)userId; - -/** - Flush current data. - */ -- (void)dispatch; - -@end diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m deleted file mode 100644 index 4a749b71a..000000000 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.m +++ /dev/null @@ -1,174 +0,0 @@ -/* - Copyright 2018 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 "DecryptionFailureTracker.h" -#import "GeneratedInterface-Swift.h" - - -// Call `checkFailures` every `CHECK_INTERVAL` -#define CHECK_INTERVAL 2 - -// Give events a chance to be decrypted by waiting `GRACE_PERIOD` before counting -// and reporting them as failures -#define GRACE_PERIOD 4 - -// E2E failures analytics category. -NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; - -@interface DecryptionFailureTracker() -{ - // Reported failures - // Every `CHECK_INTERVAL`, this list is checked for failures that happened - // more than`GRACE_PERIOD` ago. Those that did are reported to the delegate. - NSMutableDictionary *reportedFailures; - - // Event ids of failures that were tracked previously - NSMutableSet *trackedEvents; - - // Timer for periodic check - NSTimer *checkFailuresTimer; -} -@end - -@implementation DecryptionFailureTracker - -+ (instancetype)sharedInstance -{ - static DecryptionFailureTracker *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[DecryptionFailureTracker alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init -{ - self = [super init]; - if (self) - { - reportedFailures = [NSMutableDictionary dictionary]; - trackedEvents = [NSMutableSet set]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidDecrypt:) name:kMXEventDidDecryptNotification object:nil]; - - checkFailuresTimer = [NSTimer scheduledTimerWithTimeInterval:CHECK_INTERVAL - target:self - selector:@selector(checkFailures) - userInfo:nil - repeats:YES]; - } - return self; -} - -- (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState myUser:(NSString *)userId -{ - if (reportedFailures[event.eventId] || [trackedEvents containsObject:event.eventId]) - { - return; - } - - // Filter out "expected" UTDs - // We cannot decrypt messages sent before the user joined the room - MXRoomMember *myUser = [roomState.members memberWithUserId:userId]; - if (!myUser || myUser.membership != MXMembershipJoin) - { - return; - } - - NSString *failedEventId = event.eventId; - DecryptionFailureReason reason; - - // Categorise the error - switch (event.decryptionError.code) - { - case MXDecryptingErrorUnknownInboundSessionIdCode: - reason = DecryptionFailureReasonOlmKeysNotSent; - break; - - case MXDecryptingErrorOlmCode: - reason = DecryptionFailureReasonOlmIndexError; - break; - - default: - // All other error codes will be tracked as `OlmUnspecifiedError` and will include `context` containing - // the actual error code and localized description - reason = DecryptionFailureReasonUnspecified; - break; - } - - NSString *context = [NSString stringWithFormat:@"code: %ld, description: %@", event.decryptionError.code, event.decryptionError.localizedDescription]; - reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId - reason:reason - context:context]; -} - -- (void)dispatch -{ - [self checkFailures]; -} - -#pragma mark - Private methods - -/** - Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be - tracked. - */ -- (void)checkFailures -{ - if (!_delegate) - { - return; - } - - NSTimeInterval tsNow = [NSDate date].timeIntervalSince1970; - - NSMutableArray *failuresToTrack = [NSMutableArray array]; - - for (DecryptionFailure *reportedFailure in reportedFailures.allValues) - { - if (reportedFailure.ts < tsNow - GRACE_PERIOD) - { - [failuresToTrack addObject:reportedFailure]; - [reportedFailures removeObjectForKey:reportedFailure.failedEventId]; - [trackedEvents addObject:reportedFailure.failedEventId]; - } - } - - if (failuresToTrack.count) - { - // Sort failures by error reason - NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; - for (DecryptionFailure *failure in failuresToTrack) - { - failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); - [self.delegate trackE2EEError:failure.reason context:failure.context]; - } - - MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts); - } -} - -- (void)eventDidDecrypt:(NSNotification *)notif -{ - // Could be an event in the reportedFailures, remove it - MXEvent *event = notif.object; - [reportedFailures removeObjectForKey:event.eventId]; -} - -@end diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.swift b/Riot/Modules/Analytics/DecryptionFailureTracker.swift new file mode 100644 index 000000000..f33f2b7c5 --- /dev/null +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.swift @@ -0,0 +1,138 @@ +// +// Copyright 2024 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 + + +// Protocol to get the current time. Used for easy testing +protocol TimeProvider { + func nowTs() -> TimeInterval +} + +class DefaultTimeProvider: TimeProvider { + + func nowTs() -> TimeInterval { + return Date.now.timeIntervalSince1970 + } + +} + + +@objc +class DecryptionFailureTracker: NSObject { + + let GRACE_PERIOD: TimeInterval = 4 + // Call `checkFailures` every `CHECK_INTERVAL` + let CHECK_INTERVAL: TimeInterval = 2 + + @objc weak var delegate: E2EAnalytics? + + // Reported failures + var reportedFailures = [String /* eventId */: DecryptionFailure]() + + // Event ids of failures that were tracked previously + var trackedEvents = Set() + + var checkFailuresTimer: Timer? + + @objc static let sharedInstance = DecryptionFailureTracker() + + var timeProvider: TimeProvider = DefaultTimeProvider() + + override init() { + super.init() + + NotificationCenter.default.addObserver(self, + selector: #selector(eventDidDecrypt(_:)), + name: .mxEventDidDecrypt, + object: nil) + + Timer.scheduledTimer(withTimeInterval: CHECK_INTERVAL, repeats: true) { [weak self] _ in + self?.checkFailures() + } + } + + @objc + func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, myUser userId: String) { + if reportedFailures[event.eventId] != nil || trackedEvents.contains(event.eventId) { + return + } + + // Filter out "expected" UTDs + // We cannot decrypt messages sent before the user joined the room + guard let myUser = roomState.members.member(withUserId: userId) else { return } + if myUser.membership != MXMembership.join { + return + } + + guard let failedEventId = event.eventId else { return } + + guard let error = event.decryptionError as? NSError else { return } + + var reason = DecryptionFailureReason.unspecified + + if error.code == MXDecryptingErrorUnknownInboundSessionIdCode.rawValue { + reason = DecryptionFailureReason.olmKeysNotSent + } else if error.code == MXDecryptingErrorOlmCode.rawValue { + reason = DecryptionFailureReason.olmIndexError + } + + let context = String(format: "code: %ld, description: %@", error.code, event.decryptionError.localizedDescription) + + reportedFailures[failedEventId] = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs()) + } + + @objc + func dispatch() { + self.checkFailures() + } + + @objc + func eventDidDecrypt(_ notification: Notification) { + guard let event = notification.object as? MXEvent else { return } + + // Could be an event in the reportedFailures, remove it + reportedFailures.removeValue(forKey: event.eventId) + } + + /** + Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be + tracked. + */ + @objc + func checkFailures() { + guard let delegate = self.delegate else {return} + + + let tsNow = self.timeProvider.nowTs() + var failuresToCheck = [DecryptionFailure]() + + for reportedFailure in self.reportedFailures.values { + let ellapsed = tsNow - reportedFailure.ts + if ellapsed > GRACE_PERIOD { + failuresToCheck.append(reportedFailure) + reportedFailures.removeValue(forKey: reportedFailure.failedEventId) + trackedEvents.insert(reportedFailure.failedEventId) + } + } + + for failure in failuresToCheck { + delegate.trackE2EEError(failure.reason, context: failure.context) + } + + } + +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index ab17eeac5..79d263cef 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -33,7 +33,6 @@ #import "ContactDetailsViewController.h" #import "BugReportViewController.h" -#import "DecryptionFailureTracker.h" #import "Tools.h" #import "WidgetManager.h" diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index d25655a22..4494bdbb2 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -23,7 +23,6 @@ #import "WidgetManager.h" #import "MXDecryptionResult.h" -#import "DecryptionFailureTracker.h" #import diff --git a/RiotTests/DecryptionFailureTrackerTests.swift b/RiotTests/DecryptionFailureTrackerTests.swift new file mode 100644 index 000000000..9b5fe94ea --- /dev/null +++ b/RiotTests/DecryptionFailureTrackerTests.swift @@ -0,0 +1,196 @@ +// +// Copyright 2024 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 XCTest +@testable import Element + + +class DecryptionFailureTrackerTests: XCTestCase { + + class TimeShifter: TimeProvider { + + var timestamp = TimeInterval(0) + + func nowTs() -> TimeInterval { + return timestamp + } + } + + class AnalyticsDelegate : E2EAnalytics { + var reportedFailure: Element.DecryptionFailureReason?; + + func trackE2EEError(_ reason: Element.DecryptionFailureReason, context: String) { + print("Error Tracked: ", reason) + reportedFailure = reason + } + + } + + let timeShifter = TimeShifter() + + func test_grace_period() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + timeShifter.timestamp = TimeInterval(2) + + decryptionFailureTracker.checkFailures(); + + XCTAssertNil(testDelegate.reportedFailure); + + // Pass the grace period + timeShifter.timestamp = TimeInterval(5) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure, DecryptionFailureReason.olmKeysNotSent); + } + + func test_do_not_double_report() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the grace period + timeShifter.timestamp = TimeInterval(5) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure, DecryptionFailureReason.olmKeysNotSent); + + // Try to report again the same event + testDelegate.reportedFailure = nil + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + // Pass the grace period + timeShifter.timestamp = TimeInterval(10) + + decryptionFailureTracker.checkFailures(); + + XCTAssertNil(testDelegate.reportedFailure); + } + + + func test_ignore_not_member() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + let fakeMembers = FakeRoomMembers() + fakeMembers.mockMembers[myUser] = MXMembership.ban + fakeRoomState.mockMembers = fakeMembers + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the grace period + timeShifter.timestamp = TimeInterval(5) + + decryptionFailureTracker.checkFailures(); + + XCTAssertNil(testDelegate.reportedFailure); + } + + + + func test_notification_center() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Shift time below GRACE_PERIOD + timeShifter.timestamp = TimeInterval(2) + + // Simulate event gets decrypted + NotificationCenter.default.post(name: .mxEventDidDecrypt, object: fakeEvent) + + + // Shift time after GRACE_PERIOD + timeShifter.timestamp = TimeInterval(6) + + + decryptionFailureTracker.checkFailures(); + + // Event should have been graced + XCTAssertNil(testDelegate.reportedFailure); + } + +} + diff --git a/RiotTests/FakeUtils.swift b/RiotTests/FakeUtils.swift new file mode 100644 index 000000000..7bd350e4b --- /dev/null +++ b/RiotTests/FakeUtils.swift @@ -0,0 +1,109 @@ +// +// Copyright 2024 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 + + +class FakeEvent: MXEvent { + + var mockEventId: String; + var mockSender: String!; + var mockDecryptionError: Error? + + init(id: String) { + mockEventId = id + super.init() + } + + required init?(coder: NSCoder) { + fatalError() + } + + override var sender: String! { + get { return mockSender } + set { mockSender = newValue } + } + + override var eventId: String! { + get { return mockEventId } + set { mockEventId = newValue } + } + + override var decryptionError: Error? { + get { return mockDecryptionError } + set { mockDecryptionError = newValue } + } + +} + + +class FakeRoomState: MXRoomState { + + var mockMembers: MXRoomMembers? + + override var members: MXRoomMembers? { + get { return mockMembers } + set { mockMembers = newValue } + } + +} + +class FakeRoomMember: MXRoomMember { + var mockMembership: MXMembership = MXMembership.join + var mockUserId: String! + var mockMembers: MXRoomMembers? = FakeRoomMembers() + + init(mockUserId: String!) { + self.mockUserId = mockUserId + super.init() + } + + override var membership: MXMembership { + get { return mockMembership } + set { mockMembership = newValue } + } + + override var userId: String!{ + get { return mockUserId } + set { mockUserId = newValue } + } + +} + + +class FakeRoomMembers: MXRoomMembers { + + var mockMembers = [String : MXMembership]() + + init(joined: [String] = [String]()) { + for userId in joined { + self.mockMembers[userId] = MXMembership.join + } + super.init() + } + + override func member(withUserId userId: String!) -> MXRoomMember? { + let membership = mockMembers[userId] + if membership != nil { + let mockMember = FakeRoomMember(mockUserId: userId) + mockMember.mockMembership = membership! + return mockMember + } else { + return nil + } + } + +} From 0a6528703d987efe993ffec945fb18ec9348f24a Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 29 Mar 2024 18:14:15 +0100 Subject: [PATCH 14/21] DecryptionTracker: Permanent vs Temporary UTD --- Riot/Modules/Analytics/Analytics.swift | 19 +-- .../DecryptionFailure+Analytics.swift | 44 ++++++ .../Modules/Analytics/DecryptionFailure.swift | 3 + .../Analytics/DecryptionFailureTracker.swift | 52 +++++- RiotTests/DecryptionFailureTrackerTests.swift | 148 +++++++++++++++++- 5 files changed, 235 insertions(+), 31 deletions(-) create mode 100644 Riot/Modules/Analytics/DecryptionFailure+Analytics.swift diff --git a/Riot/Modules/Analytics/Analytics.swift b/Riot/Modules/Analytics/Analytics.swift index 849efd0b8..d9d68e2f8 100644 --- a/Riot/Modules/Analytics/Analytics.swift +++ b/Riot/Modules/Analytics/Analytics.swift @@ -215,7 +215,7 @@ import AnalyticsEvents @objc protocol E2EAnalytics { - func trackE2EEError(_ reason: DecryptionFailureReason, context: String) + func trackE2EEError(_ failure: DecryptionFailure) } @@ -225,21 +225,8 @@ protocol E2EAnalytics { /// - Parameters: /// - reason: The error that occurred. /// - context: Additional context of the error that occured - func trackE2EEError(_ reason: DecryptionFailureReason, context: String) { - let event = AnalyticsEvent.Error( - context: context, - cryptoModule: .Rust, - cryptoSDK: AnalyticsEvent.Error.CryptoSDK.Rust, - domain: .E2EE, - // XXX not yet supported. - eventLocalAgeMillis: nil, - isFederated: nil, - isMatrixDotOrg: nil, - name: reason.errorName, - timeToDecryptMillis: nil, - userTrustsOwnIdentity: nil, - wasVisibleToUser: nil - ) + func trackE2EEError(_ failure: DecryptionFailure) { + let event = failure.toAnalyticsEvent() capture(event: event) } diff --git a/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift new file mode 100644 index 000000000..cd129aee3 --- /dev/null +++ b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift @@ -0,0 +1,44 @@ +// +// Copyright 2024 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 DecryptionFailure { + + public func toAnalyticsEvent() -> AnalyticsEvent.Error { + + let timeToDecryptMillis: Int = if self.timeToDecrypt != nil { + Int(self.timeToDecrypt! * 1000) + } else { + -1 + } + return AnalyticsEvent.Error( + context: self.context, + cryptoModule: .Rust, + cryptoSDK: .Rust, + domain: .E2EE, + + eventLocalAgeMillis: nil, + isFederated: nil, + isMatrixDotOrg: nil, + name: self.reason.errorName, + timeToDecryptMillis: timeToDecryptMillis, + userTrustsOwnIdentity: nil, + wasVisibleToUser: nil + ) + } +} diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index b5804d258..9e0ca5785 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -44,6 +44,9 @@ import AnalyticsEvents /// Additional context of failure let context: String + /// UTDs can be permanent or temporary. If temporary, this field will contain the time it took to decrypt the message in milliseconds. If permanent should be nil + var timeToDecrypt: TimeInterval? + init(failedEventId: String, reason: DecryptionFailureReason, context: String, ts: TimeInterval) { self.failedEventId = failedEventId self.reason = reason diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.swift b/Riot/Modules/Analytics/DecryptionFailureTracker.swift index f33f2b7c5..19b8afb19 100644 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.swift +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.swift @@ -36,7 +36,10 @@ class DecryptionFailureTracker: NSObject { let GRACE_PERIOD: TimeInterval = 4 // Call `checkFailures` every `CHECK_INTERVAL` - let CHECK_INTERVAL: TimeInterval = 2 + let CHECK_INTERVAL: TimeInterval = 15 + + // The maximum time to wait for a late decryption before reporting as permanent UTD + let MAX_WAIT_FOR_LATE_DECRYPTION: TimeInterval = 60 @objc weak var delegate: E2EAnalytics? @@ -60,9 +63,6 @@ class DecryptionFailureTracker: NSObject { name: .mxEventDidDecrypt, object: nil) - Timer.scheduledTimer(withTimeInterval: CHECK_INTERVAL, repeats: true) { [weak self] _ in - self?.checkFailures() - } } @objc @@ -93,6 +93,14 @@ class DecryptionFailureTracker: NSObject { let context = String(format: "code: %ld, description: %@", error.code, event.decryptionError.localizedDescription) reportedFailures[failedEventId] = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs()) + + // Start the ticker if needed. There is no need to have a ticker if no failures are tracked + if checkFailuresTimer == nil { + self.checkFailuresTimer = Timer.scheduledTimer(withTimeInterval: CHECK_INTERVAL, repeats: true) { [weak self] _ in + self?.checkFailures() + } + } + } @objc @@ -104,8 +112,30 @@ class DecryptionFailureTracker: NSObject { func eventDidDecrypt(_ notification: Notification) { guard let event = notification.object as? MXEvent else { return } - // Could be an event in the reportedFailures, remove it + guard let reportedFailure = self.reportedFailures[event.eventId] else { return } + + let now = self.timeProvider.nowTs() + let ellapsedTime = now - reportedFailure.ts + + if ellapsedTime < 4 { + // event is graced + reportedFailures.removeValue(forKey: event.eventId) + } else { + // It's a late decrypt must be reported as a late decrypt + reportedFailure.timeToDecrypt = ellapsedTime + self.delegate?.trackE2EEError(reportedFailure) + } + // Remove from reported failures + self.trackedEvents.insert(event.eventId) reportedFailures.removeValue(forKey: event.eventId) + + // Check if we still need the ticker timer + if reportedFailures.isEmpty { + // Invalidate the current timer, nothing to check for + self.checkFailuresTimer?.invalidate() + self.checkFailuresTimer = nil + } + } /** @@ -116,23 +146,29 @@ class DecryptionFailureTracker: NSObject { func checkFailures() { guard let delegate = self.delegate else {return} - let tsNow = self.timeProvider.nowTs() var failuresToCheck = [DecryptionFailure]() for reportedFailure in self.reportedFailures.values { let ellapsed = tsNow - reportedFailure.ts - if ellapsed > GRACE_PERIOD { + if ellapsed > MAX_WAIT_FOR_LATE_DECRYPTION { failuresToCheck.append(reportedFailure) + reportedFailure.timeToDecrypt = nil reportedFailures.removeValue(forKey: reportedFailure.failedEventId) trackedEvents.insert(reportedFailure.failedEventId) } } for failure in failuresToCheck { - delegate.trackE2EEError(failure.reason, context: failure.context) + delegate.trackE2EEError(failure) } + // Check if we still need the ticker timer + if reportedFailures.isEmpty { + // Invalidate the current timer, nothing to check for + self.checkFailuresTimer?.invalidate() + self.checkFailuresTimer = nil + } } } diff --git a/RiotTests/DecryptionFailureTrackerTests.swift b/RiotTests/DecryptionFailureTrackerTests.swift index 9b5fe94ea..3175ef19e 100644 --- a/RiotTests/DecryptionFailureTrackerTests.swift +++ b/RiotTests/DecryptionFailureTrackerTests.swift @@ -32,10 +32,9 @@ class DecryptionFailureTrackerTests: XCTestCase { } class AnalyticsDelegate : E2EAnalytics { - var reportedFailure: Element.DecryptionFailureReason?; + var reportedFailure: Element.DecryptionFailure?; - func trackE2EEError(_ reason: Element.DecryptionFailureReason, context: String) { - print("Error Tracked: ", reason) + func trackE2EEError(_ reason: Element.DecryptionFailure) { reportedFailure = reason } @@ -66,6 +65,9 @@ class DecryptionFailureTrackerTests: XCTestCase { timeShifter.timestamp = TimeInterval(2) + // simulate decrypted in the grace period + NotificationCenter.default.post(name: .mxEventDidDecrypt, object: fakeEvent) + decryptionFailureTracker.checkFailures(); XCTAssertNil(testDelegate.reportedFailure); @@ -74,10 +76,70 @@ class DecryptionFailureTrackerTests: XCTestCase { timeShifter.timestamp = TimeInterval(5) decryptionFailureTracker.checkFailures(); + XCTAssertNil(testDelegate.reportedFailure); - XCTAssertEqual(testDelegate.reportedFailure, DecryptionFailureReason.olmKeysNotSent); } + func test_report_ratcheted_key_utd() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorOlmCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the max period + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmIndexError); + } + + func test_report_unspecified_error() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorBadRoomCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Pass the max period + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.unspecified); + } + + + func test_do_not_double_report() { let myUser = "test@example.com"; @@ -100,12 +162,12 @@ class DecryptionFailureTrackerTests: XCTestCase { decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); - // Pass the grace period - timeShifter.timestamp = TimeInterval(5) + // Pass the max period + timeShifter.timestamp = TimeInterval(70) decryptionFailureTracker.checkFailures(); - XCTAssertEqual(testDelegate.reportedFailure, DecryptionFailureReason.olmKeysNotSent); + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); // Try to report again the same event testDelegate.reportedFailure = nil @@ -192,5 +254,77 @@ class DecryptionFailureTrackerTests: XCTestCase { XCTAssertNil(testDelegate.reportedFailure); } + + func test_should_report_late_decrypt() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Simulate succesful decryption after grace period but before max wait + timeShifter.timestamp = TimeInterval(20) + + // Simulate event gets decrypted + NotificationCenter.default.post(name: .mxEventDidDecrypt, object: fakeEvent) + + + decryptionFailureTracker.checkFailures(); + + // Event should have been reported as a late decrypt + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); + XCTAssertEqual(testDelegate.reportedFailure?.timeToDecrypt, TimeInterval(20)); + + } + + + + func test_should_report_permanent_decryption_error() { + + let myUser = "test@example.com"; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + + // Simulate succesful decryption after max wait + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + // Event should have been reported as a late decrypt + XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); + XCTAssertNil(testDelegate.reportedFailure?.timeToDecrypt); + + } } From 1d633e8fa5dcdf20ff113502661dcf4dafa115f6 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 29 Mar 2024 18:21:56 +0100 Subject: [PATCH 15/21] Add more test for convertion to analytics error --- RiotTests/DecryptionFailureTrackerTests.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/RiotTests/DecryptionFailureTrackerTests.swift b/RiotTests/DecryptionFailureTrackerTests.swift index 3175ef19e..7cd9bf480 100644 --- a/RiotTests/DecryptionFailureTrackerTests.swift +++ b/RiotTests/DecryptionFailureTrackerTests.swift @@ -290,6 +290,11 @@ class DecryptionFailureTrackerTests: XCTestCase { XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); XCTAssertEqual(testDelegate.reportedFailure?.timeToDecrypt, TimeInterval(20)); + // Assert that it's converted to millis for reporting + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.timeToDecryptMillis, 20000) + } @@ -325,6 +330,12 @@ class DecryptionFailureTrackerTests: XCTestCase { XCTAssertEqual(testDelegate.reportedFailure?.reason, DecryptionFailureReason.olmKeysNotSent); XCTAssertNil(testDelegate.reportedFailure?.timeToDecrypt); + + // Assert that it's converted to -1 for reporting + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.timeToDecryptMillis, -1) + } } From 56e66539d87f3a6cef6cc0343ce9e6021d166692 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Apr 2024 11:13:10 +0200 Subject: [PATCH 16/21] post merge fix --- .../Analytics/DecryptionFailureTracker.m | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 Riot/Modules/Analytics/DecryptionFailureTracker.m diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.m b/Riot/Modules/Analytics/DecryptionFailureTracker.m deleted file mode 100644 index 86b847f22..000000000 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.m +++ /dev/null @@ -1,174 +0,0 @@ -/* - Copyright 2018 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 "DecryptionFailureTracker.h" -#import "GeneratedInterface-Swift.h" - - -// Call `checkFailures` every `CHECK_INTERVAL` -#define CHECK_INTERVAL 10 - -// Give events a chance to be decrypted by waiting `GRACE_PERIOD` before counting -// and reporting them as failures -#define GRACE_PERIOD 30 - -// E2E failures analytics category. -NSString *const kDecryptionFailureTrackerAnalyticsCategory = @"e2e.failure"; - -@interface DecryptionFailureTracker() -{ - // Reported failures - // Every `CHECK_INTERVAL`, this list is checked for failures that happened - // more than`GRACE_PERIOD` ago. Those that did are reported to the delegate. - NSMutableDictionary *reportedFailures; - - // Event ids of failures that were tracked previously - NSMutableSet *trackedEvents; - - // Timer for periodic check - NSTimer *checkFailuresTimer; -} -@end - -@implementation DecryptionFailureTracker - -+ (instancetype)sharedInstance -{ - static DecryptionFailureTracker *sharedInstance = nil; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - sharedInstance = [[DecryptionFailureTracker alloc] init]; - }); - - return sharedInstance; -} - -- (instancetype)init -{ - self = [super init]; - if (self) - { - reportedFailures = [NSMutableDictionary dictionary]; - trackedEvents = [NSMutableSet set]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(eventDidDecrypt:) name:kMXEventDidDecryptNotification object:nil]; - - checkFailuresTimer = [NSTimer scheduledTimerWithTimeInterval:CHECK_INTERVAL - target:self - selector:@selector(checkFailures) - userInfo:nil - repeats:YES]; - } - return self; -} - -- (void)reportUnableToDecryptErrorForEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState myUser:(NSString *)userId -{ - if (reportedFailures[event.eventId] || [trackedEvents containsObject:event.eventId]) - { - return; - } - - // Filter out "expected" UTDs - // We cannot decrypt messages sent before the user joined the room - MXRoomMember *myUser = [roomState.members memberWithUserId:userId]; - if (!myUser || myUser.membership != MXMembershipJoin) - { - return; - } - - NSString *failedEventId = event.eventId; - DecryptionFailureReason reason; - - // Categorise the error - switch (event.decryptionError.code) - { - case MXDecryptingErrorUnknownInboundSessionIdCode: - reason = DecryptionFailureReasonOlmKeysNotSent; - break; - - case MXDecryptingErrorOlmCode: - reason = DecryptionFailureReasonOlmIndexError; - break; - - default: - // All other error codes will be tracked as `OlmUnspecifiedError` and will include `context` containing - // the actual error code and localized description - reason = DecryptionFailureReasonUnspecified; - break; - } - - NSString *context = [NSString stringWithFormat:@"code: %ld, description: %@", event.decryptionError.code, event.decryptionError.localizedDescription]; - reportedFailures[event.eventId] = [[DecryptionFailure alloc] initWithFailedEventId:failedEventId - reason:reason - context:context]; -} - -- (void)dispatch -{ - [self checkFailures]; -} - -#pragma mark - Private methods - -/** - Mark reported failures that occured before tsNow - GRACE_PERIOD as failures that should be - tracked. - */ -- (void)checkFailures -{ - if (!_delegate) - { - return; - } - - NSTimeInterval tsNow = [NSDate date].timeIntervalSince1970; - - NSMutableArray *failuresToTrack = [NSMutableArray array]; - - for (DecryptionFailure *reportedFailure in reportedFailures.allValues) - { - if (reportedFailure.ts < tsNow - GRACE_PERIOD) - { - [failuresToTrack addObject:reportedFailure]; - [reportedFailures removeObjectForKey:reportedFailure.failedEventId]; - [trackedEvents addObject:reportedFailure.failedEventId]; - } - } - - if (failuresToTrack.count) - { - // Sort failures by error reason - NSMutableDictionary *failuresCounts = [NSMutableDictionary dictionary]; - for (DecryptionFailure *failure in failuresToTrack) - { - failuresCounts[@(failure.reason)] = @(failuresCounts[@(failure.reason)].unsignedIntegerValue + 1); - [self.delegate trackE2EEError:failure.reason context:failure.context]; - } - - MXLogDebug(@"[DecryptionFailureTracker] trackFailures: %@", failuresCounts); - } -} - -- (void)eventDidDecrypt:(NSNotification *)notif -{ - // Could be an event in the reportedFailures, remove it - MXEvent *event = notif.object; - [reportedFailures removeObjectForKey:event.eventId]; -} - -@end From cc068abe56f69518aba37bbd248a6e841de73781 Mon Sep 17 00:00:00 2001 From: Maksim Kliazovich Date: Tue, 26 Mar 2024 00:51:32 +0000 Subject: [PATCH 17/21] Translated using Weblate (Belarusian) Currently translated at 0.2% (5 of 2416 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/be/ --- Riot/Assets/be.lproj/Vector.strings | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Riot/Assets/be.lproj/Vector.strings b/Riot/Assets/be.lproj/Vector.strings index 8b1378917..64295bad4 100644 --- a/Riot/Assets/be.lproj/Vector.strings +++ b/Riot/Assets/be.lproj/Vector.strings @@ -1 +1,9 @@ + + +// Titles +"title_home" = "Галоўная"; +"people_empty_view_title" = "Удзельнікі"; +"group_details_home" = "Галоўная"; +"spaces_home_space_title" = "Галоўная"; +"title_people" = "Удзельнікі"; From a92c842df287d1d3a37d880fa5997519d7a9b370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Mon, 1 Apr 2024 06:33:55 +0000 Subject: [PATCH 18/21] Translated using Weblate (Estonian) Currently translated at 100.0% (2416 of 2416 strings) Translation: Element iOS/Element iOS Translate-URL: https://translate.element.io/projects/riot-ios/riot-ios/et/ --- Riot/Assets/et.lproj/Vector.strings | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/et.lproj/Vector.strings b/Riot/Assets/et.lproj/Vector.strings index 3df4d23b9..22e9f3419 100644 --- a/Riot/Assets/et.lproj/Vector.strings +++ b/Riot/Assets/et.lproj/Vector.strings @@ -1889,8 +1889,8 @@ "notice_conference_call_started" = "VoIP rühmakõne algas"; "notice_conference_call_finished" = "VoIP rühmakõne lõppes"; // Notice Events with "You" -"notice_room_invite_by_you" = "Sina kutsusid kasutajat %@"; -"notice_room_invite_you" = "%@ kutsus sind"; +"notice_room_invite_by_you" = "Sina saatsid kutse kasutajale %@"; +"notice_room_invite_you" = "%@ saatis sulle kutse"; "notice_room_third_party_invite_by_you" = "Sina saatsid kasutajale %@ kutse jututoaga liitumiseks"; "notice_room_third_party_registered_invite_by_you" = "Sina võtsid vastu kutse %@ nimel"; "notice_room_third_party_revoked_invite_by_you" = "Sina võtsid tagasi jututoaga liitumise kutse kasutajalt %@"; @@ -2025,7 +2025,7 @@ "notice_room_third_party_invite_for_dm" = "%@ saatis kutse kasutajale %@"; "notice_room_third_party_revoked_invite_for_dm" = "%@ võttis tagasi kasutaja %@ kutse"; "notice_room_name_changed_for_dm" = "%@ muutis jututoa uueks nimeks %@."; -"notice_room_third_party_invite_by_you_for_dm" = "Sina kutsusid kasutajat %@"; +"notice_room_third_party_invite_by_you_for_dm" = "Sina saatsid kutse kasutajale %@"; "notice_room_third_party_revoked_invite_by_you_for_dm" = "Sina võtsid tagasi kasutaja %@ kutse"; "notice_room_name_changed_by_you_for_dm" = "Sa muutsid jututoa uueks nimeks %@."; "notice_room_name_removed_by_you_for_dm" = "Sa eemaldasid jututoa nime"; From 487a4f7bdaa5ee7fdd1146f47255b977b43d56e2 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 2 Apr 2024 11:58:49 +0200 Subject: [PATCH 19/21] version++ --- CHANGES.md | 7 +++++++ changelog.d/pr-7768.misc | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) delete mode 100644 changelog.d/pr-7768.misc diff --git a/CHANGES.md b/CHANGES.md index 174be9ac8..085677837 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +## Changes in 1.11.9 (2024-04-02) + +Others + +- Update matrix-analytics-events to version 0.15.0 ([#7768](https://github.com/element-hq/element-ios/pull/7768)) + + ## Changes in 1.11.8 (2024-03-05) 🙌 Improvements diff --git a/changelog.d/pr-7768.misc b/changelog.d/pr-7768.misc deleted file mode 100644 index 053b12b37..000000000 --- a/changelog.d/pr-7768.misc +++ /dev/null @@ -1 +0,0 @@ -Update matrix-analytics-events to version 0.15.0 From c2b17a0ef99fbfc79f29487b7aadcdd5fc232dcb Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:02:12 +0200 Subject: [PATCH 20/21] Update CHANGES.md --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 085677837..eb5267b66 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,8 @@ Others - Update matrix-analytics-events to version 0.15.0 ([#7768](https://github.com/element-hq/element-ios/pull/7768)) +- Upgrade to build with Xcode 15.2 +- Add a privacy manifest ## Changes in 1.11.8 (2024-03-05) From 24f1fb59bfec67029ed3c7216dad851578fba991 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 2 Apr 2024 15:06:22 +0200 Subject: [PATCH 21/21] finish version++ --- Podfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index 8ca318a66..07f1fb4d9 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -25,7 +25,7 @@ PODS: - GBDeviceInfo (7.1.0): - GBDeviceInfo/Core (= 7.1.0) - GBDeviceInfo/Core (7.1.0) - - GZIP (1.3.1) + - GZIP (1.3.2) - Introspect (0.11.0) - JitsiMeetSDKLite (8.1.2-lite): - JitsiWebRTC (~> 111.0) @@ -176,7 +176,7 @@ SPEC CHECKSUMS: FLEX: e51461dd6f0bfb00643c262acdfea5d5d12c596b FlowCommoniOS: ca92071ab526dc89905495a37844fd7e78d1a7f2 GBDeviceInfo: 5d62fa85bdcce3ed288d83c28789adf1173e4376 - GZIP: e6922ed5bdd1d77d84589d50821ac34ea0c38d4b + GZIP: 3c0abf794bfce8c7cb34ea05a1837752416c8868 Introspect: 4cc1e4c34dd016540c8d86a591c231c09dafbee3 JitsiMeetSDKLite: 895213158cf62342069a10634a41d2f1c00057f7 JitsiWebRTC: 80f62908fcf2a1160e0d14b584323fb6e6be630b @@ -210,4 +210,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: c87b532985dd755b373732f841e3bcfe616f4e4f -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3