8. App start and screens
8.1 Spans
Section titled “8.1 Spans”| Span | Attributes | Starts | Ends |
|---|---|---|---|
app.start |
app.start.type = cold, warm or hot; app.start.prewarmed on iOS |
Process start (cold), or the start of the relaunch (warm, hot) | The first frame of the first screen is drawn |
app.screen.load |
app.screen.name; app.screen.load.ttfd |
The screen is created | TTID: its first frame is drawn |
app.screen.loadmeasures a screen being created and drawn: it starts only when the screen is created (an activity’sonCreate, a view controller loading its view). A screen shown again without being created (back navigation, the return from the background, a dialog closed over it) starts none; 8.6 records those shows.cold: the process starts.warm: the process exists but the activity or scene is recreated.hot: the app is brought back with its UI intact (Android).- iOS prewarming. iOS can start a process before the user opens the app. A prewarmed
launch MUST set
app.start.prewarmed = trueand MUST measure from the moment the app is activated, not from process start; otherwise prewarmed launches show minutes of start time.
8.2 TTID and TTFD
Section titled “8.2 TTID and TTFD”- TTID (time to initial display) is measured automatically: the span ends at the screen’s first drawn frame.
- TTFD (time to full display) is reported by the app when the screen shows its real
content:
reportFullyDrawn()on Android, the SDK call on every platform (section 13). The SDK MUST record it on the screen’s span asapp.screen.load.ttfd, in milliseconds from the span’s start. - When the app never reports TTFD for a screen, the attribute is absent; a backend MUST NOT substitute TTID.
8.3 Phases
Section titled “8.3 Phases”- MAY record phase timings as attributes, in milliseconds from the span’s start, under
apsio.start.phase.<name>forapp.start(for examplepre_main,main,first_frame) andapsio.screen.phase.<name>forapp.screen.load.
8.4 Network spans
Section titled “8.4 Network spans”-
HTTP client spans follow the OpenTelemetry HTTP conventions.
-
An SDK MUST NOT add a
traceparentheader to requests unless the host is in a list of first-party hosts the app configures. An unexpected header can break a third-party API, and it leaks the trace id. -
MUST NOT record request or response bodies, or query strings, unless the app turns that on (section 11).
-
MAY set
url.template, the low-cardinality template of the request’s path (for example/orders/{id}), when the app or its HTTP library knows it. It MUST NOT contain values that identify a user or a record. A backend ignores anything from a?or#in it. -
A backend that groups requests by endpoint uses
url.templatewhen it is present. When it is absent, the backend derives one from the path ofurl.full, testing each segment after percent-decoding it and replacing it with{id}when it is:- all digits, a UUID, or hexadecimal of 8 or more characters with at least one digit;
- 5 or more digits anywhere, or digits with only
+,-,(,),.and spaces (a phone number); - 10 or more characters of base64 or base64url that mix upper case, lower case and digits (a token);
- 20 or more characters with at least one digit, or any segment of 32 or more characters;
- any segment containing
@or whitespace (an email address, free text).
The derived template is the backend’s grouping key only; the record keeps the attributes it was sent with.
8.5 Frames
Section titled “8.5 Frames”Slow and frozen frames tell how smooth each screen is. They are counted on the device and sent as delta sums per screen visit, never one record per frame.
- A frame is one frame the platform reports for the current screen’s window while the
app is in the foreground, with its duration and its budget:
- on iOS, the interval from one
CADisplayLinkcallback to the next, on the display of the key window; its budget is the previous callback’stargetTimestampminus itstimestamp; - on Android, each frame
FrameMetricsreports for the resumed activity’s window; its duration isFrameMetrics.TOTAL_DURATION, and its budgetFrameMetrics.DEADLINEfrom API 31, before that1e9 / Display.getRefreshRate()nanoseconds, read for that frame (the rate is updated when the display changes mode).
- on iOS, the interval from one
- The budget follows the display’s current refresh rate frame by frame (ProMotion, Android displays with several modes): 16.7 ms at 60 Hz, 8.3 ms at 120 Hz.
- A frame is frozen when it lasts more than 700 ms, and slow when it lasts more than its budget and is not frozen. An SDK MAY add a tolerance of up to half a budget to absorb clock jitter. Slow and frozen are disjoint; both count in the total.
- MUST NOT count a window’s first frame (Android’s
FIRST_DRAW_FRAME): the screen’s load (8.1) measures it. iOS reports no such frame, so it needs no rule. - MUST NOT count frames the platform does not report, nor frames of other windows (a dialog’s own window on Android, a window on another display on iOS).
- The callback that receives frames SHOULD be cheap and run off the UI thread where the
platform allows it (Android’s
addOnFrameMetricsAvailableListenertakes a handler on another thread;CADisplayLinkruns on the main thread, so its callback only compares two timestamps and adds to counters). - The SDK SHOULD send three metrics, each a
Sum, monotonic, delta temporality, unit{frame}:apsio.frames.total,apsio.frames.slowandapsio.frames.frozen. - One point of each covers a stretch of one screen visit in the foreground. It starts when
the screen is shown (or the app returns to the foreground on it) and ends when another
screen is shown, the app leaves the foreground or the session ends. Its points carry
app.screen.name(absent before the first screen) andsession.id. A stretch with no frames sends nothing. - MUST stop counting while the app is in the background, and MUST NOT count the interval spent there as a frame.
- MAY stop counting while the screen is idle, to save the cost of a frame callback on every refresh: for example 2 seconds after the last touch, screen change or keyboard event, extended while a scroll is still moving, and resuming on the next. The interval while paused is not a frame, and frames in it are not counted. The frame that ends when the pause begins (the last one the platform reported, a hang’s included) is counted, so a hang that outlasts the idle window is still a frozen frame; nothing runs from that moment to the first frame after the pause ends, so a resume is never a frozen frame. A pause can miss slow frames of an animation nobody touched.
- A frame the app spends blocked is one long frame. A hang (6.3) of more than 700 ms is also a frozen frame only when the screen draws a frame at its end: one that ends in a crash, a kill or the background is not.
- Idle frames. On iOS the display link fires on every refresh of the display, also
while nothing on screen changes, and nothing public tells a refresh that drew from one
that did not; Android reports only frames that were drawn. An idle refresh is never slow
or frozen, so slow and frozen counts per minute of a screen in the foreground compare
across platforms, while
apsio.frames.total, and the share of slow frames in it, compare only within one platform.
8.6 Screen views
Section titled “8.6 Screen views”Journeys and funnels need every time a screen is shown, including a screen shown again by
back navigation, which starts no app.screen.load: load times stay the times of screens
that were created.
- The SDK SHOULD send a log record with event name
app.screen.vieweach time a screen is shown, withapp.screen.name, andapp.screen.previous_name, the screen viewed before it in the same session (absent for the session’s first view). Its time is the moment the screen is shown: on Android an activity’s resume or the app’strackScreen, on iOSviewDidAppearor SwiftUI’s.apsioScreenappearing, in a cross-platform framework its navigation focusing a route. It has no body, and severityINFO. - A screen shown again without another screen in between is the same view, and sends nothing: the app back from the background, or a system dialog, permission prompt or share sheet closed over it. A screen shown again after another one (back navigation), or as another instance (a second product page over the first), is a new view. An instance is the shown object itself, told by a token the SDK stores on it (an activity, a view controller, a SwiftUI view’s identity), never by its address, which a later object can reuse.
- An overlay counts as a screen when it is a tracked screen itself: a sheet or a dialog-style activity the SDK tracks as a screen. An alert, an action or share sheet, a system dialog and a permission prompt are not screens: they send no view, and closing them shows nothing again.
- When an overlay that counts as a screen closes, the screen under it is shown again: a
new view when it is the screen in front. Where the platform does not call the screen
under it again (iOS page and form sheets, SwiftUI’s
.sheet), the SDK MUST send that view itself when the overlay goes away. - A session’s first view is sent even when the screen did not change: a session that starts on the return from the background (after its timeout, 3.2) starts with the screen in front. Where the platform shows nothing again on that return (iOS), the SDK MUST send it itself, for the screen it knows is in front.
- It is a record of its session: it carries
session.idand follows the session’s sampling (section 9).capture.logs_min_severitydoes not apply to it: it is an event of the SDK, not the app’s logging. - One record per view, nothing measured: no span, no timer.
screen.views(appendix B.2) states which shows send one.