Sentry integration
Changelog-Added: Added Sentry error and diagnostics reporting to aid future quality improvements Closes: #3731 Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -7,3 +7,5 @@ TODO.bak
|
|||||||
tags
|
tags
|
||||||
build-git-hash.txt
|
build-git-hash.txt
|
||||||
.build
|
.build
|
||||||
|
|
||||||
|
.sentryclirc
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"Sentry": {
|
||||||
|
"url": "https://mcp.sentry.dev/mcp/damus-nostr-inc/apple-ios"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import UserNotifications
|
|||||||
import Foundation
|
import Foundation
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
import Intents
|
import Intents
|
||||||
|
import Sentry
|
||||||
|
|
||||||
class NotificationService: UNNotificationServiceExtension {
|
class NotificationService: UNNotificationServiceExtension {
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||||
|
DamusSentry.startIfEnabled()
|
||||||
configureKingfisherCache()
|
configureKingfisherCache()
|
||||||
|
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
@@ -117,6 +119,12 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
improvedContent.attachments = [attachment]
|
improvedContent.attachments = [attachment]
|
||||||
} catch {
|
} catch {
|
||||||
Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
|
Log.error("failed to get notification attachment: %s", for: .push_notifications, error.localizedDescription)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "fetch_notification_attachment",
|
||||||
|
"image_url": sender_profile.picture.absoluteString
|
||||||
|
], key: "notification_service")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let kind = nostr_event.known_kind
|
let kind = nostr_event.known_kind
|
||||||
@@ -146,6 +154,12 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
contentHandler(updated)
|
contentHandler(updated)
|
||||||
} catch {
|
} catch {
|
||||||
Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
|
Log.error("failed to donate interaction: %s", for: .push_notifications, error.localizedDescription)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "donate_notification_interaction",
|
||||||
|
"note_kind": nostr_event.kind
|
||||||
|
], key: "notification_service")
|
||||||
|
}
|
||||||
contentHandler(improvedContent)
|
contentHandler(improvedContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,44 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<array/>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePerformanceData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -12,6 +49,15 @@
|
|||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>1C8F.1</string>
|
<string>1C8F.1</string>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>35F9.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
+47
-1
@@ -3,7 +3,44 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyCollectedDataTypes</key>
|
<key>NSPrivacyCollectedDataTypes</key>
|
||||||
<array/>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeCrashData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypePerformanceData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyCollectedDataType</key>
|
||||||
|
<string>NSPrivacyCollectedDataTypeOtherDiagnosticData</string>
|
||||||
|
<key>NSPrivacyCollectedDataTypeLinked</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypeTracking</key>
|
||||||
|
<false/>
|
||||||
|
<key>NSPrivacyCollectedDataTypePurposes</key>
|
||||||
|
<array>
|
||||||
|
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>NSPrivacyAccessedAPITypes</key>
|
<key>NSPrivacyAccessedAPITypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -12,6 +49,15 @@
|
|||||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
<array>
|
<array>
|
||||||
<string>1C8F.1</string>
|
<string>1C8F.1</string>
|
||||||
|
<string>CA92.1</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
|
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||||
|
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||||
|
<array>
|
||||||
|
<string>35F9.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if ! command -v sentry-cli >/dev/null 2>&1; then
|
||||||
|
echo "Installing sentry-cli on Xcode Cloud builder"
|
||||||
|
SENTRY_CLI_VERSION="3.4.2"
|
||||||
|
export SENTRY_CLI_VERSION
|
||||||
|
curl -fsSL https://sentry.io/get-cli/ | SENTRY_CLI_VERSION="$SENTRY_CLI_VERSION" sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v sentry-cli >/dev/null 2>&1; then
|
||||||
|
echo "warning: sentry-cli not installed, skipping Sentry dSYM upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${CI_ARCHIVE_PATH:-}" ]; then
|
||||||
|
echo "warning: CI_ARCHIVE_PATH is not set, skipping Sentry dSYM upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
DSYM_PATH="${CI_ARCHIVE_PATH}/dSYMs"
|
||||||
|
if [ ! -d "${DSYM_PATH}" ]; then
|
||||||
|
echo "warning: No dSYMs directory found at ${DSYM_PATH}, skipping Sentry dSYM upload"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
export SENTRY_ORG=damus-nostr-inc
|
||||||
|
export SENTRY_PROJECT=apple-ios
|
||||||
|
|
||||||
|
if ! sentry-cli debug-files upload --include-sources "${DSYM_PATH}" >/dev/null 2>&1; then
|
||||||
|
echo "warning: sentry-cli failed to upload dSYMs from ${DSYM_PATH}"
|
||||||
|
fi
|
||||||
@@ -1093,6 +1093,7 @@
|
|||||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
|
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
|
||||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
|
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
|
||||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||||
|
ABE98D51D7DF44DAAD333D5E /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 5A958478DCE24724BA3B6460 /* Sentry */; };
|
||||||
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
|
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
|
||||||
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
|
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
|
||||||
B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */; };
|
B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */; };
|
||||||
@@ -1738,6 +1739,7 @@
|
|||||||
D77DA2CE2F1C2596000B7093 /* SubscriptionManagerNegentropyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DA2CD2F1C2596000B7093 /* SubscriptionManagerNegentropyTests.swift */; };
|
D77DA2CE2F1C2596000B7093 /* SubscriptionManagerNegentropyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DA2CD2F1C2596000B7093 /* SubscriptionManagerNegentropyTests.swift */; };
|
||||||
D77DA2D42F1EEB47000B7093 /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
|
D77DA2D42F1EEB47000B7093 /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
|
||||||
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
|
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
|
||||||
|
D7840B782FA28C6500221E4D /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 5A958478DCE24724BA3B6460 /* Sentry */; };
|
||||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
|
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
|
||||||
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; };
|
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; };
|
||||||
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
|
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
|
||||||
@@ -1788,6 +1790,18 @@
|
|||||||
D798D22C2B086C7400234419 /* NostrEvent+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D798D22B2B086C7400234419 /* NostrEvent+.swift */; };
|
D798D22C2B086C7400234419 /* NostrEvent+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D798D22B2B086C7400234419 /* NostrEvent+.swift */; };
|
||||||
D798D22D2B086DC400234419 /* NostrEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB228049D640006080F /* NostrEvent.swift */; };
|
D798D22D2B086DC400234419 /* NostrEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB228049D640006080F /* NostrEvent.swift */; };
|
||||||
D798D22E2B086E4800234419 /* NostrResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB028049D510006080F /* NostrResponse.swift */; };
|
D798D22E2B086E4800234419 /* NostrResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB028049D510006080F /* NostrResponse.swift */; };
|
||||||
|
D79C07402FA579DE00BD4BFF /* DamusSentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C073F2FA579D600BD4BFF /* DamusSentry.swift */; };
|
||||||
|
D79C07412FA579DE00BD4BFF /* DamusSentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C073F2FA579D600BD4BFF /* DamusSentry.swift */; };
|
||||||
|
D79C07422FA579DE00BD4BFF /* DamusSentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C073F2FA579D600BD4BFF /* DamusSentry.swift */; };
|
||||||
|
D79C07442FA57BB700BD4BFF /* SensitiveDataScrubbingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07432FA57BB200BD4BFF /* SensitiveDataScrubbingTests.swift */; };
|
||||||
|
D79C07462FA69DDB00BD4BFF /* PrivacySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07452FA69DDB00BD4BFF /* PrivacySettingsView.swift */; };
|
||||||
|
D79C07472FA69DDB00BD4BFF /* PrivacySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07452FA69DDB00BD4BFF /* PrivacySettingsView.swift */; };
|
||||||
|
D79C07482FA69DDB00BD4BFF /* PrivacySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07452FA69DDB00BD4BFF /* PrivacySettingsView.swift */; };
|
||||||
|
D79C074A2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07492FA6A5AE00BD4BFF /* GlobalSettingsStore.swift */; };
|
||||||
|
D79C074B2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07492FA6A5AE00BD4BFF /* GlobalSettingsStore.swift */; };
|
||||||
|
D79C074C2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07492FA6A5AE00BD4BFF /* GlobalSettingsStore.swift */; };
|
||||||
|
D79C074D2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C07492FA6A5AE00BD4BFF /* GlobalSettingsStore.swift */; };
|
||||||
|
D79C074E2FA9252600BD4BFF /* DamusSentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C073F2FA579D600BD4BFF /* DamusSentry.swift */; };
|
||||||
D79C4C172AFEB061003A41B4 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C4C162AFEB061003A41B4 /* NotificationService.swift */; };
|
D79C4C172AFEB061003A41B4 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79C4C162AFEB061003A41B4 /* NotificationService.swift */; };
|
||||||
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */; };
|
D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */; };
|
||||||
@@ -2900,6 +2914,10 @@
|
|||||||
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
||||||
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
||||||
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
||||||
|
D79C073F2FA579D600BD4BFF /* DamusSentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusSentry.swift; sourceTree = "<group>"; };
|
||||||
|
D79C07432FA57BB200BD4BFF /* SensitiveDataScrubbingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensitiveDataScrubbingTests.swift; sourceTree = "<group>"; };
|
||||||
|
D79C07452FA69DDB00BD4BFF /* PrivacySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsView.swift; sourceTree = "<group>"; };
|
||||||
|
D79C07492FA6A5AE00BD4BFF /* GlobalSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSettingsStore.swift; sourceTree = "<group>"; };
|
||||||
D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DamusNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DamusNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||||
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@@ -2982,6 +3000,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D7840B782FA28C6500221E4D /* Sentry in Frameworks */,
|
||||||
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */,
|
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */,
|
||||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||||
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
|
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
|
||||||
@@ -3056,6 +3075,7 @@
|
|||||||
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
||||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
||||||
D7E5B2DF2EA0A68600CF47AC /* Negentropy in Frameworks */,
|
D7E5B2DF2EA0A68600CF47AC /* Negentropy in Frameworks */,
|
||||||
|
ABE98D51D7DF44DAAD333D5E /* Sentry in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -3934,6 +3954,7 @@
|
|||||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D79C07432FA57BB200BD4BFF /* SensitiveDataScrubbingTests.swift */,
|
||||||
D7E14B922F68B22D00BF84B7 /* NdbCompactionTests.swift */,
|
D7E14B922F68B22D00BF84B7 /* NdbCompactionTests.swift */,
|
||||||
D774A5CB2F4F9679006A4D64 /* StorageStatsManagerTests.swift */,
|
D774A5CB2F4F9679006A4D64 /* StorageStatsManagerTests.swift */,
|
||||||
D77A96BE2F3131BE00CC3246 /* RelayHintsTests.swift */,
|
D77A96BE2F3131BE00CC3246 /* RelayHintsTests.swift */,
|
||||||
@@ -4559,6 +4580,7 @@
|
|||||||
5C78A7912E3036DA00CF177D /* Views */ = {
|
5C78A7912E3036DA00CF177D /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D79C07452FA69DDB00BD4BFF /* PrivacySettingsView.swift */,
|
||||||
D7B62B992F4CE59B001EE26F /* NostrDBDetailView.swift */,
|
D7B62B992F4CE59B001EE26F /* NostrDBDetailView.swift */,
|
||||||
D78778252F49478200DA73E4 /* StorageSettingsView.swift */,
|
D78778252F49478200DA73E4 /* StorageSettingsView.swift */,
|
||||||
4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */,
|
4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */,
|
||||||
@@ -4646,6 +4668,7 @@
|
|||||||
5C78A7992E303C1400CF177D /* Models */ = {
|
5C78A7992E303C1400CF177D /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D79C07492FA6A5AE00BD4BFF /* GlobalSettingsStore.swift */,
|
||||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
|
BA693073295D649800ADDB87 /* UserSettingsStore.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
@@ -4948,6 +4971,7 @@
|
|||||||
5C78A7B82E3047DE00CF177D /* Utilities */ = {
|
5C78A7B82E3047DE00CF177D /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D79C073F2FA579D600BD4BFF /* DamusSentry.swift */,
|
||||||
D77DA2C72F19D452000B7093 /* NegentropyUtilities.swift */,
|
D77DA2C72F19D452000B7093 /* NegentropyUtilities.swift */,
|
||||||
D77DA2C32F19CA40000B7093 /* AsyncStreamUtilities.swift */,
|
D77DA2C32F19CA40000B7093 /* AsyncStreamUtilities.swift */,
|
||||||
D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */,
|
D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */,
|
||||||
@@ -5523,6 +5547,7 @@
|
|||||||
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
|
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
|
||||||
3ACF94372DA9A52F00971A4E /* FaviconFinder */,
|
3ACF94372DA9A52F00971A4E /* FaviconFinder */,
|
||||||
D7E5B2DA2EA080BE00CF47AC /* Negentropy */,
|
D7E5B2DA2EA080BE00CF47AC /* Negentropy */,
|
||||||
|
5A958478DCE24724BA3B6460 /* Sentry */,
|
||||||
);
|
);
|
||||||
productName = damus;
|
productName = damus;
|
||||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||||
@@ -5740,6 +5765,7 @@
|
|||||||
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */,
|
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */,
|
||||||
D7E5B2D92EA080BE00CF47AC /* XCRemoteSwiftPackageReference "negentropy-swift" */,
|
D7E5B2D92EA080BE00CF47AC /* XCRemoteSwiftPackageReference "negentropy-swift" */,
|
||||||
D74723EA2F15B0C3002DA12A /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */,
|
D74723EA2F15B0C3002DA12A /* XCRemoteSwiftPackageReference "nostr-sdk-swift" */,
|
||||||
|
B1B8FE6C91C1401D965BD8DA /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -5847,6 +5873,7 @@
|
|||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "git rev-parse --short HEAD > \"${SRCROOT}/build-git-hash.txt\"\n";
|
shellScript = "git rev-parse --short HEAD > \"${SRCROOT}/build-git-hash.txt\"\n";
|
||||||
};
|
};
|
||||||
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -5955,6 +5982,7 @@
|
|||||||
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
|
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
|
||||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||||
4C15C7152A55DE7A00D0A0DB /* ReactionsSettingsView.swift in Sources */,
|
4C15C7152A55DE7A00D0A0DB /* ReactionsSettingsView.swift in Sources */,
|
||||||
|
D79C074D2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */,
|
||||||
4C32B94C2A9AD44700DC3548 /* FbConstants.swift in Sources */,
|
4C32B94C2A9AD44700DC3548 /* FbConstants.swift in Sources */,
|
||||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
|
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
|
||||||
4C687C272A6039500092C550 /* TestData.swift in Sources */,
|
4C687C272A6039500092C550 /* TestData.swift in Sources */,
|
||||||
@@ -6115,11 +6143,13 @@
|
|||||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
||||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
||||||
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */,
|
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */,
|
||||||
|
D79C07472FA69DDB00BD4BFF /* PrivacySettingsView.swift in Sources */,
|
||||||
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */,
|
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */,
|
||||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */,
|
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */,
|
||||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
|
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
|
||||||
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */,
|
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */,
|
||||||
|
D79C07412FA579DE00BD4BFF /* DamusSentry.swift in Sources */,
|
||||||
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */,
|
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */,
|
||||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
||||||
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
|
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
|
||||||
@@ -6464,6 +6494,7 @@
|
|||||||
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
|
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
|
||||||
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */,
|
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */,
|
||||||
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
||||||
|
D79C07442FA57BB700BD4BFF /* SensitiveDataScrubbingTests.swift in Sources */,
|
||||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
||||||
D774A5CC2F4F9679006A4D64 /* StorageStatsManagerTests.swift in Sources */,
|
D774A5CC2F4F9679006A4D64 /* StorageStatsManagerTests.swift in Sources */,
|
||||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||||
@@ -6558,6 +6589,7 @@
|
|||||||
82D6FAB52CD99F7900C925F4 /* NativeObject.swift in Sources */,
|
82D6FAB52CD99F7900C925F4 /* NativeObject.swift in Sources */,
|
||||||
82D6FAB62CD99F7900C925F4 /* String+extension.swift in Sources */,
|
82D6FAB62CD99F7900C925F4 /* String+extension.swift in Sources */,
|
||||||
3A515C552DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */,
|
3A515C552DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */,
|
||||||
|
D79C07422FA579DE00BD4BFF /* DamusSentry.swift in Sources */,
|
||||||
82D6FAB72CD99F7900C925F4 /* FlatBufferObject.swift in Sources */,
|
82D6FAB72CD99F7900C925F4 /* FlatBufferObject.swift in Sources */,
|
||||||
82D6FAB82CD99F7900C925F4 /* Enum.swift in Sources */,
|
82D6FAB82CD99F7900C925F4 /* Enum.swift in Sources */,
|
||||||
82D6FAB92CD99F7900C925F4 /* builder.c in Sources */,
|
82D6FAB92CD99F7900C925F4 /* builder.c in Sources */,
|
||||||
@@ -6754,6 +6786,7 @@
|
|||||||
82D6FB672CD99F7900C925F4 /* Zaps+.swift in Sources */,
|
82D6FB672CD99F7900C925F4 /* Zaps+.swift in Sources */,
|
||||||
D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||||
82D6FB682CD99F7900C925F4 /* WalletConnect+.swift in Sources */,
|
82D6FB682CD99F7900C925F4 /* WalletConnect+.swift in Sources */,
|
||||||
|
D79C07482FA69DDB00BD4BFF /* PrivacySettingsView.swift in Sources */,
|
||||||
82D6FB692CD99F7900C925F4 /* DamusPurpleNotificationManagement.swift in Sources */,
|
82D6FB692CD99F7900C925F4 /* DamusPurpleNotificationManagement.swift in Sources */,
|
||||||
82D6FB6A2CD99F7900C925F4 /* DamusPurple.swift in Sources */,
|
82D6FB6A2CD99F7900C925F4 /* DamusPurple.swift in Sources */,
|
||||||
82D6FB6B2CD99F7900C925F4 /* StoreObserver.swift in Sources */,
|
82D6FB6B2CD99F7900C925F4 /* StoreObserver.swift in Sources */,
|
||||||
@@ -6987,6 +7020,7 @@
|
|||||||
82D6FC312CD99F7900C925F4 /* ProxyView.swift in Sources */,
|
82D6FC312CD99F7900C925F4 /* ProxyView.swift in Sources */,
|
||||||
82D6FC322CD99F7900C925F4 /* SelectedEventView.swift in Sources */,
|
82D6FC322CD99F7900C925F4 /* SelectedEventView.swift in Sources */,
|
||||||
5C8F97352EB46145009399B1 /* LiveStreamView.swift in Sources */,
|
5C8F97352EB46145009399B1 /* LiveStreamView.swift in Sources */,
|
||||||
|
D79C074A2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */,
|
||||||
82D6FC332CD99F7900C925F4 /* EventBody.swift in Sources */,
|
82D6FC332CD99F7900C925F4 /* EventBody.swift in Sources */,
|
||||||
D5C1AFC02E5DF7E60092F72F /* ContactCardManager.swift in Sources */,
|
D5C1AFC02E5DF7E60092F72F /* ContactCardManager.swift in Sources */,
|
||||||
82D6FC342CD99F7900C925F4 /* BuilderEventView.swift in Sources */,
|
82D6FC342CD99F7900C925F4 /* BuilderEventView.swift in Sources */,
|
||||||
@@ -7334,6 +7368,7 @@
|
|||||||
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */,
|
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */,
|
||||||
D73E5ED82C6A97F4007EB227 /* DamusVideoPlayerView.swift in Sources */,
|
D73E5ED82C6A97F4007EB227 /* DamusVideoPlayerView.swift in Sources */,
|
||||||
D73E5ED92C6A97F4007EB227 /* DamusVideoPlayer.swift in Sources */,
|
D73E5ED92C6A97F4007EB227 /* DamusVideoPlayer.swift in Sources */,
|
||||||
|
D79C07462FA69DDB00BD4BFF /* PrivacySettingsView.swift in Sources */,
|
||||||
D73E5EDA2C6A97F4007EB227 /* DamusVideoCoordinator.swift in Sources */,
|
D73E5EDA2C6A97F4007EB227 /* DamusVideoCoordinator.swift in Sources */,
|
||||||
D73E5EDC2C6A97F4007EB227 /* ReactionsSettingsView.swift in Sources */,
|
D73E5EDC2C6A97F4007EB227 /* ReactionsSettingsView.swift in Sources */,
|
||||||
D73E5EDD2C6A97F4007EB227 /* NotificationSettingsView.swift in Sources */,
|
D73E5EDD2C6A97F4007EB227 /* NotificationSettingsView.swift in Sources */,
|
||||||
@@ -7560,6 +7595,7 @@
|
|||||||
D703D7A92C670E5A00A400EA /* refmap.c in Sources */,
|
D703D7A92C670E5A00A400EA /* refmap.c in Sources */,
|
||||||
D73C7EDC2DE51699001F9392 /* OnboardingContentSettings.swift in Sources */,
|
D73C7EDC2DE51699001F9392 /* OnboardingContentSettings.swift in Sources */,
|
||||||
5CFDE6F72EF4F93D004E8661 /* Secrets.swift in Sources */,
|
5CFDE6F72EF4F93D004E8661 /* Secrets.swift in Sources */,
|
||||||
|
D79C07402FA579DE00BD4BFF /* DamusSentry.swift in Sources */,
|
||||||
D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */,
|
D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */,
|
||||||
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
|
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
|
||||||
D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */,
|
D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */,
|
||||||
@@ -7618,6 +7654,7 @@
|
|||||||
D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */,
|
D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */,
|
||||||
5CB645AA2EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */,
|
5CB645AA2EAC01430018BD91 /* DamusLabsExperiments.swift in Sources */,
|
||||||
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */,
|
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */,
|
||||||
|
D79C074C2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */,
|
||||||
D73E5F852C6AA628007EB227 /* LoadScript.swift in Sources */,
|
D73E5F852C6AA628007EB227 /* LoadScript.swift in Sources */,
|
||||||
D703D74E2C6709DA00A400EA /* Pubkey.swift in Sources */,
|
D703D74E2C6709DA00A400EA /* Pubkey.swift in Sources */,
|
||||||
D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||||
@@ -7741,10 +7778,12 @@
|
|||||||
D7CCFC162B05894300323D86 /* Pubkey.swift in Sources */,
|
D7CCFC162B05894300323D86 /* Pubkey.swift in Sources */,
|
||||||
D7EDED2C2B128CFA0018B19C /* DamusColors.swift in Sources */,
|
D7EDED2C2B128CFA0018B19C /* DamusColors.swift in Sources */,
|
||||||
D74AAFC52B1538DF006CF0F4 /* NotificationExtensionState.swift in Sources */,
|
D74AAFC52B1538DF006CF0F4 /* NotificationExtensionState.swift in Sources */,
|
||||||
|
D79C074E2FA9252600BD4BFF /* DamusSentry.swift in Sources */,
|
||||||
D798D2292B08686C00234419 /* ContentParsing.swift in Sources */,
|
D798D2292B08686C00234419 /* ContentParsing.swift in Sources */,
|
||||||
D798D2242B0859C900234419 /* LocalizationUtil.swift in Sources */,
|
D798D2242B0859C900234419 /* LocalizationUtil.swift in Sources */,
|
||||||
D7CE1B322B0BE6C3002EDAD4 /* NdbTxn.swift in Sources */,
|
D7CE1B322B0BE6C3002EDAD4 /* NdbTxn.swift in Sources */,
|
||||||
D7CE1B372B0BE719002EDAD4 /* Verifier.swift in Sources */,
|
D7CE1B372B0BE719002EDAD4 /* Verifier.swift in Sources */,
|
||||||
|
D79C074B2FA6A5AE00BD4BFF /* GlobalSettingsStore.swift in Sources */,
|
||||||
D74AAFC82B155C9D006CF0F4 /* InsertSort.swift in Sources */,
|
D74AAFC82B155C9D006CF0F4 /* InsertSort.swift in Sources */,
|
||||||
D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */,
|
D723411A2B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */,
|
||||||
D7EDED292B1182060018B19C /* AttachMediaUtility.swift in Sources */,
|
D7EDED292B1182060018B19C /* AttachMediaUtility.swift in Sources */,
|
||||||
@@ -8131,6 +8170,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -8185,6 +8225,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -8609,6 +8650,16 @@
|
|||||||
minimumVersion = 0.2.26;
|
minimumVersion = 0.2.26;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
B1B8FE6C91C1401D965BD8DA /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/getsentry/sentry-cocoa/";
|
||||||
|
requirement = {
|
||||||
|
kind = exactVersion;
|
||||||
|
version = 9.11.0;
|
||||||
|
};
|
||||||
|
traits = (
|
||||||
|
);
|
||||||
|
};
|
||||||
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
|
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/twostraws/CodeScanner.git";
|
repositoryURL = "https://github.com/twostraws/CodeScanner.git";
|
||||||
@@ -8708,6 +8759,11 @@
|
|||||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||||
productName = secp256k1;
|
productName = secp256k1;
|
||||||
};
|
};
|
||||||
|
5A958478DCE24724BA3B6460 /* Sentry */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = B1B8FE6C91C1401D965BD8DA /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
|
||||||
|
productName = Sentry;
|
||||||
|
};
|
||||||
82D6FC802CD99FC500C925F4 /* secp256k1 */ = {
|
82D6FC802CD99FC500C925F4 /* secp256k1 */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "c718c1e7dcc1a07671694b2d7d7311e11804fbbaf22f4b81e49523a3df816ad6",
|
"originHash" : "69116d3b516f6eb000e56fb88ccfca91c324778f3b97dfa2704e221da07b1ca2",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "codescanner",
|
"identity" : "codescanner",
|
||||||
@@ -88,6 +88,15 @@
|
|||||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sentry-cocoa",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/getsentry/sentry-cocoa/",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "a49e7c2148ac9e38bd35ef4f13bc9d6ea3ff0b81",
|
||||||
|
"version" : "9.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-collections",
|
"identity" : "swift-collections",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import os
|
import os
|
||||||
import Negentropy
|
import Negentropy
|
||||||
|
import Sentry
|
||||||
|
|
||||||
|
|
||||||
extension NostrNetworkManager {
|
extension NostrNetworkManager {
|
||||||
@@ -272,6 +273,13 @@ extension NostrNetworkManager {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Self.logger.error("Network subscription \(id.uuidString, privacy: .public): Streaming error: \(error.localizedDescription, privacy: .public)")
|
Self.logger.error("Network subscription \(id.uuidString, privacy: .public): Streaming error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "network_subscription_stream",
|
||||||
|
"subscription_id": id.uuidString,
|
||||||
|
"filter_count": filters.count
|
||||||
|
], key: "network")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Network streaming ended")
|
Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Network streaming ended")
|
||||||
continuation.finish()
|
continuation.finish()
|
||||||
@@ -308,6 +316,13 @@ extension NostrNetworkManager {
|
|||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
Self.logger.error("Network subscription \(id.uuidString, privacy: .public): Streaming error: \(error.localizedDescription, privacy: .public)")
|
Self.logger.error("Network subscription \(id.uuidString, privacy: .public): Streaming error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "negentropy_subscription_stream",
|
||||||
|
"subscription_id": id.uuidString,
|
||||||
|
"filter_count": filters.count
|
||||||
|
], key: "network")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Network
|
import Network
|
||||||
import Negentropy
|
import Negentropy
|
||||||
|
import Sentry
|
||||||
|
|
||||||
struct RelayHandler {
|
struct RelayHandler {
|
||||||
let sub_id: String
|
let sub_id: String
|
||||||
@@ -313,6 +314,12 @@ class RelayPool {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
print("[RelayPool] Failed to add relay \(url.absoluteString): \(error)")
|
print("[RelayPool] Failed to add relay \(url.absoluteString): \(error)")
|
||||||
#endif
|
#endif
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "add_ephemeral_relay",
|
||||||
|
"relay_url": url.absoluteString
|
||||||
|
], key: "relay_pool")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -886,6 +893,12 @@ class RelayPool {
|
|||||||
Log.error("Error while negentropy streaming: %s", for: .networking, error.localizedDescription)
|
Log.error("Error while negentropy streaming: %s", for: .networking, error.localizedDescription)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "negentropy_sync_multi_relay",
|
||||||
|
"ignore_unsupported": ignoreUnsupportedRelays
|
||||||
|
], key: "relay_pool")
|
||||||
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import Sentry
|
||||||
|
|
||||||
/// Manages periodic snapshots of the main NostrDB database to a shared container location.
|
/// Manages periodic snapshots of the main NostrDB database to a shared container location.
|
||||||
///
|
///
|
||||||
@@ -155,6 +156,12 @@ actor DatabaseSnapshotManager {
|
|||||||
do {
|
do {
|
||||||
try fileManager.createDirectory(atPath: tempSnapshotPath.path, withIntermediateDirectories: true)
|
try fileManager.createDirectory(atPath: tempSnapshotPath.path, withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "create_temp_snapshot_directory",
|
||||||
|
"path": tempSnapshotPath.path
|
||||||
|
], key: "snapshot")
|
||||||
|
}
|
||||||
throw SnapshotError.directoryCreationFailed(error)
|
throw SnapshotError.directoryCreationFailed(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +172,12 @@ actor DatabaseSnapshotManager {
|
|||||||
try fileManager.removeItem(atPath: tempSnapshotPath.path)
|
try fileManager.removeItem(atPath: tempSnapshotPath.path)
|
||||||
} catch {
|
} catch {
|
||||||
Log.error("Failed to cleanup temporary snapshot directory: %{public}@", for: .storage, error.localizedDescription)
|
Log.error("Failed to cleanup temporary snapshot directory: %{public}@", for: .storage, error.localizedDescription)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "cleanup_temp_snapshot_directory",
|
||||||
|
"path": tempSnapshotPath.path
|
||||||
|
], key: "snapshot")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +186,14 @@ actor DatabaseSnapshotManager {
|
|||||||
|
|
||||||
// Create a new Ndb instance in the temporary directory
|
// Create a new Ndb instance in the temporary directory
|
||||||
guard let snapshotNdb = Ndb(path: tempSnapshotPath.path, owns_db_file: true) else {
|
guard let snapshotNdb = Ndb(path: tempSnapshotPath.path, owns_db_file: true) else {
|
||||||
throw SnapshotError.failedToCreateSnapshotDatabase
|
let error = SnapshotError.failedToCreateSnapshotDatabase
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "create_snapshot_ndb",
|
||||||
|
"path": tempSnapshotPath.path
|
||||||
|
], key: "snapshot")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
@@ -307,6 +327,12 @@ actor DatabaseSnapshotManager {
|
|||||||
do {
|
do {
|
||||||
try fileManager.createDirectory(atPath: parentDir, withIntermediateDirectories: true)
|
try fileManager.createDirectory(atPath: parentDir, withIntermediateDirectories: true)
|
||||||
} catch {
|
} catch {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "create_parent_directory",
|
||||||
|
"path": parentDir
|
||||||
|
], key: "snapshot")
|
||||||
|
}
|
||||||
throw SnapshotError.directoryCreationFailed(error)
|
throw SnapshotError.directoryCreationFailed(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,6 +347,14 @@ actor DatabaseSnapshotManager {
|
|||||||
|
|
||||||
Log.debug("Moved snapshot from %{public}@ to %{public}@", for: .storage, tempPath, finalPath)
|
Log.debug("Moved snapshot from %{public}@ to %{public}@", for: .storage, tempPath, finalPath)
|
||||||
} catch {
|
} catch {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "move_snapshot_to_final_destination",
|
||||||
|
"temp_path": tempPath,
|
||||||
|
"final_path": finalPath,
|
||||||
|
"file_exists": fileManager.fileExists(atPath: finalPath)
|
||||||
|
], key: "snapshot")
|
||||||
|
}
|
||||||
throw SnapshotError.moveFailed(error)
|
throw SnapshotError.moveFailed(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Sentry
|
||||||
|
|
||||||
/// A view that handles database compaction on a background thread before showing the main content.
|
/// A view that handles database compaction on a background thread before showing the main content.
|
||||||
///
|
///
|
||||||
@@ -45,6 +46,11 @@ struct CompactionView: View {
|
|||||||
|
|
||||||
@State private var compactionState: CompactionState = .idle
|
@State private var compactionState: CompactionState = .idle
|
||||||
@State private var hasStartedCompactionFlow: Bool = false
|
@State private var hasStartedCompactionFlow: Bool = false
|
||||||
|
|
||||||
|
/// The source of the compaction request (automatic scheduling or manual user action).
|
||||||
|
/// Used to determine error handling behavior: automatic compactions report to Sentry silently,
|
||||||
|
/// while manual compactions show errors to the user.
|
||||||
|
@State private var compactionSource: NdbCompactionRequestSource = .automatic
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -92,6 +98,9 @@ struct CompactionView: View {
|
|||||||
compactionState = .complete
|
compactionState = .complete
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if this is a manual or automatic compaction request
|
||||||
|
compactionSource = Ndb.get_compact_on_next_launch_source() ?? .automatic
|
||||||
|
|
||||||
let showsLargeDatabaseWarning = Ndb.db_path.map({ Ndb.is_large_database(path: $0) }) ?? false
|
let showsLargeDatabaseWarning = Ndb.db_path.map({ Ndb.is_large_database(path: $0) }) ?? false
|
||||||
compactionState = CompactionState.initialCompactingState(showsLargeDatabaseWarning: showsLargeDatabaseWarning)
|
compactionState = CompactionState.initialCompactingState(showsLargeDatabaseWarning: showsLargeDatabaseWarning)
|
||||||
@@ -116,7 +125,21 @@ struct CompactionView: View {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
compactionState = .failed(error: error.localizedDescription)
|
// Always send errors to Sentry with compaction source context
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"compaction_source": compactionSource.rawValue,
|
||||||
|
"error_description": error.localizedDescription
|
||||||
|
], key: "compaction")
|
||||||
|
}
|
||||||
|
|
||||||
|
// For automatic compaction, silently continue
|
||||||
|
// For manual compaction, show the error UI to the user
|
||||||
|
if compactionSource == .automatic {
|
||||||
|
compactionState = .complete
|
||||||
|
} else {
|
||||||
|
compactionState = .failed(error: error.localizedDescription)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Sentry
|
||||||
|
|
||||||
// Minimum threshold the hellthread pubkey tag count setting can go down to.
|
// Minimum threshold the hellthread pubkey tag count setting can go down to.
|
||||||
let HELLTHREAD_MIN_PUBKEYS: Int = 6
|
let HELLTHREAD_MIN_PUBKEYS: Int = 6
|
||||||
@@ -55,7 +56,15 @@ struct PushNotificationClient {
|
|||||||
Log.info("Sent device token to Damus push notification server successfully", for: .push_notifications)
|
Log.info("Sent device token to Damus push notification server successfully", for: .push_notifications)
|
||||||
default:
|
default:
|
||||||
Log.error("Error in sending device_token to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error in sending device_token to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
let error = ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "send_device_token",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"data_length": data.count
|
||||||
|
], key: "push_notifications")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +99,15 @@ struct PushNotificationClient {
|
|||||||
Log.info("Sent device token removal request to Damus push notification server successfully", for: .push_notifications)
|
Log.info("Sent device token removal request to Damus push notification server successfully", for: .push_notifications)
|
||||||
default:
|
default:
|
||||||
Log.error("Error in sending device_token removal to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error in sending device_token removal to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
let error = ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "revoke_device_token",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"data_length": data.count
|
||||||
|
], key: "push_notifications")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +142,15 @@ struct PushNotificationClient {
|
|||||||
Log.info("Sent notification settings to Damus push notification server successfully", for: .push_notifications)
|
Log.info("Sent notification settings to Damus push notification server successfully", for: .push_notifications)
|
||||||
default:
|
default:
|
||||||
Log.error("Error in sending notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error in sending notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
let error = ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "set_notification_settings",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"data_length": data.count
|
||||||
|
], key: "push_notifications")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +180,28 @@ struct PushNotificationClient {
|
|||||||
if let httpResponse = response as? HTTPURLResponse {
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
switch httpResponse.statusCode {
|
switch httpResponse.statusCode {
|
||||||
case 200:
|
case 200:
|
||||||
guard let notification_settings = NotificationSettings.from(json_data: data) else { throw ClientError.json_decoding_error }
|
guard let notification_settings = NotificationSettings.from(json_data: data) else {
|
||||||
|
let error = ClientError.json_decoding_error
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "get_notification_settings_decode",
|
||||||
|
"data_length": data.count
|
||||||
|
], key: "push_notifications")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return notification_settings
|
return notification_settings
|
||||||
default:
|
default:
|
||||||
Log.error("Error in getting notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error in getting notification settings to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
let error = ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "get_notification_settings",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"data_length": data.count
|
||||||
|
], key: "push_notifications")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw ClientError.could_not_process_response
|
throw ClientError.could_not_process_response
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import StoreKit
|
import StoreKit
|
||||||
|
import Sentry
|
||||||
|
|
||||||
class DamusPurple: StoreObserverDelegate {
|
class DamusPurple: StoreObserverDelegate {
|
||||||
let settings: UserSettingsStore
|
let settings: UserSettingsStore
|
||||||
@@ -151,11 +152,19 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
|
|
||||||
if let httpResponse = response as? HTTPURLResponse {
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
switch httpResponse.statusCode {
|
switch httpResponse.statusCode {
|
||||||
case 200:
|
case 200:
|
||||||
Log.info("Got user UUID from Damus Purple server", for: .damus_purple)
|
Log.info("Successfully fetched account UUID from Damus Purple server", for: .damus_purple)
|
||||||
default:
|
default:
|
||||||
Log.error("Error in getting user UUID with Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error fetching account UUID from Damus Purple server. Status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "")
|
||||||
throw PurpleError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
let error = PurpleError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "fetch_uuid_for_account",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"response": String(data: data, encoding: .utf8) ?? "Unknown"
|
||||||
|
], key: "damus_purple")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +202,15 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple)
|
Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple)
|
||||||
default:
|
default:
|
||||||
Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
throw DamusPurple.PurpleError.iap_receipt_verification_error(status: httpResponse.statusCode, response: data)
|
let error = DamusPurple.PurpleError.iap_receipt_verification_error(status: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "send_iap_receipt",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"response": String(data: data, encoding: .utf8) ?? "Unknown"
|
||||||
|
], key: "damus_purple")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,8 +236,16 @@ class DamusPurple: StoreObserverDelegate {
|
|||||||
case 200:
|
case 200:
|
||||||
Log.info("Sent transaction ID to Damus Purple server and activated successfully", for: .damus_purple)
|
Log.info("Sent transaction ID to Damus Purple server and activated successfully", for: .damus_purple)
|
||||||
default:
|
default:
|
||||||
Log.error("Error in sending or verifying transaction ID with Damus Purple server. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||||
throw DamusPurple.PurpleError.iap_receipt_verification_error(status: httpResponse.statusCode, response: data)
|
let error = DamusPurple.PurpleError.iap_receipt_verification_error(status: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "send_transaction_id",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"response": String(data: data, encoding: .utf8) ?? "Unknown"
|
||||||
|
], key: "damus_purple")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// GlobalSettingsStore.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created for device-level settings that apply globally across all accounts
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// A property wrapper for global (non-pubkey-scoped) settings.
|
||||||
|
/// Use this for device-level preferences that should apply regardless of which account is logged in.
|
||||||
|
@propertyWrapper struct GlobalSetting<T: Equatable> {
|
||||||
|
private let key: String
|
||||||
|
private var value: T
|
||||||
|
|
||||||
|
init(key: String, default_value: T) {
|
||||||
|
if T.self == Bool.self {
|
||||||
|
GlobalSettingsStore.bool_options.insert(key)
|
||||||
|
}
|
||||||
|
self.key = key
|
||||||
|
self.value = DamusUserDefaults.standard.object(forKey: key) as? T ?? default_value
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrappedValue: T {
|
||||||
|
get { return value }
|
||||||
|
set {
|
||||||
|
guard value != newValue else { return }
|
||||||
|
self.value = newValue
|
||||||
|
DamusUserDefaults.standard.set(newValue, forKey: key)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
GlobalSettingsStore.shared.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store for global (device-level) settings that apply regardless of which account is logged in.
|
||||||
|
///
|
||||||
|
/// Unlike `UserSettingsStore`, these settings are not scoped to a specific pubkey.
|
||||||
|
/// Use this for device preferences like privacy settings, accessibility options, etc.
|
||||||
|
class GlobalSettingsStore: ObservableObject {
|
||||||
|
static let shared = GlobalSettingsStore()
|
||||||
|
static var bool_options = Set<String>()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
/// Controls whether Sentry telemetry and error reporting is enabled.
|
||||||
|
/// When disabled, no data is sent to Sentry for crash reporting or diagnostics.
|
||||||
|
/// This is a global setting that applies to all accounts on this device.
|
||||||
|
@GlobalSetting(key: "enable_sentry_telemetry", default_value: false)
|
||||||
|
var enable_sentry_telemetry: Bool
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ struct ConfigView: View {
|
|||||||
// String constants
|
// String constants
|
||||||
private let DELETE_KEYWORD = "DELETE"
|
private let DELETE_KEYWORD = "DELETE"
|
||||||
private let keysTitle = NSLocalizedString("Keys", comment: "Settings section for managing keys")
|
private let keysTitle = NSLocalizedString("Keys", comment: "Settings section for managing keys")
|
||||||
|
private let privacyTitle = NSLocalizedString("Privacy", comment: "Settings section for privacy and data collection settings")
|
||||||
private let appearanceTitle = NSLocalizedString("Appearance and filters", comment: "Section header for text, appearance, and content filter settings")
|
private let appearanceTitle = NSLocalizedString("Appearance and filters", comment: "Section header for text, appearance, and content filter settings")
|
||||||
private let searchUniverseTitle = NSLocalizedString("Search / Universe", comment: "Section header for search/universe settings")
|
private let searchUniverseTitle = NSLocalizedString("Search / Universe", comment: "Section header for search/universe settings")
|
||||||
private let notificationsTitle = NSLocalizedString("Notifications", comment: "Section header for Damus notifications")
|
private let notificationsTitle = NSLocalizedString("Notifications", comment: "Section header for Damus notifications")
|
||||||
@@ -62,6 +63,12 @@ struct ConfigView: View {
|
|||||||
IconLabel(keysTitle,img_name:"Key",color:.purple)
|
IconLabel(keysTitle,img_name:"Key",color:.purple)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Privacy
|
||||||
|
if showSettingsButton(title: privacyTitle){
|
||||||
|
NavigationLink(value: Route.PrivacySettings){
|
||||||
|
IconLabel(privacyTitle, img_name: "eye-off.fill", color:.blue)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Appearance and filters
|
// Appearance and filters
|
||||||
if showSettingsButton(title: appearanceTitle){
|
if showSettingsButton(title: appearanceTitle){
|
||||||
NavigationLink(value:Route.AppearanceSettings(settings: settings)){
|
NavigationLink(value:Route.AppearanceSettings(settings: settings)){
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
//
|
||||||
|
// PrivacySettingsView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created for privacy and data collection settings
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import Sentry
|
||||||
|
|
||||||
|
struct PrivacySettingsView: View {
|
||||||
|
@ObservedObject var globalSettings = GlobalSettingsStore.shared
|
||||||
|
@State private var isShowingPrivacyDetails = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section(
|
||||||
|
header: Text("Error & Diagnostics Reporting", comment: "Section header for error and diagnostics reporting settings"),
|
||||||
|
footer: VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text("Sentry provides reporting for crashes, errors, and app health issues. When enabled, anonymous technical data is sent to help improve the app. No personal information or private keys are collected. This setting applies to the entire app on this device and is not tied to a specific account. Restart the app for changes to take effect.", comment: "Footer explaining Sentry telemetry")
|
||||||
|
Button(NSLocalizedString("Learn more", comment: "Button title to show more details about privacy and data handling")) {
|
||||||
|
isShowingPrivacyDetails = true
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Toggle(NSLocalizedString("Enable Error & Diagnostics Reporting", comment: "Setting to enable Sentry error and diagnostics reporting"), isOn: $globalSettings.enable_sentry_telemetry)
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(NSLocalizedString("Privacy", comment: "Navigation title for Privacy settings"))
|
||||||
|
.sheet(isPresented: $isShowingPrivacyDetails) {
|
||||||
|
PrivacyDataHandlingDetailsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Presents additional context about diagnostics reporting and the data shared with Sentry.
|
||||||
|
struct PrivacyDataHandlingDetailsView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
Text("When diagnostics reporting is enabled, Damus sends anonymous technical information to Sentry so crashes, errors, and app stability problems can be investigated and fixed faster.", comment: "Overview of diagnostics reporting and why it exists")
|
||||||
|
} header: {
|
||||||
|
Text("How it works", comment: "Section title describing how diagnostics reporting works")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
PrivacyInfoRow(
|
||||||
|
icon: "chart.bar.xaxis",
|
||||||
|
title: NSLocalizedString("What is shared", comment: "Title for information about data that is shared"),
|
||||||
|
description: NSLocalizedString("Crash reports, error messages, app version, iOS version, device model, and basic performance diagnostics.", comment: "Description of shared diagnostics data")
|
||||||
|
)
|
||||||
|
PrivacyInfoRow(
|
||||||
|
icon: "hand.raised.fill",
|
||||||
|
title: NSLocalizedString("What is not shared", comment: "Title for information about data that is not shared"),
|
||||||
|
description: NSLocalizedString("Private keys, seed phrases, wallet secrets, direct message contents, and profile content you create in the app.", comment: "Description of private data that is not shared")
|
||||||
|
)
|
||||||
|
PrivacyInfoRow(
|
||||||
|
icon: "switch.2",
|
||||||
|
title: NSLocalizedString("Your control", comment: "Title for information about user control over diagnostics reporting"),
|
||||||
|
description: NSLocalizedString("You can turn diagnostics reporting on or off at any time from Privacy settings. Changes apply after restarting the app. Turning it off stops future reports from being sent, but does not delete reports that were already sent.", comment: "Description of user control over diagnostics reporting")
|
||||||
|
)
|
||||||
|
} header: {
|
||||||
|
Text("Data handling details", comment: "Section title for detailed data handling information")
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
CollectedDataRow(
|
||||||
|
title: NSLocalizedString("Crash and exception details", comment: "Title for crash and exception details row"),
|
||||||
|
description: NSLocalizedString("Technical stack traces and failure context used to debug app issues.", comment: "Description for crash and exception details row"),
|
||||||
|
collected: true
|
||||||
|
)
|
||||||
|
CollectedDataRow(
|
||||||
|
title: NSLocalizedString("Device and app metadata", comment: "Title for device and app metadata row"),
|
||||||
|
description: NSLocalizedString("Information such as app version, operating system version, and device type.", comment: "Description for device and app metadata row"),
|
||||||
|
collected: true
|
||||||
|
)
|
||||||
|
CollectedDataRow(
|
||||||
|
title: NSLocalizedString("Messages, notes, and private keys", comment: "Title for sensitive data row"),
|
||||||
|
description: NSLocalizedString("Personal content and credentials are not intentionally collected for diagnostics reporting. Damus is designed not to send sensitive information such as private keys, public keys, or email addresses, and it also automatically detects and redacts those patterns as an additional safeguard before reports are sent.", comment: "Description for sensitive data row"),
|
||||||
|
collected: false
|
||||||
|
)
|
||||||
|
} header: {
|
||||||
|
Text("Collected data summary", comment: "Section title summarizing collected data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(NSLocalizedString("Diagnostics Privacy", comment: "Navigation title for the privacy details sheet"))
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button(NSLocalizedString("Done", comment: "Button to dismiss privacy details sheet")) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Supporting Views
|
||||||
|
|
||||||
|
struct PrivacyInfoRow: View {
|
||||||
|
let icon: String
|
||||||
|
let title: String
|
||||||
|
let description: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.frame(width: 24)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text(description)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CollectedDataRow: View {
|
||||||
|
let title: String
|
||||||
|
let description: String
|
||||||
|
let collected: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: collected ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||||
|
.foregroundColor(collected ? .blue : .gray)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.frame(width: 24)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text(description)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
struct PrivacySettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NavigationView {
|
||||||
|
PrivacySettingsView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Sentry
|
||||||
|
|
||||||
/// Implements a client that can talk to the Coinos API server with a deterministic account derived from the user's private key.
|
/// Implements a client that can talk to the Coinos API server with a deterministic account derived from the user's private key.
|
||||||
///
|
///
|
||||||
@@ -95,7 +96,15 @@ class CoinosDeterministicAccountClient {
|
|||||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
throw ClientError.unexpectedHTTPResponse(status_code: (response as? HTTPURLResponse)?.statusCode ?? -1, response: data)
|
let error = ClientError.unexpectedHTTPResponse(status_code: (response as? HTTPURLResponse)?.statusCode ?? -1, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "coinos_register",
|
||||||
|
"status_code": (response as? HTTPURLResponse)?.statusCode ?? -1,
|
||||||
|
"response_length": data.count
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +137,16 @@ class CoinosDeterministicAccountClient {
|
|||||||
switch httpResponse.statusCode {
|
switch httpResponse.statusCode {
|
||||||
case 200: return try JSONDecoder().decode(AuthResponse.self, from: data)
|
case 200: return try JSONDecoder().decode(AuthResponse.self, from: data)
|
||||||
case 401: throw ClientError.unauthorized
|
case 401: throw ClientError.unauthorized
|
||||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
default:
|
||||||
|
let error = ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "coinos_login",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"response_length": data.count
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw ClientError.errorProcessingResponse
|
throw ClientError.errorProcessingResponse
|
||||||
@@ -159,10 +177,27 @@ class CoinosDeterministicAccountClient {
|
|||||||
if let httpResponse = response as? HTTPURLResponse {
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
switch httpResponse.statusCode {
|
switch httpResponse.statusCode {
|
||||||
case 200:
|
case 200:
|
||||||
guard let nwc = try await self.getNWCUrl() else { throw ClientError.errorProcessingResponse }
|
guard let nwc = try await self.getNWCUrl() else {
|
||||||
|
let error = ClientError.errorProcessingResponse
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "create_nwc_connection_get_url"
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return nwc
|
return nwc
|
||||||
case 401: throw ClientError.unauthorized
|
case 401: throw ClientError.unauthorized
|
||||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
default:
|
||||||
|
let error = ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "create_nwc_connection",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"response_length": data.count
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw ClientError.errorProcessingResponse
|
throw ClientError.errorProcessingResponse
|
||||||
@@ -203,10 +238,28 @@ class CoinosDeterministicAccountClient {
|
|||||||
if let httpResponse = response as? HTTPURLResponse {
|
if let httpResponse = response as? HTTPURLResponse {
|
||||||
switch httpResponse.statusCode {
|
switch httpResponse.statusCode {
|
||||||
case 200:
|
case 200:
|
||||||
guard let nwc = try await self.getNWCUrl() else { throw ClientError.errorProcessingResponse }
|
guard let nwc = try await self.getNWCUrl() else {
|
||||||
|
let error = ClientError.errorProcessingResponse
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "update_nwc_connection_get_url"
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return nwc
|
return nwc
|
||||||
case 401: throw ClientError.unauthorized
|
case 401: throw ClientError.unauthorized
|
||||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
default:
|
||||||
|
let error = ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "update_nwc_connection",
|
||||||
|
"status_code": httpResponse.statusCode,
|
||||||
|
"max_amount": maxAmount,
|
||||||
|
"response_length": data.count
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw ClientError.errorProcessingResponse
|
throw ClientError.errorProcessingResponse
|
||||||
|
|||||||
@@ -72,6 +72,25 @@ extension WalletConnect {
|
|||||||
|
|
||||||
// MARK: - Interface
|
// MARK: - Interface
|
||||||
|
|
||||||
|
/// Returns a privacy-safe string representing only the type of request, without exposing sensitive details
|
||||||
|
/// like invoice strings, amounts, or timestamps.
|
||||||
|
///
|
||||||
|
/// This is intended for logging and error reporting to external services like Sentry.
|
||||||
|
var requestTypeForLogging: String {
|
||||||
|
return methodForLogging.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
private var methodForLogging: Method {
|
||||||
|
switch self {
|
||||||
|
case .payInvoice:
|
||||||
|
return .payInvoice
|
||||||
|
case .getBalance:
|
||||||
|
return .getBalance
|
||||||
|
case .getTransactionList:
|
||||||
|
return .listTransactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts the NWC request into a raw Nostr event to be sent in the network
|
/// Converts the NWC request into a raw Nostr event to be sent in the network
|
||||||
///
|
///
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Sentry
|
||||||
|
|
||||||
enum WalletConnectState {
|
enum WalletConnectState {
|
||||||
case new(WalletConnectURL)
|
case new(WalletConnectURL)
|
||||||
@@ -100,6 +101,13 @@ class WalletModel: ObservableObject {
|
|||||||
/// - Parameter response: The NWC response received from the network
|
/// - Parameter response: The NWC response received from the network
|
||||||
func handle_nwc_response(response: WalletConnect.FullWalletResponse) {
|
func handle_nwc_response(response: WalletConnect.FullWalletResponse) {
|
||||||
if let error = response.response.error {
|
if let error = response.response.error {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "nwc_response_error",
|
||||||
|
"error_code": error.code,
|
||||||
|
"error_message": error.message ?? "No message"
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
self.resume(request: response.req_id, throwing: error)
|
self.resume(request: response.req_id, throwing: error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -148,7 +156,20 @@ class WalletModel: ObservableObject {
|
|||||||
|
|
||||||
func fetchTransactions(from: UInt64?, until: UInt64?, limit: Int?, offset: Int?, unpaid: Bool?, type: String?) async throws -> [WalletConnect.Transaction] {
|
func fetchTransactions(from: UInt64?, until: UInt64?, limit: Int?, offset: Int?, unpaid: Bool?, type: String?) async throws -> [WalletConnect.Transaction] {
|
||||||
let response = try await self.request(.getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type))
|
let response = try await self.request(.getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type))
|
||||||
guard case .list_transactions(let transactionResponse) = response else { throw FetchError.responseMismatch }
|
let safeResponseType = response.safeTypeForLogging
|
||||||
|
guard case .list_transactions(let transactionResponse) = response else {
|
||||||
|
let error = FetchError.responseMismatch
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "fetch_transactions_response_mismatch",
|
||||||
|
"response_type": safeResponseType,
|
||||||
|
"from": from as Any,
|
||||||
|
"until": until as Any,
|
||||||
|
"limit": limit as Any
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return transactionResponse.transactions
|
return transactionResponse.transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +177,17 @@ class WalletModel: ObservableObject {
|
|||||||
/// Fetches the balance amount from the network and returns the amount in sats
|
/// Fetches the balance amount from the network and returns the amount in sats
|
||||||
func fetchBalance() async throws -> Int64 {
|
func fetchBalance() async throws -> Int64 {
|
||||||
let response = try await self.request(.getBalance)
|
let response = try await self.request(.getBalance)
|
||||||
guard case .get_balance(let balanceResponse) = response else { throw FetchError.responseMismatch }
|
let safeResponseType = response.safeTypeForLogging
|
||||||
|
guard case .get_balance(let balanceResponse) = response else {
|
||||||
|
let error = FetchError.responseMismatch
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "fetch_balance_response_mismatch",
|
||||||
|
"response_type": safeResponseType
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
return balanceResponse.balance / 1000
|
return balanceResponse.balance / 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +200,15 @@ class WalletModel: ObservableObject {
|
|||||||
func request(_ request: WalletConnect.Request, timeout: Duration = .seconds(10)) async throws(WalletRequestError) -> WalletConnect.Response.Result {
|
func request(_ request: WalletConnect.Request, timeout: Duration = .seconds(10)) async throws(WalletRequestError) -> WalletConnect.Response.Result {
|
||||||
guard let nostrNetwork else { throw .notConnectedToTheNostrNetwork }
|
guard let nostrNetwork else { throw .notConnectedToTheNostrNetwork }
|
||||||
guard let currentNwcUrl = self.connect_state.currentNwcUrl() else { throw .noConnectedWallet }
|
guard let currentNwcUrl = self.connect_state.currentNwcUrl() else { throw .noConnectedWallet }
|
||||||
guard let requestEvent = request.to_nostr_event(to_pk: currentNwcUrl.pubkey, keypair: currentNwcUrl.keypair) else { throw .errorFormattingRequest }
|
guard let requestEvent = request.to_nostr_event(to_pk: currentNwcUrl.pubkey, keypair: currentNwcUrl.keypair) else {
|
||||||
|
DamusSentry.captureSentryMessage("Wallet request failed: error formatting request") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "wallet_request_format",
|
||||||
|
"request_type": request.requestTypeForLogging
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw .errorFormattingRequest
|
||||||
|
}
|
||||||
|
|
||||||
let responseFilters = [
|
let responseFilters = [
|
||||||
NostrFilter(
|
NostrFilter(
|
||||||
@@ -182,19 +221,60 @@ class WalletModel: ObservableObject {
|
|||||||
|
|
||||||
await nostrNetwork.send(event: requestEvent, to: [currentNwcUrl.relay], skipEphemeralRelays: false)
|
await nostrNetwork.send(event: requestEvent, to: [currentNwcUrl.relay], skipEphemeralRelays: false)
|
||||||
for await event in nostrNetwork.reader.timedStream(filters: responseFilters, to: [currentNwcUrl.relay], timeout: timeout) {
|
for await event in nostrNetwork.reader.timedStream(filters: responseFilters, to: [currentNwcUrl.relay], timeout: timeout) {
|
||||||
guard let responseEvent = try? event.getCopy() else { throw .internalError }
|
let responseEvent: NdbNote
|
||||||
|
do {
|
||||||
|
responseEvent = try event.getCopy()
|
||||||
|
} catch {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "wallet_response_get_copy",
|
||||||
|
"error_type": String(describing: type(of: error))
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw .internalError
|
||||||
|
}
|
||||||
|
|
||||||
let fullWalletResponse: WalletConnect.FullWalletResponse
|
let fullWalletResponse: WalletConnect.FullWalletResponse
|
||||||
do { fullWalletResponse = try WalletConnect.FullWalletResponse(from: responseEvent, nwc: currentNwcUrl) }
|
do { fullWalletResponse = try WalletConnect.FullWalletResponse(from: responseEvent, nwc: currentNwcUrl) }
|
||||||
catch { throw WalletRequestError.walletResponseDecodingError(error) }
|
catch {
|
||||||
|
DamusSentry.captureSentryError(error) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "wallet_response_decode",
|
||||||
|
"error_type": String(describing: type(of: error))
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw WalletRequestError.walletResponseDecodingError(error)
|
||||||
|
}
|
||||||
|
|
||||||
guard fullWalletResponse.req_id == requestEvent.id else { continue } // Our filters may match other responses
|
guard fullWalletResponse.req_id == requestEvent.id else { continue } // Our filters may match other responses
|
||||||
if let responseError = fullWalletResponse.response.error { throw .walletResponseError(responseError) }
|
if let responseError = fullWalletResponse.response.error {
|
||||||
|
DamusSentry.captureSentryError(responseError) { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "wallet_response_error",
|
||||||
|
"error_code": responseError.code,
|
||||||
|
"error_message": responseError.message ?? "No message"
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw .walletResponseError(responseError)
|
||||||
|
}
|
||||||
|
|
||||||
guard let result = fullWalletResponse.response.result else { throw .walletEmptyResponse }
|
guard let result = fullWalletResponse.response.result else {
|
||||||
|
DamusSentry.captureSentryMessage("Wallet response has no result") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "wallet_empty_response",
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
|
throw .walletEmptyResponse
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
do { try Task.checkCancellation() } catch { throw .cancelled }
|
do { try Task.checkCancellation() } catch { throw .cancelled }
|
||||||
|
DamusSentry.captureSentryMessage("Wallet request timeout") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "wallet_request_timeout",
|
||||||
|
"request_type": request.requestTypeForLogging,
|
||||||
|
], key: "wallet")
|
||||||
|
}
|
||||||
throw .responseTimeout
|
throw .responseTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,3 +327,17 @@ class WalletModel: ObservableObject {
|
|||||||
case timeout
|
case timeout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension WalletConnect.Response.Result {
|
||||||
|
/// Returns a privacy-safe label describing only the response case for telemetry.
|
||||||
|
var safeTypeForLogging: String {
|
||||||
|
switch self {
|
||||||
|
case .get_balance:
|
||||||
|
return "get_balance"
|
||||||
|
case .pay_invoice:
|
||||||
|
return "pay_invoice"
|
||||||
|
case .list_transactions:
|
||||||
|
return "list_transactions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Sentry
|
||||||
|
|
||||||
enum ZappingEventType {
|
enum ZappingEventType {
|
||||||
case failed(ZappingError)
|
case failed(ZappingError)
|
||||||
@@ -184,6 +185,14 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
|
|
||||||
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
|
DamusSentry.captureSentryMessage("Failed to create zap request event") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "make_zap_request_event",
|
||||||
|
"zap_type": String(describing: zap_type),
|
||||||
|
"relay_count": relays.count,
|
||||||
|
"has_comment": comment != nil
|
||||||
|
], key: "zaps")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,6 +212,11 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
notify(.zapping(ev))
|
notify(.zapping(ev))
|
||||||
|
DamusSentry.captureSentryMessage("Failed to fetch lnurl payreq for zap") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "zap_fetch_lnurl"
|
||||||
|
], key: "zaps")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,6 +225,12 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
notify(.zapping(ev))
|
notify(.zapping(ev))
|
||||||
|
DamusSentry.captureSentryMessage("Failed to fetch zap invoice") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "zap_fetch_invoice",
|
||||||
|
"zap_type": String(describing: zap_type)
|
||||||
|
], key: "zaps")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,6 +268,11 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
|||||||
let typ = ZappingEventType.failed(.send_failed)
|
let typ = ZappingEventType.failed(.send_failed)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
|
||||||
notify(.zapping(ev))
|
notify(.zapping(ev))
|
||||||
|
DamusSentry.captureSentryMessage("Failed to send NWC zap request") { scope in
|
||||||
|
scope.setContext(value: [
|
||||||
|
"operation": "nwc_pay_request",
|
||||||
|
], key: "zaps")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,498 @@
|
|||||||
|
//
|
||||||
|
// DamusSentry.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
import Sentry
|
||||||
|
|
||||||
|
/// Namespace for Damus Sentry startup, capture helpers, and sensitive-data scrubbing.
|
||||||
|
///
|
||||||
|
/// This type centralizes Sentry configuration during app startup and provides
|
||||||
|
/// privacy-preserving helpers used before telemetry leaves the device.
|
||||||
|
struct DamusSentry {
|
||||||
|
|
||||||
|
/// Returns the Sentry environment name for the current build configuration.
|
||||||
|
///
|
||||||
|
/// Debug builds are tagged separately so local development traffic does not
|
||||||
|
/// get grouped together with production events in Sentry.
|
||||||
|
///
|
||||||
|
/// - Returns: The Sentry environment name for the current build.
|
||||||
|
static func environmentName() -> String {
|
||||||
|
#if DEBUG
|
||||||
|
return "debug"
|
||||||
|
#else
|
||||||
|
return "production"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts Sentry with Damus-specific configuration when telemetry is enabled.
|
||||||
|
///
|
||||||
|
/// This centralizes Sentry startup so the app entry point stays small and all
|
||||||
|
/// telemetry-related configuration lives in one place.
|
||||||
|
static func startIfEnabled() {
|
||||||
|
guard GlobalSettingsStore.shared.enable_sentry_telemetry else {
|
||||||
|
Log.info("Sentry telemetry disabled by user preference", for: .app_lifecycle)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SentrySDK.start { options in
|
||||||
|
options.dsn = "https://9be03ae55b9c1b55feb599f470772deb@o4511304644558848.ingest.us.sentry.io/4511304645738496"
|
||||||
|
options.environment = environmentName()
|
||||||
|
|
||||||
|
// Adds IP for users.
|
||||||
|
// For more information, visit: https://docs.sentry.io/platforms/apple/data-management/data-collected/
|
||||||
|
options.sendDefaultPii = false
|
||||||
|
|
||||||
|
// Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||||
|
options.tracesSampleRate = 0.5
|
||||||
|
|
||||||
|
// Configure profiling. Visit https://docs.sentry.io/platforms/apple/profiling/ to learn more.
|
||||||
|
options.configureProfiling = {
|
||||||
|
$0.sessionSampleRate = 0.5
|
||||||
|
$0.lifecycle = .trace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub sensitive data before sending to Sentry.
|
||||||
|
options.beforeSend = { event in
|
||||||
|
return scrubSensitiveData(in: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.beforeBreadcrumb = { breadcrumb in
|
||||||
|
return scrubSensitiveDataInBreadcrumb(breadcrumb)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.beforeSendSpan = { span in
|
||||||
|
return scrubSensitiveDataInSpan(span)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uncomment the following lines to add more data to your events
|
||||||
|
// options.attachScreenshot = true // This adds a screenshot to the error events
|
||||||
|
// options.attachViewHierarchy = true // This adds the view hierarchy to the error events
|
||||||
|
|
||||||
|
// Enable experimental logging features
|
||||||
|
// options.experimental.enableLogs = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safely captures an error to Sentry only if the user has enabled telemetry.
|
||||||
|
///
|
||||||
|
/// This is a convenience wrapper around `SentrySDK.capture(error:)` that respects
|
||||||
|
/// the user's privacy preference from GlobalSettingsStore.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - error: The error to capture
|
||||||
|
/// - scopeBlock: Optional block to configure the scope for this capture
|
||||||
|
static func captureSentryError(_ error: Error, scopeBlock: ((Scope) -> Void)? = nil) {
|
||||||
|
guard GlobalSettingsStore.shared.enable_sentry_telemetry else { return }
|
||||||
|
if let scopeBlock = scopeBlock {
|
||||||
|
SentrySDK.capture(error: error, block: scopeBlock)
|
||||||
|
} else {
|
||||||
|
SentrySDK.capture(error: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safely captures a message to Sentry only if the user has enabled telemetry.
|
||||||
|
///
|
||||||
|
/// This is a convenience wrapper around `SentrySDK.capture(message:)` that respects
|
||||||
|
/// the user's privacy preference from GlobalSettingsStore.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - message: The message to capture
|
||||||
|
/// - scopeBlock: Optional block to configure the scope for this capture
|
||||||
|
static func captureSentryMessage(_ message: String, scopeBlock: ((Scope) -> Void)? = nil) {
|
||||||
|
guard GlobalSettingsStore.shared.enable_sentry_telemetry else { return }
|
||||||
|
if let scopeBlock = scopeBlock {
|
||||||
|
SentrySDK.capture(message: message, block: scopeBlock)
|
||||||
|
} else {
|
||||||
|
SentrySDK.capture(message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive data from Sentry events before they are sent.
|
||||||
|
///
|
||||||
|
/// This method removes or redacts:
|
||||||
|
/// - Nostr private keys (nsec1...)
|
||||||
|
/// - Nostr public keys (npub1...)
|
||||||
|
/// - Nostr note IDs (note1...)
|
||||||
|
/// - Nostr note events with hints (nevent1...)
|
||||||
|
/// - Nostr profiles with hints (nprofile1...)
|
||||||
|
/// - Nostr addressable events (naddr1...)
|
||||||
|
/// - Nostr relay references (nrelay1...)
|
||||||
|
/// - Hex-encoded keys (64-character hex strings)
|
||||||
|
/// - Email addresses
|
||||||
|
///
|
||||||
|
/// - Parameter event: The Sentry event to scrub
|
||||||
|
/// - Returns: The scrubbed event, or nil to discard the event
|
||||||
|
static func scrubSensitiveData(in event: Event) -> Event? {
|
||||||
|
// Scrub message - create new SentryMessage with scrubbed content
|
||||||
|
if let messageText = event.message?.formatted {
|
||||||
|
let scrubbedText = scrubString(messageText)
|
||||||
|
event.message = SentryMessage(formatted: scrubbedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub exception messages and values
|
||||||
|
if let exceptions = event.exceptions {
|
||||||
|
for exception in exceptions {
|
||||||
|
if let value = exception.value {
|
||||||
|
exception.value = scrubString(value)
|
||||||
|
}
|
||||||
|
if let mechanism = exception.mechanism, let description = mechanism.desc {
|
||||||
|
mechanism.desc = scrubString(description)
|
||||||
|
}
|
||||||
|
// Scrub stack trace
|
||||||
|
if let stacktrace = exception.stacktrace {
|
||||||
|
scrubStacktrace(stacktrace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub threads
|
||||||
|
if let threads = event.threads {
|
||||||
|
for thread in threads {
|
||||||
|
if let stacktrace = thread.stacktrace {
|
||||||
|
scrubStacktrace(stacktrace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub context data
|
||||||
|
if var context = event.context as? [String: Any] {
|
||||||
|
scrubDictionary(&context)
|
||||||
|
event.context = context as? [String: [String: Any]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub extra data
|
||||||
|
if var extra = event.extra {
|
||||||
|
scrubDictionary(&extra)
|
||||||
|
event.extra = extra
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub tags
|
||||||
|
if let tags = event.tags {
|
||||||
|
for (key, value) in tags {
|
||||||
|
event.tags?[key] = scrubString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub user data
|
||||||
|
if let user = event.user {
|
||||||
|
if let email = user.email {
|
||||||
|
user.email = scrubEmail(email)
|
||||||
|
}
|
||||||
|
if let userId = user.userId {
|
||||||
|
user.userId = scrubString(userId)
|
||||||
|
}
|
||||||
|
if let username = user.username {
|
||||||
|
user.username = scrubString(username)
|
||||||
|
}
|
||||||
|
if var data = user.data {
|
||||||
|
scrubDictionary(&data)
|
||||||
|
user.data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub request data
|
||||||
|
if let request = event.request {
|
||||||
|
if let url = request.url {
|
||||||
|
request.url = scrubString(url)
|
||||||
|
}
|
||||||
|
if let queryString = request.queryString {
|
||||||
|
request.queryString = scrubString(queryString)
|
||||||
|
}
|
||||||
|
if var headers = request.headers as? [String: Any] {
|
||||||
|
scrubDictionary(&headers)
|
||||||
|
request.headers = headers as? [String: String]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive data from Sentry breadcrumbs.
|
||||||
|
///
|
||||||
|
/// - Parameter breadcrumb: The breadcrumb to scrub
|
||||||
|
/// - Returns: The scrubbed breadcrumb, or nil to discard it
|
||||||
|
static func scrubSensitiveDataInBreadcrumb(_ breadcrumb: Breadcrumb) -> Breadcrumb? {
|
||||||
|
// Scrub message
|
||||||
|
if let message = breadcrumb.message {
|
||||||
|
breadcrumb.message = scrubString(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrub data
|
||||||
|
if var data = breadcrumb.data {
|
||||||
|
scrubDictionary(&data)
|
||||||
|
breadcrumb.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumb
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive data from Sentry spans.
|
||||||
|
///
|
||||||
|
/// Spans represent units of work in performance tracing (e.g., network requests, database queries).
|
||||||
|
/// If a span contains sensitive data, it's dropped entirely rather than sent with potentially unscrubbed data.
|
||||||
|
///
|
||||||
|
/// - Parameter span: The span to scrub
|
||||||
|
/// - Returns: The scrubbed span, or nil to discard it
|
||||||
|
static func scrubSensitiveDataInSpan(_ span: Span) -> Span? {
|
||||||
|
// Check span description for sensitive data
|
||||||
|
if let spanDescription = span.spanDescription {
|
||||||
|
if containsSensitiveData(spanDescription) {
|
||||||
|
// Drop the entire span if it contains sensitive data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check span data for sensitive data (read-only, can't scrub)
|
||||||
|
if let data = span.data as? [String: Any] {
|
||||||
|
if containsSensitiveDataInDictionary(data) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check span tags for sensitive data (read-only, can't scrub)
|
||||||
|
for (_, value) in span.tags {
|
||||||
|
if containsSensitiveData(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return span
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a string contains sensitive data patterns.
|
||||||
|
///
|
||||||
|
/// - Parameter string: The string to check
|
||||||
|
/// - Returns: True if the string contains sensitive data
|
||||||
|
private static func containsSensitiveData(_ string: String) -> Bool {
|
||||||
|
// Check for nsec (private keys)
|
||||||
|
if string.range(of: "nsec1[a-zA-Z0-9]{58,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for npub (public keys)
|
||||||
|
if string.range(of: "npub1[a-zA-Z0-9]{58,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for note IDs
|
||||||
|
if string.range(of: "note1[a-zA-Z0-9]{58,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nevent
|
||||||
|
if string.range(of: "nevent1[a-zA-Z0-9]{58,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nprofile
|
||||||
|
if string.range(of: "nprofile1[a-zA-Z0-9]{58,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for naddr
|
||||||
|
if string.range(of: "naddr1[a-zA-Z0-9]{58,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for nrelay
|
||||||
|
if string.range(of: "nrelay1[a-zA-Z0-9]+", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for hex keys (64-character hex strings)
|
||||||
|
if string.range(of: "\\b[0-9a-fA-F]{64}\\b", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for email addresses
|
||||||
|
if string.range(of: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", options: .regularExpression) != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a dictionary contains sensitive data.
|
||||||
|
///
|
||||||
|
/// - Parameter dictionary: The dictionary to check
|
||||||
|
/// - Returns: True if the dictionary contains sensitive data
|
||||||
|
private static func containsSensitiveDataInDictionary(_ dictionary: [String: Any]) -> Bool {
|
||||||
|
for (_, value) in dictionary {
|
||||||
|
if let stringValue = value as? String {
|
||||||
|
if containsSensitiveData(stringValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if let nestedDict = value as? [String: Any] {
|
||||||
|
if containsSensitiveDataInDictionary(nestedDict) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if let array = value as? [Any] {
|
||||||
|
if containsSensitiveDataInArray(array) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if an array contains sensitive data.
|
||||||
|
///
|
||||||
|
/// - Parameter array: The array to check
|
||||||
|
/// - Returns: True if the array contains sensitive data
|
||||||
|
private static func containsSensitiveDataInArray(_ array: [Any]) -> Bool {
|
||||||
|
for item in array {
|
||||||
|
if let stringValue = item as? String {
|
||||||
|
if containsSensitiveData(stringValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if let dictValue = item as? [String: Any] {
|
||||||
|
if containsSensitiveDataInDictionary(dictValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if let arrayValue = item as? [Any] {
|
||||||
|
if containsSensitiveDataInArray(arrayValue) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive patterns from a string.
|
||||||
|
///
|
||||||
|
/// - Parameter string: The string to scrub
|
||||||
|
/// - Returns: The scrubbed string with sensitive data redacted
|
||||||
|
private static func scrubString(_ string: String) -> String {
|
||||||
|
var scrubbed = string
|
||||||
|
|
||||||
|
// Scrub nsec (private keys) - most critical
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "nsec1[a-zA-Z0-9]{58,}",
|
||||||
|
with: "[REDACTED_NSEC]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub npub (public keys)
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "npub1[a-zA-Z0-9]{58,}",
|
||||||
|
with: "[REDACTED_NPUB]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub note IDs
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "note1[a-zA-Z0-9]{58,}",
|
||||||
|
with: "[REDACTED_NOTE]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub nevent (note events with relay hints)
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "nevent1[a-zA-Z0-9]{58,}",
|
||||||
|
with: "[REDACTED_NEVENT]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub nprofile (profiles with relay hints)
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "nprofile1[a-zA-Z0-9]{58,}",
|
||||||
|
with: "[REDACTED_NPROFILE]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub naddr (addressable/replaceable events)
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "naddr1[a-zA-Z0-9]{58,}",
|
||||||
|
with: "[REDACTED_NADDR]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub nrelay (relay references)
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "nrelay1[a-zA-Z0-9]+",
|
||||||
|
with: "[REDACTED_NRELAY]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub hex keys (64-character hex strings that might be keys)
|
||||||
|
// Use word boundaries to avoid matching legitimate hex in other contexts
|
||||||
|
scrubbed = scrubbed.replacingOccurrences(
|
||||||
|
of: "\\b[0-9a-fA-F]{64}\\b",
|
||||||
|
with: "[REDACTED_HEX]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
|
||||||
|
// Scrub email addresses
|
||||||
|
scrubbed = scrubEmail(scrubbed)
|
||||||
|
|
||||||
|
return scrubbed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs email addresses from a string.
|
||||||
|
///
|
||||||
|
/// - Parameter string: The string to scrub
|
||||||
|
/// - Returns: The string with email addresses redacted
|
||||||
|
private static func scrubEmail(_ string: String) -> String {
|
||||||
|
return string.replacingOccurrences(
|
||||||
|
of: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}",
|
||||||
|
with: "[REDACTED_EMAIL]",
|
||||||
|
options: .regularExpression
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive data from a dictionary recursively.
|
||||||
|
///
|
||||||
|
/// - Parameter dictionary: The dictionary to scrub (modified in place)
|
||||||
|
private static func scrubDictionary(_ dictionary: inout [String: Any]) {
|
||||||
|
for (key, value) in dictionary {
|
||||||
|
if let stringValue = value as? String {
|
||||||
|
dictionary[key] = scrubString(stringValue)
|
||||||
|
} else if var nestedDict = value as? [String: Any] {
|
||||||
|
scrubDictionary(&nestedDict)
|
||||||
|
dictionary[key] = nestedDict
|
||||||
|
} else if let array = value as? [Any] {
|
||||||
|
dictionary[key] = scrubArray(array)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive data from an array recursively.
|
||||||
|
///
|
||||||
|
/// - Parameter array: The array to scrub
|
||||||
|
/// - Returns: A new array with scrubbed values
|
||||||
|
private static func scrubArray(_ array: [Any]) -> [Any] {
|
||||||
|
return array.map { item in
|
||||||
|
if let stringValue = item as? String {
|
||||||
|
return scrubString(stringValue)
|
||||||
|
} else if var dictValue = item as? [String: Any] {
|
||||||
|
scrubDictionary(&dictValue)
|
||||||
|
return dictValue
|
||||||
|
} else if let arrayValue = item as? [Any] {
|
||||||
|
return scrubArray(arrayValue)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scrubs sensitive data from a stacktrace.
|
||||||
|
///
|
||||||
|
/// - Parameter stacktrace: The stacktrace to scrub (modified in place)
|
||||||
|
private static func scrubStacktrace(_ stacktrace: SentryStacktrace) {
|
||||||
|
let frames = stacktrace.frames
|
||||||
|
for frame in frames {
|
||||||
|
// Scrub function names
|
||||||
|
if let function = frame.function {
|
||||||
|
frame.function = scrubString(function)
|
||||||
|
}
|
||||||
|
// Scrub file paths (might contain sensitive info in URLs)
|
||||||
|
if let fileName = frame.fileName {
|
||||||
|
frame.fileName = scrubString(fileName)
|
||||||
|
}
|
||||||
|
// Scrub local variables
|
||||||
|
if var vars = frame.vars {
|
||||||
|
scrubDictionary(&vars)
|
||||||
|
frame.vars = vars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ enum Route: Hashable {
|
|||||||
case DMChat(dms: DirectMessageModel)
|
case DMChat(dms: DirectMessageModel)
|
||||||
case UserRelays(relays: [RelayURL])
|
case UserRelays(relays: [RelayURL])
|
||||||
case KeySettings(keypair: Keypair)
|
case KeySettings(keypair: Keypair)
|
||||||
|
case PrivacySettings
|
||||||
case AppearanceSettings(settings: UserSettingsStore)
|
case AppearanceSettings(settings: UserSettingsStore)
|
||||||
case NotificationSettings(settings: UserSettingsStore)
|
case NotificationSettings(settings: UserSettingsStore)
|
||||||
case ZapSettings(settings: UserSettingsStore)
|
case ZapSettings(settings: UserSettingsStore)
|
||||||
@@ -86,6 +87,8 @@ enum Route: Hashable {
|
|||||||
UserRelaysView(state: damusState, relays: relays)
|
UserRelaysView(state: damusState, relays: relays)
|
||||||
case .KeySettings(let keypair):
|
case .KeySettings(let keypair):
|
||||||
KeySettingsView(keypair: keypair)
|
KeySettingsView(keypair: keypair)
|
||||||
|
case .PrivacySettings:
|
||||||
|
PrivacySettingsView()
|
||||||
case .AppearanceSettings(let settings):
|
case .AppearanceSettings(let settings):
|
||||||
AppearanceSettingsView(damus_state: damusState, settings: settings)
|
AppearanceSettingsView(damus_state: damusState, settings: settings)
|
||||||
case .NotificationSettings(let settings):
|
case .NotificationSettings(let settings):
|
||||||
@@ -194,6 +197,8 @@ enum Route: Hashable {
|
|||||||
case .KeySettings(let keypair):
|
case .KeySettings(let keypair):
|
||||||
hasher.combine("keySettings")
|
hasher.combine("keySettings")
|
||||||
hasher.combine(keypair.pubkey)
|
hasher.combine(keypair.pubkey)
|
||||||
|
case .PrivacySettings:
|
||||||
|
hasher.combine("privacySettings")
|
||||||
case .AppearanceSettings:
|
case .AppearanceSettings:
|
||||||
hasher.combine("appearanceSettings")
|
hasher.combine("appearanceSettings")
|
||||||
case .NotificationSettings:
|
case .NotificationSettings:
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
import Sentry
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import StoreKit
|
import StoreKit
|
||||||
|
|
||||||
@@ -77,6 +79,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
|||||||
var state: DamusState? = nil
|
var state: DamusState? = nil
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||||
|
DamusSentry.startIfEnabled()
|
||||||
|
|
||||||
UNUserNotificationCenter.current().delegate = self
|
UNUserNotificationCenter.current().delegate = self
|
||||||
SKPaymentQueue.default().add(StoreObserver.standard)
|
SKPaymentQueue.default().add(StoreObserver.standard)
|
||||||
registerNotificationCategories()
|
registerNotificationCategories()
|
||||||
|
|||||||
@@ -0,0 +1,761 @@
|
|||||||
|
//
|
||||||
|
// SensitiveDataScrubbingTests.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
import Sentry
|
||||||
|
@testable import damus
|
||||||
|
|
||||||
|
final class SensitiveDataScrubbingTests: XCTestCase {
|
||||||
|
|
||||||
|
// MARK: - Test String Scrubbing
|
||||||
|
|
||||||
|
func testScrubNsecPrivateKey() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "User logged in with nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NSEC]"))
|
||||||
|
XCTAssertFalse(message.contains("nsec1qqqq"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubNpubPublicKey() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Profile: npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NPUB]"))
|
||||||
|
XCTAssertFalse(message.contains("npub1abc123"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubNoteId() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Viewing note note1xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz7")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NOTE]"))
|
||||||
|
XCTAssertFalse(message.contains("note1xyz789"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubNevent() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Event reference: nevent1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NEVENT]"))
|
||||||
|
XCTAssertFalse(message.contains("nevent1qqsxyz"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubNprofile() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Profile link: nprofile1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NPROFILE]"))
|
||||||
|
XCTAssertFalse(message.contains("nprofile1qqsxyz"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubNaddr() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Address: naddr1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NADDR]"))
|
||||||
|
XCTAssertFalse(message.contains("naddr1qqsxyz"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubNrelay() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Relay: nrelay1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NRELAY]"))
|
||||||
|
XCTAssertFalse(message.contains("nrelay1qqsxyz"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubShortNrelay() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Relay: nrelay1abc123")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NRELAY]"))
|
||||||
|
XCTAssertFalse(message.contains("nrelay1abc123"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubHexKey() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Key: 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_HEX]"))
|
||||||
|
XCTAssertFalse(message.contains("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubEmailAddress() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Contact user@example.com for support")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_EMAIL]"))
|
||||||
|
XCTAssertFalse(message.contains("user@example.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubMultipleSensitivePatterns() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "User nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq with npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123 and nevent1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789 emailed test@test.com")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NSEC]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NPUB]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NEVENT]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_EMAIL]"))
|
||||||
|
XCTAssertFalse(message.contains("nsec1qqqq"))
|
||||||
|
XCTAssertFalse(message.contains("npub1abc123"))
|
||||||
|
XCTAssertFalse(message.contains("nevent1qqsxyz"))
|
||||||
|
XCTAssertFalse(message.contains("test@test.com"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Exception Scrubbing
|
||||||
|
|
||||||
|
func testScrubExceptionValue() {
|
||||||
|
let event = Event()
|
||||||
|
|
||||||
|
let sentryException = Exception(value: "Error: npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123", type: "Error")
|
||||||
|
event.exceptions = [sentryException]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions)
|
||||||
|
let exceptionValue = scrubbedEvent?.exceptions?.first?.value ?? ""
|
||||||
|
XCTAssertTrue(exceptionValue.contains("[REDACTED_NPUB]"))
|
||||||
|
XCTAssertFalse(exceptionValue.contains("npub1abc123"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubExceptionMechanismDescription() {
|
||||||
|
let event = Event()
|
||||||
|
let sentryException = Exception(value: "Error", type: "Error")
|
||||||
|
let mechanism = Mechanism(type: "generic")
|
||||||
|
mechanism.desc = "Failure for nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
|
||||||
|
sentryException.mechanism = mechanism
|
||||||
|
event.exceptions = [sentryException]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
let mechanismDescription = scrubbedEvent?.exceptions?.first?.mechanism?.desc ?? ""
|
||||||
|
XCTAssertTrue(mechanismDescription.contains("[REDACTED_NSEC]"))
|
||||||
|
XCTAssertFalse(mechanismDescription.contains("nsec1qqqq"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Context Scrubbing
|
||||||
|
|
||||||
|
func testScrubContextData() {
|
||||||
|
let event = Event()
|
||||||
|
event.context = [
|
||||||
|
"nostr": [
|
||||||
|
"pubkey": "npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123",
|
||||||
|
"email": "context@example.com",
|
||||||
|
"nested": [
|
||||||
|
"private_key": "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||||
|
"hex": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
guard let nostrContext = scrubbedEvent?.context?["nostr"] else {
|
||||||
|
XCTFail("Expected nostr context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(nostrContext["pubkey"] as? String, "[REDACTED_NPUB]")
|
||||||
|
XCTAssertEqual(nostrContext["email"] as? String, "[REDACTED_EMAIL]")
|
||||||
|
|
||||||
|
guard let nested = nostrContext["nested"] as? [String: Any] else {
|
||||||
|
XCTFail("Expected nested context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(nested["private_key"] as? String, "[REDACTED_NSEC]")
|
||||||
|
XCTAssertEqual(nested["hex"] as? String, "[REDACTED_HEX]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Tag Scrubbing
|
||||||
|
|
||||||
|
func testScrubTags() {
|
||||||
|
let event = Event()
|
||||||
|
event.tags = [
|
||||||
|
"user_id": "npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123",
|
||||||
|
"key": "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.tags)
|
||||||
|
let userId = scrubbedEvent?.tags?["user_id"] ?? ""
|
||||||
|
let key = scrubbedEvent?.tags?["key"] ?? ""
|
||||||
|
XCTAssertTrue(userId.contains("[REDACTED_NPUB]"))
|
||||||
|
XCTAssertTrue(key.contains("[REDACTED_HEX]"))
|
||||||
|
XCTAssertFalse(userId.contains("npub1abc123"))
|
||||||
|
XCTAssertFalse(key.contains("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test User Data Scrubbing
|
||||||
|
|
||||||
|
func testScrubUserEmail() {
|
||||||
|
let event = Event()
|
||||||
|
let user = User()
|
||||||
|
user.email = "sensitive@example.com"
|
||||||
|
user.userId = "npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123"
|
||||||
|
user.username = "user@domain.com"
|
||||||
|
event.user = user
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.user)
|
||||||
|
XCTAssertTrue(scrubbedEvent?.user?.email?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
XCTAssertTrue(scrubbedEvent?.user?.userId?.contains("[REDACTED_NPUB]") ?? false)
|
||||||
|
XCTAssertTrue(scrubbedEvent?.user?.username?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubUserData() {
|
||||||
|
let event = Event()
|
||||||
|
let user = User()
|
||||||
|
user.data = [
|
||||||
|
"private_key": "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||||
|
"email": "test@example.com"
|
||||||
|
]
|
||||||
|
event.user = user
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.user?.data)
|
||||||
|
let privateKey = scrubbedEvent?.user?.data?["private_key"] as? String
|
||||||
|
let email = scrubbedEvent?.user?.data?["email"] as? String
|
||||||
|
XCTAssertTrue(privateKey?.contains("[REDACTED_NSEC]") ?? false)
|
||||||
|
XCTAssertTrue(email?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Breadcrumb Scrubbing
|
||||||
|
|
||||||
|
func testScrubBreadcrumbMessage() {
|
||||||
|
let breadcrumb = Breadcrumb()
|
||||||
|
breadcrumb.message = "User logged in with nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
|
||||||
|
|
||||||
|
let scrubbedBreadcrumb = DamusSentry.scrubSensitiveDataInBreadcrumb(breadcrumb)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedBreadcrumb?.message)
|
||||||
|
XCTAssertTrue(scrubbedBreadcrumb?.message?.contains("[REDACTED_NSEC]") ?? false)
|
||||||
|
XCTAssertFalse(scrubbedBreadcrumb?.message?.contains("nsec1") ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubBreadcrumbData() {
|
||||||
|
let breadcrumb = Breadcrumb()
|
||||||
|
breadcrumb.data = [
|
||||||
|
"pubkey": "npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123",
|
||||||
|
"email": "user@example.com"
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedBreadcrumb = DamusSentry.scrubSensitiveDataInBreadcrumb(breadcrumb)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedBreadcrumb?.data)
|
||||||
|
let pubkey = scrubbedBreadcrumb?.data?["pubkey"] as? String
|
||||||
|
let email = scrubbedBreadcrumb?.data?["email"] as? String
|
||||||
|
XCTAssertTrue(pubkey?.contains("[REDACTED_NPUB]") ?? false)
|
||||||
|
XCTAssertTrue(email?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubBreadcrumbShortNrelay() {
|
||||||
|
let breadcrumb = Breadcrumb()
|
||||||
|
breadcrumb.message = "Relay: nrelay1abc123"
|
||||||
|
|
||||||
|
let scrubbedBreadcrumb = DamusSentry.scrubSensitiveDataInBreadcrumb(breadcrumb)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedBreadcrumb?.message)
|
||||||
|
XCTAssertTrue(scrubbedBreadcrumb?.message?.contains("[REDACTED_NRELAY]") ?? false)
|
||||||
|
XCTAssertFalse(scrubbedBreadcrumb?.message?.contains("nrelay1abc123") ?? true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Nested Dictionary Scrubbing
|
||||||
|
|
||||||
|
func testScrubNestedDictionary() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"level1": [
|
||||||
|
"level2": [
|
||||||
|
"key": "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||||
|
"email": "nested@example.com"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let level1 = scrubbedEvent?.extra?["level1"] as? [String: Any],
|
||||||
|
let level2 = level1["level2"] as? [String: Any] {
|
||||||
|
let key = level2["key"] as? String
|
||||||
|
let email = level2["email"] as? String
|
||||||
|
XCTAssertTrue(key?.contains("[REDACTED_NSEC]") ?? false)
|
||||||
|
XCTAssertTrue(email?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
} else {
|
||||||
|
XCTFail("Nested dictionary not properly structured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Array Scrubbing
|
||||||
|
|
||||||
|
func testScrubArrayOfStrings() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"keys": [
|
||||||
|
"nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||||
|
"npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123",
|
||||||
|
"user@example.com"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let keys = scrubbedEvent?.extra?["keys"] as? [String] {
|
||||||
|
XCTAssertTrue(keys[0].contains("[REDACTED_NSEC]"))
|
||||||
|
XCTAssertTrue(keys[1].contains("[REDACTED_NPUB]"))
|
||||||
|
XCTAssertTrue(keys[2].contains("[REDACTED_EMAIL]"))
|
||||||
|
} else {
|
||||||
|
XCTFail("Array not properly scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Edge Cases
|
||||||
|
|
||||||
|
func testDoesNotScrubNonSensitiveData() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "This is a normal message with no sensitive data")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
XCTAssertEqual(scrubbedEvent?.message?.formatted, "This is a normal message with no sensitive data")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDoesNotScrubShortHexStrings() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Color: #FF5733 is nice")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
// Should not scrub short hex strings
|
||||||
|
XCTAssertTrue(message.contains("FF5733"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubIncompleteEmailPattern() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Contact: user@example.io")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
// This should be scrubbed as it matches the email pattern
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_EMAIL]"))
|
||||||
|
XCTAssertFalse(message.contains("user@example.io"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReturnsEventWhenNoSensitiveData() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: "Normal log message")
|
||||||
|
event.tags = ["environment": "production"]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent)
|
||||||
|
XCTAssertEqual(scrubbedEvent?.message?.formatted, "Normal log message")
|
||||||
|
XCTAssertEqual(scrubbedEvent?.tags?["environment"], "production")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test containsSensitiveDataInDictionary
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInDictionaryDetectsSensitiveString() {
|
||||||
|
// Test via event extra which internally uses containsSensitiveDataInDictionary
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"user_key": "npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123",
|
||||||
|
"safe_data": "normal value"
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let userKey = scrubbedEvent?.extra?["user_key"] as? String {
|
||||||
|
XCTAssertTrue(userKey.contains("[REDACTED_NPUB]"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInDictionaryNestedDictionary() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"level1": [
|
||||||
|
"level2": [
|
||||||
|
"secret": "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let level1 = scrubbedEvent?.extra?["level1"] as? [String: Any],
|
||||||
|
let level2 = level1["level2"] as? [String: Any],
|
||||||
|
let secret = level2["secret"] as? String {
|
||||||
|
XCTAssertTrue(secret.contains("[REDACTED_NSEC]"))
|
||||||
|
} else {
|
||||||
|
XCTFail("Nested dictionary should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInDictionaryWithArray() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"items": [
|
||||||
|
"safe_value",
|
||||||
|
"user@example.com",
|
||||||
|
"another_safe_value"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let items = scrubbedEvent?.extra?["items"] as? [String] {
|
||||||
|
XCTAssertEqual(items[0], "safe_value")
|
||||||
|
XCTAssertTrue(items[1].contains("[REDACTED_EMAIL]"))
|
||||||
|
XCTAssertEqual(items[2], "another_safe_value")
|
||||||
|
} else {
|
||||||
|
XCTFail("Array should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test containsSensitiveDataInArray
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInArrayDetectsSensitiveString() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"keys": [
|
||||||
|
"nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
||||||
|
"clean_value"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let keys = scrubbedEvent?.extra?["keys"] as? [String] {
|
||||||
|
XCTAssertTrue(keys[0].contains("[REDACTED_NSEC]"))
|
||||||
|
XCTAssertEqual(keys[1], "clean_value")
|
||||||
|
} else {
|
||||||
|
XCTFail("Array with sensitive data should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInArrayNestedDictionary() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"users": [
|
||||||
|
["name": "Alice", "email": "alice@example.com"],
|
||||||
|
["name": "Bob", "id": "safe_id"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let users = scrubbedEvent?.extra?["users"] as? [[String: String]] {
|
||||||
|
XCTAssertEqual(users[0]["name"], "Alice")
|
||||||
|
XCTAssertTrue(users[0]["email"]?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
XCTAssertEqual(users[1]["name"], "Bob")
|
||||||
|
XCTAssertEqual(users[1]["id"], "safe_id")
|
||||||
|
} else {
|
||||||
|
XCTFail("Array with nested dictionaries should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInArrayNestedArray() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"matrix": [
|
||||||
|
["safe1", "safe2"],
|
||||||
|
["3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "safe3"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let matrix = scrubbedEvent?.extra?["matrix"] as? [[String]] {
|
||||||
|
XCTAssertEqual(matrix[0][0], "safe1")
|
||||||
|
XCTAssertEqual(matrix[0][1], "safe2")
|
||||||
|
XCTAssertTrue(matrix[1][0].contains("[REDACTED_HEX]"))
|
||||||
|
XCTAssertEqual(matrix[1][1], "safe3")
|
||||||
|
} else {
|
||||||
|
XCTFail("Nested array should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContainsSensitiveDataInArrayAllCleanValues() {
|
||||||
|
let event = Event()
|
||||||
|
event.extra = [
|
||||||
|
"items": ["value1", "value2", "value3"]
|
||||||
|
]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.extra)
|
||||||
|
if let items = scrubbedEvent?.extra?["items"] as? [String] {
|
||||||
|
XCTAssertEqual(items, ["value1", "value2", "value3"])
|
||||||
|
} else {
|
||||||
|
XCTFail("Clean array should remain unchanged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test scrubStacktrace
|
||||||
|
|
||||||
|
func testScrubStacktraceFunction() {
|
||||||
|
let event = Event()
|
||||||
|
let exception = Exception(value: "Test error", type: "TestException")
|
||||||
|
|
||||||
|
let frame1 = Frame()
|
||||||
|
frame1.function = "loginWithKey(nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq)"
|
||||||
|
|
||||||
|
let frame2 = Frame()
|
||||||
|
frame2.function = "processUser(user@example.com)"
|
||||||
|
|
||||||
|
let stacktrace = SentryStacktrace(frames: [frame1, frame2], registers: [:])
|
||||||
|
exception.stacktrace = stacktrace
|
||||||
|
event.exceptions = [exception]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions)
|
||||||
|
if let frames = scrubbedEvent?.exceptions?.first?.stacktrace?.frames {
|
||||||
|
XCTAssertEqual(frames.count, 2)
|
||||||
|
XCTAssertTrue(frames[0].function?.contains("[REDACTED_NSEC]") ?? false)
|
||||||
|
XCTAssertFalse(frames[0].function?.contains("nsec1") ?? true)
|
||||||
|
XCTAssertTrue(frames[1].function?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
XCTAssertFalse(frames[1].function?.contains("user@example.com") ?? true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Stacktrace frames should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubStacktraceFileName() {
|
||||||
|
let event = Event()
|
||||||
|
let exception = Exception(value: "Test error", type: "TestException")
|
||||||
|
|
||||||
|
let frame = Frame()
|
||||||
|
frame.function = "testFunction"
|
||||||
|
frame.fileName = "UserProfile_npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123.swift"
|
||||||
|
|
||||||
|
let stacktrace = SentryStacktrace(frames: [frame], registers: [:])
|
||||||
|
exception.stacktrace = stacktrace
|
||||||
|
event.exceptions = [exception]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions)
|
||||||
|
if let frames = scrubbedEvent?.exceptions?.first?.stacktrace?.frames {
|
||||||
|
XCTAssertEqual(frames.count, 1)
|
||||||
|
XCTAssertTrue(frames[0].fileName?.contains("[REDACTED_NPUB]") ?? false)
|
||||||
|
XCTAssertFalse(frames[0].fileName?.contains("npub1abc123") ?? true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Stacktrace filename should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubStacktraceVars() {
|
||||||
|
let event = Event()
|
||||||
|
let exception = Exception(value: "Test error", type: "TestException")
|
||||||
|
|
||||||
|
let frame = Frame()
|
||||||
|
frame.function = "processData"
|
||||||
|
frame.vars = [
|
||||||
|
"userKey": "npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123",
|
||||||
|
"email": "test@example.com",
|
||||||
|
"count": "5"
|
||||||
|
]
|
||||||
|
|
||||||
|
let stacktrace = SentryStacktrace(frames: [frame], registers: [:])
|
||||||
|
exception.stacktrace = stacktrace
|
||||||
|
event.exceptions = [exception]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions)
|
||||||
|
if let frames = scrubbedEvent?.exceptions?.first?.stacktrace?.frames,
|
||||||
|
let vars = frames[0].vars {
|
||||||
|
XCTAssertTrue((vars["userKey"] as? String)?.contains("[REDACTED_NPUB]") ?? false)
|
||||||
|
XCTAssertTrue((vars["email"] as? String)?.contains("[REDACTED_EMAIL]") ?? false)
|
||||||
|
XCTAssertEqual(vars["count"] as? String, "5")
|
||||||
|
} else {
|
||||||
|
XCTFail("Stacktrace vars should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubStacktraceMultipleFrames() {
|
||||||
|
let event = Event()
|
||||||
|
let exception = Exception(value: "Test error", type: "TestException")
|
||||||
|
|
||||||
|
let frame1 = Frame()
|
||||||
|
frame1.function = "topLevel"
|
||||||
|
frame1.vars = ["key": "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"]
|
||||||
|
|
||||||
|
let frame2 = Frame()
|
||||||
|
frame2.function = "middleLevel with 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d hex"
|
||||||
|
frame2.fileName = "path/to/NormalFile.swift"
|
||||||
|
|
||||||
|
let frame3 = Frame()
|
||||||
|
frame3.function = "bottomLevel(email: admin@example.com)"
|
||||||
|
|
||||||
|
let stacktrace = SentryStacktrace(frames: [frame1, frame2, frame3], registers: [:])
|
||||||
|
exception.stacktrace = stacktrace
|
||||||
|
event.exceptions = [exception]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions)
|
||||||
|
if let frames = scrubbedEvent?.exceptions?.first?.stacktrace?.frames {
|
||||||
|
XCTAssertEqual(frames.count, 3)
|
||||||
|
|
||||||
|
// Check frame 1
|
||||||
|
let frame1Key = frames[0].vars?["key"] as? String
|
||||||
|
XCTAssertNotNil(frame1Key, "Frame 1 should have vars with key")
|
||||||
|
XCTAssertTrue(frame1Key?.contains("[REDACTED_NSEC]") ?? false, "Frame 1 key should be redacted, got: \(frame1Key ?? "nil")")
|
||||||
|
|
||||||
|
// Check frame 2
|
||||||
|
let frame2Function = frames[1].function
|
||||||
|
XCTAssertNotNil(frame2Function, "Frame 2 should have function")
|
||||||
|
XCTAssertTrue(frame2Function?.contains("[REDACTED_HEX]") ?? false, "Frame 2 function should be redacted, got: \(frame2Function ?? "nil")")
|
||||||
|
|
||||||
|
// Check frame 3
|
||||||
|
let frame3Function = frames[2].function
|
||||||
|
XCTAssertNotNil(frame3Function, "Frame 3 should have function")
|
||||||
|
XCTAssertTrue(frame3Function?.contains("[REDACTED_EMAIL]") ?? false, "Frame 3 function should be redacted, got: \(frame3Function ?? "nil")")
|
||||||
|
} else {
|
||||||
|
XCTFail("Multiple stacktrace frames should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubStacktraceInThread() {
|
||||||
|
let event = Event()
|
||||||
|
let thread = SentryThread(threadId: 1)
|
||||||
|
|
||||||
|
let frame = Frame()
|
||||||
|
frame.function = "authenticate(nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq)"
|
||||||
|
|
||||||
|
let stacktrace = SentryStacktrace(frames: [frame], registers: [:])
|
||||||
|
thread.stacktrace = stacktrace
|
||||||
|
event.threads = [thread]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.threads)
|
||||||
|
if let threads = scrubbedEvent?.threads,
|
||||||
|
let frames = threads.first?.stacktrace?.frames {
|
||||||
|
XCTAssertTrue(frames[0].function?.contains("[REDACTED_NSEC]") ?? false)
|
||||||
|
XCTAssertFalse(frames[0].function?.contains("nsec1") ?? true)
|
||||||
|
} else {
|
||||||
|
XCTFail("Thread stacktrace should be scrubbed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testScrubStacktraceEmptyVars() {
|
||||||
|
let event = Event()
|
||||||
|
let exception = Exception(value: "Test error", type: "TestException")
|
||||||
|
|
||||||
|
let frame = Frame()
|
||||||
|
frame.function = "testFunction"
|
||||||
|
frame.vars = [:] // Empty vars
|
||||||
|
|
||||||
|
let stacktrace = SentryStacktrace(frames: [frame], registers: [:])
|
||||||
|
exception.stacktrace = stacktrace
|
||||||
|
event.exceptions = [exception]
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions)
|
||||||
|
// Should not crash with empty vars
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.exceptions?.first?.stacktrace?.frames)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test Real-world Scenarios
|
||||||
|
|
||||||
|
func testScrubComplexErrorMessage() {
|
||||||
|
let event = Event()
|
||||||
|
event.message = SentryMessage(formatted: """
|
||||||
|
Authentication failed for user npub1abc123abc123abc123abc123abc123abc123abc123abc123abc123abc123
|
||||||
|
Private key: nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
|
||||||
|
Hex ID: 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d
|
||||||
|
Contact: support@damus.io
|
||||||
|
Note: note1xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz7
|
||||||
|
Event: nevent1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789
|
||||||
|
Profile: nprofile1qqsxyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789xyz789
|
||||||
|
""")
|
||||||
|
|
||||||
|
let scrubbedEvent = DamusSentry.scrubSensitiveData(in: event)
|
||||||
|
|
||||||
|
XCTAssertNotNil(scrubbedEvent?.message?.formatted)
|
||||||
|
let message = scrubbedEvent?.message?.formatted ?? ""
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NPUB]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NSEC]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_HEX]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_EMAIL]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NOTE]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NEVENT]"))
|
||||||
|
XCTAssertTrue(message.contains("[REDACTED_NPROFILE]"))
|
||||||
|
|
||||||
|
// Ensure original sensitive data is not present
|
||||||
|
XCTAssertFalse(message.contains("npub1abc"))
|
||||||
|
XCTAssertFalse(message.contains("nsec1qqq"))
|
||||||
|
XCTAssertFalse(message.contains("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"))
|
||||||
|
XCTAssertFalse(message.contains("support@damus.io"))
|
||||||
|
XCTAssertFalse(message.contains("note1xyz"))
|
||||||
|
XCTAssertFalse(message.contains("nevent1qqsxyz"))
|
||||||
|
XCTAssertFalse(message.contains("nprofile1qqsxyz"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"Sentry": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://mcp.sentry.dev/mcp/damus-nostr-inc/apple-ios",
|
||||||
|
"oauth": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user