feat(navigation): wire instrumentation — header repo spans + getHeader withSpan

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 17:59:14 +02:00
parent 404cb07e79
commit e14a23dccd
7 changed files with 202 additions and 31 deletions

View File

@@ -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 { IHeaderRepository } from "../../application/repositories/header.repository.interface";
import type { Header, HeaderItem } from "../../entities/models/header";
@@ -13,12 +19,27 @@ const DEFAULT_ITEMS: HeaderItem[] = [
@injectable()
export class MockHeaderRepository implements IHeaderRepository {
private readonly data: Header;
private tracer: ITracer;
private logger: ILogger;
constructor(initialData?: Header) {
constructor(
initialData?: Header,
tracer: ITracer = new NoopTracer(),
logger: ILogger = new NoopLogger(),
) {
this.data = initialData ?? { items: DEFAULT_ITEMS };
this.tracer = tracer;
this.logger = logger;
void this.logger; // currently unused; reserved for future mock-thrown captures
}
async getHeader(): Promise<Header> {
return this.data;
return this.tracer.startSpan(
{ name: "header.getHeader", op: "repository", attributes: {} },
async (span) => {
span.setAttribute("itemCount", this.data.items.length);
return this.data;
},
);
}
}

View File

@@ -0,0 +1,19 @@
import { describe, it, expect } from "vitest";
import { RecordingTracer, RecordingLogger } from "@repo/core-testing/instrumentation";
import { MockHeaderRepository } from "@/infrastructure/repositories/header.repository.mock";
// Mock repo also wraps in spans (R42).
describe("MockHeaderRepository emits spans (R42)", () => {
it("getHeader emits one span with op='repository'", async () => {
const tracer = new RecordingTracer();
const logger = new RecordingLogger();
const repo = new MockHeaderRepository(undefined, tracer, logger);
await repo.getHeader();
expect(tracer.spans).toHaveLength(1);
expect(tracer.spans[0]).toMatchObject({
name: "header.getHeader",
op: "repository",
});
expect(typeof tracer.spans[0]!.attributes.itemCount).toBe("number");
});
});

View File

@@ -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 { IHeaderRepository } from "../../application/repositories/header.repository.interface";
import type { Header, HeaderItem } from "../../entities/models/header";
@@ -15,34 +21,59 @@ type PayloadHeaderGlobal = {
}> | null;
};
const FEATURE = "navigation" as const;
const REPO = "header" as const;
@injectable()
export class HeaderRepository implements IHeaderRepository {
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 getHeader(): Promise<Header> {
const payload = await getPayload({ config: this.config });
const doc = (await payload.findGlobal({
slug: "header",
overrideAccess: true,
})) as PayloadHeaderGlobal;
return this.tracer.startSpan(
{ name: "header.getHeader", op: "repository", attributes: {} },
async (span) => {
try {
const payload = await getPayload({ config: this.config });
const doc = (await payload.findGlobal({
slug: "header",
overrideAccess: true,
})) as PayloadHeaderGlobal;
const logoId =
typeof doc.logo === "object" && doc.logo !== null
? String(doc.logo.id)
: doc.logo != null
? String(doc.logo)
: undefined;
const logoId =
typeof doc.logo === "object" && doc.logo !== null
? String(doc.logo.id)
: doc.logo != null
? String(doc.logo)
: undefined;
const items: HeaderItem[] = (doc.items ?? []).map((item) => ({
label: item.label ?? "",
href: item.href ?? "",
external: item.external ?? false,
}));
const items: HeaderItem[] = (doc.items ?? []).map((item) => ({
label: item.label ?? "",
href: item.href ?? "",
external: item.external ?? false,
}));
return { logoId, items };
span.setAttribute("itemCount", items.length);
return { logoId, items };
} catch (err) {
this.logger.captureException(err, {
tags: { feature: FEATURE, repo: REPO, method: "getHeader" },
});
span.setStatus("error", err instanceof Error ? err.message : String(err));
throw err;
}
},
);
}
}