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