Letting Antigravity Background Agent Watch the First 72 Hours After Release: A Monitoring Pipeline Forged Across Six Wallpaper Apps
Drawing on twelve years of indie iOS and Android development and six wallpaper apps released in parallel, this article shares a release-monitoring workflow built on Antigravity Background Agent. It covers how to collapse Firebase Analytics, Crashlytics, and AdMob into one Markdown report, with the actual queries and thresholds used in production.
The first 72 hours after a release is the stretch that still makes me sit up straight, even after twelve years of shipping apps as an indie developer. Once an update is pushed to the App Store and Google Play, no amount of pre-release testing fully tells you what the first overnight cycle will look like.
For the past few years I have been running six wallpaper apps in parallel under the Dolice umbrella, and for a long time my post-release ritual involved refreshing dashboards over and over. Firebase Crashlytics in one tab, AdMob in another, App Store Connect Analytics in a third, Play Console somewhere else. Repeat the loop across six apps and concentration begins to thin out exactly when you most need to catch a subtle warning sign.
What changed the rhythm was handing the watch shift to an Antigravity Background Agent. I will walk through the full setup here, including the thresholds and queries that settled into place after running six apps through this loop.
Why the 72 Hours After a Release Behave Like a Hazard Zone
App Store and Google Play rollouts are both called "staged releases," but their internals diverge enough to make the early hours feel like four separate clocks running at once. With a 50-million-download portfolio of wallpaper apps spread across both stores, I have come to expect four kinds of signals to move on their own schedules.
Crash rates move fastest. Within fifteen minutes of a new build going live, the first stack traces often start appearing in Crashlytics. ANRs on Android tend to surface a few hours later. eCPM in AdMob takes roughly six to twelve hours to settle because the mediation engine needs that window to recompute the auction order. Retention metrics — D1 in particular — cannot be evaluated until at least twenty-four hours have passed.
That means a single dashboard cannot capture release health. Each signal needs its own reading window and its own tolerance band, and asking a human to track them in parallel for three full days is unrealistic.
Once a Background Agent sits in the middle, the dashboard hopping disappears. The agent runs each API call inside its sandbox VM, joins the outputs, and delivers one Markdown summary. That single artifact is the only thing I now check.
Why a Background Agent Suits the Watch-Shift Role
The Antigravity Background Agent is an async agent that runs in a separate window from your normal editing session. It is most often described as a coding partner, but I have found that it shines in production monitoring for three reasons.
First, the sandbox VM does not touch the local machine. My dev box is one Mac mini that has to hold the IDE for six apps simultaneously, so being able to run the watch job on a separate lane is the difference between a focused editing session and an interrupted one.
Second, network access is permitted out of the box. As long as the agent is given service account credentials, hitting Firebase REST endpoints, BigQuery, or the AdMob Reporting API is not a problem.
Third, the runtime is designed for long-running tasks. Inline agents tend to lose context after a stretch, but a Background Agent can comfortably handle "run every hour" or "aggregate every six hours" pacing — exactly the cadence a 72-hour monitor needs.
Both of my grandfathers were temple carpenters in Japan, and as a child I remember being told that once a roof beam is fixed in place you still keep going back to check on the joinery through the next storm. Releasing software has a similar texture for me. The carpenter returns to the site; the indie developer returns to the dashboard. Letting a Background Agent take over that return visit was the closest fit I found.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A Background Agent configuration that watches crash-free users, ANR rate, eCPM, and D1 retention in parallel and folds them into a single Markdown summary
✦The exact alert thresholds that have stabilized across a 50-million-download wallpaper portfolio, plus the BigQuery and AdMob Reporting API snippets behind them
✦How to wire the agent into Google Play staged rollouts (5% → 20% → 50% → 100%) so the rollout decision becomes a numbers-driven step rather than a gut call
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The biggest lesson from my early attempts was that watching everything is the surest way to see nothing. New builds tempt you into staring at twenty graphs at once. After enough release cycles, I narrowed the agent's responsibility to four signals.
The four I keep are crash-free users, ANR rate, eCPM, and D1 retention. The thresholds I am about to share were not theoretical from the start — they are the values that have stayed stable across roughly thirty releases.
Signal
Source
Settles
Alert Threshold
crash-free users
Crashlytics
from 1h after release
drop below 99.5%
ANR rate
Play Console Vitals
from 3h after release
above 0.47%
eCPM (rewarded)
AdMob Reporting API
from 12h after release
20% below the trailing 7-day median
D1 retention
Firebase Analytics (BigQuery)
from 24h after release
5pt below the trailing 7-day median
At 50 million downloads, these numbers settle into a fairly conservative band. Crash-free users typically sit between 99.7% and 99.9%, so anything below 99.5% is unambiguously off. The 0.47% ANR threshold is anchored to Google Play's Bad Behavior Threshold; crossing it puts store visibility itself at risk.
The Background Agent runs every hour, pulls these four signals, and surfaces only the ones that breached. The skeleton in Python looks like this.
# release_monitor/runner.pyfrom datetime import datetimefrom typing import TypedDictclass MetricResult(TypedDict): name: str value: float threshold: float breached: bool source: strdef collect_metrics(app_id: str, build: str) -> list[MetricResult]: return [ crashlytics_crash_free(app_id, build), play_vitals_anr(app_id, build), admob_ecpm_dip(app_id, build), ga4_d1_retention(app_id, build), ]def build_report(app_id: str, build: str) -> str: metrics = collect_metrics(app_id, build) breached = [m for m in metrics if m["breached"]] header = f"# {app_id} / build {build} / {datetime.utcnow().isoformat()}Z\n\n" if not breached: return header + "All four signals are within tolerance. Idle until the next sweep.\n" body = "\n".join( f"- ⚠️ **{m['name']}** = {m['value']:.3f} (threshold {m['threshold']:.3f}, source: {m['source']})" for m in breached ) return header + "## Threshold Breach\n\n" + body + "\n"
The runner lives inside the agent's VM and writes the report to disk. Because the agent's filesystem is contained, the file ends up at reports/2026-05-20T03Z.md inside its own VM, and the report is then attached to a pull request so I can review it from my normal environment.
Automating the Firebase Analytics Query
Pulling D1 retention from Firebase Analytics is straightforward if BigQuery export is on. I enabled it across the whole wallpaper portfolio, and I now read directly from the daily partitioned events_* tables.
-- bigquery/d1_retention.sqlWITH first_open AS ( SELECT user_pseudo_id, MIN(event_date) AS first_date FROM `dolice-wallpapers.analytics_xxxxx.events_*` WHERE event_name = 'first_open' AND _TABLE_SUFFIX BETWEEN @start_date AND @end_date GROUP BY user_pseudo_id),returned AS ( SELECT DISTINCT user_pseudo_id FROM `dolice-wallpapers.analytics_xxxxx.events_*` WHERE event_name = 'session_start' AND _TABLE_SUFFIX BETWEEN @start_date AND @end_date)SELECT fo.first_date, COUNT(DISTINCT fo.user_pseudo_id) AS new_users, COUNT(DISTINCT r.user_pseudo_id) AS retained_d1, SAFE_DIVIDE(COUNT(DISTINCT r.user_pseudo_id), COUNT(DISTINCT fo.user_pseudo_id)) AS d1_rateFROM first_open foLEFT JOIN returned r ON fo.user_pseudo_id = r.user_pseudo_idWHERE r.user_pseudo_id IN ( SELECT user_pseudo_id FROM `dolice-wallpapers.analytics_xxxxx.events_*` WHERE event_name = 'session_start' AND PARSE_DATE('%Y%m%d', event_date) = DATE_ADD(PARSE_DATE('%Y%m%d', fo.first_date), INTERVAL 1 DAY))GROUP BY fo.first_dateORDER BY fo.first_date DESC;
The Background Agent needs both the bq CLI installed and a service account key in place. I keep that contract in an AGENTS.md at the root of the monitoring repo so the agent does not improvise.
# AGENTS.md (excerpt — release-monitor repository)This repository hosts the post-release monitoring job that runs inside anAntigravity Background Agent.## Authentication- `GOOGLE_APPLICATION_CREDENTIALS` points to a service account key for bq.- AdMob uses `ADMOB_OAUTH_REFRESH_TOKEN` (Reporting API scope) injected via env.- Crashlytics goes through Firebase REST API and reuses the bq service account.## Running- `python release_monitor/runner.py --app=<bundle_id> --build=<version>`- Cron entry for hourly runs lives at `cron/release-monitor.cron`.- Output: `reports/<utc-timestamp>.md`## Hands-off zones- `secrets/` — keys are placed and rotated manually only.- `reports/archive/` — append-only; never overwrite past reports.
Maintaining AGENTS.md has been one of the biggest single changes since I started using Antigravity in earnest. With it, the Background Agent stops asking "is it okay to do X" and instead behaves like a contractor who actually read the project brief before showing up.
Crashlytics Stack-Trace Summarization Pipeline
A fresh crash on Crashlytics is rarely useful by itself — the cost is in reading the stack traces. This is where the Background Agent earns its keep: it collapses many incidents into a small set of grouped causes.
Because Crashlytics does not expose an Issue API, I pull the data from the BigQuery export at firebase_crashlytics.events_*. Here is the query I use.
-- bigquery/crashlytics_top_issues.sqlSELECT issue_id, ANY_VALUE(issue_title) AS title, ANY_VALUE(issue_subtitle) AS subtitle, COUNT(DISTINCT installation_uuid) AS affected_installs, COUNT(*) AS crash_count, MIN(event_timestamp) AS first_seen, MAX(event_timestamp) AS last_seen, ANY_VALUE(application.display_version) AS versionFROM `dolice-wallpapers.firebase_crashlytics.<app_id>_ANDROID_REALTIME`WHERE _PARTITIONTIME >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 72 HOUR) AND application.display_version = @target_version AND event_type = 'crash'GROUP BY issue_idORDER BY affected_installs DESCLIMIT 10;
For the agent, I convert the JSON output into a compact summary of the top three issues plus likely suspect commits.
# release_monitor/crashlytics_summary.pyimport json, subprocess, textwrapdef fetch_top_issues(app_id: str, version: str) -> list[dict]: out = subprocess.check_output([ "bq", "query", "--use_legacy_sql=false", "--format=json", f"--parameter=target_version::{version}", open("bigquery/crashlytics_top_issues.sql").read(), ]) return json.loads(out)def summarize_for_agent(issues: list[dict]) -> str: if not issues: return "No crashes in this window." lines = [] for i, issue in enumerate(issues[:3], 1): lines.append(textwrap.dedent(f""" ### Issue {i}: {issue["title"]} - Affected installations: {issue["affected_installs"]} - Crash count: {issue["crash_count"]} - First seen: {issue["first_seen"]} - Likely cause: {issue["subtitle"]} """).strip()) return "\n\n".join(lines)
The key choice is the hard cut at three issues. Feeding the full list to the agent burns context and slows the decision down. In my experience, the issues worth acting on in the first 72 hours sit in the top two or three; below that the noise is usually familiar.
Detecting eCPM Dips on AdMob
AdMob is comfortable to read in the dashboard, but the API has its quirks. Reporting API v1 is the stable surface. After issuing an OAuth refresh token, the Background Agent can call it directly. When you manage several apps, taking the Network Report at AD_UNIT granularity and filtering to rewarded inventory only is what makes dip detection believable.
In my own wallpaper apps, a 20%-or-greater eCPM drop on release day almost always means something is happening. Breaking down the cases I have seen, roughly 30% trace back to mediation order shifting in the wrong direction, 20% to a misconfigured ad frequency, and the remaining 50% to in-app behavior changes (users seeing fewer rewarded prompts after a UX tweak, for example).
I deliberately leave only the dip detection to the agent and keep root cause analysis on the human side. Pushing root cause to the agent invited speculative explanations that were hard to undo. Drawing the line at detection keeps the agent's outputs trustworthy.
Composing One Markdown Report at the End
Gathering four signals separately leaves you with the same alignment work you wanted to avoid. I made the agent's last step a hard requirement: collapse everything into a single Markdown file.
Markdown is the format that survives. It attaches cleanly to pull requests, renders correctly inside GitHub commit comments, and is searchable by ordinary tooling. I now keep all 72-hour reports inside a release-monitor repository under reports/. Three years in, that archive has quietly become my best primary source when comparing the trajectory of past releases.
Pairing the Agent with Staged Rollouts
The reports are useful but neutral on their own. They become decisions only when wired into staged rollouts.
For wallpaper apps on Google Play I keep a fixed ladder: 5% → 20% → 50% → 100%. The Background Agent produces a report every three hours, and only after every signal stays inside its band does the next rung become safe.
GitHub Actions runs the workflow on schedule, while the script it invokes is iteratively improved by the Background Agent. When I want to add a new signal, I assign the task to the agent, review the resulting pull request, and merge once it looks right.
On the App Store side, Phased Release is coarser — daily steps, less granular control. Even there, the same crash-free reading is enough to justify pausing the rollout from App Store Connect. I treat that as a fail-safe rather than as the primary controller.
What the Six-App Workflow Looks Like in Practice
Running six apps in parallel forced me to evolve the agent setup. The biggest single change was giving each app its own Background Agent session.
I originally tried monitoring everything from one session. Context began bleeding across apps. Beautiful HD Wallpapers and Ukiyo-e Wallpapers, for instance, have different user expectations and different monetization shapes; the same threshold applied across both can produce misleading verdicts.
The arrangement I now recommend looks like this.
Create a dedicated release-monitor-<app> repository per app.
Assign each repository its own Background Agent.
Extract shared logic into a release-monitor-core package; per-app repos hold only configuration.
Once a day, fold the six reports into a release-monitor-dashboard repository for cross-app comparison.
This keeps changes to one app's monitor from rippling into the others, and lets each agent stay narrow enough to keep its context budget useful.
Boundaries After Thirty Days of Use
After roughly thirty days of this arrangement, the line between what to delegate and what to keep on the human side has become clear.
Safe to delegate: continuous metric pulls, threshold breach alerts, diffs against prior releases, top-three crash summaries, and pull requests that improve the release_monitor package itself.
Better not to delegate: final root-cause identification (speculation creeps in), rollout halt decisions (the responsibility lives with the developer), drafting replies to store reviewers (the words to a user should be hand-shaped), and resolving Crashlytics issues (a false resolution is harder to roll back than a false alert).
If I borrow the carpenter image from my grandfathers once more, the boundary maps cleanly: the tools can move material around, but the moment of saying "this is done" stays in the craftsman's hand. Background Agent moves an astonishing amount of weight, yet I keep the irreversible decisions — pausing a rollout, sending a reply to an upset user — on my own desk.
Even after the relief of stepping away from the dashboards, the nature of the work has not really changed. A new build still needs someone to come back and check on the joinery as it settles. The agent simply takes that watch shift, and what arrives at my hand is one Markdown report — which I still read myself. Thank you for reading along.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.