# Analytics > Page views, custom events, sessions, and funnels, recorded against the content that was live and the reports that came in. Myna Analytics records what people did in your product. That alone is unremarkable. Beside [Content](/concepts/collections) and [Feedback](/concepts/reports) on the same project, it answers a question a standalone analytics tool cannot: > What happened after we changed the pricing page last week? Content knows what you shipped and when. Analytics knows what people did. Feedback knows what they said. All three sit on the same project, read with the same credentials, and reach your coding agent through the [MCP server](/mcp/setup) and the [CLI](/cli/overview). ## Turn it on Analytics is a [product](/concepts/products), so a project declares that it runs one: ```bash myna api PUT /projects/$PROJECT/products --data '{"product":"analytics","enabled":true}' ``` Or enable it from **Settings → Products** in the dashboard. Until then, every analytics route answers [`PRODUCT_NOT_ENABLED`](/errors/product-not-enabled). ## What Myna collects, and what it refuses to The same boundary [Feedback draws](/concepts/collecting-feedback#reports-are-submitted-never-harvested). A measurement product is under more pressure to cross it, so it is worth writing down twice. - **No session replay, no heatmaps, no autocapture.** Myna does not read the DOM, wrap `fetch`, patch `console`, or watch for clicks. An event exists because your code called `track()`, or because you asked for page views. - **No IP address is stored.** The address is read to rate-limit a burst and never written down. That is also why there is no geolocation: Myna cannot locate an address it does not keep. - **No raw `User-Agent` string is stored.** Myna parses it to a browser family, a platform family, and one of three device classes, `desktop`, `mobile`, or `tablet`. The string itself is discarded. - **No cookie is set by Myna.** The client generates the visitor id and keeps it in your own first-party storage, which is yours to govern. Set `storage: "memory"` and nothing is written to the device at all. - **Query strings are discarded.** A URL is reduced to its path before it is stored. Session tokens, password-reset links, and invitation codes all travel in query strings. UTM parameters are read out first, and the rest is dropped. ## Recording events Install the SDK and use a [publishable ingest key](/concepts/collecting-feedback#ingest-keys), the same `myna_ik_…` credential Feedback uses, bound to the same registered origins. ```ts import { createAnalytics } from "@myna-sh/sdk/analytics"; const analytics = createAnalytics({ ingestKey: "myna_ik_...", environment: "production", appVersion: process.env.COMMIT_SHA, autoPageviews: true, }); analytics.track("project_created", { template: "nextjs" }); ``` The client buffers events and flushes them on a timer, and again when the page is hidden. See the [analytics client](/sdk/analytics) for the whole API, and [`@myna-sh/react/analytics`](/sdk/react-analytics) for the React bindings. `$pageview` is the only event Myna defines. The `$` prefix is reserved so an event you ship today cannot collide with one Myna adds tomorrow. Every other name is yours. ### Custom properties An event may carry up to 32 scalar properties: ```ts analytics.track("checkout_started", { plan: "pro", seats: 12, trial: false }); ``` Nested objects and arrays are dropped rather than stored. A property is something a breakdown groups by, and a group key that is a JSON blob is a group of one. The [catalog](#the-event-catalog) remembers every property key an event has carried, so you can break down by it later without having known in advance that you would want to. ## Identity Exactly Feedback's rule, for exactly Feedback's reason. An `anonymousId` only stitches events together. The client made it up and can make up another, so nothing that matters is decided by it. A visitor becomes a **person** only through an identity your own backend signed: ```ts // On your server import { signIdentity } from "@myna-sh/sdk/feedback/server"; const signature = signIdentity(process.env.MYNA_IDENTITY_SECRET, user.id); // In the browser analytics.identify({ id: user.id, email: user.email, signature }); ``` Without a valid signature the visitor stays anonymous. An unsigned claim would let any visitor file their behaviour against a stranger's profile, and poison whatever your team segments on. The signature is the same HMAC a report submission carries, verified by the same code, so a person who files a bug and a person who abandons a checkout are the same row. Call `analytics.identify(null)` on sign-out. Without it, the next person on a shared machine inherits the last one's identity. ## Content-aware analytics This is the part that only works because Myna served the content. When your page renders a Myna entry, tell the analytics client which one: ```tsx const { data: page } = useEntry("pages", "pricing"); useContentContext({ entry: page.id, collection: "pages", revision: page.revisionId }); ``` Every event from then on carries the entry, the revision the visitor actually saw, and the release it shipped in. You write one line. Myna already knew the rest. That makes three things queryable that otherwise need a data pipeline: - **`GET /v1/projects/{project}/analytics/content`** returns traffic and complaints per entry. An entry with heavy traffic and a high report count is where a copy change pays. - **`GET /v1/projects/{project}/analytics/content/{entry}`** returns one entry, every time it was published in the window, and the metric movement either side of each publication. - **`GET /v1/projects/{project}/analytics/releases/{release}/impact`** returns a release, the entries it touched, the movement on each, the movement per event, and the reports that arrived afterwards. Release impact reports **correlation**, and says so. A release is not the only thing that happened that day. ```bash myna analytics impact 48 --window 7 ``` ``` Release #48 — Pricing clarity — published 3 days ago Comparing 3 day(s) either side. This is correlation: a release is not the only thing that happened. METRIC BEFORE AFTER CHANGE Visitors 4,102 4,061 -1% Page views 11,904 11,455 -3.77% Per event EVENT BEFORE AFTER CHANGE cta_clicked 842 771 -8.43% signup_started 410 389 -5.12% Reports: 2 before, 9 after. #212 Can't tell whether Pro includes overages (triage) ``` `entry_id` on an event is text supplied by a browser, so every join is scoped to the event's own project. A forged or cross-tenant id resolves to nothing rather than to somebody else's entry. An entry deleted since still appears, marked `exists: false`, because analytics is a record of what happened. ## Sessions A session is a visit: the events one visitor produced without going idle for 30 minutes. Myna derives it, never the client. A browser that decided its own session boundaries would report numbers you cannot reproduce and an attacker can inflate. Sessions carry attribution taken from their first event and never overwritten: referrer, campaign, browser, platform, device class, locale, environment, and app version. A visit came from wherever it came from, and a later page carrying a `utm_source` does not get to relabel it. ## Asking questions Every read takes the same filters and echoes back the window it resolved, so you can reproduce a number, cite it, and compare it against one taken an hour later. Name **either** `period` (`24h`, `7d`, `30d`, `12w`) **or** `from`/`to`. Naming both is an error rather than a precedence rule. A caller who believes they asked for a month and was answered for a day has a wrong answer that looks exactly like a right one. | Route | Answers | |---|---| | `analytics/overview` | Headline numbers, the previous period, and the change | | `analytics/series` | All four metrics over time | | `analytics/breakdown` | Group by a dimension or a custom property | | `analytics/events` | The event catalog, with what each event means | | `analytics/funnel` | Ordered, time-bounded conversion steps | | `analytics/friction` | Exits, bounces, and pages that generate reports | | `analytics/content` | Traffic and complaints per content entry | | `analytics/releases/{n}/impact` | What measurably happened after a release | | `analytics/profiles` | People your backend vouched for | | `analytics/sessions` | Visits, event by event | ### Metrics | Metric | Means | |---|---| | `visitors` | Distinct people. A signed identity counts once across devices; everyone else counts once per anonymous id. | | `sessions` | Visits, cut wherever a visitor was idle for 30 minutes. | | `pageviews` | `$pageview` events. | | `events` | Every event, including page views. | ### Dimensions `event`, `path`, `referrerDomain`, `utmSource`, `utmMedium`, `utmCampaign`, `browser`, `os`, `deviceType`, `locale`, `environment`, `appVersion`, `entry`, `collection`, `release`. Any custom property works too, addressed as `property:`. ```bash myna analytics breakdown property:plan --event checkout_started --period 30d ``` The last three name things that live in the CMS on the other side of the same project, which is why a general-purpose analytics product cannot offer them. ### Funnels ```bash myna analytics funnel pricing_viewed signup_started signup_completed --within 24 ``` Order is enforced: somebody who saw the confirmation page before starting a checkout has not converted. `--within` is measured from the visitor's **first** step, so a signup in March is never counted as the conversion of a checkout abandoned in January. ### Friction `analytics/friction` returns three signals and no fourth: - pages visits most often end on, with their exit rates; - pages people arrive at and leave without going anywhere else; - **pages that generate reports out of proportion to their traffic**, measured as reports per thousand sessions on the page they were filed from. There are no rage clicks or dead clicks here, and there will not be. Measuring either means reading the DOM. The third signal is the join Feedback makes possible. Feedback alone tells you twelve people complained about onboarding. Analytics alone tells you 38% abandon step two. Together they tell you which. ## The event catalog Every event name your project has sent registers itself the first time one arrives. Nobody declares a schema before they can measure anything. The catalog also stores a **description**: ```bash myna analytics describe checkout_started \ "Fires when the Start checkout button is clicked, before the plan is chosen. \ The 'plan' property is the plan the visitor was looking at, not the one they bought." ``` Nothing in the data says whether `checkout_started` fires on the click or on the page, and whoever reads it next has to decide. It is the same idea as a collection's [`guidance`](/schema/defining-schemas): what somebody worked out about the data, stored beside it. `myna_analytics_events` returns descriptions, so an agent reads the meaning before it writes the funnel. ## People `analytics/profiles` lists everybody your backend vouched for, and nobody else. An anonymous visitor has sessions and events and no profile. Myna does not know who they are, so it does not offer a page claiming to. A profile is addressable by its Myna id (`apr_…`) or by **your own user id**, so an agent holding a user id from the application it is fixing does not have to look one up first: ```bash myna analytics person user_8812 ``` The response carries their recent sessions, their recent events, and the reports they filed. Both products keyed them on the same signed id, so putting the two together is a fact rather than a guess. The events leading up to a report are usually the reproduction steps the reporter did not write down. ## Retention and exclusions Your plan sets a ceiling. A project may set anything shorter: ```bash myna analytics settings --retention 30 myna analytics settings --exclude '/health' '/admin/**' ``` Myna refuses excluded paths at ingest rather than filtering them at query time, so a health check or an admin area is never stored and never billed. `*` matches inside a path segment, `**` across them. Shortening retention deletes events at the next sweep. There is no undo. | | Free | Pro | |---|---|---| | Events per month | 100,000 | 5,000,000 | | Retention | 90 days | 365 days | Past the allowance ingest keeps answering and stops recording; it does not queue, and it does not bill you for the overage. The response reports what it dropped and why: ```json { "accepted": 0, "dropped": 12, "sessionId": "ase_...", "reasons": [{ "reason": "quota_exceeded", "count": 12 }] } ``` An analytics event is worthless individually and expensive in aggregate. A bug report is the opposite, which is why one over quota is [still accepted](/concepts/collecting-feedback#what-happens-over-the-cap): a bug filed during an incident is the most valuable thing that will arrive all week. ## For agents The MCP server exposes ten analytics tools on a project that runs the product. They are shaped so one call answers a whole question. | Tool | Answers | |---|---| | `myna_analytics_overview` | Start here. Is anything different? | | `myna_analytics_events` | What this project measures, and what each event means | | `myna_analytics_breakdown` | Group by any dimension or property | | `myna_analytics_funnel` | Ordered conversion | | `myna_analytics_friction` | Where people stop, including complaint hotspots | | `myna_analytics_content` | Traffic and complaints per entry | | `myna_analytics_entry` | One entry, its publications, and their effect | | `myna_analytics_release_impact` | What happened after a release | | `myna_analytics_person` | One user's behaviour beside their reports | | `myna_describe_analytics_event` | Write down what an event means | Myna collects the numbers. Your agent reads them, and can use the Content tools to open a [change set](/concepts/change-sets) fixing what it found, which still goes through validation, preview, and your publish gates. Myna does not ship an agent of its own. Yours is the agent.