Concepts
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 and Feedback 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 and the CLI.
Turn it on
Analytics is a product, so a project declares that it runs one:
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.
What Myna collects, and what it refuses to
The same boundary Feedback draws. 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, patchconsole, or watch for clicks. An event exists because your code calledtrack(), 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-Agentstring is stored. Myna parses it to a browser family, a platform family, and one of three device classes,desktop,mobile, ortablet. 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, the same myna_ik_… credential Feedback uses, bound to the same registered origins.
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 for the whole API, and @myna-sh/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:
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 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:
// 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:
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/contentreturns 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}/impactreturns 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.
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:<key>.
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
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:
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: 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:
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:
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:
{ "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: 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 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.
