After every gipity deploy, verify the result - don't assume it worked. The deploy → inspect → fix loop is how you catch a blank page, a 500, or a broken asset before the user does. These commands are the inspect half.

gipity page inspect <url> - is the page healthy?

The first thing to run after a deploy. Loads the page in a real browser and reports what's wrong:

gipity page inspect https://dev.gipity.ai/<account>/<project>/

In the build loop, prefer gipity deploy dev --inspect - it deploys and runs this same report on the live URL in one command. The standalone form is for re-checking a page without deploying (or with the extra flags below).

Output:

Flag Effect
--all Also report render-blocking resources, files >100KB, oversized images, overflowing elements, and LCP detail
--wait <ms> Settle time after DOMContentLoaded before capturing (default 500) - raise it for late async work
--wait-for <selector> Wait until a CSS selector appears before capturing (deterministic; replaces --wait). Use for apps that render after an async boot, e.g. --wait-for "#app:not([hidden])". --wait-timeout <ms> caps it (default 5000).
--device <name> Inspect as a real touch device (mobile, tablet, or an exact name like iPhone 16 Pro / Pixel 9) — touch events, mobile user-agent, DPR. Use it to catch console errors and overflow in the mobile layout.
--no-reprobe Skip the automatic second probe that filters one-time transient console errors - faster, but a cold-load transient may be reported as real
--no-truncate Show full URLs instead of middle-ellipsis
--json Machine-readable output

Diagnosing a blank or broken page: check Console for the JS error and Failed resources for anything that didn't load - a wrong asset path or an uncaught exception is almost always one of those two. An app that boots asynchronously may be snapshotted before it renders; if Console looks empty but the page seems blank, re-run with --wait-for "<a selector that only exists once booted>" to capture the settled state, not the loading screen.

If page inspect or page screenshot fails with "produced no output within Ns", retry it once. That error means the browser session was still busy with an abandoned earlier command, not that your page is broken or "too heavy to load headlessly". The session is reset automatically when this happens, so the retry gets a clean browser. Heavy WebGL/WASM apps (Three.js, Rapier, Phaser) inspect fine — a page whose external CDN genuinely hangs still returns a usable report, flagged navigationIncomplete with the culprit under Failed resources. Don't conclude the inspector can't handle your app; conclude that from a second identical failure.

page inspect does not produce an image - it reports health signals. For a screenshot, use gipity page screenshot <url> (below); that's the one command for captures.

gipity page screenshot <url> - what does it actually look like?

Captures a PNG. With no -o, it auto-saves to screenshots/ss-<host>-<yyyy-mm-dd_hh-mm-ss>.png — a timestamped, collision-safe name in the project's screenshot history. That folder syncs to Gipity (the capture is also stored server-side at the same path, so the web view shows it instantly) but is never deployed to the CDN — it's a browsable visual record of the app being built. Let it use that default; don't dump captures to /tmp with -o. Add --ephemeral for a throwaway capture that skips the history. To look at the result, read the saved path — and in the web view, that read shows up as an inline thumbnail the user can click into a full-screen viewer.

gipity page screenshot https://dev.gipity.ai/<account>/<project>/
gipity page screenshot <url> --full                 # whole scrollable page
gipity page screenshot <url> --device mobile,desktop # one load, multiple viewports
Flag Effect
--full Capture the full scrollable page (default: viewport only). Scrolls the page through first, so scroll-reveal / fade-in-on-scroll sections (IntersectionObserver-driven, held at opacity:0 until in view) render into the shot instead of photographing blank below the fold.
--device <names> Device presets: default, desktop, laptop, tablet, mobile (comma-separated). mobile/tablet emulate a real touch device; also takes exact names (iPhone 16 Pro, Pixel 9, iPad Pro, Galaxy S25, …)
--viewport <WxH[@dpr]> Raw viewport, e.g. 414x896@2 (sizes the window only — no touch, no mobile user-agent)
-o <file> Output filename (single viewport only)
--post-load-delay <ms> Delay after load before capture (default 1000)
--action <js> Run JS in the page before capturing (e.g. click a button), then settle again — for a state that only appears after an interaction
--json Output metadata (page title, status, performance) instead of a summary

Use --device mobile,tablet,desktop to check responsive layout in one shot.

Shooting mobile UI: use --device mobile, not --viewport 390x844. A raw viewport only resizes the window — the browser is still a desktop one, so 'ontouchstart' in window is false, navigator.maxTouchPoints is 0, and @media (pointer: coarse) doesn't match. An app that mounts its touch controls behind any of those checks will render its desktop layout and you'll photograph the wrong thing. --device mobile emulates the handset end to end — touch events, maxTouchPoints, pointer: coarse, mobile user-agent, DPR — so the mobile build is what lands in the PNG.

Capturing a state behind an interaction (start a game, open a menu, dismiss a modal)? Use --action:

gipity page screenshot <url> --action "document.getElementById('play').click()"   # in-game frame, not the start screen

The action runs after the post-load delay, then the page settles again so the result has painted. Don't hand-roll a page eval that returns a base64 image — the eval result is capped (~16KB) and truncates the PNG; --action writes a normal PNG to disk like any other screenshot.

gipity page eval <url> <expr> - ask the page anything else

page inspect reports a fixed set of health signals. When you need something it doesn't cover - a computed style, an element's exact position or size, whether a node is visible, a count of matching elements - evaluate a JavaScript expression in the page (same context as the DevTools console) and get the serialized result back:

gipity page eval <url> "getComputedStyle(document.querySelector('.hero')).position"
gipity page eval <url> "document.querySelectorAll('.card').length"
gipity page eval <url> "document.querySelector('#cta').getBoundingClientRect()"
Flag Effect
--wait <ms> Settle time after DOMContentLoaded before evaluating (default 500)
--wait-for <selector> Wait until a CSS selector appears before evaluating (deterministic; replaces --wait). --wait-timeout <ms> caps it (default 5000).
--reload <expr> After the first eval, reload the page in place (localStorage/sessionStorage/cookies preserved) and evaluate this second expression against the post-reload DOM. --reload-file <path> reads it from a file. See the persistence check below.
--json Output { url, result, truncated } instead of a summary

The expression may be async - a returned Promise is awaited and its resolved value serialized, so (async()=>{ const r = await fetch('/health'); return r.status })() and await window.Gipity.auth.user() work.

The expression runs in page context only - it can read the DOM and that origin's storage, but has no server or filesystem reach. The result is serialized and length-capped; if it comes back truncated, narrow the expression (select fewer nodes, return a single field).

It's an expression, not a statement block. The input is spliced into await (<expr>), so a bare top-level const/let/return fails to parse (Unexpected token const). To run multiple statements, wrap them in an IIFE that returns the value:

gipity page eval <url> "(()=>{ const el=document.getElementById('email'); el.value='a@b.com'; return { id: el.id, value: el.value, visible: !!el.offsetParent }; })()"

Return the value, don't post-process the output. page eval already serializes the result - make the expression compute exactly what you want and read it directly (--json gives { url, result, truncated }). Don't pipe the output through python3 -c, sed, or a shell regex to dig a value back out - that's slower and fragile (one unescaped ( in the URL breaks the regex). If you need three facts, return one object: gipity page eval <url> "JSON.stringify({ booted: !!window.MyApp, cards: document.querySelectorAll('.card').length, err: window.MyApp?.lastError })".

Each separate gipity page command is a fresh, anonymous browser. Every page eval/page inspect/page screenshot invocation opens the URL in a clean profile - no localStorage, cookies, or globals from a previous command survive, so you can't "install a probe, then read it" across two commands, and a state one command set (a language choice, a signed-in session) never skews the next. To test persistence across a reload, use --reload (storage IS preserved within the one command); for multi-part checks on one loaded page, use --step.

Driving the page harder → read app-debugging-reference: --step (many assertions, one page load), --reload (does the state survive a refresh?), --fixture (feed the app a real MP3/image/PDF), --file (a long eval body without shell-quoting hell), the ~20s in-page budget, an in-app error trap for errors the console misses, --camera (point the headless browser at a picture), and stepping a simulation's clock with advance(seconds) instead of waiting on real time.

What the headless browser can't test

The inspect/eval/screenshot browser is headless with no human and no logged-in session, so anything gated on a trusted user gesture won't fire from a programmatic click:

Camera apps, and other things to read next

The camera is not in that list - --camera <image> gives page eval / screenshot / inspect a synthetic webcam that plays your image on a loop, so a vision app's real getUserMedia → inference → UI pipeline runs headlessly. That, plus stepping a game's clock with advance(seconds) (never wait on real time - the headless browser paints at ~2-3 fps, so simulated time crawls and your assertion fails as a false negative), is in app-debugging-reference.

gipity logs fn <name> - why did a function fail?

When a function returns a 500 or wrong data, read its execution logs:

gipity logs fn list-items            # recent runs
gipity logs fn list-items --limit 50

Each entry shows time, status (ok / error / limit_exceeded), duration, trigger type, svc:N when the invocation made N platform service calls, the error message for failed runs, and — indented beneath each run — any console.log / console.warn / console.error that invocation printed. So sprinkling console.log through a function and re-reading its logs is a real debugging loop: the output is captured on every run (success or failure) and persisted with that run. console is variadic and Node-like — objects serialize to JSON, and the full surface (log/info/debug/warn/error, dir/table/trace/assert/group/time) is available, so reaching for one never crashes the function. (Output is capped at 1000 lines per run, 10k chars per line, and ~64k chars total; beyond any of those a truncation marker is added.) gipity fn logs <name> is the equivalent under the fn command group.

Did my service call (notify / email / LLM / image) actually go out? The svc:N chip on an ok run says N service calls were made; for the authoritative per-call outcome — status, provider, latency, its own error message — read gipity logs app --type services. --json on logs fn emits a top-level array of invocation rows ({ status, duration_ms, error_message, limits_consumed, logs, ... }), so pipe it to jq '.[0]', not .data.

gipity fn call <name> [body] - test a function directly

Invoke a deployed function from the CLI, bypassing the frontend - the fastest way to confirm a fix:

gipity fn call list-items '{"limit": 5}'
gipity fn call list-items --data '{"limit": 5}'

It prints the function's JSON response.

gipity fn call runs authenticated as the logged-in CLI user - it sends your session token, so an auth: user function sees ctx.auth.userId set to you. This is not the same as ctx.fn.call in tests (anonymous) or a logged-out web visitor. The practical hazard: smoke-testing a mutating auth: user function (claim, create, purchase) against a deployed app writes real rows owned by your account - on prod, real production state. To exercise the logged-out path, hit the endpoint with no auth header (plain curl -X POST https://a.gipity.ai/api/<PROJECT_GUID>/fn/<name> -d '{...}') and confirm it 401s.

gipity fn list shows every deployed function with its version and auth level. gipity fn delete <name> (alias rm, add --yes to skip the prompt) removes one - useful for cleaning up an orphaned function, since gipity deploy does NOT prune functions whose source file you deleted.

What gipity logs app does and doesn't catch

The Gipity client SDK (gipity.js, auto-injected into every template) reports four things to gipity logs app:

  1. Uncaught window errors - any thrown error not caught by app code.
  2. Unhandled promise rejections - any async failure without a .catch.
  3. Non-2xx fetch / XHR responses - patched transparently; agents see status, URL, and a response snippet.
  4. Manual reports - anything app code passes to Gipity.captureError(...).

What it does not auto-catch: a 200 OK with the wrong shape (e.g. a function returned { data: { post } } and the client expected { post }), a caught exception swallowed into console.error, or a logical failure with no exception. The request succeeded on the wire, so the fetch patch stays quiet. For these cases use the manual capture API:

// Caught error you still want to log:
try {
  doSomething();
} catch (err) {
  Gipity.captureError(err, { tags: { feature: 'editor' } });
}

// Semantically-wrong success - throw or capture manually:
const data = await Gipity.fn('add-post', { x, y, text });
if (!data?.post) {
  Gipity.captureError(new Error('add-post returned no post field'));
  return;
}

Using Gipity.fn(name, body) (see app-development) covers most of this automatically through path 1 - it throws on non-2xx, which the SDK catches as an uncaught error.

A debugging pass

  1. gipity page inspect <url> - console clean? resources all loading?
  2. Console shows a JS error → fix the frontend code, redeploy.
  3. A fetch to your own function failing → gipity logs fn <name> for the server-side error.
  4. Reproduce and confirm the function fix with gipity fn call.
  5. gipity page screenshot --device mobile,desktop - looks right everywhere?

Related skills