feat(core-analytics): add React provider and useAnalytics hook
Adds a ./react subpath export to @repo/core-analytics containing
<AnalyticsProvider value={IAnalytics}> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-18T15:19:07.787Z",
|
"generatedAt": "2026-05-18T15:54:10.661Z",
|
||||||
"commit": "81f75f7",
|
"commit": "065ca1b",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 95.89,
|
"statements": 95.92,
|
||||||
"branches": 89.07,
|
"branches": 89.18,
|
||||||
"functions": 100,
|
"functions": 100,
|
||||||
"lines": 95.89,
|
"lines": 95.92,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 3140,
|
"lf": 3165,
|
||||||
"lh": 3011,
|
"lh": 3036,
|
||||||
"brf": 494,
|
"brf": 499,
|
||||||
"brh": 440,
|
"brh": 445,
|
||||||
"fnf": 149,
|
"fnf": 152,
|
||||||
"fnh": 149
|
"fnh": 152
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"byPackage": {
|
"byPackage": {
|
||||||
@@ -50,12 +50,12 @@
|
|||||||
"functions": 100,
|
"functions": 100,
|
||||||
"lines": 100,
|
"lines": 100,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 27,
|
"lf": 52,
|
||||||
"lh": 27,
|
"lh": 52,
|
||||||
"brf": 7,
|
"brf": 12,
|
||||||
"brh": 7,
|
"brh": 12,
|
||||||
"fnf": 7,
|
"fnf": 10,
|
||||||
"fnh": 7
|
"fnh": 10
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/marketing-pages": {
|
"@repo/marketing-pages": {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts",
|
||||||
|
"./react": "./src/react/index.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --noEmit",
|
"build": "tsc --noEmit",
|
||||||
@@ -12,6 +13,14 @@
|
|||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run --passWithNoTests"
|
"test": "vitest run --passWithNoTests"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@repo/core-shared": "workspace:*"
|
"@repo/core-shared": "workspace:*"
|
||||||
},
|
},
|
||||||
@@ -19,7 +28,11 @@
|
|||||||
"@repo/core-eslint": "workspace:*",
|
"@repo/core-eslint": "workspace:*",
|
||||||
"@repo/core-testing": "workspace:*",
|
"@repo/core-testing": "workspace:*",
|
||||||
"@repo/core-typescript": "workspace:*",
|
"@repo/core-typescript": "workspace:*",
|
||||||
|
"@testing-library/react": "^16.0.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
|
"jsdom": "^25.0.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
"typescript": "^5.8.0",
|
"typescript": "^5.8.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
<AnalyticsProvider value={recording}>
|
||||||
|
<Tracker />
|
||||||
|
</AnalyticsProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
33
packages/core-analytics/src/react/analytics-provider.tsx
Normal file
33
packages/core-analytics/src/react/analytics-provider.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createContext, useContext, type ReactNode } from "react";
|
||||||
|
import type { IAnalytics } from "../analytics.interface";
|
||||||
|
|
||||||
|
const AnalyticsContext = createContext<IAnalytics | null>(null);
|
||||||
|
|
||||||
|
export class AnalyticsContextError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("useAnalytics() must be called within an <AnalyticsProvider>.");
|
||||||
|
this.name = "AnalyticsContextError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnalyticsProvider({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
value: IAnalytics;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<AnalyticsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AnalyticsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAnalytics(): IAnalytics {
|
||||||
|
const analytics = useContext(AnalyticsContext);
|
||||||
|
if (analytics === null) {
|
||||||
|
throw new AnalyticsContextError();
|
||||||
|
}
|
||||||
|
return analytics;
|
||||||
|
}
|
||||||
5
packages/core-analytics/src/react/index.ts
Normal file
5
packages/core-analytics/src/react/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export {
|
||||||
|
AnalyticsProvider,
|
||||||
|
useAnalytics,
|
||||||
|
AnalyticsContextError,
|
||||||
|
} from "./analytics-provider";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@repo/core-typescript/base.json",
|
"extends": "@repo/core-typescript/react-library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"],
|
"include": ["**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import path from "node:path";
|
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";
|
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
|
||||||
|
|
||||||
export default mergeConfig(nodeVitestConfig, {
|
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: {
|
resolve: {
|
||||||
alias: { "@": path.resolve(__dirname, "./src") },
|
alias: { "@": path.resolve(__dirname, "./src") },
|
||||||
},
|
},
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -430,9 +430,21 @@ importers:
|
|||||||
"@repo/core-typescript":
|
"@repo/core-typescript":
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../core-typescript
|
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":
|
"@vitest/coverage-v8":
|
||||||
specifier: ^3.0.0
|
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))
|
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:
|
typescript:
|
||||||
specifier: ^5.8.0
|
specifier: ^5.8.0
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
|||||||
Reference in New Issue
Block a user