diff --git a/.changeset/cold-push-observability.md b/.changeset/cold-push-observability.md new file mode 100644 index 000000000..66ef60261 --- /dev/null +++ b/.changeset/cold-push-observability.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Make notifications more reliable when the app is closed on Android: the push is decrypted inside a short-lived foreground service instead of being cut short by battery saving. diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 79497d9bd..56e00a70e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -8325,7 +8325,7 @@ dependencies = [ [[package]] name = "tauri-plugin-notifications" version = "0.5.0" -source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=302944f44e0a876b39a37e4f08741379202bbdd4#302944f44e0a876b39a37e4f08741379202bbdd4" +source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=d06bc72dae599174ffa6d57c72c3a0d652346507#d06bc72dae599174ffa6d57c72c3a0d652346507" dependencies = [ "log", "notify-rust", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ddbbf1d75..5f189e059 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -109,12 +109,12 @@ windows = { version = "0.62", features = [ tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] } [target.'cfg(any(windows, target_os = "linux"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "302944f44e0a876b39a37e4f08741379202bbdd4" } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "d06bc72dae599174ffa6d57c72c3a0d652346507" } # default-features = false drops notify-rust so macOS uses the native # UNUserNotificationCenter backend (needs a signed .app to deliver). [target.'cfg(target_os = "macos")'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "302944f44e0a876b39a37e4f08741379202bbdd4", default-features = false } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "d06bc72dae599174ffa6d57c72c3a0d652346507", default-features = false } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = { version = "2", optional = true } @@ -138,7 +138,7 @@ libloading = "0.9" zbus = "5" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "302944f44e0a876b39a37e4f08741379202bbdd4", features = [ +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "d06bc72dae599174ffa6d57c72c3a0d652346507", features = [ "push-notifications", ] } tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" } diff --git a/src/app/features/settings/notifications/UnifiedPushNotifications.ts b/src/app/features/settings/notifications/UnifiedPushNotifications.ts index 529f97fe1..d22572eaf 100644 --- a/src/app/features/settings/notifications/UnifiedPushNotifications.ts +++ b/src/app/features/settings/notifications/UnifiedPushNotifications.ts @@ -1178,6 +1178,23 @@ export async function setEncryptedContentAllowed(allowed: boolean): Promise; + lastOutcome?: string; + lastAt: number; +}; + +export async function takePushDiagnostics(): Promise { + if (!isTauri()) return undefined; + try { + return await invoke(TAKE_PUSH_DIAGNOSTICS); + } catch { + return undefined; + } +} + export async function listenForUnifiedPushMessages(getSettings: () => NotificationSettings) { const dispatch = createUnifiedPushMessageListener( (notification) => handleUnifiedPushPayload(notification, getSettings), diff --git a/src/app/hooks/usePushDiagnosticsReport.ts b/src/app/hooks/usePushDiagnosticsReport.ts new file mode 100644 index 000000000..4f9c564b3 --- /dev/null +++ b/src/app/hooks/usePushDiagnosticsReport.ts @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import * as Sentry from '@sentry/react'; +import { takePushDiagnostics } from '$features/settings/notifications/UnifiedPushNotifications'; +import { isMobileTauri } from '$utils/platform'; + +const report = async (): Promise => { + const diagnostics = await takePushDiagnostics(); + if (!diagnostics) return; + + const entries = Object.entries(diagnostics.counts); + if (entries.length === 0) return; + + entries.forEach(([outcome, occurrences]) => { + Sentry.metrics.count('sable.push.cold_outcome', occurrences, { attributes: { outcome } }); + }); + + Sentry.addBreadcrumb({ + category: 'notification', + message: 'Cold push outcomes since last report', + level: 'info', + data: { ...diagnostics.counts, lastOutcome: diagnostics.lastOutcome }, + }); +}; + +export const usePushDiagnosticsReport = (): void => { + useEffect(() => { + if (!isMobileTauri()) return undefined; + + const drain = () => { + if (document.visibilityState === 'hidden') return; + void report(); + }; + + document.addEventListener('visibilitychange', drain); + drain(); + + return () => document.removeEventListener('visibilitychange', drain); + }, []); +}; diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index 1d9506ab5..11b9f033b 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -45,6 +45,7 @@ import { useAppVisibility } from '$hooks/useAppVisibility'; import { useNetworkRecovery } from '$hooks/useNetworkRecovery'; import { useLoopbackMediaRecovery } from '$hooks/useLoopbackMediaRecovery'; import { useSyncOrchestrator } from '$hooks/useSyncOrchestrator'; +import { usePushDiagnosticsReport } from '$hooks/usePushDiagnosticsReport'; import { composerIcon, DotsThreeOutlineVerticalIcon } from '$components/icons/phosphor'; import { getHomePath } from '$pages/pathUtils'; import { DIRECT_ROOM_PATH, HOME_ROOM_PATH, SPACE_ROOM_PATH } from '$pages/paths'; @@ -374,6 +375,7 @@ export function ClientRoot({ children }: ClientRootProps) { useAppVisibility(mx); useNetworkRecovery(mx); useSyncOrchestrator(startState.status === AsyncStatus.Success ? mx : undefined); + usePushDiagnosticsReport(); useLoopbackMediaRecovery(); useCrossSigningResetDetect(mx);