feat(core-testing): add RecordingAnalytics test double

Implements IAnalytics with recorded arrays (tracked, identified,
pageViewed) and flush() that drains the buffer. Mirrors the pattern
established by RecordingAuditLog; inline type aliases avoid a
build-graph cycle with @repo/core-analytics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 11:35:32 +00:00
parent 1434fe9d82
commit 67879ffad2
3 changed files with 120 additions and 0 deletions

View File

@@ -5,3 +5,9 @@ export { RecordingJobQueue } from "./recording-job-queue";
export { RecordingEventBus } from "./recording-event-bus";
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
export { RecordingAuditLog } from "./recording-audit-log";
export {
RecordingAnalytics,
type RecordedTrack,
type RecordedIdentify,
type RecordedPageView,
} from "./recording-analytics";

View File

@@ -0,0 +1,49 @@
import { describe, it, expect } from "vitest";
import { RecordingAnalytics } from "./recording-analytics";
describe("RecordingAnalytics", () => {
it("track() pushes to tracked[]", () => {
const analytics = new RecordingAnalytics();
analytics.track("button_clicked", { page: "home" });
expect(analytics.tracked).toHaveLength(1);
expect(analytics.tracked[0]!.event).toBe("button_clicked");
expect(analytics.tracked[0]!.attributes).toEqual({ page: "home" });
});
it("identify() pushes to identified[]", () => {
const analytics = new RecordingAnalytics();
analytics.identify({ id: "user_1" }, { plan: "pro" });
expect(analytics.identified).toHaveLength(1);
expect(analytics.identified[0]!.user).toEqual({ id: "user_1" });
expect(analytics.identified[0]!.attributes).toEqual({ plan: "pro" });
});
it("pageView() pushes to pageViewed[]", () => {
const analytics = new RecordingAnalytics();
analytics.pageView("/home", { referrer: "google" });
expect(analytics.pageViewed).toHaveLength(1);
expect(analytics.pageViewed[0]!.path).toBe("/home");
expect(analytics.pageViewed[0]!.attributes).toEqual({ referrer: "google" });
});
it("flush() drains all arrays and resolves", async () => {
const analytics = new RecordingAnalytics();
analytics.track("event_1");
analytics.identify({ id: "user_1" });
analytics.pageView("/about");
await analytics.flush();
expect(analytics.tracked).toEqual([]);
expect(analytics.identified).toEqual([]);
expect(analytics.pageViewed).toEqual([]);
});
it("methods accept calls without attributes", () => {
const analytics = new RecordingAnalytics();
analytics.track("no_attrs");
analytics.identify({ id: "user_2" });
analytics.pageView("/contact");
expect(analytics.tracked[0]!.attributes).toBeUndefined();
expect(analytics.identified[0]!.attributes).toBeUndefined();
expect(analytics.pageViewed[0]!.attributes).toBeUndefined();
});
});

View File

@@ -0,0 +1,65 @@
// Local type aliases mirroring IAnalytics from `@repo/core-analytics`.
// Kept inline to avoid a build-graph cycle between core-testing (tooling)
// and core-analytics (core). Same pattern used by recording-audit-log and
// recording-event-bus.
type AnalyticsAttributeValue = string | number | boolean;
type AnalyticsUser = {
id: string;
};
export type RecordedTrack = {
event: string;
attributes?: Record<string, AnalyticsAttributeValue>;
};
export type RecordedIdentify = {
user: AnalyticsUser;
attributes?: Record<string, AnalyticsAttributeValue>;
};
export type RecordedPageView = {
path: string;
attributes?: Record<string, AnalyticsAttributeValue>;
};
export class RecordingAnalytics {
public tracked: RecordedTrack[] = [];
public identified: RecordedIdentify[] = [];
public pageViewed: RecordedPageView[] = [];
track(
event: string,
attributes?: Record<string, AnalyticsAttributeValue>,
): void {
this.tracked.push({ event, attributes });
}
identify(
user: AnalyticsUser,
attributes?: Record<string, AnalyticsAttributeValue>,
): void {
this.identified.push({ user, attributes });
}
pageView(
path: string,
attributes?: Record<string, AnalyticsAttributeValue>,
): void {
this.pageViewed.push({ path, attributes });
}
flush(): Promise<void> {
this.tracked = [];
this.identified = [];
this.pageViewed = [];
return Promise.resolve();
}
reset(): void {
this.tracked = [];
this.identified = [];
this.pageViewed = [];
}
}