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

Analytics Consent #4559

Merged
merged 11 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- develop
- feat/*
- main
- py-panels-develop
- release/v[0-9]+.[0-9]+.[0-9]+

jobs:
Expand Down
1 change: 1 addition & 0 deletions app/packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"copy-to-desktop": "rm -rf ../desktop/dist/ && cp -r ./dist ../desktop/dist"
},
"dependencies": {
"@fiftyone/analytics": "*",
"@fiftyone/components": "*",
"@fiftyone/core": "*",
"@fiftyone/relay": "*",
Expand Down
46 changes: 46 additions & 0 deletions app/packages/app/src/components/Analytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { isElectron } from "@fiftyone/utilities";
import React, { useCallback } from "react";
import ReactGA from "react-ga4";
import { graphql, useFragment } from "react-relay";
import gaConfig from "../ga";
import AnalyticsConsent from "./AnalyticsConsent";
import type { NavGA$data, NavGA$key } from "./__generated__/NavGA.graphql";

const useCallGA = (info: NavGA$data) => {
return useCallback(() => {
const dev = info.dev;
const buildType = dev ? "dev" : "prod";
ReactGA.initialize(gaConfig.app_ids[buildType], {
testMode: false,
gaOptions: {
storage: "none",
cookieDomain: "none",
clientId: info.uid,
page_location: "omitted",
page_path: "omitted",
kind: isElectron() ? "Desktop" : "Web",
version: info.version,
context: info.context,
checkProtocolTask: null, // disable check, allow file:// URLs
},
});
}, [info]);
benjaminpkane marked this conversation as resolved.
Show resolved Hide resolved
};

export default function Analytics({ fragment }: { fragment: NavGA$key }) {
const info = useFragment(
graphql`
fragment Analytics on Query {
context
dev
doNotTrack
uid
version
}
`,
fragment
);
const callGA = useCallGA(info);

return <AnalyticsConsent callGA={callGA} info={info} />;
}
117 changes: 117 additions & 0 deletions app/packages/app/src/components/AnalyticsConsent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { DEFAULT_WRITE_KEYS, useAnalyticsInfo } from "@fiftyone/analytics";
import { Button } from "@fiftyone/components";
import { Box, Grid, Link, Typography } from "@mui/material";
import React, { useCallback, useEffect, useState } from "react";
import type { NavGA$data } from "./__generated__/NavGA.graphql";

const FIFTYONE_DO_NOT_TRACK_LS = "fiftyone-do-not-track";

function useAnalyticsConsent(disabled?: boolean) {
const [ready, setReady] = useState(false);
const [show, setShow] = useState(false);
const doNotTrack = window.localStorage.getItem(FIFTYONE_DO_NOT_TRACK_LS);
useEffect(() => {
if (disabled || doNotTrack === "true" || doNotTrack === "false") {
setShow(false);
setReady(true);
} else {
setShow(true);
}
}, [disabled, doNotTrack]);

const handleDisable = useCallback(() => {
window.localStorage.setItem(FIFTYONE_DO_NOT_TRACK_LS, "true");
setShow(false);
setReady(true);
}, []);

const handleEnable = useCallback(() => {
window.localStorage.setItem(FIFTYONE_DO_NOT_TRACK_LS, "false");
setReady(true);
setShow(false);
}, []);
benjaminpkane marked this conversation as resolved.
Show resolved Hide resolved

return {
doNotTrack: doNotTrack === "true" || disabled,
handleDisable,
handleEnable,
ready,
show,
};
}

export default function AnalyticsConsent({
callGA,
info,
}: {
callGA: () => void;
info: NavGA$data;
}) {
const [_, setAnalyticsInfo] = useAnalyticsInfo();

const { doNotTrack, handleDisable, handleEnable, ready, show } =
useAnalyticsConsent(info.doNotTrack);

useEffect(() => {
if (!ready) {
return;
}
const buildType = info.dev ? "dev" : "prod";
const writeKey = DEFAULT_WRITE_KEYS[buildType];
setAnalyticsInfo({
userId: info.uid,
userGroup: "fiftyone-oss",
writeKey,
doNotTrack: doNotTrack,
debug: info.dev,
});
!doNotTrack && callGA();
}, [callGA, doNotTrack, info, ready, setAnalyticsInfo]);

if (!show) {
return null;
}

return (
benjaminpkane marked this conversation as resolved.
Show resolved Hide resolved
<PinBottom>
<Grid
container
direction="column"
alignItems="center"
borderTop={(theme) => `1px solid ${theme.palette.divider}`}
backgroundColor="background.paper"
>
<Grid padding={2}>
<Typography variant="h6" marginBottom={1}>
Help us improve FiftyOne
</Typography>
<Typography marginBottom={1}>
We use cookies to understand how FiftyOne is used and to improve the
product. You can help us by enabling analytics.
</Typography>
<Grid container gap={2} justifyContent="end" direction="row">
<Grid item alignContent="center">
<Link style={{ cursor: "pointer" }} onClick={handleDisable}>
Disable
</Link>
</Grid>
<Grid item>
<Button variant="contained" onClick={handleEnable}>
Enable
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
</PinBottom>
);
}

// a component that pins the content to the bottom of the screen, floating
function PinBottom({ children }: React.PropsWithChildren) {
return (
<Box position="fixed" bottom={0} width="100%" zIndex={51}>
benjaminpkane marked this conversation as resolved.
Show resolved Hide resolved
{children}
</Box>
);
}
145 changes: 47 additions & 98 deletions app/packages/app/src/components/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,18 @@ import {
import { ViewBar } from "@fiftyone/core";
import * as fos from "@fiftyone/state";
import { useRefresh } from "@fiftyone/state";
import { isElectron } from "@fiftyone/utilities";
import { DarkMode, LightMode } from "@mui/icons-material";
import { useColorScheme } from "@mui/material";
import React, { Suspense, useEffect, useMemo } from "react";
import ReactGA from "react-ga4";
import React, { Suspense, useMemo } from "react";
import { useFragment, usePaginationFragment } from "react-relay";
import { useDebounce } from "react-use";
import {
useRecoilState,
useRecoilValue,
useResetRecoilState,
useSetRecoilState,
} from "recoil";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { graphql } from "relay-runtime";
import gaConfig from "../ga";
import Analytics from "./Analytics";
import DatasetSelector from "./DatasetSelector";
import Teams from "./Teams";
import { NavDatasets$key } from "./__generated__/NavDatasets.graphql";
import { NavFragment$key } from "./__generated__/NavFragment.graphql";
import { DEFAULT_WRITE_KEYS, useAnalyticsInfo } from "@fiftyone/analytics";

const getUseSearch = (fragment: NavDatasets$key) => {
return (search: string) => {
Expand Down Expand Up @@ -69,107 +61,64 @@ const getUseSearch = (fragment: NavDatasets$key) => {
};
};

export const useGA = (info) => {
useEffect(() => {
if (!info || info.doNotTrack) {
return;
}
const dev = info.dev;
const buildType = dev ? "dev" : "prod";
ReactGA.initialize(gaConfig.app_ids[buildType], {
testMode: false,
gaOptions: {
storage: "none",
cookieDomain: "none",
clientId: info.uid,
page_location: "omitted",
page_path: "omitted",
kind: isElectron() ? "Desktop" : "Web",
version: info.version,
context: info.context,
checkProtocolTask: null, // disable check, allow file:// URLs
},
});
}, [info]);
};

const Nav: React.FC<{
fragment: NavFragment$key;
hasDataset: boolean;
}> = ({ fragment, hasDataset }) => {
const Nav: React.FC<
React.PropsWithChildren<{
fragment: NavFragment$key;
hasDataset: boolean;
}>
> = ({ children, fragment, hasDataset }) => {
const data = useFragment(
graphql`
fragment NavFragment on Query {
...Analytics
...NavDatasets
...NavGA
}
`,
fragment
);
const info = useFragment(
graphql`
fragment NavGA on Query {
context
dev
doNotTrack
uid
version
}
`,
data
);
const [analyticsInfo, setAnalyticsInfo] = useAnalyticsInfo();
useEffect(() => {
const buildType = info.dev ? "dev" : "prod";
const writeKey = DEFAULT_WRITE_KEYS[buildType];
setAnalyticsInfo({
userId: info.uid,
userGroup: "fiftyone-oss",
writeKey,
doNotTrack: info.doNotTrack,
debug: info.dev,
});
}, [info, setAnalyticsInfo]);
useGA(info);

const useSearch = getUseSearch(data);
const refresh = useRefresh();
const { mode, setMode } = useColorScheme();
const [_, setTheme] = useRecoilState(fos.theme);
const setTheme = useSetRecoilState(fos.theme);

return (
<Header
title={"FiftyOne"}
onRefresh={refresh}
navChildren={<DatasetSelector useSearch={useSearch} />}
>
{hasDataset && (
<Suspense fallback={<div style={{ flex: 1 }}></div>}>
<ViewBar />
</Suspense>
)}
{!hasDataset && <div style={{ flex: 1 }}></div>}
<div className={iconContainer}>
<Teams />
<IconButton
title={mode === "dark" ? "Light mode" : "Dark mode"}
onClick={() => {
const nextMode = mode === "dark" ? "light" : "dark";
setMode(nextMode);
setTheme(nextMode);
}}
sx={{
color: (theme) => theme.palette.text.secondary,
pr: 0,
}}
>
{mode === "dark" ? <LightMode color="inherit" /> : <DarkMode />}
</IconButton>
<SlackLink />
<GitHubLink />
<DocsLink />
</div>
</Header>
<>
<Header
title={"FiftyOne"}
onRefresh={refresh}
navChildren={<DatasetSelector useSearch={useSearch} />}
>
{hasDataset && (
<Suspense fallback={<div style={{ flex: 1 }}></div>}>
<ViewBar />
</Suspense>
)}
{!hasDataset && <div style={{ flex: 1 }}></div>}
<div className={iconContainer}>
<Teams />
<IconButton
title={mode === "dark" ? "Light mode" : "Dark mode"}
onClick={() => {
const nextMode = mode === "dark" ? "light" : "dark";
setMode(nextMode);
setTheme(nextMode);
}}
sx={{
color: (theme) => theme.palette.text.secondary,
pr: 0,
}}
>
{mode === "dark" ? <LightMode color="inherit" /> : <DarkMode />}
</IconButton>
<SlackLink />
<GitHubLink />
<DocsLink />
</div>
</Header>
{children}
<Analytics fragment={data} />
</>
);
};

Expand Down
Loading
Loading