Adds `withAnalytics(analytics, factory)` to packages/core-analytics — mirrors the `withAudit` pattern: thin forwarding closure that attaches the `__analyzed` brand via `attachBrand` from `@repo/core-shared/conformance` without mutating the original factory. Exports `Analyzed<F>` type and `withAnalytics` from the `@repo/core-analytics` root barrel. Adds `with-analytics.test.ts` asserting brand is present after wrapping, absent on the original fn, output passes through unchanged, and errors propagate. Adds `@repo/core-shared` as a production dependency. Also fixes `scripts/library-decisions/check.mjs` to exempt workspace-protocol entries (`workspace:*`) from the library trace requirement — internal monorepo packages are not third-party libraries and were incorrectly gated. Adds a regression test in `check.test.mjs` covering the exemption. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { describe, it, expect, expectTypeOf } from "vitest";
|
|
import { withAnalytics, type Analyzed } from "@/with-analytics";
|
|
import type { IAnalytics } from "@/analytics.interface";
|
|
import { isAnalyzed } from "@repo/core-shared/conformance";
|
|
|
|
function makeAnalytics(): IAnalytics {
|
|
return {
|
|
track: () => undefined,
|
|
identify: () => undefined,
|
|
pageView: () => undefined,
|
|
flush: () => Promise.resolve(),
|
|
};
|
|
}
|
|
|
|
describe("withAnalytics", () => {
|
|
it("returns an Analyzed<F>", () => {
|
|
const analytics = makeAnalytics();
|
|
const fn = async (_input: { id: string }) => ({ ok: true });
|
|
const wrapped = withAnalytics(analytics, fn);
|
|
expectTypeOf(wrapped).toMatchTypeOf<Analyzed<typeof fn>>();
|
|
});
|
|
|
|
it("attaches __analyzed as a non-enumerable property on the wrapped function", () => {
|
|
const analytics = makeAnalytics();
|
|
const fn = async () => ({ ok: true });
|
|
const wrapped = withAnalytics(analytics, fn);
|
|
expect(isAnalyzed(wrapped)).toBe(true);
|
|
expect(Object.keys(wrapped)).not.toContain("__analyzed");
|
|
});
|
|
|
|
it("does NOT pollute the original input function with the brand", () => {
|
|
const analytics = makeAnalytics();
|
|
const fn = async () => ({ ok: true });
|
|
const wrapped = withAnalytics(analytics, fn);
|
|
expect(isAnalyzed(fn)).toBe(false);
|
|
expect(wrapped).not.toBe(fn);
|
|
});
|
|
|
|
it("passes input and output through unchanged", async () => {
|
|
const analytics = makeAnalytics();
|
|
const fn = async (input: { id: string }) => ({ ok: true, id: input.id });
|
|
const wrapped = withAnalytics(analytics, fn);
|
|
const result = await wrapped({ id: "abc" });
|
|
expect(result).toEqual({ ok: true, id: "abc" });
|
|
});
|
|
|
|
it("propagates errors", async () => {
|
|
const analytics = makeAnalytics();
|
|
const err = new Error("boom");
|
|
const wrapped = withAnalytics(analytics, async () => {
|
|
throw err;
|
|
});
|
|
await expect(wrapped()).rejects.toBe(err);
|
|
});
|
|
});
|