Skip to content

chore(sc-46274): retire the legacy fallback, park the experiments framework (phase 3) - #3579

Open
dcschreiber wants to merge 19 commits into
masterfrom
chore/sc-46274/phase-3-remove-legacy-fallback
Open

chore(sc-46274): retire the legacy fallback, park the experiments framework (phase 3)#3579
dcschreiber wants to merge 19 commits into
masterfrom
chore/sc-46274/phase-3-remove-legacy-fallback

Conversation

@dcschreiber

@dcschreiber dcschreiber commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

(Claude writing on Daniel's behalf)

Phase 3 of the Library Assistant opt-out rollout. The opt-out migration ran on production on Aug 10, so every profile now carries an explicit settings.library_assistant; this PR removes the legacy fallback that covered profiles without it. Replaces #3573 — per Akiva's review, the experiments framework (model, admin tooling, opt-in endpoint) stays in the codebase, parked for future experiments. Only the Library Assistant stops reading it.

⚠️ Prerequisites before this deploys (2026-08-18)

A review found that the invariant this PR rests on was false on master: SSO signups and lazily-created profiles never got settings.library_assistant. Removing the fallback would have turned the assistant off permanently for every new Google/Apple account. This was proven with a real SSO signup against the local databases, and re-proven fixed the same way after the fix. Details and evidence are in #3638.

In order:

  1. fix(library-assistant): write the settings key when a profile is created #3638 merges and deploys. It makes every profile-creation path write the key.
  2. The keyless profiles created since the Aug 10 migration get backfilled, as an ops step. The rule: whitelist-row value wins, no row means on.
  3. This PR deploys. Master is already merged into this branch; once fix(library-assistant): write the settings key when a profile is created #3638 lands, master gets merged in one more time so this branch carries the fix.

What changes

  • A profile without the setting key now reads as off. sefaria.helper.library_assistant no longer falls back to the experiments whitelist, and the in_chatbot_experiment prop is gone.

  • The one-time migration script is deleted (migrate_experiments_to_library_assistant.py) — it ran on production Aug 10 and is spent. Its catch-up capability moves into the wipe script, so a straggler profile can't strand Phase 4.

  • The promo banner shows to logged-out visitors only (product decision, 2026-08-11). Without this, removing the fallback would have aimed the "Try It" banner at exactly the logged-in users who had just turned the assistant off. The gate is now !Sefaria._uid, and the logged-in branch it makes unreachable — the Try It button, its join handler, and the chatbot_experiment_banner_dismissed cookie — is deleted.

  • Rename: ChatbotExperimentBannerLibraryAssistantPromoBanner. The sefaria/helper/library_assistant.py docstring records where the chatbot vs library_assistant naming line runs (vendor/wire contracts keep the chatbot name).

  • Phase 4 wipe script (scripts/migrations/wipe_experiments_data.py), designed as a one-run tool — --dry-run, then the real run, with the launch-day checks baked in rather than run by hand around it:

    • names the Mongo and Postgres databases it resolved before printing any count (a run pointed at the wrong database prints numbers that look perfectly consistent);
    • backfills any profile still missing the setting key (same decision logic and write path as the retired migration: row value wins, no row means on), then hard-verifies none remain — aborting before touching anything if one does;
    • archives both stores before destroying anything: every UserExperimentSettings row and every experiments: True profile go to db.experiments_data_archive, tagged source: "row" / "profile" (False values are the serialized-on-save default and record nothing a user chose). It also reports how many True profiles have no row behind them — deleted accounts are the one known source — instead of assuming the stores mirror;
    • deletes exactly the rows it archived, and unsets profiles.experiments;
    • closes with a verification block that re-reads both stores — not its own counters — and prints PASS/WARN per check, including a warning if a row landed mid-run.

    --dry-run performs zero writes; --force is removed (post-backfill, a still-missing key means a failed write, and overriding would destroy the only record of an opt-out). Phase 4 is that one script run — no schema change, the (empty) table stays.

  • The migration rollback script is deleted. Its guard made it inert the moment this PR merges. The archive collection remains as the raw record for manual repair, to be dropped after a soak window (~end of August).

  • The TEMPORARY markers on the surviving experiments plumbing are reworded as "parked framework" notes, since that code is no longer scheduled for removal.

Verification

  • Targeted pytest: 44/44 green at the branch tip; the broader failures reproduce identically on master (pre-existing, environmental).
  • The wipe script was rehearsed end to end against a local restored dump (248k profiles): the dry run's arithmetic verified against an independently taken pre-state snapshot; the real run verified by fresh reads, not the script's output — rows deleted, fields unset, archive contents exact per uid/value/source, and the opted-out user emerging with settings.library_assistant: False; then an idempotent re-run (full no-op), a late-row catch-up (archived and deleted under a new run_id), and a straggler backfill. All checks and all in-script PASS/WARN lines came out PASS. --dry-run separately confirmed write-free (before/after snapshots identical).
  • The opt-out browser suite (test(sc-46273): Library Assistant opt-out test infrastructure #3585) was merged in locally and run against this branch post-migration: 19/19, identical to its pre-migration result — the fallback removal is unobservable to users. (That suite has since dropped its promo-banner specs; the shared e2e harness suppresses banners by design, so the promo gate is asserted here, not in e2e.)
  • Not run on staging or a cauldron.

Merges independently of #3585, in either order.

🤖 Generated with Claude Code

@gitvelocity-reviewer

gitvelocity-reviewer Bot commented Aug 4, 2026

Copy link
Copy Markdown

📊 Code Quality Score: 33/100

Base Score 55 × ESF 0.6 = 33

Category Score Factors
🔭 Scope 10/20 11 files across frontend JSX/JS, backend Python views/helper, tests, templates, and migration scripts; removes a cross-cutting feature flag system; no new public APIs
🏗️ Architecture 12/20 Removes two-rule fallback system simplifying architecture; eliminates cross-cutting dependency from reader.models into library_assistant.py; new migration script adds data lifecycle management; clean removal of circular-ish dependency
⚙️ Implementation 10/20 Core logic simplification (two-rule to one-rule); migration script has pre-flight checks, archiving, dry-run mode; JSX comment syntax bug (// inside JSX return) will cause build failure; banner visibility logic change is straightforward
⚠️ Risk 11/20 Removes fallback protecting unmigrated users (requires migration to have run first); rollback script now refuses to run narrowing recovery window; data deletion in wipe script is irreversible though archived; JSX syntax error risks build failure
✅ Quality 10/15 Tests updated to match new behavior with appropriate removals; good documentation in migration scripts with ordering requirements; JSX comment bug reduces quality; no new E2E tests but change is primarily a removal
🔒 Perf / Security 2/5 Removes user_has_experiments DB query from hot base_props path (minor perf improvement); archive-before-delete pattern in migration script

Was this score accurate? 👍 Yes · 👎 No

How this was scored →

Scored by GitVelocity · How are scores calculated?

dcschreiber and others added 5 commits August 6, 2026 08:11
…ramework

The framework itself (model, admin tooling, opt-in endpoint) stays in the
codebase, parked for a future experiment — but the assistant no longer reads
it: the legacy fallback for a missing settings key is gone (absent now reads
as off, pending a catch-up migration run), and the in_chatbot_experiment prop
went with it. TEMPORARY markers on the surviving plumbing become 'parked
experiments framework' notes, since the code is staying.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The banner promotes the product, not the retired experiment. The dismissal
storage key keeps its original name so nobody's dismissal history resets.
The show condition now keys on chatbot_enabled: with in_chatbot_experiment
gone, the banner shows whenever the assistant isn't running for the viewer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 4 of the opt-out rollout: the framework code stays parked, but the
data is not retained. Archives every UserExperimentSettings row to
db.experiments_data_archive, deletes the rows, and unsets the Mongo
profiles.experiments field. Refuses to run before the migration has given
every profile its own setting key, since the migration's cohorts come from
the rows this deletes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The banner's logged-in branch offered a "Try It" button gated on the assistant
being off. Once every profile carries an explicit setting, the only logged-in
users it reaches are the ones who deliberately turned the assistant off — so it
asked precisely the people who had said no, immediately, on the settings page
where they said it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dcschreiber
dcschreiber force-pushed the chore/sc-46274/phase-3-remove-legacy-fallback branch from ddd47d6 to 6e1da35 Compare August 6, 2026 05:13
@dcschreiber
dcschreiber changed the base branch from chore/sc-46272/phase-1-library-assistant-setting to master August 6, 2026 05:13
@dcschreiber
dcschreiber marked this pull request as draft August 6, 2026 10:01
@dcschreiber
dcschreiber marked this pull request as ready for review August 6, 2026 10:01
dcschreiber and others added 13 commits August 11, 2026 10:11
A bare `python scripts/migrations/wipe_experiments_data.py` cannot import
sefaria or find DJANGO_SETTINGS_MODULE, so the documented usage never worked.
Match the two sibling migration scripts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d db

The missing-key guard returned before the counts were printed, so --dry-run
could not rehearse the wipe anywhere the backfill had yet to run — the one
place a rehearsal is worth having. A dry run writes nothing either way, so it
now downgrades the abort to a warning and goes on to report; a real run still
refuses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reworded comments left their trailing `#}` off the column the rest of
the template keeps them on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The script promises that every deleted row is archived, but it archived the
rows it had read and then deleted the whole table, so a row written between
the two would go unarchived. Delete the archived ids instead, in batches like
the sibling migration; a straggler survives for a re-run, which archives and
deletes it in turn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restores the behavior the story asks for. The gate had been narrowed to
logged-out visitors only, which was a product change beyond this ticket's
scope: it made the banner's logged-in branch — the "Try it" button that
turns the assistant on in place — unreachable, and left the preserved
`chatbot_experiment_banner_dismissed` storage key with nothing to store.

`chatbot_enabled` is the server-side signal for "the assistant is running
for this viewer": it defaults to False and is set True only for an
authenticated user whose `library_assistant` setting is on. Negating it
covers logged-out visitors and logged-in users who have it off, which is
what the retired `Sefaria.in_chatbot_experiment` check approximated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rollback window closes when this phase deploys; there is no post-Phase-3
undo. The script was already inert — with the legacy fallback gone, unsetting
the setting key would turn the assistant off for everyone it touched rather
than restore what each user had, so it refused to run at all.

The `library_assistant_migration_archive` collection stays: it is the
historical record of what the flip wrote, independent of any way to undo it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ors only

Product ruled that no logged-in user should see the promo. The banner was gated
on the assistant being off, which after the opt-out migration means it reached
exactly the users who had deliberately turned it off — asking them to reverse
the one choice they made about it.

Gate the banner on the viewer being logged out, and delete the logged-in branch
it made unreachable: the "Try It" button, the join handler that POSTed the
setting and reloaded, its pending state, and the per-audience cookie ternary.
The logged-out key `signup_promo_banner_dismissed` is now the only one; with no
logged-in viewers the old `chatbot_experiment_banner_dismissed` key is never
read or written again, and stale entries on clients simply age out.

The `site_wide_banner.try_it` entry stays in the interface JSON: those files are
Weblate's, not ours to prune by hand.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A standalone docs/decisions file was more ceremony than the decision needs.
The substance — library_assistant is the product, chatbot is the vendor/wire
name whose contracts a rename would break — now lives in the docstring of the
module every reader of the setting already goes through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
migrate_experiments_to_library_assistant.py has run to completion on
production twice (launch + catch-up); it is spent, and keeping it around
invites running it out of order. Its straggler-backfill capability moves
into wipe_experiments_data.py, which needed it anyway: the wipe deletes
the very UserExperimentSettings rows the backfill reads its cohorts from,
so a profile missing settings.library_assistant at wipe time would lose
its opt-out forever. The wipe is now self-contained: backfill any profile
lacking the key (same cohort rules and write path as the migration,
archived to the same migration archive) -> verify none remain, aborting
before any archive/delete if one does -> archive -> delete -> unset.

--force is removed: its only purpose was to bypass the refuse-guard when
profiles lacked the key, which the built-in backfill now handles. After
the backfill, a profile still missing the key means a write failed, and
proceeding would destroy the only record of a deliberate opt-out — there
is no legitimate reason to override that, so no flag offers to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comments now describe the code as it stands — what the key means, who the
promo is for — rather than narrating the migration that got it here. The
history lives in git and in the wipe script, whose subject it is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ivergence

The archive now holds the full pre-wipe record of both stores' meaningful values,
with a `source` of "row" or "profile" distinguishing them. False flags stay
unarchived: they are the serialized-on-save default and record nothing a user chose.

The docstring's claim that the Mongo Trues mirror the archived rows is replaced by a
measurement — the run counts True profiles with no row behind them, type-exactly,
and prints it without blocking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ript

A run pointed at the wrong database prints numbers that look perfectly consistent, so
both modes now open by naming the resolved Mongo and Postgres databases.

A real run closes by re-reading both stores rather than trusting its own counters,
reporting PASS or WARN for the remaining rows, the profiles still missing the setting,
the profile documents still carrying the field, and the archive entry count for the run.
Each WARN carries the operator instruction that goes with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e-3-remove-legacy-fallback

# Conflicts:
#	reader/views.py
#	static/js/ReaderApp.jsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant