diff --git a/packages/core-testing/src/instrumentation/index.ts b/packages/core-testing/src/instrumentation/index.ts index 8d29f2f..b6c50de 100644 --- a/packages/core-testing/src/instrumentation/index.ts +++ b/packages/core-testing/src/instrumentation/index.ts @@ -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"; diff --git a/packages/core-testing/src/instrumentation/recording-analytics.test.ts b/packages/core-testing/src/instrumentation/recording-analytics.test.ts new file mode 100644 index 0000000..8852fd9 --- /dev/null +++ b/packages/core-testing/src/instrumentation/recording-analytics.test.ts @@ -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(); + }); +}); diff --git a/packages/core-testing/src/instrumentation/recording-analytics.ts b/packages/core-testing/src/instrumentation/recording-analytics.ts new file mode 100644 index 0000000..33c40d3 --- /dev/null +++ b/packages/core-testing/src/instrumentation/recording-analytics.ts @@ -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; +}; + +export type RecordedIdentify = { + user: AnalyticsUser; + attributes?: Record; +}; + +export type RecordedPageView = { + path: string; + attributes?: Record; +}; + +export class RecordingAnalytics { + public tracked: RecordedTrack[] = []; + public identified: RecordedIdentify[] = []; + public pageViewed: RecordedPageView[] = []; + + track( + event: string, + attributes?: Record, + ): void { + this.tracked.push({ event, attributes }); + } + + identify( + user: AnalyticsUser, + attributes?: Record, + ): void { + this.identified.push({ user, attributes }); + } + + pageView( + path: string, + attributes?: Record, + ): void { + this.pageViewed.push({ path, attributes }); + } + + flush(): Promise { + this.tracked = []; + this.identified = []; + this.pageViewed = []; + return Promise.resolve(); + } + + reset(): void { + this.tracked = []; + this.identified = []; + this.pageViewed = []; + } +}