Merged in fix/warmup-not-throwing (pull request #3179)

fix: warmup threw error

* fix: warmup threw error

* .


Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-11-19 13:49:44 +00:00
parent ac5fdc64a9
commit 0e66f1b6de
5 changed files with 89 additions and 74 deletions

View File

@@ -16,6 +16,7 @@ export async function GET(req: NextRequest) {
const key = url.searchParams.get("key") const key = url.searchParams.get("key")
if (!isAuthroized(req)) { if (!isAuthroized(req)) {
logger.error("Unauthorized warmup request")
return unauthorizedResponse() return unauthorizedResponse()
} }
@@ -25,7 +26,7 @@ export async function GET(req: NextRequest) {
return invalidKeyResponse(key) return invalidKeyResponse(key)
} }
logger.debug("Warming up:", key) logger.info("Warming up:", key)
const warmupResult = await warmup(key) const warmupResult = await warmup(key)
const executionTime = performance.now() - executionStart const executionTime = performance.now() - executionStart
@@ -61,7 +62,7 @@ function warmupCompletedResponse(
key: string, key: string,
executionTime: number executionTime: number
) { ) {
logger.debug(`Warmup completed: ${key} in ${executionTime.toFixed(2)}ms`) logger.info(`Warmup completed: ${key} in ${executionTime.toFixed(2)}ms`)
return NextResponse.json( return NextResponse.json(
{ {
...warmupResult, ...warmupResult,
@@ -77,7 +78,7 @@ function warmupSkippedResponse(
key: string, key: string,
executionTime: number executionTime: number
) { ) {
logger.debug("Warmup skipped:", key) logger.info("Warmup skipped:", key)
return NextResponse.json( return NextResponse.json(
{ {

View File

@@ -1,19 +1,50 @@
import crypto from "crypto" import crypto from "node:crypto"
import Sentry from "@sentry/nextjs"
import jwt from "jsonwebtoken" import jwt from "jsonwebtoken"
import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { type WarmupFunctionsKey } from "@/services/warmup/warmupKeys"
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
import { import { configureSentry } from "../utils/initSentry"
type WarmupFunctionsKey, import { safeTry } from "../utils/safeTry"
warmupKeys,
} from "@/services/warmup/warmupKeys"
import { timeout } from "@/utils/timeout"
import type { Config, Context } from "@netlify/functions" import type { Config, Context } from "@netlify/functions"
async function WarmupHandler(request: Request, context: Context) { await configureSentry()
const warmupLogger = createLogger("warmup")
export const config: Config = {
method: "POST",
}
const warmupLogger = {
info: (message: string, ...args: unknown[]) => {
// eslint-disable-next-line no-console
console.log(`[WARMUP] ${message}`, ...args)
Sentry.logger.info(`[WARMUP] ${message}`, { ...args })
},
warn: (message: string, ...args: unknown[]) => {
// eslint-disable-next-line no-console
console.warn(`[WARMUP] ${message}`, ...args)
Sentry.logger.warn(`[WARMUP] ${message}`, { ...args })
},
error: (message: string, ...args: unknown[]) => {
// eslint-disable-next-line no-console
console.error(`[WARMUP] ${message}`, ...args)
Sentry.logger.error(`[WARMUP] ${message}`, { ...args })
},
}
const langs = ["en", "sv", "no", "fi", "da", "de"] as const
export const warmupKeys = [
...langs.map((lang) => `countries_${lang}` as const),
"hotelsByCountry",
...langs.map((lang) => `hotelData_${lang}` as const),
...langs.map((lang) => `autoComplete_${lang}` as const),
] satisfies WarmupFunctionsKey[]
export default async function WarmupHandler(
request: Request,
context: Context
) {
const [_, error] = await safeTry(validateRequest(request, context)) const [_, error] = await safeTry(validateRequest(request, context))
if (error) { if (error) {
@@ -42,13 +73,11 @@ async function validateRequest(
request: Request, request: Request,
context: Context context: Context
): Promise<true> { ): Promise<true> {
const warmupLogger = createLogger("warmup")
if (request.method !== "POST") { if (request.method !== "POST") {
throw new Error(ErrorCodes.METHOD_NOT_ALLOWED) throw new Error(ErrorCodes.METHOD_NOT_ALLOWED)
} }
const warmupEnabled = const warmupEnabled =
true ||
process.env.WARMUP_ENABLED === "true" || process.env.WARMUP_ENABLED === "true" ||
Netlify.env.get("WARMUP_ENABLED") === "true" Netlify.env.get("WARMUP_ENABLED") === "true"
@@ -61,7 +90,6 @@ async function validateRequest(
} }
const body = await request.text() const body = await request.text()
warmupLogger.info("Warmup body", body)
const deployment = JSON.parse(body) as DeploymentInfo const deployment = JSON.parse(body) as DeploymentInfo
if (!deployment) { if (!deployment) {
throw new Error(ErrorCodes.UNABLE_TO_PARSE_DEPLOYMENT_INFO) throw new Error(ErrorCodes.UNABLE_TO_PARSE_DEPLOYMENT_INFO)
@@ -76,7 +104,6 @@ async function validateRequest(
throw new Error(ErrorCodes.REQUEST_NOT_FOR_CURRENT_CONTEXT) throw new Error(ErrorCodes.REQUEST_NOT_FOR_CURRENT_CONTEXT)
} }
warmupLogger.info("Warmup request", deployment)
let signature: string let signature: string
try { try {
const headerValue = request.headers.get("x-webhook-signature") const headerValue = request.headers.get("x-webhook-signature")
@@ -103,7 +130,6 @@ async function validateRequest(
} }
export async function performWarmup(context: Context) { export async function performWarmup(context: Context) {
const warmupLogger = createLogger("warmup")
for (const key of warmupKeys) { for (const key of warmupKeys) {
warmupLogger.info("Warming up cache", key) warmupLogger.info("Warming up cache", key)
await callWarmup(key, context) await callWarmup(key, context)
@@ -112,16 +138,12 @@ export async function performWarmup(context: Context) {
} }
} }
async function callWarmup(key: WarmupFunctionsKey, context: Context) { async function callWarmup(key: (typeof warmupKeys)[number], context: Context) {
const warmupLogger = createLogger("warmup")
const baseUrl = context.url.origin const baseUrl = context.url.origin
const url = new URL("/api/web/warmup", baseUrl) const url = new URL("/api/web/warmup", baseUrl)
url.searchParams.set("key", key) url.searchParams.set("key", key)
const warmupToken = const warmupToken =
process.env.WARMUP_TOKEN || Netlify.env.get("WARMUP_TOKEN") process.env.WARMUP_TOKEN || Netlify.env.get("WARMUP_TOKEN")
try { try {
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
@@ -130,7 +152,6 @@ async function callWarmup(key: WarmupFunctionsKey, context: Context) {
}, },
signal: AbortSignal.timeout(30_000), signal: AbortSignal.timeout(30_000),
}) })
if (!response.ok) { if (!response.ok) {
warmupLogger.error( warmupLogger.error(
`Warmup failed '${url.href}' with error: ${response.status}: ${response.statusText}` `Warmup failed '${url.href}' with error: ${response.status}: ${response.statusText}`
@@ -143,13 +164,7 @@ async function callWarmup(key: WarmupFunctionsKey, context: Context) {
} }
} }
export default WarmupHandler
export const config: Config = {
method: "POST",
}
async function validateSignature(token: string, buffer: string) { async function validateSignature(token: string, buffer: string) {
const warmupLogger = createLogger("warmup")
try { try {
const secret = const secret =
process.env.WARMUP_SIGNATURE_SECRET || process.env.WARMUP_SIGNATURE_SECRET ||
@@ -218,14 +233,14 @@ type DeploymentInfo = {
updated_at: string updated_at: string
user_id: string user_id: string
error_message: string | null error_message: string | null
required: any[] required: unknown[]
required_functions: any[] required_functions: unknown[]
commit_ref: string commit_ref: string
review_id: string | null review_id: string | null
branch: string branch: string
commit_url: string commit_url: string
skipped: any skipped: unknown
locked: any locked: unknown
title: string title: string
commit_message: string | null commit_message: string | null
review_url: string | null review_url: string | null
@@ -235,10 +250,10 @@ type DeploymentInfo = {
available_functions: unknown[] available_functions: unknown[]
screenshot_url: string | null screenshot_url: string | null
committer: string committer: string
skipped_log: any skipped_log: unknown
manual_deploy: boolean manual_deploy: boolean
plugin_state: string plugin_state: string
lighthouse_plugin_scores: any lighthouse_plugin_scores: unknown
links: { links: {
permalink: string permalink: string
alias: string alias: string
@@ -253,7 +268,7 @@ type DeploymentInfo = {
}[] }[]
public_repo: boolean public_repo: boolean
pending_review_reason: string | null pending_review_reason: string | null
lighthouse: any lighthouse: unknown
edge_functions_present: boolean edge_functions_present: boolean
expires_at: string | null expires_at: string | null
blobs_region: string blobs_region: string
@@ -273,3 +288,7 @@ const ErrorCodes = {
WARMUP_DISABLED: "WARMUP IS DISABLED", WARMUP_DISABLED: "WARMUP IS DISABLED",
} as const } as const
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes] type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
function timeout(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}

View File

@@ -1,37 +0,0 @@
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import type { Lang } from "@scandic-hotels/common/constants/language"
export async function warmupHotelDataOnLang(lang: Lang) {
const warmupHotelDataOnLangLogger = createLogger("warmupHotelDataOnLang")
const PUBLIC_URL = Netlify.env.get("PUBLIC_URL")
warmupHotelDataOnLangLogger.info(
`[WARMUP] Started warmup cache hoteldata for language ${lang} at: ${new Date().toISOString()}!`
)
try {
const hotelsResponse = await fetch(
`${PUBLIC_URL}/api/hoteldata?lang=${lang}`,
{
headers: { cache: "no-store" },
signal: AbortSignal.timeout(30_000),
}
)
if (!hotelsResponse.ok) {
throw new Error(
`[WARMUP] Failed to warmup cache for hotels on language ${lang} with error: ${hotelsResponse.statusText}`
)
}
const hotels = await hotelsResponse.json()
warmupHotelDataOnLangLogger.info(
`[WARMUP] Retrieved ${hotels.length} hotels.`
)
} catch (error) {
warmupHotelDataOnLangLogger.error(
`[WARMUP] Error warming cache with hoteldata on language ${lang} with error: ${error}`
)
}
}

View File

@@ -0,0 +1,21 @@
import Sentry from "@sentry/nextjs"
export const denyUrls: (string | RegExp)[] = [
// Ignore preview urls
/\/.{2}\/preview\//,
]
export const onRequestError = Sentry.captureRequestError
export async function configureSentry() {
const sentryEnvironment = Netlify.env.get("SENTRY_ENVIRONMENT")
const sampleRate = Number(Netlify.env.get("SENTRY_SERVER_SAMPLERATE") ?? 0.01)
Sentry.init({
dsn: "https://fe39c070b4154e2f9cc35f0e5de0aedb@o4508102497206272.ingest.de.sentry.io/4508102500286544",
environment: sentryEnvironment,
enabled: sentryEnvironment !== "development",
tracesSampleRate: sampleRate,
denyUrls: denyUrls,
enableLogs: true,
})
}

View File

@@ -0,0 +1,11 @@
export type SafeTryResult<T> = Promise<
[T, undefined] | [undefined, Error | unknown]
>
export async function safeTry<T>(func: Promise<T>): SafeTryResult<T> {
try {
return [await func, undefined] as const
} catch (err) {
return [undefined, err instanceof Error ? err : (err as unknown)] as const
}
}