Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 16 additions & 8 deletions app/api/cron/blob-prune/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NextResponse } from "next/server";
import { logger } from "@/lib/logger";
import { pruneDueBlobsOnce } from "@/lib/storage";

const log = logger({ module: "cron:blob-prune" });

export const dynamic = "force-dynamic";

export async function GET(request: Request) {
Expand All @@ -11,45 +14,50 @@ export async function GET(request: Request) {
: null;

if (!expectedAuth) {
log.error("cron.misconfigured", { reason: "CRON_SECRET missing" });
return NextResponse.json(
{ error: "CRON_SECRET not configured" },
{ status: 500 },
);
}

if (authHeader !== expectedAuth) {
log.warn("cron.unauthorized", { provided: Boolean(authHeader) });
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

try {
const startedAt = Date.now();
const result = await pruneDueBlobsOnce(startedAt);

const durationMs = Date.now() - startedAt;
if (result.errorCount > 0) {
console.warn("[blob-prune] completed with errors", {
log.warn("completed.with.errors", {
deletedCount: result.deletedCount,
errorCount: result.errorCount,
duration_ms: Date.now() - startedAt,
durationMs,
});
} else {
console.info("[blob-prune] completed", {
log.info("completed", {
deletedCount: result.deletedCount,
duration_ms: Date.now() - startedAt,
durationMs,
});
}

return NextResponse.json({
success: true,
deletedCount: result.deletedCount,
errorCount: result.errorCount,
duration_ms: Date.now() - startedAt,
durationMs,
});
} catch (err) {
log.error("cron.failed", {
err: err instanceof Error ? err : new Error(String(err)),
});
} catch (error) {
console.error("[blob-prune] cron failed", error);
return NextResponse.json(
{
error: "Internal error",
message: error instanceof Error ? error.message : "unknown",
message: err instanceof Error ? err.message : "unknown",
},
{ status: 500 },
);
Expand Down
25 changes: 19 additions & 6 deletions app/api/cron/due-drain/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { NextResponse } from "next/server";
import { inngest } from "@/lib/inngest/client";
import { logger } from "@/lib/logger";
import { ns, redis } from "@/lib/redis";
import { drainDueDomainsOnce } from "@/lib/schedule";

const log = logger({ module: "cron:due-drain" });

export const dynamic = "force-dynamic";

export async function GET(request: Request) {
Expand Down Expand Up @@ -50,24 +53,34 @@ export async function GET(request: Request) {
e.data.sections.map((s) => redis.zrem(ns("due", s), e.data.domain)),
),
);
} catch (e) {
console.warn("[due-drain] cleanup failed", e);
} catch (err) {
log.warn("cleanup.failed", {
err: err instanceof Error ? err : new Error(String(err)),
});
}
emitted += chunk.length;
}

log.info("cron.ok", {
emitted,
groups: result.groups,
durationMs: Date.now() - startedAt,
});

return NextResponse.json({
success: true,
emitted,
groups: result.groups,
duration_ms: Date.now() - startedAt,
durationMs: Date.now() - startedAt,
});
} catch (err) {
log.error("cron.failed", {
err: err instanceof Error ? err : new Error(String(err)),
});
} catch (error) {
console.error("[due-drain] cron failed", error);
return NextResponse.json(
{
error: "Internal error",
message: error instanceof Error ? error.message : "unknown",
message: err instanceof Error ? err.message : "unknown",
},
{ status: 500 },
);
Expand Down
9 changes: 3 additions & 6 deletions app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { logger } from "@/lib/logger";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/trpc/init";

Expand Down Expand Up @@ -33,12 +34,8 @@ const handler = (req: Request) =>
return { headers, status: 429 };
},
onError: ({ path, error }) => {
// Development logging
if (process.env.NODE_ENV === "development") {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
const log = logger({ module: "trpc:handler" });
log.error("unhandled", { path, err: error });
},
});

Expand Down
40 changes: 31 additions & 9 deletions instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
import type { Instrumentation } from "next";

// Conditionally register Node.js-specific instrumentation
export const register = async () => {
if (process.env.NEXT_RUNTIME === "nodejs") {
// Dynamic import to avoid bundling Node.js code into Edge runtime
const { logger } = await import("@/lib/logger");
const log = logger({ module: "instrumentation" });

// Process-level error hooks (Node only)
process.on("uncaughtException", (err) =>
log.error("uncaughtException", { err }),
);
process.on("unhandledRejection", (reason) =>
log.error("unhandledRejection", {
err: reason instanceof Error ? reason : new Error(String(reason)),
}),
);
}
};

export const onRequestError: Instrumentation.onRequestError = async (
err,
request,
) => {
if (process.env.NEXT_RUNTIME === "nodejs") {
try {
// Dynamic imports for Node.js-only code
const { logger } = await import("@/lib/logger");
const log = logger({ module: "instrumentation" });

const { getServerPosthog } = await import("@/lib/analytics/server");
const phClient = getServerPosthog();

if (!phClient) {
return; // PostHog not available, skip error tracking
}

let distinctId = null;
if (request.headers.cookie) {
let distinctId: string | null = null;
if (request.headers?.cookie) {
const cookieString = request.headers.cookie;
const postHogCookieMatch =
typeof cookieString === "string"
Expand All @@ -26,8 +49,10 @@ export const onRequestError: Instrumentation.onRequestError = async (
const decodedCookie = decodeURIComponent(postHogCookieMatch[1]);
const postHogData = JSON.parse(decodedCookie);
distinctId = postHogData.distinct_id;
} catch (e) {
console.error("Error parsing PostHog cookie:", e);
} catch (err) {
log.error("cookie.parse.error", {
err: err instanceof Error ? err : new Error(String(err)),
});
}
}
}
Expand All @@ -38,12 +63,9 @@ export const onRequestError: Instrumentation.onRequestError = async (
});

await phClient.shutdown();
} catch (instrumentationError) {
} catch (err) {
// Graceful degradation - log error but don't throw to avoid breaking the request
console.error(
"Instrumentation error tracking failed:",
instrumentationError,
);
console.error("Instrumentation error", err);
}
}
};
8 changes: 0 additions & 8 deletions lib/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ describe("cached assets", () => {
indexKey,
lockKey,
ttlSeconds: 60,
eventName: "test_asset",
baseMetrics: { domain: "example.com" },
produceAndUpload: async () => ({
url: "https://cdn/y.webp",
key: "k",
Expand All @@ -67,8 +65,6 @@ describe("cached assets", () => {
indexKey,
lockKey,
ttlSeconds: 60,
eventName: "test_asset",
baseMetrics: { domain: "example.com" },
produceAndUpload: async () => ({ url: "https://cdn/unused.webp" }),
});

Expand All @@ -83,8 +79,6 @@ describe("cached assets", () => {
indexKey,
lockKey,
ttlSeconds: 60,
eventName: "test_asset",
baseMetrics: { domain: "example.com" },
purgeQueue: "purge-test",
produceAndUpload: async () => ({
url: "https://cdn/new.webp",
Expand All @@ -111,8 +105,6 @@ describe("cached assets", () => {
indexKey,
lockKey,
ttlSeconds: 60,
eventName: "test_asset",
baseMetrics: { domain: "example.com" },
produceAndUpload: async () => ({ url: null }),
});

Expand Down
Loading