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

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.