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:
@@ -5,3 +5,9 @@ export { RecordingJobQueue } from "./recording-job-queue";
|
|||||||
export { RecordingEventBus } from "./recording-event-bus";
|
export { RecordingEventBus } from "./recording-event-bus";
|
||||||
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
|
export { RecordingRealtimeBroadcaster } from "./recording-realtime-broadcaster";
|
||||||
export { RecordingAuditLog } from "./recording-audit-log";
|
export { RecordingAuditLog } from "./recording-audit-log";
|
||||||
|
export {
|
||||||
|
RecordingAnalytics,
|
||||||
|
type RecordedTrack,
|
||||||
|
type RecordedIdentify,
|
||||||
|
type RecordedPageView,
|
||||||
|
} from "./recording-analytics";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user