TF-4269 Integrate Sentry into iOS NSE and automate dSYM upload in CI
This commit is contained in:
@@ -101,10 +101,11 @@ jobs:
|
||||
# 1. Install Sentry CLI
|
||||
curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="2.42.2" bash
|
||||
|
||||
# 2. Validate tag version matches pubspec.yaml version
|
||||
# 2. Validate tag version matches pubspec.yaml version (base version only, ignoring -rc* suffix)
|
||||
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||
TAG_VERSION="${TAG_VERSION%%-rc*}"
|
||||
PUBSPEC_VERSION=$(awk '/^version:/{split($2,a,"+"); print a[1]; exit}' pubspec.yaml)
|
||||
PUBSPEC_VERSION="${PUBSPEC_VERSION%%-rc*}"
|
||||
if [ "$PUBSPEC_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "::error::Tag version ($TAG_VERSION) does not match pubspec version ($PUBSPEC_VERSION)."
|
||||
exit 1
|
||||
@@ -117,7 +118,6 @@ jobs:
|
||||
|
||||
# 4. Upload ProGuard Mapping
|
||||
MAPPING_FILE="build/app/outputs/mapping/release/mapping.txt"
|
||||
|
||||
if [ -f "$MAPPING_FILE" ]; then
|
||||
echo "Found mapping.txt, uploading ProGuard mapping..."
|
||||
sentry-cli upload-proguard "$MAPPING_FILE"
|
||||
@@ -128,7 +128,6 @@ jobs:
|
||||
|
||||
# 5. Upload Dart Debug Symbols (Native Dart Stacktrace)
|
||||
SYMBOLS_DIR="build/app/outputs/symbols"
|
||||
|
||||
if [ -d "$SYMBOLS_DIR" ]; then
|
||||
echo "Uploading Dart debug symbols from $SYMBOLS_DIR..."
|
||||
sentry-cli debug-files upload "$SYMBOLS_DIR"
|
||||
@@ -136,6 +135,48 @@ jobs:
|
||||
echo "::error::Symbols directory not found at $SYMBOLS_DIR. Check your Fastfile build arguments."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# 6. Finalize
|
||||
sentry-cli releases finalize "$SENTRY_RELEASE"
|
||||
|
||||
# --- UPLOAD DSYM TO SENTRY FOR IOS ---
|
||||
# sentry-cli debug-files upload works independently of the release lifecycle,
|
||||
# so no releases new/finalize needed here — avoids race condition with Android leg.
|
||||
- name: Upload iOS dSYMs to Sentry
|
||||
if: matrix.os == 'ios'
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
|
||||
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
|
||||
run: |
|
||||
# 1. Install Sentry CLI
|
||||
curl -sL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="2.42.2" bash
|
||||
|
||||
# 2. Validate tag version matches pubspec.yaml version (base version only, ignoring -rc* suffix)
|
||||
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||
TAG_VERSION="${TAG_VERSION%%-rc*}"
|
||||
PUBSPEC_VERSION=$(awk '/^version:/{split($2,a,"+"); print a[1]; exit}' pubspec.yaml)
|
||||
PUBSPEC_VERSION="${PUBSPEC_VERSION%%-rc*}"
|
||||
if [ "$PUBSPEC_VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "::error::Tag version ($TAG_VERSION) does not match pubspec version ($PUBSPEC_VERSION)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Read the xcarchive path written by Fastlane after build_app.
|
||||
# Fastlane writes lane_context[XCODEBUILD_ARCHIVE] to ios/gym_archive_path.txt.
|
||||
XCARCHIVE=$(cat ios/gym_archive_path.txt 2>/dev/null)
|
||||
if [ -z "$XCARCHIVE" ] || [ ! -d "$XCARCHIVE" ]; then
|
||||
echo "::error::xcarchive not found. Check that Fastlane wrote gym_archive_path.txt correctly."
|
||||
exit 1
|
||||
fi
|
||||
echo "Found xcarchive: $XCARCHIVE"
|
||||
|
||||
# 4. Upload dSYMs (Native code: Swift, Objective-C, C++)
|
||||
DSYM_DIR="$XCARCHIVE/dSYMs"
|
||||
if [ -d "$DSYM_DIR" ]; then
|
||||
echo "Uploading dSYMs from $DSYM_DIR..."
|
||||
sentry-cli debug-files upload --include-sources "$DSYM_DIR"
|
||||
else
|
||||
echo "::error::dSYMs directory not found inside xcarchive at $DSYM_DIR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -95,4 +95,24 @@ class SentryConfig {
|
||||
dist: sentryDist.isNotEmpty ? sentryDist : null,
|
||||
);
|
||||
}
|
||||
|
||||
static const String sentryConfigKeyChain = 'sentry_config_data';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'dsn': dsn,
|
||||
'environment': environment,
|
||||
'release': release,
|
||||
if (dist != null) 'dist': dist,
|
||||
'tracesSampleRate': tracesSampleRate,
|
||||
'profilesSampleRate': profilesSampleRate,
|
||||
'sessionSampleRate': sessionSampleRate,
|
||||
'onErrorSampleRate': onErrorSampleRate,
|
||||
'enableLogs': enableLogs,
|
||||
'isDebug': isDebug,
|
||||
'attachScreenshot': attachScreenshot,
|
||||
'isAvailable': isAvailable,
|
||||
'enableFramesTracking': enableFramesTracking,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ ServiceDefinitions.json
|
||||
Runner/GeneratedPluginRegistrant.*
|
||||
Flutter/ephemeral/
|
||||
|
||||
# Build artifacts generated by Fastlane
|
||||
gym_archive_path.txt
|
||||
|
||||
# Exceptions to above rules.
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
|
||||
+12
@@ -38,6 +38,18 @@ target 'Runner' do
|
||||
end
|
||||
end
|
||||
|
||||
target 'TwakeMailNSE' do
|
||||
use_frameworks!
|
||||
# Must use HybridSDK subspec at the same version as sentry_flutter requires.
|
||||
# sentry_flutter 9.8.0 depends on Sentry/HybridSDK (= 8.56.2).
|
||||
# Using plain `pod 'Sentry'` (Core) causes a version conflict and missing PrivateSentrySDKOnly errors.
|
||||
#
|
||||
# UPGRADE NOTE: When bumping sentry_flutter, update this version to match the new
|
||||
# Sentry/HybridSDK constraint declared in Podfile.lock under DEPENDENCIES.
|
||||
# Verify with: grep 'Sentry/HybridSDK' ios/Podfile.lock
|
||||
pod 'Sentry/HybridSDK', '8.56.2'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
|
||||
+3
-2
@@ -262,6 +262,7 @@ DEPENDENCIES:
|
||||
- pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`)
|
||||
- printing (from `.symlinks/plugins/printing/ios`)
|
||||
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
|
||||
- Sentry/HybridSDK (= 8.56.2)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
@@ -418,7 +419,7 @@ SPEC CHECKSUMS:
|
||||
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
|
||||
package_info_plus: 580e9a5f1b6ca5594e7c9ed5f92d1dfb2a66b5e1
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
patrol: 5df5d241d7f95f0df12a6906bbf45acb43a1e537
|
||||
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
|
||||
pdfrx: 310e84d01e06fd2af26e16507a0e48c27e99195c
|
||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||
photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413
|
||||
@@ -438,6 +439,6 @@ SPEC CHECKSUMS:
|
||||
UniversalDetector2: 7c9ffd935cf050eeb19edf7e90f6febe3743a1af
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
|
||||
PODFILE CHECKSUM: 40b12ce0bc437886ee4f4050970375d7d253708d
|
||||
PODFILE CHECKSUM: 01821a4f5c4164186c3bcdf0b69845d9f6e8604e
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -11,9 +11,12 @@
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
756BBC4C51FCB44047AB39E4 /* Pods_TeamMailShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */; };
|
||||
84EC933C2F236E8300A0EDC7 /* SentryConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EC933B2F236E7800A0EDC7 /* SentryConfig.swift */; };
|
||||
84EC933E2F23705E00A0EDC7 /* SentryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EC933D2F23705D00A0EDC7 /* SentryManager.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
B561AD69C2BE6DF40BF29406 /* Pods_TwakeMailNSE.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA243B93F65E4E3CD7DC0348 /* Pods_TwakeMailNSE.framework */; };
|
||||
CDFECA8C54311B749F044831 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */; };
|
||||
F522E87F2C0EE23400DDA35B /* AuthenticationSSOTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */; };
|
||||
F522E8812C0EE3F200DDA35B /* AuthenticationSSO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E7D8832B3877050009BB8A /* AuthenticationSSO.swift */; };
|
||||
@@ -126,10 +129,14 @@
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4D124E74293A67D900BA5186 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
||||
5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5E35AF242935B79C2DCACB65 /* Pods-TwakeMailNSE.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TwakeMailNSE.debug.xcconfig"; path = "Target Support Files/Pods-TwakeMailNSE/Pods-TwakeMailNSE.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
631346BB444C71671599207F /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
6B7C6E49525344DE515F7373 /* Pods-TwakeMailNSE.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TwakeMailNSE.release.xcconfig"; path = "Target Support Files/Pods-TwakeMailNSE/Pods-TwakeMailNSE.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
84EC933B2F236E7800A0EDC7 /* SentryConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryConfig.swift; sourceTree = "<group>"; };
|
||||
84EC933D2F23705D00A0EDC7 /* SentryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryManager.swift; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -137,8 +144,10 @@
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
AF31502A83CA492E27C36A7B /* Pods-TwakeMailNSE.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TwakeMailNSE.profile.xcconfig"; path = "Target Support Files/Pods-TwakeMailNSE/Pods-TwakeMailNSE.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
B2EAFF659572E6B9F5AFAAF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TeamMailShareExtension.release.xcconfig"; path = "Target Support Files/Pods-TeamMailShareExtension/Pods-TeamMailShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EA243B93F65E4E3CD7DC0348 /* Pods_TwakeMailNSE.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_TwakeMailNSE.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
F522E87E2C0EE23400DDA35B /* AuthenticationSSOTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationSSOTests.swift; sourceTree = "<group>"; };
|
||||
F522E8852C0EE8B600DDA35B /* CoreUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreUtils.swift; sourceTree = "<group>"; };
|
||||
F52F992D27FD6EB900346091 /* TeamMailShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TeamMailShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -218,6 +227,7 @@
|
||||
files = (
|
||||
F5630C812B2CEBC0003CC0FD /* KeychainAccess in Frameworks */,
|
||||
F53D1E632B2DE80200051FD0 /* Alamofire in Frameworks */,
|
||||
B561AD69C2BE6DF40BF29406 /* Pods_TwakeMailNSE.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -229,6 +239,7 @@
|
||||
children = (
|
||||
5810EDDA99BEFEACD742F507 /* Pods_Runner.framework */,
|
||||
143BD7B2CF181BA69CDE351A /* Pods_TeamMailShareExtension.framework */,
|
||||
EA243B93F65E4E3CD7DC0348 /* Pods_TwakeMailNSE.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -393,6 +404,7 @@
|
||||
F5630C792B2CE133003CC0FD /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
84EC933B2F236E7800A0EDC7 /* SentryConfig.swift */,
|
||||
F5630C7A2B2CE148003CC0FD /* KeychainSharingSession.swift */,
|
||||
F5630C842B2CF50C003CC0FD /* TypeName.swift */,
|
||||
);
|
||||
@@ -402,6 +414,7 @@
|
||||
F5630C7C2B2CE33A003CC0FD /* Utils */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
84EC933D2F23705D00A0EDC7 /* SentryManager.swift */,
|
||||
F5630C7D2B2CE359003CC0FD /* InfoPlistReader.swift */,
|
||||
F5BBBF542B2EEF3D007930E1 /* BundleExtension.swift */,
|
||||
F5630C862B2D0387003CC0FD /* PayloadParser.swift */,
|
||||
@@ -491,6 +504,9 @@
|
||||
07753111B751BFDB3187FE6F /* Pods-TeamMailShareExtension.debug.xcconfig */,
|
||||
BC08294F5AE09BF8CC592AD1 /* Pods-TeamMailShareExtension.release.xcconfig */,
|
||||
1FB76FA91BBCB2BF7B08705B /* Pods-TeamMailShareExtension.profile.xcconfig */,
|
||||
5E35AF242935B79C2DCACB65 /* Pods-TwakeMailNSE.debug.xcconfig */,
|
||||
6B7C6E49525344DE515F7373 /* Pods-TwakeMailNSE.release.xcconfig */,
|
||||
AF31502A83CA492E27C36A7B /* Pods-TwakeMailNSE.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -564,6 +580,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F5D4EA0B2B2ABF090090DDFC /* Build configuration list for PBXNativeTarget "TwakeMailNSE" */;
|
||||
buildPhases = (
|
||||
0F3B486617C7C494D3DB7CF9 /* [CP] Check Pods Manifest.lock */,
|
||||
F5D4E9FC2B2ABF090090DDFC /* Sources */,
|
||||
F5D4E9FD2B2ABF090090DDFC /* Frameworks */,
|
||||
F5D4E9FE2B2ABF090090DDFC /* Resources */,
|
||||
@@ -674,6 +691,28 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
0F3B486617C7C494D3DB7CF9 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-TwakeMailNSE-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
2A73AAD0923EF3B729E8973A /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -827,6 +866,7 @@
|
||||
files = (
|
||||
F5BBBF512B2EEC37007930E1 /* NetworkExceptions.swift in Sources */,
|
||||
F53D1E7F2B2E3C2600051FD0 /* JmapConstants.swift in Sources */,
|
||||
84EC933C2F236E8300A0EDC7 /* SentryConfig.swift in Sources */,
|
||||
F5E7D8822B3876F60009BB8A /* AuthenticationCredential.swift in Sources */,
|
||||
F5D4EA032B2ABF090090DDFC /* NotificationService.swift in Sources */,
|
||||
F522E8872C0EE8B600DDA35B /* CoreUtils.swift in Sources */,
|
||||
@@ -838,6 +878,7 @@
|
||||
F53D1E662B2DE8B800051FD0 /* AlamofireService.swift in Sources */,
|
||||
F5BBBF532B2EECAA007930E1 /* JmapExceptions.swift in Sources */,
|
||||
F53D1E882B2E4B3B00051FD0 /* AuthenticationType.swift in Sources */,
|
||||
84EC933E2F23705E00A0EDC7 /* SentryManager.swift in Sources */,
|
||||
F53D1E6C2B2E208C00051FD0 /* Email.swift in Sources */,
|
||||
F5630C872B2D0387003CC0FD /* PayloadParser.swift in Sources */,
|
||||
F5E7D8862B3877390009BB8A /* TokenResponse.swift in Sources */,
|
||||
@@ -1419,6 +1460,7 @@
|
||||
};
|
||||
F5D4EA082B2ABF090090DDFC /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5E35AF242935B79C2DCACB65 /* Pods-TwakeMailNSE.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_GROUP_ID = group.com.linagora.teammail;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@@ -1466,6 +1508,7 @@
|
||||
};
|
||||
F5D4EA092B2ABF090090DDFC /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 6B7C6E49525344DE515F7373 /* Pods-TwakeMailNSE.release.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_GROUP_ID = group.com.linagora.teammail;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
@@ -1510,6 +1553,7 @@
|
||||
};
|
||||
F5D4EA0A2B2ABF090090DDFC /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF31502A83CA492E27C36A7B /* Pods-TwakeMailNSE.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
APP_GROUP_ID = group.com.linagora.teammail;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
|
||||
@@ -58,3 +58,21 @@ class KeychainController: KeychainControllerDelegate {
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
extension KeychainController {
|
||||
/// The key used in Dart to store the Sentry configuration JSON
|
||||
private var sentryConfigKey: String { "sentry_config_data" }
|
||||
|
||||
/// Retrieves and decodes the SentryConfig from Keychain
|
||||
func retrieveSentryConfig() -> SentryConfig? {
|
||||
do {
|
||||
guard let configData = try keychain.getData(sentryConfigKey) else {
|
||||
return nil
|
||||
}
|
||||
return try JSONDecoder().decode(SentryConfig.self, from: configData)
|
||||
} catch {
|
||||
TwakeLogger.shared.log(message: "SentryConfig could not be decoded from Keychain")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Sentry
|
||||
|
||||
struct KeychainSharingSession: Codable {
|
||||
let accountId: String
|
||||
@@ -61,4 +62,12 @@ extension KeychainSharingSession {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var sentryUser: User {
|
||||
let user = User()
|
||||
user.userId = self.accountId
|
||||
user.email = self.userName
|
||||
user.username = self.userName
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
|
||||
struct SentryConfig: Codable {
|
||||
/// DSN (Data Source Name) endpoint for the Sentry project
|
||||
let dsn: String
|
||||
|
||||
/// Running environment (production/staging/dev)
|
||||
let environment: String
|
||||
|
||||
/// Current app release version
|
||||
let release: String
|
||||
|
||||
/// Distribution (e.g. Git SHA). Must match --dist used when uploading symbols to Sentry.
|
||||
/// Optional: only set when the main app passes it via --dart-define=SENTRY_DIST.
|
||||
let dist: String?
|
||||
|
||||
/// Performance monitoring: Set to 1.0 to capture 100% of transactions for tracing.
|
||||
/// High values in NSE might impact extension memory limit (24MB).
|
||||
let tracesSampleRate: Double
|
||||
|
||||
/// Optional profiling sample rate.
|
||||
let profilesSampleRate: Double
|
||||
|
||||
/// Release Health: The sampling rate for sessions (0.0 to 1.0).
|
||||
let sessionSampleRate: Double
|
||||
|
||||
/// Error tracking: The sampling rate for errors (0.0 to 1.0).
|
||||
/// If set to 0.1, only 10% of errors are sent.
|
||||
let onErrorSampleRate: Double
|
||||
|
||||
/// Enable logs to be sent to Sentry (or internal console logging).
|
||||
let enableLogs: Bool
|
||||
|
||||
/// Debug logs during development.
|
||||
let isDebug: Bool
|
||||
|
||||
/// Automatically attaches a screenshot when capturing an error.
|
||||
/// Ignored in NSE as there is no UI to screenshot.
|
||||
let attachScreenshot: Bool
|
||||
|
||||
/// Master switch to check if Sentry integration is allowed/available.
|
||||
let isAvailable: Bool
|
||||
|
||||
/// Performance: Tracks UI rendering performance.
|
||||
/// Ignored in NSE as there is no UI rendering.
|
||||
let enableFramesTracking: Bool
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import UserNotifications
|
||||
import Sentry
|
||||
import SwiftUI
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
@@ -13,6 +14,10 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
|
||||
SentryManager.shared.configure(with: keychainController)
|
||||
SentryManager.shared.clearUser()
|
||||
|
||||
handler = contentHandler
|
||||
modifiedContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
@@ -22,10 +27,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
if isAppActive == true {
|
||||
self.modifiedContent?.userInfo = request.content.userInfo.merging(["data": request.content.userInfo], uniquingKeysWith: {(_, new) in new})
|
||||
contentHandler(self.modifiedContent ?? request.content)
|
||||
return
|
||||
}
|
||||
|
||||
guard let payloadData = request.content.userInfo as? [String: Any],
|
||||
!keychainController.retrieveSharingSessions().isEmpty else {
|
||||
SentryManager.shared.capture(message: "NSE: Payload invalid or No Session found in Keychain")
|
||||
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
||||
return self.notify()
|
||||
}
|
||||
@@ -42,6 +49,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
SentryManager.shared.capture(message: "NSE: Service Extension Time Expired (Timeout)", flushTimeout: 0.3)
|
||||
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
||||
self.notify()
|
||||
}
|
||||
@@ -55,14 +63,23 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
let mapStateChanges: [String: [TypeName: String]] = PayloadParser.shared.parsingPayloadNotification(payloadData: payloadData)
|
||||
|
||||
if (mapStateChanges.isEmpty) {
|
||||
SentryManager.shared.capture(message: "NSE: Payload parsing returned empty state changes")
|
||||
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
||||
return self.notify()
|
||||
} else {
|
||||
guard let currentAccountId = mapStateChanges.keys.first,
|
||||
let keychainSharingSession = keychainController.retrieveSharingSessionFromKeychain(accountId: currentAccountId),
|
||||
keychainSharingSession.tokenOIDC != nil || keychainSharingSession.basicAuth != nil,
|
||||
let listStateOfAccount = mapStateChanges[currentAccountId],
|
||||
keychainSharingSession.tokenOIDC != nil || keychainSharingSession.basicAuth != nil else {
|
||||
SentryManager.shared.capture(message: "NSE: Session missing or invalid credential for account: \(mapStateChanges.keys.first ?? "unknown")")
|
||||
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
||||
return self.notify()
|
||||
}
|
||||
|
||||
SentryManager.shared.setSentryUser(keychainSharingSession.sentryUser)
|
||||
|
||||
guard let listStateOfAccount = mapStateChanges[currentAccountId],
|
||||
let newEmailDeliveryState = listStateOfAccount[TypeName.emailDelivery] else {
|
||||
SentryManager.shared.capture(message: "NSE: Missing emailDelivery state in payload")
|
||||
self.showDefaultNotification(message: NSLocalizedString(self.newNotificationDefaultMessageKey, comment: "Localizable"))
|
||||
return self.notify()
|
||||
}
|
||||
@@ -106,7 +123,8 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
TwakeLogger.shared.log(message: "JmapClient.shared.getNewEmails: \(error)")
|
||||
TwakeLogger.shared.log(message: "Error processing emails: \(error)")
|
||||
SentryManager.shared.capture(error: error)
|
||||
self.showDefaultNotification(message: NSLocalizedString(self.newEmailDefaultMessageKey, comment: "Localizable"))
|
||||
return self.notify()
|
||||
}
|
||||
@@ -183,7 +201,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
content.userInfo = userInfo
|
||||
|
||||
// Create a notification trigger
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false)
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 2, repeats: false)
|
||||
// Create a notification request
|
||||
let request = UNNotificationRequest(identifier: notificationId, content: content, trigger: trigger)
|
||||
|
||||
@@ -191,6 +209,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
TwakeLogger.shared.log(message: "Error scheduling notification: \(error.localizedDescription)")
|
||||
SentryManager.shared.capture(error: error)
|
||||
} else {
|
||||
TwakeLogger.shared.log(message: "Notification scheduled successfully")
|
||||
}
|
||||
@@ -212,6 +231,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
|
||||
private func cleanUp() {
|
||||
SentryManager.shared.clearUser()
|
||||
handler = nil
|
||||
modifiedContent = nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import Foundation
|
||||
import Sentry
|
||||
|
||||
class SentryManager {
|
||||
|
||||
/// Singleton instance for easy access
|
||||
static let shared = SentryManager()
|
||||
|
||||
/// Internal flag to prevent multiple initializations
|
||||
private var isInitialized: Bool = false
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Configures Sentry using the config stored in Keychain.
|
||||
func configure(with keychainController: KeychainController) {
|
||||
// Prevent re-initialization
|
||||
if isInitialized { return }
|
||||
|
||||
// Retrieve config and validate 'isAvailable' and DSN presence
|
||||
guard let config = keychainController.retrieveSentryConfig(),
|
||||
config.isAvailable,
|
||||
!config.dsn.isEmpty else {
|
||||
TwakeLogger.shared.log(message: "Sentry is disabled or config is missing")
|
||||
return
|
||||
}
|
||||
|
||||
// Start Sentry SDK with options mapped from the config
|
||||
SentrySDK.start { options in
|
||||
options.dsn = config.dsn
|
||||
options.environment = config.environment
|
||||
options.releaseName = config.release
|
||||
options.dist = config.dist
|
||||
options.debug = config.isDebug
|
||||
// Map enableLogs to diagnostic level if needed
|
||||
options.diagnosticLevel = config.isDebug ? .debug : .none
|
||||
// Maps 'onErrorSampleRate' (Dart) to 'sampleRate' (iOS).
|
||||
// tracesSampleRate, profilesSampleRate, sessionSampleRate are intentionally not applied:
|
||||
// NSE has no UI and its lifecycle is too short for performance/session tracking.
|
||||
options.sampleRate = NSNumber(value: config.onErrorSampleRate)
|
||||
// Disable App Hang tracking: NSE execution is short, this causes false positives.
|
||||
options.enableAppHangTracking = false
|
||||
// Disable Watchdog tracking: Prevent OOM reports specific to extensions.
|
||||
options.enableWatchdogTerminationTracking = false
|
||||
// Disable UI/Interaction tracing: NSE has no UI.
|
||||
options.enableUserInteractionTracing = false
|
||||
options.enableAutoPerformanceTracing = false
|
||||
options.enablePreWarmedAppStartTracing = false
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
TwakeLogger.shared.log(message: "Sentry has been successfully initialized.")
|
||||
}
|
||||
|
||||
/// Safely captures an error if Sentry is initialized.
|
||||
/// - Parameter flushTimeout: If provided, blocks until events are sent or the timeout elapses.
|
||||
/// Use in critical paths (e.g. serviceExtensionTimeWillExpire) where the process may be
|
||||
/// suspended before Sentry flushes its queue.
|
||||
func capture(error: Error, flushTimeout: TimeInterval? = nil) {
|
||||
guard isInitialized else { return }
|
||||
SentrySDK.capture(error: error)
|
||||
if let flushTimeout {
|
||||
SentrySDK.flush(timeout: flushTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Safely captures a message if Sentry is initialized.
|
||||
/// - Parameter flushTimeout: If provided, blocks until events are sent or the timeout elapses.
|
||||
/// Use in critical paths (e.g. serviceExtensionTimeWillExpire) where the process may be
|
||||
/// suspended before Sentry flushes its queue.
|
||||
func capture(message: String, flushTimeout: TimeInterval? = nil) {
|
||||
guard isInitialized else { return }
|
||||
SentrySDK.capture(message: message)
|
||||
if let flushTimeout {
|
||||
SentrySDK.flush(timeout: flushTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set user context cho Sentry
|
||||
func setSentryUser(_ user: User) {
|
||||
guard isInitialized else { return }
|
||||
SentrySDK.setUser(user)
|
||||
}
|
||||
|
||||
/// Clear user
|
||||
func clearUser() {
|
||||
guard isInitialized else { return }
|
||||
SentrySDK.setUser(nil)
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,10 @@ platform :ios do
|
||||
workspace: "Runner.xcworkspace",
|
||||
export_method: "app-store"
|
||||
)
|
||||
# Write the xcarchive path to a file so CI can locate dSYMs for Sentry upload.
|
||||
# lane_context[XCODEBUILD_ARCHIVE] is set by build_app and contains the exact path.
|
||||
archive_path = Actions.lane_context[SharedValues::XCODEBUILD_ARCHIVE]
|
||||
File.write("gym_archive_path.txt", archive_path.to_s) if archive_path
|
||||
upload_to_testflight(
|
||||
skip_waiting_for_build_processing: true,
|
||||
ipa: "Runner.ipa"
|
||||
|
||||
+5
-1
@@ -234,6 +234,7 @@ import 'package:tmail_ui_user/main/universal_import/html_stub.dart' as html;
|
||||
import 'package:tmail_ui_user/main/utils/app_config.dart';
|
||||
import 'package:tmail_ui_user/main/utils/email_receive_manager.dart';
|
||||
import 'package:tmail_ui_user/main/utils/ios_notification_manager.dart';
|
||||
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
|
||||
import 'package:tmail_ui_user/main/utils/toast_manager.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@@ -406,7 +407,10 @@ class MailboxDashBoardController extends ReloadableController
|
||||
@override
|
||||
void onInit() {
|
||||
if (PlatformInfo.isMobile) {
|
||||
_sentryEcosystem = SentryEcosystem(getBinding<SentryConfigurationCacheManager>());
|
||||
_sentryEcosystem = SentryEcosystem(
|
||||
getBinding<SentryConfigurationCacheManager>(),
|
||||
getBinding<IOSSharingManager>(),
|
||||
);
|
||||
_registerReceivingFileSharingStream();
|
||||
_registerDeepLinks();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import 'package:core/presentation/extensions/string_extension.dart';
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:core/utils/sentry/sentry_config.dart';
|
||||
import 'package:core/utils/sentry/sentry_manager.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:tmail_ui_user/features/caching/extensions/sentry_cache_extensions.dart';
|
||||
import 'package:tmail_ui_user/features/caching/manager/sentry_configuration_cache_manager.dart';
|
||||
import 'package:tmail_ui_user/features/mailbox_dashboard/domain/linagora_ecosystem/sentry_config_linagora_ecosystem.dart';
|
||||
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
|
||||
|
||||
class SentryEcosystem {
|
||||
final SentryConfigurationCacheManager? _cacheManager;
|
||||
final IOSSharingManager? _iosSharingManager;
|
||||
|
||||
SentryUser? _sentryUser;
|
||||
|
||||
SentryEcosystem(this._cacheManager);
|
||||
SentryEcosystem(this._cacheManager, this._iosSharingManager);
|
||||
|
||||
void initUser(SentryUser? user) {
|
||||
_sentryUser = user;
|
||||
@@ -40,6 +43,10 @@ class SentryEcosystem {
|
||||
_applyUser();
|
||||
|
||||
await _cacheData(sentryConfig, _sentryUser);
|
||||
|
||||
if (PlatformInfo.isIOS) {
|
||||
await _saveSentryConfigToKeychain(sentryConfig);
|
||||
}
|
||||
}
|
||||
|
||||
void _applyUser() {
|
||||
@@ -64,4 +71,8 @@ class SentryEcosystem {
|
||||
await _cacheManager!.clearSentryConfiguration().catchError((_) {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveSentryConfigToKeychain(SentryConfig sentryConfig) async {
|
||||
await _iosSharingManager?.saveSentryConfigToKeychain(sentryConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:core/utils/sentry/sentry_config.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||
import 'package:model/extensions/account_id_extensions.dart';
|
||||
@@ -11,11 +12,16 @@ class KeychainSharingManager {
|
||||
|
||||
KeychainSharingManager(this._secureStorage);
|
||||
|
||||
Future<void> save(KeychainSharingSession keychainSharingSession) => _secureStorage.write(
|
||||
Future<void> saveSharingSession(KeychainSharingSession keychainSharingSession) => _secureStorage.write(
|
||||
key: keychainSharingSession.accountId.asString,
|
||||
value: jsonEncode(keychainSharingSession.toJson()),
|
||||
);
|
||||
|
||||
Future<void> saveSentryConfig(SentryConfig sentryConfig) => _secureStorage.write(
|
||||
key: SentryConfig.sentryConfigKeyChain,
|
||||
value: jsonEncode(sentryConfig.toJson()),
|
||||
);
|
||||
|
||||
Future<bool> isSessionExist(AccountId accountId) =>
|
||||
_secureStorage.containsKey(key: accountId.asString);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/sentry/sentry_config.dart';
|
||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||
import 'package:jmap_dart_client/jmap/core/user_name.dart';
|
||||
import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart';
|
||||
@@ -103,7 +104,7 @@ class IOSSharingManager {
|
||||
isTWP: tokenRecords?.isTWP ?? false,
|
||||
);
|
||||
|
||||
await _keychainSharingManager.save(keychainSharingSession);
|
||||
await _keychainSharingManager.saveSharingSession(keychainSharingSession);
|
||||
|
||||
log('IOSSharingManager::_saveKeyChainSharingSession: COMPLETED');
|
||||
} catch (e) {
|
||||
@@ -194,7 +195,7 @@ class IOSSharingManager {
|
||||
return;
|
||||
}
|
||||
final newKeychain = keychainSharingStored.updating(emailState: newEmailState);
|
||||
await _keychainSharingManager.save(newKeychain);
|
||||
await _keychainSharingManager.saveSharingSession(newKeychain);
|
||||
}
|
||||
|
||||
Future<bool> isExistMailboxIdsBlockNotificationInKeyChain(AccountId accountId) async {
|
||||
@@ -217,7 +218,7 @@ class IOSSharingManager {
|
||||
return;
|
||||
}
|
||||
final newKeychain = keychainSharingStored.updating(mailboxIdsBlockNotification: mailboxIds);
|
||||
await _keychainSharingManager.save(newKeychain);
|
||||
await _keychainSharingManager.saveSharingSession(newKeychain);
|
||||
} catch (e) {
|
||||
logWarning('IOSSharingManager::updateMailboxIdsBlockNotificationInKeyChain: Exception = $e');
|
||||
}
|
||||
@@ -240,4 +241,14 @@ class IOSSharingManager {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveSentryConfigToKeychain(SentryConfig sentryConfig) async {
|
||||
log('IOSSharingManager::saveSentryConfigToKeychain: START');
|
||||
try {
|
||||
await _keychainSharingManager.saveSentryConfig(sentryConfig);
|
||||
log('IOSSharingManager::saveSentryConfigToKeychain: COMPLETED');
|
||||
} catch (e) {
|
||||
logWarning('IOSSharingManager::saveSentryConfigToKeychain: Exception: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:core/utils/sentry/sentry_config.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jmap_dart_client/jmap/account_id.dart';
|
||||
@@ -13,6 +14,59 @@ void main() {
|
||||
late KeychainSharingManager keychainSharingManager;
|
||||
late FlutterSecureStorage flutterSecureStorage;
|
||||
|
||||
group('KeychainSharingManager:saveSentryConfig', () {
|
||||
setUp(() {
|
||||
flutterSecureStorage = const FlutterSecureStorage();
|
||||
keychainSharingManager = KeychainSharingManager(flutterSecureStorage);
|
||||
FlutterSecureStorage.setMockInitialValues({});
|
||||
});
|
||||
|
||||
test('WHEN saveSentryConfig called \n'
|
||||
'THEN stores JSON under sentryConfigKeyChain key', () async {
|
||||
final config = SentryConfig(
|
||||
dsn: 'https://test@sentry.io/123',
|
||||
environment: 'production',
|
||||
release: '1.0.0',
|
||||
);
|
||||
|
||||
await keychainSharingManager.saveSentryConfig(config);
|
||||
|
||||
final stored = await flutterSecureStorage.read(
|
||||
key: SentryConfig.sentryConfigKeyChain,
|
||||
);
|
||||
expect(stored, isNotNull);
|
||||
final decoded = jsonDecode(stored!) as Map<String, dynamic>;
|
||||
expect(decoded['dsn'], equals('https://test@sentry.io/123'));
|
||||
expect(decoded['environment'], equals('production'));
|
||||
expect(decoded['release'], equals('1.0.0'));
|
||||
});
|
||||
|
||||
test('WHEN SentryConfig has no dist \n'
|
||||
'THEN toJson does not include dist key', () {
|
||||
final config = SentryConfig(
|
||||
dsn: 'https://test@sentry.io/123',
|
||||
environment: 'staging',
|
||||
release: '1.0.0',
|
||||
);
|
||||
|
||||
final json = config.toJson();
|
||||
expect(json.containsKey('dist'), isFalse);
|
||||
});
|
||||
|
||||
test('WHEN SentryConfig has dist \n'
|
||||
'THEN toJson includes dist key', () {
|
||||
final config = SentryConfig(
|
||||
dsn: 'https://test@sentry.io/123',
|
||||
environment: 'staging',
|
||||
release: '1.0.0',
|
||||
dist: 'abc123',
|
||||
);
|
||||
|
||||
final json = config.toJson();
|
||||
expect(json['dist'], equals('abc123'));
|
||||
});
|
||||
});
|
||||
|
||||
group('KeychainSharingManager:save for the same accountId', () {
|
||||
setUp(() {
|
||||
flutterSecureStorage = const FlutterSecureStorage();
|
||||
@@ -36,7 +90,7 @@ void main() {
|
||||
});
|
||||
|
||||
// act
|
||||
await keychainSharingManager.save(keychainSharingSession);
|
||||
await keychainSharingManager.saveSharingSession(keychainSharingSession);
|
||||
|
||||
// assert
|
||||
final keychainSession = await keychainSharingManager.getSharingSession(AccountId(Id(
|
||||
@@ -79,7 +133,7 @@ void main() {
|
||||
);
|
||||
|
||||
// act
|
||||
await keychainSharingManager.save(keychainSharingSession2);
|
||||
await keychainSharingManager.saveSharingSession(keychainSharingSession2);
|
||||
|
||||
// assert
|
||||
final keychainSession = await keychainSharingManager.getSharingSession(AccountId(Id(
|
||||
|
||||
Reference in New Issue
Block a user