chore(logging): normalize wakeup flow observability with timings and summaries

Standardize console prefixes across scheduler, push, refresh, register,
auth, and debug endpoints. Add pass-level scheduler summaries, elapsed-time
logs, and masked-token-only push failure messages while reducing per-device
noise in scheduler loops.
This commit is contained in:
Jose Olarte III
2026-05-21 19:18:28 +08:00
parent e82c3ae5bc
commit f12dd03725
6 changed files with 184 additions and 68 deletions

View File

@@ -50,8 +50,8 @@ export async function requireAuth(
const errorTime = new Date().toISOString(); const errorTime = new Date().toISOString();
console.log("[Auth] Authentication failed"); console.log("[Auth] Authentication failed");
console.error( console.error(
errorTime, "[Auth] Invalid JWT at",
"Got invalid JWT in Authorization header:", errorTime + ":",
verified verified
); );
res.status(401).json({ res.status(401).json({
@@ -72,7 +72,11 @@ export async function requireAuth(
} catch (err) { } catch (err) {
const errorTime = new Date().toISOString(); const errorTime = new Date().toISOString();
console.log("[Auth] Authentication failed"); console.log("[Auth] Authentication failed");
console.error(errorTime, "Got invalid JWT in Authorization header:", err); console.error(
"[Auth] Invalid JWT at",
errorTime + ":",
err
);
res.status(401).json({ res.status(401).json({
success: false, success: false,
message: message:

View File

@@ -5,6 +5,7 @@ import {
computeNextEligibleAt, computeNextEligibleAt,
sendPushToDevice, sendPushToDevice,
} from "../services/pushService.js"; } from "../services/pushService.js";
import { formatElapsedMs } from "../util/formatElapsed.js";
import { maskToken } from "../util/maskToken.js"; import { maskToken } from "../util/maskToken.js";
// TODO: Protect this endpoint before production deployment // TODO: Protect this endpoint before production deployment
@@ -47,6 +48,7 @@ function sendWakeupFailureReason(
// TODO: Protect this endpoint before production deployment // TODO: Protect this endpoint before production deployment
debugRouter.get("/device/:token", async (req, res) => { debugRouter.get("/device/:token", async (req, res) => {
const started = Date.now();
const userId = req.did; const userId = req.did;
if (userId === undefined) { if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" }); res.status(401).json({ success: false, message: "Unauthorized" });
@@ -54,25 +56,33 @@ debugRouter.get("/device/:token", async (req, res) => {
} }
const fcmToken = decodeURIComponent(req.params.token); const fcmToken = decodeURIComponent(req.params.token);
const suffix = maskToken(fcmToken);
console.log("[DebugEndpoint] Device lookup request, token suffix:", suffix);
const row = await db.resolveOwnedDevice(userId, { fcmToken }); const row = await db.resolveOwnedDevice(userId, { fcmToken });
if (row === undefined) { if (row === undefined) {
console.log( console.log(
"[DebugEndpoint] Device lookup not found for user, token suffix:", "[DebugEndpoint] Device lookup not found in",
maskToken(fcmToken) formatElapsedMs(Date.now() - started) + ",",
"token suffix:",
suffix
); );
res.status(404).json({ error: "Device not found" }); res.status(404).json({ error: "Device not found" });
return; return;
} }
console.log(
"[DebugEndpoint] Device lookup for user, token suffix:",
maskToken(fcmToken)
);
res.json(deviceDebugPayload(row)); res.json(deviceDebugPayload(row));
console.log(
"[DebugEndpoint] Device lookup completed in",
formatElapsedMs(Date.now() - started) + ",",
"token suffix:",
suffix
);
}); });
// TODO: Protect this endpoint before production deployment // TODO: Protect this endpoint before production deployment
debugRouter.post("/send-wakeup", async (req, res) => { debugRouter.post("/send-wakeup", async (req, res) => {
const started = Date.now();
const userId = req.did; const userId = req.did;
if (userId === undefined) { if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" }); res.status(401).json({ success: false, message: "Unauthorized" });
@@ -81,6 +91,11 @@ debugRouter.post("/send-wakeup", async (req, res) => {
const { fcmToken } = req.body as { fcmToken?: unknown }; const { fcmToken } = req.body as { fcmToken?: unknown };
if (typeof fcmToken !== "string" || fcmToken.length === 0) { if (typeof fcmToken !== "string" || fcmToken.length === 0) {
console.log(
"[DebugEndpoint] Send-wakeup rejected in",
formatElapsedMs(Date.now() - started) + ":",
"fcmToken is required"
);
res.status(400).json({ res.status(400).json({
success: false, success: false,
failureReason: "fcmToken is required", failureReason: "fcmToken is required",
@@ -88,38 +103,41 @@ debugRouter.post("/send-wakeup", async (req, res) => {
return; return;
} }
const suffix = maskToken(fcmToken);
console.log("[DebugEndpoint] Send-wakeup request, token suffix:", suffix);
const row = await db.resolveOwnedDevice(userId, { fcmToken }); const row = await db.resolveOwnedDevice(userId, { fcmToken });
if (row === undefined) { if (row === undefined) {
console.log( console.log(
"[DebugEndpoint] Send-wakeup rejected, token suffix:", "[DebugEndpoint] Send-wakeup rejected in",
maskToken(fcmToken) formatElapsedMs(Date.now() - started) + ",",
"token suffix:",
suffix + ",",
"reason: Device not found"
); );
res.status(404).json({ res.status(404).json({
success: false, success: false,
failureReason: "Device not found", failureReason: "Device not found",
fcmTokenSuffix: maskToken(fcmToken), fcmTokenSuffix: suffix,
}); });
return; return;
} }
console.log(
"[DebugEndpoint] Send-wakeup for token suffix:",
maskToken(fcmToken)
);
const result = await sendPushToDevice(fcmToken); const result = await sendPushToDevice(fcmToken);
const success = result === "sent"; const success = result === "sent";
const failureReason = sendWakeupFailureReason(result); const failureReason = sendWakeupFailureReason(result);
console.log(
"[DebugEndpoint] Send-wakeup result:",
success ? "success" : result,
"token suffix:",
maskToken(fcmToken)
);
res.json({ res.json({
success, success,
...(failureReason !== undefined ? { failureReason } : {}), ...(failureReason !== undefined ? { failureReason } : {}),
fcmTokenSuffix: maskToken(fcmToken), fcmTokenSuffix: suffix,
}); });
console.log(
"[DebugEndpoint] Send-wakeup completed in",
formatElapsedMs(Date.now() - started) + ",",
success ? "success" : result + ",",
"token suffix:",
suffix
);
}); });

View File

@@ -1,6 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import { db } from "../db/fcmTokens.js"; import { db } from "../db/fcmTokens.js";
import { requireAuth } from "../middleware/auth.js"; import { requireAuth } from "../middleware/auth.js";
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
import { maskToken } from "../util/maskToken.js"; import { maskToken } from "../util/maskToken.js";
export const notificationsRouter = Router(); export const notificationsRouter = Router();
@@ -10,6 +11,7 @@ notificationsRouter.get("/", (_req, res) => {
}); });
notificationsRouter.post("/refresh", requireAuth, async (req, res) => { notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
const started = Date.now();
const userId = req.did; const userId = req.did;
if (userId === undefined) { if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" }); res.status(401).json({ success: false, message: "Unauthorized" });
@@ -26,10 +28,21 @@ notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
const token = const token =
typeof fcmToken === "string" && fcmToken.length > 0 ? fcmToken : undefined; typeof fcmToken === "string" && fcmToken.length > 0 ? fcmToken : undefined;
console.log(
"[Refresh] Request received",
canonicalDeviceId !== undefined ? `deviceId=${canonicalDeviceId}` : "",
token !== undefined ? `token suffix=${maskToken(token)}` : ""
);
if ( if (
(canonicalDeviceId === undefined || canonicalDeviceId.length === 0) && (canonicalDeviceId === undefined || canonicalDeviceId.length === 0) &&
token === undefined token === undefined
) { ) {
console.log(
"[Refresh] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"deviceId or fcmToken is required"
);
res.status(400).json({ error: "deviceId or fcmToken is required" }); res.status(400).json({ error: "deviceId or fcmToken is required" });
return; return;
} }
@@ -41,8 +54,8 @@ notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
if (device === undefined) { if (device === undefined) {
console.log( console.log(
"[Refresh] Device lookup failed for user:", "[Refresh] Device not found in",
userId, formatElapsedMs(Date.now() - started),
canonicalDeviceId !== undefined canonicalDeviceId !== undefined
? `deviceId=${canonicalDeviceId}` ? `deviceId=${canonicalDeviceId}`
: "", : "",
@@ -52,20 +65,21 @@ notificationsRouter.post("/refresh", requireAuth, async (req, res) => {
return; return;
} }
console.log(
"[Refresh] Device ownership validated:",
device.deviceId,
maskToken(device.fcmToken)
);
const now = Date.now(); const now = Date.now();
res.json({ res.json({
shouldNotify: true, shouldNotify: true,
nextNotifications: [{ timestamp: now + 600000 }], nextNotifications: [{ timestamp: now + 600000 }],
}); });
console.log(
"[Refresh] Completed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + device.deviceId + ",",
"token suffix=" + maskToken(device.fcmToken)
);
}); });
notificationsRouter.post("/register", requireAuth, async (req, res) => { notificationsRouter.post("/register", requireAuth, async (req, res) => {
const started = Date.now();
const userId = req.did; const userId = req.did;
if (userId === undefined) { if (userId === undefined) {
res.status(401).json({ success: false, message: "Unauthorized" }); res.status(401).json({ success: false, message: "Unauthorized" });
@@ -77,6 +91,11 @@ notificationsRouter.post("/register", requireAuth, async (req, res) => {
typeof req.body === "object" && typeof req.body === "object" &&
"userId" in req.body "userId" in req.body
) { ) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"userId must not be sent in the request body"
);
res.status(400).json({ res.status(400).json({
error: "userId must not be sent in the request body", error: "userId must not be sent in the request body",
}); });
@@ -91,39 +110,49 @@ notificationsRouter.post("/register", requireAuth, async (req, res) => {
}; };
if (typeof deviceId !== "string" || deviceId.trim().length === 0) { if (typeof deviceId !== "string" || deviceId.trim().length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"deviceId is required"
);
res.status(400).json({ error: "deviceId is required" }); res.status(400).json({ error: "deviceId is required" });
return; return;
} }
if (typeof fcmToken !== "string" || fcmToken.length === 0) { if (typeof fcmToken !== "string" || fcmToken.length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"fcmToken is required"
);
res.status(400).json({ error: "fcmToken is required" }); res.status(400).json({ error: "fcmToken is required" });
return; return;
} }
if (typeof platform !== "string" || platform.length === 0) { if (typeof platform !== "string" || platform.length === 0) {
console.log(
"[Register] Rejected in",
formatElapsedMs(Date.now() - started) + ":",
"platform is required"
);
res.status(400).json({ error: "platform is required" }); res.status(400).json({ error: "platform is required" });
return; return;
} }
const canonicalDeviceId = deviceId.trim(); const canonicalDeviceId = deviceId.trim();
console.log(
"[Register] Request received,",
"deviceId=" + canonicalDeviceId + ",",
"platform=" + platform + ",",
"token suffix=" + maskToken(fcmToken)
);
try { try {
const existing = await db.getByDeviceId(userId, canonicalDeviceId); const existing = await db.getByDeviceId(userId, canonicalDeviceId);
if (existing !== undefined) { const action =
if (existing.fcmToken !== fcmToken) { existing === undefined
console.log( ? "create"
"[Register] Updating existing device token:", : existing.fcmToken !== fcmToken
canonicalDeviceId, ? "update-token"
maskToken(fcmToken) : "update";
);
} else {
console.log("[Register] Updating existing device:", canonicalDeviceId);
}
} else {
console.log(
"[Register] Creating new device registration:",
canonicalDeviceId,
maskToken(fcmToken)
);
}
await db.upsert({ await db.upsert({
userId, userId,
@@ -134,8 +163,19 @@ notificationsRouter.post("/register", requireAuth, async (req, res) => {
updatedAt: new Date(), updatedAt: new Date(),
}); });
res.sendStatus(200); res.sendStatus(200);
console.log(
"[Register] Completed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + canonicalDeviceId + ",",
"action=" + action
);
} catch (err) { } catch (err) {
console.error("register failed", err); console.error(
"[Register] Failed in",
formatElapsedMs(Date.now() - started) + ",",
"deviceId=" + canonicalDeviceId + ":",
errorMessage(err)
);
res.sendStatus(500); res.sendStatus(500);
} }
}); });

View File

@@ -1,6 +1,6 @@
import { db } from "./db/fcmTokens.js"; import { db } from "./db/fcmTokens.js";
import { sendPushToDevice } from "./services/pushService.js"; import { sendPushToDevice } from "./services/pushService.js";
import { maskToken } from "./util/maskToken.js"; import { errorMessage, formatElapsedMs } from "./util/formatElapsed.js";
let intervalId: ReturnType<typeof setInterval> | undefined; let intervalId: ReturnType<typeof setInterval> | undefined;
@@ -8,25 +8,52 @@ export function startScheduler(): void {
if (intervalId !== undefined) return; if (intervalId !== undefined) return;
intervalId = setInterval(async () => { intervalId = setInterval(async () => {
const passStarted = Date.now();
console.log("[Scheduler] Pass started");
try { try {
console.log("[Scheduler] Checking devices...");
const devices = await db.getAllForScheduler(); const devices = await db.getAllForScheduler();
const seenTokens = new Set<string>(); const seenTokens = new Set<string>();
let checked = 0;
let sent = 0;
let skipped = 0;
let failed = 0;
let duplicates = 0;
for (const d of devices) { for (const d of devices) {
if (seenTokens.has(d.fcmToken)) { if (seenTokens.has(d.fcmToken)) {
console.log( duplicates++;
"[Scheduler] Duplicate device skipped:",
d.deviceId,
maskToken(d.fcmToken)
);
continue; continue;
} }
seenTokens.add(d.fcmToken); seenTokens.add(d.fcmToken);
await sendPushToDevice(d.fcmToken); checked++;
const result = await sendPushToDevice(d.fcmToken);
if (result === "sent") sent++;
else if (result === "skipped") skipped++;
else failed++;
} }
const summaryParts = [
`Checked ${checked} devices`,
`sent ${sent} pushes`,
`skipped ${skipped}`,
];
if (failed > 0) summaryParts.push(`failed ${failed}`);
if (duplicates > 0) {
summaryParts.push(`${duplicates} duplicates ignored`);
}
console.log("[Scheduler]", summaryParts.join(", "));
console.log(
"[Scheduler] Pass completed in",
formatElapsedMs(Date.now() - passStarted)
);
} catch (err) { } catch (err) {
console.error("[Scheduler] Tick failed", err); console.error(
"[Scheduler] Pass failed in",
formatElapsedMs(Date.now() - passStarted) + ":",
errorMessage(err)
);
} }
}, 5 * 60 * 1000); }, 5 * 60 * 1000);
} }

View File

@@ -1,4 +1,5 @@
import { db, type StoredRow } from "../db/fcmTokens.js"; import { db, type StoredRow } from "../db/fcmTokens.js";
import { errorMessage, formatElapsedMs } from "../util/formatElapsed.js";
import { maskToken } from "../util/maskToken.js"; import { maskToken } from "../util/maskToken.js";
import { messaging } from "./firebase.js"; import { messaging } from "./firebase.js";
@@ -46,6 +47,7 @@ export async function sendPushToDevice(
fcmToken: string, fcmToken: string,
payload: Record<string, unknown> = {} payload: Record<string, unknown> = {}
): Promise<"sent" | "skipped" | "failed"> { ): Promise<"sent" | "skipped" | "failed"> {
const suffix = maskToken(fcmToken);
const row = await db.getByFcmToken(fcmToken); const row = await db.getByFcmToken(fcmToken);
const now = Date.now(); const now = Date.now();
const last = lastNotifiedMs(row); const last = lastNotifiedMs(row);
@@ -54,23 +56,20 @@ export async function sendPushToDevice(
last !== undefined && last !== undefined &&
now - last < notifyThresholdMs(row?.testMode) now - last < notifyThresholdMs(row?.testMode)
) { ) {
const device = { id: row?.id ?? "unknown" };
console.log("[Skip] Recently notified:", device.id);
return "skipped"; return "skipped";
} }
const sendStarted = Date.now();
console.log("[Push] Send attempt, token suffix:", suffix);
try { try {
const device = { fcmToken };
const data: Record<string, string> = { const data: Record<string, string> = {
...stringifyData(payload), ...stringifyData(payload),
type: "WAKEUP_PING", type: "WAKEUP_PING",
}; };
const token = maskToken(fcmToken);
console.log("[Push] Sending to:", token);
await messaging.send({ await messaging.send({
token: device.fcmToken, token: fcmToken,
apns: { apns: {
headers: { headers: {
"apns-push-type": "background", "apns-push-type": "background",
@@ -89,10 +88,21 @@ export async function sendPushToDevice(
if (persisted !== undefined) { if (persisted !== undefined) {
await db.update(persisted.id, { lastNotifiedAt: Date.now() }); await db.update(persisted.id, { lastNotifiedAt: Date.now() });
} }
console.log("[Push] Success:", token); console.log(
"[Push] Send completed in",
formatElapsedMs(Date.now() - sendStarted) + ",",
"token suffix:",
suffix
);
return "sent"; return "sent";
} catch (err) { } catch (err) {
console.error("[Push] Failed:", err); console.error(
"[Push] Send failed in",
formatElapsedMs(Date.now() - sendStarted) + ",",
"token suffix:",
suffix + ":",
errorMessage(err)
);
return "failed"; return "failed";
} }
} }

17
src/util/formatElapsed.ts Normal file
View File

@@ -0,0 +1,17 @@
/** Human-readable duration for console logs (e.g. 842ms, 2.1s). */
export function formatElapsedMs(elapsedMs: number): string {
if (elapsedMs < 1000) {
return `${Math.round(elapsedMs)}ms`;
}
return `${(elapsedMs / 1000).toFixed(1)}s`;
}
export function errorMessage(err: unknown): string {
if (err instanceof Error && err.message.length > 0) {
return err.message;
}
if (typeof err === "string" && err.length > 0) {
return err;
}
return "Unknown error";
}