diff --git a/.changeset/tiny-files-judge.md b/.changeset/tiny-files-judge.md new file mode 100644 index 0000000000..781adef3c7 --- /dev/null +++ b/.changeset/tiny-files-judge.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +A new analytics event `not-found` will be published when a user visits a documentation site that does not exist diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index bc12d44c25..0673566217 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -58,13 +58,14 @@ learn how to contribute the integration yourself! The following table summarizes events that, depending on the plugins you have installed, may be captured. -| Action | Subject | Other Notes | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `navigate` | The URL of the page that was navigated to. | Fired immediately when route location changes (unless associated plugin/route data is ambiguous, in which case the event is fired after plugin/route data becomes known, immediately before the next event or document unload). The parameters of the current route will be included as attributes. | -| `click` | The text of the link that was clicked on. | The `to` attribute represents the URL clicked to. | -| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`). | -| `search` | The search term entered in any search bar component. | The context holds `searchTypes`, representing `types` constraining the search. The `value` represents the total number of search results for the query. This may not be visible if the permission framework is being used. | -| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided. | +| Action | Subject | Other Notes | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `navigate` | The URL of the page that was navigated to. | Fired immediately when route location changes (unless associated plugin/route data is ambiguous, in which case the event is fired after plugin/route data becomes known, immediately before the next event or document unload). The parameters of the current route will be included as attributes. | +| `click` | The text of the link that was clicked on. | The `to` attribute represents the URL clicked to. | +| `create` | The `name` of the software being created; if no `name` property is requested by the given Software Template, then the string `new {templateName}` is used instead. | The context holds an `entityRef`, set to the template's ref (e.g. `template:default/template-name`). | +| `search` | The search term entered in any search bar component. | The context holds `searchTypes`, representing `types` constraining the search. The `value` represents the total number of search results for the query. This may not be visible if the permission framework is being used. | +| `discover` | The title of the search result that was clicked on | The `value` is the result rank. A `to` attribute is also provided. | +| `not-found` | The path of the resource that resulted in a not found page | Fired by at least TechDocs. | If there is an event you'd like to see captured, please [open an issue](https://github.com/backstage/backstage/issues/new?assignees=&labels=enhancement&template=feature_template.md&title=[Analytics%20Event]:%20THE+EVENT+TO+CAPTURE) describing the event you want to see and the questions it diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index ce029a1910..59c388df69 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -16,8 +16,37 @@ import { TechDocsNotFound } from './TechDocsNotFound'; import React from 'react'; -import { screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { render, screen, waitFor } from '@testing-library/react'; +import { + MockAnalyticsApi, + TestApiProvider, + renderInTestApp, + wrapInTestApp, +} from '@backstage/test-utils'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; + +jest.mock('@backstage/plugin-techdocs-react', () => { + const actualModule = jest.requireActual('@backstage/plugin-techdocs-react'); + return { + ...actualModule, + useTechDocsReaderPage: () => ({ + entityRef: { name: 'name', namespace: 'namespace', kind: 'kind' }, + }), + }; +}); + +jest.mock('react-router-dom', () => { + const actualModule = jest.requireActual('react-router-dom'); + return { + ...actualModule, + useLocation: () => + ({ + pathname: '/the/pathname', + search: '?the=search', + hash: '#the-anchor', + } as Location), + }; +}); describe('', () => { it('should render with status code, status message and go back link', async () => { @@ -27,21 +56,33 @@ describe('', () => { screen.getByText(/Looks like someone dropped the mic!/i); expect(screen.getByTestId('go-back-link')).toBeDefined(); }); -}); -describe('', () => { - it('should render with status code, custom error message and go back link', async () => { - await renderInTestApp( - , + it('should trigger analytics event not-found', async () => { + const mockAnalyticsApi = new MockAnalyticsApi(); + + render( + wrapInTestApp( + + + , + ), ); - screen.getByText(/This is a custom error message/i); - screen.getByText(/404/i); - screen.getByText(/Looks like someone dropped the mic!/i); - expect(screen.getByTestId('go-back-link')).toBeDefined(); + + await waitFor(() => { + expect(mockAnalyticsApi.getEvents()[0]).toMatchObject({ + action: 'not-found', + subject: '/the/pathname?the=search#the-anchor', + attributes: { + name: 'name', + namespace: 'namespace', + kind: 'kind', + }, + }); + }); }); }); -describe('', () => { +describe('', () => { it('should render with a 404 code, custom error message and go back link', async () => { await renderInTestApp( , diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx index 4bc7d4fa59..bd2ef26b1e 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx @@ -14,9 +14,11 @@ * limitations under the License. */ -import React from 'react'; -import { useApi, configApiRef } from '@backstage/core-plugin-api'; +import React, { useEffect } from 'react'; +import { useApi, configApiRef, useAnalytics } from '@backstage/core-plugin-api'; import { ErrorPage } from '@backstage/core-components'; +import { useTechDocsReaderPage } from '@backstage/plugin-techdocs-react'; +import { useLocation } from 'react-router-dom'; type Props = { errorMessage?: string; @@ -25,6 +27,16 @@ type Props = { export const TechDocsNotFound = ({ errorMessage }: Props) => { const techdocsBuilder = useApi(configApiRef).getOptionalString('techdocs.builder'); + const analyticsApi = useAnalytics(); + const { entityRef } = useTechDocsReaderPage(); + const location = useLocation(); + + useEffect(() => { + const { pathname, search, hash } = location; + analyticsApi.captureEvent('not-found', `${pathname}${search}${hash}`, { + attributes: entityRef, + }); + }, [analyticsApi, entityRef, location]); let additionalInfo = ''; if (techdocsBuilder !== 'local') {