From a7e0bf290de78273b6bc29e6653a5d4c0149d6be Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 18 May 2026 15:54:30 +0000 Subject: [PATCH] feat(core-analytics): add React provider and useAnalytics hook Adds a ./react subpath export to @repo/core-analytics containing and useAnalytics(): IAnalytics. useAnalytics() throws AnalyticsContextError when called outside a provider. React Testing Library test verifies track() flows through context using RecordingAnalytics. Switches vitest config to pick up .tsx test files via environmentMatchGlobs and extends tsconfig to react-library.json for JSX support. Co-Authored-By: Claude Sonnet 4.6 --- coverage/summary.json | 34 +++++++-------- packages/core-analytics/package.json | 15 ++++++- .../src/react/analytics-provider.test.tsx | 41 +++++++++++++++++++ .../src/react/analytics-provider.tsx | 33 +++++++++++++++ packages/core-analytics/src/react/index.ts | 5 +++ packages/core-analytics/tsconfig.json | 4 +- packages/core-analytics/vitest.config.ts | 20 ++++++--- pnpm-lock.yaml | 12 ++++++ 8 files changed, 138 insertions(+), 26 deletions(-) create mode 100644 packages/core-analytics/src/react/analytics-provider.test.tsx create mode 100644 packages/core-analytics/src/react/analytics-provider.tsx create mode 100644 packages/core-analytics/src/react/index.ts diff --git a/coverage/summary.json b/coverage/summary.json index 0a2d1e7..22f9a4f 100644 --- a/coverage/summary.json +++ b/coverage/summary.json @@ -1,18 +1,18 @@ { - "generatedAt": "2026-05-18T15:19:07.787Z", - "commit": "81f75f7", + "generatedAt": "2026-05-18T15:54:10.661Z", + "commit": "065ca1b", "repo": { - "statements": 95.89, - "branches": 89.07, + "statements": 95.92, + "branches": 89.18, "functions": 100, - "lines": 95.89, + "lines": 95.92, "counts": { - "lf": 3140, - "lh": 3011, - "brf": 494, - "brh": 440, - "fnf": 149, - "fnh": 149 + "lf": 3165, + "lh": 3036, + "brf": 499, + "brh": 445, + "fnf": 152, + "fnh": 152 } }, "byPackage": { @@ -50,12 +50,12 @@ "functions": 100, "lines": 100, "counts": { - "lf": 27, - "lh": 27, - "brf": 7, - "brh": 7, - "fnf": 7, - "fnh": 7 + "lf": 52, + "lh": 52, + "brf": 12, + "brh": 12, + "fnf": 10, + "fnh": 10 } }, "@repo/marketing-pages": { diff --git a/packages/core-analytics/package.json b/packages/core-analytics/package.json index e9b170e..215c0bc 100644 --- a/packages/core-analytics/package.json +++ b/packages/core-analytics/package.json @@ -4,7 +4,8 @@ "private": true, "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./react": "./src/react/index.ts" }, "scripts": { "build": "tsc --noEmit", @@ -12,6 +13,14 @@ "typecheck": "tsc --noEmit", "test": "vitest run --passWithNoTests" }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + }, "dependencies": { "@repo/core-shared": "workspace:*" }, @@ -19,7 +28,11 @@ "@repo/core-eslint": "workspace:*", "@repo/core-testing": "workspace:*", "@repo/core-typescript": "workspace:*", + "@testing-library/react": "^16.0.0", + "@types/react": "^19.0.0", "@vitest/coverage-v8": "^3.0.0", + "jsdom": "^25.0.0", + "react": "^19.0.0", "typescript": "^5.8.0", "vitest": "^3.0.0" } diff --git a/packages/core-analytics/src/react/analytics-provider.test.tsx b/packages/core-analytics/src/react/analytics-provider.test.tsx new file mode 100644 index 0000000..9bad9e4 --- /dev/null +++ b/packages/core-analytics/src/react/analytics-provider.test.tsx @@ -0,0 +1,41 @@ +import { describe, expect, it, vi } from "vitest"; +import { render, renderHook } from "@testing-library/react"; +import { RecordingAnalytics } from "@repo/core-testing"; +import { + AnalyticsContextError, + AnalyticsProvider, + useAnalytics, +} from "@/react/index"; + +function Tracker() { + const analytics = useAnalytics(); + analytics.track("test.event"); + return null; +} + +describe("AnalyticsProvider", () => { + it("makes analytics available through context and track flows through", () => { + const recording = new RecordingAnalytics(); + + render( + + + , + ); + + expect(recording.tracked).toContainEqual({ event: "test.event" }); + }); +}); + +describe("useAnalytics", () => { + it("throws AnalyticsContextError when called outside a provider", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + expect(() => renderHook(() => useAnalytics())).toThrow( + AnalyticsContextError, + ); + } finally { + spy.mockRestore(); + } + }); +}); diff --git a/packages/core-analytics/src/react/analytics-provider.tsx b/packages/core-analytics/src/react/analytics-provider.tsx new file mode 100644 index 0000000..0a7f42a --- /dev/null +++ b/packages/core-analytics/src/react/analytics-provider.tsx @@ -0,0 +1,33 @@ +import { createContext, useContext, type ReactNode } from "react"; +import type { IAnalytics } from "../analytics.interface"; + +const AnalyticsContext = createContext(null); + +export class AnalyticsContextError extends Error { + constructor() { + super("useAnalytics() must be called within an ."); + this.name = "AnalyticsContextError"; + } +} + +export function AnalyticsProvider({ + value, + children, +}: { + value: IAnalytics; + children: ReactNode; +}) { + return ( + + {children} + + ); +} + +export function useAnalytics(): IAnalytics { + const analytics = useContext(AnalyticsContext); + if (analytics === null) { + throw new AnalyticsContextError(); + } + return analytics; +} diff --git a/packages/core-analytics/src/react/index.ts b/packages/core-analytics/src/react/index.ts new file mode 100644 index 0000000..17b6ae4 --- /dev/null +++ b/packages/core-analytics/src/react/index.ts @@ -0,0 +1,5 @@ +export { + AnalyticsProvider, + useAnalytics, + AnalyticsContextError, +} from "./analytics-provider"; diff --git a/packages/core-analytics/tsconfig.json b/packages/core-analytics/tsconfig.json index 8facef5..bfb5a39 100644 --- a/packages/core-analytics/tsconfig.json +++ b/packages/core-analytics/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@repo/core-typescript/base.json", + "extends": "@repo/core-typescript/react-library.json", "compilerOptions": { "outDir": "dist", "rootDir": ".", @@ -7,6 +7,6 @@ "@/*": ["./src/*"] } }, - "include": ["**/*.ts"], + "include": ["**/*.ts", "**/*.tsx"], "exclude": ["node_modules", "dist"] } diff --git a/packages/core-analytics/vitest.config.ts b/packages/core-analytics/vitest.config.ts index 2ee07c1..79a7ee6 100644 --- a/packages/core-analytics/vitest.config.ts +++ b/packages/core-analytics/vitest.config.ts @@ -1,9 +1,17 @@ import path from "node:path"; -import { mergeConfig } from "vitest/config"; +import { defineConfig, mergeConfig } from "vitest/config"; import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; -export default mergeConfig(nodeVitestConfig, { - resolve: { - alias: { "@": path.resolve(__dirname, "./src") }, - }, -}); +export default mergeConfig( + nodeVitestConfig, + defineConfig({ + test: { + include: ["src/**/*.test.{ts,tsx}", "tests/**/*.test.{ts,tsx}"], + environmentMatchGlobs: [["**/*.test.tsx", "jsdom"]], + setupFiles: ["@repo/core-testing/setup/jsdom"], + }, + resolve: { + alias: { "@": path.resolve(__dirname, "./src") }, + }, + }), +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb805b8..ddd5939 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -430,9 +430,21 @@ importers: "@repo/core-typescript": specifier: workspace:* version: link:../core-typescript + "@testing-library/react": + specifier: ^16.0.0 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + "@types/react": + specifier: ^19.0.0 + version: 19.2.14 "@vitest/coverage-v8": specifier: ^3.0.0 version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)) + jsdom: + specifier: ^25.0.0 + version: 25.0.1 + react: + specifier: ^19.0.0 + version: 19.2.4 typescript: specifier: ^5.8.0 version: 5.9.3