feat(core-shared): SentryTracer adapter
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@sentry/nextjs": "^10.51.0",
|
||||||
"@trpc/server": "^11.0.0",
|
"@trpc/server": "^11.0.0",
|
||||||
"payload": "^3.14.0",
|
"payload": "^3.14.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// packages/core-shared/src/instrumentation/sentry/sentry-tracer.test.ts
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@sentry/nextjs", () => ({
|
||||||
|
startSpan: vi.fn((_opts: unknown, fn: (span: unknown) => unknown) =>
|
||||||
|
fn({ setAttribute: vi.fn(), setStatus: vi.fn() }),
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import { SentryTracer } from "@/instrumentation/sentry/sentry-tracer";
|
||||||
|
|
||||||
|
describe("SentryTracer", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates startSpan to @sentry/nextjs.startSpan", async () => {
|
||||||
|
const tracer = new SentryTracer();
|
||||||
|
const result = await tracer.startSpan(
|
||||||
|
{ name: "blog.getArticles", op: "use-case" },
|
||||||
|
async () => "value",
|
||||||
|
);
|
||||||
|
expect(result).toBe("value");
|
||||||
|
expect(Sentry.startSpan).toHaveBeenCalledTimes(1);
|
||||||
|
expect((Sentry.startSpan as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
||||||
|
name: "blog.getArticles",
|
||||||
|
op: "use-case",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forwards attributes to Sentry", async () => {
|
||||||
|
const tracer = new SentryTracer();
|
||||||
|
await tracer.startSpan(
|
||||||
|
{ name: "articles.findAll", op: "repository", attributes: { collection: "articles", limit: 10 } },
|
||||||
|
async () => undefined,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
(Sentry.startSpan as ReturnType<typeof vi.fn>).mock.calls[0][0].attributes,
|
||||||
|
).toEqual({
|
||||||
|
collection: "articles",
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates errors from the wrapped function", async () => {
|
||||||
|
const tracer = new SentryTracer();
|
||||||
|
await expect(
|
||||||
|
tracer.startSpan({ name: "x" }, async () => {
|
||||||
|
throw new Error("boom");
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("boom");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ISpan adapter forwards setAttribute and setStatus to Sentry's span", async () => {
|
||||||
|
const sentrySpan = { setAttribute: vi.fn(), setStatus: vi.fn() };
|
||||||
|
(Sentry.startSpan as ReturnType<typeof vi.fn>).mockImplementationOnce(
|
||||||
|
(_opts: unknown, fn: (span: unknown) => unknown) => fn(sentrySpan),
|
||||||
|
);
|
||||||
|
const tracer = new SentryTracer();
|
||||||
|
await tracer.startSpan({ name: "x" }, async (span) => {
|
||||||
|
span.setAttribute("k", "v");
|
||||||
|
span.setStatus("error", "msg");
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
expect(sentrySpan.setAttribute).toHaveBeenCalledWith("k", "v");
|
||||||
|
expect(sentrySpan.setStatus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// packages/core-shared/src/instrumentation/sentry/sentry-tracer.ts
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import type { ITracer, ISpan, SpanOpts } from "../tracer.interface";
|
||||||
|
|
||||||
|
export class SentryTracer implements ITracer {
|
||||||
|
async startSpan<T>(opts: SpanOpts, fn: (span: ISpan) => Promise<T>): Promise<T> {
|
||||||
|
return Sentry.startSpan(
|
||||||
|
{
|
||||||
|
name: opts.name,
|
||||||
|
op: opts.op,
|
||||||
|
attributes: opts.attributes,
|
||||||
|
},
|
||||||
|
async (sentrySpan) => {
|
||||||
|
const adapter: ISpan = {
|
||||||
|
setAttribute(key, value) {
|
||||||
|
sentrySpan?.setAttribute?.(key, value as string | number | boolean);
|
||||||
|
},
|
||||||
|
setStatus(status, message) {
|
||||||
|
// Sentry v8+ uses { code: number, message?: string }; we map our enum
|
||||||
|
const code = status === "ok" ? 1 : 2;
|
||||||
|
sentrySpan?.setStatus?.({ code, message });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return fn(adapter);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user