feat(navigation): wire instrumentation — header repo spans + getHeader withSpan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user