mirror of
https://github.com/suitenumerique/meet.git
synced 2026-10-14 09:01:21 +00:00
✨(backend) allow static feature flags without an analytics backend
Feature flags are read from the analytics backend. Without one, NoOpAnalytics returned no flag, so "summary-enabled" was always off and a self-hosted instance could not get automatic summaries without PostHog. NoOpAnalytics now accepts a static "feature_flags" mapping through the existing ANALYTICS_BACKEND_SETTINGS, and get_analytics() passes the settings to it. Nothing changes when no flag is declared. Fixes #1610
This commit is contained in:
committed by
aleb_the_flash
parent
4854fc0c13
commit
eb422309b9
@@ -19,6 +19,7 @@ and this project adheres to
|
||||
- ✨(frontend) let users pick the recording encoding
|
||||
- ✨(backend) introduce a token exchange endpoint for iframe embeds
|
||||
- ⚡️(backend) send recording notification emails from a Celery task
|
||||
- ✨(backend) allow static feature flags without an analytics backend #1809
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -120,6 +120,26 @@ DJANGO_EMAIL_BRAND_NAME=<brand name used in email templates> # e.g. "La Suite Nu
|
||||
DJANGO_EMAIL_LOGO_IMG=<logo image to use in email templates.> # e.g. "https://meet.yourdomain.tld/assets/logo-suite-numerique.png"
|
||||
```
|
||||
|
||||
### Feature flags without an analytics backend
|
||||
|
||||
Some features, such as automatic meeting summaries (`summary-enabled`), are gated by a feature flag read from the analytics backend. You do not need an analytics backend to enable them: leave `ANALYTICS_BACKEND` unset and declare the flags in `env.d/common`. No data is sent anywhere.
|
||||
|
||||
```env
|
||||
ANALYTICS_BACKEND_SETTINGS={"feature_flags": {"summary-enabled": True}}
|
||||
```
|
||||
|
||||
You can also set it directly in `compose.yaml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
ANALYTICS_BACKEND_SETTINGS: '{"feature_flags": {"summary-enabled": True}}'
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The value is a Python dictionary, not JSON: write `True`, not `true`. Set it on every container running the backend image.
|
||||
|
||||
## Step 3: Configure your firewall
|
||||
|
||||
If you are using a firewall as it is usually recommended in a production environment you will need to allow the webservice traffic on ports 80 and 443 but also to allow UDP traffic for the WebRTC service.
|
||||
|
||||
@@ -416,6 +416,8 @@ These are the environmental options available on meet backend.
|
||||
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
|
||||
| ANALYTICS_BACKEND | Analytics backend class, e.g. `core.analytics.posthog.PostHogAnalytics`. Nothing is sent when unset | |
|
||||
| ANALYTICS_BACKEND_SETTINGS | Analytics backend settings. Without a backend, `{"feature_flags": {"summary-enabled": True}}` sets feature flags statically | {} |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | Brevo API key for marketing emails | |
|
||||
|
||||
@@ -39,7 +39,7 @@ def get_analytics() -> AnalyticsBackend:
|
||||
options = getattr(settings, "ANALYTICS_BACKEND_SETTINGS", {}) or {}
|
||||
|
||||
if not dotted_path:
|
||||
return NoOpAnalytics()
|
||||
return NoOpAnalytics(**options)
|
||||
|
||||
backend_class = import_string(dotted_path)
|
||||
return backend_class(**options)
|
||||
|
||||
@@ -53,7 +53,23 @@ class AnalyticsBackend(ABC):
|
||||
|
||||
|
||||
class NoOpAnalytics(AnalyticsBackend):
|
||||
"""Default backend: silently discards everything."""
|
||||
"""
|
||||
Default backend: discards events and serves static feature flags.
|
||||
|
||||
Feature flags can be declared without any analytics provider, e.g.:
|
||||
|
||||
ANALYTICS_BACKEND_SETTINGS = {"feature_flags": {"summary-enabled": True}}
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
feature_flags: Mapping[str, bool | str | None] | None = None,
|
||||
**_kwargs: Any,
|
||||
) -> None:
|
||||
# Other settings are ignored, so that settings meant for another
|
||||
# backend never break an instance running without one.
|
||||
self._feature_flags = dict(feature_flags or {})
|
||||
|
||||
def identify(self, user: User, properties=None) -> None:
|
||||
"""No-op: discards identify calls."""
|
||||
@@ -63,3 +79,10 @@ class NoOpAnalytics(AnalyticsBackend):
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""No-op: nothing to flush."""
|
||||
|
||||
def get_user_feature_flags(
|
||||
self,
|
||||
user: User,
|
||||
) -> Mapping[UserFeatureFlag, bool | str | None]:
|
||||
"""Return the feature flags declared in settings, for any user."""
|
||||
return self._feature_flags
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Unit tests for NoOpAnalytics and the analytics backend loader.
|
||||
"""
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument
|
||||
|
||||
import pytest
|
||||
|
||||
from core.analytics import get_analytics
|
||||
from core.analytics.base import NoOpAnalytics
|
||||
from core.analytics.user_feature_flags import UserFeatureFlag
|
||||
from core.factories import UserFactory
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_analytics_cache():
|
||||
"""Clear the cached backend before and after each test."""
|
||||
get_analytics.cache_clear()
|
||||
yield
|
||||
get_analytics.cache_clear()
|
||||
|
||||
|
||||
# ==============================
|
||||
# NoOpAnalytics
|
||||
# ==============================
|
||||
|
||||
|
||||
def test_noop_has_no_feature_flags_by_default():
|
||||
"""Should keep every feature flag disabled when none is declared."""
|
||||
backend = NoOpAnalytics()
|
||||
user = UserFactory()
|
||||
|
||||
assert not backend.get_user_feature_flags(user)
|
||||
assert (
|
||||
backend.is_user_feature_enabled(
|
||||
user, UserFeatureFlag.TRANSCRIPT_SUMMARY_ENABLED
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_noop_enables_declared_feature_flags():
|
||||
"""Should enable a feature flag declared as True."""
|
||||
backend = NoOpAnalytics(feature_flags={"summary-enabled": True})
|
||||
user = UserFactory()
|
||||
|
||||
assert backend.get_user_feature_flags(user) == {"summary-enabled": True}
|
||||
assert (
|
||||
backend.is_user_feature_enabled(
|
||||
user, UserFeatureFlag.TRANSCRIPT_SUMMARY_ENABLED
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [False, None, "true"])
|
||||
def test_noop_only_enables_feature_flags_set_to_true(value):
|
||||
"""Should keep a feature flag disabled unless its value is exactly True."""
|
||||
backend = NoOpAnalytics(feature_flags={"summary-enabled": value})
|
||||
user = UserFactory()
|
||||
|
||||
assert (
|
||||
backend.is_user_feature_enabled(
|
||||
user, UserFeatureFlag.TRANSCRIPT_SUMMARY_ENABLED
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_noop_ignores_unrelated_settings():
|
||||
"""Should accept settings meant for another backend without failing."""
|
||||
backend = NoOpAnalytics(api_key="my-key", host="https://eu.i.posthog.com")
|
||||
|
||||
assert not backend.get_user_feature_flags(UserFactory())
|
||||
|
||||
|
||||
# ==============================
|
||||
# get_analytics
|
||||
# ==============================
|
||||
|
||||
|
||||
def test_get_analytics_defaults_to_noop_without_feature_flags(settings):
|
||||
"""Should return a no-op backend with no feature flag by default."""
|
||||
settings.ANALYTICS_BACKEND = None
|
||||
settings.ANALYTICS_BACKEND_SETTINGS = {}
|
||||
|
||||
backend = get_analytics()
|
||||
|
||||
assert isinstance(backend, NoOpAnalytics)
|
||||
assert not backend.get_user_feature_flags(UserFactory())
|
||||
|
||||
|
||||
def test_get_analytics_passes_feature_flags_to_noop(settings):
|
||||
"""Should forward the feature flags when no backend is configured."""
|
||||
settings.ANALYTICS_BACKEND = None
|
||||
settings.ANALYTICS_BACKEND_SETTINGS = {"feature_flags": {"summary-enabled": True}}
|
||||
|
||||
backend = get_analytics()
|
||||
|
||||
assert isinstance(backend, NoOpAnalytics)
|
||||
assert (
|
||||
backend.is_user_feature_enabled(
|
||||
UserFactory(), UserFeatureFlag.TRANSCRIPT_SUMMARY_ENABLED
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_get_analytics_instantiates_configured_backend_with_settings(settings):
|
||||
"""Should instantiate the configured backend with its settings."""
|
||||
settings.ANALYTICS_BACKEND = "core.analytics.base.NoOpAnalytics"
|
||||
settings.ANALYTICS_BACKEND_SETTINGS = {"feature_flags": {"summary-enabled": True}}
|
||||
|
||||
backend = get_analytics()
|
||||
|
||||
assert isinstance(backend, NoOpAnalytics)
|
||||
assert (
|
||||
backend.is_user_feature_enabled(
|
||||
UserFactory(), UserFeatureFlag.TRANSCRIPT_SUMMARY_ENABLED
|
||||
)
|
||||
is True
|
||||
)
|
||||
Reference in New Issue
Block a user