# React analytics
> Record page views and events from a React app, and tell Myna which content entry each page is showing.
`@myna-sh/react/analytics` is a thin layer over [`@myna-sh/sdk/analytics`](/sdk/analytics): a provider, a hook to reach the client, and two hooks for the things a React application gets wrong on its own, which are page views on client-side navigation and telling Myna which content entry a page is showing.
```bash
$ npm install @myna-sh/react @myna-sh/sdk
```
```tsx
import { AnalyticsProvider } from "@myna-sh/react/analytics";
```
`signature` comes from your own backend, out of [`signIdentity`](/concepts/collecting-feedback#identifying-your-users) in `@myna-sh/sdk/feedback/server`. Without it the visitor stays anonymous.
## Recording events
```tsx
import { useAnalytics } from "@myna-sh/react/analytics";
function PricingCta() {
const analytics = useAnalytics();
return (
);
}
```
`useAnalytics` throws outside a provider rather than returning a no-op. A tracking call that silently does nothing is the bug that surfaces a month later, when somebody asks why a funnel has no second step.
## Page views
```tsx
import { usePageview } from "@myna-sh/react/analytics";
function App() {
const { pathname } = useLocation();
usePageview(pathname);
return ;
}
```
Your router knows when a route actually resolved. The History API only knows when the URL changed, which is earlier and is sometimes wrong: a navigation that suspends, redirects, or fails would still have been counted.
The hook is guarded against React's development StrictMode double-effect, so a page view is recorded once per navigation rather than twice.
Use `usePageview` **or** the provider's `autoPageviews`, never both. Two sources double every number on the dashboard, and nothing in the data says which half is spurious.
## Content context
```tsx
import { useContentContext } from "@myna-sh/react/analytics";
import { useMynaEntry } from "@myna-sh/react";
function PricingPage() {
const { data: page } = useMynaEntry("pages", "pricing");
useContentContext(page && { entry: page.id, collection: "pages", revision: page.revisionId });
return ;
}
```
Every event fired while this component is mounted carries the entry, the revision the visitor actually saw, and the release it shipped in. That is what makes [release impact](/concepts/analytics#content-aware-analytics) answerable without a data pipeline.
The hook clears the content on unmount, so an event on the next route is not attributed to the entry the last one was showing.
## Provider options
| Prop | Means |
|---|---|
| `ingestKey` | Required. A publishable `myna_ik_…` key. |
| `apiUrl` | Override for a self-hosted or staging deployment. |
| `environment` | Your environment name, attached to every event. |
| `appVersion` | Your build: a version, a commit sha, a deployment id. |
| `identify` | The signed identity, or `undefined` when nobody is signed in. |
| `autoPageviews` | Send `$pageview` on load and on History navigation. |
| `storage` | `local`, `session`, or `memory`. |
| `onError` | Called when a batch is refused. |
The client is memoized on everything except `identify` and `onError`. Your backend refreshes a signature far more often than a key changes, and rebuilding the client on each one would throw away the events it was about to send. The identity is pushed into the existing client instead, which flushes the buffer first.
The provider flushes on unmount, so the last event of a visit, very often the interesting one, is not the one that never arrives.
## Server rendering
Everything here is client-side. `createAnalytics` reads `window` defensively, so a provider rendered on the server produces no events and throws nothing. Recording starts when the tree hydrates.