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

fix: analytics at boot time #3759

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
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
10 changes: 4 additions & 6 deletions packages/extension/src/companion/Companion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,11 @@ export default function Companion({
}),
});
const [assetsLoadedDebounce] = useDebounceFn(() => setAssetsLoaded(true), 10);
const routeChangedCallbackRef = useLogPageView();
const routeChangedCallback = useLogPageView();

useEffect(() => {
if (routeChangedCallbackRef.current) {
routeChangedCallbackRef.current();
}
}, [routeChangedCallbackRef]);
routeChangedCallback?.();
}, [routeChangedCallback]);

const [checkAssets, clearCheckAssets] = useDebounceFn(() => {
if (containerRef?.current?.offsetLeft === 0) {
Expand All @@ -133,7 +131,7 @@ export default function Companion({
}

checkAssets();
routeChangedCallbackRef.current();
routeChangedCallback?.();
// @NOTE see https://dailydotdev.atlassian.net/l/cp/dK9h1zoM
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [containerRef]);
Expand Down
8 changes: 4 additions & 4 deletions packages/extension/src/newtab/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function InternalApp(): ReactElement {
const { contentScriptGranted } = useContentScriptStatus();
const { hostGranted, isFetching: isCheckingHostPermissions } =
useHostStatus();
const routeChangedCallbackRef = useLogPageView();
const routeChangedCallback = useLogPageView();
useConsoleLogo();
const { user, isAuthReady } = useAuthContext();
const { growthbook } = useGrowthBookContext();
Expand All @@ -108,10 +108,10 @@ function InternalApp(): ReactElement {
const shouldRedirectOnboarding = !user && isPageReady && !isTesting;

useEffect(() => {
if (routeChangedCallbackRef.current && isPageReady) {
routeChangedCallbackRef.current();
if (isPageReady && currentPage) {
routeChangedCallback?.();
}
}, [isPageReady, routeChangedCallbackRef, currentPage]);
}, [isPageReady, routeChangedCallback, currentPage]);

const { dismissToast } = useToastNotification();

Expand Down
11 changes: 6 additions & 5 deletions packages/shared/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { AccessToken, Boot, Visit } from '../lib/boot';
import { isCompanionActivated } from '../lib/element';
import { AuthTriggers, AuthTriggersType } from '../lib/auth';
import { Squad } from '../graphql/sources';
import { checkIsExtension, isNullOrUndefined } from '../lib/func';
import { checkIsExtension } from '../lib/func';

export interface LoginState {
trigger: AuthTriggersType;
Expand Down Expand Up @@ -65,6 +65,7 @@ export interface AuthContextData {
isAuthReady?: boolean;
geo?: Boot['geo'];
}

const isExtension = checkIsExtension();
const AuthContext = React.createContext<AuthContextData>(null);
export const useAuthContext = (): AuthContextData => useContext(AuthContext);
Expand Down Expand Up @@ -104,7 +105,7 @@ export type AuthContextProviderProps = {
isFetched?: boolean;
isLegacyLogout?: boolean;
children?: ReactNode;
firstLoad?: boolean;
isAuthReady?: boolean;
} & Pick<
AuthContextData,
| 'getRedirectUri'
Expand Down Expand Up @@ -133,15 +134,15 @@ export const AuthContextProvider = ({
isLegacyLogout,
accessToken,
squads,
firstLoad,
isAuthReady,
geo,
}: AuthContextProviderProps): ReactElement => {
const [loginState, setLoginState] = useState<LoginState | null>(null);
const endUser = user && 'providers' in user ? user : null;
const referral = user?.referralId || user?.referrer;
const referralOrigin = user?.referralOrigin;

if (firstLoad === true && endUser && !endUser?.infoConfirmed) {
if (!!isAuthReady && endUser && !endUser?.infoConfirmed) {
logout(LogoutReason.IncomleteOnboarding);
}

Expand All @@ -152,7 +153,7 @@ export const AuthContextProvider = ({
return (
<AuthContext.Provider
value={{
isAuthReady: !isNullOrUndefined(firstLoad),
isAuthReady,
user: endUser,
isLoggedIn: !!endUser?.id,
referral: loginState?.referral ?? referral,
Expand Down
81 changes: 41 additions & 40 deletions packages/shared/src/contexts/BootProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import React, {
ReactElement,
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
Expand Down Expand Up @@ -87,7 +86,7 @@ const updateLocalBootData = (
return result;
};

const getCachedOrNull = () => {
const getCachedBootOrNull = () => {
try {
return JSON.parse(storage.getItem(BOOT_LOCAL_KEY));
} catch (err) {
Expand All @@ -112,6 +111,8 @@ export const BootDataProvider = ({
getRedirectUri,
getPage,
}: BootDataProviderProps): ReactElement => {
const { hostGranted } = useHostStatus();
const isExtension = checkIsExtension();
const queryClient = useQueryClient();
const preloadFeedsRef = useRef<PreloadFeeds>();
preloadFeedsRef.current = ({ feeds, user }) => {
Expand All @@ -130,8 +131,7 @@ export const BootDataProvider = ({
);
};

const [initialLoad, setInitialLoad] = useState<boolean>(null);
const [cachedBootData, setCachedBootData] = useState<Partial<Boot>>(() => {
const initialData = useMemo(() => {
if (localBootData) {
return localBootData;
}
Expand All @@ -149,15 +149,14 @@ export const BootDataProvider = ({
preloadFeedsRef.current({ feeds: boot.feeds, user: boot.user });

return boot;
});
const { hostGranted } = useHostStatus();
const isExtension = checkIsExtension();
const logged = cachedBootData?.user as LoggedUser;
}, [localBootData]);

const logged = initialData?.user as LoggedUser;
const shouldRefetch = !!logged?.providers && !!logged?.id;
const lastAppliedChangeRef = useRef<Partial<BootCacheData>>();

const {
data: remoteData,
data: bootData,
error,
refetch,
isFetched,
Expand All @@ -168,24 +167,25 @@ export const BootDataProvider = ({
queryFn: async () => {
const result = await getBootData(app);
preloadFeedsRef.current({ feeds: result.feeds, user: result.user });
updateLocalBootData(bootData || {}, result);

return result;
},
refetchOnWindowFocus: shouldRefetch,
staleTime: STALE_TIME,
enabled: !isExtension || !!hostGranted,
placeholderData: initialData,
});

const isBootReady = isFetched && !isError;
const loadedFromCache = !!cachedBootData;
const { user, settings, alerts, notifications, squads, geo } =
cachedBootData || {};
const isBootReady = isFetched && !isError && !!bootData;
const loadedFromCache = !!bootData;
const { user, settings, alerts, notifications, squads, geo } = bootData || {};

useRefreshToken(remoteData?.accessToken, refetch);
useRefreshToken(bootData?.accessToken, refetch);
const updatedAtActive = user ? dataUpdatedAt : null;
const updateBootData = useCallback(
const updateQueryCache = useCallback(
(updatedBootData: Partial<BootCacheData>, update = true) => {
const cachedData = getCachedOrNull() || {};
const cachedData = getCachedBootOrNull() ?? {};
const lastAppliedChange = lastAppliedChangeRef.current;
let updatedData = { ...updatedBootData };
if (update) {
Expand All @@ -201,51 +201,52 @@ export const BootDataProvider = ({
}

const updated = updateLocalBootData(cachedData, updatedData);
setCachedBootData(updated);

queryClient.setQueryData<Partial<Boot>>(BOOT_QUERY_KEY, (previous) => {
if (!previous) {
return updated;
}

return { ...previous, ...updated };
});
},
[],
[queryClient],
);

const updateUser = useCallback(
async (newUser: LoggedUser | AnonymousUser) => {
updateBootData({ user: newUser });
updateQueryCache({ user: newUser });
await queryClient.invalidateQueries({
queryKey: generateQueryKey(RequestKey.Profile, newUser),
});
},
[updateBootData, queryClient],
[updateQueryCache, queryClient],
);

const updateSettings = useCallback(
(updatedSettings) => updateBootData({ settings: updatedSettings }),
[updateBootData],
(updatedSettings: Boot['settings']) =>
updateQueryCache({ settings: updatedSettings }),
[updateQueryCache],
);

const updateAlerts = useCallback(
(updatedAlerts) => updateBootData({ alerts: updatedAlerts }),
[updateBootData],
(updatedAlerts: Boot['alerts']) =>
updateQueryCache({ alerts: updatedAlerts }),
[updateQueryCache],
);

const updateExperimentation = useCallback(
(exp: BootCacheData['exp']) => {
updateLocalBootData(cachedBootData, { exp });
updateLocalBootData(bootData, { exp });
},
[cachedBootData],
[bootData],
);

gqlClient.setHeader(
'content-language',
(user as Partial<LoggedUser>)?.language || ContentLanguage.English,
);

useEffect(() => {
if (remoteData) {
setInitialLoad(initialLoad === null);
updateBootData(remoteData);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [remoteData]);

if (error) {
return (
<div className="mx-2 flex h-screen items-center justify-center">
Expand All @@ -259,7 +260,7 @@ export const BootDataProvider = ({
app={app}
user={user}
deviceId={deviceId}
experimentation={cachedBootData?.exp}
experimentation={bootData?.exp}
updateExperimentation={updateExperimentation}
>
<AuthContextProvider
Expand All @@ -269,13 +270,13 @@ export const BootDataProvider = ({
getRedirectUri={getRedirectUri}
loadingUser={!dataUpdatedAt || !user}
loadedUserFromCache={loadedFromCache}
visit={remoteData?.visit}
visit={bootData?.visit}
refetchBoot={refetch}
isFetched={isBootReady}
isLegacyLogout={remoteData?.isLegacyLogout}
accessToken={remoteData?.accessToken}
isLegacyLogout={bootData?.isLegacyLogout}
accessToken={bootData?.accessToken}
squads={squads}
firstLoad={initialLoad}
isAuthReady={isBootReady}
geo={geo}
>
<SettingsContextProvider
Expand Down
65 changes: 37 additions & 28 deletions packages/shared/src/hooks/log/useLogContextData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MutableRefObject, useMemo } from 'react';
import { MutableRefObject, useCallback } from 'react';
import { LogEvent, PushToQueueFunc } from './useLogQueue';
import { getCurrentLifecycleState } from '../../lib/lifecycle';
import { Origin } from '../../lib/log';
Expand Down Expand Up @@ -51,33 +51,42 @@ export default function useLogContextData(
durationEventsQueue: MutableRefObject<Map<string, LogEvent>>,
sendBeacon: () => void,
): LogContextData {
return useMemo<LogContextData>(
() => ({
logEvent(event: LogEvent) {
pushToQueue([generateEvent(event, sharedPropsRef, getPage())]);
},
logEventStart(id, event) {
if (!durationEventsQueue.current.has(id)) {
durationEventsQueue.current.set(
id,
generateEvent(event, sharedPropsRef, getPage()),
);
}
},
logEventEnd(id, now = new Date()) {
const event = durationEventsQueue.current.get(id);
if (event) {
durationEventsQueue.current.delete(id);
event.event_duration =
now.getTime() - event.event_timestamp.getTime();
if (window.scrollY > 0 && event.event_name !== 'page inactive') {
event.page_state = 'active';
}
pushToQueue([event]);
const logEvent = useCallback(
(event: LogEvent) => {
pushToQueue([generateEvent(event, sharedPropsRef, getPage())]);
},
[getPage, pushToQueue, sharedPropsRef],
);
const logEventStart = useCallback(
(id, event) => {
if (!durationEventsQueue.current.has(id)) {
durationEventsQueue.current.set(
id,
generateEvent(event, sharedPropsRef, getPage()),
);
}
},
[durationEventsQueue, getPage, sharedPropsRef],
);
const logEventEnd = useCallback(
(id, now = new Date()) => {
const event = durationEventsQueue.current.get(id);
if (event) {
durationEventsQueue.current.delete(id);
event.event_duration = now.getTime() - event.event_timestamp.getTime();
if (window.scrollY > 0 && event.event_name !== 'page inactive') {
event.page_state = 'active';
}
},
sendBeacon,
}),
[sharedPropsRef, getPage, pushToQueue, durationEventsQueue, sendBeacon],
pushToQueue([event]);
}
},
[durationEventsQueue, pushToQueue],
);

return {
logEvent,
logEventStart,
logEventEnd,
sendBeacon,
};
}
Loading