App usage
An App’s own pages and phone apps can count how they’re used, and you see the totals in Flow on the App’s page under Measure › Insights. Nothing names a person:
- A device is told apart by a random id the App makes on the device.
- A visit is told apart by a random id the App makes when it opens.
- The address and the browser are never kept.
Send events
Section titled “Send events”The App posts batches of events to its own endpoint, from one of its own sites or its phone apps’ web views:
POST https://api.work.studio/api/v1/public/apps/{appGlobalId}/events{ "id": "<device id>", "app": "web", "events": [ { "e": "design_made", "t": 1760000000000, "s": "<visit id>", "p": { "mode": "words", "proposals": 3 } } ] }| Field | What it is |
|---|---|
id |
A random id kept on the device |
app |
web, pwa, ios or android |
e |
The event name, in lower case with underscores |
t |
When it happened, in epoch milliseconds. Optional: left out, it’s now |
s |
The visit id |
p |
A few flat properties: short labels, numbers, or yes and no |
- Send the body as
text/plain, so the browser sends no CORS preflight.navigator.sendBeaconworks. - A batch holds at most 50 events and 32 KB. The endpoint answers 204.
- It takes requests only from the App’s own origins.
- Crawlers and headless browsers aren’t counted: the endpoint answers them and stores nothing.
Declare the events your App sends in its manifest, under usage.events in workstudio.app.yaml. Once you declare any, only those are kept, apart from the platform’s own events below, which every App may send.
Time in app
Section titled “Time in app”active_time is a platform event. Every App may send it without declaring it in usage.events, and the platform fixes its shape. Insights turns it into Time in app: the average and median visit, the total time, the number of visits, the minutes per device per day, and a daily trend.
| Field | Value |
|---|---|
e |
active_time |
s |
The visit id, the same one the App sends with its other events. Required: a report without one is dropped |
p.value |
The active seconds since this page last sent active_time, a whole number from 1 to 3600. Required |
p.kind |
Optional: web, ios or android |
{ "e": "active_time", "s": "<visit id>", "p": { "value": 42 } }- When to send it: when the page is hidden or closed, and about every 5 minutes while it’s visible.
- What counts as active: only time while the page is visible and the person has done something (a key, a click, a touch or a scroll) in the last 60 seconds. Idle time and time in a background tab don’t count.
- What the server does with it: a value over 3600 counts as 3600, and a fraction is rounded to whole seconds (at least 1). A report with no value, a value that isn’t a number, zero or less, or no visit id is dropped.
- How it’s added up: a visit is one device’s visit id. Its time is the sum of its
active_timereports, and it belongs to the UTC day of its first report. The average and the median are per visit; with an even number of visits, the median is the mean of the middle two.
A sender, in a few lines:
let active = 0, lastInput = Date.now(), lastTick = Date.now();const touch = () => { lastInput = Date.now(); };['pointerdown', 'keydown', 'scroll', 'touchstart'].forEach(e => addEventListener(e, touch, { passive: true }));setInterval(() => { const now = Date.now(); if (document.visibilityState === 'visible' && now - lastInput < 60_000) active += now - lastTick; lastTick = now;}, 1000);function report() { const seconds = Math.min(3600, Math.round(active / 1000)); if (seconds >= 1) { track('active_time', { value: seconds }); active = 0; }}setInterval(report, 5 * 60_000);document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') report(); });addEventListener('pagehide', report);track stands for your App’s own usage code: it queues the event with the visit id and sends the batch. Send the batch right after report() when the page is hidden, so the last minutes aren’t lost. Apps made with the App Kit have this built in, as trackActiveTime() in src/usage.ts.