Chat widget versioning & deprecation
The chat widget (open-chat-studio-widget on npm, source in components/chat_widget)
is embedded on customer sites with a version-pinned script tag. The Django app's
version policy lives in apps/channels/widget_versions.py.
How version tracking works
- The widget sends
x-ocs-widget-versionon chat API requests (since v0.5.1). chat_start_sessionpersists it toExperimentChannel.widget_version/widget_version_updated_at(unaudited telemetry columns, throttled writes).- The channel button and widget params dialog show upgrade/deprecation badges
based on
get_widget_update_status().
Auditing which versions are in use
list_outdated_widget_versions reports embedded-widget channels running anything
older than LATEST_VERSION, with a session count per channel:
python manage.py list_outdated_widget_versions
Only channels with real recent usage are listed: a session counts when it has at
least one human message inside the activity window (30 days by default), and a
channel appears only if that count is non-zero — so the session count is exactly
the traffic that qualified it. Each row is classified sunset (past sunset_at),
deprecated (covered by a WidgetDeprecation) or outdated (older than latest,
still supported).
Channels that have never reported a version (widget_version is null) are
excluded, since their version can't be determined; --include-unreported adds
them under a fourth status, unreported. They deliberately get no deprecation
verdict or sunset date — a null version only means the recording path never ran,
so the widget could equally be on LATEST_VERSION. Note that unknown is different — that is what a pre-0.5.1 widget gets
recorded as, and it always appears in the report: it counts as older than every
release, so it shows as deprecated (or sunset) under the configured
deprecation, and as outdated when no deprecation is configured.
Useful options: --days N to change the window, --team <slug> to scope to one
team, --deprecated-only to drop merely-outdated rows, and --format csv for a
spreadsheet-ready dump (adds the chatbot URL and channel ID).
Per-channel authentication policy
Each embedded-widget channel stores a durable ExperimentChannel.required_auth_level
(WidgetAuthLevel) that fixes the minimum authentication the server demands, rather
than inferring it from per-request heuristics:
| Level | Value | What the server enforces |
|---|---|---|
NONE |
0 | Legacy path: embed key optional, is_public/allowlist fallback permitted (pre-0.5.1 widgets). |
EMBED_KEY |
1 | A valid X-Embed-Key + allowed-domain check; no session token. The is_public fallback is blocked. |
SESSION_TOKEN |
2 | A valid X-Embed-Key and X-Session-Token. No legacy path reachable. |
- New channels default to
SESSION_TOKEN(the strictest level). chat_start_sessionconsults the resolved channel's level (see_issue_or_opt_out_session_token):SESSION_TOKENalways issues and enforces a token;EMBED_KEY/NONEopt the session out of token enforcement.SessionAccessPermissionenforces the level on every subsequent request.- Migration
0029_experimentchannel_required_auth_levelgrandfathers existing channels from their last-reportedwidget_version:unknown/< 0.5.1→NONE,0.5.1–0.8.x→EMBED_KEY,>= 0.9.0and never-connected (null) →SESSION_TOKEN. - The level is system-managed, not user-editable: it is set by the model default for new channels and by the grandfathering migration for existing ones. It is visible (read-only) in the Django admin for inspection/support.
Ratcheting the level up when a widget upgrades
A grandfathered channel keeps recording the version its widget reports
(widget_version), but that never moves required_auth_level on its own. The
ratchet_widget_auth_levels Celery task (daily; apps/channels/tasks.py) closes that
gap. For each embedded-widget channel it maps the recorded version to the level it can
satisfy (widget_versions.level_for_version) and, if that is higher than the current
required_auth_level, runs a two-phase, notify-then-apply upgrade:
- First detection — record
pending_auth_level+auth_level_notified_atand notify the team (widget_auth_level_upgrade_notification) with the minimum widget version every embed must run before the change lands. - After the grace period (
ExperimentChannel.AUTH_LEVEL_RATCHET_GRACE, 14 days) — apply the level via an auditedsave(), then clear the pending state.
The ratchet is monotonic: it only ever raises a level, so a stale or spoofed version header can only tighten auth, never relax it. If the reported version drops back below the pending level before the grace period elapses, the pending bump is dropped. The channel details dialog shows the current minimum required version and any pending upgrade.
Releasing a new widget version
- Publish the new version to npm (see
components/chat_widget). - In the same PR, bump
LATEST_VERSIONinapps/channels/widget_versions.py. The embed snippet ({% widget_script_url %}) and the "update available" badges follow automatically. - Optionally announce the release in-app. Add a data migration in
apps/channels/migrations/that triggers the notification on deploy, with the version and release notes inline:from apps.data_migrations.utils.migrations import RunDataMigration operations = [ RunDataMigration( "notify_widget_version_release", command_options={ "force": True, "widget_version": "0.10.0", "notes": "Adds dark mode and faster load times.", "changelog_url": "https://docs.openchatstudio.com/chat_widget/", }, ), ]
widget_version defaults to LATEST_VERSION and changelog_url to the chat
widget docs, so both can be omitted. Every team with an embedded-widget
channel gets an INFO notification linking to their widget chatbots and the
changelog. The command slug is fixed; Django tracks each migration's single
run, so force=True is required and nothing needs bumping. Preview with:
python manage.py notify_widget_version_release --dry-run --widget-version 0.10.0
Deprecating old versions
- Add a
WidgetDeprecationentry toDEPRECATIONSinapps/channels/widget_versions.pywith a sunset date at least 60 days out. Versions belowbelow_versionare deprecated; widgets that predate the version header (< 0.5.1) count as deprecated too. - In the docs repo, update the chat widget changelog (published at
https://docs.openchatstudio.com/chat_widget/): tag the affected version(s)
as deprecated and note the same sunset date used in the
WidgetDeprecationentry. This is the public record customers check to see whether their pinned version is still supported. - Add a data migration in
apps/channels/migrations/that triggers the notification on deploy (see0027_notify_widget_deprecation_below_0_6_0.py):from apps.data_migrations.utils.migrations import RunDataMigration operations = [ RunDataMigration("notify_deprecated_widget_versions", command_options={"force": True}), ]
The command slug is fixed; Django tracks each migration's single run, so
nothing needs bumping. Teams with affected channels (running a deprecated
version and active in the last 90 days) get an in-app notification on deploy;
dormant channels are surfaced passively by the UI badge instead.
4. Deploy. Deprecated widgets now receive RFC 8594 Deprecation/Sunset
headers on chat API responses, affected channels show a warning badge, and
the migration sends the notifications.
5. At sunset nothing breaks automatically — the date marks when breaking
server-side changes may land. Plan any actual removal separately, following
feature deprecation.
Keep the sunset date consistent across all three places — the WidgetDeprecation
entry, the changelog tag, and the Sunset header (which is derived from the
entry automatically).
To preview who would be notified before deploying, run the command manually:
python manage.py notify_deprecated_widget_versions --dry-run
To size up the impact before choosing a below_version and sunset date, see
which versions are actually in use.