Skip to content

Commit

Permalink
Privacy center consent reporting (#3847)
Browse files Browse the repository at this point in the history
  • Loading branch information
allisonking authored Jul 27, 2023
1 parent 6ef9c2e commit 74c6458
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

### Changed
- Simplified the file structure for HTML DSR packages [#3848](https://github.com/ethyca/fides/pull/3848)
Expand Down
51 changes: 42 additions & 9 deletions clients/privacy-center/components/consent/NoticeDrivenConsent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
selectUserRegion,
selectPrivacyExperience,
useUpdatePrivacyPreferencesMutation,
useUpdateNoticesServedMutation,
} from "~/features/consent/consent.slice";

import {
Expand All @@ -25,6 +26,7 @@ import {
ConsentOptionCreate,
PrivacyNoticeResponseWithUserPreferences,
PrivacyPreferencesRequest,
ServingComponent,
UserConsentPreference,
} from "~/types/api";
import { useRouter } from "next/router";
Expand Down Expand Up @@ -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]) => {
Expand All @@ -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 [];
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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;
Expand Down
53 changes: 53 additions & 0 deletions clients/privacy-center/cypress/e2e/consent-notices.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ConsentOptionCreate,
LastServedNoticeSchema,
PrivacyNoticeResponseWithUserPreferences,
} from "~/types/api";
import { CONSENT_COOKIE_NAME, FidesCookie } from "fides-js";
Expand All @@ -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 = {
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
});
});
});
13 changes: 13 additions & 0 deletions clients/privacy-center/features/consent/consent.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
ConsentPreferences,
ConsentPreferencesWithVerificationCode,
CurrentPrivacyPreferenceSchema,
LastServedNoticeSchema,
NoticesServedRequest,
Page_PrivacyExperienceResponse_,
PrivacyNoticeRegion,
PrivacyPreferencesRequest,
Expand Down Expand Up @@ -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,
}),
}),
}),
});

Expand All @@ -101,6 +113,7 @@ export const {
useGetPrivacyExperienceQuery,
useUpdatePrivacyPreferencesMutation,
useGetUserGeolocationQuery,
useUpdateNoticesServedMutation,
} = consentApi;

type State = {
Expand Down
3 changes: 3 additions & 0 deletions clients/privacy-center/types/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ 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";
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";
2 changes: 1 addition & 1 deletion clients/privacy-center/types/api/models/ConsentMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
export enum ConsentMethod {
BUTTON = "button",
GPC = "gpc",
API = "api",
INDIVIDUAL_NOTICE = "individual_notice",
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ import type { UserConsentPreference } from "./UserConsentPreference";
export type ConsentOptionCreate = {
privacy_notice_history_id: string;
preference: UserConsentPreference;
served_notice_history_id?: string;
};
15 changes: 15 additions & 0 deletions clients/privacy-center/types/api/models/LastServedNoticeSchema.ts
Original file line number Diff line number Diff line change
@@ -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;
};
19 changes: 19 additions & 0 deletions clients/privacy-center/types/api/models/NoticesServedRequest.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
privacy_experience_id?: string;
user_geography?: string;
acknowledge_mode?: boolean;
serving_component: ServingComponent;
};
13 changes: 13 additions & 0 deletions clients/privacy-center/types/api/models/ServingComponent.ts
Original file line number Diff line number Diff line change
@@ -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",
}

0 comments on commit 74c6458

Please sign in to comment.