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

🪟 🎉 Add integration for Osano/GDPR #17565

Merged
merged 2 commits into from
Oct 5, 2022
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
6 changes: 0 additions & 6 deletions airbyte-webapp/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,6 @@
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="%PUBLIC_URL%/index.css">
<title>Airbyte</title>
<% if (process.env.REACT_APP_OSANO) { %>
<style>
.osano-cm-widget { display: none; }
</style>
<script src="https://cmp.osano.com/%REACT_APP_OSANO%/osano.js"></script>
<% } %>
</head>
<body>
<noscript>
Expand Down
8 changes: 6 additions & 2 deletions airbyte-webapp/src/components/ui/SideMenu/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ export interface SideMenuItem {
path: string;
name: string | React.ReactNode;
indicatorCount?: number;
component: React.ComponentType;
component?: React.ComponentType;
id?: string;
/**
* Will be called instead of the onSelect of the component if this link is clicked.
*/
onClick?: () => void;
}

export interface CategoryItem {
Expand Down Expand Up @@ -52,7 +56,7 @@ export const SideMenu: React.FC<SideMenuProps> = ({ data, onSelect, activeItem }
name={route.name}
isActive={activeItem?.endsWith(route.path)}
count={route.indicatorCount}
onClick={() => onSelect(route.path)}
onClick={route.onClick ?? (() => onSelect(route.path))}
/>
))}
</Category>
Expand Down
6 changes: 6 additions & 0 deletions airbyte-webapp/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import ReactDOM from "react-dom";

import "react-reflex/styles.css";
import { isCloudApp } from "utils/app";
import { loadOsano } from "utils/dataPrivacy";

import "./globals";

Expand All @@ -16,6 +17,11 @@ Sentry.init({
tracesSampleRate: 1.0, // may need to adjust this in the future
});

// In Cloud load the Osano script (GDPR consent tool before anything else)
if (isCloudApp()) {
loadOsano();
}

const CloudApp = lazy(() => import(`packages/cloud/App`));
const App = lazy(() => import(`./App`));

Expand Down
1 change: 1 addition & 0 deletions airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@
"settings.newsletter": "Newsletter with feature updates.",
"settings.account": "Account",
"settings.accountSettings.updateEmailSuccess": "Email updated",
"settings.cookiePreferences": "Cookie Preferences",

"connector.requestConnectorBlock": "+ Request a new connector",
"connector.requestConnector": "Request a new connector",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import * as yup from "yup";

import HeadTitle from "components/HeadTitle";

import { isGdprCountry } from "utils/dataPrivacy";

import { FieldError } from "../lib/errors/FieldError";
import { useAuthService } from "../services/auth/AuthService";
import { EmailLinkErrorCodes } from "../services/auth/types";
Expand Down Expand Up @@ -35,7 +37,7 @@ export const AcceptEmailInvite: React.FC = () => {
name: "",
email: "",
password: "",
news: true,
news: !isGdprCountry(),
}}
validationSchema={ValidationSchema}
onSubmit={async ({ name, email, password, news }, { setFieldError, setStatus }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useConfig } from "config";
import { useExperiment } from "hooks/services/Experiment";
import { FieldError } from "packages/cloud/lib/errors/FieldError";
import { useAuthService } from "packages/cloud/services/auth/AuthService";
import { isGdprCountry } from "utils/dataPrivacy";

import CheckBoxControl from "../../components/CheckBoxControl";
import { BottomBlock, FieldItem, Form, RowFieldItem } from "../../components/FormComponents";
Expand Down Expand Up @@ -204,7 +205,7 @@ export const SignupForm: React.FC = () => {
companyName: search.company ?? "",
email: search.email ?? "",
password: "",
news: true,
news: !isGdprCountry(),
};
return (
<Formik<FormValues>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
// import ConfigurationsPage from "pages/SettingsPage/pages/ConfigurationsPage";
import NotificationPage from "pages/SettingsPage/pages/NotificationPage";
import { PageConfig, SettingsRoute } from "pages/SettingsPage/SettingsPage";
import { isOsanoActive, showOsanoDrawer } from "utils/dataPrivacy";

const CloudSettingsRoutes = {
Configuration: SettingsRoute.Configuration,
Expand Down Expand Up @@ -40,6 +41,15 @@ export const CloudSettingsPage: React.FC = () => {
name: <FormattedMessage id="settings.account" />,
component: AccountSettingsView,
},
...(isOsanoActive()
? [
{
name: <FormattedMessage id="settings.cookiePreferences" />,
path: "__COOKIE_PREFERENCES__", // Special path with no meaning, since the onClick will be triggered
onClick: () => showOsanoDrawer(),
},
]
: []),
],
},
{
Expand Down
6 changes: 5 additions & 1 deletion airbyte-webapp/src/pages/SettingsPage/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import HeadTitle from "components/HeadTitle";
import LoadingPage from "components/LoadingPage";
import MainPageWithScroll from "components/MainPageWithScroll";
import { PageHeader } from "components/ui/PageHeader";
import { SideMenu, CategoryItem } from "components/ui/SideMenu";
import { SideMenu, CategoryItem, SideMenuItem } from "components/ui/SideMenu";

import useConnector from "hooks/services/useConnector";

Expand Down Expand Up @@ -105,6 +105,10 @@ const SettingsPage: React.FC<SettingsPageProps> = ({ pageConfig }) => {
<Routes>
{menuItems
.flatMap((menuItem) => menuItem.routes)
.filter(
(menuItem): menuItem is SideMenuItem & { component: NonNullable<SideMenuItem["component"]> } =>
!!menuItem.component
)
.map(({ path, component: Component }) => (
<Route key={path} path={path} element={<Component />} />
))}
Expand Down
33 changes: 33 additions & 0 deletions airbyte-webapp/src/utils/dataPrivacy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isGdprCountry } from "./dataPrivacy";

const mockTimeZone = (timeZone: string) => {
jest.spyOn(Intl, "DateTimeFormat").mockImplementation(
() =>
({
resolvedOptions: () =>
({
timeZone,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any)
);
};

describe("dataPrivacy", () => {
describe("isGdprCountry()", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("should return true for timezones inside EU", () => {
mockTimeZone("Europe/Berlin");
expect(isGdprCountry()).toBe(true);
});

it("should return false for non EU countries", () => {
mockTimeZone("America/Chicago");
expect(isGdprCountry()).toBe(false);
});
});
});
75 changes: 75 additions & 0 deletions airbyte-webapp/src/utils/dataPrivacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
declare global {
interface Window {
Osano?: {
cm: {
mode: "production" | "debug";
showDrawer: (type: string) => void;
};
};
}
}

const GDPR_TIMEZONES = [
"Africa/Ceuta",
"Asia/Famagusta",
"Asia/Nicosia",
"Atlantic/Azores",
"Atlantic/Canary",
"Atlantic/Madeira",
"Europe/Amsterdam",
"Europe/Athens",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Helsinki",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Paris",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Warsaw",
"Europe/Zagreb",
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious: is browser timezone a sufficient way to enforce GDPR? Seems a bit unreliable to me, but I don't have a better suggestion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, this is not a really foolproof way. Luckily this is really only used to determine the default state of the newsletter flag and such I think it's an "okay enough" approach, without requiring to get a third party geo location service in place (which would be the better approach detecting by IP location). Osano itself for showing the right consent banner actually uses a proper IP based approach, so it's really only for the default state of this one switch we're needing to use this weaker approach.


export const isGdprCountry = (): boolean => {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
return GDPR_TIMEZONES.includes(timeZone);
};

export const loadOsano = (): void => {
if (!process.env.REACT_APP_OSANO) {
return;
}

// Create style element to hide osano widget
const style = document.createElement("style");
style.appendChild(document.createTextNode(".osano-cm-widget { display: none; }"));
document.head.appendChild(style);

// Create and append the script tag to load osano
const script = document.createElement("script");
script.src = `https://cmp.osano.com/${process.env.REACT_APP_OSANO}/osano.js`;
document.head.appendChild(script);
};

export const isOsanoActive = (): boolean => {
return window.Osano?.cm.mode === "production";
};

export const showOsanoDrawer = (): void => {
window.Osano?.cm.showDrawer("osano-cm-dom-info-dialog-open");
};