diff --git a/docs/env.md b/docs/env.md index 3e92bc53..a9f0fa53 100644 --- a/docs/env.md +++ b/docs/env.md @@ -200,6 +200,8 @@ The application uses a new environment file structure with `.defaults` and `.loc | Variable | Default | Description | Required | |----------|---------|-------------|----------| | `POSTHOG_KEY` | None | PostHog analytics key | Optional | +| `POSTHOG_HOST` | `https://eu.i.posthog.com` | PostHog analytics host url | Optional | +| `POSTHOG_SURVEY_ID` | None | PostHog survey id to get feedback from users | Optional | ### Logging @@ -303,4 +305,4 @@ For production deployments, ensure: 2. Secrets are managed through secure secret management systems 3. HTTPS is enforced for all external communications 4. Database connections use SSL/TLS -5. File storage uses appropriate access controls \ No newline at end of file +5. File storage uses appropriate access controls diff --git a/src/backend/core/api/openapi.json b/src/backend/core/api/openapi.json index ef03f924..eaa40454 100644 --- a/src/backend/core/api/openapi.json +++ b/src/backend/core/api/openapi.json @@ -158,6 +158,11 @@ "nullable": true, "readOnly": true }, + "POSTHOG_SURVEY_ID": { + "type": "string", + "nullable": true, + "readOnly": true + }, "LANGUAGES": { "type": "array", "items": { @@ -174,7 +179,7 @@ "ENVIRONMENT", "POSTHOG_KEY", "POSTHOG_HOST", - "LANGUAGES", + "POSTHOG_SURVEY_IDLANGUAGES", "LANGUAGE_CODE" ] } diff --git a/src/backend/core/api/viewsets/config.py b/src/backend/core/api/viewsets/config.py index d2f1ddd0..33d8d583 100644 --- a/src/backend/core/api/viewsets/config.py +++ b/src/backend/core/api/viewsets/config.py @@ -3,8 +3,8 @@ from django.conf import settings import rest_framework as drf +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny -from drf_spectacular.utils import extend_schema, OpenApiResponse class ConfigView(drf.views.APIView): @@ -20,19 +20,37 @@ class ConfigView(drf.views.APIView): response={ "type": "object", "properties": { - "ENVIRONMENT": { "type": "string", "readOnly": True }, - "POSTHOG_KEY": { "type": "string", "nullable": True, "readOnly": True }, - "POSTHOG_HOST": { "type": "string", "nullable": True, "readOnly": True }, - "LANGUAGES": { "type": "array", "items": { "type": "string" }, "readOnly": True }, - "LANGUAGE_CODE": { "type": "string", "readOnly": True }, + "ENVIRONMENT": {"type": "string", "readOnly": True}, + "POSTHOG_KEY": { + "type": "string", + "nullable": True, + "readOnly": True, + }, + "POSTHOG_HOST": { + "type": "string", + "nullable": True, + "readOnly": True, + }, + "POSTHOG_SURVEY_ID": { + "type": "string", + "nullable": True, + "readOnly": True, + }, + "LANGUAGES": { + "type": "array", + "items": {"type": "string"}, + "readOnly": True, + }, + "LANGUAGE_CODE": {"type": "string", "readOnly": True}, }, "required": [ "ENVIRONMENT", "POSTHOG_KEY", "POSTHOG_HOST", + "POSTHOG_SURVEY_ID", "LANGUAGES", - "LANGUAGE_CODE" - ] + "LANGUAGE_CODE", + ], }, ) }, @@ -47,8 +65,9 @@ class ConfigView(drf.views.APIView): "ENVIRONMENT", "POSTHOG_KEY", "POSTHOG_HOST", + "POSTHOG_SURVEY_ID", "LANGUAGES", - "LANGUAGE_CODE" + "LANGUAGE_CODE", ] dict_settings = {} for setting in array_settings: diff --git a/src/backend/core/api/viewsets/draft.py b/src/backend/core/api/viewsets/draft.py index 2c9517a5..8570296a 100644 --- a/src/backend/core/api/viewsets/draft.py +++ b/src/backend/core/api/viewsets/draft.py @@ -253,7 +253,6 @@ class DraftMessageView(APIView): # Update draft body if provided if "draftBody" in request_data: - try: if message.draft_blob: message.draft_blob.delete() diff --git a/src/backend/core/tests/api/test_config.py b/src/backend/core/tests/api/test_config.py index 9f059cff..d8cce3eb 100644 --- a/src/backend/core/tests/api/test_config.py +++ b/src/backend/core/tests/api/test_config.py @@ -16,10 +16,11 @@ pytestmark = pytest.mark.django_db @override_settings( - FRONTEND_THEME="test-theme", - MEDIA_BASE_URL="http://testserver/", - POSTHOG_KEY={"id": "132456", "host": "https://eu.i.posthog-test.com"}, - SENTRY_DSN="https://sentry.test/123", + POSTHOG_KEY="132456", + POSTHOG_HOST="https://test.i.posthog-test.com", + POSTHOG_SURVEY_ID="7890", + LANGUAGES=[["en-us", "English"], ["fr-fr", "French"], ["de-de", "German"]], + LANGUAGE_CODE="en-us", ) @pytest.mark.parametrize("is_authenticated", [False, True]) def test_api_config(is_authenticated): @@ -34,10 +35,9 @@ def test_api_config(is_authenticated): assert response.status_code == HTTP_200_OK assert response.json() == { "ENVIRONMENT": "test", - "FRONTEND_THEME": "test-theme", "LANGUAGES": [["en-us", "English"], ["fr-fr", "French"], ["de-de", "German"]], "LANGUAGE_CODE": "en-us", - "MEDIA_BASE_URL": "http://testserver/", - "POSTHOG_KEY": {"id": "132456", "host": "https://eu.i.posthog-test.com"}, - "SENTRY_DSN": "https://sentry.test/123", + "POSTHOG_KEY": "132456", + "POSTHOG_HOST": "https://test.i.posthog-test.com", + "POSTHOG_SURVEY_ID": "7890", } diff --git a/src/backend/messages/settings.py b/src/backend/messages/settings.py index d99e3538..6ec826ea 100755 --- a/src/backend/messages/settings.py +++ b/src/backend/messages/settings.py @@ -407,6 +407,9 @@ class Base(Configuration): POSTHOG_HOST = values.Value( "https://eu.i.posthog.com", environ_name="POSTHOG_HOST", environ_prefix=None ) + POSTHOG_SURVEY_ID = values.Value( + None, environ_name="POSTHOG_SURVEY_ID", environ_prefix=None + ) # Celery CELERY_BROKER_URL = values.Value( diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 9c742e58..801a1a5d 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -24,6 +24,7 @@ "dompurify": "3.2.6", "i18next": "25.3.0", "next": "15.3.4", + "posthog-js": "1.257.0", "pretty-bytes": "7.0.0", "react": "19.1.0", "react-dom": "19.1.0", @@ -7120,6 +7121,17 @@ "node": ">= 0.6" } }, + "node_modules/core-js": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz", + "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "license": "MIT", @@ -11942,6 +11954,46 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/posthog-js": { + "version": "1.257.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.257.0.tgz", + "integrity": "sha512-Ujg9RGtWVCu+4tmlRpALSy2ZOZI6JtieSYXIDDdgMWm167KYKvTtbMPHdoBaPWcNu0Km+1hAIBnQFygyn30KhA==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@rrweb/types": "2.0.0-alpha.17", + "rrweb-snapshot": "2.0.0-alpha.17" + }, + "peerDependenciesMeta": { + "@rrweb/types": { + "optional": true + }, + "rrweb-snapshot": { + "optional": true + } + } + }, + "node_modules/posthog-js/node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.26.9", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz", + "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -15265,6 +15317,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "dev": true, diff --git a/src/frontend/package.json b/src/frontend/package.json index 6f5f7cee..957b5fb4 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -36,6 +36,7 @@ "dompurify": "3.2.6", "i18next": "25.3.0", "next": "15.3.4", + "posthog-js": "1.257.0", "pretty-bytes": "7.0.0", "react": "19.1.0", "react-dom": "19.1.0", @@ -65,7 +66,7 @@ "vitest": "3.2.4" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-musl": "4.44.1", - "@rollup/rollup-linux-x64-gnu": "4.44.1" + "@rollup/rollup-linux-x64-gnu": "4.44.1", + "@rollup/rollup-linux-x64-musl": "4.44.1" } } diff --git a/src/frontend/src/features/api/gen/models/config_retrieve200.ts b/src/frontend/src/features/api/gen/models/config_retrieve200.ts index 6cf9e463..9b174127 100644 --- a/src/frontend/src/features/api/gen/models/config_retrieve200.ts +++ b/src/frontend/src/features/api/gen/models/config_retrieve200.ts @@ -12,6 +12,8 @@ export type ConfigRetrieve200 = { readonly POSTHOG_KEY: string | null; /** @nullable */ readonly POSTHOG_HOST: string | null; - readonly LANGUAGES: readonly string[]; + /** @nullable */ + readonly POSTHOG_SURVEY_ID?: string | null; + readonly LANGUAGES?: readonly string[]; readonly LANGUAGE_CODE: string; }; diff --git a/src/frontend/src/features/auth/index.tsx b/src/frontend/src/features/auth/index.tsx index 93e4e197..80926d14 100644 --- a/src/frontend/src/features/auth/index.tsx +++ b/src/frontend/src/features/auth/index.tsx @@ -4,8 +4,10 @@ import { getRequestUrl } from "@/features/api/utils"; import { useUsersMeRetrieve } from "@/features/api/gen/users/users"; import { User } from "@/features/api/gen/models/user"; import { Spinner } from "@gouvfr-lasuite/ui-kit"; +import { posthog } from "posthog-js"; export const logout = () => { + posthog.reset() window.location.replace(getRequestUrl("/api/v1.0/logout/")); }; diff --git a/src/frontend/src/features/i18n/translations.json b/src/frontend/src/features/i18n/translations.json index eb7b9621..477e0b6a 100644 --- a/src/frontend/src/features/i18n/translations.json +++ b/src/frontend/src/features/i18n/translations.json @@ -371,6 +371,9 @@ "new_address": "New address" }, "loading": "Loading addresses..." + }, + "posthog": { + "cta": "Give feedback" } } }, @@ -741,6 +744,9 @@ "new_address": "Nouvelle adresse" }, "loading": "Chargement des adresses..." + }, + "posthog": { + "cta": "Faire un retour" } } } diff --git a/src/frontend/src/features/layouts/components/main/header/_index.scss b/src/frontend/src/features/layouts/components/main/header/_index.scss index 2dc365d2..fb3d3dd6 100644 --- a/src/frontend/src/features/layouts/components/main/header/_index.scss +++ b/src/frontend/src/features/layouts/components/main/header/_index.scss @@ -18,7 +18,7 @@ @media screen and (max-width: breakpoint(tablet)) { grid-template-columns: auto 1fr 1fr; grid-template-rows: 1fr 1fr; - grid-template-areas: + grid-template-areas: "toggle left left" "center center center"; padding: 0 var(--c--theme--spacings--base); @@ -65,10 +65,5 @@ &__right { grid-area: right; justify-content: flex-end; - &__language-picker { - @media screen and (max-width: breakpoint(tablet)) { - display: none; - } - } } } diff --git a/src/frontend/src/features/layouts/components/main/header/anonymous.tsx b/src/frontend/src/features/layouts/components/main/header/anonymous.tsx index 72301527..b77c3dbc 100644 --- a/src/frontend/src/features/layouts/components/main/header/anonymous.tsx +++ b/src/frontend/src/features/layouts/components/main/header/anonymous.tsx @@ -1,7 +1,8 @@ -import { HeaderProps } from "@gouvfr-lasuite/ui-kit"; +import { HeaderProps, useResponsive } from "@gouvfr-lasuite/ui-kit"; import { Button } from "@openfun/cunningham-react"; -import { LanguagePicker } from "../language-picker"; import { useTranslation } from "react-i18next"; +import { PostHogSurveyButton } from "@/features/ui/components/feedback-button"; +import { LanguagePicker } from "../language-picker"; export const AnonymousHeader = ({ @@ -10,6 +11,7 @@ export const AnonymousHeader = ({ isPanelOpen, }: HeaderProps) => { const { t } = useTranslation(); + const { isDesktop } = useResponsive(); return (
= NextPage
& {
getLayout?: (page: ReactElement) => ReactNode;
@@ -84,11 +86,15 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {