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') {