diff --git a/.changeset/search-odd-starfishes-raise.md b/.changeset/search-odd-starfishes-raise.md
new file mode 100644
index 0000000000..73627cf457
--- /dev/null
+++ b/.changeset/search-odd-starfishes-raise.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-search': patch
+---
+
+Captures the search term entered in the SearchBarBase as a `search` event.
diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md
index ace9a89968..7fbd256e86 100644
--- a/docs/plugins/analytics.md
+++ b/docs/plugins/analytics.md
@@ -55,10 +55,11 @@ learn how to contribute the integration yourself!
The following table summarizes events that, depending on the plugins you have
installed, may be captured.
-| Action | Provided By | Subject |
-| ---------- | -------------- | ----------------------------------------- |
-| `navigate` | Backstage Core | The URL of the page that was navigated to |
-| `click` | Backstage Core | The text of the link that was clicked on |
+| Action | Provided By | Subject |
+| ---------- | -------------- | --------------------------------------------------- |
+| `navigate` | Backstage Core | The URL of the page that was navigated to |
+| `click` | Backstage Core | The text of the link that was clicked on |
+| `search` | Backstage Core | The search term entered in any search bar component |
If there is an event you'd like to see captured, please [open an
issue][add-event] describing the event you want to see and the questions it
diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx
index b3f30473ac..cb4008d494 100644
--- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx
+++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx
@@ -20,10 +20,10 @@ import userEvent from '@testing-library/user-event';
import { SearchContextProvider } from '../SearchContext';
import { SearchBar } from './SearchBar';
-import { configApiRef } from '@backstage/core-plugin-api';
+import { configApiRef, analyticsApiRef } from '@backstage/core-plugin-api';
import { ApiProvider, ConfigReader } from '@backstage/core-app-api';
import { searchApiRef } from '../../apis';
-import { TestApiRegistry } from '@backstage/test-utils';
+import { MockAnalyticsApi, TestApiRegistry } from '@backstage/test-utils';
jest.mock('@backstage/core-plugin-api', () => ({
...jest.requireActual('@backstage/core-plugin-api'),
@@ -38,9 +38,16 @@ describe('SearchBar', () => {
};
const query = jest.fn().mockResolvedValue({});
+ const analyticsApiSpy = new MockAnalyticsApi();
+ let apiRegistry: TestApiRegistry;
- const apiRegistry = TestApiRegistry.from(
- [configApiRef, new ConfigReader({ app: { title: 'Mock title' } })],
+ apiRegistry = TestApiRegistry.from(
+ [
+ configApiRef,
+ new ConfigReader({
+ app: { title: 'Mock title' },
+ }),
+ ],
[searchApiRef, { query }],
);
@@ -210,4 +217,128 @@ describe('SearchBar', () => {
expect.objectContaining({ term: value }),
);
});
+
+ it('does not capture analytics event if not enabled in app', async () => {
+ jest.useFakeTimers();
+
+ const debounceTime = 600;
+
+ render(
+
+
+
+
+ ,
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
+ });
+
+ const textbox = screen.getByRole('textbox', { name });
+
+ const value = 'value';
+
+ userEvent.type(textbox, value);
+
+ act(() => {
+ jest.advanceTimersByTime(debounceTime);
+ });
+
+ await waitFor(() => expect(textbox).toHaveValue(value));
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(0);
+ });
+
+ it('captures analytics events if enabled in app', async () => {
+ jest.useFakeTimers();
+
+ const debounceTime = 600;
+
+ apiRegistry = TestApiRegistry.from(
+ [analyticsApiRef, analyticsApiSpy],
+ [
+ configApiRef,
+ new ConfigReader({
+ app: {
+ title: 'Mock title',
+ analytics: {
+ ga: {
+ trackingId: 'xyz123',
+ },
+ },
+ },
+ }),
+ ],
+ [searchApiRef, { query }],
+ );
+
+ render(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByRole('textbox', { name })).toBeInTheDocument();
+ });
+
+ const textbox = screen.getByRole('textbox', { name });
+
+ const value = 'value';
+
+ userEvent.type(textbox, value);
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(0);
+
+ act(() => {
+ jest.advanceTimersByTime(debounceTime);
+ });
+
+ await waitFor(() => expect(textbox).toHaveValue(value));
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(1);
+ expect(analyticsApiSpy.getEvents()[0]).toEqual({
+ action: 'search',
+ context: {
+ extension: 'App',
+ pluginId: 'root',
+ routeRef: 'unknown',
+ searchTypes: 'software-catalog,techdocs',
+ },
+ subject: 'value',
+ });
+
+ userEvent.clear(textbox);
+
+ // make sure new term is captured
+ userEvent.type(textbox, 'new value');
+
+ act(() => {
+ jest.advanceTimersByTime(debounceTime);
+ });
+
+ await waitFor(() => expect(textbox).toHaveValue('new value'));
+
+ expect(analyticsApiSpy.getEvents()).toHaveLength(2);
+ expect(analyticsApiSpy.getEvents()[1]).toEqual({
+ action: 'search',
+ context: {
+ extension: 'App',
+ pluginId: 'root',
+ routeRef: 'unknown',
+ searchTypes: 'software-catalog,techdocs',
+ },
+ subject: 'new value',
+ });
+ });
});
diff --git a/plugins/search/src/components/SearchBar/SearchBar.tsx b/plugins/search/src/components/SearchBar/SearchBar.tsx
index 95bf1570e1..aa4b1e728a 100644
--- a/plugins/search/src/components/SearchBar/SearchBar.tsx
+++ b/plugins/search/src/components/SearchBar/SearchBar.tsx
@@ -33,6 +33,7 @@ import SearchIcon from '@material-ui/icons/Search';
import ClearButton from '@material-ui/icons/Clear';
import { useSearch } from '../SearchContext';
+import { TrackSearch } from '../SearchTracker';
/**
* Props for {@link SearchBarBase}.
@@ -119,18 +120,20 @@ export const SearchBarBase = ({
);
return (
-
+
+
+
);
};
@@ -150,8 +153,11 @@ export const SearchBar = ({ onChange, ...props }: SearchBarProps) => {
const { term, setTerm } = useSearch();
const handleChange = (newValue: string) => {
- setTerm(newValue);
- if (onChange) onChange(newValue);
+ if (onChange) {
+ onChange(newValue);
+ } else {
+ setTerm(newValue);
+ }
};
return ;
diff --git a/plugins/search/src/components/SearchContext/SearchContext.tsx b/plugins/search/src/components/SearchContext/SearchContext.tsx
index dd9310d782..300a191983 100644
--- a/plugins/search/src/components/SearchContext/SearchContext.tsx
+++ b/plugins/search/src/components/SearchContext/SearchContext.tsx
@@ -15,7 +15,7 @@
*/
import { JsonObject } from '@backstage/types';
-import { useApi } from '@backstage/core-plugin-api';
+import { useApi, AnalyticsContext } from '@backstage/core-plugin-api';
import { SearchResultSet } from '@backstage/search-common';
import React, {
createContext,
@@ -130,7 +130,11 @@ export const SearchContextProvider = ({
fetchPreviousPage: hasPreviousPage ? fetchPreviousPage : undefined,
};
- return ;
+ return (
+
+
+
+ );
};
export const useSearch = () => {
diff --git a/plugins/search/src/components/SearchTracker/SearchTracker.tsx b/plugins/search/src/components/SearchTracker/SearchTracker.tsx
new file mode 100644
index 0000000000..b9da9b5560
--- /dev/null
+++ b/plugins/search/src/components/SearchTracker/SearchTracker.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useEffect } from 'react';
+import { useAnalytics } from '@backstage/core-plugin-api';
+import { useSearch } from '../SearchContext';
+
+/**
+ * Capture search event on term change.
+ */
+export const TrackSearch = ({ children }: { children: React.ReactChild }) => {
+ const analytics = useAnalytics();
+ const { term } = useSearch();
+
+ useEffect(() => {
+ if (term) {
+ // Capture analytics search event with search term provided as value
+ analytics.captureEvent('search', term);
+ }
+ }, [analytics, term]);
+
+ return <>{children}>;
+};
diff --git a/plugins/search/src/components/SearchTracker/index.ts b/plugins/search/src/components/SearchTracker/index.ts
new file mode 100644
index 0000000000..5e6a75aa25
--- /dev/null
+++ b/plugins/search/src/components/SearchTracker/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { TrackSearch } from './SearchTracker';