Docs Running a business
Analytics
Analytics in the sidebar shows each business you own: revenue, signups, visits, clicks and conversion over 7, 30 or 90 days, then a product panel for each business with what happens inside its own app.
Numbers from the hosted page
The studio counts these itself, with no scripts:
- Visits: views of the hosted page at
/p/<slug>, one per visitor per 30 minutes, bots skipped. - Clicks: links that go through
/p/<slug>/go. - Signups: people who join the waitlist.
- Revenue: from Stripe, or recorded by hand.
Product events from your app
To see what people do inside the app itself, send events.
- On Analytics, open Set up events under the business and press Make a write key. Copy it right away. It isn't shown again, because only a hash of it is kept. New write key replaces it, and the old one stops working.
- Add the snippet to your app's pages: ``
html <script src="https://<your hub>/e/<slug>/snippet.js" data-key="ek_..." defer></script>`It sends avisiton each page load. Send your own events withsteward.track("trial_started", { plan: "pro" }). After sign-in, callsteward.identify(hash)` with a hash of the user's id (for example SHA-256 in hex), so one person counts once across devices. Never pass an email or a raw id; the hub refuses anything that isn't a hash. - Browsers may only send from the business's own sites: the site set on Releases, the health check URL on Integrations, and any sites you add under More sites. Requests from anywhere else are refused.
From a server (Bun or Node 18+), send the key in a header:
await fetch("https://<your hub>/e/<slug>", {
method: "POST",
headers: { "content-type": "application/json", "x-steward-key": process.env.STEWARD_WRITE_KEY },
body: JSON.stringify({ name: "paid", anonymousId: user.anonymousId, userId: sha256(user.id), properties: { plan: "pro" },
context: { userAgent: req.headers.get("user-agent") } }),
});
Pass the visitor's user agent in context.userAgent if you have it, so bots are filtered for server events too.
The event
| Field | Rule |
|---|---|
name | Letters, numbers, _ . : -, up to 60. Lowercased. |
anonymousId | 8 to 64 letters, numbers, _ or -. The snippet makes one per browser. |
userId | Optional. A hex hash (16 to 128 characters) of your user's id. |
properties | Optional. Up to 20 keys; strings (cut at 200 characters), numbers or true/false; 1 KB in all. A path loses its query string. A ref is kept for attribution. |
The whole request is at most 4 KB. The answer is 204 when it's accepted (or quietly dropped), and 400, 401, 403, 413 or 429 with a short reason when it isn't.
Privacy defaults
- No cookies. The snippet keeps its anonymous id and first
refin the browser's local storage. Adddata-storage="memory"to keep nothing at all (each page load is then a new visitor). - Do Not Track and Global Privacy Control are honored: the snippet sends nothing, and the hub drops browser requests that carry either signal.
- No IP addresses are stored. An address is hashed with a salt that changes every day and is deleted after a day, so hashes can't be tied to an address or followed from one day to the next. Anonymous ids are stored as a keyed hash.
- Retention. Raw events are kept 90 days (
EVENTS_RETENTION_DAYS, 90 at least). The daily totals stay. - Limits. 120 events a minute per visitor (
EVENTS_PER_MINUTE) and 20,000 a day per business (EVENTS_PER_DAY). Bots are skipped.
The write key sits in your pages, so anyone can read it, like any analytics key. The site check, the limits and the size caps are what keep it honest.
What you see
- Active today, Weekly active and people in the window. A person is a user id hash when there is one, else an anonymous id.
- Goals: events you mark as goals under Goals, counted per day.
- Top events in the window.
- The funnel: visited, signed up, activated, paid. Visits and signups add the hosted page's visits and waitlist signups to the app's
visitandsignupevents. Activated and paid count people who sent the activation and paid events (activatedandpaidunless you name others). The hosted page counts visits, not people, so the first two steps are close, not exact. - Ad spend and clicks, when the business runs ads (see Paid ads).
- Cross-promotions that involve the business, with their results.
Every 10 minutes the day is rolled up into the numbers store as active_users, weekly_active_users and goal_<event>. @steward reads them in the heartbeat and the brief, and goals in the charter can name them.
Cross-promotion
When two of the studio's businesses serve the same kind of customer, @steward can propose that one mentions the other: a short block on the hosted page, or a line in its waitlist emails. It finds pairs with crosspromo_candidates (shared words in the charters' customer fields, plus tactics both have outcomes for) and proposes one with crosspromo_propose.
- Only between businesses in this studio, and only ones everyone in the room can see.
- Every owner of both businesses must approve the one card. If the owners aren't the same people, the card goes to a direct message with all of them. Until everyone picks Approve, nothing appears. Owners can't change after the card without asking again: the hub checks the current owners when it starts.
- Any owner of either business can end it, or decline it, on Analytics.
- Each promotion has its own ref code (like
xp-dogwalk-3fa2c1) on the link. The partner's page counts visits by that source, its waitlist form carries the code so signups count, and the app snippet keeps it asproperties.ref. When it ends, the result is recorded in the outcomes network (tactic "cross promotion").