# Changelog of EU Safety Gate API (`sourcing-data-studio/eu-safety-gate-api`) Actor

- **URL**: https://apify.com/sourcing-data-studio/eu-safety-gate-api/changelog.md
- **Full Actor documentation**: https://apify.com/sourcing-data-studio/eu-safety-gate-api.md

## Changelog

### 0.1.7 (2026-10-10)

- A run that ended before it had read every weekly report could look like a complete one. On 2026-10-09 the Shopify Products API of this studio showed that the crawler (the part that reads the pages) can return while a request that its last handler had just added is still waiting. That request is then never read, and the run still ended as succeeded with a wrong message. It showed only when several runs were started at the same moment. The cause is not known. It was seen on that Actor and not on this one. This Actor has the same kind of chain: each weekly report is queued when the one before it is done. From its code, a run that ended this way would have ended as succeeded with "Saved N alerts from k weekly reports" and no word about the reports that were left. So the same check is added here.
- When the crawler returns while a request is still waiting and nothing stopped the run (your **Maximum alerts**, your **Maximum cost per run**, the time limit of the run, an abort, or a source that asked to stop), the Actor now starts the crawler again for it, up to three times, each time after a longer wait (2, 4 and 8 seconds). Each start is logged as `run.pending`. The run summary counts the starts as `totals.extraPasses` and the requests that were still left at the end as `totals.requestsLeft`. With a proxy the Actor does not start the crawler again: a new start could use another proxy address, and the Actor does not move a run to another address by itself. There it only reports the waiting requests. The limit of 30 requests a minute is counted for each start of the crawler, so a new start does not count the requests of the one before it.
- Run summary: new field `scan.reportsNotRead`, the weekly reports that were planned but not read, although nothing stopped the run and no request for them failed. It is 0 for a run that was stopped on purpose and for a run that was restarted. Another new field, `restarted`, is `true` when Apify restarted the run. The counts in the record then cover only the part of the run after the restart.
- Status messages. A run that saved alerts and did not read every planned weekly report still ends as succeeded. Its message now says how many weekly reports were not read, that alerts from those weeks may be missing, and that a new run reads every week again and charges for every alert it saves. A run that saved nothing and did not read every planned weekly report ends as failed, says how many were not read, and says that nothing was charged for alerts. Before, such a run ended as succeeded without a word about the reports that were left, or as failed with "the weekly reports could not be read" although no request had failed. The message "No alerts matched your filters" is still used only when every planned weekly report was read and no alert matched. A run that was stopped on purpose has the same messages as before and says nothing about reports that were not read. With a proxy, the message also says that the run did not start again by itself, because a new start could use another proxy address.
- A run that the platform restarted says only what the Actor knows. The Actor keeps no list of weekly reports and no counts across a restart, so after a restart it cannot tell which reports were read before it, and it cannot go on to the weekly reports that were still to be read. When the list had been read before the restart, the run reads at most the weekly report that was already waiting, and then it ends. This was so in 0.1.6 as well, and its message did not say it. The message now says that the run was restarted. When the run had no list of weekly reports and nothing stopped it, the message also says how many weekly reports it read after the restart, and that older weekly reports may not have been read. It recognises a restart when the request queue of the run already holds requests when the new process starts, or, with pay per event, when alerts were already saved. No report is then counted as not read, and the run never ends as failed because no alerts were saved, since alerts may have been saved before the restart. With pay per event the message gives the number of alerts the run has saved in all, before and after the restart: "Saved 12 alerts in all, including the alerts saved before the run was restarted." When that number is 0, it says "No alerts saved, before or after the run was restarted." Without pay per event no count is known. The message then says that the run was restarted and that the alerts saved before the restart are in the dataset. It never says "Nothing was charged for alerts" or "No alerts matched your filters" then, and it gives no advice to run it again, which would save and charge the earlier alerts a second time. The early exit for a **Maximum cost per run** that cannot pay for one alert follows the same rule. Before, a restarted run could end as failed, and its message did not say that the run had been restarted.
- The crawler can now be started again, and the crawler library clears its own stop flag every time it starts, so a stop that came at that moment (the time limit of the run, an abort) would be forgotten. The list of reports and each weekly report therefore queue the next report only when the run is not stopping, and a run that is already stopping does not start the crawler at all. The Actor also opens the request queue first, then sets the time limit and the abort handler, and waits for nothing between that and the first start (new file `src/first-start.ts`). Before, a weekly report that was being read when the time limit came still queued the next one, and one request was left waiting.
- An aborted run keeps the summary that the abort wrote, also when the abort comes between two starts of the crawler. The Actor writes its final summary only when no abort came.
- README: the Limits section and the answer on 0 alerts now describe a run that ends before it has read every weekly report, a run that Apify restarts, the new summary fields, and what a new run charges.
- README: the `url` field opens the alert on the Safety Gate site for about 88 rows in 100 (3,487 of 3,977 rows in the 52 weekly reports to 9 October 2026), measured on 2026-10-09. The text said about nine rows in ten.
- No change to the input or to the rows in the dataset.

### 0.1.6 (2026-10-09)

- README: the Python example now works with version 3 of Apify's Python client, the version `pip install apify-client` installs today (it needs Python 3.11 or later). The example read the dataset id as `run["defaultDatasetId"]`. Client 3 returns a typed object, so that line stopped with `TypeError: 'Run' object is not subscriptable`, after the run had finished and been charged. It now reads `run.default_dataset_id`, and the line above it says `pip install "apify-client>=3"`.
- The faulty example was added on 2026-10-09 and had not been run before it was published. The corrected one was run against this Actor the same day with client 3.3.0.
- No change to the code, the input or the output.

### 0.1.5 (2026-10-09)

- README: the opening now states the price: what one saved row costs on the Free tier and what a typical query costs, from the same figures as the cost section. The sentence above the sample rows no longer names a "quick-test example".
- The short description on the Store now says what a row holds (product, risk, measures taken, picture links) and that only saved alerts are paid for.
- No source change.

### 0.1.4 (2026-10-09)

- README: the output example now comes right after the feature list; the API section shows a curl, a JavaScript and a Python example; the cost section adds the price of a typical query. No change to the code, the input or the output.

### 0.1.3 (2026-10-06)

- A run that saved nothing now ends as failed, with a message that names the cause, unless it read the weekly reports and no alert matched. Before, such a run ended as succeeded with "No alerts matched your filters" even when the scan had stopped early (several reports in a row could not be read, the source asked for a long pause or said that its request limit was used up, the run timeout was near), when a request had failed, when planned reports were left unread, or when every report parsed to zero alerts because the format of the source had changed. A genuine no-match (reports read, filters matched nothing) still succeeds.
- A weekly report that lists no alerts at all is a format error: it counts as a failed request (retried, then listed in the run summary), no longer as a report that was read and matched nothing. A run in which every alert that matched your filters failed the checks also fails now, and says so.
- No new address after a refusal (CLAUDE.md rule 4, "if a source blocks, fail gracefully"). A refusal never ends a session or starts a request from another address (with a proxy, a new session is a new address). The crawler keeps one session for the whole run: a pool of one session, and its limits for failed requests, uses and age are far above what a run reaches. Before, the library gave every request a session of its own and retired it on HTTP 401, 403 and 429, and the error handler retired it on every block.
- HTTP 401, 403 and 407, and a block or verification page, are final: the report is asked for once, listed as `blocked` in the run summary and never charged. Before, it was asked for again up to **Retries per request** times, each time with a new session (with a proxy: from another address). A proxy that refuses a tunnel with HTTP 407 is final too.
- HTTP 429 is asked for again after the pause the source asks for (the `Retry-After` header, up to two minutes; the back-off without it), from the same address. The header `x-ratelimit-remaining: 0`, or a `Retry-After` above two minutes, ends the scan: the report is not asked for again and no further report is asked for. The run summary then says `rate-limit` or `retry-after` in `scan.stopReason`. Before, a used-up request limit was not read at all.
- Run summary: `blockedAttempts` counts every refused attempt once, also when **Retries per request** is 0. The message of a failed proxy tunnel no longer carries the start of the proxy's answer.
- Input schema and README: no text says any more that a retry uses a new session, and both say that a proxy is never used to get around a refusal or a limit. The README has a new answer on blocked reports and request limits and a section for AI agents; the answer on 0 alerts says how a succeeded run differs from a failed one.
- Listing: new search-engine title and description and new search terms.

### 0.1 (2026-10-05)

- First version: EU Safety Gate API returns dangerous non-food product alerts from the newest weekly Safety Gate reports, filterable by keyword, country of origin and category.
- Keywords match at the start of words. Country codes such as CN are understood. The description is quoted as published; the seller fields of an alert are not collected. Recall links to social media are replaced by the alert page. No proxy is used by default. The scan stops when the source keeps failing.
