feat(notifications): add structured observability for push wake and refresh flows
Introduce NotificationDebugEvents and [Notifications] console/panel logging for push handlers, token registration, refresh timing, schedule replacement, and WAKEUP_PING.
This commit is contained in:
@@ -277,8 +277,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||||
import { copyToClipboard } from "@/services/ClipboardService";
|
import { copyToClipboard } from "@/services/ClipboardService";
|
||||||
|
import { subscribe } from "@/services/notifications/NotificationDebugEvents";
|
||||||
import { NotificationDebugService } from "@/services/notifications/NotificationDebugService";
|
import { NotificationDebugService } from "@/services/notifications/NotificationDebugService";
|
||||||
|
|
||||||
type PendingInfo = {
|
type PendingInfo = {
|
||||||
@@ -319,7 +320,8 @@ const truncatedFcmToken = computed(() => {
|
|||||||
return `${t.slice(0, 12)}…${t.slice(-8)}`;
|
return `${t.slice(0, 12)}…${t.slice(-8)}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventLog = computed(() => NotificationDebugService.eventLog.value);
|
const eventLog = ref<string[]>([]);
|
||||||
|
let unsubscribeEventLog: (() => void) | undefined;
|
||||||
|
|
||||||
const intervalLabel = computed(() => {
|
const intervalLabel = computed(() => {
|
||||||
const preset = presets.find((p) => p.ms === intervalMs.value);
|
const preset = presets.find((p) => p.ms === intervalMs.value);
|
||||||
@@ -428,7 +430,14 @@ async function onCopyFcmToken(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
unsubscribeEventLog = subscribe((entries) => {
|
||||||
|
eventLog.value = [...entries];
|
||||||
|
});
|
||||||
syncBackendState();
|
syncBackendState();
|
||||||
void refreshPending();
|
void refreshPending();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
unsubscribeEventLog?.();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ import {
|
|||||||
getNotificationApiBaseUrl,
|
getNotificationApiBaseUrl,
|
||||||
getTestMode,
|
getTestMode,
|
||||||
} from "./NotificationDebugConfig";
|
} from "./NotificationDebugConfig";
|
||||||
|
import {
|
||||||
|
logNotificationClearing,
|
||||||
|
logRefreshFailure,
|
||||||
|
logRefreshStarted,
|
||||||
|
logRefreshSuccess,
|
||||||
|
logScheduleReplacement,
|
||||||
|
} from "./notificationLog";
|
||||||
|
import { logNotification } from "./NotificationDebugEvents";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended type for DailyNotification that includes the actual Swift implementation
|
* Extended type for DailyNotification that includes the actual Swift implementation
|
||||||
@@ -560,12 +568,19 @@ export type RefreshNotificationsResult = {
|
|||||||
* Re-applies native API fetcher credentials (JWT pool, active DID) so background
|
* Re-applies native API fetcher credentials (JWT pool, active DID) so background
|
||||||
* notification workers can run. No UI; safe from push handlers while backgrounded.
|
* notification workers can run. No UI; safe from push handlers while backgrounded.
|
||||||
*/
|
*/
|
||||||
export async function refreshNotificationsWithDiagnostics(): Promise<RefreshNotificationsResult> {
|
export async function refreshNotificationsWithDiagnostics(options?: {
|
||||||
|
source?: string;
|
||||||
|
}): Promise<RefreshNotificationsResult> {
|
||||||
|
const startedAt = performance.now();
|
||||||
|
logRefreshStarted(options?.source);
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
|
const errorMessage = "not a native platform";
|
||||||
|
logRefreshFailure(startedAt, errorMessage);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
scheduledCount: 0,
|
scheduledCount: 0,
|
||||||
errorMessage: "not a native platform",
|
errorMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,15 +596,17 @@ export async function refreshNotificationsWithDiagnostics(): Promise<RefreshNoti
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
const errorMessage = res.statusText || `HTTP ${res.status}`;
|
||||||
logger.warn("[NativeNotificationService] refreshNotifications failed", {
|
logger.warn("[NativeNotificationService] refreshNotifications failed", {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
statusText: res.statusText,
|
statusText: res.statusText,
|
||||||
});
|
});
|
||||||
|
logRefreshFailure(startedAt, errorMessage, res.status);
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
scheduledCount: 0,
|
scheduledCount: 0,
|
||||||
status: res.status,
|
status: res.status,
|
||||||
errorMessage: res.statusText || `HTTP ${res.status}`,
|
errorMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,10 +616,12 @@ export async function refreshNotificationsWithDiagnostics(): Promise<RefreshNoti
|
|||||||
? payload.nextNotifications.length
|
? payload.nextNotifications.length
|
||||||
: 0;
|
: 0;
|
||||||
await applyNotificationRefreshPayload(data);
|
await applyNotificationRefreshPayload(data);
|
||||||
|
logRefreshSuccess(startedAt, scheduledCount);
|
||||||
return { ok: true, scheduledCount };
|
return { ok: true, scheduledCount };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error("[NativeNotificationService] Refresh failed", err);
|
logger.error("[NativeNotificationService] Refresh failed", err);
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
logRefreshFailure(startedAt, message);
|
||||||
return { ok: false, scheduledCount: 0, errorMessage: message };
|
return { ok: false, scheduledCount: 0, errorMessage: message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -648,15 +667,14 @@ export async function applyNotificationRefreshPayload(
|
|||||||
.filter((t): t is number => typeof t === "number" && Number.isFinite(t));
|
.filter((t): t is number => typeof t === "number" && Number.isFinite(t));
|
||||||
|
|
||||||
if (timestamps.length === 0) {
|
if (timestamps.length === 0) {
|
||||||
|
logNotification("Schedule replacement skipped (no valid timestamps)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep existing behavior: ensure background worker credentials are current.
|
// Keep existing behavior: ensure background worker credentials are current.
|
||||||
await configureNativeFetcherIfReady();
|
await configureNativeFetcherIfReady();
|
||||||
|
|
||||||
// Backend (or debug harness) is the source of truth: apply exact timestamps.
|
logScheduleReplacement(timestamps.length);
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log("[Notifications] Applying timestamps:", nextNotifications);
|
|
||||||
|
|
||||||
const plugin = DailyNotification as unknown as {
|
const plugin = DailyNotification as unknown as {
|
||||||
clearAllNotifications?: () => Promise<void>;
|
clearAllNotifications?: () => Promise<void>;
|
||||||
@@ -667,14 +685,18 @@ export async function applyNotificationRefreshPayload(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (typeof plugin.clearAllNotifications === "function") {
|
if (typeof plugin.clearAllNotifications === "function") {
|
||||||
|
logNotificationClearing("clearAllNotifications");
|
||||||
await plugin.clearAllNotifications();
|
await plugin.clearAllNotifications();
|
||||||
} else if (typeof plugin.cancelAllNotifications === "function") {
|
} else if (typeof plugin.cancelAllNotifications === "function") {
|
||||||
// Back-compat: older builds expose cancelAllNotifications.
|
logNotificationClearing("cancelAllNotifications");
|
||||||
await plugin.cancelAllNotifications();
|
await plugin.cancelAllNotifications();
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"[NativeNotificationService] No clearAllNotifications/cancelAllNotifications on plugin; cannot replace schedule",
|
"[NativeNotificationService] No clearAllNotifications/cancelAllNotifications on plugin; cannot replace schedule",
|
||||||
);
|
);
|
||||||
|
logNotification(
|
||||||
|
"Schedule replacement aborted (clear method unavailable on plugin)",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,10 +704,16 @@ export async function applyNotificationRefreshPayload(
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
"[NativeNotificationService] scheduleNotifications not available on plugin; cannot apply timestamps",
|
"[NativeNotificationService] scheduleNotifications not available on plugin; cannot apply timestamps",
|
||||||
);
|
);
|
||||||
|
logNotification(
|
||||||
|
"Schedule replacement aborted (scheduleNotifications unavailable)",
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await plugin.scheduleNotifications({ timestamps });
|
await plugin.scheduleNotifications({ timestamps });
|
||||||
|
logNotification(
|
||||||
|
`Schedule replacement applied (${timestamps.length} timestamp(s))`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -695,6 +723,13 @@ export async function handleCapacitorPushNotificationReceived(
|
|||||||
notification: PushNotificationSchema,
|
notification: PushNotificationSchema,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (notification.data?.type === "WAKEUP_PING") {
|
if (notification.data?.type === "WAKEUP_PING") {
|
||||||
await refreshNotifications();
|
logNotification("WAKEUP_PING handler — invoking refresh");
|
||||||
|
await refreshNotificationsWithDiagnostics({ source: "WAKEUP_PING" });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const type =
|
||||||
|
typeof notification.data?.type === "string"
|
||||||
|
? notification.data.type
|
||||||
|
: "(none)";
|
||||||
|
logNotification(`push handler ignored type=${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
74
src/services/notifications/NotificationDebugEvents.ts
Normal file
74
src/services/notifications/NotificationDebugEvents.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Lightweight in-memory notification debug log + console observability.
|
||||||
|
* Used by production notification flows and the Notification Debug Panel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const NOTIFICATION_LOG_PREFIX = "[Notifications]";
|
||||||
|
|
||||||
|
const MAX_ENTRIES = 100;
|
||||||
|
|
||||||
|
type LogListener = (entries: readonly string[]) => void;
|
||||||
|
|
||||||
|
const entries: string[] = [];
|
||||||
|
const listeners = new Set<LogListener>();
|
||||||
|
|
||||||
|
function formatTime(d: Date): string {
|
||||||
|
const hh = d.getHours().toString().padStart(2, "0");
|
||||||
|
const mm = d.getMinutes().toString().padStart(2, "0");
|
||||||
|
const ss = d.getSeconds().toString().padStart(2, "0");
|
||||||
|
return `${hh}:${mm}:${ss}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPanelLine(message: string): string {
|
||||||
|
return `[${formatTime(new Date())}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyListeners(): void {
|
||||||
|
const snapshot = [...entries] as readonly string[];
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener(snapshot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Append a timestamped line to the in-memory debug log (panel). */
|
||||||
|
export function appendLog(message: string): void {
|
||||||
|
entries.push(formatPanelLine(message));
|
||||||
|
if (entries.length > MAX_ENTRIES) {
|
||||||
|
entries.splice(0, entries.length - MAX_ENTRIES);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribe(listener: LogListener): () => void {
|
||||||
|
listeners.add(listener);
|
||||||
|
listener([...entries]);
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearNotificationDebugLogs(): void {
|
||||||
|
entries.length = 0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNotificationDebugLogEntries(): readonly string[] {
|
||||||
|
return [...entries];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structured console log (`[Notifications] …`) plus debug panel entry.
|
||||||
|
*/
|
||||||
|
export function logNotification(
|
||||||
|
message: string,
|
||||||
|
detail?: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
if (detail !== undefined) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`${NOTIFICATION_LOG_PREFIX} ${message}`, detail);
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`${NOTIFICATION_LOG_PREFIX} ${message}`);
|
||||||
|
}
|
||||||
|
appendLog(message);
|
||||||
|
}
|
||||||
@@ -8,9 +8,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
import { ref, readonly, type Ref } from "vue";
|
|
||||||
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
import type { PushNotificationSchema } from "@capacitor/push-notifications";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
|
import {
|
||||||
|
clearNotificationDebugLogs,
|
||||||
|
logNotification,
|
||||||
|
} from "./NotificationDebugEvents";
|
||||||
|
import { logNotificationClearing } from "./notificationLog";
|
||||||
import {
|
import {
|
||||||
getBackendBaseUrl,
|
getBackendBaseUrl,
|
||||||
getNotificationApiBaseUrl,
|
getNotificationApiBaseUrl,
|
||||||
@@ -56,40 +60,9 @@ function isUnimplementedError(e: unknown): boolean {
|
|||||||
|
|
||||||
const LOG = "[NotificationDebugService]";
|
const LOG = "[NotificationDebugService]";
|
||||||
|
|
||||||
function formatTime(d: Date): string {
|
|
||||||
const hh = d.getHours().toString().padStart(2, "0");
|
|
||||||
const mm = d.getMinutes().toString().padStart(2, "0");
|
|
||||||
const ss = d.getSeconds().toString().padStart(2, "0");
|
|
||||||
return `${hh}:${mm}:${ss}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function logLine(message: string): string {
|
|
||||||
return `[${formatTime(new Date())}] ${message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateToken(token: string): string {
|
|
||||||
const t = token.trim();
|
|
||||||
if (t.length <= 24) {
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
return `${t.slice(0, 12)}…${t.slice(-8)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const _eventLog: Ref<string[]> = ref([]);
|
|
||||||
|
|
||||||
const EVENT_LOG_MAX = 100;
|
|
||||||
|
|
||||||
function append(message: string): void {
|
|
||||||
const line = logLine(message);
|
|
||||||
_eventLog.value = [..._eventLog.value, line].slice(-EVENT_LOG_MAX);
|
|
||||||
logger.debug(`${LOG} ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotificationDebugService = {
|
export const NotificationDebugService = {
|
||||||
eventLog: readonly(_eventLog),
|
|
||||||
|
|
||||||
clearDebugLogs(): void {
|
clearDebugLogs(): void {
|
||||||
_eventLog.value = [];
|
clearNotificationDebugLogs();
|
||||||
},
|
},
|
||||||
|
|
||||||
getActiveBackendUrl(): string {
|
getActiveBackendUrl(): string {
|
||||||
@@ -102,7 +75,7 @@ export const NotificationDebugService = {
|
|||||||
|
|
||||||
saveBackendBaseUrl(url: string): void {
|
saveBackendBaseUrl(url: string): void {
|
||||||
setBackendBaseUrl(url);
|
setBackendBaseUrl(url);
|
||||||
append(
|
logNotification(
|
||||||
url.trim()
|
url.trim()
|
||||||
? `Backend URL saved (${getNotificationApiBaseUrl()})`
|
? `Backend URL saved (${getNotificationApiBaseUrl()})`
|
||||||
: "Backend URL cleared (using default)",
|
: "Backend URL cleared (using default)",
|
||||||
@@ -111,7 +84,7 @@ export const NotificationDebugService = {
|
|||||||
|
|
||||||
setTestModeEnabled(enabled: boolean): void {
|
setTestModeEnabled(enabled: boolean): void {
|
||||||
setTestMode(enabled);
|
setTestMode(enabled);
|
||||||
append(`Test mode ${enabled ? "enabled" : "disabled"}`);
|
logNotification(`Test mode ${enabled ? "enabled" : "disabled"}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
isTestModeEnabled(): boolean {
|
isTestModeEnabled(): boolean {
|
||||||
@@ -123,34 +96,20 @@ export const NotificationDebugService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async registerTokenNow(): Promise<void> {
|
async registerTokenNow(): Promise<void> {
|
||||||
append("Register token started");
|
logNotification("Register token now (debug panel)");
|
||||||
try {
|
await reregisterFcmTokenNow();
|
||||||
const token = await reregisterFcmTokenNow();
|
|
||||||
append(`Register token success (${truncateToken(token)})`);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const msg = e instanceof Error ? e.message : String(e);
|
|
||||||
append(`Register token failure: ${msg}`);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async triggerBackendRefresh(): Promise<void> {
|
async triggerBackendRefresh(): Promise<void> {
|
||||||
append("Refresh started");
|
await refreshNotificationsWithDiagnostics({ source: "debug panel" });
|
||||||
const result = await refreshNotificationsWithDiagnostics();
|
|
||||||
if (result.ok) {
|
|
||||||
append(`Refresh success`);
|
|
||||||
append(`Scheduled ${result.scheduledCount} notifications`);
|
|
||||||
} else {
|
|
||||||
const detail = result.errorMessage ?? "unknown error";
|
|
||||||
const status = result.status != null ? ` (HTTP ${result.status})` : "";
|
|
||||||
append(`Refresh failure: ${detail}${status}`);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Local simulation: same API call as a WAKEUP_PING handler (no push payload). */
|
/** Local simulation: same API call as a WAKEUP_PING handler (no push payload). */
|
||||||
async simulateWakeupViaRefresh(): Promise<void> {
|
async simulateWakeupViaRefresh(): Promise<void> {
|
||||||
append("WAKEUP_PING simulation started (local refresh API only)");
|
logNotification("WAKEUP_PING simulation (local refresh API only)");
|
||||||
await this.triggerBackendRefresh();
|
await refreshNotificationsWithDiagnostics({
|
||||||
|
source: "WAKEUP_PING simulation",
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
generateMockNotifications(
|
generateMockNotifications(
|
||||||
@@ -167,31 +126,26 @@ export const NotificationDebugService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async triggerMockRefresh(intervalMs?: number): Promise<void> {
|
async triggerMockRefresh(intervalMs?: number): Promise<void> {
|
||||||
append("Refresh requested (mock)");
|
logNotification("Mock refresh requested");
|
||||||
|
|
||||||
const payload = this.generateMockNotifications(intervalMs);
|
const payload = this.generateMockNotifications(intervalMs);
|
||||||
const timestamps = payload.nextNotifications?.map((n) => n.timestamp) ?? [];
|
const timestamps = payload.nextNotifications?.map((n) => n.timestamp) ?? [];
|
||||||
append(`Mock payload generated (${timestamps.length} timestamps)`);
|
logNotification(`Mock payload generated (${timestamps.length} timestamps)`);
|
||||||
for (const ts of timestamps) {
|
|
||||||
if (typeof ts === "number") {
|
|
||||||
append(`Scheduling timestamp ${ts}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
append("Skipped: not running on native platform");
|
logNotification("Mock refresh skipped: not running on native platform");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await applyNotificationRefreshPayload(payload);
|
await applyNotificationRefreshPayload(payload);
|
||||||
append("Mock refresh applied");
|
logNotification("Mock refresh applied");
|
||||||
},
|
},
|
||||||
|
|
||||||
async simulateWakeupPing(): Promise<void> {
|
async simulateWakeupPing(): Promise<void> {
|
||||||
append("Simulating WAKEUP_PING");
|
logNotification("Simulating WAKEUP_PING (production push handler)");
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
append("Skipped: not running on native platform");
|
logNotification("WAKEUP_PING simulation skipped: not native platform");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,23 +157,22 @@ export const NotificationDebugService = {
|
|||||||
} as unknown as PushNotificationSchema;
|
} as unknown as PushNotificationSchema;
|
||||||
|
|
||||||
await handleCapacitorPushNotificationReceived(notification);
|
await handleCapacitorPushNotificationReceived(notification);
|
||||||
append("WAKEUP_PING handled (production handler)");
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async runFloodTest(intervalMs?: number): Promise<void> {
|
async runFloodTest(intervalMs?: number): Promise<void> {
|
||||||
append("Flood test started (20 sequential refreshes)");
|
logNotification("Flood test started (20 sequential refreshes)");
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
append(`Flood iteration ${i + 1}/20`);
|
logNotification(`Flood iteration ${i + 1}/20`);
|
||||||
await this.triggerMockRefresh(intervalMs);
|
await this.triggerMockRefresh(intervalMs);
|
||||||
}
|
}
|
||||||
append("Flood test completed");
|
logNotification("Flood test completed");
|
||||||
},
|
},
|
||||||
|
|
||||||
async clearNotifications(): Promise<void> {
|
async clearNotifications(): Promise<void> {
|
||||||
append("Clearing notifications");
|
logNotification("Clear notifications (debug panel)");
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
append("Skipped: not running on native platform");
|
logNotification("Clear skipped: not running on native platform");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,29 +182,31 @@ export const NotificationDebugService = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (typeof plugin.clearAllNotifications === "function") {
|
if (typeof plugin.clearAllNotifications === "function") {
|
||||||
|
logNotificationClearing("clearAllNotifications");
|
||||||
await plugin.clearAllNotifications();
|
await plugin.clearAllNotifications();
|
||||||
} else if (typeof plugin.cancelAllNotifications === "function") {
|
} else if (typeof plugin.cancelAllNotifications === "function") {
|
||||||
|
logNotificationClearing("cancelAllNotifications");
|
||||||
await plugin.cancelAllNotifications();
|
await plugin.cancelAllNotifications();
|
||||||
} else {
|
} else {
|
||||||
append("Clear not available (plugin method missing)");
|
logNotification("Clear not available (plugin method missing)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
append("Cleared notifications");
|
logNotification("Notifications cleared");
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPendingNotifications(): Promise<PendingNotificationsResult> {
|
async getPendingNotifications(): Promise<PendingNotificationsResult> {
|
||||||
append("Fetching pending notifications");
|
logNotification("Fetching pending notifications");
|
||||||
|
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) {
|
||||||
append("Skipped: not running on native platform");
|
logNotification("Pending fetch skipped: not running on native platform");
|
||||||
return { pending: [] };
|
return { pending: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await NotificationInspector.getPendingNotifications();
|
const res = await NotificationInspector.getPendingNotifications();
|
||||||
const items = (res?.pending ?? []) as PendingNotificationInfo[];
|
const items = (res?.pending ?? []) as PendingNotificationInfo[];
|
||||||
append(`Pending fetched (${items.length})`);
|
logNotification(`Pending fetched (${items.length})`);
|
||||||
return { pending: items };
|
return { pending: items };
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (isUnimplementedError(e)) {
|
if (isUnimplementedError(e)) {
|
||||||
@@ -261,7 +216,7 @@ export const NotificationDebugService = {
|
|||||||
"Pending notification inspection is currently supported on iOS only.",
|
"Pending notification inspection is currently supported on iOS only.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
append("Pending fetch failed");
|
logNotification("Pending fetch failed");
|
||||||
logger.warn(`${LOG} getPendingNotifications failed`, e);
|
logger.warn(`${LOG} getPendingNotifications failed`, e);
|
||||||
return { pending: [] };
|
return { pending: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ import {
|
|||||||
getNotificationApiBaseUrl,
|
getNotificationApiBaseUrl,
|
||||||
getTestMode,
|
getTestMode,
|
||||||
} from "./NotificationDebugConfig";
|
} from "./NotificationDebugConfig";
|
||||||
|
import {
|
||||||
|
logTokenRegistrationFailure,
|
||||||
|
logTokenRegistrationStarted,
|
||||||
|
logTokenRegistrationSuccess,
|
||||||
|
} from "./notificationLog";
|
||||||
import { NativeNotificationService } from "./NativeNotificationService";
|
import { NativeNotificationService } from "./NativeNotificationService";
|
||||||
import { WebPushNotificationService } from "./WebPushNotificationService";
|
import { WebPushNotificationService } from "./WebPushNotificationService";
|
||||||
|
|
||||||
@@ -27,8 +32,10 @@ import { WebPushNotificationService } from "./WebPushNotificationService";
|
|||||||
* Registers an FCM device token with the app backend (native Capacitor token or web getToken).
|
* Registers an FCM device token with the app backend (native Capacitor token or web getToken).
|
||||||
*/
|
*/
|
||||||
export async function registerToken(fcmToken: string): Promise<void> {
|
export async function registerToken(fcmToken: string): Promise<void> {
|
||||||
|
logTokenRegistrationStarted(fcmToken);
|
||||||
const deviceId = await getOrCreateDeviceId();
|
const deviceId = await getOrCreateDeviceId();
|
||||||
const baseUrl = getNotificationApiBaseUrl();
|
const baseUrl = getNotificationApiBaseUrl();
|
||||||
|
try {
|
||||||
const res = await fetch(`${baseUrl}/notifications/register`, {
|
const res = await fetch(`${baseUrl}/notifications/register`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -46,6 +53,11 @@ export async function registerToken(fcmToken: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
throw new Error(`registerToken failed: HTTP ${res.status}`);
|
throw new Error(`registerToken failed: HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
|
logTokenRegistrationSuccess(fcmToken);
|
||||||
|
} catch (err) {
|
||||||
|
logTokenRegistrationFailure(fcmToken, err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ import {
|
|||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
import { handleCapacitorPushNotificationReceived } from "./NativeNotificationService";
|
||||||
import { registerToken } from "./NotificationService";
|
import { registerToken } from "./NotificationService";
|
||||||
|
import {
|
||||||
|
logPushNotificationActionPerformed,
|
||||||
|
logPushNotificationReceived,
|
||||||
|
logTokenRegistrationSkippedDuplicate,
|
||||||
|
} from "./notificationLog";
|
||||||
|
|
||||||
const LOG = "[FirebaseMessaging]";
|
const LOG = "[FirebaseMessaging]";
|
||||||
|
|
||||||
@@ -43,6 +48,7 @@ async function registerRetrievedToken(
|
|||||||
}
|
}
|
||||||
lastSeenFcmToken = trimmed;
|
lastSeenFcmToken = trimmed;
|
||||||
if (!options?.force && trimmed === lastRegisteredFcmToken) {
|
if (!options?.force && trimmed === lastRegisteredFcmToken) {
|
||||||
|
logTokenRegistrationSkippedDuplicate(trimmed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await registerToken(trimmed);
|
await registerToken(trimmed);
|
||||||
@@ -246,6 +252,7 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
|||||||
"pushNotificationReceived",
|
"pushNotificationReceived",
|
||||||
(notification) => {
|
(notification) => {
|
||||||
logger.debug(`${LOG} pushNotificationReceived`, notification);
|
logger.debug(`${LOG} pushNotificationReceived`, notification);
|
||||||
|
logPushNotificationReceived(notification);
|
||||||
void handleCapacitorPushNotificationReceived(notification).catch(
|
void handleCapacitorPushNotificationReceived(notification).catch(
|
||||||
(err) => {
|
(err) => {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -261,6 +268,7 @@ async function initializeNativePushAndFirebaseMessagingImpl(): Promise<void> {
|
|||||||
"pushNotificationActionPerformed",
|
"pushNotificationActionPerformed",
|
||||||
(action) => {
|
(action) => {
|
||||||
logger.debug(`${LOG} pushNotificationActionPerformed`, action);
|
logger.debug(`${LOG} pushNotificationActionPerformed`, action);
|
||||||
|
logPushNotificationActionPerformed(action);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,14 @@ export {
|
|||||||
setBackendBaseUrl,
|
setBackendBaseUrl,
|
||||||
setTestMode,
|
setTestMode,
|
||||||
} from "./NotificationDebugConfig";
|
} from "./NotificationDebugConfig";
|
||||||
|
export {
|
||||||
|
appendLog,
|
||||||
|
clearNotificationDebugLogs,
|
||||||
|
getNotificationDebugLogEntries,
|
||||||
|
logNotification,
|
||||||
|
NOTIFICATION_LOG_PREFIX,
|
||||||
|
subscribe,
|
||||||
|
} from "./NotificationDebugEvents";
|
||||||
export { NotificationService, registerToken } from "./NotificationService";
|
export { NotificationService, registerToken } from "./NotificationService";
|
||||||
export { NativeNotificationService } from "./NativeNotificationService";
|
export { NativeNotificationService } from "./NativeNotificationService";
|
||||||
export { WebPushNotificationService } from "./WebPushNotificationService";
|
export { WebPushNotificationService } from "./WebPushNotificationService";
|
||||||
|
|||||||
111
src/services/notifications/notificationLog.ts
Normal file
111
src/services/notifications/notificationLog.ts
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
/**
|
||||||
|
* Shared observability helpers for notification flows (console + debug panel).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logNotification } from "./NotificationDebugEvents";
|
||||||
|
|
||||||
|
export function truncateFcmTokenForLog(token: string): string {
|
||||||
|
const t = token.trim();
|
||||||
|
if (t.length <= 24) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
return `${t.slice(0, 12)}…${t.slice(-8)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logPushNotificationReceived(notification: {
|
||||||
|
title?: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
}): void {
|
||||||
|
const type =
|
||||||
|
typeof notification.data?.type === "string"
|
||||||
|
? notification.data.type
|
||||||
|
: "(none)";
|
||||||
|
logNotification(`pushNotificationReceived type=${type}`, {
|
||||||
|
title: notification.title,
|
||||||
|
dataType: type,
|
||||||
|
});
|
||||||
|
if (type === "WAKEUP_PING") {
|
||||||
|
logNotification("WAKEUP_PING received — will trigger refresh");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logPushNotificationActionPerformed(action: {
|
||||||
|
actionId?: string;
|
||||||
|
notification?: { title?: string; data?: Record<string, unknown> };
|
||||||
|
}): void {
|
||||||
|
const type =
|
||||||
|
typeof action.notification?.data?.type === "string"
|
||||||
|
? action.notification.data.type
|
||||||
|
: "(none)";
|
||||||
|
logNotification(
|
||||||
|
`pushNotificationActionPerformed actionId=${action.actionId ?? "(none)"} type=${type}`,
|
||||||
|
{
|
||||||
|
actionId: action.actionId,
|
||||||
|
dataType: type,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logTokenRegistrationStarted(token: string): void {
|
||||||
|
logNotification("Token registration started", {
|
||||||
|
token: truncateFcmTokenForLog(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logTokenRegistrationSuccess(token: string): void {
|
||||||
|
logNotification("Token registration success", {
|
||||||
|
token: truncateFcmTokenForLog(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logTokenRegistrationSkippedDuplicate(token: string): void {
|
||||||
|
logNotification("Token registration skipped (duplicate token)", {
|
||||||
|
token: truncateFcmTokenForLog(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logTokenRegistrationFailure(
|
||||||
|
token: string,
|
||||||
|
error: unknown,
|
||||||
|
): void {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logNotification(`Token registration failure: ${message}`, {
|
||||||
|
token: truncateFcmTokenForLog(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logRefreshStarted(source?: string): void {
|
||||||
|
logNotification(source ? `Refresh started (${source})` : "Refresh started");
|
||||||
|
}
|
||||||
|
|
||||||
|
function elapsedMsSince(startedAt: number): number {
|
||||||
|
return performance.now() - startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logRefreshSuccess(
|
||||||
|
startedAt: number,
|
||||||
|
scheduledCount: number,
|
||||||
|
): void {
|
||||||
|
logNotification(
|
||||||
|
`Refresh completed in ${Math.round(elapsedMsSince(startedAt))}ms (scheduled ${scheduledCount})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logRefreshFailure(
|
||||||
|
startedAt: number,
|
||||||
|
errorMessage: string,
|
||||||
|
status?: number,
|
||||||
|
): void {
|
||||||
|
const statusPart = status != null ? ` HTTP ${status}` : "";
|
||||||
|
logNotification(
|
||||||
|
`Refresh failed in ${Math.round(elapsedMsSince(startedAt))}ms: ${errorMessage}${statusPart}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logNotificationClearing(method: string): void {
|
||||||
|
logNotification(`Clearing notifications via ${method}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logScheduleReplacement(count: number): void {
|
||||||
|
logNotification(`Schedule replacement: ${count} notification(s)`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user