diff --git a/apps/web-next/src/server/bind-production.ts b/apps/web-next/src/server/bind-production.ts index 03cf0e2..ffcd7fe 100644 --- a/apps/web-next/src/server/bind-production.ts +++ b/apps/web-next/src/server/bind-production.ts @@ -56,7 +56,7 @@ export async function bindAllProduction(): Promise { const resolvedConfig = await config; bindProductionAuth(resolvedConfig, tracer, logger); // Phase E task 19 bindProductionBlog(resolvedConfig, tracer, logger); // Phase E task 18 - bindProductionMarketingPages(resolvedConfig); + bindProductionMarketingPages(resolvedConfig, tracer, logger); // Phase E task 20 bindProductionNavigation(resolvedConfig); bindProductionMedia(resolvedConfig); } @@ -72,7 +72,7 @@ export async function bindAllDevSeed(): Promise { const { tracer, logger } = resolveInstrumentation(); // Rule 0 await bindDevSeedAuth(tracer, logger); // Phase E task 19 await bindDevSeedBlog(tracer, logger); // Phase E task 18 - await bindDevSeedMarketingPages(); + await bindDevSeedMarketingPages(tracer, logger); // Phase E task 20 await bindDevSeedNavigation(); await bindDevSeedMedia(); } diff --git a/packages/marketing-pages/src/di/bind-dev-seed.test.ts b/packages/marketing-pages/src/di/bind-dev-seed.test.ts index 07e9c1d..cac2e39 100644 --- a/packages/marketing-pages/src/di/bind-dev-seed.test.ts +++ b/packages/marketing-pages/src/di/bind-dev-seed.test.ts @@ -1,5 +1,6 @@ import "reflect-metadata"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { NoopTracer, NoopLogger } from "@repo/core-shared/instrumentation"; import { bindDevSeedMarketingPages } from "@/di/bind-dev-seed"; import { marketingPagesContainer } from "@/di/container"; import { MARKETING_PAGES_SYMBOLS } from "@/di/symbols"; @@ -8,6 +9,8 @@ import { MockSiteSettingsRepository } from "@/infrastructure/repositories/site-s import type { IPagesRepository } from "@/application/repositories/pages.repository.interface"; import type { ISiteSettingsRepository } from "@/application/repositories/site-settings.repository.interface"; +const noop = { tracer: new NoopTracer(), logger: new NoopLogger() }; + describe("bindDevSeedMarketingPages", () => { // Each test starts from default mock bindings and tears down afterwards so // the global marketingPagesContainer state stays clean for siblings. @@ -56,7 +59,7 @@ describe("bindDevSeedMarketingPages", () => { }); it("populates the pages repository with the dev pages", async () => { - await bindDevSeedMarketingPages(); + await bindDevSeedMarketingPages(noop.tracer, noop.logger); const repo = marketingPagesContainer.get( MARKETING_PAGES_SYMBOLS.IPagesRepository, @@ -67,7 +70,7 @@ describe("bindDevSeedMarketingPages", () => { }); it("seeds site settings with a non-default site name", async () => { - await bindDevSeedMarketingPages(); + await bindDevSeedMarketingPages(noop.tracer, noop.logger); const settingsRepo = marketingPagesContainer.get( MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository, @@ -79,13 +82,13 @@ describe("bindDevSeedMarketingPages", () => { }); it("is idempotent — calling twice rebuilds fresh populated repos", async () => { - await bindDevSeedMarketingPages(); + await bindDevSeedMarketingPages(noop.tracer, noop.logger); const before = marketingPagesContainer.get( MARKETING_PAGES_SYMBOLS.IPagesRepository, ); const beforeCount = (await before.getPages()).length; - await bindDevSeedMarketingPages(); + await bindDevSeedMarketingPages(noop.tracer, noop.logger); const after = marketingPagesContainer.get( MARKETING_PAGES_SYMBOLS.IPagesRepository, ); diff --git a/packages/marketing-pages/src/di/bind-dev-seed.ts b/packages/marketing-pages/src/di/bind-dev-seed.ts index 102d699..c8d52b1 100644 --- a/packages/marketing-pages/src/di/bind-dev-seed.ts +++ b/packages/marketing-pages/src/di/bind-dev-seed.ts @@ -1,7 +1,18 @@ +import { + withSpan, + INSTRUMENTATION_SYMBOLS, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import { marketingPagesContainer } from "./container.js"; import { MARKETING_PAGES_SYMBOLS } from "./symbols.js"; import { MockPagesRepository } from "../infrastructure/repositories/pages.repository.mock.js"; +import { MockSiteSettingsRepository } from "../infrastructure/repositories/site-settings.repository.mock.js"; import { buildDevPages, buildDevSiteSettings } from "../__seeds__/dev.js"; +import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settings.use-case.js"; +import { getPageBySlugUseCase } from "../application/use-cases/get-page-by-slug.use-case.js"; +import { getSiteSettingsController } from "../interface-adapters/controllers/get-site-settings.controller.js"; +import { getPageBySlugController } from "../interface-adapters/controllers/get-page-by-slug.controller.js"; import type { IPagesRepository } from "../application/repositories/pages.repository.interface.js"; import type { ISiteSettingsRepository } from "../application/repositories/site-settings.repository.interface.js"; @@ -15,32 +26,82 @@ import type { ISiteSettingsRepository } from "../application/repositories/site-s * Idempotent: safe to call multiple times; each call rebuilds fresh populated * repos and rebinds both symbols. */ -export async function bindDevSeedMarketingPages(): Promise { +export async function bindDevSeedMarketingPages(tracer: ITracer, logger: ILogger): Promise { + // Bind shared instrumentation into feature container + if (marketingPagesContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) { + marketingPagesContainer.unbind(INSTRUMENTATION_SYMBOLS.TRACER); + } + if (marketingPagesContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) { + marketingPagesContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER); + } + marketingPagesContainer.bind(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer); + marketingPagesContainer.bind(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger); + // Pages repository — MockPagesRepository accepts an initial array. if (marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.IPagesRepository)) { marketingPagesContainer.unbind(MARKETING_PAGES_SYMBOLS.IPagesRepository); } - + const pagesRepo = new MockPagesRepository(buildDevPages(), tracer, logger); marketingPagesContainer .bind(MARKETING_PAGES_SYMBOLS.IPagesRepository) - .toConstantValue(new MockPagesRepository(buildDevPages())); + .toConstantValue(pagesRepo); - // Site settings repository — MockSiteSettingsRepository has no mutable - // storage so we bind a constant-value implementation directly. - if ( - marketingPagesContainer.isBound( - MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository, - ) - ) { - marketingPagesContainer.unbind( - MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository, - ); + // Site settings repository + if (marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository)) { + marketingPagesContainer.unbind(MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository); } - const settings = buildDevSiteSettings(); + // Inline constant so dev seed returns exactly the seeded value (not the mock's default) + const siteSettingsRepo: ISiteSettingsRepository = { + getSiteSettings: async () => settings, + }; marketingPagesContainer .bind(MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository) - .toConstantValue({ - getSiteSettings: async () => settings, - }); + .toConstantValue(siteSettingsRepo); + + // Wrap use cases + controllers identically to bind-production + const wrappedGetSiteSettings = withSpan( + tracer, + { name: "marketing-pages.getSiteSettings", op: "use-case" }, + getSiteSettingsUseCase(siteSettingsRepo), + ); + const wrappedGetPageBySlug = withSpan( + tracer, + { name: "marketing-pages.getPageBySlug", op: "use-case" }, + getPageBySlugUseCase(pagesRepo), + ); + + for (const sym of [ + MARKETING_PAGES_SYMBOLS.IGetSiteSettingsUseCase, + MARKETING_PAGES_SYMBOLS.IGetPageBySlugUseCase, + MARKETING_PAGES_SYMBOLS.IGetSiteSettingsController, + MARKETING_PAGES_SYMBOLS.IGetPageBySlugController, + ]) { + if (marketingPagesContainer.isBound(sym)) marketingPagesContainer.unbind(sym); + } + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetSiteSettingsUseCase) + .toConstantValue(wrappedGetSiteSettings); + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetPageBySlugUseCase) + .toConstantValue(wrappedGetPageBySlug); + + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetSiteSettingsController) + .toConstantValue( + withSpan( + tracer, + { name: "marketing-pages.getSiteSettings", op: "controller" }, + getSiteSettingsController(wrappedGetSiteSettings), + ), + ); + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetPageBySlugController) + .toConstantValue( + withSpan( + tracer, + { name: "marketing-pages.getPageBySlug", op: "controller" }, + getPageBySlugController(wrappedGetPageBySlug), + ), + ); } diff --git a/packages/marketing-pages/src/di/bind-production.ts b/packages/marketing-pages/src/di/bind-production.ts index 22590e9..db71408 100644 --- a/packages/marketing-pages/src/di/bind-production.ts +++ b/packages/marketing-pages/src/di/bind-production.ts @@ -1,29 +1,99 @@ import type { SanitizedConfig } from "payload"; +import { + withSpan, + INSTRUMENTATION_SYMBOLS, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import { marketingPagesContainer } from "./container"; import { MARKETING_PAGES_SYMBOLS } from "./symbols"; import { PagesRepository } from "../infrastructure/repositories/pages.repository"; import { SiteSettingsRepository } from "../infrastructure/repositories/site-settings.repository"; +import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settings.use-case"; +import { getPageBySlugUseCase } from "../application/use-cases/get-page-by-slug.use-case"; +import { getSiteSettingsController } from "../interface-adapters/controllers/get-site-settings.controller"; +import { getPageBySlugController } from "../interface-adapters/controllers/get-page-by-slug.controller"; -export function bindProductionMarketingPages(config: SanitizedConfig): void { - if ( - marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.IPagesRepository) - ) { +export function bindProductionMarketingPages( + config: SanitizedConfig, + tracer: ITracer, + logger: ILogger, +): void { + // Bind shared instrumentation into feature container + if (marketingPagesContainer.isBound(INSTRUMENTATION_SYMBOLS.TRACER)) { + marketingPagesContainer.unbind(INSTRUMENTATION_SYMBOLS.TRACER); + } + if (marketingPagesContainer.isBound(INSTRUMENTATION_SYMBOLS.LOGGER)) { + marketingPagesContainer.unbind(INSTRUMENTATION_SYMBOLS.LOGGER); + } + marketingPagesContainer.bind(INSTRUMENTATION_SYMBOLS.TRACER).toConstantValue(tracer); + marketingPagesContainer.bind(INSTRUMENTATION_SYMBOLS.LOGGER).toConstantValue(logger); + + // Real repositories + if (marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.IPagesRepository)) { marketingPagesContainer.unbind(MARKETING_PAGES_SYMBOLS.IPagesRepository); } + const pagesRepo = new PagesRepository(config, tracer, logger); marketingPagesContainer .bind(MARKETING_PAGES_SYMBOLS.IPagesRepository) - .toConstantValue(new PagesRepository(config)); + .toConstantValue(pagesRepo); - if ( - marketingPagesContainer.isBound( - MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository, - ) - ) { - marketingPagesContainer.unbind( - MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository, - ); + if (marketingPagesContainer.isBound(MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository)) { + marketingPagesContainer.unbind(MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository); } + const siteSettingsRepo = new SiteSettingsRepository(config, tracer, logger); marketingPagesContainer .bind(MARKETING_PAGES_SYMBOLS.ISiteSettingsRepository) - .toConstantValue(new SiteSettingsRepository(config)); + .toConstantValue(siteSettingsRepo); + + // Use cases — wrapped with span at bind time + const wrappedGetSiteSettings = withSpan( + tracer, + { name: "marketing-pages.getSiteSettings", op: "use-case" }, + getSiteSettingsUseCase(siteSettingsRepo), + ); + const wrappedGetPageBySlug = withSpan( + tracer, + { name: "marketing-pages.getPageBySlug", op: "use-case" }, + getPageBySlugUseCase(pagesRepo), + ); + + for (const sym of [ + MARKETING_PAGES_SYMBOLS.IGetSiteSettingsUseCase, + MARKETING_PAGES_SYMBOLS.IGetPageBySlugUseCase, + ]) { + if (marketingPagesContainer.isBound(sym)) marketingPagesContainer.unbind(sym); + } + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetSiteSettingsUseCase) + .toConstantValue(wrappedGetSiteSettings); + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetPageBySlugUseCase) + .toConstantValue(wrappedGetPageBySlug); + + // Controllers — wrapped with span at bind time + for (const sym of [ + MARKETING_PAGES_SYMBOLS.IGetSiteSettingsController, + MARKETING_PAGES_SYMBOLS.IGetPageBySlugController, + ]) { + if (marketingPagesContainer.isBound(sym)) marketingPagesContainer.unbind(sym); + } + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetSiteSettingsController) + .toConstantValue( + withSpan( + tracer, + { name: "marketing-pages.getSiteSettings", op: "controller" }, + getSiteSettingsController(wrappedGetSiteSettings), + ), + ); + marketingPagesContainer + .bind(MARKETING_PAGES_SYMBOLS.IGetPageBySlugController) + .toConstantValue( + withSpan( + tracer, + { name: "marketing-pages.getPageBySlug", op: "controller" }, + getPageBySlugController(wrappedGetPageBySlug), + ), + ); } diff --git a/packages/marketing-pages/src/infrastructure/repositories/pages.repository.mock.ts b/packages/marketing-pages/src/infrastructure/repositories/pages.repository.mock.ts index 8508012..724378b 100644 --- a/packages/marketing-pages/src/infrastructure/repositories/pages.repository.mock.ts +++ b/packages/marketing-pages/src/infrastructure/repositories/pages.repository.mock.ts @@ -1,5 +1,11 @@ import "reflect-metadata"; import { injectable } from "inversify"; +import { + NoopTracer, + NoopLogger, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import type { IPagesRepository } from "../../application/repositories/pages.repository.interface"; import type { Page } from "../../entities/models/page"; @@ -24,13 +30,29 @@ const DEFAULT_SEED: Page[] = [ @injectable() export class MockPagesRepository implements IPagesRepository { private _pages: Page[]; + private tracer: ITracer; + private logger: ILogger; - constructor(initialPages: Page[] = DEFAULT_SEED) { + constructor( + initialPages: Page[] = DEFAULT_SEED, + tracer: ITracer = new NoopTracer(), + logger: ILogger = new NoopLogger(), + ) { this._pages = [...initialPages]; + this.tracer = tracer; + this.logger = logger; + void this.logger; // currently unused; reserved for future mock-thrown captures } async getPageBySlug(slug: string): Promise { - return this._pages.find((p) => p.slug === slug); + return this.tracer.startSpan( + { name: "pages.getPageBySlug", op: "repository", attributes: { slug } }, + async (span) => { + const found = this._pages.find((p) => p.slug === slug); + span.setAttribute("found", Boolean(found)); + return found; + }, + ); } async getPages(options?: { @@ -38,12 +60,27 @@ export class MockPagesRepository implements IPagesRepository { limit?: number; offset?: number; }): Promise { - let result = [...this._pages]; - if (options?.status) { - result = result.filter((p) => p.status === options.status); - } - const offset = options?.offset ?? 0; - const limit = options?.limit ?? 50; - return result.slice(offset, offset + limit); + return this.tracer.startSpan( + { + name: "pages.getPages", + op: "repository", + attributes: { + status: options?.status ?? null, + limit: options?.limit ?? null, + offset: options?.offset ?? null, + }, + }, + async (span) => { + let result = [...this._pages]; + if (options?.status) { + result = result.filter((p) => p.status === options.status); + } + const offset = options?.offset ?? 0; + const limit = options?.limit ?? 50; + const page = result.slice(offset, offset + limit); + span.setAttribute("count", page.length); + return page; + }, + ); } } diff --git a/packages/marketing-pages/src/infrastructure/repositories/pages.repository.span.test.ts b/packages/marketing-pages/src/infrastructure/repositories/pages.repository.span.test.ts new file mode 100644 index 0000000..7d18b29 --- /dev/null +++ b/packages/marketing-pages/src/infrastructure/repositories/pages.repository.span.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from "vitest"; +import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation"; +import { MockPagesRepository } from "@/infrastructure/repositories/pages.repository.mock"; + +// Mock repo also wraps in spans (R42). +describe("MockPagesRepository emits spans (R42)", () => { + it("getPageBySlug emits one span with op='repository'", async () => { + const tracer = new RecordingTracer(); + const logger = new RecordingLogger(); + const repo = new MockPagesRepository([], tracer, logger); + await repo.getPageBySlug("about"); + expect(tracer.spans).toHaveLength(1); + expect(tracer.spans[0]).toMatchObject({ + name: "pages.getPageBySlug", + op: "repository", + }); + expect(tracer.spans[0]!.attributes.slug).toBe("about"); + expect(tracer.spans[0]!.attributes.found).toBe(false); + }); + + it("getPages emits a span with count attribute", async () => { + const tracer = new RecordingTracer(); + const repo = new MockPagesRepository(undefined, tracer); + await repo.getPages({ limit: 10 }); + expect(tracer.findSpan("pages.getPages")).toBeDefined(); + expect(tracer.findSpan("pages.getPages")!.attributes.limit).toBe(10); + expect(tracer.findSpan("pages.getPages")!.attributes.count).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/marketing-pages/src/infrastructure/repositories/pages.repository.ts b/packages/marketing-pages/src/infrastructure/repositories/pages.repository.ts index 9ddc2c6..e0cd975 100644 --- a/packages/marketing-pages/src/infrastructure/repositories/pages.repository.ts +++ b/packages/marketing-pages/src/infrastructure/repositories/pages.repository.ts @@ -2,6 +2,12 @@ import "reflect-metadata"; import { injectable } from "inversify"; import { getPayload } from "payload"; import type { SanitizedConfig } from "payload"; +import { + NoopTracer, + NoopLogger, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import type { IPagesRepository } from "../../application/repositories/pages.repository.interface"; import type { Page } from "../../entities/models/page"; @@ -54,24 +60,49 @@ function mapDoc(doc: PayloadPageDoc): Page { }; } +const FEATURE = "marketing-pages" as const; +const REPO = "pages" as const; + @injectable() export class PagesRepository implements IPagesRepository { private config: SanitizedConfig; + private tracer: ITracer; + private logger: ILogger; - constructor(config: SanitizedConfig) { + constructor( + config: SanitizedConfig, + tracer: ITracer = new NoopTracer(), + logger: ILogger = new NoopLogger(), + ) { this.config = config; + this.tracer = tracer; + this.logger = logger; } async getPageBySlug(slug: string): Promise { - const payload = await getPayload({ config: this.config }); - const result = await payload.find({ - collection: "pages", - where: { slug: { equals: slug } }, - limit: 1, - overrideAccess: true, - }); - const doc = result.docs[0] as PayloadPageDoc | undefined; - return doc ? mapDoc(doc) : undefined; + return this.tracer.startSpan( + { name: "pages.getPageBySlug", op: "repository", attributes: { slug } }, + async (span) => { + try { + const payload = await getPayload({ config: this.config }); + const result = await payload.find({ + collection: "pages", + where: { slug: { equals: slug } }, + limit: 1, + overrideAccess: true, + }); + const doc = result.docs[0] as PayloadPageDoc | undefined; + span.setAttribute("found", Boolean(doc)); + return doc ? mapDoc(doc) : undefined; + } catch (err) { + this.logger.captureException(err, { + tags: { feature: FEATURE, repo: REPO, method: "getPageBySlug" }, + }); + span.setStatus("error", err instanceof Error ? err.message : String(err)); + throw err; + } + }, + ); } async getPages(options?: { @@ -79,18 +110,41 @@ export class PagesRepository implements IPagesRepository { limit?: number; offset?: number; }): Promise { - const payload = await getPayload({ config: this.config }); - const where: Record = {}; - if (options?.status) where.status = { equals: options.status }; - const result = await payload.find({ - collection: "pages", - where: where as never, - limit: options?.limit ?? 50, - page: options?.offset - ? Math.floor(options.offset / (options.limit ?? 50)) + 1 - : 1, - overrideAccess: true, - }); - return result.docs.map((d) => mapDoc(d as PayloadPageDoc)); + return this.tracer.startSpan( + { + name: "pages.getPages", + op: "repository", + attributes: { + status: options?.status ?? null, + limit: options?.limit ?? null, + offset: options?.offset ?? null, + }, + }, + async (span) => { + try { + const payload = await getPayload({ config: this.config }); + const where: Record = {}; + if (options?.status) where.status = { equals: options.status }; + const result = await payload.find({ + collection: "pages", + where: where as never, + limit: options?.limit ?? 50, + page: options?.offset + ? Math.floor(options.offset / (options.limit ?? 50)) + 1 + : 1, + overrideAccess: true, + }); + const pages = result.docs.map((d) => mapDoc(d as PayloadPageDoc)); + span.setAttribute("count", pages.length); + return pages; + } catch (err) { + this.logger.captureException(err, { + tags: { feature: FEATURE, repo: REPO, method: "getPages" }, + }); + span.setStatus("error", err instanceof Error ? err.message : String(err)); + throw err; + } + }, + ); } } diff --git a/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.mock.ts b/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.mock.ts index eee9693..4a4ef34 100644 --- a/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.mock.ts +++ b/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.mock.ts @@ -1,15 +1,39 @@ import "reflect-metadata"; import { injectable } from "inversify"; +import { + NoopTracer, + NoopLogger, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import type { ISiteSettingsRepository } from "../../application/repositories/site-settings.repository.interface"; import type { SiteSettings } from "../../entities/models/site-settings"; @injectable() export class MockSiteSettingsRepository implements ISiteSettingsRepository { + private tracer: ITracer; + private logger: ILogger; + + constructor( + tracer: ITracer = new NoopTracer(), + logger: ILogger = new NoopLogger(), + ) { + this.tracer = tracer; + this.logger = logger; + void this.logger; // currently unused; reserved for future mock-thrown captures + } + async getSiteSettings(): Promise { - return { - siteName: "My App", - siteDescription: "A vertical-feature monorepo template", - }; + return this.tracer.startSpan( + { name: "site-settings.getSiteSettings", op: "repository", attributes: {} }, + async (span) => { + span.setAttribute("found", true); + return { + siteName: "My App", + siteDescription: "A vertical-feature monorepo template", + }; + }, + ); } } diff --git a/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.span.test.ts b/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.span.test.ts new file mode 100644 index 0000000..765e806 --- /dev/null +++ b/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.span.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation"; +import { MockSiteSettingsRepository } from "@/infrastructure/repositories/site-settings.repository.mock"; + +// Mock repo also wraps in spans (R42). +describe("MockSiteSettingsRepository emits spans (R42)", () => { + it("getSiteSettings emits one span with op='repository'", async () => { + const tracer = new RecordingTracer(); + const logger = new RecordingLogger(); + const repo = new MockSiteSettingsRepository(tracer, logger); + await repo.getSiteSettings(); + expect(tracer.spans).toHaveLength(1); + expect(tracer.spans[0]).toMatchObject({ + name: "site-settings.getSiteSettings", + op: "repository", + }); + expect(tracer.spans[0]!.attributes.found).toBe(true); + }); +}); diff --git a/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.ts b/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.ts index 239ce17..4fb2fa1 100644 --- a/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.ts +++ b/packages/marketing-pages/src/infrastructure/repositories/site-settings.repository.ts @@ -2,6 +2,12 @@ import "reflect-metadata"; import { injectable } from "inversify"; import { getPayload } from "payload"; import type { SanitizedConfig } from "payload"; +import { + NoopTracer, + NoopLogger, + type ITracer, + type ILogger, +} from "@repo/core-shared/instrumentation"; import type { ISiteSettingsRepository } from "../../application/repositories/site-settings.repository.interface"; import type { SiteSettings } from "../../entities/models/site-settings"; @@ -11,23 +17,48 @@ type PayloadSiteSettings = { siteDescription?: string | null; }; +const FEATURE = "marketing-pages" as const; +const REPO = "site-settings" as const; + @injectable() export class SiteSettingsRepository implements ISiteSettingsRepository { private config: SanitizedConfig; + private tracer: ITracer; + private logger: ILogger; - constructor(config: SanitizedConfig) { + constructor( + config: SanitizedConfig, + tracer: ITracer = new NoopTracer(), + logger: ILogger = new NoopLogger(), + ) { this.config = config; + this.tracer = tracer; + this.logger = logger; } async getSiteSettings(): Promise { - const payload = await getPayload({ config: this.config }); - const doc = (await payload.findGlobal({ - slug: "site-settings", - overrideAccess: true, - })) as PayloadSiteSettings; - return { - siteName: doc.siteName ?? "My App", - siteDescription: doc.siteDescription ?? undefined, - }; + return this.tracer.startSpan( + { name: "site-settings.getSiteSettings", op: "repository", attributes: {} }, + async (span) => { + try { + const payload = await getPayload({ config: this.config }); + const doc = (await payload.findGlobal({ + slug: "site-settings", + overrideAccess: true, + })) as PayloadSiteSettings; + span.setAttribute("found", true); + return { + siteName: doc.siteName ?? "My App", + siteDescription: doc.siteDescription ?? undefined, + }; + } catch (err) { + this.logger.captureException(err, { + tags: { feature: FEATURE, repo: REPO, method: "getSiteSettings" }, + }); + span.setStatus("error", err instanceof Error ? err.message : String(err)); + throw err; + } + }, + ); } }