# Changelog of LinkedIn Jobs Scraper (`badr_xba/linkedin-jobs-scraper`) Actor

- **URL**: https://apify.com/badr_xba/linkedin-jobs-scraper/changelog.md
- **Full Actor documentation**: https://apify.com/badr_xba/linkedin-jobs-scraper.md

## Changelog

### 2026-11-09 (review invitation)

- Successful runs that saved at least one result end their status message with one short, optional invitation to leave a Store review or open an issue (never on failed or empty runs); README section "Feedback and reviews". Output and billing unchanged.

### 2026-11-08 (visual kit 1.1.0)

#### Added

- **Only new jobs** (`onlyNewJobsStore`, optional, empty by default = unchanged behaviour): IDs of saved jobs are
  kept in a named key-value store; later runs with the same name skip them (not billed).
  `RUN_SUMMARY.skippedSeenInEarlierRuns`.
- `postedAtRelative`: LinkedIn's own wording of the posting age ("4 days ago"), from the search card or job page.
- `errorCode` on every `FAILED_INPUTS` entry (`BLOCKED`, `JOB_GONE`, `NO_TITLE`, `INVALID_INPUT`,
  `UNEXPECTED_ERROR`).
- Visual report on kit 1.1.0: live progress (searches and job URLs done / planned), errors grouped by code and
  coded warnings, data-quality panel, final report saved as FAILED when the run crashes.

### 2026-11-08

#### Changed

- *Companies* and *Workplace and salary* views now open with the company logo (image column, LinkedIn's own
  `media.licdn.com` URL already in every row), like the *Overview* view.
- LinkedIn's generic grey "ghost" picture (`static.licdn.com/aero-v1/sc/h/...`, served for confidential
  employers) is no longer stored as `companyLogo` / `jobPosterPhoto`: those rows now have `null` (the report shows
  initials) instead of a placeholder that is not a real logo (1 of 15 rows in the 11-07 platform run).

#### Added

- **Visual report** (`REPORT` record, Output tab → *Visual report*): job cards (company logo or initials, title,
  company, location, posted date, employment type, seniority, workplace, salary when shown, applicants, recruiter,
  expandable description), key figures, top companies / top locations / posting-date charts, search, filters,
  sorting, table view and CSV/JSON export of the filtered rows. Built by the shared `src/visual_kit.py` (v1.0.1)
  with this Actor's `src/report.py`. At most 2,000 rows are kept in memory for it; it is never billed and a report
  error is only logged as a warning. Recruiter photos are marked as person images (blurred only in marketing
  screenshots with `#blur=people`, never in the customer's report).
- Output schema: *Visual report* link; key-value store schema: `REPORT` (text/html). *Overview* view: company logo
  column (image), job link and posted date formats; *Companies* view: link/number formats.

#### Fixed

- `companyLogo` and `jobPosterPhoto` keep only real http(s) links: LinkedIn's lazy-load placeholder (a `data:` URI)
  or a relative path now gives null instead of an unusable value, and `//host/…` becomes `https://host/…`. The
  lazy-load source is still preferred; valid links are unchanged.

#### Tests

- `tests/test_report.py`: 13 tests on 18 real rows (`tests/fixtures/report_run_items.json`: run AnLQMMEUVvw2gb5m2 +
  3 detail rows saved by the local build): image ownership, recruiter photo flagged, initials tile, invalid image
  links dropped, escaping, 5,000 synthetic rows (time, size, cap banner), JSON export = embedded data, report errors
  never raise, bounded buffer, and a whole offline run on the saved search page whose `REPORT` holds exactly the
  jobs pushed.

### 2026-11-07

#### Fixed

- A non-numeric "Requests at the same time" (`"maxConcurrency": "lots"`) crashed the run (exit 91). Text fields (`keywords`, `location`, `geoId`, `splitCountry`) sent as numbers or null no longer crash either; non-numeric number fields fall back to the default with a warning, and a non-numeric `distance` is no longer sent to LinkedIn.
- "Maximum jobs per search" is now exact: the last page of a search could add up to 9 extra jobs.
- A run started without `maxItems` (API call or local run that does not apply the schema defaults) had no ceiling; it now uses the schema default of 100 jobs. `0` still means no ceiling.
- HTTP 400/401/405/422 answers are no longer retried 5 times (they cannot succeed); 410 is treated like 404 (job gone).

#### Improved

- Result pages are requested only as far as the remaining quota can use: with 15 jobs to collect, 2 pages instead of 3.
- Jitter on the network-error back-off.
- Log, status and FAILED_INPUTS messages are in English (they were partly in French).
- Dataset schema now declares every output column (types, nullability, and which ones are only filled with job/company details or are often empty logged-out).
- `experienceLevel`, `jobType`, `workplaceType` have an explicit empty default.
- README: note that `jobFunction` is often empty, FAQ on the per-search ceiling.
- Offline test suite (`python -m pytest -q`) with saved LinkedIn pages; excluded from the Docker image.
