From 9e14a13ee6a1b3b47cdfefc10d520fdca73daf81 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 4 Dec 2024 16:05:00 -0500 Subject: [PATCH 01/13] Set an analytics prop on User based on analytics consent state --- Backend/Models/User.cs | 7 +++++++ Backend/Repositories/UserRepository.cs | 1 + src/api/models/user.ts | 18 ++++++++++++------ src/components/UserSettings/UserSettings.tsx | 9 ++++++++- 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index d6407b1416..b18a928732 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -66,6 +66,9 @@ public class User [BsonElement("username")] public string Username { get; set; } + [BsonElement("otelConsent")] + public bool OtelConsent { get; set; } + [BsonElement("uiLang")] public string UILang { get; set; } @@ -97,6 +100,7 @@ public User() Agreement = false; Password = ""; Username = ""; + OtelConsent = false; UILang = ""; GlossSuggestion = AutocompleteSetting.On; Token = ""; @@ -119,6 +123,7 @@ public User Clone() Agreement = Agreement, Password = Password, Username = Username, + OtelConsent = OtelConsent, UILang = UILang, GlossSuggestion = GlossSuggestion, Token = Token, @@ -141,6 +146,7 @@ public bool ContentEquals(User other) other.Agreement == Agreement && other.Password.Equals(Password, StringComparison.Ordinal) && other.Username.Equals(Username, StringComparison.Ordinal) && + other.OtelConsent == OtelConsent && other.UILang.Equals(UILang, StringComparison.Ordinal) && other.GlossSuggestion.Equals(GlossSuggestion) && other.Token.Equals(Token, StringComparison.Ordinal) && @@ -178,6 +184,7 @@ public override int GetHashCode() hash.Add(Agreement); hash.Add(Password); hash.Add(Username); + hash.Add(OtelConsent); hash.Add(UILang); hash.Add(GlossSuggestion); hash.Add(Token); diff --git a/Backend/Repositories/UserRepository.cs b/Backend/Repositories/UserRepository.cs index 0ab2f006bb..d48f311116 100644 --- a/Backend/Repositories/UserRepository.cs +++ b/Backend/Repositories/UserRepository.cs @@ -196,6 +196,7 @@ public async Task Update(string userId, User user, bool updateIs .Set(x => x.ProjectRoles, user.ProjectRoles) .Set(x => x.Agreement, user.Agreement) .Set(x => x.Username, user.Username) + .Set(x => x.OtelConsent, user.OtelConsent) .Set(x => x.UILang, user.UILang) .Set(x => x.GlossSuggestion, user.GlossSuggestion); diff --git a/src/api/models/user.ts b/src/api/models/user.ts index fef3669aef..7fd261072e 100644 --- a/src/api/models/user.ts +++ b/src/api/models/user.ts @@ -92,6 +92,12 @@ export interface User { * @memberof User */ username: string; + /** + * + * @type {boolean} + * @memberof User + */ + otelConsent?: boolean; /** * * @type {string} @@ -100,20 +106,20 @@ export interface User { uiLang?: string | null; /** * - * @type {string} + * @type {AutocompleteSetting} * @memberof User */ - token: string; + glossSuggestion: AutocompleteSetting; /** * - * @type {boolean} + * @type {string} * @memberof User */ - isAdmin: boolean; + token: string; /** * - * @type {AutocompleteSetting} + * @type {boolean} * @memberof User */ - glossSuggestion: AutocompleteSetting; + isAdmin: boolean; } diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index ecf98a692c..bfde8a5935 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -11,7 +11,7 @@ import { Typography, } from "@mui/material"; import { enqueueSnackbar } from "notistack"; -import { FormEvent, Fragment, ReactElement, useState } from "react"; +import { FormEvent, Fragment, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { show } from "vanilla-cookieconsent"; @@ -65,6 +65,7 @@ export function UserSettings(props: { const [name, setName] = useState(props.user.name); const [phone, setPhone] = useState(props.user.phone); const [email, setEmail] = useState(props.user.email); + const [otelConsent, setOtelConsent] = useState(analyticsConsent); const [uiLang, setUiLang] = useState(props.user.uiLang ?? ""); const [glossSuggestion, setGlossSuggestion] = useState( props.user.glossSuggestion @@ -80,10 +81,15 @@ export function UserSettings(props: { return unchanged || !(await isEmailTaken(unicodeEmail)); } + useEffect(() => { + setOtelConsent(analyticsConsent); + }, [analyticsConsent]); + const disabled = name === props.user.name && phone === props.user.phone && punycode.toUnicode(email) === props.user.email && + otelConsent === props.user.otelConsent && uiLang === (props.user.uiLang ?? "") && glossSuggestion === props.user.glossSuggestion; @@ -95,6 +101,7 @@ export function UserSettings(props: { name, phone, email: punycode.toUnicode(email), + otelConsent, uiLang, glossSuggestion, hasAvatar: !!avatar, From 8307f7056cf69e65c59a97dac174a075ed2c9907 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 4 Dec 2024 17:10:22 -0500 Subject: [PATCH 02/13] access analytics consent inside OtelKernel --- Backend/Otel/OtelKernel.cs | 10 +++++++++- src/backend/index.ts | 3 +++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Backend/Otel/OtelKernel.cs b/Backend/Otel/OtelKernel.cs index 74842e3da1..1332f5a413 100644 --- a/Backend/Otel/OtelKernel.cs +++ b/Backend/Otel/OtelKernel.cs @@ -33,6 +33,12 @@ public static void AddOpenTelemetryInstrumentation(this IServiceCollection servi ); } + internal static void TrackConsent(Activity activity, HttpRequest request) + { + var consent = request.Headers.TryGetValue("otelConsent", out var values) ? values.FirstOrDefault() : "nothing"; + activity.SetBaggage("otelConsent", consent); + } + internal static void TrackSession(Activity activity, HttpRequest request) { var sessionId = request.Headers.TryGetValue("sessionId", out var values) ? values.FirstOrDefault() : null; @@ -67,6 +73,7 @@ private static void AspNetCoreBuilder(AspNetCoreTraceInstrumentationOptions opti options.EnrichWithHttpRequest = (activity, request) => { GetContentLengthAspNet(activity, request.Headers, "inbound.http.request.body.size"); + TrackConsent(activity, request); TrackSession(activity, request); }; options.EnrichWithHttpResponse = (activity, response) => @@ -98,7 +105,8 @@ internal class LocationEnricher(ILocationProvider locationProvider) : BaseProces { public override async void OnEnd(Activity data) { - var uriPath = (string?)data.GetTagItem("url.full"); + data?.SetTag("consent value", data?.GetBaggageItem("otelConsent")); + var uriPath = (string?)data?.GetTagItem("url.full"); var locationUri = LocationProvider.locationGetterUri; if (uriPath is null || !uriPath.Contains(locationUri)) { diff --git a/src/backend/index.ts b/src/backend/index.ts index 33bf1c40bc..d23a822041 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -55,6 +55,9 @@ const whiteListedErrorUrls = [ const axiosInstance = axios.create({ baseURL: apiBaseURL }); axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { config.headers.sessionId = getSessionId(); + LocalStorage.getCurrentUser()?.otelConsent + ? (config.headers.otelConsent = "yay") + : (config.headers.otelConsent = "nay"); return config; }); axiosInstance.interceptors.response.use(undefined, (err: AxiosError) => { From 2abf257fcd7000a35e6b642f51e0a1aac945a815 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Tue, 10 Dec 2024 13:43:47 -0500 Subject: [PATCH 03/13] switch consent from string to bool --- Backend/Otel/OtelKernel.cs | 42 ++++++++++++++++++++++---------------- src/backend/index.ts | 4 ++-- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/Backend/Otel/OtelKernel.cs b/Backend/Otel/OtelKernel.cs index 1332f5a413..96b6a9900d 100644 --- a/Backend/Otel/OtelKernel.cs +++ b/Backend/Otel/OtelKernel.cs @@ -35,8 +35,8 @@ public static void AddOpenTelemetryInstrumentation(this IServiceCollection servi internal static void TrackConsent(Activity activity, HttpRequest request) { - var consent = request.Headers.TryGetValue("otelConsent", out var values) ? values.FirstOrDefault() : "nothing"; - activity.SetBaggage("otelConsent", consent); + var consent = request.Headers.TryGetValue("otelConsent", out var values) ? bool.TryParse(values.FirstOrDefault(), out bool _) : false; + activity.SetBaggage("otelConsentBaggage", consent.ToString()); } internal static void TrackSession(Activity activity, HttpRequest request) @@ -105,23 +105,29 @@ internal class LocationEnricher(ILocationProvider locationProvider) : BaseProces { public override async void OnEnd(Activity data) { - data?.SetTag("consent value", data?.GetBaggageItem("otelConsent")); - var uriPath = (string?)data?.GetTagItem("url.full"); - var locationUri = LocationProvider.locationGetterUri; - if (uriPath is null || !uriPath.Contains(locationUri)) + var consentString = data?.GetBaggageItem("otelConsentBaggage"); + data?.AddTag("otelConsent", consentString); + var consent = bool.TryParse(consentString, out bool value) ? value : false; + // Note: A bool value also would have worked for SetTag + if (consent) { - var location = await locationProvider.GetLocation(); - data?.AddTag("country", location?.Country); - data?.AddTag("regionName", location?.RegionName); - data?.AddTag("city", location?.City); - } - data?.SetTag("sessionId", data?.GetBaggageItem("sessionBaggage")); - if (uriPath is not null && uriPath.Contains(locationUri)) - { - // When getting location externally, url.full includes site URI and user IP. - // In such cases, only add url without IP information to traces. - data?.SetTag("url.full", ""); - data?.SetTag("url.redacted.ip", LocationProvider.locationGetterUri); + var uriPath = (string?)data?.GetTagItem("url.full"); + var locationUri = LocationProvider.locationGetterUri; + if (uriPath is null || !uriPath.Contains(locationUri)) + { + var location = await locationProvider.GetLocation(); + data?.AddTag("country", location?.Country); + data?.AddTag("regionName", location?.RegionName); + data?.AddTag("city", location?.City); + } + data?.SetTag("sessionId", data?.GetBaggageItem("sessionBaggage")); + if (uriPath is not null && uriPath.Contains(locationUri)) + { + // When getting location externally, url.full includes site URI and user IP. + // In such cases, only add url without IP information to traces. + data?.SetTag("url.full", ""); + data?.SetTag("url.redacted.ip", LocationProvider.locationGetterUri); + } } } } diff --git a/src/backend/index.ts b/src/backend/index.ts index d23a822041..d582ee2b7b 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -56,8 +56,8 @@ const axiosInstance = axios.create({ baseURL: apiBaseURL }); axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { config.headers.sessionId = getSessionId(); LocalStorage.getCurrentUser()?.otelConsent - ? (config.headers.otelConsent = "yay") - : (config.headers.otelConsent = "nay"); + ? (config.headers.otelConsent = true) + : (config.headers.otelConsent = false); return config; }); axiosInstance.interceptors.response.use(undefined, (err: AxiosError) => { From ab63bbed411231c46bbad4476902e7ed6ff376c2 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Tue, 10 Dec 2024 13:45:39 -0500 Subject: [PATCH 04/13] condition sessionId trace on consent --- src/backend/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/index.ts b/src/backend/index.ts index d582ee2b7b..2e72a5ec23 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -54,9 +54,9 @@ const whiteListedErrorUrls = [ // Create an axios instance to allow for attaching interceptors to it. const axiosInstance = axios.create({ baseURL: apiBaseURL }); axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { - config.headers.sessionId = getSessionId(); LocalStorage.getCurrentUser()?.otelConsent - ? (config.headers.otelConsent = true) + ? ((config.headers.otelConsent = true), + (config.headers.sessionId = getSessionId())) : (config.headers.otelConsent = false); return config; }); From e47e04c1f12da2d3fa2b8d07a2cac7d7bf194888 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 11 Dec 2024 13:46:15 -0500 Subject: [PATCH 05/13] track consent without state - functionality --- Backend/Models/User.cs | 7 ++++ Backend/Otel/OtelKernel.cs | 1 - Backend/Repositories/UserRepository.cs | 1 + src/api/models/user.ts | 6 ++++ .../AnalyticsConsent/AnalyticsConsent.tsx | 20 ++++++++++++ src/components/App/AppLoggedIn.tsx | 20 ++++++++++++ src/components/App/index.tsx | 2 -- src/components/UserSettings/UserSettings.tsx | 32 +++++++++++-------- src/types/user.ts | 1 + 9 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 src/components/AnalyticsConsent/AnalyticsConsent.tsx diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index b18a928732..10c12604cf 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -69,6 +69,9 @@ public class User [BsonElement("otelConsent")] public bool OtelConsent { get; set; } + [BsonElement("answeredConsent")] + public bool AnsweredConsent { get; set; } + [BsonElement("uiLang")] public string UILang { get; set; } @@ -101,6 +104,7 @@ public User() Password = ""; Username = ""; OtelConsent = false; + AnsweredConsent = false; UILang = ""; GlossSuggestion = AutocompleteSetting.On; Token = ""; @@ -124,6 +128,7 @@ public User Clone() Password = Password, Username = Username, OtelConsent = OtelConsent, + AnsweredConsent = AnsweredConsent, UILang = UILang, GlossSuggestion = GlossSuggestion, Token = Token, @@ -147,6 +152,7 @@ public bool ContentEquals(User other) other.Password.Equals(Password, StringComparison.Ordinal) && other.Username.Equals(Username, StringComparison.Ordinal) && other.OtelConsent == OtelConsent && + other.AnsweredConsent == AnsweredConsent && other.UILang.Equals(UILang, StringComparison.Ordinal) && other.GlossSuggestion.Equals(GlossSuggestion) && other.Token.Equals(Token, StringComparison.Ordinal) && @@ -185,6 +191,7 @@ public override int GetHashCode() hash.Add(Password); hash.Add(Username); hash.Add(OtelConsent); + hash.Add(AnsweredConsent); hash.Add(UILang); hash.Add(GlossSuggestion); hash.Add(Token); diff --git a/Backend/Otel/OtelKernel.cs b/Backend/Otel/OtelKernel.cs index 96b6a9900d..88725313df 100644 --- a/Backend/Otel/OtelKernel.cs +++ b/Backend/Otel/OtelKernel.cs @@ -108,7 +108,6 @@ public override async void OnEnd(Activity data) var consentString = data?.GetBaggageItem("otelConsentBaggage"); data?.AddTag("otelConsent", consentString); var consent = bool.TryParse(consentString, out bool value) ? value : false; - // Note: A bool value also would have worked for SetTag if (consent) { var uriPath = (string?)data?.GetTagItem("url.full"); diff --git a/Backend/Repositories/UserRepository.cs b/Backend/Repositories/UserRepository.cs index d48f311116..35e3d8e44f 100644 --- a/Backend/Repositories/UserRepository.cs +++ b/Backend/Repositories/UserRepository.cs @@ -197,6 +197,7 @@ public async Task Update(string userId, User user, bool updateIs .Set(x => x.Agreement, user.Agreement) .Set(x => x.Username, user.Username) .Set(x => x.OtelConsent, user.OtelConsent) + .Set(x => x.AnsweredConsent, user.AnsweredConsent) .Set(x => x.UILang, user.UILang) .Set(x => x.GlossSuggestion, user.GlossSuggestion); diff --git a/src/api/models/user.ts b/src/api/models/user.ts index 7fd261072e..7b3cc9a52d 100644 --- a/src/api/models/user.ts +++ b/src/api/models/user.ts @@ -98,6 +98,12 @@ export interface User { * @memberof User */ otelConsent?: boolean; + /** + * + * @type {boolean} + * @memberof User + */ + answeredConsent?: boolean; /** * * @type {string} diff --git a/src/components/AnalyticsConsent/AnalyticsConsent.tsx b/src/components/AnalyticsConsent/AnalyticsConsent.tsx new file mode 100644 index 0000000000..67090d44fb --- /dev/null +++ b/src/components/AnalyticsConsent/AnalyticsConsent.tsx @@ -0,0 +1,20 @@ +import { ReactElement } from "react"; + +interface ConsentProps { + onChangeConsent: (consentVal: boolean) => void; +} + +export function AnalyticsConsent(props: ConsentProps): ReactElement { + const acceptAnalytics = (): void => { + props.onChangeConsent(true); + }; + const rejectAnalytics = (): void => { + props.onChangeConsent(false); + }; + return ( +
+ + +
+ ); +} diff --git a/src/components/App/AppLoggedIn.tsx b/src/components/App/AppLoggedIn.tsx index 5fb0c49417..46367202a8 100644 --- a/src/components/App/AppLoggedIn.tsx +++ b/src/components/App/AppLoggedIn.tsx @@ -4,6 +4,9 @@ import { Theme, ThemeProvider, createTheme } from "@mui/material/styles"; import { ReactElement, useEffect, useMemo, useState } from "react"; import { Route, Routes } from "react-router-dom"; +import { updateUser } from "backend"; +import { getCurrentUser } from "backend/localStorage"; +import { AnalyticsConsent } from "components/AnalyticsConsent/AnalyticsConsent"; import DatePickersLocalizationProvider from "components/App/DatePickersLocalizationProvider"; import SignalRHub from "components/App/SignalRHub"; import AppBar from "components/AppBar/AppBarComponent"; @@ -47,6 +50,18 @@ export default function AppWithBar(): ReactElement { const projFonts = useMemo(() => new ProjectFonts(proj), [proj]); const [styleOverrides, setStyleOverrides] = useState(); + const [answeredConsent, setAnsweredConsent] = useState( + getCurrentUser()?.answeredConsent + ); + + async function handleConsentChange(otelConsent: boolean): Promise { + await updateUser({ + ...getCurrentUser()!, + otelConsent, + answeredConsent: true, + }); + setAnsweredConsent(true); + } useEffect(() => { updateLangFromUser(); @@ -83,6 +98,11 @@ export default function AppWithBar(): ReactElement { + {answeredConsent ? null : ( + + )} } /> } /> diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 7d906a381c..d66ae6d3d1 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -3,7 +3,6 @@ import { RouterProvider } from "react-router-dom"; import AnnouncementBanner from "components/AnnouncementBanner"; import UpperRightToastContainer from "components/Toast/UpperRightToastContainer"; -import CookieConsent from "cookies/CookieConsent"; import router from "router/browserRouter"; /** @@ -13,7 +12,6 @@ export default function App(): ReactElement { return (
}> - diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index bfde8a5935..07661cd473 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -11,18 +11,17 @@ import { Typography, } from "@mui/material"; import { enqueueSnackbar } from "notistack"; -import { FormEvent, Fragment, ReactElement, useEffect, useState } from "react"; +import { FormEvent, Fragment, ReactElement, useState } from "react"; import { useTranslation } from "react-i18next"; -import { show } from "vanilla-cookieconsent"; import { AutocompleteSetting, User } from "api/models"; import { isEmailTaken, updateUser } from "backend"; import { getAvatar, getCurrentUser } from "backend/localStorage"; +import { AnalyticsConsent } from "components/AnalyticsConsent/AnalyticsConsent"; import { asyncLoadSemanticDomains } from "components/Project/ProjectActions"; import ClickableAvatar from "components/UserSettings/ClickableAvatar"; import { updateLangFromUser } from "i18n"; -import { useAppDispatch, useAppSelector } from "rootRedux/hooks"; -import { StoreState } from "rootRedux/types"; +import { useAppDispatch } from "rootRedux/hooks"; import theme from "types/theme"; import { uiWritingSystems } from "types/writingSystem"; @@ -58,14 +57,10 @@ export function UserSettings(props: { }): ReactElement { const dispatch = useAppDispatch(); - const analyticsConsent = useAppSelector( - (state: StoreState) => state.analyticsState.consent - ); - const [name, setName] = useState(props.user.name); const [phone, setPhone] = useState(props.user.phone); const [email, setEmail] = useState(props.user.email); - const [otelConsent, setOtelConsent] = useState(analyticsConsent); + const [otelConsent, setOtelConsent] = useState(props.user.otelConsent); const [uiLang, setUiLang] = useState(props.user.uiLang ?? ""); const [glossSuggestion, setGlossSuggestion] = useState( props.user.glossSuggestion @@ -81,9 +76,13 @@ export function UserSettings(props: { return unchanged || !(await isEmailTaken(unicodeEmail)); } - useEffect(() => { - setOtelConsent(analyticsConsent); - }, [analyticsConsent]); + const [displayConsent, setDisplayConsent] = useState(false); + const show = (): void => setDisplayConsent(true); + + const handleConsentChange = (consentVal: boolean): void => { + setOtelConsent(consentVal); + setDisplayConsent(false); + }; const disabled = name === props.user.name && @@ -293,7 +292,7 @@ export function UserSettings(props: { {t( - analyticsConsent + otelConsent ? "userSettings.analyticsConsent.consentYes" : "userSettings.analyticsConsent.consentNo" )} @@ -301,11 +300,16 @@ export function UserSettings(props: { + {displayConsent ? ( + + ) : null} diff --git a/src/types/user.ts b/src/types/user.ts index 29d6d0177c..55ad01aeb1 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -15,6 +15,7 @@ export function newUser(name = "", username = "", password = ""): User { glossSuggestion: AutocompleteSetting.On, token: "", isAdmin: false, + answeredConsent: false, }; } From fffdc2e265020ab71a7b1b6bd87cca0260420622 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 11 Dec 2024 14:37:47 -0500 Subject: [PATCH 06/13] fix tests to account for conditioning Otel tags on consent --- Backend.Tests/Otel/OtelKernelTests.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Backend.Tests/Otel/OtelKernelTests.cs b/Backend.Tests/Otel/OtelKernelTests.cs index d21a66f663..1663b77edd 100644 --- a/Backend.Tests/Otel/OtelKernelTests.cs +++ b/Backend.Tests/Otel/OtelKernelTests.cs @@ -12,9 +12,14 @@ namespace Backend.Tests.Otel { public class OtelKernelTests : IDisposable { + + private const string FrontendConsentKey = "otelConsent"; private const string FrontendSessionIdKey = "sessionId"; + private const string OtelConsentKey = "otelConsent"; private const string OtelSessionIdKey = "sessionId"; + private const string OtelConsentBaggageKey = "otelConsentBaggage"; private const string OtelSessionBaggageKey = "sessionBaggage"; + private LocationEnricher _locationEnricher = null!; public void Dispose() @@ -32,41 +37,46 @@ protected virtual void Dispose(bool disposing) } [Test] - public void BuildersSetSessionBaggageFromHeader() + public void BuildersSetConsentAndSessionBaggageFromHeader() { // Arrange var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers[FrontendConsentKey] = "true"; httpContext.Request.Headers[FrontendSessionIdKey] = "123"; var activity = new Activity("testActivity").Start(); // Act + TrackConsent(activity, httpContext.Request); TrackSession(activity, httpContext.Request); // Assert + Assert.That(activity.Baggage.Any(_ => _.Key == OtelConsentBaggageKey)); Assert.That(activity.Baggage.Any(_ => _.Key == OtelSessionBaggageKey)); } [Test] - public void OnEndSetsSessionTagFromBaggage() + public void OnEndSetsConsentAndSessionTagFromBaggage() { // Arrange var activity = new Activity("testActivity").Start(); + activity.SetBaggage(OtelConsentBaggageKey, "true"); activity.SetBaggage(OtelSessionBaggageKey, "test session id"); // Act _locationEnricher.OnEnd(activity); // Assert + Assert.That(activity.Tags.Any(_ => _.Key == OtelConsentKey)); Assert.That(activity.Tags.Any(_ => _.Key == OtelSessionIdKey)); } - [Test] public void OnEndSetsLocationTags() { // Arrange _locationEnricher = new LocationEnricher(new LocationProviderMock()); var activity = new Activity("testActivity").Start(); + activity.SetBaggage(OtelConsentBaggageKey, "true"); // Act _locationEnricher.OnEnd(activity); @@ -81,11 +91,13 @@ public void OnEndSetsLocationTags() Assert.That(activity.Tags, Is.SupersetOf(testLocation)); } + [Test] public void OnEndRedactsIp() { // Arrange _locationEnricher = new LocationEnricher(new LocationProviderMock()); var activity = new Activity("testActivity").Start(); + activity.SetBaggage(OtelConsentBaggageKey, "true"); activity.SetTag("url.full", $"{LocationProvider.locationGetterUri}100.0.0.0"); // Act From 9a91fc86daca5d45e6611fd5a0211cd15e9d3943 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Fri, 13 Dec 2024 16:14:06 -0500 Subject: [PATCH 07/13] add MUI drawer component progress --- .../AnalyticsConsent/AnalyticsConsent.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/AnalyticsConsent/AnalyticsConsent.tsx b/src/components/AnalyticsConsent/AnalyticsConsent.tsx index 67090d44fb..6e4ed373cc 100644 --- a/src/components/AnalyticsConsent/AnalyticsConsent.tsx +++ b/src/components/AnalyticsConsent/AnalyticsConsent.tsx @@ -1,20 +1,34 @@ -import { ReactElement } from "react"; +import { Button } from "@mui/material"; +import Drawer from "@mui/material/Drawer"; +import { ReactElement, useState } from "react"; interface ConsentProps { onChangeConsent: (consentVal: boolean) => void; } export function AnalyticsConsent(props: ConsentProps): ReactElement { + + const [responded, setResponded] = useState(false); const acceptAnalytics = (): void => { + + setResponded(true); props.onChangeConsent(true); }; const rejectAnalytics = (): void => { + + setResponded(false); props.onChangeConsent(false); }; + + return (
- - + + MyDrawer! + + + +
); } From 20558e90d73d81233378748313cc2de1898231cb Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Tue, 17 Dec 2024 23:36:40 -0500 Subject: [PATCH 08/13] allow clickaway to escape consent options in UserSettings --- .../AnalyticsConsent/AnalyticsConsent.tsx | 23 +++++++++---------- src/components/App/AppLoggedIn.tsx | 5 +++- src/components/UserSettings/UserSettings.tsx | 5 ++-- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/AnalyticsConsent/AnalyticsConsent.tsx b/src/components/AnalyticsConsent/AnalyticsConsent.tsx index 6e4ed373cc..419dcab8d7 100644 --- a/src/components/AnalyticsConsent/AnalyticsConsent.tsx +++ b/src/components/AnalyticsConsent/AnalyticsConsent.tsx @@ -1,34 +1,33 @@ -import { Button } from "@mui/material"; import Drawer from "@mui/material/Drawer"; -import { ReactElement, useState } from "react"; +import { ReactElement } from "react"; interface ConsentProps { - onChangeConsent: (consentVal: boolean) => void; + onChangeConsent: (consentVal: boolean | undefined) => void; + required: boolean; } export function AnalyticsConsent(props: ConsentProps): ReactElement { - - const [responded, setResponded] = useState(false); const acceptAnalytics = (): void => { - - setResponded(true); props.onChangeConsent(true); }; const rejectAnalytics = (): void => { - - setResponded(false); props.onChangeConsent(false); }; + const clickedAway = (): void => { + props.onChangeConsent(undefined); + }; return (
- - MyDrawer! + -
); } diff --git a/src/components/App/AppLoggedIn.tsx b/src/components/App/AppLoggedIn.tsx index 46367202a8..9577c46a7b 100644 --- a/src/components/App/AppLoggedIn.tsx +++ b/src/components/App/AppLoggedIn.tsx @@ -54,7 +54,9 @@ export default function AppWithBar(): ReactElement { getCurrentUser()?.answeredConsent ); - async function handleConsentChange(otelConsent: boolean): Promise { + async function handleConsentChange( + otelConsent: boolean | undefined + ): Promise { await updateUser({ ...getCurrentUser()!, otelConsent, @@ -101,6 +103,7 @@ export default function AppWithBar(): ReactElement { {answeredConsent ? null : ( )} diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index 07661cd473..016665b410 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -79,8 +79,8 @@ export function UserSettings(props: { const [displayConsent, setDisplayConsent] = useState(false); const show = (): void => setDisplayConsent(true); - const handleConsentChange = (consentVal: boolean): void => { - setOtelConsent(consentVal); + const handleConsentChange = (consentVal: boolean | undefined): void => { + setOtelConsent(consentVal ?? otelConsent); setDisplayConsent(false); }; @@ -308,6 +308,7 @@ export function UserSettings(props: { {displayConsent ? ( ) : null} From 3cd0c6f7326dfe47cf3226891f7a0eac9f5415d9 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 18 Dec 2024 12:36:12 -0500 Subject: [PATCH 09/13] use MUI buttons (horizontal) --- src/components/AnalyticsConsent/AnalyticsConsent.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/AnalyticsConsent/AnalyticsConsent.tsx b/src/components/AnalyticsConsent/AnalyticsConsent.tsx index 419dcab8d7..4cbbc2ab09 100644 --- a/src/components/AnalyticsConsent/AnalyticsConsent.tsx +++ b/src/components/AnalyticsConsent/AnalyticsConsent.tsx @@ -1,3 +1,4 @@ +import { Button, List } from "@mui/material"; import Drawer from "@mui/material/Drawer"; import { ReactElement } from "react"; @@ -25,8 +26,10 @@ export function AnalyticsConsent(props: ConsentProps): ReactElement { open onClose={!props.required ? clickedAway : undefined} > - - + + + +
); From ce7b9dc4dbd81ea311470e8c048c39a32ec3c6f6 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 18 Dec 2024 12:55:25 -0500 Subject: [PATCH 10/13] vertical buttons option --- .../AnalyticsConsent/AnalyticsConsent.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/AnalyticsConsent/AnalyticsConsent.tsx b/src/components/AnalyticsConsent/AnalyticsConsent.tsx index 4cbbc2ab09..3326d4f4eb 100644 --- a/src/components/AnalyticsConsent/AnalyticsConsent.tsx +++ b/src/components/AnalyticsConsent/AnalyticsConsent.tsx @@ -1,4 +1,4 @@ -import { Button, List } from "@mui/material"; +import { List, ListItemButton, Typography } from "@mui/material"; import Drawer from "@mui/material/Drawer"; import { ReactElement } from "react"; @@ -27,8 +27,18 @@ export function AnalyticsConsent(props: ConsentProps): ReactElement { onClose={!props.required ? clickedAway : undefined} > - - + + Accept + + + Reject + From d7d1e61d2f3dde02f0aaf0803ee9477883f59d44 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 18 Dec 2024 13:35:20 -0500 Subject: [PATCH 11/13] use translation for buttons --- public/locales/en/translation.json | 14 ++++++++------ .../AnalyticsConsent/AnalyticsConsent.tsx | 11 +++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 7e757db4b4..7df6a4b104 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -33,6 +33,14 @@ "pressEnter": "Press Enter to save word", "vernacular": "Vernacular" }, + "analyticsConsent": { + "consentModal": { + "acceptAllBtn": "Yes, allow analytics cookies", + "acceptNecessaryBtn": "No, reject analytics cookies", + "description": "The Combine stores basic info about your current session on your device. This info is necessary and isn't shared with anybody. The Combine also uses analytics cookies, which are only for us to fix bugs and compile anonymized statistics. Do you consent to our usage of analytics cookies?", + "title": "Cookies on The Combine" + } + }, "appBar": { "dataEntry": "Data Entry", "dataCleanup": "Data Cleanup", @@ -129,12 +137,6 @@ "userSettings": { "analyticsConsent": { "button": "Change consent", - "consentModal": { - "acceptAllBtn": "Yes, allow analytics cookies", - "acceptNecessaryBtn": "No, reject analytics cookies", - "description": "The Combine stores basic info about your current session on your device. This info is necessary and isn't shared with anybody. The Combine also uses analytics cookies, which are only for us to fix bugs and compile anonymized statistics. Do you consent to our usage of analytics cookies?", - "title": "Cookies on The Combine" - }, "consentNo": "You have not consented to our use of analytics cookies.", "consentYes": "You have consented to our use of analytics cookies.", "title": "Analytics cookies" diff --git a/src/components/AnalyticsConsent/AnalyticsConsent.tsx b/src/components/AnalyticsConsent/AnalyticsConsent.tsx index 3326d4f4eb..8f61a7943d 100644 --- a/src/components/AnalyticsConsent/AnalyticsConsent.tsx +++ b/src/components/AnalyticsConsent/AnalyticsConsent.tsx @@ -1,6 +1,7 @@ import { List, ListItemButton, Typography } from "@mui/material"; import Drawer from "@mui/material/Drawer"; import { ReactElement } from "react"; +import { useTranslation } from "react-i18next"; interface ConsentProps { onChangeConsent: (consentVal: boolean | undefined) => void; @@ -8,6 +9,8 @@ interface ConsentProps { } export function AnalyticsConsent(props: ConsentProps): ReactElement { + const { t } = useTranslation(); + const acceptAnalytics = (): void => { props.onChangeConsent(true); }; @@ -31,13 +34,17 @@ export function AnalyticsConsent(props: ConsentProps): ReactElement { onClick={acceptAnalytics} style={{ justifyContent: "center" }} > - Accept + + {t("analyticsConsent.consentModal.acceptAllBtn")} + - Reject + + {t("analyticsConsent.consentModal.acceptNecessaryBtn")} + From f1012d924b78c83497899cb095278e22836f1c0e Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 18 Dec 2024 13:45:53 -0500 Subject: [PATCH 12/13] analytics initially true --- Backend/Models/User.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/Models/User.cs b/Backend/Models/User.cs index 10c12604cf..591fe5a1ef 100644 --- a/Backend/Models/User.cs +++ b/Backend/Models/User.cs @@ -103,7 +103,7 @@ public User() Agreement = false; Password = ""; Username = ""; - OtelConsent = false; + OtelConsent = true; AnsweredConsent = false; UILang = ""; GlossSuggestion = AutocompleteSetting.On; From 17b558209b9ffa5b79e9f380ae58b212b4958f04 Mon Sep 17 00:00:00 2001 From: Andra Constantin Date: Wed, 18 Dec 2024 13:53:25 -0500 Subject: [PATCH 13/13] tooltip progress --- src/components/UserSettings/UserSettings.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/UserSettings/UserSettings.tsx b/src/components/UserSettings/UserSettings.tsx index 016665b410..360a83c572 100644 --- a/src/components/UserSettings/UserSettings.tsx +++ b/src/components/UserSettings/UserSettings.tsx @@ -312,6 +312,14 @@ export function UserSettings(props: { > ) : null} + + + + +