Skip to content

Commit

Permalink
feat(posthog): disable autocapture and track pageview on location change
Browse files Browse the repository at this point in the history
- Disable autocapture to not send any event anymore.
- Disable autocapture of pageview in favor of TrackerPageView, which sends a $pageview event each time the location change.
- Move the TrackerProvider under the Router in order to receive updates on the location.
- Keep autocapture of pageleave enable.
- Configure PostHog client to use POSTHOG_API_HOST env variable if set, and fallback to stat.zextrsa.tools. This configuration allows using a different PostHog instance in development. Be aware that the host env is not set in Jenkins, meaning that for the packages build through it only the key will be set, while the host will always use the fallback.
- Move each component/hook in a specific file, under the tracker folder.

Refs: SHELL-251 (#529)
  • Loading branch information
beawar authored Oct 18, 2024
1 parent 05d6f29 commit c951174
Show file tree
Hide file tree
Showing 16 changed files with 179 additions and 71 deletions.
3 changes: 2 additions & 1 deletion __mocks__/posthog-js/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const postHog = {
has_opted_in_capturing: (): boolean => false,
setPersonProperties: (): void => undefined,
set_config: (): void => undefined,
config: {} as PostHogConfig
config: {} as PostHogConfig,
capture: (): undefined => undefined
} satisfies Partial<ReturnType<(typeof PostHogReact)['usePostHog']>>;

export const usePostHog: (typeof PostHogReact)['usePostHog'] = () =>
Expand Down
3 changes: 2 additions & 1 deletion carbonio.webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const configFn = (
new DefinePlugin({
COMMIT_ID: JSON.stringify(commitHash.toString().trim()),
BASE_PATH: JSON.stringify(baseStaticPath),
POSTHOG_API_KEY: JSON.stringify(process.env.POSTHOG_API_KEY)
POSTHOG_API_KEY: JSON.stringify(process.env.POSTHOG_API_KEY),
POSTHOG_API_HOST: JSON.stringify(process.env.POSTHOG_API_HOST)
}),
new HtmlWebpackPlugin({
inject: true,
Expand Down
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ const config: Config = {
// A set of global variables that need to be available in all test environments
globals: {
BASE_PATH: '',
POSTHOG_API_HOST: '',
POSTHOG_API_KEY: ''
},

Expand Down
3 changes: 1 addition & 2 deletions src/boot/app/app-direct-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,5 @@ export const {

export { useIsCarbonioCE } from '../../store/login/hooks';

export { useTracker } from '../posthog';

export type { NewAction } from '../../shell/creation-button';
export { useTracker } from '../../tracker/tracker';
1 change: 0 additions & 1 deletion src/boot/app/default-views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable no-param-reassign */

import { useEffect, useMemo } from 'react';

Expand Down
18 changes: 9 additions & 9 deletions src/boot/bootstrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import AppLoaderMounter from './app/app-loader-mounter';
import { DefaultViewsRegister } from './app/default-views';
import { ContextBridge } from './context-bridge';
import { Loader } from './loader';
import { TrackerProvider } from './posthog';
import { ShellI18nextProvider } from './shell-i18n-provider';
import { ThemeProvider } from './theme-provider';
import { BASENAME, IS_FOCUS_MODE } from '../constants';
import { NotificationPermissionChecker } from '../notification/NotificationPermissionChecker';
import ShellView from '../shell/shell-view';
import { useAppStore } from '../store/app/store';
import { TrackerProvider } from '../tracker/provider';

const FocusModeListener = (): null => {
const { route } = useParams<{ route?: string }>();
Expand All @@ -30,10 +30,10 @@ const FocusModeListener = (): null => {
};

const Bootstrapper = (): React.JSX.Element => (
<TrackerProvider>
<ThemeProvider>
<ShellI18nextProvider>
<BrowserRouter basename={BASENAME}>
<ThemeProvider>
<ShellI18nextProvider>
<BrowserRouter basename={BASENAME}>
<TrackerProvider>
<SnackbarManager>
<Loader />
{IS_FOCUS_MODE && (
Expand All @@ -49,10 +49,10 @@ const Bootstrapper = (): React.JSX.Element => (
<AppLoaderMounter />
<ShellView />
</SnackbarManager>
</BrowserRouter>
</ShellI18nextProvider>
</ThemeProvider>
</TrackerProvider>
</TrackerProvider>
</BrowserRouter>
</ShellI18nextProvider>
</ThemeProvider>
);

export default Bootstrapper;
6 changes: 3 additions & 3 deletions src/boot/loader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { EventEmitter } from 'node:events';

import type * as loadAppsModule from './app/load-apps';
import { Loader } from './loader';
import * as posthog from './posthog';
import { LOGIN_V3_CONFIG_PATH } from '../constants';
import { getGetInfoRequest } from '../mocks/handlers/getInfoRequest';
import server from '../mocks/server';
Expand All @@ -20,6 +19,7 @@ import { useLoginConfigStore } from '../store/login/store';
import { TIMERS } from '../tests/constants';
import { spyOnPosthog } from '../tests/posthog-utils';
import { controlConsoleError, setup, screen } from '../tests/utils';
import * as tracker from '../tracker/tracker';
import type { AccountSettingsPrefs } from '../types/account';
import * as utils from '../utils/utils';

Expand Down Expand Up @@ -120,7 +120,7 @@ describe('Loader', () => {
)
);
jest
.spyOn(posthog, 'useTracker')
.spyOn(tracker, 'useTracker')
.mockReturnValue({ enableTracker: enableTrackerFn, reset: jest.fn(), capture: jest.fn() });
setup(
<span data-testid={'loader'}>
Expand Down Expand Up @@ -180,7 +180,7 @@ describe('Loader', () => {
)
);
jest
.spyOn(posthog, 'useTracker')
.spyOn(tracker, 'useTracker')
.mockReturnValue({ enableTracker: enableTrackerFn, reset: jest.fn(), capture: jest.fn() });
setup(
<span data-testid={'loader'}>
Expand Down
2 changes: 1 addition & 1 deletion src/boot/loader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { find } from 'lodash';
import { useTranslation } from 'react-i18next';

import { loadApps, unloadAllApps } from './app/load-apps';
import { useTracker } from './posthog';
import { IS_FOCUS_MODE } from '../constants';
import { getComponents } from '../network/get-components';
import { getInfo } from '../network/get-info';
Expand All @@ -20,6 +19,7 @@ import { logout } from '../network/logout';
import { goToLogin } from '../network/utils';
import { useAccountStore } from '../store/account';
import { useAppStore } from '../store/app';
import { useTracker } from '../tracker/tracker';

export function isPromiseRejectedResult<T>(
promiseSettledResult: PromiseSettledResult<T>
Expand Down
1 change: 1 addition & 0 deletions src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ComponentType } from 'react';
declare global {
const BASE_PATH: string;
const POSTHOG_API_KEY: string;
const POSTHOG_API_HOST: string;
interface Window {
__ZAPP_SHARED_LIBRARIES__?: {
'@zextras/carbonio-shell-ui': {
Expand Down
55 changes: 55 additions & 0 deletions src/tracker/page-view.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react';

import { Link } from 'react-router-dom';

import { TrackerPageView } from './page-view';
import * as useTracker from './tracker';
import type { Tracker } from './tracker';
import { screen, setup } from '../tests/utils';

describe('TrackerPageView', () => {
it('should capture pageview event when pathname change', async () => {
const tracker: Tracker = {
capture: jest.fn(),
enableTracker: jest.fn(),
reset: jest.fn()
};
jest.spyOn(useTracker, 'useTracker').mockReturnValue(tracker);
const { user } = setup(
<>
<TrackerPageView />
<Link to={'/different-path'}>Go to different path</Link>
</>,
{ initialRouterEntries: ['/initial-path'] }
);
await user.click(screen.getByRole('link'));
expect(tracker.capture).toHaveBeenLastCalledWith('$pageview', {
$current_url: `${window.origin}/different-path`
});
});

it('should capture pageview event when search params change', async () => {
const tracker: Tracker = {
capture: jest.fn(),
enableTracker: jest.fn(),
reset: jest.fn()
};
jest.spyOn(useTracker, 'useTracker').mockReturnValue(tracker);
const { user } = setup(
<>
<TrackerPageView />
<Link to={'/initial-path?param=2'}>Go to different path</Link>
</>,
{ initialRouterEntries: ['/initial-path?param=1'] }
);
await user.click(screen.getByRole('link'));
expect(tracker.capture).toHaveBeenLastCalledWith('$pageview', {
$current_url: `${window.origin}/initial-path?param=2`
});
});
});
22 changes: 22 additions & 0 deletions src/tracker/page-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect } from 'react';

import { useLocation } from 'react-router-dom';

import { useTracker } from './tracker';

export const TrackerPageView = (): null => {
const tracker = useTracker();
const { pathname, search } = useLocation();
useEffect(() => {
tracker.capture('$pageview', {
$current_url: window.origin + pathname + search
});
}, [pathname, search, tracker]);

return null;
};
38 changes: 38 additions & 0 deletions src/tracker/provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

import React from 'react';

import * as posthogJsReact from 'posthog-js/react';
import type * as PostHogReact from 'posthog-js/react';

import { TrackerProvider } from './provider';
import { setup } from '../tests/utils';
import * as utils from '../utils/utils';

beforeEach(() => {
jest.spyOn(utils, 'getCurrentLocationHost').mockReturnValue('differentHost');
});

describe('TrackerProvider', () => {
it('should invoke tracker provider with trackers disabled by default', () => {
const mockProvider = jest.spyOn(posthogJsReact, 'PostHogProvider');
setup(<TrackerProvider />);
type PostHogProviderProps = React.ComponentPropsWithoutRef<
(typeof PostHogReact)['PostHogProvider']
>;
expect(mockProvider).toHaveBeenLastCalledWith(
expect.objectContaining<PostHogProviderProps>({
options: expect.objectContaining<NonNullable<PostHogProviderProps['options']>>({
opt_out_capturing_by_default: true,
disable_session_recording: true,
disable_surveys: true
})
}),
expect.anything()
);
});
});
36 changes: 36 additions & 0 deletions src/tracker/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useMemo } from 'react';

import type { PostHogConfig } from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';

import { TrackerPageView } from './page-view';

export const TrackerProvider = ({
children
}: React.PropsWithChildren<Record<never, never>>): React.JSX.Element => {
const options = useMemo(
(): Partial<PostHogConfig> => ({
api_host: POSTHOG_API_HOST || 'https://stats.zextras.tools',
person_profiles: 'identified_only',
opt_out_capturing_by_default: true,
disable_session_recording: true,
mask_all_text: true,
disable_surveys: true,
capture_pageview: false,
capture_pageleave: true,
autocapture: false
}),
[]
);
return (
<PostHogProvider apiKey={POSTHOG_API_KEY} options={options}>
{children}
<TrackerPageView />
</PostHogProvider>
);
};
30 changes: 3 additions & 27 deletions src/boot/posthog.test.tsx → src/tracker/tracker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,21 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

import React from 'react';

import { act, waitFor, renderHook } from '@testing-library/react';
import { act, renderHook, waitFor } from '@testing-library/react';
import type { CaptureOptions } from 'posthog-js';
import * as posthogJsReact from 'posthog-js/react';
import type * as PostHogReact from 'posthog-js/react';

import { TrackerProvider, useTracker } from './posthog';
import { useTracker } from './tracker';
import { useAccountStore } from '../store/account';
import { useLoginConfigStore } from '../store/login/store';
import { mockedAccount } from '../tests/account-utils';
import { spyOnPosthog } from '../tests/posthog-utils';
import { setup } from '../tests/utils';
import * as utils from '../utils/utils';

beforeEach(() => {
jest.spyOn(utils, 'getCurrentLocationHost').mockReturnValue('differentHost');
});

describe('Posthog', () => {
describe('useTracker', () => {
it('should opt-in posthog if host is not localhost and enableTracker is called with true value', () => {
const posthog = spyOnPosthog();
const { result } = renderHook(() => useTracker());
Expand Down Expand Up @@ -67,24 +61,6 @@ describe('Posthog', () => {
expect(posthog.capture).toHaveBeenCalledWith(eventName, properties, options);
});

it('should invoke posthog provider with trackers disabled by default', () => {
const mockProvider = jest.spyOn(posthogJsReact, 'PostHogProvider');
setup(<TrackerProvider></TrackerProvider>);
type PostHogProviderProps = React.ComponentPropsWithoutRef<
(typeof PostHogReact)['PostHogProvider']
>;
expect(mockProvider).toHaveBeenLastCalledWith(
expect.objectContaining<PostHogProviderProps>({
options: expect.objectContaining<NonNullable<PostHogProviderProps['options']>>({
opt_out_capturing_by_default: true,
disable_session_recording: true,
disable_surveys: true
})
}),
expect.anything()
);
});

it('should enable surveys if user is opted in and Carbonio is CE', () => {
useLoginConfigStore.setState({ isCarbonioCE: true });
const posthog = spyOnPosthog();
Expand Down
29 changes: 4 additions & 25 deletions src/boot/posthog.tsx → src/tracker/tracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,16 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';

import type { CaptureOptions, PostHogConfig, Properties } from 'posthog-js';
import { PostHogProvider, usePostHog } from 'posthog-js/react';
import type { CaptureOptions, Properties } from 'posthog-js';
import { usePostHog } from 'posthog-js/react';

import { useAccountStore } from '../store/account';
import { useIsCarbonioCE } from '../store/login/hooks';
import { getCurrentLocationHost } from '../utils/utils';

export const TrackerProvider = ({
children
}: React.PropsWithChildren<Record<never, never>>): React.JSX.Element => {
const options = useMemo(
(): Partial<PostHogConfig> => ({
api_host: 'https://stats.zextras.tools',
person_profiles: 'identified_only',
opt_out_capturing_by_default: true,
disable_session_recording: true,
mask_all_text: true,
disable_surveys: true
}),
[]
);
return (
<PostHogProvider apiKey={POSTHOG_API_KEY} options={options}>
{children}
</PostHogProvider>
);
};

interface Tracker {
export interface Tracker {
enableTracker: (enable: boolean) => void;
reset: () => void;
capture: (
Expand Down
Loading

0 comments on commit c951174

Please sign in to comment.