TED Contract Expiry Radar - Recompete Leads
Find EU public contracts approaching expiry from TED award notices: incumbent, buyer, value, end date and renewal options.
datagrit › Data › App Release Review Impact: Rating by Version
DataGroups App Store and Google Play reviews by app version and shows how each release changed the rating, with significance flags and the complaints that are new in that version.
App Release Review Impact reads the newest public reviews of Apple App Store and Google Play apps and groups them by app version. For every version you get the review count, the average rating, the star distribution, how the rating changed against the previous version, whether that change is statistically significant, and the complaint terms that are new in that release. It is built for product managers, QA and support leads, app store optimization teams and analysts who want to know which release made users unhappy without reading thousands of reviews.
Review scrapers return one row per review with a raw version string. This Actor returns one row per version, so a regression shows up as a single line with a trend of regressed instead of a spreadsheet pivot you have to build yourself.
regressed.lowStarTopTerms with its lift against older versions and the sample low-star reviews point to the feature or bug behind a rating drop; on deep Google Play reads newComplaintTerms adds a strict statistical flag for terms that are new in the release.1064216828), apps.apple.com links, Google Play package names (such as com.spotify.music) or play.google.com links. You can mix both stores in one run.ratingDelta is the change of the average rating and ratingDeltaMargin is the half-width of its 95 % confidence interval, in stars. deltaSignificant is a Welch two-sample t test at 95 % confidence: the margin is the t quantile for the Welch degrees of freedom times the standard error, so a version with 3 to 5 reviews gets a wide margin, and the variance of each group is floored at 1/12 of a star squared (ratings are whole numbers), so identical ratings never give a margin of zero. Simulated on 20 000 pairs of versions drawn from the same rating distribution, the test flagged a change in 0.3 to 5.4 % of the pairs at 3, 5, 10 and 50 reviews per version (95 % confidence promises 5 %).trend has five values. regressed or improved: the change is significant and at least 0.15 stars. stable: the change is under 0.15 stars and the margin is at most 0.3 stars, so the sample is large enough to rule out a bigger change. inconclusive: the sample cannot tell, either a visible change that is not significant or a margin wider than 0.3 stars; read ratingDelta next to ratingDeltaMargin. baseline: the oldest version of an app, which has no predecessor. The onlyRegressions filter keeps regressed rows only; when it returns nothing, the status row says how many versions dropped by at least 0.15 stars without enough reviews to call it significant.lowStarTopTerms lists the five most frequent words and word pairs in the 1 and 2 star reviews of the version, each with lowStarReviews, versionShare (their share of the version's low-star reviews), olderShare (the same share in all older versions) and lift (versionShare divided by olderShare, where olderShare is never taken below 1 % or below one review of the older pool, so a term that is absent from a small older pool does not get a lift that merely reflects the size of the pool). olderShare and lift are null when all older versions together hold fewer than 12 low-star reviews, because a share of a handful of reviews says nothing; they are filled on 3 of the 23 rows of the example input and are mostly null at 500 reviews per app. The list itself is null when the version has fewer than 8 low-star reviews and [] when it has 8 or more but no word or pair occurs in at least 4 of them (their complaints are too scattered). On the 23 rows of the example input (500 reviews on six Google Play apps) two runs on 1 October 2026 gave terms on 7 and 8 rows, [] on 3 and 1 rows and null on 13 and 14 rows; the counts move with the reviews available. Inflected forms of a word are listed once. Where a lift exists, a value near 1 means the term is about as frequent as in older versions and a high value points to a topic that is more frequent in this release.lowStarTopTermsText is the same list as one readable string (log (lift 11.1), working (lift 4.2), login) and is the column of the Version impact table in the Store; null when lowStarTopTerms is null or [].newComplaintTerms is the strict version of that list: terms that are significantly more frequent in the low-star reviews of the version than in all older versions (one-sided two-proportion test with z of at least 3, at least twice the older frequency, in at least 4 reviews and 3 % of the low-star reviews) and that were not already as frequent in the immediately previous version. newComplaintTermStats holds the same numbers per term. An empty array means the comparison was made and no term stands out, which is common on large samples where an old complaint is simply louder; null means no comparison is possible (the oldest version, or fewer than 12 low-star reviews in the version or in the older versions together). This field is strict and mostly empty or null at the default depth: measured on 1 October 2026 on six popular Google Play apps, it was filled with at least one term on 0 of 23 rows at 500 reviews per app and on 11 of 106 rows at 5000, because the older versions inside the window hold only a few 1-2 star reviews each. Use lowStarTopTerms for day-to-day triage and newComplaintTerms when you read Google Play at 5000 reviews and want only terms that stand out statistically. Read the terms next to the sample reviews: they point to a topic, not to a confirmed bug.sampleLowStarReviews holds up to three low-rated reviews that contain those terms. Author names are never collected.You pay per version row returned. Apps that are not found, apps without enough reviews and duplicates produce a free status row with found: false and a note, so you are never charged for an empty result. The price per row is set on the Actor page and shown before you run it; set a maximum charge per run in the Apify console to cap spending, and the Actor stops cleanly when the limit is reached. Reading the reviews costs nothing extra.
baseline without a rating change and with a note saying that the data is partial. Add countries, lower the minimum or use the Google Play listing of the same app for a longer history, and read the reviewCount next to every change.note says "us: 190 of 200 reviews carry an app version, so the version statistics of this storefront rest on fewer reviews"; below it that storefront is left out of the statistics, named in the note and in the status message (separately for storefronts with no version at all and for those under the threshold), and its country disappears from countries and sourceUrl, also on the status row of an app with no qualifying version. The app fails only when no storefront reaches the threshold. The status message of each run reports how many reviews carried a version.Retry-After header is honoured only while it fits into what is left of the budget) and the time spent in attempts that failed. An app gets at most 75 seconds, and at most an equal share of what is left of a 150-second budget for the whole run (with three apps the first one gets 50 seconds), and never less than 9 seconds, so an app that has not been touched yet always keeps enough time to make its requests and a source that hangs for the first apps cannot take the attempts of the next one. Each attempt is limited to 25 seconds by a hard deadline (and to what is left of the budget), so a source that accepts the connection and never answers costs that app 75 seconds in total, not minutes per request. Requests that succeed are not counted, so a large read of a healthy source is not cut short; the run also spends time on a 1.2 second gap between App Store requests. Measured on 1 October 2026 with the App Store host set to accept connections and never answer and Google Play live, on three apps (two App Store apps and com.reddit.frontpage): each App Store app used 50.0 s and got a status row, then com.reddit.frontpage delivered 500 reviews from Google Play in 2.2 s (102 s for the run; with a cap of 150 s for the run shared in turn, the third app used to get no request at all). With the App Store feed answering 200 without entries for the three countries us, gb and de the App Store app used 81.5 s (73 s of pauses) and Google Play delivered its 500 reviews afterwards (4 s). An empty page is not taken as the end of the feed: later pages are tried too (measured: pages 1 and 2 empty, page 3 with 50 reviews in the same minute). The note tells a gap (an empty page followed by a served page, so those reviews are missing from the window) apart from an empty page after the last served one (the oldest reviews of the window may be missing). An app whose feed stays empty gets a free status row that says the feed returned no reviews and how many tries were made (fewer than six when the budget ran out), which does not show whether the app has written reviews; run again to read them. A failed app or country keeps what was already read, whatever the failure was (an empty feed, a transport error, an HTTP error such as 403 or a changed format of the feed in one of several storefronts): the storefronts that were read stay in the version rows, and the note names the storefront that was not read and why. An app is a failure only when none of its storefronts could be read. The status row carries the app name, the storefronts that were looked up and the store page whenever the store answered the lookup. The other apps are delivered and the run succeeds. The same holds for any other error of one app (a store answering with an HTTP error, a changed format): it gets a free status row and the other apps are delivered. The run ends as failed only when no app delivered any reviews. If the feed stops partway (in the App Store, or in Google Play with an HTTP error such as 403 or a changed payload after the first batch of 200 reviews), the reviews read so far stay in the version rows and the note carries the number of reviews that arrived and the reason reading stopped; an error before the first review is a failure of that app. If the store answers with reviews but none of them passes validation (for example the rating field changed its format or was renamed, so the entries carry no rating at all), the app gets a status row that says so and the run fails only when no other app delivered reviews. When that happens in one of several storefronts, the other storefronts stay in the version rows and the note names the storefront and the number of entries that failed validation (all of them, or how many of the entries it returned); the version statistics of that storefront then rest on fewer reviews. Entries of the App Store feed that lack the rating field are counted as invalid in the same way, never skipped silently, and a page whose entries all lack it is not read again as if it were empty. Google Play entries that fail validation are reported the same way ("us: 100 of 200 review entries failed validation and were left out"). Google Play can also answer 200 without a single review. The Actor then asks up to three times (pauses of 3 and 5 seconds, inside the budget above) and compares the result with the rating count in the app page: with 50 or more ratings listed the app gets a free status row that names the count and the number of tries and says that this does not show whether the app has written reviews, and the run fails when no other app delivered reviews; with fewer ratings, or none listed, the status row says that the app may have no written reviews yet. An empty batch in the middle of the Google Play reviews gets one more try, and if it stays empty the note says that reading stopped after that many reviews and that the oldest reviews of the window may be missing. A reviews payload whose shape is not a list is a changed format, not an empty app. The version stored with a review is the field the grouping stands on, so it is checked on every app, however few reviews were read: when no storefront of an app reaches the threshold above (no review carries a version, or fewer than 20 % of the Google Play reviews, or fewer than 90 % of the reviews of each App Store country), the app gets a free status row that says the source may have changed (never advice to lower the filters), and the run fails when no other app delivered reviews. When a storefront reaches it, the note of every row and of the status row says how many of the reviews of that storefront carry a version (for example "us: 209 of 240 reviews carry an app version, so the version statistics of this storefront rest on fewer reviews."), because the review counts and averages of a row rest on those reviews only; Google Play leaves the version empty on a share of reviews (31 of 200 in the Spotify sample we checked).Every result is one flat record, so it drops straight into a spreadsheet, a database or a CRM.
| Field | Type | Description | Example |
|---|---|---|---|
store | string | Store the app and its reviews come from: app-store or google-play. | app-store |
appId | string | App Store numeric ID or Google Play package name as given in the input. | 1064216828 |
appName | string | Name of the app on the store page. Empty only on the status row of an app that was not found, or whose store lookup did not answer. | Reddit |
version | string | App version string exactly as reported with the reviews. | 2026.38.0 |
versionRank | integer | Position among the versions kept for this app, 1 = newest version. | 2 |
reviewCount | integer | Number of reviews written on this version within the reviews that were read. | 108 |
avgRating | number | Mean star rating of the reviews on this version, rounded to two decimals. | 1.93 |
stars1Count | integer | Number of 1-star reviews on this version. | 70 |
stars2Count | integer | Number of 2-star reviews on this version. | 9 |
stars3Count | integer | Number of 3-star reviews on this version. | 6 |
stars4Count | integer | Number of 4-star reviews on this version. | 4 |
stars5Count | integer | Number of 5-star reviews on this version. | 19 |
lowStarCount | integer | Number of reviews with one or two stars on this version. | 79 |
lowStarShare | number | Share of reviews on this version that have one or two stars, from 0 to 1. | 0.731 |
firstReviewAt | string | ISO 8601 time of the oldest review on this version within the reviews that were read. | 2026-09-22T14:03:11.000Z |
lastReviewAt | string | ISO 8601 time of the newest review on this version within the reviews that were read. | 2026-09-26T21:40:02.000Z |
previousVersion | string | Nearest older version of the same app that passes the minimum reviews limit. Empty for the oldest version. | 2026.37.0 |
previousAvgRating | number | Mean star rating of the previous version. | 1.9 |
ratingDelta | number | Average rating of this version minus that of the previous version, in stars. Empty for the oldest version. | 0.03 |
ratingDeltaMargin | number | Half-width of the 95 % confidence interval of the rating change, in stars (plus or minus). A change smaller than this margin cannot be told from chance. Empty for the oldest version. | 0.27 |
previousLowStarShare | number | Share of 1-2 star reviews on the previous version, from 0 to 1. | 0.712 |
lowStarShareDelta | number | Share of 1-2 star reviews on this version minus that of the previous version. | 0.019 |
deltaSignificant | boolean | True when the rating change is statistically significant at 95 % confidence (Welch t test on the star ratings), false when it is not. Empty for the oldest version. | false |
trend | string | regressed or improved when the rating changed by at least 0.15 stars and the change is significant. stable when the change is under 0.15 stars and the 95 % margin is at most 0.3 stars. inconclusive when the sample is too small to tell (a visible change that is not significant, or a margin wider than 0.3 stars): read ratingDelta together with ratingDeltaMargin. baseline for the oldest version that has no predecessor. | inconclusive |
newComplaintTerms | array | Words and two-word phrases that are significantly more frequent in the 1-2 star reviews of this version than in the 1-2 star reviews of all older versions (one-sided two-proportion test, z of at least 3, at least 3 % of the version's low-star reviews, at least twice the older frequency, at least 4 reviews). Empty array: the comparison was made and no term stands out. Null: no comparison is possible (oldest version, or fewer than 12 low-star reviews in the version or in the older versions together), which is the usual case with 500 reviews per app; it fills mainly on Google Play reads of 5000 reviews. See newComplaintTermStats for the numbers and lowStarTopTerms for the most frequent low-star terms of every version. | ["login","crash on startup"] |
newComplaintTermStats | array | For each term in newComplaintTerms: lowStarReviews (1-2 star reviews of this version containing it), versionShare (their share of the version's 1-2 star reviews), olderShare (the same share in the older versions) and lift (versionShare divided by olderShare, the older share never taken below 1 % or below one review of the older pool). Null when newComplaintTerms is null. | [{"term":"login","lowStarReviews":104,"versio |
lowStarTopTerms | array | The five most frequent words and phrases in the 1-2 star reviews of this version, new or not, with the same numbers as newComplaintTermStats (olderShare and lift are null for the oldest version and whenever all older versions together hold fewer than 12 low-star reviews; lift is the version share divided by the older share, which is never taken below 1 % or below one review of the older pool). Where a lift exists, a value near 1 means the term is about as frequent as before and a high value that it is more frequent in this release. Null when the version has fewer than 8 low-star reviews; an empty array when it has 8 or more but no word or phrase occurs in at least 4 of them. | [{"term":"account","lowStarReviews":103,"vers |
lowStarTopTermsText | string | The same five terms as lowStarTopTerms as one readable string, with the lift against older versions in brackets where one exists (for example "log (lift 11.1), working (lift 4.2), login"). Null when lowStarTopTerms is null or empty. This is the column of the Version impact view. | account (lift 3.8), login, update |
sampleLowStarReviews | array | Up to three 1-2 star review texts from this version, preferring those that contain the new complaint terms, then the most helpful, cut at 280 characters. No author names. | ["App crashes on startup since the last update."] |
countries | array | Storefront countries whose reviews were merged into this row (App Store), or the Google Play country that was read. On a status row of an app that could not be read: the storefronts that were looked up. | ["us"] |
sourceUrl | string | Store page of the app. Also on the status row of an app that could not be read, whenever the store lookup answered. | https://apps.apple.com/us/app/id1064216828 |
scrapedAt | string | ISO 8601 timestamp of the run. | 2026-10-01T08:00:00.000Z |
found | boolean | False only on status rows, which explain why an app produced no version rows. Status rows are not billed. | true |
note | string | Reason for a status row. On a version row it is empty, or it warns that part of the requested reviews was not served by the store or that only some of the reviews carry an app version (counted per storefront; a storefront below the threshold, 20 % for Google Play and 90 % for the App Store, is named as left out of the statistics). | No app version has at least 10 reviews (read 120 reviews across 14 versions). Lo |
{
"store": "app-store",
"appId": "1064216828",
"appName": "Reddit",
"version": "2026.38.0",
"versionRank": 2,
"reviewCount": 108,
"avgRating": 1.93,
"stars1Count": 70,
"stars2Count": 9,
"stars3Count": 6,
"stars4Count": 4,
"stars5Count": 19,
"lowStarCount": 79,
"lowStarShare": 0.731,
"firstReviewAt": "2026-09-22T14:03:11.000Z",
"lastReviewAt": "2026-09-26T21:40:02.000Z",
"previousVersion": "2026.37.0",
"previousAvgRating": 1.9,
"ratingDelta": 0.03,
"ratingDeltaMargin": 0.27,
"previousLowStarShare": 0.712,
"lowStarShareDelta": 0.019,
"deltaSignificant": false,
"trend": "inconclusive",
"newComplaintTerms": [
"login",
"crash on startup"
],
"newComplaintTermStats": [
{
"term": "login",
"lowStarReviews": 104,
"versionShare": 0.15,
"olderShare": 0.022,
"lift": 6.5
}
],
"lowStarTopTerms": [
{
"term": "account",
"lowStarReviews": 103,
"versionShare": 0.149,
"olderShare": 0.039,
"lift": 3.8
}
],
"lowStarTopTermsText": "account (lift 3.8), login, update",
"sampleLowStarReviews": [
"App crashes on startup since the last update."
],
"countries": [
"us"
],
"sourceUrl": "https://apps.apple.com/us/app/id1064216828",
"scrapedAt": "2026-10-01T08:00:00.000Z",
"found": true,
"note": "No app version has at least 10 reviews (read 120 reviews across 14 versions). Lower minReviewsPerVersion or raise maxReviewsPerApp."
}
| Field | Name | Type | What it does |
|---|---|---|---|
apps required | Apps | array | App Store IDs (digits, e.g. 1064216828), apps.apple.com URLs, Google Play package names (e.g. com.reddit.frontpage) or play.google.com URLs. Mix both stores freely; every app is analyzed separately within its own store. |
countries | App Store countries | array | Two-letter App Store storefronts to read for App Store apps (us, gb, de, ...; uk is read as gb). A code the App Store does not know is reported and skipped. Reviews of all selected countries are merged into one row per version. The App Store feed exposes at most the 500 newest reviews per country and often only the first 50. Google Play apps ignore this field. |
googlePlayCountry | Google Play country | string | Two-letter Google Play country used when reading Google Play apps. |
googlePlayLanguage | Google Play language | string | Language code of the Google Play page (en, de, pt-br, ...). It sets the language of the interface, not a filter on review language. |
maxReviewsPerApp | Reviews to read per app | integer | Newest reviews to read per app. For App Store apps this limit applies per country; the feed pages 50 reviews at a time up to 500 and often stops after the first page. Google Play allows up to 5000, and more reviews reach further back in version history there; the strict newComplaintTerms list fills mainly at 2000 to 5000. |
minReviewsPerVersion | Minimum reviews per version | integer | Versions with fewer reviews are left out, because averages over a handful of reviews are noise. Rating changes are always compared with the nearest older version that passes this limit. |
latestVersions | Only the latest N versions | integer | Keep only the N newest versions of each app that pass the minimum reviews limit. Zero keeps all of them. |
onlyRegressions | Only versions that got worse | boolean | Return only versions whose average rating dropped by at least 0.15 stars against the previous version and the drop is statistically significant (95 % confidence). |
maxItems | Maximum version rows | integer | Stop after this many version rows in total across all apps. |
proxyConfiguration | Proxy configuration | object | Apify Proxy is on by default. The App Store review feed sometimes answers with an empty page; the Actor retries an empty first page up to six times, gives every app a budget of at most 75 seconds for everything that goes wrong (pauses after empty pages, waits before retrying HTTP 429 or server errors and the time spent in attempts that failed; each attempt is also cut off after 25 seconds), limited to an equal share of the 150 seconds of the whole run and never below 9 seconds so that apps that were not touched yet can still be read, and gives an app it cannot read within its budget a free status row. If the proxy cannot be created the run continues without it. |
Run the Actor and get the results in one request. Replace YOUR_APIFY_TOKEN with the token from your Apify account settings.
curl -X POST "https://api.apify.com/v2/acts/datagrit~app-release-review-impact/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"apps":["com.spotify.music","com.reddit.frontpage","com.whatsapp","com.instagram.android","com.netflix.mediaclient","com.duolingo"],"countries":["us"],"maxReviewsPerApp":500,"minReviewsPerVersion":10,"maxItems":200}'import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('datagrit/app-release-review-impact').call({
"apps": [
"com.spotify.music",
"com.reddit.frontpage",
"com.whatsapp",
"com.instagram.android",
"com.netflix.mediaclient",
"com.duolingo"
],
"countries": [
"us"
],
"maxReviewsPerApp": 500,
"minReviewsPerVersion": 10,
"maxItems": 200
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]);Install with npm i apify-client.
from apify_client import ApifyClient
import os
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("datagrit/app-release-review-impact").call(run_input={
"apps": [
"com.spotify.music",
"com.reddit.frontpage",
"com.whatsapp",
"com.instagram.android",
"com.netflix.mediaclient",
"com.duolingo"
],
"countries": [
"us"
],
"maxReviewsPerApp": 500,
"minReviewsPerVersion": 10,
"maxItems": 200
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
print(len(items), items[0] if items else None)Install with pip install apify-client.
The Actor reads public review data from the same public endpoints the stores use for their own pages and the App Store review feed. It collects no data behind a login and does not store reviewer names.
Up to 5000 per Google Play app. The App Store feed allows up to 500 per country, but it often serves only the first 50, so add countries or run again for more.
Weekly or after each release. A scheduled run shows the newest version as soon as it has enough reviews.
It has fewer reviews than the minimum, or it is older than the reviews that the store made available in this run.
Then the trend is `inconclusive`: with this few reviews the drop cannot be told from chance (see `ratingDeltaMargin`). `stable` is used only when the change is under 0.15 stars and the sample is large enough to rule out a bigger one.
Open an issue on the Actor page with the input you used and the run link.
Find EU public contracts approaching expiry from TED award notices: incumbent, buyer, value, end date and renewal options.
French company lead lists from Sirene screened by net result and revenue, with net margin, size, matching establishment and optional directors.
Newly registered Polish companies, foundations and associations from the official KRS court register: NIP, address, PKD, capital, email, with filters and change detection.
Ashby job postings with normalized annual salary ranges, equity flags and new-since-last-run detection.