diff --git a/CHANGELOG.md b/CHANGELOG.md index 34bca74876..3df602ea11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The types of changes are: ### Added - Additional consent reporting calls from `fides-js` [#3845](https://github.com/ethyca/fides/pull/3845) +- Additional consent reporting calls from privacy center [#3847](https://github.com/ethyca/fides/pull/3847) ## [2.17.0](https://github.com/ethyca/fides/compare/2.16.0...2.17.0) diff --git a/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx b/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx index fd3e7eee18..e11bcf798f 100644 --- a/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx +++ b/clients/privacy-center/components/consent/NoticeDrivenConsent.tsx @@ -17,6 +17,7 @@ import { selectUserRegion, selectPrivacyExperience, useUpdatePrivacyPreferencesMutation, + useUpdateNoticesServedMutation, } from "~/features/consent/consent.slice"; import { @@ -25,6 +26,7 @@ import { ConsentOptionCreate, PrivacyNoticeResponseWithUserPreferences, PrivacyPreferencesRequest, + ServingComponent, UserConsentPreference, } from "~/types/api"; import { useRouter } from "next/router"; @@ -63,6 +65,12 @@ const NoticeDrivenConsent = () => { useUpdatePrivacyPreferencesMutation(); const region = useAppSelector(selectUserRegion); + const browserIdentities = useMemo(() => { + const identities = inspectForBrowserIdentities(); + const deviceIdentity = { fides_user_device_id: fidesUserDeviceId }; + return identities ? { ...deviceIdentity, ...identities } : deviceIdentity; + }, [fidesUserDeviceId]); + const initialDraftPreferences = useMemo(() => { const newPreferences = { ...serverPreferences }; Object.entries(serverPreferences).forEach(([key, value]) => { @@ -87,6 +95,30 @@ const NoticeDrivenConsent = () => { setDraftPreferences(initialDraftPreferences); }, [initialDraftPreferences]); + const [updateNoticesServedMutationTrigger, { data: servedNotices }] = + useUpdateNoticesServedMutation(); + + useEffect(() => { + if (experience && experience.privacy_notices) { + updateNoticesServedMutationTrigger({ + id: consentRequestId, + body: { + browser_identity: browserIdentities, + privacy_experience_id: experience?.id, + privacy_notice_history_ids: experience.privacy_notices.map( + (p) => p.privacy_notice_history_id + ), + serving_component: ServingComponent.PRIVACY_CENTER, + }, + }); + } + }, [ + consentRequestId, + updateNoticesServedMutationTrigger, + experience, + browserIdentities, + ]); + const items = useMemo(() => { if (!experience) { return []; @@ -130,11 +162,6 @@ const NoticeDrivenConsent = () => { * 3. Delete any cookies that have been opted out of */ const handleSave = async () => { - const browserIdentities = inspectForBrowserIdentities(); - const deviceIdentity = { fides_user_device_id: fidesUserDeviceId }; - const identities = browserIdentities - ? { ...deviceIdentity, ...browserIdentities } - : deviceIdentity; const notices = experience?.privacy_notices ?? []; // Reconnect preferences to notices @@ -143,27 +170,32 @@ const NoticeDrivenConsent = () => { const notice = notices.find( (n) => n.privacy_notice_history_id === historyKey ); - return { historyKey, preference, notice }; + const servedNotice = servedNotices?.find( + (sn) => sn.privacy_notice_history.id === historyKey + ); + return { historyKey, preference, notice, servedNotice }; } ); const preferences: ConsentOptionCreate[] = noticePreferences.map( - ({ historyKey, preference, notice }) => { + ({ historyKey, preference, notice, servedNotice }) => { if (notice?.consent_mechanism === ConsentMechanism.NOTICE_ONLY) { return { privacy_notice_history_id: historyKey, preference: UserConsentPreference.ACKNOWLEDGE, + served_notice_history_id: servedNotice?.served_notice_history_id, }; } return { privacy_notice_history_id: historyKey, preference: preference ?? UserConsentPreference.OPT_OUT, + served_notice_history_id: servedNotice?.served_notice_history_id, }; } ); const payload: PrivacyPreferencesRequest = { - browser_identity: identities, + browser_identity: browserIdentities, preferences, user_geography: region, privacy_experience_id: experience?.id, @@ -179,7 +211,8 @@ const NoticeDrivenConsent = () => { if ("error" in result) { toast({ title: "An error occurred while saving user consent preferences", - description: result.error, + description: + typeof result.error === "string" ? result.error : undefined, ...ErrorToastOptions, }); return; diff --git a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts index ce382b73c6..8cd2e8d243 100644 --- a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts @@ -1,5 +1,6 @@ import { ConsentOptionCreate, + LastServedNoticeSchema, PrivacyNoticeResponseWithUserPreferences, } from "~/types/api"; import { CONSENT_COOKIE_NAME, FidesCookie } from "fides-js"; @@ -9,6 +10,9 @@ const VERIFICATION_CODE = "112358"; const PRIVACY_NOTICE_ID_1 = "pri_b4360591-3cc7-400d-a5ff-a9f095ab3061"; const PRIVACY_NOTICE_ID_2 = "pri_b558ab1f-5367-4f0d-94b1-ec06a81ae821"; const PRIVACY_NOTICE_ID_3 = "pri_4bed96d0-b9e3-4596-a807-26b783836375"; +const PRIVACY_NOTICE_HISTORY_ID_1 = "pri_df14051b-1eaf-4f07-ae63-232bffd2dc3e"; +const PRIVACY_NOTICE_HISTORY_ID_2 = "pri_b2a0a2fa-ef59-4f7d-8e3d-d2e9bd076707"; +const PRIVACY_NOTICE_HISTORY_ID_3 = "pri_b09058a7-9f54-4360-8da5-4521e8975d4e"; const PRIVACY_EXPERIENCE_ID = "pri_041acb07-c99b-4085-a435-c0d6f3a42b6f"; const GEOLOCATION_API_URL = "https://www.example.com/location"; const SETTINGS = { @@ -56,6 +60,12 @@ describe("Privacy notice driven consent", () => { fixture: "consent/privacy_preferences.json", } ).as("patchPrivacyPreference"); + // Consent reporting intercept + cy.intercept( + "PATCH", + `${API_URL}/consent-request/consent-request-id/notices-served`, + { fixture: "consent/notices_served.json" } + ).as("patchNoticesServed"); }); describe("when user has not consented before", () => { @@ -300,4 +310,47 @@ describe("Privacy notice driven consent", () => { }); }); }); + + describe("consent reporting", () => { + beforeEach(() => { + // Make the fixture's privacy notice history Ids match + cy.fixture("consent/notices_served.json").then((fixture) => { + // the fixture only has 2 entries, so add a third to match the experience payload + const body = [...fixture, JSON.parse(JSON.stringify(fixture[0]))]; + body[0].privacy_notice_history.id = PRIVACY_NOTICE_HISTORY_ID_1; + body[1].privacy_notice_history.id = PRIVACY_NOTICE_HISTORY_ID_2; + body[2].privacy_notice_history.id = PRIVACY_NOTICE_HISTORY_ID_3; + cy.intercept( + "PATCH", + `${API_URL}/consent-request/consent-request-id/notices-served`, + { body } + ).as("patchMatchingNoticesServed"); + }); + cy.visit("/consent"); + cy.getByTestId("consent"); + cy.overrideSettings(SETTINGS); + }); + + it("can make calls to consent reporting endpoints", () => { + cy.wait("@patchMatchingNoticesServed").then((interception) => { + expect(interception.request.body.privacy_notice_history_ids).to.eql([ + PRIVACY_NOTICE_HISTORY_ID_1, + PRIVACY_NOTICE_HISTORY_ID_2, + PRIVACY_NOTICE_HISTORY_ID_3, + ]); + cy.getByTestId("save-btn").click(); + cy.wait("@patchPrivacyPreference").then((preferenceInterception) => { + const { preferences } = preferenceInterception.request.body; + const expected = interception.response?.body.map( + (s: LastServedNoticeSchema) => s.served_notice_history_id + ); + expect( + preferences.map( + (p: ConsentOptionCreate) => p.served_notice_history_id + ) + ).to.eql(expected); + }); + }); + }); + }); }); diff --git a/clients/privacy-center/features/consent/consent.slice.ts b/clients/privacy-center/features/consent/consent.slice.ts index 8f5d716539..990abc5d89 100644 --- a/clients/privacy-center/features/consent/consent.slice.ts +++ b/clients/privacy-center/features/consent/consent.slice.ts @@ -9,6 +9,8 @@ import { ConsentPreferences, ConsentPreferencesWithVerificationCode, CurrentPrivacyPreferenceSchema, + LastServedNoticeSchema, + NoticesServedRequest, Page_PrivacyExperienceResponse_, PrivacyNoticeRegion, PrivacyPreferencesRequest, @@ -91,6 +93,16 @@ export const consentApi = baseApi.injectEndpoints({ method: "GET", }), }), + updateNoticesServed: build.mutation< + LastServedNoticeSchema[], + { id: string; body: NoticesServedRequest } + >({ + query: ({ id, body }) => ({ + url: `${VerificationType.ConsentRequest}/${id}/notices-served`, + method: "PATCH", + body, + }), + }), }), }); @@ -101,6 +113,7 @@ export const { useGetPrivacyExperienceQuery, useUpdatePrivacyPreferencesMutation, useGetUserGeolocationQuery, + useUpdateNoticesServedMutation, } = consentApi; type State = { diff --git a/clients/privacy-center/types/api/index.ts b/clients/privacy-center/types/api/index.ts index 6262763da9..77ef74f673 100644 --- a/clients/privacy-center/types/api/index.ts +++ b/clients/privacy-center/types/api/index.ts @@ -16,6 +16,8 @@ export { EnforcementLevel } from "./models/EnforcementLevel"; export type { ExperienceConfigResponse } from "./models/ExperienceConfigResponse"; export type { Identity } from "./models/Identity"; export type { IdentityVerificationConfigResponse } from "./models/IdentityVerificationConfigResponse"; +export type { LastServedNoticeSchema } from "./models/LastServedNoticeSchema"; +export type { NoticesServedRequest } from "./models/NoticesServedRequest"; export type { Page_PrivacyExperienceResponse_ } from "./models/Page_PrivacyExperienceResponse_"; export type { PrivacyExperienceResponse } from "./models/PrivacyExperienceResponse"; export type { PrivacyNoticeHistorySchema } from "./models/PrivacyNoticeHistorySchema"; @@ -23,4 +25,5 @@ export { PrivacyNoticeRegion } from "./models/PrivacyNoticeRegion"; export type { PrivacyNoticeResponse } from "./models/PrivacyNoticeResponse"; export type { PrivacyNoticeResponseWithUserPreferences } from "./models/PrivacyNoticeResponseWithUserPreferences"; export type { PrivacyPreferencesRequest } from "./models/PrivacyPreferencesRequest"; +export { ServingComponent } from "./models/ServingComponent"; export { UserConsentPreference } from "./models/UserConsentPreference"; diff --git a/clients/privacy-center/types/api/models/ConsentMethod.ts b/clients/privacy-center/types/api/models/ConsentMethod.ts index 951ebc7a4e..8d0b4dd52c 100644 --- a/clients/privacy-center/types/api/models/ConsentMethod.ts +++ b/clients/privacy-center/types/api/models/ConsentMethod.ts @@ -8,5 +8,5 @@ export enum ConsentMethod { BUTTON = "button", GPC = "gpc", - API = "api", + INDIVIDUAL_NOTICE = "individual_notice", } diff --git a/clients/privacy-center/types/api/models/ConsentOptionCreate.ts b/clients/privacy-center/types/api/models/ConsentOptionCreate.ts index d79b860804..66c42c3ddb 100644 --- a/clients/privacy-center/types/api/models/ConsentOptionCreate.ts +++ b/clients/privacy-center/types/api/models/ConsentOptionCreate.ts @@ -10,4 +10,5 @@ import type { UserConsentPreference } from "./UserConsentPreference"; export type ConsentOptionCreate = { privacy_notice_history_id: string; preference: UserConsentPreference; + served_notice_history_id?: string; }; diff --git a/clients/privacy-center/types/api/models/LastServedNoticeSchema.ts b/clients/privacy-center/types/api/models/LastServedNoticeSchema.ts new file mode 100644 index 0000000000..946448da9a --- /dev/null +++ b/clients/privacy-center/types/api/models/LastServedNoticeSchema.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { PrivacyNoticeHistorySchema } from "./PrivacyNoticeHistorySchema"; + +/** + * Schema that surfaces the last version of a notice that was shown to a user + */ +export type LastServedNoticeSchema = { + id: string; + updated_at: string; + privacy_notice_history: PrivacyNoticeHistorySchema; + served_notice_history_id: string; +}; diff --git a/clients/privacy-center/types/api/models/NoticesServedRequest.ts b/clients/privacy-center/types/api/models/NoticesServedRequest.ts new file mode 100644 index 0000000000..c580c447d2 --- /dev/null +++ b/clients/privacy-center/types/api/models/NoticesServedRequest.ts @@ -0,0 +1,19 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { Identity } from "./Identity"; +import type { ServingComponent } from "./ServingComponent"; + +/** + * Request body when indicating that notices were served in the UI + */ +export type NoticesServedRequest = { + browser_identity: Identity; + code?: string; + privacy_notice_history_ids: Array; + privacy_experience_id?: string; + user_geography?: string; + acknowledge_mode?: boolean; + serving_component: ServingComponent; +}; diff --git a/clients/privacy-center/types/api/models/ServingComponent.ts b/clients/privacy-center/types/api/models/ServingComponent.ts new file mode 100644 index 0000000000..c3625c8261 --- /dev/null +++ b/clients/privacy-center/types/api/models/ServingComponent.ts @@ -0,0 +1,13 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +/** + * An enumeration. + */ +export enum ServingComponent { + OVERLAY = "overlay", + BANNER = "banner", + PRIVACY_CENTER = "privacy_center", + GPC = "gpc", +}