# Changelog of Tennis Abstract Scraper | Match History API (`danthedataman/tennis-abstract-data-api`) Actor

- **URL**: https://apify.com/danthedataman/tennis-abstract-data-api/changelog.md
- **Full Actor documentation**: https://apify.com/danthedataman/tennis-abstract-data-api.md

## Changelog

- 2026-10-08: Trim the public README to its buyer facts in a docs-only rebuild of the deployed 0.1.4 file set; preserve runtime, schemas, prefills, pricing and listing.
- 2026-10-07: Refresh public listing and README copy in a docs-only rebuild; preserve runtime, schemas, prefills and pricing.

### 0.2

Adds a tournaments mode: every result table of a Tennis Abstract tournament
page as flat dataset rows, beside the unchanged players mode.

- WTA biography rows cite the fetched shortlist JS file supplying their bio;
  match rows and career-only null-biography rows keep their classic-page links
  (item 20, #5).
- Empty players input and invalid run-level tour make no source request,
  including no warm-up, with cap/span validation precedence preserved
  (item 21, #6).
- Transport exhaustion performs no final sleep, rotation callback or warm-up.
  A later call on the reused client opens and warms a fresh session before
  its first attempt; attempt budgets and terminal errors are unchanged (item 24).
- The buyer README starts from the reviewed 0.1.5 text and adds the measured
  tournament rows, caps and errors, with the #5/#6 claims updated (item 23).

#### What it does

- **A `mode` input**, default `players`. An input without `mode` runs exactly
  as in 0.1: same rows, same defaults, same inputs read. `tournaments` switches
  the run to tournament pages, and the players-mode inputs are then not read.
- **One tournament page per entry in `tournaments`**, written as the page's own
  `t=` value (`2024-540/Wimbledon`) or as a pasted
  `https://www.tennisabstract.com/cgi-bin/tourney.cgi?t=…` URL. The value is
  kept exactly as given — `M25-Ajaccio+H` keeps its `+` — and a value without
  its `/Name` part is refused rather than guessed. Repeated entries are fetched
  and billed once.
- **Every result table of the page, in page order.** The main draw and the
  qualifying draw become `recordType: "tournamentMatch"` rows — round, winner
  and loser with their site player ids, seeds, entry codes, countries and
  ranks, score, and each side's printed serve rates and break points saved and
  faced. The other six tables (seed outcomes, this event's finals, surface
  speed, stat summaries, winners and errors, other events' finals that week)
  become `recordType: "tournamentTable"` rows, each carrying its table's own
  header beside its cell values. On the measured 2024 Wimbledon page that is
  127 main-draw rows first and 467 rows in all.
- **A `maxRowsPerTarget` cap**, default 500, `0` for every row, applied per
  page in page order. The buyer's own spending limit stops the run between
  tournaments and writes a `charge-limit` entry naming the tournaments not
  fetched, as it does between players.

#### Correctness choices worth knowing

- **Rates ship as the page prints them.** The `singles-results` and
  `qual-results` match tables print percentages and dominance ratio as text
  and publish none of the counts behind them except break points saved and
  faced. Match rates stay strings (`"5.3%"`) and the break-point pair is split
  into two integers; no rate is derived. Generic `stat-summaries` rows retain
  published counts (`Pts`, `A`, `DF`, `BPFaced`, `BPEarned`) as printed text.
- **Tournament years are checked before billing.** The requested year is
  compared with the published title and visible event heading when present.
  A contradiction or unreadable title year is `unreadable-page`, with zero
  rows from that target. An absent site title, including a commented-only
  title, is `blocked`. No event-name or event-id derivation is claimed.
- **Table selection uses actual HTML attributes.** The table's `id` selects
  it regardless of quote style, attribute order or whitespace; `data-id` does
  not. Classification also uses the actual title and main-table `id`.
  Comments contribute no identity, table, row or cell text, and cannot consume
  the row cap. Quoted `>` in attributes does not leak into values or split cells.
- **Malformed pasted URLs are per-entry errors.** URL parsing failures are
  recorded as `unsupported-target`; later valid tournament entries still run.
- **Drift is refused, not guessed.** A main-draw or qualifying header that
  differs from the measured one, a row whose width differs from its header's,
  a player cell or rank or break-point cell outside its measured shape: the
  page is reported as `unreadable-page` and no row is taken from it.
- **A live source fault is one error, not a retry.** A current event can answer
  with the site's own Python traceback instead of the page; that is recorded
  once as `source-error` in `ERRORS`, with no row and no second request.
- **No tournaments is an empty result.** A tournaments run with no entries
  fetches nothing and is not back-filled. An unknown `mode` or an unusable
  `maxRowsPerTarget` ends as an empty result with one `unsupported-target`
  entry in `ERRORS`, like the 0.1 input errors.

#### Not part of this release

- The page's "Extreme Performances" block (prose, not a table) and its event
  header (name, date, surface and draw size) are not exported.
- The WTA tournament page is not read: it was never measured as a complete
  link. The per-player charting tables and the rankings and Elo reports remain
  planned.

### 0.1 — 2026-09-11

First release: ATP and WTA career match logs from Tennis Abstract as flat typed
dataset rows.

#### What it does

- **One row per match**, with date, tournament, surface, level, round, score,
  opponent, and the raw per-match serve and return counts the source stores —
  51 columns on an ATP row, 48 on a WTA row. Absent values are `null`, never
  `0`, and the serve/return block blanks as a whole when the source recorded no
  statistics for that match.
- **A whole career in one request.** The ATP career page embeds the full match
  array, so a career is a single HTTPS GET — 1,516 matches for Novak Djokovic,
  2003-01-06 to 2026-08-31, on the measured capture. A WTA career is assembled
  the way the source's own page assembles it, from the shortlist file plus the
  older-history file: 130 + 418 = 548 rows for Iga Swiatek.
- **Many players per run**, addressed as bare slugs, as pasted player-page URLs
  in either `p=` form, or through the discovery tokens `@elo-top:N`,
  `@leaders:1-50` and `@wta-leaders:1-50`, which resolve names through the
  slug rule the source's own JavaScript publishes.
- **Filters over rows already fetched** — season, surface, level, round,
  tournament, result and head-to-head opponent — so a head-to-head costs the
  same as the career it came from, and no filter value is ever sent to the
  source as a request parameter.
- **An opt-in player record row** (`includePlayerRecord`) carrying current and
  peak rank, birth date, hand, backhand, country, active flag, Twitter handle,
  and the match total the source served for that player.
- **Two run ceilings**: `maxMatchesPerPlayer` (default 100, `0` for all) and
  `maxPlayers` (default 50, maximum 200, applied after discovery so a token
  cannot fan out past it; a higher value is clamped with a warning, not
  refused). A run also stops when the buyer's own Apify spending limit is
  reached, writing a `charge-limit` entry to `ERRORS` naming the targets it did
  not fetch.

#### Correctness choices worth knowing

- **Column names come from the page**, not from a hard-coded list: rows are
  mapped through the header array the source publishes, so an inserted column
  cannot shift the data.
- **Identity is checked against the page's own published name.** The source
  answers `200` for a slug it does not have and serves a different player's
  page; such a page is reported as `wrong-player` and no row is taken from it.
  Where a surface publishes no name of its own, the rows are still delivered
  and the target carries an `identity-unchecked` **note** — a note beside
  delivered rows, never a failure.
- **Failures never enter the dataset.** Every per-target failure goes to the
  run's `ERRORS` key-value record with four keys (`target`, `slug`,
  `errorClass`, `message`); the class vocabulary is ten values, eight
  page-level and two run-level.
- **Empty results exit cleanly.** No players, filters matching nothing, or an
  input value the Actor cannot act on: the run succeeds with an empty dataset,
  one `ERRORS` entry where there is something to say, and guidance in the log —
  never a traceback and never a failed run.
- **No derived statistic is emitted.** The source's displayed rates are
  computed at render time; the README gives the formulas so they can be derived
  from the raw counts instead of guessed.
- **Raw counts only, no doubles.** The measured doubles history file answers
  `404` and the career page never fills its doubles array, so no doubles switch
  is offered rather than one that silently does nothing.

#### Not part of this release

- Store listing craft — icon, published example tasks and Store SEO fields — is
  the operator's pre-publication step, not something this release performs.
- Tournament draws, the per-player charting tables and the rankings/Elo reports
  are planned for 0.2; charted point logs and draw forecasts for 0.3. Both row
  families ship into the same dataset, which is why every row already carries
  `recordType`.
- The leaders files are read for names only. Their bulk match arrays are
  deferred to 0.3 as a mode of their own, so no match datum is lost — the same
  matches ship per player through `@leaders:` and the career route.
- The site's recent-results fragment, `/jsfrags/`, is excluded as a route
  rather than as data: every row it carries is already on the allowed career
  page, and the hrefs it adds are on allowed pages too.
