ScopeStack

The ambient scope + screen context of the currently visible UI, as a stack of pushed frames.

This is the non-Compose home for what AutographScope and screen context express inside Compose via CompositionLocal: a place that autocapture pipelines can read at the moment a tap or screen view is observed, regardless of which UI framework produced it (Compose, UIKit/SwiftUI, or the Android View system). Each surface pushes a frame when a screen or scope comes into view and removes it when it leaves; the capture pipeline calls current and AmbientContext.enrich to attribute the event.

Read this only from auto-capture code, not from explicit track calls. The stack answers "what was on screen when the user acted", which is the right question for an autocaptured tap but the wrong one for an arbitrary track(...) — a background event (e.g. a network response that completes after navigation) must not silently inherit whatever screen happens to be visible. Explicit instrumentation keeps its own lexical scope; see the autograph-compose decorator.

Own an instance; don't share one globally. Scope a ScopeStack to the tracker it feeds so context can't leak across trackers or outlive a tracker swap on logout — screenHistory rides along and inherits that scoping, which is what resets previous_screen when the tracker is replaced. Sharing one instance across the surfaces of a single hybrid app is the exception, and the point: pass it to AutographProvider so the Compose and native pipelines attribute against the same context and share one previous_screen chain. That stack is then yours to replace when the tracker is — the provider will not swap a caller-supplied stack out from under the native side.

Threading. push, update, remove, maskScreen, setActive and the origin-taking current must be called from the main thread (push and remove mutate the frame list; the others mutate a frame's contents and republish the snapshot, or read the list as it stands). The no-argument current is lock-free and safe from any thread: it returns an immutable snapshot that is republished atomically on every mutation, so a background reader always sees a whole, consistent context — never a half-applied one.

Constructors

Link copied to clipboard
constructor()

Properties

Link copied to clipboard

The screen history that travels with this stack.

Functions

Link copied to clipboard

The current merged ambient context, over every frame on the stack. Lock-free; safe from any thread.

The ambient context as seen from origin — the innermost frame the calling pipeline could attribute an event to. Main thread only, unlike the no-argument current: this reads the frame list rather than a published snapshot, because the answer depends on the origin and is computed per event.

Link copied to clipboard

Records name as the current screen and emits a Screen Viewed for it, carrying the screen it replaced as previous_screen. The one place that couples recording with emitting, so every native screen-capture caller — the iOS UIKit swizzle, the iOS explicit SwiftUI path, and the Android Activity/Fragment lifecycle capture — cannot drift on the order or on the self-previous guard.

Link copied to clipboard
fun maskScreen(handle: ScopeHandle)

Turns the frame handle refers to into a mask: a frame that declares there is no screen here, clearing both screen and section rather than naming one. Frames after it still win, so a mask hides what is underneath it, not everything.

Link copied to clipboard
fun push(scope: Map<String, JsonElement> = EmptyJsonObject, screen: String? = null, section: String? = null, parent: ScopeHandle? = null): ScopeHandle

Pushes a frame contributing scope properties (low precedence — an explicit call-site property always wins over them) and/or a screen/section (reserved keys, high precedence). Returns a ScopeHandle to update or remove it with. A frame may carry scope only (an AutographScope analogue), screen only (a TrackedScreen analogue), or both.

fun push(scope: Map<String, JsonElement> = EmptyJsonObject, screen: String? = null, section: String? = null, parent: ScopeHandle? = null, boundary: Boolean): ScopeHandle

push for a frame that is also an attribution boundary: the pipeline pushing it can tell, for every event it captures, whether the event happened inside this frame, and resolves such events through current with the frame — or one beneath it — as the event's origin.

Link copied to clipboard
fun remove(handle: ScopeHandle)

Removes the frame handle refers to, by identity and independent of position — screen transitions (a Compose Crossfade, an iOS interactive-pop that the user cancels) do not guarantee frames leave in push order, so a positional pop would remove the wrong one. Removing a handle that was already removed, or one from another stack, is a no-op.

Link copied to clipboard
fun setActive(handle: ScopeHandle, active: Boolean)

Marks the frame handle refers to as taking part in resolution, or not. An inactive frame keeps its position and its contents but contributes nothing — no screen, no section, no scope — as if it were not on the stack at all. The frames nested under it drop out with it — ambiently always, from an origin unless the frame belongs to the origin's own surface; see recompute and the origin-taking current for the split.

fun setActive(handles: List<ScopeHandle>, active: Boolean)

setActive for several frames at once, publishing one snapshot for the whole batch.

Link copied to clipboard
fun update(handle: ScopeHandle, scope: Map<String, JsonElement> = EmptyJsonObject, screen: String? = null, section: String? = null, parent: ScopeHandle? = null)

Replaces the contents of the frame handle refers to, in place — keeping its position, and thus its precedence, in the stack. Use this instead of remove + push when a still-mounted frame's scope/screen/section/parent changes: re-pushing would move the frame to the top and let it wrongly override inner frames that are still on the stack. A no-op (and no snapshot churn) if the contents are unchanged, the handle was already removed, or it belongs to another stack.