From 9dadd08c3e1de4011e77666906144ff0035e938b Mon Sep 17 00:00:00 2001 From: shashank-sn Date: Sun, 6 Sep 2026 20:50:53 +0530 Subject: [PATCH] fix(backlinks): keep subdomain overview without history --- .../backlinksOverviewDegradation.test.ts | 129 ++++++++++++++++++ .../services/backlinksServiceData.ts | 12 +- 2 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 src/server/features/backlinks/services/backlinksOverviewDegradation.test.ts diff --git a/src/server/features/backlinks/services/backlinksOverviewDegradation.test.ts b/src/server/features/backlinks/services/backlinksOverviewDegradation.test.ts new file mode 100644 index 000000000..c7821d4ec --- /dev/null +++ b/src/server/features/backlinks/services/backlinksOverviewDegradation.test.ts @@ -0,0 +1,129 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const { summary, history, normalizeTarget } = vi.hoisted(() => ({ + summary: vi.fn(), + history: vi.fn(), + normalizeTarget: vi.fn(() => ({ + apiTarget: "blog.example.com", + displayTarget: "blog.example.com", + scope: "subdomains" as "domain" | "subdomains", + includeSubdomains: true, + path: "", + })), +})); + +vi.mock("@/server/lib/dataforseo", () => ({ + createDataforseoClient: vi.fn(() => ({ backlinks: { summary, history } })), + normalizeBacklinksTarget: normalizeTarget, +})); + +import { profileBacklinksOverview } from "./backlinksServiceData"; + +const billingCustomer = { + organizationId: "org_123", + userId: "user_123", + userEmail: "team@example.com", +}; +const summaryResult = { + rank: 42, + backlinks: 1200, + referring_pages: 900, + referring_domains: 320, + broken_backlinks: 12, + broken_pages: 3, + backlinks_spam_score: 5, + info: { target_spam_score: 4 }, + new_backlinks: 25, + lost_backlinks: 10, + new_referring_domains: 8, + lost_referring_domains: 2, +}; + +function createCache() { + const values = new Map(); + return { + get: async (key: string) => values.get(key), + set: async (key: string, value: unknown) => { + values.set(key, value); + }, + }; +} + +beforeEach(() => vi.clearAllMocks()); + +it("keeps a subdomain summary when backlink history is unavailable", async () => { + summary.mockResolvedValue(summaryResult); + history.mockRejectedValue( + new Error("Backlink history is unavailable for this target"), + ); + const cache = createCache(); + const input = { + target: "https://blog.example.com/articles/latest", + scope: "subdomains", + } as const; + + const first = await profileBacklinksOverview( + cache, + "overview-key", + input, + billingCustomer, + ); + const second = await profileBacklinksOverview( + cache, + "overview-key", + input, + billingCustomer, + ); + + expect(first.overview.summary).toEqual( + expect.objectContaining({ + backlinks: 1200, + referringDomains: 320, + rank: 42, + }), + ); + expect(first.overview.trends).toEqual([]); + expect(first.overview.newLostTrends).toEqual([]); + expect(second).toEqual(first); + expect(summary).toHaveBeenCalledOnce(); + expect(history).not.toHaveBeenCalled(); +}); + +it("still rejects the overview when the summary is unavailable", async () => { + const summaryError = new Error("Backlink summary is unavailable"); + summary.mockRejectedValue(summaryError); + history.mockResolvedValue([]); + + await expect( + profileBacklinksOverview( + createCache(), + "overview-key", + { target: "blog.example.com", scope: "subdomains" }, + billingCustomer, + ), + ).rejects.toBe(summaryError); +}); + +it("still rejects root-domain overviews when history is unavailable", async () => { + normalizeTarget.mockReturnValueOnce({ + apiTarget: "example.com", + displayTarget: "example.com", + scope: "domain", + includeSubdomains: false, + path: "", + }); + summary.mockResolvedValue(summaryResult); + const historyError = new Error("Backlink history is unavailable"); + history.mockRejectedValue(historyError); + const cache = createCache(); + + await expect( + profileBacklinksOverview( + cache, + "overview-key", + { target: "example.com" }, + billingCustomer, + ), + ).rejects.toBe(historyError); + await expect(cache.get("overview-key")).resolves.toBeUndefined(); +}); diff --git a/src/server/features/backlinks/services/backlinksServiceData.ts b/src/server/features/backlinks/services/backlinksServiceData.ts index 5a37b8d38..c35fc856c 100644 --- a/src/server/features/backlinks/services/backlinksServiceData.ts +++ b/src/server/features/backlinks/services/backlinksServiceData.ts @@ -126,15 +126,15 @@ export async function profileBacklinksOverview( includeSubdomains: normalizedTarget.includeSubdomains, creditFeature, }), - // history/live only accepts a hostname and has no include_subdomains field, - // so trends are unavailable for a page and subdomain-inclusive otherwise. - normalizedTarget.scope === "exact_url" - ? Promise.resolve([]) - : dataforseo.backlinks.history({ + // history/live has no include_subdomains field, so its data cannot match + // subdomain-inclusive or exact-URL summaries. + normalizedTarget.scope === "domain" + ? dataforseo.backlinks.history({ target: normalizedTarget.apiTarget, ...dateRange, creditFeature, - }), + }) + : Promise.resolve([]), ]); const overview = buildOverviewResult({