Handle billing KV failures and preserve Lighthouse start-page matching (#375)
This commit is contained in:
parent
dae0067233
commit
c11517908d
@ -40,6 +40,7 @@ describe("subscription billing", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
kvGetMock.mockResolvedValue(null);
|
||||
kvPutMock.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("checks the paid plan entitlement", async () => {
|
||||
@ -87,4 +88,48 @@ describe("subscription billing", () => {
|
||||
expect(result).toEqual({ id: "org_123" });
|
||||
expect(getOrCreateMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to Autumn when the customer cache read fails", async () => {
|
||||
const cacheError = new Error("KV read unavailable");
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
kvGetMock.mockRejectedValue(cacheError);
|
||||
getOrCreateMock.mockResolvedValue({ id: "cust_123" });
|
||||
|
||||
await expect(
|
||||
getOrCreateOrganizationCustomer({
|
||||
organizationId: "org_123",
|
||||
userId: "user_123",
|
||||
userEmail: "alice@example.com",
|
||||
}),
|
||||
).resolves.toEqual({ id: "cust_123" });
|
||||
|
||||
expect(getOrCreateMock).toHaveBeenCalledWith({
|
||||
customerId: "org_123",
|
||||
email: "alice@example.com",
|
||||
});
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
"billing.customer-cache-read failed:",
|
||||
cacheError,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the resolved customer when the customer cache write fails", async () => {
|
||||
const cacheError = new Error("KV write unavailable");
|
||||
vi.spyOn(console, "warn").mockImplementation(() => undefined);
|
||||
getOrCreateMock.mockResolvedValue({ id: "cust_123" });
|
||||
kvPutMock.mockRejectedValue(cacheError);
|
||||
|
||||
await expect(
|
||||
getOrCreateOrganizationCustomer({
|
||||
organizationId: "org_123",
|
||||
userId: "user_123",
|
||||
userEmail: "alice@example.com",
|
||||
}),
|
||||
).resolves.toEqual({ id: "cust_123" });
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
"billing.customer-cache-write failed:",
|
||||
cacheError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -35,8 +35,12 @@ export async function getOrCreateOrganizationCustomer(
|
||||
context: BillingCustomerContext,
|
||||
): Promise<{ id: string }> {
|
||||
const cacheKey = customerEnsuredKey(context.organizationId);
|
||||
if (await env.KV.get(cacheKey)) {
|
||||
return { id: context.organizationId };
|
||||
try {
|
||||
if (await env.KV.get(cacheKey)) {
|
||||
return { id: context.organizationId };
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("billing.customer-cache-read failed:", error);
|
||||
}
|
||||
|
||||
const customer = await autumn.customers.getOrCreate({
|
||||
@ -48,9 +52,13 @@ export async function getOrCreateOrganizationCustomer(
|
||||
throw new AppError("INTERNAL_ERROR", "Failed to resolve billing customer");
|
||||
}
|
||||
|
||||
await env.KV.put(cacheKey, "1", {
|
||||
expirationTtl: CUSTOMER_ENSURED_TTL_SECONDS,
|
||||
});
|
||||
try {
|
||||
await env.KV.put(cacheKey, "1", {
|
||||
expirationTtl: CUSTOMER_ENSURED_TTL_SECONDS,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("billing.customer-cache-write failed:", error);
|
||||
}
|
||||
|
||||
return { id: customer.id };
|
||||
}
|
||||
|
||||
45
src/server/lib/audit/lighthouse.test.ts
Normal file
45
src/server/lib/audit/lighthouse.test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/server/lib/dataforseo", () => ({
|
||||
createDataforseoClient: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/server/lib/r2", () => ({
|
||||
putTextToR2: vi.fn(),
|
||||
}));
|
||||
|
||||
import { selectLighthouseSample } from "./lighthouse";
|
||||
|
||||
describe("selectLighthouseSample", () => {
|
||||
it("includes a start page reached through a trailing-slash redirect", () => {
|
||||
const pages = [
|
||||
...Array.from({ length: 10 }, (_, index) => ({
|
||||
url: `https://example.com/section${index}`,
|
||||
statusCode: 200,
|
||||
})),
|
||||
{ url: "https://example.com/services/", statusCode: 200 },
|
||||
];
|
||||
|
||||
const selected = selectLighthouseSample(
|
||||
pages,
|
||||
"https://example.com/services",
|
||||
"auto",
|
||||
);
|
||||
|
||||
expect(selected).toHaveLength(10);
|
||||
expect(selected[0]).toBe("https://example.com/services/");
|
||||
});
|
||||
|
||||
it("prefers an exact start page when both slash forms return 2xx", () => {
|
||||
const selected = selectLighthouseSample(
|
||||
[
|
||||
{ url: "https://example.com/services/", statusCode: 200 },
|
||||
{ url: "https://example.com/services", statusCode: 200 },
|
||||
],
|
||||
"https://example.com/services",
|
||||
"auto",
|
||||
);
|
||||
|
||||
expect(selected[0]).toBe("https://example.com/services");
|
||||
});
|
||||
});
|
||||
@ -9,6 +9,14 @@ interface LighthouseSamplePage {
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
function canonicalUrlKeyWithoutTrailingSlash(url: string): string {
|
||||
const parsed = new URL(canonicalUrlKey(url));
|
||||
if (parsed.pathname !== "/") {
|
||||
parsed.pathname = parsed.pathname.replace(/\/$/, "");
|
||||
}
|
||||
return parsed.toString();
|
||||
}
|
||||
|
||||
type LighthouseFetchResult = {
|
||||
result: LighthouseResult;
|
||||
payloadJson: string | null;
|
||||
@ -130,12 +138,17 @@ export function selectLighthouseSample(
|
||||
// strategy === "auto": homepage + 1 per URL pattern, capped at 10
|
||||
const selected = new Set<string>();
|
||||
|
||||
// Always include the start URL / homepage. Compare with canonicalUrlKey on
|
||||
// both sides so the match survives the redirects a site uses to reach its
|
||||
// canonical homepage: trailing-slash (/ -> // no, e.g. example.com ->
|
||||
// example.com/), www <-> non-www, and http -> https.
|
||||
// Always include the start URL / homepage. Prefer an exact canonical match
|
||||
// so distinct 2xx `/path` and `/path/` pages stay distinct, then tolerate a
|
||||
// trailing-slash redirect when the exact start URL was not crawled as 2xx.
|
||||
const startKey = canonicalUrlKey(startUrl);
|
||||
const startPage = validPages.find((p) => canonicalUrlKey(p.url) === startKey);
|
||||
const startPage =
|
||||
validPages.find((p) => canonicalUrlKey(p.url) === startKey) ??
|
||||
validPages.find(
|
||||
(p) =>
|
||||
canonicalUrlKeyWithoutTrailingSlash(p.url) ===
|
||||
canonicalUrlKeyWithoutTrailingSlash(startUrl),
|
||||
);
|
||||
if (startPage) selected.add(startPage.url);
|
||||
|
||||
// Group by URL template pattern
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user