Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Privacy center consent reporting #3847

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

## [2.17.0](https://github.com/ethyca/fides/compare/2.16.0...2.17.0)

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",
}