Skip to content

App usage

API

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.

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.sendBeacon works.
  • 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.

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_time reports, 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.