# Changelog of FDA & EMA Drug Safety Recall Monitor (`stefano_seggio/fda-ema-drug-recall-monitor`) Actor

- **URL**: https://apify.com/stefano\_seggio/fda-ema-drug-recall-monitor/changelog.md
- **Full Actor documentation**: https://apify.com/stefano\_seggio/fda-ema-drug-recall-monitor.md

## Changelog

### 2.0.0 - 2026-09-08

#### Fixed

- **Critical: HTTP 429 (rate-limited) responses were never retried.** `src/http.ts`'s retry logic excluded ALL 4xx status codes from its retry path, including 429 - meaning a rate-limited request from either openFDA or EMA failed immediately instead of backing off and trying again, exactly the case retries exist for. Fixed: 429 and 5xx are now explicitly retried with exponential backoff plus jitter (to avoid a thundering-herd retry if this actor is ever run concurrently across schedules); other 4xx (400/404/etc, genuine permanent client errors) are still correctly NOT retried. A `Retry-After` header (either the seconds form or an HTTP-date, per RFC 9110), when the server sends one on a 429/503, is now honored as the authoritative delay instead of the computed backoff. Covered by 6 new tests in `test/http.test.ts` that mock real 429/503/400/network-failure scenarios, including one asserting the exact `Retry-After` value is what's actually scheduled.
- Defensive fix in `src/dateUtils.ts`'s `isWithinDateRange`: added the same `diffMs >= 0` guard already proven necessary on this fleet's `mendoza-compras-monitor` and `pba-tenders-monitor` (a naive one-sided check lets any future-dated record match every window). Not a live-observed bug here - EMA `dissemination_date` is always a past publication date in practice - but the same bug class, fixed proactively rather than left for a future data shape to trigger silently.

#### Added

- Real cross-run delta classification, replacing a flat seen-id list. Every record now carries a dual content-fingerprint pair (`src/fingerprint.ts`: a `statusFingerprint` over just the status/regulatory-outcome field, and a `contentFingerprint` over the record's other mutable fields), persisted per-record in a named key-value store (`src/state.ts` - was already correctly using `Actor.openKeyValueStore()`, not the run-scoped `Actor.getValue()`/`setValue()` bug found on a sibling actor today, so no state-store migration risk here).
- `src/delta.ts`'s `classifyFdaRecord()`/`classifyEmaRecord()`: first-seen keeps this actor's existing, deliberate per-source naming (`SANCTION` for FDA, `NEW_LISTING` for EMA - preserved from v1, not replaced with a generic term); a repeat sighting is now `STATUS_CHANGE` (FDA `status` or EMA `regulatory_outcome` differs from last time), `UPDATED` (some other tracked field differs), or `SNAPSHOT_NO_DIFF` (identical).
- `schemas.ts`'s `UmsEventTypeSchema` enum updated to match what's actually assignable: added `STATUS_CHANGE` (now real), removed `AWARD_VARIATION` (a tender/contract-award concept copied from the shared fleet type file but never applicable to drug-safety data).
- New "Change detection" dataset view (`recordSource`/`record_id`/`event_type`/`status_or_estado`/`scraped_at`/`is_new`).
- `LICENSE` (Apache-2.0, matching the fleet standard), `.github/workflows/test.yaml` (lint+build+test CI - this actor had no CI pipeline of any kind before this release), `eslint.config.mjs` (also entirely missing before - `npm run lint` was defined but had never actually been runnable), `.gitignore`, this `CHANGELOG.md`, `AGENTS.md`.
- `test/delta.test.ts`, `test/fingerprint.test.ts`, `test/state.test.ts`, `test/http.test.ts`: full unit coverage of the new classification/fingerprint/state-persistence/retry logic - previously the actual delta logic (then just a flat `seenIds` check) had zero dedicated tests, only the pure UMS-mapping layer was tested.

#### Changed

- **Behavior change to the `onlyNew` input flag, disclosed here rather than silently shipped:** `onlyNew` previously meant "only deliver records never seen in a prior run" (a plain seen-id check). It now means "only deliver records that are new OR have changed since the last run" - i.e. it also delivers `STATUS_CHANGE` and `UPDATED` records, not only first-seen ones. This is a deliberate upgrade, not an accidental behavior shift: the entire point of adding richer classification was so that, for example, a recall flipping from `Ongoing` to `Terminated` is still surfaced to a recurring monitor, instead of being silently suppressed just because the recall record itself "was seen before." Existing scheduled tasks using `onlyNew: true` will see more records delivered than before whenever a status or content change occurs on an already-known record - this is the intended, improved behavior, not a bug.
- State shape is now `{ entries: Record<source, Record<recordId, { statusFingerprint, contentFingerprint, lastSeenAt }>>, lastRunAt }`, replacing v1's `{ seenIds: Record<source, string[]>, lastRunAt }`. `state.ts`'s `isValidState()` treats the old shape (or anything else unexpected) as absent rather than attempting a migration - an existing scheduled task's next run simply re-baselines against the richer shape, exactly the same non-backward-compatible-by-design pattern already used across this fleet's other v1-to-v2 migrations.

#### Not added (and why)

- **No `CLOSED` event.** Neither source is confirmed, by this actor's own live-verification standard, to ever remove a historical record - openFDA is a permanent enforcement database (a Terminated recall stays queryable, it doesn't disappear) and EMA DHPCs are permanent regulator communications. Independent of that, this actor fetches a bounded, newest-first top-N window per source (`maxItemsPerSource`, default 100) rather than exhaustively walking either source's full historical register each run - a trustworthy `CLOSED`/complete-census check requires exactly that kind of exhaustive walk, which this actor's bounded recency-window design does not do and isn't meant to do.
- **No pricing/monetization change.** The live-configured PPE price ($0.001/record) was already correct and is untouched by this release.
- **No dual-floor/baseline-floor pagination-limiting mechanism**, despite that being part of the fleet's registry-monitoring V2 pattern. That mechanism exists to avoid draining a large historical backlog cap-by-cap across many runs when establishing a cold-start baseline. It doesn't apply here: both sources are always fetched as a single bounded, newest-first top-N call (never a paginated walk of the full backlog), so there is no unbounded-backlog problem for a floor to solve - the bounded fetch is naturally its own floor.
