Skip to content

feat(CFTL-489): fail-on-missing-permissions option for Meta extractors (SUPPORT-15700)#58

Open
matyas-jirat-keboola wants to merge 5 commits into
mainfrom
matyasjirat-cftl-489-support-15700-facebook-ads-v2-no-error-returned
Open

feat(CFTL-489): fail-on-missing-permissions option for Meta extractors (SUPPORT-15700)#58
matyas-jirat-keboola wants to merge 5 commits into
mainfrom
matyasjirat-cftl-489-support-15700-facebook-ads-v2-no-error-returned

Conversation

@matyas-jirat-keboola

@matyas-jirat-keboola matyas-jirat-keboola commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Problem

The Meta extractors (keboola.ex-facebook-pages, keboola.ex-facebook-ads-v2, keboola.ex-instagram-v2 — all built from this one codebase) can finish a job with Success / exit 0 while extracting nothing beyond the accounts table.

/me/accounts does not require ads_read / ads_management, so it always succeeds. When the token has lost per-account permissions (or expired), every other query fails per-account with a Facebook OAuth error (codes 190 / 200 / 10, or 100 / subcode 33 "missing permissions"). Those errors are currently swallowed in two layers:

  1. FacebookErrorHandler.is_recoverable_error() treats the 100/33 case as recoverable → returns empty data.
  2. The per-account except Exception: ... continue loops in client.py log-and-skip non-recoverable errors (e.g. code 200).

Result: the job reports Success and the missing data goes unnoticed.

Change

Adds an opt-in config option parameters.fail-on-missing-permissions (boolean, default false → existing configs behave exactly as before).

When enabled, the extractor collects every per-account authorization error during the run and then fails once at the end with a UserException (exit 1) listing every affected account — instead of silently producing empty/partial output.

Design decisions

  • Per-account detection (not just an upfront token check) — the reported case is a valid token missing per-account grants, which a token-validity check would not catch.
  • Collect all, fail at the end (not fail-fast). A non-zero exit makes the platform discard all output, so no partial data is committed regardless of when we raise — collecting first just yields a complete list of affected accounts at zero data-integrity cost.
  • Applies to all three deployed components (shared codebase) and shows in the shared ex-facebook UI for all of them.

Files

  • configuration.py — new fail_on_missing_permissions field (alias fail-on-missing-permissions).
  • page_loader.pyAuthorizationError sentinel (not a UserException, so it doesn't abort on first hit) + FacebookErrorHandler.is_authorization_error / authorization_error_details; the flag is threaded into PageLoader, and each HTTP error boundary (_load_regular_page, load_page_from_url, start_async_insights_job, async final-results) raises AuthorizationError before the recoverable check.
  • client.pyFacebookClient collects per-account errors in permission_errors; raise_for_permission_errors() raises one deduped UserException at the end. Per-account loops record-and-continue (the page-token → user-token fallback is preserved); the batch path records too. except UserException: raise is kept for genuine immediate failures.
  • component.py — passes the flag into the client and calls raise_for_permission_errors() after all queries are processed.
  • tests/test_fail_on_missing_permissions.py — detection, page_loader boundary (AuthorizationError), and client collect-then-fail (13 tests).

Testing

  • uv run pytest tests/test_fail_on_missing_permissions.py → 13 passed.
  • Full suite: all unit + cassette-backed functional tests pass (39 passed). The 9 *_keboola_accounts_v2x functional cases fail locally because those fixture dirs are empty/untracked stale local artifacts — they don't exist in CI and are unrelated to this change.

UI

The matching checkbox ("Fail the job on authorization errors") is added in the keboola/ui monorepo (shared ex-facebook module): keboola/ui#6306.

Refs: CFTL-489, SUPPORT-15700

Meta extractors (facebook-pages, facebook-ads-v2, instagram-v2) could finish
with Success while extracting only the accounts table: per-account Facebook
OAuth errors (codes 190/200/10 and 100/33 "missing permissions") were swallowed
in both the page_loader recoverable-error path and the per-account
log-and-continue loops in client.py.

Add an opt-in config option `fail-on-missing-permissions` (default false, so
existing configs are unchanged). When enabled, the first authorization error
raised during extraction fails the job with a UserException (exit 1) instead of
silently producing empty/partial output.

- configuration.py: new fail_on_missing_permissions field
- page_loader.py: FacebookErrorHandler.is_authorization_error /
  raise_if_authorization_error; flag threaded into PageLoader and applied at
  every HTTP error boundary before the recoverable check
- client.py: flag threaded into FacebookClient + PageLoader; per-account loops
  re-raise UserException instead of swallowing it
- tests: unit coverage for detection, page_loader boundary, client propagation
@linear

linear Bot commented Jun 10, 2026

Copy link
Copy Markdown

CFTL-489

Switch the fail-on-missing-permissions behavior from fail-fast to collect-all:
attempt every account/query, accumulate authorization errors, then fail once
with a UserException listing every affected account. Safe because a non-zero
exit makes the platform discard all output, so no partial data is committed
regardless of when we raise — this just yields a complete error message.

- page_loader.py: add AuthorizationError sentinel (not a UserException, so it
  doesn't abort on first hit) + authorization_error_details(); each HTTP
  boundary raises it when the option is on
- client.py: FacebookClient collects per-account errors in permission_errors;
  raise_for_permission_errors() raises one deduped UserException at the end;
  per-account loops record-and-continue (page-token/user-token fallback honored)
- component.py: call raise_for_permission_errors() after queries are processed
- tests: cover detection, page_loader boundary (AuthorizationError), and
  client collect-then-fail (13 tests)
…support-15700-facebook-ads-v2-no-error-returned

# Conflicts:
#	src/client.py
#	src/component.py
…t loops

The merge with #57 surfaced a regression: the `except UserException: raise`
clauses added to the per-account load/start loops also propagated pre-existing
user-actionable errors (e.g. INVALID_METRIC from raise_if_user_actionable),
which main deliberately swallows per-account (skip the query, job continues).
This broke #57's instagram_account_v24/v25 cassette tests.

CFTL-489 only needs AuthorizationError collection, not a change to how other
errors are handled. Remove the over-reaching `except UserException: raise` from
_start_async_jobs_for_query, _process_single_sync_query, and its user-token
fallback (the poll handler keeps the same effective behavior as #57, where
UserException propagates by not being in _CONTAINED_OBJECT_ERRORS).

@keboola-pr-reviewer-bot keboola-pr-reviewer-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: needs_human (risk 3/5) · profile component-factory

This is a well-designed opt-in feature with default=False that preserves all existing behavior, but it modifies exception-handling boundaries across multiple async and sync extraction paths in…

Concerns:

  • src/client.py: _poll_and_process_async_jobs exception order change (UserException→AuthorizationError→contained) in async polling path needs verification
  • src/client.py: Batch path (code 406+) records AuthorizationError with account_id=None; dedupe key falls back to message string, potentially under-reporting
  • src/page_loader.py: load_page_from_url raises AuthorizationError with account_id=None; caller in client.py has no fill-in for the missing account_id

Suggested reviewers: @keboola/component-factory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants