Add build info metric and diagnostic logging for Garmin sync
All checks were successful
Deploy / deploy (push) Successful in 1m44s
All checks were successful
Deploy / deploy (push) Successful in 1m44s
- Add phaseflow_build_info metric with version and commit labels - Inject GIT_COMMIT env var at build time via next.config.ts - Add logging to all Garmin fetch functions (HRV, body battery, intensity) - Log API response status codes, actual data values, and errors This enables verifying which build is deployed and diagnosing silent failures where Garmin API returns errors but sync reports success. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,22 @@
|
|||||||
|
// ABOUTME: Next.js configuration for PhaseFlow application.
|
||||||
|
// ABOUTME: Configures standalone output and injects git commit hash for build verification.
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
// Get git commit hash at build time for deployment verification
|
||||||
|
function getGitCommit(): string {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse --short HEAD").toString().trim();
|
||||||
|
} catch {
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
env: {
|
||||||
|
GIT_COMMIT: process.env.GIT_COMMIT || getGitCommit(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// ABOUTME: Garmin Connect API client using stored OAuth tokens.
|
// ABOUTME: Garmin Connect API client using stored OAuth tokens.
|
||||||
// ABOUTME: Fetches body battery, HRV, and intensity minutes from Garmin.
|
// ABOUTME: Fetches body battery, HRV, and intensity minutes from Garmin.
|
||||||
|
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
import type { GarminTokens, HrvStatus } from "@/types";
|
import type { GarminTokens, HrvStatus } from "@/types";
|
||||||
|
|
||||||
const GARMIN_BASE_URL = "https://connect.garmin.com/modern/proxy";
|
const GARMIN_BASE_URL = "https://connect.garmin.com/modern/proxy";
|
||||||
@@ -63,6 +65,10 @@ export async function fetchHrvStatus(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
logger.warn(
|
||||||
|
{ status: response.status, endpoint: "hrv-service" },
|
||||||
|
"Garmin HRV API error",
|
||||||
|
);
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,13 +76,23 @@ export async function fetchHrvStatus(
|
|||||||
const status = data?.hrvSummary?.status;
|
const status = data?.hrvSummary?.status;
|
||||||
|
|
||||||
if (status === "BALANCED") {
|
if (status === "BALANCED") {
|
||||||
|
logger.info({ status: "BALANCED" }, "Garmin HRV data received");
|
||||||
return "Balanced";
|
return "Balanced";
|
||||||
}
|
}
|
||||||
if (status === "UNBALANCED") {
|
if (status === "UNBALANCED") {
|
||||||
|
logger.info({ status: "UNBALANCED" }, "Garmin HRV data received");
|
||||||
return "Unbalanced";
|
return "Unbalanced";
|
||||||
}
|
}
|
||||||
|
logger.info(
|
||||||
|
{ rawStatus: status, hasData: !!data?.hrvSummary },
|
||||||
|
"Garmin HRV returned unknown status",
|
||||||
|
);
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ err: error, endpoint: "hrv-service" },
|
||||||
|
"Garmin HRV fetch failed",
|
||||||
|
);
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +113,10 @@ export async function fetchBodyBattery(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
logger.warn(
|
||||||
|
{ status: response.status, endpoint: "bodyBattery" },
|
||||||
|
"Garmin body battery API error",
|
||||||
|
);
|
||||||
return { current: null, yesterdayLow: null };
|
return { current: null, yesterdayLow: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +128,22 @@ export async function fetchBodyBattery(
|
|||||||
const yesterdayStats = data?.bodyBatteryStatList?.[0];
|
const yesterdayStats = data?.bodyBatteryStatList?.[0];
|
||||||
const yesterdayLow = yesterdayStats?.min ?? null;
|
const yesterdayLow = yesterdayStats?.min ?? null;
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
current,
|
||||||
|
yesterdayLow,
|
||||||
|
hasCurrentData: !!currentData,
|
||||||
|
hasYesterdayData: !!yesterdayStats,
|
||||||
|
},
|
||||||
|
"Garmin body battery data received",
|
||||||
|
);
|
||||||
|
|
||||||
return { current, yesterdayLow };
|
return { current, yesterdayLow };
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ err: error, endpoint: "bodyBattery" },
|
||||||
|
"Garmin body battery fetch failed",
|
||||||
|
);
|
||||||
return { current: null, yesterdayLow: null };
|
return { current: null, yesterdayLow: null };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +163,10 @@ export async function fetchIntensityMinutes(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
logger.warn(
|
||||||
|
{ status: response.status, endpoint: "fitnessstats" },
|
||||||
|
"Garmin intensity minutes API error",
|
||||||
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,14 +174,28 @@ export async function fetchIntensityMinutes(
|
|||||||
const weeklyTotal = data?.weeklyTotal;
|
const weeklyTotal = data?.weeklyTotal;
|
||||||
|
|
||||||
if (!weeklyTotal) {
|
if (!weeklyTotal) {
|
||||||
|
logger.info(
|
||||||
|
{ hasWeeklyTotal: false },
|
||||||
|
"Garmin intensity minutes: no weekly data",
|
||||||
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const moderate = weeklyTotal.moderateIntensityMinutes ?? 0;
|
const moderate = weeklyTotal.moderateIntensityMinutes ?? 0;
|
||||||
const vigorous = weeklyTotal.vigorousIntensityMinutes ?? 0;
|
const vigorous = weeklyTotal.vigorousIntensityMinutes ?? 0;
|
||||||
|
const total = moderate + vigorous;
|
||||||
|
|
||||||
return moderate + vigorous;
|
logger.info(
|
||||||
} catch {
|
{ moderate, vigorous, total },
|
||||||
|
"Garmin intensity minutes data received",
|
||||||
|
);
|
||||||
|
|
||||||
|
return total;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
{ err: error, endpoint: "fitnessstats" },
|
||||||
|
"Garmin intensity minutes fetch failed",
|
||||||
|
);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,3 +47,19 @@ export const activeUsersGauge = new promClient.Gauge({
|
|||||||
help: "Number of users with activity in the last 24 hours",
|
help: "Number of users with activity in the last 24 hours",
|
||||||
registers: [metricsRegistry],
|
registers: [metricsRegistry],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Build info metric: exposes version and git commit for deployment verification
|
||||||
|
const buildInfo = new promClient.Gauge({
|
||||||
|
name: "phaseflow_build_info",
|
||||||
|
help: "Build information with version and git commit",
|
||||||
|
labelNames: ["version", "commit"] as const,
|
||||||
|
registers: [metricsRegistry],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set build info at module load (value is always 1, labels contain the info)
|
||||||
|
buildInfo
|
||||||
|
.labels({
|
||||||
|
version: process.env.npm_package_version || "unknown",
|
||||||
|
commit: process.env.GIT_COMMIT || "unknown",
|
||||||
|
})
|
||||||
|
.set(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user