diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b3c36cfb23..ac2b6f29736 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,8 @@ The types of changes are: ### Developer Experience - Migrate toggle switches from Chakra to Ant Design [#5323](https://github.com/ethyca/fides/pull/5323) +- Replace `debugLog` with global scoped `fidesDebugger` for better debug experience and optimization of prod code [#5335](https://github.com/ethyca/fides/pull/5335) + ### Fixed - Updating the hash migration status check query to use the available indexes [#5336](https://github.com/ethyca/fides/pull/5336) diff --git a/clients/admin-ui/global.d.ts b/clients/admin-ui/global.d.ts new file mode 100644 index 00000000000..84693101e0d --- /dev/null +++ b/clients/admin-ui/global.d.ts @@ -0,0 +1,4 @@ +declare module globalThis { + // needs to be in global scope of Admin UI for when we import fides-js components which contain fidesDebugger + let fidesDebugger: (...args: unknown[]) => void; +} diff --git a/clients/fides-js/__tests__/lib/cookie.test.ts b/clients/fides-js/__tests__/lib/cookie.test.ts index f6a0242477d..576083b1c3b 100644 --- a/clients/fides-js/__tests__/lib/cookie.test.ts +++ b/clients/fides-js/__tests__/lib/cookie.test.ts @@ -68,489 +68,509 @@ jest.mock("js-cookie", () => ({ })), })); -describe("makeFidesCookie", () => { - it("generates a v0.9.0 cookie with uuid", () => { - const cookie: FidesCookie = makeFidesCookie(); - expect(cookie).toEqual({ - consent: {}, - fides_meta: { - createdAt: MOCK_DATE, - updatedAt: "", - version: "0.9.0", - }, - identity: { - fides_user_device_id: MOCK_UUID, - }, - tcf_consent: {}, - }); - }); - - it("accepts default consent preferences", () => { - const defaults: NoticeConsent = { - essential: true, - performance: false, - data_sales: true, - secrets: false, - }; - const cookie: FidesCookie = makeFidesCookie(defaults); - expect(cookie.consent).toEqual(defaults); - }); -}); - -describe("getOrMakeFidesCookie", () => { - describe("when no saved cookie exists", () => { - beforeEach(() => { - mockGetCookie.mockReturnValue(undefined); - }); - it("makes and returns a default cookie", () => { - const cookie: FidesCookie = getOrMakeFidesCookie(); - expect(cookie.consent).toEqual({}); - expect(cookie.fides_meta.consentMethod).toEqual(undefined); - expect(cookie.fides_meta.createdAt).toEqual(MOCK_DATE); - expect(cookie.fides_meta.updatedAt).toEqual(""); - expect(cookie.identity.fides_user_device_id).toEqual(MOCK_UUID); - }); +describe("cookies", () => { + beforeAll(() => { + window.fidesDebugger = () => {}; }); - - describe("when a saved cookie exists", () => { - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const SAVED_UUID = "8a46c3ee-d6c3-4518-9b6c-074528b7bfd0"; - const SAVED_CONSENT = { data_sales: false, performance: true }; - - describe("in v0.9.0 format", () => { - const V090_COOKIE_OBJECT: FidesCookie = { - consent: SAVED_CONSENT, - identity: { fides_user_device_id: SAVED_UUID }, + describe("makeFidesCookie", () => { + it("generates a v0.9.0 cookie with uuid", () => { + const cookie: FidesCookie = makeFidesCookie(); + expect(cookie).toEqual({ + consent: {}, fides_meta: { - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, + createdAt: MOCK_DATE, + updatedAt: "", version: "0.9.0", }, + identity: { + fides_user_device_id: MOCK_UUID, + }, tcf_consent: {}, + }); + }); + + it("accepts default consent preferences", () => { + const defaults: NoticeConsent = { + essential: true, + performance: false, + data_sales: true, + secrets: false, }; + const cookie: FidesCookie = makeFidesCookie(defaults); + expect(cookie.consent).toEqual(defaults); + }); + }); - it("returns the saved cookie", () => { - mockGetCookie.mockReturnValue(JSON.stringify(V090_COOKIE_OBJECT)); + describe("getOrMakeFidesCookie", () => { + describe("when no saved cookie exists", () => { + beforeEach(() => { + mockGetCookie.mockReturnValue(undefined); + }); + it("makes and returns a default cookie", () => { const cookie: FidesCookie = getOrMakeFidesCookie(); - expect(cookie.consent).toEqual(SAVED_CONSENT); + expect(cookie.consent).toEqual({}); expect(cookie.fides_meta.consentMethod).toEqual(undefined); - expect(cookie.fides_meta.createdAt).toEqual(CREATED_DATE); - expect(cookie.fides_meta.updatedAt).toEqual(UPDATED_DATE); - expect(cookie.identity.fides_user_device_id).toEqual(SAVED_UUID); - expect(cookie.tcf_consent).toEqual({}); + expect(cookie.fides_meta.createdAt).toEqual(MOCK_DATE); + expect(cookie.fides_meta.updatedAt).toEqual(""); + expect(cookie.identity.fides_user_device_id).toEqual(MOCK_UUID); }); + }); - it("returns the saved cookie including optional fides_meta details like consentMethod", () => { - // extend the cookie object with some extra details on fides_meta - const extendedFidesMeta: FidesJSMeta = { - ...V090_COOKIE_OBJECT.fides_meta, - ...{ consentMethod: "accept", otherMetadata: "foo" }, + describe("when a saved cookie exists", () => { + const CREATED_DATE = "2022-12-24T12:00:00.000Z"; + const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; + const SAVED_UUID = "8a46c3ee-d6c3-4518-9b6c-074528b7bfd0"; + const SAVED_CONSENT = { data_sales: false, performance: true }; + + describe("in v0.9.0 format", () => { + const V090_COOKIE_OBJECT: FidesCookie = { + consent: SAVED_CONSENT, + identity: { fides_user_device_id: SAVED_UUID }, + fides_meta: { + createdAt: CREATED_DATE, + updatedAt: UPDATED_DATE, + version: "0.9.0", + }, + tcf_consent: {}, }; - const cookieObject = { - ...V090_COOKIE_OBJECT, - ...{ fides_meta: extendedFidesMeta }, + + it("returns the saved cookie", () => { + mockGetCookie.mockReturnValue(JSON.stringify(V090_COOKIE_OBJECT)); + const cookie: FidesCookie = getOrMakeFidesCookie(); + expect(cookie.consent).toEqual(SAVED_CONSENT); + expect(cookie.fides_meta.consentMethod).toEqual(undefined); + expect(cookie.fides_meta.createdAt).toEqual(CREATED_DATE); + expect(cookie.fides_meta.updatedAt).toEqual(UPDATED_DATE); + expect(cookie.identity.fides_user_device_id).toEqual(SAVED_UUID); + expect(cookie.tcf_consent).toEqual({}); + }); + + it("returns the saved cookie including optional fides_meta details like consentMethod", () => { + // extend the cookie object with some extra details on fides_meta + const extendedFidesMeta: FidesJSMeta = { + ...V090_COOKIE_OBJECT.fides_meta, + ...{ consentMethod: "accept", otherMetadata: "foo" }, + }; + const cookieObject = { + ...V090_COOKIE_OBJECT, + ...{ fides_meta: extendedFidesMeta }, + }; + mockGetCookie.mockReturnValue(JSON.stringify(cookieObject)); + const cookie: FidesCookie = getOrMakeFidesCookie(); + expect(cookie.consent).toEqual(SAVED_CONSENT); + expect(cookie.fides_meta.consentMethod).toEqual("accept"); + expect(cookie.fides_meta.otherMetadata).toEqual("foo"); + expect(cookie.fides_meta.createdAt).toEqual(CREATED_DATE); + expect(cookie.fides_meta.updatedAt).toEqual(UPDATED_DATE); + expect(cookie.identity.fides_user_device_id).toEqual(SAVED_UUID); + expect(cookie.tcf_consent).toEqual({}); + }); + }); + + describe("in legacy format", () => { + // Legacy cookie only contains the consent preferences + const V0_COOKIE = JSON.stringify(SAVED_CONSENT); + beforeEach(() => mockGetCookie.mockReturnValue(V0_COOKIE)); + + it("returns the saved cookie and converts to new 0.9.0 format", () => { + const cookie: FidesCookie = getOrMakeFidesCookie(); + expect(cookie.consent).toEqual(SAVED_CONSENT); + expect(cookie.fides_meta.consentMethod).toEqual(undefined); + expect(cookie.fides_meta.createdAt).toEqual(MOCK_DATE); + expect(cookie.identity.fides_user_device_id).toEqual(MOCK_UUID); + expect(cookie.tcf_consent).toEqual({}); + }); + }); + describe("in base64 format", () => { + const V090_COOKIE_OBJECT: FidesCookie = { + consent: SAVED_CONSENT, + identity: { fides_user_device_id: SAVED_UUID }, + fides_meta: { + createdAt: CREATED_DATE, + updatedAt: UPDATED_DATE, + version: "0.9.0", + }, + tcf_consent: {}, }; - mockGetCookie.mockReturnValue(JSON.stringify(cookieObject)); - const cookie: FidesCookie = getOrMakeFidesCookie(); - expect(cookie.consent).toEqual(SAVED_CONSENT); - expect(cookie.fides_meta.consentMethod).toEqual("accept"); - expect(cookie.fides_meta.otherMetadata).toEqual("foo"); - expect(cookie.fides_meta.createdAt).toEqual(CREATED_DATE); - expect(cookie.fides_meta.updatedAt).toEqual(UPDATED_DATE); - expect(cookie.identity.fides_user_device_id).toEqual(SAVED_UUID); - expect(cookie.tcf_consent).toEqual({}); + // mock base64 cookie + mockGetCookie.mockReturnValue( + base64_encode(JSON.stringify(V090_COOKIE_OBJECT)), + ); + + it("returns the saved cookie and decodes from base64", () => { + const cookie: FidesCookie = getOrMakeFidesCookie(); + expect(cookie.consent).toEqual(SAVED_CONSENT); + expect(cookie.fides_meta.consentMethod).toEqual(undefined); + expect(cookie.fides_meta.createdAt).toEqual(MOCK_DATE); + expect(cookie.identity.fides_user_device_id).toEqual(MOCK_UUID); + expect(cookie.tcf_consent).toEqual({}); + }); }); }); + }); + + describe("saveFidesCookie", () => { + beforeEach(() => + mockGetCookie.mockReturnValue( + JSON.stringify({ fides_meta: { updatedAt: MOCK_DATE } }), + ), + ); + afterEach(() => mockSetCookie.mockClear()); + + it("updates the updatedAt date", () => { + const cookie: FidesCookie = getOrMakeFidesCookie(); + expect(cookie.fides_meta.updatedAt).toEqual(""); + saveFidesCookie(cookie, false); + expect(cookie.fides_meta.updatedAt).toEqual(MOCK_DATE); + }); - describe("in legacy format", () => { - // Legacy cookie only contains the consent preferences - const V0_COOKIE = JSON.stringify(SAVED_CONSENT); - beforeEach(() => mockGetCookie.mockReturnValue(V0_COOKIE)); + it("saves optional fides_meta details like consentMethod", () => { + const cookie: FidesCookie = getOrMakeFidesCookie(); + cookie.fides_meta.consentMethod = "dismiss"; + saveFidesCookie(cookie, false); + expect(mockSetCookie.mock.calls).toHaveLength(1); + expect(mockSetCookie.mock.calls[0][0]).toEqual("fides_consent"); // name + const cookieValue = mockSetCookie.mock.calls[0][1]; + const cookieParsed = JSON.parse(cookieValue); + expect(cookieParsed.fides_meta).toHaveProperty("consentMethod"); + expect(cookieParsed.fides_meta.consentMethod).toEqual("dismiss"); + }); + + it("sets a cookie on the root domain with 1 year expiry date", () => { + const cookie: FidesCookie = getOrMakeFidesCookie(); + saveFidesCookie(cookie, false); + const expectedCookieString = JSON.stringify(cookie); + expect(mockSetCookie.mock.calls).toHaveLength(1); + const [name, value, attributes] = mockSetCookie.mock.calls[0]; + expect(name).toEqual("fides_consent"); + expect(value).toEqual(expectedCookieString); + expect(attributes).toHaveProperty("domain", "localhost"); + expect(attributes).toHaveProperty("expires", 365); + }); - it("returns the saved cookie and converts to new 0.9.0 format", () => { + it("sets a base64 cookie", () => { + const cookie: FidesCookie = getOrMakeFidesCookie(); + saveFidesCookie(cookie, true); + const expectedCookieString = base64_encode(JSON.stringify(cookie)); + expect(mockSetCookie.mock.calls).toHaveLength(1); + const [name, value, attributes] = mockSetCookie.mock.calls[0]; + expect(name).toEqual("fides_consent"); + expect(value).toEqual(expectedCookieString); + expect(attributes).toHaveProperty("domain", "localhost"); + expect(attributes).toHaveProperty("expires", 365); + }); + + it.each([ + { url: "https://example.com", expected: "example.com" }, + { url: "https://www.another.com", expected: "another.com" }, + { url: "https://privacy.bigco.ca", expected: "bigco.ca" }, + { url: "https://privacy.subdomain.example.org", expected: "example.org" }, + { + url: "https://privacy.subdomain.example.co.uk", + expected: "example.co.uk", + }, + { + url: "https://example.co.in", + expected: "example.co.in", + }, + { + url: "https://example.co.jp", + expected: "example.co.jp", + }, + ])( + "calculates the root domain from the hostname ($url)", + ({ url, expected }) => { + const mockUrl = new URL(url); + Object.defineProperty(window, "location", { + value: mockUrl, + writable: true, + }); const cookie: FidesCookie = getOrMakeFidesCookie(); - expect(cookie.consent).toEqual(SAVED_CONSENT); - expect(cookie.fides_meta.consentMethod).toEqual(undefined); - expect(cookie.fides_meta.createdAt).toEqual(MOCK_DATE); - expect(cookie.identity.fides_user_device_id).toEqual(MOCK_UUID); - expect(cookie.tcf_consent).toEqual({}); + saveFidesCookie(cookie); + const numCalls = expected.split(".").length; + expect(mockSetCookie.mock.calls).toHaveLength(numCalls); + expect(mockSetCookie.mock.calls[numCalls - 1][2]).toHaveProperty( + "domain", + expected, + ); + }, + ); + }); + + describe("makeConsentDefaultsLegacy", () => { + const config: LegacyConsentConfig = { + options: [ + { + cookieKeys: ["default_undefined"], + fidesDataUseKey: "provide.service", + }, + { + cookieKeys: ["default_true"], + default: true, + fidesDataUseKey: "functional.service.improve", + }, + { + cookieKeys: ["default_false"], + default: false, + fidesDataUseKey: "personalize.system", + }, + { + cookieKeys: ["default_true_with_gpc_false"], + default: { value: true, globalPrivacyControl: false }, + fidesDataUseKey: "advertising.third_party", + }, + { + cookieKeys: ["default_false_with_gpc_true"], + default: { value: false, globalPrivacyControl: true }, + fidesDataUseKey: "third_party_sharing.payment_processing", + }, + ], + }; + + describe("when global privacy control is not present", () => { + const context: ConsentContext = {}; + + it("returns the default consent values by key", () => { + expect(makeConsentDefaultsLegacy(config, context)).toEqual({ + default_true: true, + default_false: false, + default_true_with_gpc_false: true, + default_false_with_gpc_true: false, + }); + }); + }); + + describe("when global privacy control is set", () => { + const context: ConsentContext = { + globalPrivacyControl: true, + }; + + it("returns the default consent values by key", () => { + expect(makeConsentDefaultsLegacy(config, context)).toEqual({ + default_true: true, + default_false: false, + default_true_with_gpc_false: false, + default_false_with_gpc_true: true, + }); }); }); - describe("in base64 format", () => { + }); + + describe("isNewFidesCookie", () => { + it("returns true for new cookies", () => { + const newCookie: FidesCookie = getOrMakeFidesCookie(); + expect(isNewFidesCookie(newCookie)).toBeTruthy(); + }); + + describe("when a saved cookie exists", () => { + const CONSENT_METHOD = "accept"; + const CREATED_DATE = "2022-12-24T12:00:00.000Z"; + const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; + const SAVED_UUID = "8a46c3ee-d6c3-4518-9b6c-074528b7bfd0"; + const SAVED_CONSENT = { data_sales: false, performance: true }; const V090_COOKIE_OBJECT: FidesCookie = { consent: SAVED_CONSENT, identity: { fides_user_device_id: SAVED_UUID }, fides_meta: { + consentMethod: CONSENT_METHOD, createdAt: CREATED_DATE, updatedAt: UPDATED_DATE, version: "0.9.0", }, tcf_consent: {}, }; - // mock base64 cookie - mockGetCookie.mockReturnValue( - base64_encode(JSON.stringify(V090_COOKIE_OBJECT)), - ); - - it("returns the saved cookie and decodes from base64", () => { - const cookie: FidesCookie = getOrMakeFidesCookie(); - expect(cookie.consent).toEqual(SAVED_CONSENT); - expect(cookie.fides_meta.consentMethod).toEqual(undefined); - expect(cookie.fides_meta.createdAt).toEqual(MOCK_DATE); - expect(cookie.identity.fides_user_device_id).toEqual(MOCK_UUID); - expect(cookie.tcf_consent).toEqual({}); + const V090_COOKIE = JSON.stringify(V090_COOKIE_OBJECT); + beforeEach(() => mockGetCookie.mockReturnValue(V090_COOKIE)); + + it("returns false for saved cookies", () => { + const savedCookie: FidesCookie = getOrMakeFidesCookie(); + expect(savedCookie.fides_meta.createdAt).toEqual(CREATED_DATE); + expect(savedCookie.fides_meta.updatedAt).toEqual(UPDATED_DATE); + expect(isNewFidesCookie(savedCookie)).toBeFalsy(); }); }); }); -}); - -describe("saveFidesCookie", () => { - beforeEach(() => - mockGetCookie.mockReturnValue( - JSON.stringify({ fides_meta: { updatedAt: MOCK_DATE } }), - ), - ); - afterEach(() => mockSetCookie.mockClear()); - - it("updates the updatedAt date", () => { - const cookie: FidesCookie = getOrMakeFidesCookie(); - expect(cookie.fides_meta.updatedAt).toEqual(""); - saveFidesCookie(cookie, false); - expect(cookie.fides_meta.updatedAt).toEqual(MOCK_DATE); - }); - it("saves optional fides_meta details like consentMethod", () => { - const cookie: FidesCookie = getOrMakeFidesCookie(); - cookie.fides_meta.consentMethod = "dismiss"; - saveFidesCookie(cookie, false); - expect(mockSetCookie.mock.calls).toHaveLength(1); - expect(mockSetCookie.mock.calls[0][0]).toEqual("fides_consent"); // name - const cookieValue = mockSetCookie.mock.calls[0][1]; - const cookieParsed = JSON.parse(cookieValue); - expect(cookieParsed.fides_meta).toHaveProperty("consentMethod"); - expect(cookieParsed.fides_meta.consentMethod).toEqual("dismiss"); - }); - - it("sets a cookie on the root domain with 1 year expiry date", () => { - const cookie: FidesCookie = getOrMakeFidesCookie(); - saveFidesCookie(cookie, false); - const expectedCookieString = JSON.stringify(cookie); - expect(mockSetCookie.mock.calls).toHaveLength(1); - const [name, value, attributes] = mockSetCookie.mock.calls[0]; - expect(name).toEqual("fides_consent"); - expect(value).toEqual(expectedCookieString); - expect(attributes).toHaveProperty("domain", "localhost"); - expect(attributes).toHaveProperty("expires", 365); - }); - - it("sets a base64 cookie", () => { - const cookie: FidesCookie = getOrMakeFidesCookie(); - saveFidesCookie(cookie, true); - const expectedCookieString = base64_encode(JSON.stringify(cookie)); - expect(mockSetCookie.mock.calls).toHaveLength(1); - const [name, value, attributes] = mockSetCookie.mock.calls[0]; - expect(name).toEqual("fides_consent"); - expect(value).toEqual(expectedCookieString); - expect(attributes).toHaveProperty("domain", "localhost"); - expect(attributes).toHaveProperty("expires", 365); - }); - - it.each([ - { url: "https://example.com", expected: "example.com" }, - { url: "https://www.another.com", expected: "another.com" }, - { url: "https://privacy.bigco.ca", expected: "bigco.ca" }, - { url: "https://privacy.subdomain.example.org", expected: "example.org" }, - { - url: "https://privacy.subdomain.example.co.uk", - expected: "example.co.uk", - }, - { - url: "https://example.co.in", - expected: "example.co.in", - }, - { - url: "https://example.co.jp", - expected: "example.co.jp", - }, - ])( - "calculates the root domain from the hostname ($url)", - ({ url, expected }) => { - const mockUrl = new URL(url); - Object.defineProperty(window, "location", { - value: mockUrl, - writable: true, - }); - const cookie: FidesCookie = getOrMakeFidesCookie(); - saveFidesCookie(cookie); - const numCalls = expected.split(".").length; - expect(mockSetCookie.mock.calls).toHaveLength(numCalls); - expect(mockSetCookie.mock.calls[numCalls - 1][2]).toHaveProperty( - "domain", - expected, - ); - }, - ); -}); + describe("removeCookiesFromBrowser", () => { + afterEach(() => mockRemoveCookie.mockClear()); -describe("makeConsentDefaultsLegacy", () => { - const config: LegacyConsentConfig = { - options: [ + it.each([ + { cookies: [], expectedAttributes: [] }, + { cookies: [{ name: "_ga123" }], expectedAttributes: [{ path: "/" }] }, { - cookieKeys: ["default_undefined"], - fidesDataUseKey: "provide.service", + cookies: [{ name: "_ga123", path: "" }], + expectedAttributes: [{ path: "" }], }, { - cookieKeys: ["default_true"], - default: true, - fidesDataUseKey: "functional.service.improve", + cookies: [{ name: "_ga123", path: "/subpage" }], + expectedAttributes: [{ path: "/subpage" }], }, { - cookieKeys: ["default_false"], - default: false, - fidesDataUseKey: "personalize.system", + cookies: [{ name: "_ga123" }, { name: "shopify" }], + expectedAttributes: [{ path: "/" }, { path: "/" }], }, - { - cookieKeys: ["default_true_with_gpc_false"], - default: { value: true, globalPrivacyControl: false }, - fidesDataUseKey: "advertising.third_party", - }, - { - cookieKeys: ["default_false_with_gpc_true"], - default: { value: false, globalPrivacyControl: true }, - fidesDataUseKey: "third_party_sharing.payment_processing", + ])( + "should remove a list of cookies", + ({ + cookies, + expectedAttributes, + }: { + cookies: CookiesType[]; + expectedAttributes: CookieAttributes[]; + }) => { + removeCookiesFromBrowser(cookies); + expect(mockRemoveCookie.mock.calls).toHaveLength(cookies.length); + cookies.forEach((cookie, idx) => { + const [name, attributes] = mockRemoveCookie.mock.calls[idx]; + expect(name).toEqual(cookie.name); + expect(attributes).toEqual(expectedAttributes[idx]); + }); }, - ], - }; - - describe("when global privacy control is not present", () => { - const context: ConsentContext = {}; - - it("returns the default consent values by key", () => { - expect(makeConsentDefaultsLegacy(config, context, false)).toEqual({ - default_true: true, - default_false: false, - default_true_with_gpc_false: true, - default_false_with_gpc_true: false, - }); - }); + ); }); - describe("when global privacy control is set", () => { - const context: ConsentContext = { - globalPrivacyControl: true, - }; - - it("returns the default consent values by key", () => { - expect(makeConsentDefaultsLegacy(config, context, false)).toEqual({ - default_true: true, - default_false: false, - default_true_with_gpc_false: false, - default_false_with_gpc_true: true, - }); + describe("transformTcfPreferencesToCookieKeys", () => { + it("can handle empty preferences", () => { + const preferences: TcfSavePreferences = { + purpose_consent_preferences: [], + }; + const expected: TcfOtherConsent = { + system_consent_preferences: {}, + system_legitimate_interests_preferences: {}, + }; + expect(transformTcfPreferencesToCookieKeys(preferences)).toEqual( + expected, + ); }); - }); -}); - -describe("isNewFidesCookie", () => { - it("returns true for new cookies", () => { - const newCookie: FidesCookie = getOrMakeFidesCookie(); - expect(isNewFidesCookie(newCookie)).toBeTruthy(); - }); - describe("when a saved cookie exists", () => { - const CONSENT_METHOD = "accept"; - const CREATED_DATE = "2022-12-24T12:00:00.000Z"; - const UPDATED_DATE = "2022-12-25T12:00:00.000Z"; - const SAVED_UUID = "8a46c3ee-d6c3-4518-9b6c-074528b7bfd0"; - const SAVED_CONSENT = { data_sales: false, performance: true }; - const V090_COOKIE_OBJECT: FidesCookie = { - consent: SAVED_CONSENT, - identity: { fides_user_device_id: SAVED_UUID }, - fides_meta: { - consentMethod: CONSENT_METHOD, - createdAt: CREATED_DATE, - updatedAt: UPDATED_DATE, - version: "0.9.0", - }, - tcf_consent: {}, - }; - const V090_COOKIE = JSON.stringify(V090_COOKIE_OBJECT); - beforeEach(() => mockGetCookie.mockReturnValue(V090_COOKIE)); - - it("returns false for saved cookies", () => { - const savedCookie: FidesCookie = getOrMakeFidesCookie(); - expect(savedCookie.fides_meta.createdAt).toEqual(CREATED_DATE); - expect(savedCookie.fides_meta.updatedAt).toEqual(UPDATED_DATE); - expect(isNewFidesCookie(savedCookie)).toBeFalsy(); + it("can transform", () => { + const preferences: TcfSavePreferences = { + purpose_consent_preferences: [ + { id: 1, preference: UserConsentPreference.OPT_IN }, + ], + purpose_legitimate_interests_preferences: [ + { id: 1, preference: UserConsentPreference.OPT_OUT }, + ], + special_feature_preferences: [ + { id: 1, preference: UserConsentPreference.OPT_IN }, + { id: 2, preference: UserConsentPreference.OPT_OUT }, + ], + vendor_consent_preferences: [ + { id: "1111", preference: UserConsentPreference.OPT_OUT }, + ], + vendor_legitimate_interests_preferences: [ + { id: "1111", preference: UserConsentPreference.OPT_IN }, + ], + system_consent_preferences: [ + { id: "ctl_test_system", preference: UserConsentPreference.OPT_IN }, + ], + system_legitimate_interests_preferences: [ + { id: "ctl_test_system", preference: UserConsentPreference.OPT_IN }, + ], + }; + const expected: TcfOtherConsent = { + system_consent_preferences: { ctl_test_system: true }, + system_legitimate_interests_preferences: { ctl_test_system: true }, + }; + expect(transformTcfPreferencesToCookieKeys(preferences)).toEqual( + expected, + ); }); }); -}); -describe("removeCookiesFromBrowser", () => { - afterEach(() => mockRemoveCookie.mockClear()); - - it.each([ - { cookies: [], expectedAttributes: [] }, - { cookies: [{ name: "_ga123" }], expectedAttributes: [{ path: "/" }] }, - { - cookies: [{ name: "_ga123", path: "" }], - expectedAttributes: [{ path: "" }], - }, - { - cookies: [{ name: "_ga123", path: "/subpage" }], - expectedAttributes: [{ path: "/subpage" }], - }, - { - cookies: [{ name: "_ga123" }, { name: "shopify" }], - expectedAttributes: [{ path: "/" }, { path: "/" }], - }, - ])( - "should remove a list of cookies", - ({ - cookies, - expectedAttributes, - }: { - cookies: CookiesType[]; - expectedAttributes: CookieAttributes[]; - }) => { - removeCookiesFromBrowser(cookies); - expect(mockRemoveCookie.mock.calls).toHaveLength(cookies.length); - cookies.forEach((cookie, idx) => { - const [name, attributes] = mockRemoveCookie.mock.calls[idx]; - expect(name).toEqual(cookie.name); - expect(attributes).toEqual(expectedAttributes[idx]); - }); - }, - ); -}); + describe("updateExperienceFromCookieConsent", () => { + const baseCookie = makeFidesCookie(); -describe("transformTcfPreferencesToCookieKeys", () => { - it("can handle empty preferences", () => { - const preferences: TcfSavePreferences = { purpose_consent_preferences: [] }; - const expected: TcfOtherConsent = { - system_consent_preferences: {}, - system_legitimate_interests_preferences: {}, - }; - expect(transformTcfPreferencesToCookieKeys(preferences)).toEqual(expected); - }); - - it("can transform", () => { - const preferences: TcfSavePreferences = { - purpose_consent_preferences: [ - { id: 1, preference: UserConsentPreference.OPT_IN }, - ], - purpose_legitimate_interests_preferences: [ - { id: 1, preference: UserConsentPreference.OPT_OUT }, - ], - special_feature_preferences: [ - { id: 1, preference: UserConsentPreference.OPT_IN }, - { id: 2, preference: UserConsentPreference.OPT_OUT }, - ], - vendor_consent_preferences: [ - { id: "1111", preference: UserConsentPreference.OPT_OUT }, - ], - vendor_legitimate_interests_preferences: [ - { id: "1111", preference: UserConsentPreference.OPT_IN }, - ], - system_consent_preferences: [ - { id: "ctl_test_system", preference: UserConsentPreference.OPT_IN }, - ], - system_legitimate_interests_preferences: [ - { id: "ctl_test_system", preference: UserConsentPreference.OPT_IN }, - ], - }; - const expected: TcfOtherConsent = { - system_consent_preferences: { ctl_test_system: true }, - system_legitimate_interests_preferences: { ctl_test_system: true }, - }; - expect(transformTcfPreferencesToCookieKeys(preferences)).toEqual(expected); - }); -}); + // Notice test data + const notices = [ + { notice_key: "one" }, + { notice_key: "two" }, + { notice_key: "three" }, + ] as PrivacyExperience["privacy_notices"]; + const experienceWithNotices = { + privacy_notices: notices, + } as PrivacyExperience; + + describe("notices", () => { + it("can handle an empty cookie", () => { + const cookie = { ...baseCookie, consent: {} }; + const updatedExperience = updateExperienceFromCookieConsentNotices({ + experience: experienceWithNotices, + cookie, + }); + expect(updatedExperience.privacy_notices).toEqual([ + { notice_key: "one", current_preference: undefined }, + { notice_key: "two", current_preference: undefined }, + { notice_key: "three", current_preference: undefined }, + ]); + }); -describe("updateExperienceFromCookieConsent", () => { - const baseCookie = makeFidesCookie(); - - // Notice test data - const notices = [ - { notice_key: "one" }, - { notice_key: "two" }, - { notice_key: "three" }, - ] as PrivacyExperience["privacy_notices"]; - const experienceWithNotices = { - privacy_notices: notices, - } as PrivacyExperience; - - describe("notices", () => { - it("can handle an empty cookie", () => { - const cookie = { ...baseCookie, consent: {} }; - const updatedExperience = updateExperienceFromCookieConsentNotices({ - experience: experienceWithNotices, - cookie, + it("can handle updating preferences", () => { + const cookie = { ...baseCookie, consent: { one: true, two: false } }; + const updatedExperience = updateExperienceFromCookieConsentNotices({ + experience: experienceWithNotices, + cookie, + }); + expect(updatedExperience.privacy_notices).toEqual([ + { + notice_key: "one", + current_preference: UserConsentPreference.OPT_IN, + }, + { + notice_key: "two", + current_preference: UserConsentPreference.OPT_OUT, + }, + { notice_key: "three", current_preference: undefined }, + ]); }); - expect(updatedExperience.privacy_notices).toEqual([ - { notice_key: "one", current_preference: undefined }, - { notice_key: "two", current_preference: undefined }, - { notice_key: "three", current_preference: undefined }, - ]); - }); - it("can handle updating preferences", () => { - const cookie = { ...baseCookie, consent: { one: true, two: false } }; - const updatedExperience = updateExperienceFromCookieConsentNotices({ - experience: experienceWithNotices, - cookie, + it("can handle when cookie has values not in the experience", () => { + const cookie = { + ...baseCookie, + consent: { one: true, two: false, fake: true }, + }; + const updatedExperience = updateExperienceFromCookieConsentNotices({ + experience: experienceWithNotices, + cookie, + }); + expect(updatedExperience.privacy_notices).toEqual([ + { + notice_key: "one", + current_preference: UserConsentPreference.OPT_IN, + }, + { + notice_key: "two", + current_preference: UserConsentPreference.OPT_OUT, + }, + { notice_key: "three", current_preference: undefined }, + ]); }); - expect(updatedExperience.privacy_notices).toEqual([ - { notice_key: "one", current_preference: UserConsentPreference.OPT_IN }, - { - notice_key: "two", - current_preference: UserConsentPreference.OPT_OUT, - }, - { notice_key: "three", current_preference: undefined }, - ]); }); + }); - it("can handle when cookie has values not in the experience", () => { - const cookie = { - ...baseCookie, - consent: { one: true, two: false, fake: true }, - }; - const updatedExperience = updateExperienceFromCookieConsentNotices({ - experience: experienceWithNotices, - cookie, - }); - expect(updatedExperience.privacy_notices).toEqual([ + describe("updateCookieFromNoticePreferences", () => { + it("can receive an updated cookie obj based on notice preferences", async () => { + const cookie = makeFidesCookie(); + const notices = [ { notice_key: "one", current_preference: UserConsentPreference.OPT_IN }, { notice_key: "two", current_preference: UserConsentPreference.OPT_OUT, }, - { notice_key: "three", current_preference: undefined }, - ]); + ] as PrivacyNoticeWithPreference[]; + const preferences = notices.map( + (n) => + new SaveConsentPreference( + n, + n.current_preference ?? UserConsentPreference.OPT_OUT, + `pri_notice-history-mock-${n.notice_key}`, + ), + ); + const updatedCookie = await updateCookieFromNoticePreferences( + cookie, + preferences, + ); + expect(updatedCookie.consent).toEqual({ one: true, two: false }); }); }); }); - -describe("updateCookieFromNoticePreferences", () => { - it("can receive an updated cookie obj based on notice preferences", async () => { - const cookie = makeFidesCookie(); - const notices = [ - { notice_key: "one", current_preference: UserConsentPreference.OPT_IN }, - { notice_key: "two", current_preference: UserConsentPreference.OPT_OUT }, - ] as PrivacyNoticeWithPreference[]; - const preferences = notices.map( - (n) => - new SaveConsentPreference( - n, - n.current_preference ?? UserConsentPreference.OPT_OUT, - `pri_notice-history-mock-${n.notice_key}`, - ), - ); - const updatedCookie = await updateCookieFromNoticePreferences( - cookie, - preferences, - ); - expect(updatedCookie.consent).toEqual({ one: true, two: false }); - }); -}); diff --git a/clients/fides-js/__tests__/lib/i18n/i18n-utils.test.ts b/clients/fides-js/__tests__/lib/i18n/i18n-utils.test.ts index 97a6d640e95..5792d545e74 100644 --- a/clients/fides-js/__tests__/lib/i18n/i18n-utils.test.ts +++ b/clients/fides-js/__tests__/lib/i18n/i18n-utils.test.ts @@ -39,6 +39,9 @@ import mockExperienceJSON from "../../__fixtures__/mock_experience.json"; import mockGVLTranslationsJSON from "../../__fixtures__/mock_gvl_translations.json"; describe("i18n-utils", () => { + beforeAll(() => { + window.fidesDebugger = () => {}; + }); // Define a mock implementation of the i18n singleton for tests let mockCurrentLocale = ""; let mockDefaultLocale = DEFAULT_LOCALE; diff --git a/clients/fides-js/__tests__/lib/tcf/fidesString.ts b/clients/fides-js/__tests__/lib/tcf/fidesString.ts index be47efb6935..490ddf9d936 100644 --- a/clients/fides-js/__tests__/lib/tcf/fidesString.ts +++ b/clients/fides-js/__tests__/lib/tcf/fidesString.ts @@ -3,84 +3,93 @@ import { idsFromAcString, } from "../../../src/lib/tcf/fidesString"; -describe("decodeFidesString", () => { - it.each([ - // Empty string - { - fidesString: "", - expected: { tc: "", ac: "" }, - }, - // TC string only - { - fidesString: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", - expected: { - tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", - ac: "", +describe("fidesString", () => { + beforeAll(() => { + window.fidesDebugger = () => {}; + }); + describe("decodeFidesString", () => { + it.each([ + // Empty string + { + fidesString: "", + expected: { tc: "", ac: "" }, }, - }, - // Without vendors disclosed - { - fidesString: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA", - expected: { tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA", ac: "" }, - }, - // Invalid case of only AC string- need core TC string - { - fidesString: ",1~2.3.4", - expected: { tc: "", ac: "" }, - }, - // Both TC and AC string - { - fidesString: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA,1~2.3.4", - expected: { - tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", - ac: "1~2.3.4", + // TC string only + { + fidesString: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", + expected: { + tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", + ac: "", + }, }, - }, - // With extra unexpected stuff - { - fidesString: - "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA,1~2.3.4,extrastuff", - expected: { - tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", - ac: "1~2.3.4", + // Without vendors disclosed + { + fidesString: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA", + expected: { + tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA", + ac: "", + }, }, - }, - ])( - "can decode a fides string of varying formats", - ({ fidesString, expected }) => { - const result = decodeFidesString(fidesString); - expect(result).toEqual(expected); - }, - ); -}); + // Invalid case of only AC string- need core TC string + { + fidesString: ",1~2.3.4", + expected: { tc: "", ac: "" }, + }, + // Both TC and AC string + { + fidesString: + "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA,1~2.3.4", + expected: { + tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", + ac: "1~2.3.4", + }, + }, + // With extra unexpected stuff + { + fidesString: + "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA,1~2.3.4,extrastuff", + expected: { + tc: "CPzvOIAPzvOIAGXABBENAUEAAACAAAAAAAAAAAAAAAAA.IAAA", + ac: "1~2.3.4", + }, + }, + ])( + "can decode a fides string of varying formats", + ({ fidesString, expected }) => { + const result = decodeFidesString(fidesString); + expect(result).toEqual(expected); + }, + ); + }); -describe("idsFromAcString", () => { - it.each([ - // Empty string - { - acString: "", - expected: [], - }, - // String without ids - { - acString: "1~", - expected: [], - }, - // Invalid string - { - acString: "invalid", - expected: [], - }, - // Proper string - { - acString: "1~1.2.3", - expected: ["gacp.1", "gacp.2", "gacp.3"], - }, - ])( - "can decode a fides string of varying formats", - ({ acString, expected }) => { - const result = idsFromAcString(acString); - expect(result).toEqual(expected); - }, - ); + describe("idsFromAcString", () => { + it.each([ + // Empty string + { + acString: "", + expected: [], + }, + // String without ids + { + acString: "1~", + expected: [], + }, + // Invalid string + { + acString: "invalid", + expected: [], + }, + // Proper string + { + acString: "1~1.2.3", + expected: ["gacp.1", "gacp.2", "gacp.3"], + }, + ])( + "can decode a fides string of varying formats", + ({ acString, expected }) => { + const result = idsFromAcString(acString); + expect(result).toEqual(expected); + }, + ); + }); }); diff --git a/clients/fides-js/global.d.ts b/clients/fides-js/global.d.ts new file mode 100644 index 00000000000..5aec496f1b3 --- /dev/null +++ b/clients/fides-js/global.d.ts @@ -0,0 +1,7 @@ +declare module globalThis { + /** + * Wrapper for console.log that only logs if debug mode is enabled + * while also preserving the stack trace. + */ + let fidesDebugger: (...args: unknown[]) => void; +} diff --git a/clients/fides-js/package.json b/clients/fides-js/package.json index eaece1e5284..8db5d35e291 100644 --- a/clients/fides-js/package.json +++ b/clients/fides-js/package.json @@ -45,6 +45,7 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-strip": "^3.0.4", "@types/base-64": "^1.0.2", "@types/jest": "^29.5.12", "@types/js-cookie": "^3.0.6", diff --git a/clients/fides-js/rollup.config.mjs b/clients/fides-js/rollup.config.mjs index cdc6e91f427..b582803f515 100644 --- a/clients/fides-js/rollup.config.mjs +++ b/clients/fides-js/rollup.config.mjs @@ -8,6 +8,7 @@ import nodeResolve from "@rollup/plugin-node-resolve"; import postcss from "rollup-plugin-postcss"; import commonjs from "@rollup/plugin-commonjs"; import { visualizer } from "rollup-plugin-visualizer"; +import strip from "@rollup/plugin-strip"; const NAME = "fides"; const IS_DEV = process.env.NODE_ENV === "development"; @@ -15,7 +16,7 @@ const GZIP_SIZE_ERROR_KB = 45; // fail build if bundle size exceeds this const GZIP_SIZE_WARN_KB = 35; // log a warning if bundle size exceeds this // TCF -const GZIP_SIZE_TCF_ERROR_KB = 86; +const GZIP_SIZE_TCF_ERROR_KB = 85; const GZIP_SIZE_TCF_WARN_KB = 75; const preactAliases = { @@ -38,6 +39,14 @@ const fidesScriptPlugins = ({ name, gzipWarnSizeKb, gzipErrorSizeKb }) => [ esbuild({ minify: !IS_DEV, }), + strip( + IS_DEV + ? {} + : { + include: ["**/*.ts"], + functions: ["fidesDebugger"], + }, + ), copy({ // Automatically add the built script to the privacy center's and admin ui's static files for bundling: targets: [ @@ -140,6 +149,10 @@ SCRIPTS.forEach(({ name, gzipErrorSizeKb, gzipWarnSizeKb, isExtension }) => { commonjs(), postcss(), esbuild(), + strip({ + include: ["**/*.js", "**/*.ts"], + functions: ["fidesDebugger"], + }), ], output: [ { diff --git a/clients/fides-js/src/components/LanguageSelector.tsx b/clients/fides-js/src/components/LanguageSelector.tsx index 755f668db09..0b85dbb9657 100644 --- a/clients/fides-js/src/components/LanguageSelector.tsx +++ b/clients/fides-js/src/components/LanguageSelector.tsx @@ -3,7 +3,6 @@ import { useContext } from "preact/hooks"; import { FIDES_OVERLAY_WRAPPER } from "../lib/consent-constants"; import { FidesInitOptions } from "../lib/consent-types"; -import { debugLog } from "../lib/consent-utils"; import { DEFAULT_LOCALE, loadMessagesFromGVLTranslations, @@ -35,7 +34,6 @@ const LanguageSelector = ({ const gvlTranslations = await fetchGvlTranslations( options.fidesApiUrl, [locale], - options.debug, ); setIsLoading(false); if (gvlTranslations && Object.keys(gvlTranslations).length) { @@ -46,14 +44,14 @@ const LanguageSelector = ({ availableLocales || [DEFAULT_LOCALE], ); setCurrentLocale(locale); - debugLog(options.debug, `Fides locale updated to ${locale}`); + fidesDebugger(`Fides locale updated to ${locale}`); } else { // eslint-disable-next-line no-console console.error(`Unable to load GVL translation for ${locale}`); } } else { setCurrentLocale(locale); - debugLog(options.debug, `Fides locale updated to ${locale}`); + fidesDebugger(`Fides locale updated to ${locale}`); } } document.getElementById(FIDES_OVERLAY_WRAPPER)?.focus(); diff --git a/clients/fides-js/src/components/Overlay.tsx b/clients/fides-js/src/components/Overlay.tsx index 4e93903d77f..1e3036d7987 100644 --- a/clients/fides-js/src/components/Overlay.tsx +++ b/clients/fides-js/src/components/Overlay.tsx @@ -20,11 +20,7 @@ import { PrivacyExperience, PrivacyExperienceMinimal, } from "../lib/consent-types"; -import { - debugLog, - defaultShowModal, - shouldResurfaceConsent, -} from "../lib/consent-utils"; +import { defaultShowModal, shouldResurfaceConsent } from "../lib/consent-utils"; import { dispatchFidesEvent } from "../lib/events"; import { useHasMounted } from "../lib/hooks"; import { useI18n } from "../lib/i18n/i18n-context"; @@ -165,10 +161,9 @@ const Overlay: FunctionComponent = ({ // use a short delay to give basic page a chance to render the modal link element const delayModalLinkBinding = setTimeout(() => { const modalLinkId = options.modalLinkId || "fides-modal-link"; - debugLog(options.debug, "Searching for modal link element..."); + fidesDebugger("Searching for modal link element..."); const bindModalLink = (modalLinkEl: HTMLElement) => { - debugLog( - options.debug, + fidesDebugger( "Modal link element found, updating it to show and trigger modal on click.", ); modalLinkRef.current = modalLinkEl; @@ -180,8 +175,7 @@ const Overlay: FunctionComponent = ({ let modalLinkEl = document.getElementById(modalLinkId); if (!modalLinkEl) { // Wait until the hosting page's link element is available before attempting to bind to the click handler. This is useful for dynamic (SPA) pages and pages that load the modal link element after the Fides script has loaded. - debugLog( - options.debug, + fidesDebugger( `Modal link element not found (#${modalLinkId}), waiting for it to be added to the DOM...`, ); let attempts = 0; @@ -226,7 +220,7 @@ const Overlay: FunctionComponent = ({ } if (!experience.experience_config) { - debugLog(options.debug, "No experience config found"); + fidesDebugger("No experience config found"); return null; } diff --git a/clients/fides-js/src/components/notices/NoticeOverlay.tsx b/clients/fides-js/src/components/notices/NoticeOverlay.tsx index 0c50691968d..48386caca3e 100644 --- a/clients/fides-js/src/components/notices/NoticeOverlay.tsx +++ b/clients/fides-js/src/components/notices/NoticeOverlay.tsx @@ -15,7 +15,7 @@ import { SaveConsentPreference, ServingComponent, } from "../../lib/consent-types"; -import { debugLog, getGpcStatusFromNotice } from "../../lib/consent-utils"; +import { getGpcStatusFromNotice } from "../../lib/consent-utils"; import { resolveConsentValue } from "../../lib/consent-value"; import { getFidesConsentCookie, @@ -241,7 +241,7 @@ const NoticeOverlay: FunctionComponent = ({ const experienceConfig = experience.experience_config; if (!experienceConfig) { - debugLog(options.debug, "No experience config found"); + fidesDebugger("No experience config found"); return null; } diff --git a/clients/fides-js/src/components/tcf/TcfOverlay.tsx b/clients/fides-js/src/components/tcf/TcfOverlay.tsx index bb00db19807..ca7036c7309 100644 --- a/clients/fides-js/src/components/tcf/TcfOverlay.tsx +++ b/clients/fides-js/src/components/tcf/TcfOverlay.tsx @@ -11,7 +11,7 @@ import { PrivacyExperienceMinimal, ServingComponent, } from "../../lib/consent-types"; -import { debugLog, isPrivacyExperience } from "../../lib/consent-utils"; +import { isPrivacyExperience } from "../../lib/consent-utils"; import { dispatchFidesEvent } from "../../lib/events"; import { useNoticesServed } from "../../lib/hooks"; import { @@ -90,12 +90,11 @@ export const TcfOverlay = ({ const gvlTranslationObjects = await fetchGvlTranslations( options.fidesApiUrl, [locale], - options.debug, ); if (gvlTranslationObjects) { setGvlTranslations(gvlTranslationObjects[locale]); loadMessagesFromGVLTranslations(i18n, gvlTranslationObjects, [locale]); - debugLog(options.debug, `Fides GVL translations loaded for ${locale}`); + fidesDebugger(`Fides GVL translations loaded for ${locale}`); } }; @@ -116,8 +115,7 @@ export const TcfOverlay = ({ // which isn't available to us until the experience is fetched or when the // browser has cached the experience from a previous userLocale. In these cases, // we'll get the translations for the banner from the full experience. - debugLog( - options.debug, + fidesDebugger( `Best locale does not match minimal experience locale (${minExperienceLocale})\nLoading translations from full experience = ${bestLocale}`, ); setIsI18nLoading(true); @@ -134,7 +132,6 @@ export const TcfOverlay = ({ fetchExperience({ userLocationString: fidesRegionString, fidesApiUrl: options.fidesApiUrl, - debug: options.debug, apiOptions: options.apiOptions, propertyId, requestMinimalTCF: false, @@ -317,7 +314,7 @@ export const TcfOverlay = ({ const experienceConfig = experience?.experience_config || experienceMinimal.experience_config; if (!experienceConfig) { - debugLog(options.debug, "No experience config found"); + fidesDebugger("No experience config found"); return null; } diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 4f4709e3c40..e8c95fcfd0c 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -11,7 +11,6 @@ import type { TCData } from "@iabtechlabtcf/cmpapi"; import { TCString } from "@iabtechlabtcf/core"; import { - debugLog, defaultShowModal, FidesCookie, isPrivacyExperience, @@ -32,6 +31,7 @@ import { OverrideType, PrivacyExperience, } from "./lib/consent-types"; +import { initializeDebugger } from "./lib/debugger"; import { dispatchFidesEvent, onFidesEvent } from "./lib/events"; import type { GppFunction } from "./lib/gpp/types"; import { DEFAULT_MODAL_LINK_LABEL } from "./lib/i18n"; @@ -76,12 +76,10 @@ const updateWindowFides = (fidesGlobal: FidesGlobal) => { const updateExperience = ({ cookie, experience, - debug = false, isExperienceClientSideFetched, }: { cookie: FidesCookie; experience: PrivacyExperience; - debug?: boolean; isExperienceClientSideFetched: boolean; }): Partial => { if (!isExperienceClientSideFetched) { @@ -93,8 +91,7 @@ const updateExperience = ({ // We need the cookie.fides_string to attach user preference to an experience. // If this does not exist, we should assume no user preference has been given and leave the experience as is. if (cookie.fides_string) { - debugLog( - debug, + fidesDebugger( "Overriding preferences from client-side fetched experience with cookie fides_string consent", cookie.fides_string, ); @@ -120,6 +117,8 @@ async function init(this: FidesGlobal, providedConfig?: FidesConfig) { (this.config as FidesConfig) ?? raise("Fides must be initialized with a configuration object"); + initializeDebugger(!!config.options?.debug); + this.config = config; // no matter how the config is set, we want to store it on the global object updateWindowFides(this); @@ -190,8 +189,7 @@ async function init(this: FidesGlobal, providedConfig?: FidesConfig) { }; this.cookie = { ...this.cookie, ...updatedCookie }; } catch (error) { - debugLog( - config.options.debug, + fidesDebugger( `Could not decode tcString from ${fidesString}, it may be invalid. ${error}`, ); } diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index a3b0d0af2ce..6d44eb446c4 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -29,6 +29,7 @@ import { consentCookieObjHasSomeConsentSet, updateExperienceFromCookieConsentNotices, } from "./lib/cookie"; +import { initializeDebugger } from "./lib/debugger"; import { dispatchFidesEvent, onFidesEvent } from "./lib/events"; import { DEFAULT_MODAL_LINK_LABEL } from "./lib/i18n"; import { @@ -45,6 +46,7 @@ declare global { interface Window { Fides: FidesGlobal; fides_overrides: FidesOptions; + fidesDebugger: (...args: unknown[]) => void; } } @@ -57,7 +59,6 @@ const updateWindowFides = (fidesGlobal: FidesGlobal) => { const updateExperience: UpdateExperienceFn = ({ cookie, experience, - debug, isExperienceClientSideFetched, }): Partial => { let updatedExperience: PrivacyExperience = experience; @@ -70,7 +71,6 @@ const updateExperience: UpdateExperienceFn = ({ updatedExperience = updateExperienceFromCookieConsentNotices({ experience, cookie, - debug, }); } return updatedExperience; @@ -88,6 +88,8 @@ async function init(this: FidesGlobal, providedConfig?: FidesConfig) { (this.config as FidesConfig) ?? raise("Fides must be initialized with a configuration object"); + initializeDebugger(!!config.options?.debug); + this.config = config; // no matter how the config is set, we want to store it on the global object dispatchFidesEvent( diff --git a/clients/fides-js/src/lib/consent-utils.ts b/clients/fides-js/src/lib/consent-utils.ts index c2d07780fe7..7aa9e859be1 100644 --- a/clients/fides-js/src/lib/consent-utils.ts +++ b/clients/fides-js/src/lib/consent-utils.ts @@ -24,21 +24,6 @@ import { import { noticeHasConsentInCookie } from "./shared-consent-utils"; import { TcfModelsRecord } from "./tcf/types"; -/** - * Wrapper around 'console.log' that only logs output when the 'debug' banner - * option is truthy - */ -type ConsoleLogParameters = Parameters; -export const debugLog = ( - enabled: boolean = false, - ...args: ConsoleLogParameters -): void => { - if (enabled) { - // eslint-disable-next-line no-console - console.log(...args); - } -}; - /** * Returns true if the provided input is a valid PrivacyExperience object. * @@ -86,12 +71,10 @@ export const allNoticesAreDefaultOptIn = ( */ export const constructFidesRegionString = ( geoLocation?: UserGeolocation | null, - debug: boolean = false, ): string | null => { - debugLog(debug, "constructing geolocation..."); + fidesDebugger("constructing geolocation..."); if (!geoLocation) { - debugLog( - debug, + fidesDebugger( "cannot construct user location since geoLocation is undefined or null", ); return null; @@ -101,13 +84,16 @@ export const constructFidesRegionString = ( VALID_ISO_3166_LOCATION_REGEX.test(geoLocation.location) ) { // Fides backend requires underscore deliminator - return geoLocation.location.replace("-", "_").toLowerCase(); + const regionString = geoLocation.location.replace("-", "_").toLowerCase(); + fidesDebugger(`using geolocation: ${regionString}`); + return regionString; } if (geoLocation.country && geoLocation.region) { - return `${geoLocation.country.toLowerCase()}_${geoLocation.region.toLowerCase()}`; + const regionString = `${geoLocation.country.toLowerCase()}_${geoLocation.region.toLowerCase()}`; + fidesDebugger(`using geolocation: ${regionString}`); + return regionString; } - debugLog( - debug, + fidesDebugger( "cannot construct user location from provided geoLocation params...", ); return null; @@ -118,22 +104,18 @@ export const constructFidesRegionString = ( */ export const validateOptions = (options: FidesInitOptions): boolean => { // Check if options is an invalid type - debugLog( - options.debug, - "Validating Fides consent overlay options...", - options, - ); + fidesDebugger("Validating Fides consent overlay options...", options); if (typeof options !== "object") { return false; } if (!options.fidesApiUrl) { - debugLog(options.debug, "Invalid options: fidesApiUrl is required!"); + fidesDebugger("Invalid options: fidesApiUrl is required!"); return false; } if (!options.privacyCenterUrl) { - debugLog(options.debug, "Invalid options: privacyCenterUrl is required!"); + fidesDebugger("Invalid options: privacyCenterUrl is required!"); return false; } @@ -143,8 +125,7 @@ export const validateOptions = (options: FidesInitOptions): boolean => { // eslint-disable-next-line no-new new URL(options.fidesApiUrl); } catch (e) { - debugLog( - options.debug, + fidesDebugger( "Invalid options: privacyCenterUrl or fidesApiUrl is an invalid URL!", options.privacyCenterUrl, ); @@ -176,19 +157,16 @@ export const getOverrideValidatorMapByType = ( */ export const experienceIsValid = ( effectiveExperience: PrivacyExperience | undefined | EmptyExperience, - options: FidesInitOptions, ): boolean => { if (!isPrivacyExperience(effectiveExperience)) { - debugLog( - options.debug, + fidesDebugger( "No relevant experience found. Skipping overlay initialization.", ); return false; } const expConfig = effectiveExperience.experience_config; if (!expConfig) { - debugLog( - options.debug, + fidesDebugger( "No experience config found for experience. Skipping overlay initialization.", ); return false; @@ -200,8 +178,7 @@ export const experienceIsValid = ( expConfig.component === ComponentType.TCF_OVERLAY ) ) { - debugLog( - options.debug, + fidesDebugger( "No experience found with modal, banner_and_modal, or tcf_overlay component. Skipping overlay initialization.", ); return false; @@ -213,8 +190,7 @@ export const experienceIsValid = ( effectiveExperience.privacy_notices.length > 0 ) ) { - debugLog( - options.debug, + fidesDebugger( `Privacy experience has no notices. Skipping overlay initialization.`, ); return false; @@ -334,8 +310,5 @@ export const getGpcStatusFromNotice = ({ }; export const defaultShowModal = () => { - debugLog( - window.Fides.options.debug, - "The current experience does not support displaying a modal.", - ); + fidesDebugger("The current experience does not support displaying a modal."); }; diff --git a/clients/fides-js/src/lib/cookie.ts b/clients/fides-js/src/lib/cookie.ts index 240f15fb62a..0c0f1034106 100644 --- a/clients/fides-js/src/lib/cookie.ts +++ b/clients/fides-js/src/lib/cookie.ts @@ -12,7 +12,6 @@ import { PrivacyNoticeWithPreference, SaveConsentPreference, } from "./consent-types"; -import { debugLog } from "./consent-utils"; import { resolveLegacyConsentValue } from "./consent-value"; import { transformConsentToFidesUserPreference, @@ -101,9 +100,7 @@ export const getCookieByName = (cookieName: string): string | undefined => /** * Retrieve and decode fides consent cookie */ -export const getFidesConsentCookie = ( - debug: boolean = false, -): FidesCookie | undefined => { +export const getFidesConsentCookie = (): FidesCookie | undefined => { const cookieString = getCookieByName(CONSENT_COOKIE_NAME); if (!cookieString) { return undefined; @@ -115,7 +112,7 @@ export const getFidesConsentCookie = ( try { return JSON.parse(base64_decode(cookieString)); } catch (e) { - debugLog(debug, `Unable to read consent cookie`, e); + fidesDebugger(`Unable to read consent cookie`, e); return undefined; } } @@ -131,7 +128,6 @@ export const getFidesConsentCookie = ( */ export const getOrMakeFidesCookie = ( defaults?: NoticeConsent, - debug: boolean = false, fidesClearCookie: boolean = false, ): FidesCookie => { // Create a default cookie and set the configured consent defaults @@ -149,8 +145,7 @@ export const getOrMakeFidesCookie = ( // Check for an existing cookie for this device let parsedCookie: FidesCookie | undefined = getFidesConsentCookie(); if (!parsedCookie) { - debugLog( - debug, + fidesDebugger( `No existing Fides consent cookie found, returning defaults.`, parsedCookie, ); @@ -178,15 +173,15 @@ export const getOrMakeFidesCookie = ( ...parsedCookie.consent, }; parsedCookie.consent = updatedConsent; - // since console.log is synchronous, we stringify to accurately read the parsedCookie obj - debugLog( - debug, + // since fidesDebugger is synchronous, we stringify to accurately read the parsedCookie obj + fidesDebugger( `Applied existing consent to data from existing Fides consent cookie.`, JSON.stringify(parsedCookie), ); return parsedCookie; } catch (err) { - debugLog(debug, `Unable to read consent cookie: invalid JSON.`, err); + // eslint-disable-next-line no-console + console.error(`Unable to read consent cookie: invalid JSON.`, err); return defaultCookie; } }; @@ -254,11 +249,9 @@ export const saveFidesCookie = ( export const updateExperienceFromCookieConsentNotices = ({ experience, cookie, - debug, }: { experience: PrivacyExperience; cookie: FidesCookie; - debug?: boolean; }): PrivacyExperience => { // If the given experience has no notices, return immediately and do not mutate // the experience object in any way @@ -277,13 +270,10 @@ export const updateExperienceFromCookieConsentNotices = ({ return { ...notice, current_preference: preference }; }); - if (debug) { - debugLog( - debug, - `Returning updated pre-fetched experience with user consent.`, - experience, - ); - } + fidesDebugger( + `Returning updated pre-fetched experience with user consent.`, + experience, + ); return { ...experience, privacy_notices: noticesWithConsent }; }; @@ -316,7 +306,6 @@ export const transformTcfPreferencesToCookieKeys = ( export const makeConsentDefaultsLegacy = ( config: LegacyConsentConfig | undefined, context: ConsentContext, - debug: boolean, ): NoticeConsent => { const defaults: NoticeConsent = {}; config?.options.forEach(({ cookieKeys, default: current }) => { @@ -336,7 +325,7 @@ export const makeConsentDefaultsLegacy = ( defaults[cookieKey] = previous && value; }); }); - debugLog(debug, `Returning defaults for legacy config.`, defaults); + fidesDebugger(`Returning defaults for legacy config.`, defaults); return defaults; }; diff --git a/clients/fides-js/src/lib/debugger.ts b/clients/fides-js/src/lib/debugger.ts new file mode 100644 index 00000000000..10e27a7ec75 --- /dev/null +++ b/clients/fides-js/src/lib/debugger.ts @@ -0,0 +1,15 @@ +/** + * Initialize the global fidesDebugger function if it doesn't already exist. + * @param isDebugMode boolean whether or not to enable the debugger + */ +export const initializeDebugger = (isDebugMode: boolean) => { + if (typeof window !== "undefined" && !window.fidesDebugger) { + // eslint-disable-next-line no-console + window.fidesDebugger = isDebugMode ? console.log : () => {}; + } else { + // avoid any errors if window is not defined + (globalThis as any).fidesDebugger = () => {}; + } + // will only log if debug mode is enabled + fidesDebugger("Fides debugger enabled"); +}; diff --git a/clients/fides-js/src/lib/events.ts b/clients/fides-js/src/lib/events.ts index e717a1c383b..7c36dbeed90 100644 --- a/clients/fides-js/src/lib/events.ts +++ b/clients/fides-js/src/lib/events.ts @@ -1,6 +1,5 @@ import type { FidesEventType } from "../docs"; import { FidesCookie } from "./consent-types"; -import { debugLog } from "./consent-utils"; // Bonus points: update the WindowEventMap interface with our custom event types declare global { @@ -66,8 +65,7 @@ export const dispatchFidesEvent = ( detail: { ...cookie, debug, extraDetails: constructedExtraDetails }, }); const perfMark = performance?.mark(type); - debugLog( - debug, + fidesDebugger( `Dispatching event type ${type} ${ constructedExtraDetails?.servingComponent ? `from ${constructedExtraDetails.servingComponent} ` diff --git a/clients/fides-js/src/lib/i18n/i18n-utils.ts b/clients/fides-js/src/lib/i18n/i18n-utils.ts index 63f1f0ca0ee..8bf68904cdf 100644 --- a/clients/fides-js/src/lib/i18n/i18n-utils.ts +++ b/clients/fides-js/src/lib/i18n/i18n-utils.ts @@ -10,7 +10,6 @@ import { PrivacyNotice, PrivacyNoticeTranslation, } from "../consent-types"; -import { debugLog } from "../consent-utils"; import { GVLTranslations } from "../tcf/types"; import { DEFAULT_LOCALE, @@ -494,8 +493,7 @@ export function initializeI18n( ? experience.available_locales : [DEFAULT_LOCALE]; loadMessagesFromExperience(i18n, experience, experienceTranslationOverrides); - debugLog( - options?.debug, + fidesDebugger( `Loaded Fides i18n with available locales (${availableLocales.length}) = ${availableLocales}`, ); @@ -512,8 +510,7 @@ export function initializeI18n( availableLanguages.unshift(availableLanguages.splice(indexOfDefault, 1)[0]); } i18n.setAvailableLanguages(availableLanguages); - debugLog( - options?.debug, + fidesDebugger( `Loaded Fides i18n with available languages`, availableLanguages, ); @@ -522,25 +519,21 @@ export function initializeI18n( const defaultLocale: Locale = extractDefaultLocaleFromExperience(experience) || DEFAULT_LOCALE; i18n.setDefaultLocale(defaultLocale); - debugLog( - options?.debug, + fidesDebugger( `Setting Fides i18n default locale = ${i18n.getDefaultLocale()}`, ); // Detect the user's locale, unless it's been *explicitly* disabled in the experience config let userLocale = defaultLocale; if (experience.experience_config?.auto_detect_language === false) { - debugLog( - options?.debug, - "Auto-detection of Fides i18n user locale disabled!", - ); + fidesDebugger("Auto-detection of Fides i18n user locale disabled!"); } else { userLocale = detectUserLocale( navigator, options?.fidesLocale, defaultLocale, ); - debugLog(options?.debug, `Detected Fides i18n user locale = ${userLocale}`); + fidesDebugger(`Detected Fides i18n user locale = ${userLocale}`); } // Match the user locale to the "best" available locale from the experience API @@ -565,14 +558,12 @@ export function initializeI18n( ); const bestAvailableLocale = bestTranslation?.language || bestLocale; i18n.activate(bestTranslation?.language || bestLocale); - debugLog( - options?.debug, + fidesDebugger( `Initialized Fides i18n with available translations = ${bestAvailableLocale}`, ); } else { i18n.activate(bestLocale); - debugLog( - options?.debug, + fidesDebugger( `Initialized Fides i18n with best locale match = ${bestLocale}`, ); } diff --git a/clients/fides-js/src/lib/initOverlay.ts b/clients/fides-js/src/lib/initOverlay.ts index 7a7159dcae0..75119ac5489 100644 --- a/clients/fides-js/src/lib/initOverlay.ts +++ b/clients/fides-js/src/lib/initOverlay.ts @@ -2,7 +2,6 @@ import { ContainerNode, render } from "preact"; import { OverlayProps } from "../components/types"; import { ComponentType } from "./consent-types"; -import { debugLog } from "./consent-utils"; import { ColorFormat, generateLighterColor } from "./style-utils"; const FIDES_EMBED_CONTAINER_ID = "fides-embed-container"; @@ -33,21 +32,17 @@ export const initOverlay = async ({ }: OverlayProps & { renderOverlay: (props: OverlayProps, parent: ContainerNode) => void; }): Promise => { - debugLog(options.debug, "Initializing Fides consent overlays..."); + fidesDebugger("Initializing Fides consent overlays..."); async function renderFidesOverlay(): Promise { try { - debugLog( - options.debug, - "Injecting Fides overlay CSS & HTML into the DOM...", - ); + fidesDebugger("Injecting Fides overlay CSS & HTML into the DOM..."); // If this function is called multiple times (e.g. due to calling // Fides.reinitialize() or similar), first ensure we unmount any // previously rendered instances if (renderedParentElem) { - debugLog( - options.debug, + fidesDebugger( "Detected that Fides overlay was previously rendered! Unmounting previous instance from the DOM.", ); @@ -89,8 +84,7 @@ export const initOverlay = async ({ parentElem = document.getElementById(FIDES_EMBED_CONTAINER_ID); if (!parentElem) { // wait until the hosting page's container element is available before proceeding in this script and attempting to render the embedded overlay. This is useful for dynamic (SPA) pages and pages that load the modal link element after the Fides script has loaded. - debugLog( - options.debug, + fidesDebugger( `Embed container not found (#${FIDES_EMBED_CONTAINER_ID}), waiting for it to be added to the DOM...`, ); const checkEmbedContainer = async () => @@ -119,8 +113,7 @@ export const initOverlay = async ({ options.overlayParentId || FIDES_OVERLAY_DEFAULT_ID; parentElem = document.getElementById(overlayParentId); if (!parentElem) { - debugLog( - options.debug, + fidesDebugger( `Parent element not found (#${overlayParentId}), creating and appending to body...`, ); // Create our own parent element and prepend to body @@ -157,12 +150,12 @@ export const initOverlay = async ({ }, parentElem, ); - debugLog(options.debug, "Fides overlay is now in the DOM!"); + fidesDebugger("Fides overlay is now in the DOM!"); renderedParentElem = parentElem; } return await Promise.resolve(); } catch (e) { - debugLog(options.debug, e); + fidesDebugger(e); return Promise.reject(e); } } @@ -170,13 +163,12 @@ export const initOverlay = async ({ // Ensure we only render the overlay to the document once it's interactive // NOTE: do not wait for "complete" state, as this can delay rendering on sites with heavy assets if (document?.readyState === "loading") { - debugLog( - options.debug, + fidesDebugger( "document readyState is not yet 'interactive', adding 'readystatechange' event listener and waiting...", ); document.addEventListener("readystatechange", async () => { if (document.readyState === "interactive") { - debugLog(options.debug, "document fully loaded and parsed"); + fidesDebugger("document fully loaded and parsed"); renderFidesOverlay(); } }); diff --git a/clients/fides-js/src/lib/initialize.ts b/clients/fides-js/src/lib/initialize.ts index 26456b93907..694da9634a6 100644 --- a/clients/fides-js/src/lib/initialize.ts +++ b/clients/fides-js/src/lib/initialize.ts @@ -23,7 +23,6 @@ import { } from "./consent-types"; import { constructFidesRegionString, - debugLog, experienceIsValid, getOverrideValidatorMapByType, getWindowObjFromPath, @@ -76,7 +75,6 @@ const retrieveEffectiveRegionString = async ( await getGeolocation( options.isGeolocationEnabled, options.geolocationApiUrl, - options.debug, ), ); } @@ -244,18 +242,10 @@ export const getOverridesByType = ( export const getInitialCookie = ({ consent, options }: FidesConfig) => { // Configure the default legacy consent values const context = getConsentContext(); - const consentDefaults = makeConsentDefaultsLegacy( - consent, - context, - options.debug, - ); + const consentDefaults = makeConsentDefaultsLegacy(consent, context); // Load any existing user preferences from the browser cookie - return getOrMakeFidesCookie( - consentDefaults, - options.debug, - options.fidesClearCookie, - ); + return getOrMakeFidesCookie(consentDefaults, options.fidesClearCookie); }; /** @@ -345,8 +335,7 @@ export const initialize = async ({ if (shouldInitOverlay) { if (!validateOptions(options)) { - debugLog( - options.debug, + fidesDebugger( "Invalid overlay options. Skipping overlay initialization.", options, ); @@ -361,8 +350,7 @@ export const initialize = async ({ let fetchedClientSideExperience = false; if (!fidesRegionString) { - debugLog( - options.debug, + fidesDebugger( `User location could not be obtained. Skipping overlay initialization.`, ); shouldInitOverlay = false; @@ -373,7 +361,6 @@ export const initialize = async ({ fides.experience = await fetchExperience({ userLocationString: fidesRegionString, fidesApiUrl: options.fidesApiUrl, - debug: options.debug, apiOptions: options.apiOptions, requestMinimalTCF: false, }); @@ -381,7 +368,7 @@ export const initialize = async ({ if ( isPrivacyExperience(fides.experience) && - experienceIsValid(fides.experience, options) + experienceIsValid(fides.experience) ) { /** * Now that we've determined the effective PrivacyExperience, update it @@ -391,11 +378,9 @@ export const initialize = async ({ const updatedExperience = updateExperience({ cookie: fides.cookie!, experience: fides.experience, - debug: options.debug, isExperienceClientSideFetched: fetchedClientSideExperience, }); - debugLog( - options.debug, + fidesDebugger( "Updated experience from saved preferences", updatedExperience, ); @@ -422,8 +407,7 @@ export const initialize = async ({ cookie: fides.cookie, experience: fides.experience, }); - debugLog( - options.debug, + fidesDebugger( "Updated current cookie state from experience", updatedCookie, ); @@ -461,7 +445,7 @@ export const initialize = async ({ propertyId, translationOverrides: overrides?.experienceTranslationOverrides, }).catch((e) => { - debugLog(options.debug, e); + fidesDebugger(e); }); /** diff --git a/clients/fides-js/src/lib/preferences.ts b/clients/fides-js/src/lib/preferences.ts index 6e8ac8ea586..4fe5c2cd3e4 100644 --- a/clients/fides-js/src/lib/preferences.ts +++ b/clients/fides-js/src/lib/preferences.ts @@ -10,7 +10,6 @@ import { SaveConsentPreference, UserConsentPreference, } from "./consent-types"; -import { debugLog } from "./consent-utils"; import { removeCookiesFromBrowser, saveFidesCookie } from "./cookie"; import { dispatchFidesEvent } from "./events"; import { TcfSavePreferences } from "./tcf/types"; @@ -30,7 +29,7 @@ async function savePreferencesApi( servedNoticeHistoryId?: string, propertyId?: string, ) { - debugLog(options.debug, "Saving preferences to Fides API"); + fidesDebugger("Saving preferences to Fides API"); // Derive the Fides user preferences array from consent preferences const fidesUserPreferences: ConsentOptionCreate[] = ( consentPreferencesToSave || [] @@ -103,13 +102,13 @@ export const updateConsentPreferences = async ({ dispatchFidesEvent("FidesUpdating", cookie, options.debug); // 3. Update the window.Fides object - debugLog(options.debug, "Updating window.Fides"); + fidesDebugger("Updating window.Fides"); window.Fides.consent = cookie.consent; window.Fides.fides_string = cookie.fides_string; window.Fides.tcf_consent = cookie.tcf_consent; // 4. Save preferences to the cookie in the browser - debugLog(options.debug, "Saving preferences to cookie"); + fidesDebugger("Saving preferences to cookie"); saveFidesCookie(cookie, options.base64Cookie); window.Fides.saved_consent = cookie.consent; @@ -129,8 +128,7 @@ export const updateConsentPreferences = async ({ propertyId, ); } catch (e) { - debugLog( - options.debug, + fidesDebugger( "Error saving updated preferences to API, continuing. Error: ", e, ); diff --git a/clients/fides-js/src/lib/tcf/fidesString.ts b/clients/fides-js/src/lib/tcf/fidesString.ts index 2cf49821059..90f06677524 100644 --- a/clients/fides-js/src/lib/tcf/fidesString.ts +++ b/clients/fides-js/src/lib/tcf/fidesString.ts @@ -1,4 +1,3 @@ -import { debugLog } from "../consent-utils"; import { FIDES_SEPARATOR } from "./constants"; import { VendorSources } from "./vendors"; @@ -24,13 +23,10 @@ export const decodeFidesString = (fidesString: string) => { * // returns [gacp.1, gacp.2, gacp.3] * idsFromAcString("1~1.2.3") */ -export const idsFromAcString = (acString: string, debug?: boolean) => { +export const idsFromAcString = (acString: string) => { const isValidAc = /\d~/; if (!isValidAc.test(acString)) { - debugLog( - !!debug, - `Received invalid AC string ${acString}, returning no ids`, - ); + fidesDebugger(`Received invalid AC string ${acString}, returning no ids`); return []; } const split = acString.split("~"); diff --git a/clients/fides-js/src/lib/tcf/utils.ts b/clients/fides-js/src/lib/tcf/utils.ts index d6caa2cf73a..e3aedaa0a73 100644 --- a/clients/fides-js/src/lib/tcf/utils.ts +++ b/clients/fides-js/src/lib/tcf/utils.ts @@ -8,7 +8,6 @@ import { PrivacyExperienceMinimal, RecordConsentServedRequest, } from "../consent-types"; -import { debugLog } from "../consent-utils"; import { transformTcfPreferencesToCookieKeys } from "../cookie"; import { transformConsentToFidesUserPreference, @@ -144,8 +143,7 @@ export const updateExperienceFromCookieConsentTcf = ({ ); if (debug) { - debugLog( - debug, + fidesDebugger( `Returning updated pre-fetched experience with user consent.`, experience, ); diff --git a/clients/fides-js/src/services/api.ts b/clients/fides-js/src/services/api.ts index 6a27278ac76..589ddc41470 100644 --- a/clients/fides-js/src/services/api.ts +++ b/clients/fides-js/src/services/api.ts @@ -11,7 +11,6 @@ import { RecordConsentServedRequest, RecordsServedResponse, } from "../lib/consent-types"; -import { debugLog } from "../lib/consent-utils"; import { Locale } from "../lib/i18n"; import { GVLTranslations } from "../lib/tcf/types"; @@ -26,7 +25,6 @@ interface FetchExperienceOptions { userLocationString: string; userLanguageString?: string; fidesApiUrl: string; - debug?: boolean; apiOptions?: FidesApiOptions | null; propertyId?: string | null; requestMinimalTCF?: boolean; @@ -40,13 +38,12 @@ export const fetchExperience = async ({ userLocationString, userLanguageString, fidesApiUrl, - debug, apiOptions, propertyId, requestMinimalTCF, }: FetchExperienceOptions): Promise => { if (apiOptions?.getPrivacyExperienceFn) { - debugLog(debug, "Calling custom fetch experience fn"); + fidesDebugger("Calling custom fetch experience fn"); try { return await apiOptions.getPrivacyExperienceFn( userLocationString, @@ -55,8 +52,7 @@ export const fetchExperience = async ({ null, ); } catch (e) { - debugLog( - debug, + fidesDebugger( "Error fetching experience from custom API, returning {}. Error: ", e, ); @@ -94,8 +90,7 @@ export const fetchExperience = async ({ params = new URLSearchParams(params); /* Fetch experience */ - debugLog( - debug, + fidesDebugger( `Fetching ${requestMinimalTCF ? "minimal TCF" : "full"} experience in location: ${userLocationString}`, ); const response = await fetch( @@ -104,8 +99,7 @@ export const fetchExperience = async ({ ); if (!response.ok) { - debugLog( - debug, + fidesDebugger( "Error getting experience from Fides API, returning {}. Response:", response, ); @@ -123,14 +117,12 @@ export const fetchExperience = async ({ const firstLanguage = experience.experience_config?.translations?.[0].language; - debugLog( - debug, + fidesDebugger( `Recieved ${requestMinimalTCF ? "minimal TCF" : "full"} experience response from Fides API${requestMinimalTCF ? ` (${firstLanguage})` : ""}`, ); return experience as T; } catch (e) { - debugLog( - debug, + fidesDebugger( "Error parsing experience response body from Fides API, returning {}. Response:", response, ); @@ -141,9 +133,8 @@ export const fetchExperience = async ({ export const fetchGvlTranslations = async ( fidesApiUrl: string, locales?: Locale[], - debug?: boolean, ): Promise => { - debugLog(debug, "Calling Fides GET GVL translations API..."); + fidesDebugger("Calling Fides GET GVL translations API..."); const params = new URLSearchParams(); locales?.forEach((locale) => { params.append("language", locale); @@ -164,12 +155,11 @@ export const fetchGvlTranslations = async ( return {}; } if (!response.ok) { - debugLog(debug, "Error fetching GVL translations", response); + fidesDebugger("Error fetching GVL translations", response); return {}; } const gvlTranslations: GVLTranslations = await response.json(); - debugLog( - debug, + fidesDebugger( `Recieved GVL languages response from Fides API (${ Object.keys(gvlTranslations).length })`, @@ -199,9 +189,9 @@ export const patchUserPreference = async ( cookie: FidesCookie, experience: PrivacyExperience | PrivacyExperienceMinimal, ): Promise => { - debugLog(options.debug, "Saving user consent preference...", preferences); + fidesDebugger("Saving user consent preference...", preferences); if (options.apiOptions?.savePreferencesFn) { - debugLog(options.debug, "Calling custom save preferences fn"); + fidesDebugger("Calling custom save preferences fn"); try { await options.apiOptions.savePreferencesFn( consentMethod, @@ -210,8 +200,7 @@ export const patchUserPreference = async ( experience, ); } catch (e) { - debugLog( - options.debug, + fidesDebugger( "Error saving preferences to custom API, continuing. Error: ", e, ); @@ -219,7 +208,7 @@ export const patchUserPreference = async ( } return Promise.resolve(); } - debugLog(options.debug, "Calling Fides save preferences API"); + fidesDebugger("Calling Fides save preferences API"); const fetchOptions: RequestInit = { ...PATCH_FETCH_OPTIONS, body: JSON.stringify({ ...preferences, source: REQUEST_SOURCE }), @@ -229,8 +218,7 @@ export const patchUserPreference = async ( fetchOptions, ); if (!response.ok) { - debugLog( - options.debug, + fidesDebugger( "Error patching user preference Fides API. Response:", response, ); @@ -245,21 +233,20 @@ export const patchNoticesServed = async ({ request: RecordConsentServedRequest; options: FidesInitOptions; }): Promise => { - debugLog(options.debug, "Saving that notices were served..."); + fidesDebugger("Saving that notices were served..."); if (options.apiOptions?.patchNoticesServedFn) { - debugLog(options.debug, "Calling custom patch notices served fn"); + fidesDebugger("Calling custom patch notices served fn"); try { return await options.apiOptions.patchNoticesServedFn(request); } catch (e) { - debugLog( - options.debug, + fidesDebugger( "Error patching notices served to custom API, continuing. Error: ", e, ); return null; } } - debugLog(options.debug, "Calling Fides patch notices served API"); + fidesDebugger("Calling Fides patch notices served API"); const fetchOptions: RequestInit = { ...PATCH_FETCH_OPTIONS, body: JSON.stringify(request), @@ -269,11 +256,7 @@ export const patchNoticesServed = async ({ fetchOptions, ); if (!response.ok) { - debugLog( - options.debug, - "Error patching notices served. Response:", - response, - ); + fidesDebugger("Error patching notices served. Response:", response); return null; } return response.json(); diff --git a/clients/fides-js/src/services/external/geolocation.ts b/clients/fides-js/src/services/external/geolocation.ts index 188f44872e1..7636956ece9 100644 --- a/clients/fides-js/src/services/external/geolocation.ts +++ b/clients/fides-js/src/services/external/geolocation.ts @@ -1,5 +1,4 @@ import { UserGeolocation } from "../../lib/consent-types"; -import { debugLog } from "../../lib/consent-utils"; /** * Fetch the user's geolocation from an external API @@ -7,35 +6,31 @@ import { debugLog } from "../../lib/consent-utils"; export const getGeolocation = async ( isGeolocationEnabled?: boolean, geolocationApiUrl?: string, - debug: boolean = false, ): Promise => { - debugLog(debug, "Running getLocation..."); + fidesDebugger("Running getLocation..."); if (!isGeolocationEnabled) { - debugLog( - debug, + fidesDebugger( `User location could not be retrieved because geolocation is disabled.`, ); return null; } if (!geolocationApiUrl) { - debugLog( - debug, + fidesDebugger( "Location cannot be found due to no configured geoLocationApiUrl.", ); return null; } - debugLog(debug, `Calling geolocation API: GET ${geolocationApiUrl}...`); + fidesDebugger(`Calling geolocation API: GET ${geolocationApiUrl}...`); const fetchOptions: RequestInit = { mode: "cors", }; const response = await fetch(geolocationApiUrl, fetchOptions); if (!response.ok) { - debugLog( - debug, + fidesDebugger( "Error getting location from geolocation API, returning {}. Response:", response, ); @@ -44,15 +39,13 @@ export const getGeolocation = async ( try { const body = await response.json(); - debugLog( - debug, + fidesDebugger( "Got location response from geolocation API, returning:", body, ); return body; } catch (e) { - debugLog( - debug, + fidesDebugger( "Error parsing response body from geolocation API, returning {}. Response:", response, ); diff --git a/clients/fides-js/src/services/external/preferences.ts b/clients/fides-js/src/services/external/preferences.ts index 6410f107bfd..9c50e2b6b4d 100644 --- a/clients/fides-js/src/services/external/preferences.ts +++ b/clients/fides-js/src/services/external/preferences.ts @@ -1,5 +1,4 @@ import { FidesConfig, GetPreferencesFnResp } from "../../lib/consent-types"; -import { debugLog } from "../../lib/consent-utils"; /** * Helper function to get preferences from an external API @@ -10,12 +9,11 @@ export async function customGetConsentPreferences( if (!config.options.apiOptions?.getPreferencesFn) { return null; } - debugLog(config.options.debug, "Calling custom get preferences fn"); + fidesDebugger("Calling custom get preferences fn"); try { return await config.options.apiOptions.getPreferencesFn(config); } catch (e) { - debugLog( - config.options.debug, + fidesDebugger( "Error retrieving preferences from custom API, continuing. Error: ", e, ); diff --git a/clients/package-lock.json b/clients/package-lock.json index 64dcfa79b22..368424518e3 100644 --- a/clients/package-lock.json +++ b/clients/package-lock.json @@ -688,6 +688,7 @@ "@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-strip": "^3.0.4", "@types/base-64": "^1.0.2", "@types/jest": "^29.5.12", "@types/js-cookie": "^3.0.6", @@ -5468,6 +5469,29 @@ } } }, + "node_modules/@rollup/plugin-strip": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-strip/-/plugin-strip-3.0.4.tgz", + "integrity": "sha512-LDRV49ZaavxUo2YoKKMQjCxzCxugu1rCPQa0lDYBOWLj6vtzBMr8DcoJjsmg+s450RbKbe3qI9ZLaSO+O1oNbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", diff --git a/clients/package.json b/clients/package.json index 5d085637ced..e1173e8c57c 100644 --- a/clients/package.json +++ b/clients/package.json @@ -21,6 +21,7 @@ "format:ci": "turbo run format:ci", "test": "turbo run test", "test:ci": "turbo run test:ci", + "typecheck": "turbo run typecheck", "prod-export-admin-ui": "turbo run prod-export --filter=admin-ui", "export-admin-ui": "turbo run export --filter=admin-ui", "cy-ui:start": "turbo run cy:start --filter=admin-ui", diff --git a/clients/privacy-center/__tests__/app/server-environment.test.ts b/clients/privacy-center/__tests__/app/server-environment.test.ts index 28f0420c25b..ed8af227ca5 100644 --- a/clients/privacy-center/__tests__/app/server-environment.test.ts +++ b/clients/privacy-center/__tests__/app/server-environment.test.ts @@ -98,6 +98,9 @@ jest.mock( ); describe("loadPrivacyCenterEnvironment", () => { + beforeAll(() => { + (globalThis as any).fidesDebugger = () => {}; + }); beforeEach(() => { mockAsServerSide(); loadEnvironmentVariablesMock.mockReturnValue({ diff --git a/clients/privacy-center/app/server-environment.ts b/clients/privacy-center/app/server-environment.ts index 3e90991ac88..ee09bc2f773 100644 --- a/clients/privacy-center/app/server-environment.ts +++ b/clients/privacy-center/app/server-environment.ts @@ -116,9 +116,7 @@ const loadConfigFile = async ( path = urlString.replace("file:", ""); } const file = await fsPromises.readFile(path || url, "utf-8"); - if (process.env.NODE_ENV === "development") { - console.log(`Loaded configuration file: ${urlString}`); - } + fidesDebugger(`Loaded configuration file: ${urlString}`); return file; } catch (err: any) { // Catch "file not found" errors (ENOENT) @@ -126,7 +124,7 @@ const loadConfigFile = async ( continue; } // Log everything else and continue - console.log( + console.error( `Failed to load configuration file from ${urlString}. Error: `, err, ); @@ -294,9 +292,7 @@ export const loadPrivacyCenterEnvironment = async ({ ); } // DEFER: Log a version number here (see https://github.com/ethyca/fides/issues/3171) - if (process.env.NODE_ENV === "development") { - console.log("Load Privacy Center environment for session..."); - } + fidesDebugger("Load Privacy Center environment for session..."); // Load environment variables const settings = loadEnvironmentVariables(); diff --git a/clients/privacy-center/features/consent/consent.slice.ts b/clients/privacy-center/features/consent/consent.slice.ts index a2054672f26..a48d7e6ef7f 100644 --- a/clients/privacy-center/features/consent/consent.slice.ts +++ b/clients/privacy-center/features/consent/consent.slice.ts @@ -236,6 +236,7 @@ export const selectUserRegion = createSelector( settings.GEOLOCATION_API_URL, )(RootState)?.data; } + return constructFidesRegionString(geolocation) as PrivacyNoticeRegion; } return undefined; diff --git a/clients/privacy-center/global.d.ts b/clients/privacy-center/global.d.ts new file mode 100644 index 00000000000..495feafe22e --- /dev/null +++ b/clients/privacy-center/global.d.ts @@ -0,0 +1,6 @@ +declare module globalThis { + /** Wrapper for console.log that only logs if debug mode is enabled. */ + let fidesDebugger: (...args: unknown[]) => void; + /** Wrapper for console.error that only logs if debug mode is enabled. */ + let fidesError: (...args: unknown[]) => void; +} diff --git a/clients/privacy-center/next.config.js b/clients/privacy-center/next.config.js index 7d99cc0ed36..872d8de4a0a 100644 --- a/clients/privacy-center/next.config.js +++ b/clients/privacy-center/next.config.js @@ -1,5 +1,11 @@ -const path = require("path"); -const { version } = require("./package.json"); +const isDebugMode = process.env.FIDES_PRIVACY_CENTER__DEBUG === "true"; +const debugMarker = "=>"; +globalThis.fidesDebugger = isDebugMode + ? (...args) => console.log(`\x1b[33m${debugMarker}\x1b[0m`, ...args) + : () => {}; +globalThis.fidesError = isDebugMode + ? (...args) => console.log(`\x1b[31m${debugMarker}\x1b[0m`, ...args) + : () => {}; const withBundleAnalyzer = require("@next/bundle-analyzer")({ enabled: process.env.ANALYZE === "true", diff --git a/clients/privacy-center/package.json b/clients/privacy-center/package.json index 5d2d0c24c69..6c5773fef7b 100644 --- a/clients/privacy-center/package.json +++ b/clients/privacy-center/package.json @@ -12,8 +12,9 @@ "lint-staged:fix": "lint-staged --diff=main", "format": "prettier --write .", "format:ci": "prettier --check .", + "typecheck": "tsc --noEmit", "test": "jest --watchAll", - "test:ci": "tsc --noEmit && jest", + "test:ci": "npm run typecheck && jest", "clean": "rm -rf .turbo node_modules", "cy:open": "cypress open", "cy:run": "cypress run", diff --git a/clients/privacy-center/pages/api/fides-js.ts b/clients/privacy-center/pages/api/fides-js.ts index f843f99721e..6d5a9c7f7f9 100644 --- a/clients/privacy-center/pages/api/fides-js.ts +++ b/clients/privacy-center/pages/api/fides-js.ts @@ -3,7 +3,6 @@ import { ComponentType, ConsentOption, constructFidesRegionString, - debugLog, DEFAULT_LOCALE, EmptyExperience, fetchExperience, @@ -139,8 +138,7 @@ export default async function handler( fidesString, ); } catch (error) { - // eslint-disable-next-line no-console - console.error(error); + fidesError(error); res .status(400) // 400 Bad Request. Malformed request. .send( @@ -181,8 +179,7 @@ export default async function handler( const userLanguageString = fidesLocale || req.headers["accept-language"] || DEFAULT_LOCALE; - debugLog( - environment.settings.DEBUG, + fidesDebugger( `Fetching relevant experiences from server-side (${userLanguageString})...`, ); @@ -197,7 +194,6 @@ export default async function handler( fidesApiUrl: serverSettings.SERVER_SIDE_FIDES_API_URL || environment.settings.FIDES_API_URL, - debug: environment.settings.DEBUG, propertyId, requestMinimalTCF: true, }); @@ -205,10 +201,7 @@ export default async function handler( } if (!geolocation) { - debugLog( - environment.settings.DEBUG, - "No geolocation found, unable to prefetch experience.", - ); + fidesDebugger("No geolocation found, unable to prefetch experience."); } // This query param is used for testing purposes only, and should not be used @@ -275,8 +268,7 @@ export default async function handler( }; const fidesConfigJSON = JSON.stringify(fidesConfig); - debugLog( - environment.settings.DEBUG, + fidesDebugger( "Bundling generic fides.js & Privacy Center configuration together...", ); const fidesJsFile = tcfEnabled @@ -289,8 +281,7 @@ export default async function handler( } let fidesGPP: string = ""; if (gppEnabled) { - debugLog( - environment.settings.DEBUG, + fidesDebugger( `GPP extension ${ forcedGppQuery === "true" ? "forced" : "enabled" }, bundling fides-ext-gpp.js...`, @@ -377,8 +368,12 @@ async function fetchCustomFidesCss( const data = await response.text(); if (!response.ok) { - // eslint-disable-next-line no-console - console.error( + if (response.status === 404) { + fidesDebugger("No custom-fides.css found, skipping..."); + autoRefresh = false; + return null; + } + fidesError( "Error fetching custom-fides.css:", response.status, response.statusText, @@ -391,19 +386,16 @@ async function fetchCustomFidesCss( throw new Error("No data returned by the server"); } - // eslint-disable-next-line no-console - console.log("Successfully retrieved custom-fides.css"); + fidesDebugger("Successfully retrieved custom-fides.css"); autoRefresh = true; cachedCustomFidesCss = data; lastFetched = currentTime; } catch (error) { autoRefresh = false; // /custom-asset endpoint unreachable stop auto-refresh if (error instanceof Error) { - // eslint-disable-next-line no-console - console.error("Error during fetch operation:", error.message); + fidesError("Error during fetch operation:", error.message); } else { - // eslint-disable-next-line no-console - console.error("Unknown error occurred:", error); + fidesError("Unknown error occurred:", error); } } } diff --git a/clients/turbo.json b/clients/turbo.json index fb707b30090..e58fa4abfe8 100644 --- a/clients/turbo.json +++ b/clients/turbo.json @@ -40,6 +40,9 @@ "format:ci": { "dependsOn": [] }, + "typecheck": { + "dependsOn": [] + }, "test": { "dependsOn": ["^build"], "cache": false,