diff --git a/.changeset/analytics-haunts-dismembered-constellations.md b/.changeset/analytics-haunts-dismembered-constellations.md
new file mode 100644
index 0000000000..cc685d68f0
--- /dev/null
+++ b/.changeset/analytics-haunts-dismembered-constellations.md
@@ -0,0 +1,23 @@
+---
+'@backstage/core-components': patch
+---
+
+The `` component now automatically instruments all link clicks using
+the new Analytics API. Each click triggers a `click` event, containing the
+location the user clicked to. In addition, these events inherit plugin-level
+metadata, allowing clicks to be attributed to the plugin containing the link:
+
+```json
+{
+ "verb": "click",
+ "noun": "/value/of-the/to-prop/passed-to-the-link",
+ "domain": {
+ "componentName": "Link",
+ "pluginId": "plugin-in-which-link-was-clicked",
+ "routeRef": "any-associated-route-ref-id"
+ }
+}
+```
+
+These events can be identified and handled by checking for the verb `click`
+and the componentName `Link`.
diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx
index 97a71c5760..0be4749f0a 100644
--- a/packages/core-components/src/components/Link/Link.test.tsx
+++ b/packages/core-components/src/components/Link/Link.test.tsx
@@ -16,10 +16,12 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
-import { wrapInTestApp } from '@backstage/test-utils';
+import { MockAnalyticsApi, wrapInTestApp } from '@backstage/test-utils';
+import { ApiProvider, ApiRegistry } from '@backstage/core-app-api';
import { isExternalUri, Link } from './Link';
import { Route, Routes } from 'react-router';
import { act } from 'react-dom/test-utils';
+import { analyticsApiRef } from '../../../../core-plugin-api/src';
describe('', () => {
it('navigates using react-router', async () => {
@@ -40,6 +42,36 @@ describe('', () => {
expect(getByText(testString)).toBeInTheDocument();
});
+ it('captures click using analytics api', async () => {
+ const linkText = 'Navigate!';
+ const analyticsApi = new MockAnalyticsApi();
+ const customOnClick = jest.fn();
+
+ const { getByText } = render(
+ wrapInTestApp(
+
+
+ {linkText}
+
+ ,
+ ),
+ );
+
+ await act(async () => {
+ fireEvent.click(getByText(linkText));
+ });
+
+ // Analytics event should have been fired.
+ expect(analyticsApi.getEvents()[0]).toMatchObject({
+ verb: 'click',
+ noun: '/test',
+ domain: { componentName: 'Link' },
+ });
+
+ // Custom onClick handler should have still been fired too.
+ expect(customOnClick).toHaveBeenCalled();
+ });
+
describe('isExternalUri', () => {
it.each([
[true, 'http://'],
diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx
index 0d301dccf9..b985fcb922 100644
--- a/packages/core-components/src/components/Link/Link.tsx
+++ b/packages/core-components/src/components/Link/Link.tsx
@@ -14,16 +14,19 @@
* limitations under the License.
*/
+import { useAnalytics, withAnalyticsDomain } from '@backstage/core-plugin-api';
import {
Link as MaterialLink,
LinkProps as MaterialLinkProps,
} from '@material-ui/core';
-import React, { ElementType } from 'react';
+import React, { ElementType, MutableRefObject } from 'react';
import {
Link as RouterLink,
LinkProps as RouterLinkProps,
} from 'react-router-dom';
+type OptionalRef = MutableRefObject | ((instance: any) => void) | null;
+
export const isExternalUri = (uri: string) => /^([a-z+.-]+):/.test(uri);
export type LinkProps = MaterialLinkProps &
@@ -36,28 +39,53 @@ declare function LinkType(props: LinkProps): JSX.Element;
/**
* Thin wrapper on top of material-ui's Link component
* Makes the Link to utilise react-router
+ * Thin wrapper on top of material-ui's Link component, which...
+ * - Makes the Link use react-router
+ * - Captures Link clicks as analytics events.
*/
-const ActualLink = React.forwardRef((props, ref) => {
- const to = String(props.to);
- const external = isExternalUri(to);
- const newWindow = external && !!/^https?:/.exec(to);
- return external ? (
- // External links
-
- ) : (
- // Interact with React Router for internal links
-
- );
-});
+const ActualLink = withAnalyticsDomain(
+ ({ inputRef, onClick, ...props }: LinkProps & { inputRef: OptionalRef }) => {
+ const analytics = useAnalytics();
+ const to = String(props.to);
+ const external = isExternalUri(to);
+ const newWindow = external && !!/^https?:/.exec(to);
+
+ const handleClick = (event: React.MouseEvent) => {
+ if (onClick !== undefined) {
+ onClick(event);
+ }
+ analytics.captureEvent('click', to);
+ };
+
+ return external ? (
+ // External links
+
+ ) : (
+ // Interact with React Router for internal links
+
+ );
+ },
+ { componentName: 'Link' },
+);
+
+export const WrappedLink = React.forwardRef((props, ref) => (
+
+));
// TODO(Rugvip): We use this as a workaround to make the exported type be a
// function, which makes our API reference docs much nicer.
// The first type to be exported gets priority, but it will
// be thrown away when compiling to JS.
// @ts-ignore
-export { LinkType as Link, ActualLink as Link };
+export { LinkType as Link, WrappedLink as Link };