diff --git a/.changeset/techdocs-analyze-clicks.md b/.changeset/techdocs-analyze-clicks.md new file mode 100644 index 0000000000..664fea10bd --- /dev/null +++ b/.changeset/techdocs-analyze-clicks.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +An analytics event matching the semantics of the `click` action is now captured when users click links within a TechDocs document. diff --git a/.changeset/techdocs-analyze-entities.md b/.changeset/techdocs-analyze-entities.md new file mode 100644 index 0000000000..b9c701c1a8 --- /dev/null +++ b/.changeset/techdocs-analyze-entities.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-react': patch +--- + +Analytics events captured within the `` now include the conventional `entityRef` context value, associating those events with their corresponding entity. diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index 6cc5537d9e..4800204f36 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -19,9 +19,13 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; -import { TestApiProvider } from '@backstage/test-utils'; +import { MockAnalyticsApi, TestApiProvider } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; -import { configApiRef } from '@backstage/core-plugin-api'; +import { + analyticsApiRef, + configApiRef, + useAnalytics, +} from '@backstage/core-plugin-api'; import { techdocsApiRef } from './api'; import { useTechDocsReaderPage, TechDocsReaderPageProvider } from './context'; @@ -60,6 +64,8 @@ const configApiMock = { getOptionalBoolean: jest.fn().mockReturnValue(undefined), }; +const analyticsApiMock = new MockAnalyticsApi(); + const wrapper = ({ entityRef = { kind: mockEntityMetadata.kind, @@ -74,6 +80,7 @@ const wrapper = ({ { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should set title', async () => { const { result, waitForNextUpdate } = renderHook( () => useTechDocsReaderPage(), @@ -157,4 +168,21 @@ describe('useTechDocsReaderPage', () => { expect(result.current.entityRef).toStrictEqual(caseSensitiveEntityRef); }); + + it('entityRef provided as analytics context', async () => { + const { waitForNextUpdate } = renderHook( + () => useAnalytics().captureEvent('action', 'subject'), + { wrapper }, + ); + + await waitForNextUpdate(); + + expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + action: 'action', + subject: 'subject', + context: { + entityRef: 'component:default/test', + }, + }); + }); }); diff --git a/plugins/techdocs-react/src/context.tsx b/plugins/techdocs-react/src/context.tsx index 66faf858fa..2a5b151824 100644 --- a/plugins/techdocs-react/src/context.tsx +++ b/plugins/techdocs-react/src/context.tsx @@ -33,7 +33,11 @@ import { createVersionedValueMap, } from '@backstage/version-bridge'; -import { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { + AnalyticsContext, + configApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { techdocsApiRef } from './api'; import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; @@ -141,9 +145,13 @@ export const TechDocsReaderPageProvider = memo( const versionedValue = createVersionedValueMap({ 1: value }); return ( - - {children instanceof Function ? children(value) : children} - + + + {children instanceof Function ? children(value) : children} + + ); }, (prevProps, nextProps) => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index e886454594..9babff0c54 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -21,7 +21,7 @@ import { useTheme, useMediaQuery } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { CompoundEntityRef } from '@backstage/catalog-model'; -import { useApi } from '@backstage/core-plugin-api'; +import { useAnalytics, useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { @@ -63,6 +63,7 @@ export const useTechDocsReaderDom = ( const isMobileMedia = useMediaQuery(MOBILE_MEDIA_QUERY); const sanitizerTransformer = useSanitizerTransformer(); const stylesTransformer = useStylesTransformer(); + const analytics = useAnalytics(); const techdocsStorageApi = useApi(techdocsStorageApiRef); const scmIntegrationsApi = useApi(scmIntegrationsApiRef); @@ -177,6 +178,12 @@ export const useTechDocsReaderDom = ( const parsedUrl = new URL(url); const fullPath = `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; + // capture link clicks within documentation + const linkText = + (event.target as HTMLAnchorElement | undefined)?.innerText || url; + const to = url.replace(window.location.origin, ''); + analytics.captureEvent('click', linkText, { attributes: { to } }); + // hash exists when anchor is clicked on secondary sidebar if (parsedUrl.hash) { if (modifierActive) { @@ -219,7 +226,7 @@ export const useTechDocsReaderDom = ( onLoaded: () => {}, }), ]), - [theme, navigate], + [theme, navigate, analytics], ); useEffect(() => {