App Release Complaint Regression Monitor
Pricing
from $500.00 / 1,000 app analyses
App Release Complaint Regression Monitor
Compare public complaint rates before and after an app release, with denominators, sample sizes, and short review examples. A rate change is a time-window association, not proof of cause.
Pricing
from $500.00 / 1,000 app analyses
Rating
0.0
(0)
Developer
Olalekan Johnson
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
App Release Complaint Regression Monitor (Google Play)
Unofficial monitor for mobile product teams and app agencies. Give it public app ids, languages, and countries. It collects new and edited reviews, clusters complaints, and compares rates across a before window and an after window.
A row looks like login complaints 3/150 to 18/160. The denominator is the number of reviews in that window. An alert is emitted only when both windows are covered, both samples meet the minimum size, and both the percentage-point increase and the relative increase are met. The comparison is a time-window or version association, not causation. A higher rate after a date or version does not prove the release caused the complaints.
Unofficial, and not affiliated with Google or Apple. Public review text stays the reviewer's. This Actor keeps a short excerpt for your investigation and redacts email addresses and phone numbers. It does not store profile images or profile links.
Contact: plusonecares@gmail.com
Who it is for
Teams that already read reviews after a release and need a rate they can check, with the reviews that produced it. It is a poor fit for a low-volume app that never reaches the sample minimum, and it will say so instead of inventing an alert.
What you provide
| Field | Default | Role |
|---|---|---|
appIds | com.whatsapp | Google Play package names |
languages | en | Play language code, such as en or pt-BR |
countries | us | Storefront, such as us |
lookbackDays | 7 | Timestamp window to compare. Split into two equal halves unless you set a release |
maxReviewsPerScope | 200 | Newest-first cap per app, language, and country. Maximum 1000 |
publicationLagDays | 2 | Extra days re-read every run so a late-appearing review can still be seen |
releaseDate | empty | YYYY-MM-DD. Splits every app that has no per-app override |
appVersion | empty | Exact version string. Used only when the store put that version on the review |
releases | empty | Optional [{ "appId", "version", "releasedAt" }] |
customCategories | empty | Optional [{ "name": "notifications", "keywords": ["notification", "push"] }] |
minSampleSize | 40 | Minimum reviews in each window |
minRateIncreasePercentagePoints | 5 | Minimum rise in the complaint share |
minRelativeIncrease | 1 | 1 means the later rate is at least double the earlier rate. A jump from zero uses the percentage-point rule |
minComplaintCount | 5 | Minimum complaints on the later side |
maxComplaintScore | 3 | Stars at or below this count as complaints when a keyword hits. Higher stars count only when the text also describes a failure |
alertOnlyOnChange | false | Skip a repeat alert when both rates stayed within one percentage point of the previous run |
outputMode | full | full, alerts-only, or changes-only |
appleAppIds | empty | Optional numeric App Store ids |
enableLlm / llmApiKey | off | Optional labels for unmatched low-star reviews, on your own key |
stateStoreName | app-release-complaint-state | Named store for review hashes and previous alerts |
At most 12 app-language-country scopes run per execution. Further scopes are reported as unsupported and are not charged.
Built-in categories: login, crash, payments, performance, ads, ui, sync. English keywords always apply. Spanish, Portuguese, German, and French add a short extra list when the language code matches. Other languages still collect reviews. Add customCategories, or turn on the optional model, when the built-in list is the wrong vocabulary.
What you get
The default dataset is flat, so a webhook, Make, n8n, or a spreadsheet can filter it without unpacking nested objects. Every row has recordType, sourceUrl, observedAt, coverageStatus, and associationNote.
recordType | When it appears |
|---|---|
review | A new or edited review that was delivered. Unchanged reviews are not written again |
category-rate | One row per category, with both counts and both denominators |
regression-alert | The thresholds and sample sizes were met |
regression-cleared | alertOnlyOnChange is on and a previous alert no longer meets the rule |
scope-summary | What was collected, which comparison was used, and what was charged |
coverageStatus is one of complete, partial, empty, blocked, or unsupported. Those are different outcomes. A finished run can still be partial.
comparisonBasis is one of:
calendar-halves— the lookback split into two equal periodsrelease-date— equal-length periods before and after the date you suppliedapp-version— the version you named versus other reviews that carry a version. Reviews with no version are excluded and counted inversionUnknownReviewssampled-halves— the cap was hit before the requested window was covered. Rates describe the older half and the newer half of the sample. Alerts are withheld
The run summary is in the default key-value store under OUTPUT. It includes counts, chargedUnits, deliveredUnits, and per-scope coverage. Review baselines live in the named state store, not in OUTPUT.
Example alert
{"recordType": "regression-alert","recordId": "alert:google-play__com.example.app__en__us:login:3-18","source": "google-play","appId": "com.example.app","language": "en","country": "us","category": "login","ratePhrase": "login complaints 3/150 to 18/160","beforeCount": 3,"beforeSample": 150,"afterCount": 18,"afterSample": 160,"beforeRate": 0.02,"afterRate": 0.1125,"direction": "up","regressionDetected": true,"alertEmitted": true,"comparisonBasis": "calendar-halves","coverageStatus": "complete","windowsComplete": true,"examplesText": "Cannot log in after the update || Login fails on the password screen","sourceUrl": "https://play.google.com/store/apps/details?id=com.example.app&hl=en&gl=us","associationNote": "Time-window or version association, not causation. A higher complaint rate after a date or app version does not prove the release caused the complaints."}
A 5-star review that says login is smooth is a mention, not a complaint, so it stays in the denominator and out of the numerator. A 1-star review that matches no keyword is counted in unclassifiedComplaintCount on the scope summary so you can see what the lexicon missed.
Pricing
This Actor uses pay-per-event pricing. You are charged only for a unit it is about to deliver, and there is no separate per-result charge. Each run also has a $0.005 start event. Default memory is 512 MB (maximum 1024 MB).
| Event | When it is billed | Price |
|---|---|---|
review-processed | One new review, or one review whose text, stars, or version changed | $0.002 |
app-analysis | One completed app-language-country analysis (Play), or one app-country analysis (App Store) | $0.50 |
An unchanged review is not billed again. A blocked or unusable collection is not billed, and it does not replace a saved baseline. If the run spend limit is reached, delivery stops. Reviews that were not delivered are not marked as done, so a later run can still process them.
| Workload | Events | Cost |
|---|---|---|
| Default first look: WhatsApp, English, United States, 200 new reviews, 1 analysis | 200 × $0.002 + 1 × $0.50 | $0.90 |
| Same scope the next morning, 30 new reviews | 30 × $0.002 + 1 × $0.50 | $0.56 |
| Eight apps, one language, weekly, about 2,000 new reviews | 2,000 × $0.002 + 8 × $0.50 | $8.00 per run |
| Larger month: 20,000 new or edited reviews and 60 analyses | 20,000 × $0.002 + 60 × $0.50 | $70.00 |
The cap inside one run is 12 scopes and 1,000 reviews each: 12,000 × $0.002 + 12 × $0.50 = $30 before the platform start event. Set a max total charge on the run if you want a harder ceiling. Optional model calls are billed by your model provider, not by these events.
Local runs and accounts that are not on pay-per-event pricing still receive the dataset. chargedUnits stays at zero in that case. deliveredUnits shows what would be billed once the events are priced.
Default input, checked 23 October 2026
com.whatsapp, language en, country us, 7-day lookback, cap 200. The run finished in a few seconds and wrote 208 rows: 200 reviews, 7 category-rate rows, and 1 scope summary.
Those 200 newest reviews spanned about 2 hours, so the 7-day window was partial and the basis was sampled-halves. One rate row was login complaints 0/100 to 1/100. No regression alert was emitted, because a cut-off sample is not allowed to alert. A second run on the same baseline wrote 8 rows (the rates and the summary) and did not deliver the 200 reviews again.
That is the honest result for a very high-volume app at this cap. To cover a real release window, raise maxReviewsPerScope, narrow the lookback, or pass releaseDate / appVersion and accept partial until the read reaches the earlier side.
How a scheduled run behaves
Each successful scope stores review ids and content hashes in the named key-value store. The next run re-reads the lookback plus publicationLagDays, so a review that shows up late can still be clustered. Edits change the hash and are delivered once as edited. A run that is blocked, or that returns nothing while a baseline already exists, leaves the baseline in place.
alertOnlyOnChange compares the new rates with the previous alert. A move smaller than one percentage point does not emit another regression-alert. If the category no longer qualifies, the dataset gets regression-cleared.
Sources and limits
Google Play reviews come from the public Play batch endpoint, the same call the google-play-scraper library wraps. Requests are throttled, retried, and sent with the User-Agent app-release-complaint-monitor/1.0 (+mailto:plusonecares@gmail.com). No browser is used.
Apple is optional. The public customer-review RSS feed is storefront-scoped, about 10 pages, and sometimes returns an empty page in the middle. Empty pages are skipped. The feed is not a language feed: one analysis is charged per Apple app and country, not once per language.
Known limits:
- The Play batch shape can change. A broken response is
blocked, not an empty product. - Newest-first collection plus a few hundred reviews does not cover a week of a top app.
- Keywords miss paraphrases and can hit an incidental word.
unclassifiedComplaintCountand the excerpts are there so a person can check. - Some Play reviews have no version. They are left out of a version split.
- The storefront country is not the reviewer's location.
- A cleared or withheld alert is still a rate. It is not a statement that the release was fine.
- Platform charge idempotency is short. The state store is what stops a later run from billing the same review hash again.
- Review text in the dataset is an excerpt, capped and redacted. It is for your investigation, not a corpus to republish.
Optional model
Leave enableLlm off and the Actor is fully deterministic. Turn it on and supply llmApiKey if you want unmatched reviews at or below maxComplaintScore sent to an OpenAI-compatible chat/completions endpoint. At most llmMaxReviews (default 15) are sent per scope. Results are cached by text hash in the state store. A bad response, a non-HTTPS URL, or a host that resolves to a private or link-local address is skipped, and the keyword labels stand. The key is a secret input and is not written to the dataset.
Run it
pip install -r requirements.txtpython3 main.py
With no input file, the defaults above are used. To pass input locally, put INPUT.json in the working directory or start the run from the Apify console. Tests:
pip install pytest pytest-asynciopytest
On Apify, push this directory with the Apify CLI when you are ready. Memory for a normal run is 512 MB.
Integrations
Filter recordType = regression-alert if the webhook should page a human. Filter category-rate if you want every denominator, including the quiet categories.
- Webhook. On the Actor task, add a webhook for the run succeeded event. Point it at your endpoint. Read
ratePhrase,examplesText,coverageStatus, andassociationNote. - n8n. HTTP Request node runs the Actor, then a second node fetches
{{links.apiDefaultDatasetUrl}}/items?fields=recordType,appId,category,ratePhrase,examplesText,coverageStatus. Keep the items whererecordTypeisregression-alert. - Make. Use the Apify module to run the Actor and list dataset items. Map
ratePhraseinto the message body. - Google Sheets. The dataset columns are flat. Export the run or connect the Apify Google Sheets integration.
examplesTextis the CSV-friendly form of the excerpts.
A useful schedule is once after you ship, then daily while the release is fresh. Pass the same releaseDate or appVersion each time so the windows stay comparable. Turn on alertOnlyOnChange when the first alert has already been seen.
FAQ
Does an alert mean the release caused the complaints? No. The note on every row says the figure is a time-window or version association. Use it to decide whether a person should open the excerpts and your own crash or login telemetry.
Why did the default WhatsApp run not alert?
Two hundred newest reviews covered about two hours, not seven days. The Actor still returns the rates and labels the coverage partial. Alerts wait until both windows are actually covered and the sample minimum is met.
Why is a quiet category still a row?
crash complaints 0/150 to 0/160 tells you the denominator existed and the lexicon found nothing. A missing row would look the same as a failed collection.
What if Google or Apple returns an error?
That scope is blocked or unsupported. Nothing is charged for it. If a baseline already exists, it is kept. If every scope is blocked, the run fails after writing OUTPUT, so a schedule does not look successful.
Can I point the model URL at an internal network?
No. Only public https hosts are accepted. Localhost, link-local, and private addresses are rejected.
Who do I write to?
plusonecares@gmail.com. Include the run id, the input (without the model key), and whether coverageStatus was blocked or partial.