Skip to content

8. App start and screens

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.load measures a screen being created and drawn: it starts only when the screen is created (an activity’s onCreate, 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 = true and MUST measure from the moment the app is activated, not from process start; otherwise prewarmed launches show minutes of start time.
  • 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 as app.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.
  • MAY record phase timings as attributes, in milliseconds from the span’s start, under apsio.start.phase.<name> for app.start (for example pre_main, main, first_frame) and apsio.screen.phase.<name> for app.screen.load.
  • HTTP client spans follow the OpenTelemetry HTTP conventions.

  • An SDK MUST NOT add a traceparent header 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.template when it is present. When it is absent, the backend derives one from the path of url.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.

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 CADisplayLink callback to the next, on the display of the key window; its budget is the previous callback’s targetTimestamp minus its timestamp;
    • on Android, each frame FrameMetrics reports for the resumed activity’s window; its duration is FrameMetrics.TOTAL_DURATION, and its budget FrameMetrics.DEADLINE from API 31, before that 1e9 / Display.getRefreshRate() nanoseconds, read for that frame (the rate is updated when the display changes mode).
  • 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 addOnFrameMetricsAvailableListener takes a handler on another thread; CADisplayLink runs 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.slow and apsio.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) and session.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.

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.view each time a screen is shown, with app.screen.name, and app.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’s trackScreen, on iOS viewDidAppear or SwiftUI’s .apsioScreen appearing, in a cross-platform framework its navigation focusing a route. It has no body, and severity INFO.
  • 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.id and follows the session’s sampling (section 9). capture.logs_min_severity does 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.