SDK

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: 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.

$ npm install @myna-sh/react @myna-sh/sdk
import { AnalyticsProvider } from "@myna-sh/react/analytics";

<AnalyticsProvider
  ingestKey="myna_ik_..."
  environment="production"
  identify={user ? { id: user.id, email: user.email, signature } : undefined}
>
  <App />
</AnalyticsProvider>

signature comes from your own backend, out of signIdentity in @myna-sh/sdk/feedback/server. Without it the visitor stays anonymous.

Recording events

import { useAnalytics } from "@myna-sh/react/analytics";

function PricingCta() {
  const analytics = useAnalytics();
  return (
    <button onClick={() => analytics.track("signup_clicked", { plan: "pro" })}>
      Start building
    </button>
  );
}

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

import { usePageview } from "@myna-sh/react/analytics";

function App() {
  const { pathname } = useLocation();
  usePageview(pathname);
  return <Routes />;
}

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

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 <Pricing content={page} />;
}

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 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.