Skip to main content

tui-test for Python

Control, inspect, and test terminal apps from Python.

Install

pip install tui-test

Python 3.8+ is supported.

The wheel contains an ordinary shared library. Python uses generated ctypes declarations for its typed C ABI. The package has no Python native extension.

Set TUI_TEST_NATIVE_LIBRARY to an absolute library path to override the bundled library. The loader checks the ABI version before the first operation.

Build from source

Install Rust 1.90 or later and the build dependencies for the supported terminal backends. Build a wheel with the standard Python build frontend:

python -m pip install build
env -u CARGO_TARGET_DIR python -m build bindings/python

The build uses setuptools-rust with Binding.NoBinding. Wheels use py3-none-<platform> tags. The source distribution includes the shared ABI, the Rust engine, their assets, and the Cargo configuration that separates backend symbols. Source builds need Cargo dependency access.

For cross builds, set CARGO_BUILD_TARGET and pass -C--build-option=--plat-name=<platform> to python -m build --wheel. Build Linux release wheels in the required manylinux or musllinux container. Use auditwheel repair before publication. Setuptools-rust disables static libc linkage for musl shared libraries.

For a checkout, use the shared library directly:

env -u CARGO_TARGET_DIR cargo build -p tui-test-ffi
PYTHONPATH=bindings/python/src \
TUI_TEST_NATIVE_LIBRARY="$PWD/target/debug/libtui_test_ffi.dylib" \
python bindings/python/examples/shared_library.py

Use libtui_test_ffi.so on Linux or tui_test_ffi.dll on Windows. The example proves that a copied screen remains valid after the session closes.

Each asynchronous call uses a separate worker thread. A blocked wait cannot prevent close() from interrupting waits. Cancellation releases each native result when its call returns.

Cancellation stops the Python await, not the native operation. A canceled open(), run(), or restart() can still complete. The adapter does not close the session automatically after cancellation. This preserves any replacement session with the same name. The caller remains responsible for explicit session cleanup; cancellation does not confirm that a spawn stopped.

Quick start

from tui_test import TuiTest

async with TuiTest.ephemeral() as terminal:
    await terminal.run("my-app")
    await terminal.get_by_text("Ready").expect()
    await terminal.get_by_text("Continue").click()
    await terminal.get_by_text("Done").expect()

API

TuiTest

TuiTest(session=None, *, backend=None, timeouts=None, profile=None, screen_history_limit=None, artifacts=None, recording=None)
Option Type Default
session str TUI_TEST_SESSION or "default"
backend "alacritty" | "ghostty" | "rio" | "xtermjs" "alacritty"
timeouts Timeouts | dict built-in defaults
profile Profile | dict built-in profile
screen_history_limit int | None core default
artifacts dict off
recording AutomaticRecording | dict (directory only) default recording directory
trace TraceOptions | dict {"mode": "off", "directory": ".tui-test/traces"}

Set trace.mode to "on" for every session or "on-failure" for failures. Users can open trace.html; agents should read trace.md, trace.json, and timeline.json.

artifacts["on_failure"] selects "none", "text", "html", or "all". Use include_recording to include the cast in failure artifacts.

Properties

Property Type
session str
keyboard keyboard helper
mouse mouse helper

Lifecycle

Method Description
TuiTest.ephemeral(prefix=None, **options) Create a unique session.
await open(**options) Open a shell.
await run(program, *args, **options) Run a program.
await restart(graceful_timeout=5000) Restart the session.
await close() Close the session.
await close_quiet() Close without raising.
async with TuiTest() Close on exit.

open() options are shell, backend, cols, rows, cwd, env, wait_ready, restart, retries, profile, and timeouts. run() accepts the same options except shell.

The default size is 80 by 30. Timeout defaults are 5 seconds for text and idle, and 30 seconds for command, exit, and ready.

Input

Method Description
await submit(text=None) Type text and press Enter.
await type(text) Type text.
await write(data) Write raw bytes.
await press(*keys) Alias for keyboard.press().
await resize(cols, rows) Resize the terminal.
await scroll(lines) Move through native history; positive values show older output.
await scroll_to_top() / await scroll_to_bottom() Show the oldest output or the live screen.
await get_viewport() Return a Viewport dictionary with offset and history, in rows.
await signal(name) Send INT, TERM, KILL, or QUIT.
await kill() Kill the child process.

Native history

scroll(lines) accepts integers from -2147483648 through 2147483647. Positive values show older output. Negative values move toward the live screen. Zero leaves the viewport unchanged. Movement stops at the available history boundaries. get_viewport() returns a Viewport dictionary with offset and history integer fields. Both fields count rows. An offset of zero means the live screen.

Visible text, locators, and captures use this viewport. text(full=True) remains independent: it returns history plus the live screen. New output preserves the historical view until retained history expires. Keyboard input returns to the live screen. Mouse input and historical locator clicks fail while the viewport shows history. Call scroll_to_bottom() before mouse input. This API is separate from mouse.scroll().

get_cursor() and State.cursor retain the application cursor coordinates. Captures project that cursor into the viewport and hide it outside the view. Raster recordings capture viewport changes. Asciicast remains a PTY stream.

await terminal.scroll_to_top()
print(await terminal.get_viewport())
print(await terminal.text())
print(await terminal.text(full=True))
await terminal.scroll(-2)
await terminal.scroll_to_bottom()

State

Method Returns
await state() State
await text(full=False) str
await cells(x, y, w=1, h=1) list[Cell]
await get_command() str | None
await get_output() str | None
await get_exit_code() int | None
await get_cwd() str | None
await get_cursor() Cursor
await get_size() dict
await get_title() str | None
await get_clipboard() str
await get_bell_count() int
await get_bell_events() list[BellEvent]

Waits and assertions

Method Description
await wait_title(text, regex=False, not_=False, timeout=None) Wait for a title.
await wait_clipboard(text=None, timeout=None) Wait for a clipboard change or match.
await wait_idle(timeout=None) Wait for the screen to stop changing.
await wait_command(timeout=None) Wait for a submitted command.
await wait_exit(timeout=None) Wait for the program to exit.
await wait_ready(timeout=None) Wait for a shell prompt.
await wait_bell(timeout=None) Wait for a bell.
await expect_title(text, regex=False, not_=False, timeout=None) Assert the title.
await expect_exit_code(code, timeout=None) Assert the last exit code.
await expect_output(text, regex=False) Assert command output.
await expect_bell_count(count, timeout=None) Wait until the cumulative bell count reaches count.
await expect_snapshot(name, **options) Assert or update a snapshot.

wait_clipboard() waits for the next change. A string matches text. A compiled text re.Pattern uses Python's search() semantics; bytes patterns are rejected.

Snapshot options are update, include_style, and include_title.

Capture

Method Description
await screenshot(path=None, full=False, zoom=None, background=None, transparent=False) Return text or save SVG or PNG.
await start_recording(path, **options) Start APNG, GIF, MP4, or asciinema recording.
await stop_recording() Finish the recording and return its path.

Recording options: format, fps, speed, idle_time_limit, zoom, background, and transparent. MP4 requires ffmpeg and does not support transparency.

The extension selects the format: .png or .apng, .gif, .mp4, or .cast. format overrides it.

Locator

Locators resolve against the latest terminal screen before every read or action.

from tui_test import TextStyle

save = (
    terminal
    .get_by_text("Settings")
    .get_by_text("Save", direction="after")
    .get_by_style(TextStyle(foreground="green"))
    .unique()
)

await save.click()

Create a locator

Method Options
terminal.get_by_text(text, **options) regex, full, whitespace
terminal.get_by_style(style, **options) full
terminal.get_by_link(uri, **options) full
locator.get_by_text(text, **options) regex, full, whitespace, direction
locator.get_by_style(style, **options) full, direction
locator.get_by_link(uri, **options) full, direction

whitespace is "exact" or "normalize". direction is "within", "after", or "before".

TextStyle fields are foreground, background, bold, dim, italic, underline_style, underline_color, inverse, hidden, strikethrough, and blink.

get_by_link(uri) matches an exact OSC 8 target, not visible URL text; get_by_link("") requires no link. Root style/link selectors find runs within each row. Chained calls with the default within direction check whole matches. Styles skip blanks if visible text exists; links check every cell.

Compose locators

link = terminal.get_by_link("https://example.com")
bold = terminal.get_by_style(TextStyle(bold=True))
bold_link_cells = bold.and_(link)
either = link.or_(terminal.get_by_text("Help"))
sections = terminal.get_by_text("Docs and Help")
contains_link = sections.filter(has=link)
without_old_text = sections.filter(has_not=terminal.get_by_text("old"))
entirely_linked = sections.get_by_link("https://example.com")

and_() keeps shared cells; or_() combines cells without duplicates. Adjacent cells merge within each physical row, even across original matches. Gaps and row breaks split runs. Counts and clicks use these runs; text keeps exact whitespace.

filter accepts only locators. has requires a match inside each candidate; has_not requires none. Both conditions apply when supplied, and the inner match may cover the whole candidate. For partially linked "Docs", filter(has=link) keeps the whole word, get_by_link(uri) rejects it, and and_(link) returns its linked cells.

Use locators from one TuiTest. Composition leaves them unchanged and reads one fresh snapshot when used. Selection order matters: a.first().and_(b) differs from a.and_(b).first(). Any full branch includes scrollback for the whole query. Errors propagate.

Select matches

Method Description
any() Keep all matches.
unique() Require one match.
first() Select the first match.
last() Select the last match.
nth(index) Select a zero-based match.

Read and act

Method Description
await locations() Return all selected locations.
await location() Return one location.
await count() Return the current count.
await all() Return one locator per current match.
await wait(state="visible", timeout=None) Wait for "visible" or "hidden".
await expect(not_=False, timeout=None) Assert the locator.
await click(**options) Click the middle cell.
await highlight(timeout=None) Highlight matches.

click() accepts button, alt, ctrl, shift, clicks, and timeout. button is "left", "middle", or "right".

location() and click() require one match. all() does not wait.

Keyboard

Method Description
await keyboard.press(*keys) Press keys.
await keyboard.down(*keys) Send keydown events.
await keyboard.repeat(*keys) Send repeat events.
await keyboard.up(*keys) Send keyup events.
await terminal.keyboard.press("Ctrl+C")
await terminal.keyboard.press("Escape", ":", "w", "q", "Enter")

Named keys include Up, Down, Left, Right, Home, End, PageUp, PageDown, Insert, Delete, Backspace, Tab, Enter, Space, Escape, and F1 through F12. Join modifiers such as Ctrl, Alt, Shift, Super, Meta, or Hyper with +.

Mouse

Coordinates are zero-based terminal cells.

Method Description
await mouse.click(x=None, y=None, **options) Click a cell or on_text.
await mouse.move(x, y) Move the pointer.
await mouse.down(x, y, **options) Press a button.
await mouse.up(x, y, **options) Release a button.
await mouse.drag(x1, y1, x2, y2, **options) Drag between cells.
await mouse.scroll("up" | "down", amount=3) Scroll.

Button options are button, alt, ctrl, and shift. Click also accepts on_text and clicks.

await terminal.mouse.click(10, 5, button="right", ctrl=True)

Module functions

Function Description
await sessions() List sessions in this process.
await close_all() Close all sessions in this process.
await get_recording(session=None) Return an automatic asciinema recording.
unique_session(prefix=None) Create a unique session name.

Test helpers

Import from tui_test.testing.

Function Description
await create_terminal(**options) Create, open, and track a terminal.
async with terminal(**options) Open and close a terminal.
await close_all_tracked() Close tracked terminals.
set_terminal_defaults(**options) Set suite defaults.
reset_terminal_defaults() Reset suite defaults.
track_terminal(terminal) Track a terminal.
untrack_terminal(terminal) Stop tracking a terminal.
tracked_count() Count tracked terminals.
terminal_snapshot(text) Normalize text for snapshots.

TerminalOptions adds shell, program, session, and prefix to the client and spawn options.

DEFAULT_SHELL is the platform default.

from tui_test.testing import terminal

async with terminal(program=("my-app",)) as app:
    await app.get_by_text("Ready").expect()

Configuration

from tui_test import AutomaticRecording, Colors, Profile, Timeouts

terminal = TuiTest(
    profile=Profile(
        scrollback=500,
        colors=Colors(foreground="#ffffff", background="#000000"),
    ),
    timeouts=Timeouts(text=10_000, command=60_000),
    artifacts={"dir": "artifacts", "on_failure": "all"},
    trace={"mode": "on-failure", "directory": "artifacts/traces"},
    recording=AutomaticRecording(directory="artifacts/casts"),
)

Types

Type Description
State Session state and visible text.
Cursor Typed dictionary with integer x/y, boolean visible, and string shape/color.
Cell One terminal cell and its style.
TextMatch Matched text, positions, and spans.
TextStyle Locator style fields.
Profile Scrollback and colors.
Timeouts Text, idle, command, exit, and ready timeouts.
AutomaticRecording Automatic recording mode and directory.
Colors Terminal palette.
MouseButton "left", "middle", or "right".
TextPosition, TextSpan Match coordinates.
FailureDetails Structured operation, locator, process, runtime, and screen evidence.
FailureArtifactRef Paths and write status for a failure artifact.

__version__ contains the package version.

Errors

Error Exit code
ExpectationError 1
UsageError 2
NoSessionError 3
InternalError 5

All errors extend TuiTestError. Structured native failures expose details and artifact; expectation errors continue to populate compatibility terminal.text and terminal.screenshot fields. Failure artifacts can contain terminal output, titles, locator operands, and recordings, so review them before uploading.

Sessions are local to the current process and cannot be controlled by the CLI. Cancelling a task does not stop an active terminal operation.

Metadata

Release files for tui-test 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tui-test 0.1.1
File Size Uploaded Provenance Actions
tui_test-0.1.1.tar.gz 4.2 MB With provenance Details for tui_test-0.1.1.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for tui-test 0.1.1
File Actions
tui_test-0.1.1-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for tui_test-0.1.1-py3-none-win_amd64.whl
tui_test-0.1.1-py3-none-musllinux_1_2_x86_64.whl Python 3 none Linux musl 1.2+ x86-64 Details for tui_test-0.1.1-py3-none-musllinux_1_2_x86_64.whl
tui_test-0.1.1-py3-none-musllinux_1_2_aarch64.whl Python 3 none Linux musl 1.2+ ARM64 Details for tui_test-0.1.1-py3-none-musllinux_1_2_aarch64.whl
tui_test-0.1.1-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details for tui_test-0.1.1-py3-none-manylinux_2_28_x86_64.whl
tui_test-0.1.1-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details for tui_test-0.1.1-py3-none-manylinux_2_28_aarch64.whl
tui_test-0.1.1-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details for tui_test-0.1.1-py3-none-macosx_11_0_arm64.whl
tui_test-0.1.1-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details for tui_test-0.1.1-py3-none-macosx_10_12_x86_64.whl

Total release size: 49.0 MB

Release files / tui_test-0.1.1.tar.gz

Download URL tui_test-0.1.1.tar.gz
Size 4.2 MB
Tags Source
SHA-256 checksum
How to use checksums
e9909fa3c82750e98aae710b80161b1477404b0ff0395274c5c9a40eb35e6ad6
BLAKE2b-256 checksum
How to use checksums
a21b7cec11454248f8beb86469765d943a16cd26b4e4ee59cc45293bda3ea573
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-win_amd64.whl

Download URL tui_test-0.1.1-py3-none-win_amd64.whl
Size 6.3 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
bffec5e34aeaf1371141bb03ef92bbedbc71767a6c4a642deaca44724cf81a0b
BLAKE2b-256 checksum
How to use checksums
dcb7c65659b7c219219f7f45cdf87c6178ef0a9ce15372d784e5aa2ceb380247
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-musllinux_1_2_x86_64.whl

Download URL tui_test-0.1.1-py3-none-musllinux_1_2_x86_64.whl
Size 7.5 MB
Tags Linux musl 1.2+ x86-64 Python 3
SHA-256 checksum
How to use checksums
002bfb05c8820ecf7e4d0c5e1dc84a1f8ab5de75bdd39e0674bd02211f7ac6b8
BLAKE2b-256 checksum
How to use checksums
eb1b9507849088128c02928f3f936d8e41b12bc07736227c1f6c235590940d66
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-musllinux_1_2_aarch64.whl

Download URL tui_test-0.1.1-py3-none-musllinux_1_2_aarch64.whl
Size 7.0 MB
Tags Linux musl 1.2+ ARM64 Python 3
SHA-256 checksum
How to use checksums
ca416d50a6ecd42aed7e5a83b5fefd6d8a1a0531204cf64bcbaeac694ef4dd45
BLAKE2b-256 checksum
How to use checksums
5a17956a28cfcd4f537edf5ca877dd043c96cde9fcda12635b65e216adbb46dc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-manylinux_2_28_x86_64.whl

Download URL tui_test-0.1.1-py3-none-manylinux_2_28_x86_64.whl
Size 6.4 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
8c257575586bf1a78c48c9af398d13532a4d4341fac53ba6f2cf9fc8fbbc72c9
BLAKE2b-256 checksum
How to use checksums
a815eefa5721ae713e71c5a8ce346ae9bd4af55d0ebb6da4e691d97b1e3e2070
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-manylinux_2_28_aarch64.whl

Download URL tui_test-0.1.1-py3-none-manylinux_2_28_aarch64.whl
Size 6.0 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
cde255166a218593490682b2b089f89bad7ec28d87442f7c4ccd3319ae0f7bb3
BLAKE2b-256 checksum
How to use checksums
c9c3b0c0dfe86ffcedd8a69005699caed6c4f7fa236c12c859756a3acb1932f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-macosx_11_0_arm64.whl

Download URL tui_test-0.1.1-py3-none-macosx_11_0_arm64.whl
Size 5.6 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
59b20c4255c3fce425f5579f3e810b02b1a5d26f3dd55e0a607aa1cb2127e4e6
BLAKE2b-256 checksum
How to use checksums
9550328494199ee77e45b64aa9b93aa574270d3e1908f05da3d75a28c0e48c3a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Release files / tui_test-0.1.1-py3-none-macosx_10_12_x86_64.whl

Download URL tui_test-0.1.1-py3-none-macosx_10_12_x86_64.whl
Size 6.0 MB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
44bd5f6ddd4ba22f6d7fcbe72057e8650ec586b7033d0f8d8dd15bfda236a193
BLAKE2b-256 checksum
How to use checksums
ee304446df40dc422cac486e0ac0d29fffb8c63e991004b63e227854033438d1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

Security for tui-test 0.1.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 8 files in this release have been uploaded with provenance metadata.

Select from 8 files

Attestation for tui_test-0.1.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page