Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to
- ✨(frontend) integrate configurable Waffle #1795
- ✨ Import of documents #1609
- 🚨(CI) gives warning if theme not updated #1811
- ✨(frontend) Add stat from Crisp #1824
- ✨(auth) add silent login #1690
- 🔧(project) add DJANGO_EMAIL_URL_APP environment variable #1825

Expand Down
16 changes: 10 additions & 6 deletions src/frontend/apps/impress/src/core/config/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
useSynchronizedLanguage,
} from '@/features/language';
import { useAnalytics } from '@/libs';
import { CrispProvider, PostHogAnalytic } from '@/services';
import { CrispAnalytic, PostHogAnalytic } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';

import { useConfig } from './api/useConfig';
Expand Down Expand Up @@ -73,6 +73,14 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
new PostHogAnalytic(conf.POSTHOG_KEY);
}, [conf?.POSTHOG_KEY]);

useEffect(() => {
if (!conf?.CRISP_WEBSITE_ID) {
return;
}

new CrispAnalytic({ websiteId: conf.CRISP_WEBSITE_ID });
}, [conf?.CRISP_WEBSITE_ID]);

if (!conf) {
return (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
Expand All @@ -91,11 +99,7 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
{conf?.FRONTEND_JS_URL && (
<Script src={conf?.FRONTEND_JS_URL} strategy="afterInteractive" />
)}
<AnalyticsProvider>
<CrispProvider websiteId={conf?.CRISP_WEBSITE_ID}>
{children}
</CrispProvider>
</AnalyticsProvider>
<AnalyticsProvider>{children}</AnalyticsProvider>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { render } from '@testing-library/react';
import React from 'react';
import { describe, expect, test, vi } from 'vitest';

import { AppWrapper } from '@/tests/utils';

import { LinkReach } from '../../doc-management';
import { DocEditor } from '../components/DocEditor';

vi.mock('@/stores', () => ({
useResponsiveStore: () => ({ isDesktop: false }),
}));

vi.mock('@/features/skeletons', () => ({
useSkeletonStore: () => ({
setIsSkeletonVisible: vi.fn(),
}),
}));

vi.mock('../../doc-management', async () => {
const actual = await vi.importActual<any>('../../doc-management');
return {
...actual,
useIsCollaborativeEditable: () => ({ isEditable: true, isLoading: false }),
useProviderStore: () => ({
provider: {
configuration: { name: 'test-doc-id' },
document: {
getXmlFragment: () => null,
},
},
isReady: true,
}),
getDocLinkReach: (doc: any) => doc.computed_link_reach,
};
});

vi.mock('../../doc-table-content', () => ({
TableContent: () => null,
}));

vi.mock('../../doc-header', () => ({
DocHeader: () => null,
}));

vi.mock('../components/BlockNoteEditor', () => ({
BlockNoteEditor: () => null,
BlockNoteReader: () => null,
}));

vi.mock('../../../auth', async () => {
const actual = await vi.importActual<any>('../../../auth');
return {
...actual,
useAuth: () => ({ authenticated: true }),
};
});

const TrackEventMock = vi.fn();
vi.mock('../../../../libs', async () => {
const actual = await vi.importActual<any>('../../../../libs');
return {
...actual,
useAnalytics: () => ({
trackEvent: TrackEventMock,
}),
};
});

describe('DocEditor', () => {
test('it checks that trackevent is called with correct parameters', () => {
const doc = {
id: 'test-doc-id-1',
computed_link_reach: LinkReach.PUBLIC,
deleted_at: null,
abilities: {
partial_update: true,
},
} as any;

const { rerender } = render(<DocEditor doc={doc} />, {
wrapper: AppWrapper,
});

expect(TrackEventMock).toHaveBeenCalledWith({
eventName: 'doc',
isPublic: true,
authenticated: true,
});

// Rerender with same doc to check that event is not tracked again
rerender(
<DocEditor doc={{ ...doc, computed_link_reach: LinkReach.RESTRICTED }} />,
);

expect(TrackEventMock).toHaveBeenNthCalledWith(1, {
eventName: 'doc',
isPublic: true,
authenticated: true,
});

// Rerender with different doc to check that event is tracked again
rerender(
<DocEditor
doc={{
...doc,
id: 'test-doc-id-2',
computed_link_reach: LinkReach.RESTRICTED,
}}
/>,
);

expect(TrackEventMock).toHaveBeenNthCalledWith(2, {
eventName: 'doc',
isPublic: false,
authenticated: true,
});
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import clsx from 'clsx';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';

import { Box, Loading } from '@/components';
import { DocHeader } from '@/docs/doc-header/';
import {
Doc,
LinkReach,
getDocLinkReach,
useIsCollaborativeEditable,
useProviderStore,
} from '@/docs/doc-management';
import { TableContent } from '@/docs/doc-table-content/';
import { useAuth } from '@/features/auth/';
import { useSkeletonStore } from '@/features/skeletons';
import { useAnalytics } from '@/libs';
import { useResponsiveStore } from '@/stores';

import { BlockNoteEditor, BlockNoteReader } from './BlockNoteEditor';
Expand Down Expand Up @@ -83,13 +87,41 @@ export const DocEditor = ({ doc }: DocEditorProps) => {
!doc.abilities.partial_update || !isEditable || isLoading || isDeletedDoc;
const { setIsSkeletonVisible } = useSkeletonStore();
const isProviderReady = isReady && provider;
const { trackEvent } = useAnalytics();
const [hasTracked, setHasTracked] = useState(false);
const { authenticated } = useAuth();
const isPublicDoc = getDocLinkReach(doc) === LinkReach.PUBLIC;

useEffect(() => {
if (isProviderReady) {
setIsSkeletonVisible(false);
}
}, [isProviderReady, setIsSkeletonVisible]);

/**
* Track doc view event only once per doc change
*/
useEffect(() => {
setHasTracked(false);
}, [doc.id]);

/**
* Track doc view event
*/
useEffect(() => {
if (hasTracked) {
return;
}

setHasTracked(true);

trackEvent({
eventName: 'doc',
isPublic: isPublicDoc,
authenticated,
});
}, [authenticated, hasTracked, isPublicDoc, trackEvent]);

if (!isProviderReady || provider?.configuration.name !== doc.id) {
return <Loading />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
useIsCollaborativeEditable,
useTrans,
} from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useDate } from '@/hooks';
import { useResponsiveStore } from '@/stores';

interface DocHeaderInfoProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';

import { useClipboard } from '@/hook';
import { useClipboard } from '@/hooks';

import { Doc } from '../types';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
TextErrors,
} from '@/components';
import { Doc } from '@/docs/doc-management';
import { useDate } from '@/hook';
import { useDate } from '@/hooks';

import { useDocVersionsInfiniteQuery } from '../api/useDocVersions';
import { Versions } from '../types';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useConfig } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach, SimpleDocItem } from '@/docs/doc-management';
import { DocShareModal } from '@/docs/doc-share';
import { useDate } from '@/hook';
import { useDate } from '@/hooks';
import { useResponsiveStore } from '@/stores';

import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Box, Icon, SeparatedSection } from '@/components';
import { useDocStore } from '@/docs/doc-management';
import { DocSearchModal } from '@/docs/doc-search/';
import { useAuth } from '@/features/auth';
import { useCmdK } from '@/hook/useCmdK';
import { useCmdK } from '@/hooks/useCmdK';

import { useLeftPanelStore } from '../stores';

Expand Down
2 changes: 0 additions & 2 deletions src/frontend/apps/impress/src/hook/index.ts

This file was deleted.

3 changes: 3 additions & 0 deletions src/frontend/apps/impress/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './useClipboard';
export * from './useCmdK';
export * from './useDate';
export * from './useKeyboardAction';
10 changes: 9 additions & 1 deletion src/frontend/apps/impress/src/libs/Analytics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ type AnalyticEventUser = {
id: string;
email: string;
};
type AnalyticEventDoc = {
eventName: 'doc';
isPublic: boolean;
authenticated: boolean;
};

export type AnalyticEvent = AnalyticEventClick | AnalyticEventUser;
export type AnalyticEvent =
| AnalyticEventClick
| AnalyticEventUser
| AnalyticEventDoc;

export abstract class AbstractAnalytic {
public constructor() {
Expand Down
34 changes: 33 additions & 1 deletion src/frontend/apps/impress/src/services/Crisp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
*/

import { Crisp } from 'crisp-sdk-web';
import { PropsWithChildren, useEffect, useState } from 'react';
import { JSX, PropsWithChildren, ReactNode, useEffect, useState } from 'react';
import { createGlobalStyle } from 'styled-components';

import { User } from '@/features/auth';
import { AbstractAnalytic, AnalyticEvent } from '@/libs';

const CrispStyle = createGlobalStyle`
#crisp-chatbox a{
Expand Down Expand Up @@ -70,3 +71,34 @@ export const CrispProvider = ({
</>
);
};

export class CrispAnalytic extends AbstractAnalytic {
private conf?: CrispProviderProps = undefined;
private EVENT = {
PUBLIC_DOC_NOT_CONNECTED: 'public-doc-not-connected',
};

public constructor(conf?: CrispProviderProps) {
super();

this.conf = conf;
}

public Provider(children?: ReactNode): JSX.Element {
return (
<CrispProvider websiteId={this.conf?.websiteId}>{children}</CrispProvider>
);
}

public trackEvent(evt: AnalyticEvent): void {
if (evt.eventName === 'doc') {
if (evt.isPublic && !evt.authenticated) {
Crisp.trigger.run(this.EVENT.PUBLIC_DOC_NOT_CONNECTED);
}
}
}

public isFeatureFlagActivated(): boolean {
return true;
}
}
Loading