From c37e4803f0e31ff5dbe7edc18520f90878fdf317 Mon Sep 17 00:00:00 2001 From: Agata Drapala Date: Fri, 7 Feb 2025 16:44:36 +0100 Subject: [PATCH 1/3] fix: capturing the correct number of search results in the search analytics event Signed-off-by: Agata Drapala --- .changeset/cool-moons-lay.md | 5 + .../src/context/SearchContext.test.tsx | 152 +++++++++--------- .../src/context/SearchContext.tsx | 2 +- 3 files changed, 85 insertions(+), 74 deletions(-) create mode 100644 .changeset/cool-moons-lay.md diff --git a/.changeset/cool-moons-lay.md b/.changeset/cool-moons-lay.md new file mode 100644 index 0000000000..98367ff979 --- /dev/null +++ b/.changeset/cool-moons-lay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': patch +--- + +Capture the number of search results in the search analytics event that correspond to the term entered. diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index f858715adb..796eca351f 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -421,31 +421,34 @@ describe('SearchContext', () => { }); describe('analytics', () => { - it('captures analytics events if enabled in app', async () => { - const analyticsApiMock = mockApis.analytics(); + const analyticsApiMock = mockApis.analytics(); + const Wrapper = ({ children }: React.PropsWithChildren) => ( + + + {children} + + + ); - searchApiMock.query.mockResolvedValue({ - results: [], - numberOfResults: 3, - }); + it('captures analytics events if enabled in app', async () => { + searchApiMock.query + .mockResolvedValueOnce({ + results: [], + numberOfResults: 10, + }) + .mockResolvedValueOnce({ + results: [], + numberOfResults: 3, + }); const { result } = renderHook(() => useSearch(), { - wrapper: ({ children }) => { - const configApiMock = mockApis.config(); - return ( - - - {children} - - - ); - }, + wrapper: Wrapper, }); await waitFor(() => { @@ -476,59 +479,62 @@ describe('SearchContext', () => { }); }); }); - }); - it('captures analytics events even if number of results does not exist', async () => { - const analyticsApiMock = mockApis.analytics(); - - searchApiMock.query.mockResolvedValue({ - results: [], - }); - - const { result } = renderHook(() => useSearch(), { - wrapper: ({ children }) => { - const configApiMock = mockApis.config(); - return ( - - - {children} - - - ); - }, - }); - - await waitFor(() => { - expect(result.current).toEqual(expect.objectContaining(initialState)); - }); - - const term = 'term'; - - await act(async () => { - result.current.setTerm(term); - }); - - await waitFor(() => { - expect(searchApiMock.query).toHaveBeenLastCalledWith({ - term: 'term', - types: ['*'], - filters: {}, + it('does not capture analytics events if term is empty', async () => { + searchApiMock.query.mockResolvedValue({ + results: [], + numberOfResults: 10, }); - expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ - action: 'search', - subject: 'term', - value: undefined, - context: { - extension: 'App', - pluginId: 'root', - routeRef: 'unknown', - }, + + renderHook(() => useSearch(), { + wrapper: Wrapper, + }); + + await waitFor(() => { + expect(searchApiMock.query).toHaveBeenCalledWith({ + term: '', + types: ['*'], + filters: {}, + }); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); + }); + }); + + it('captures analytics events even if number of results does not exist', async () => { + searchApiMock.query.mockResolvedValue({ + results: [], + }); + + const { result } = renderHook(() => useSearch(), { + wrapper: Wrapper, + }); + + await waitFor(() => { + expect(result.current).toEqual(expect.objectContaining(initialState)); + }); + + const term = 'term'; + + await act(async () => { + result.current.setTerm(term); + }); + + await waitFor(() => { + expect(searchApiMock.query).toHaveBeenLastCalledWith({ + term: 'term', + types: ['*'], + filters: {}, + }); + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ + action: 'search', + subject: 'term', + value: undefined, + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + }, + }); }); }); }); diff --git a/plugins/search-react/src/context/SearchContext.tsx b/plugins/search-react/src/context/SearchContext.tsx index f09ebac42a..ef9063ba30 100644 --- a/plugins/search-react/src/context/SearchContext.tsx +++ b/plugins/search-react/src/context/SearchContext.tsx @@ -140,7 +140,7 @@ const useSearchContextValue = ( }); if (term) { analytics.captureEvent('search', term, { - value: result.value?.numberOfResults ?? undefined, + value: resultSet.numberOfResults, }); } return resultSet; From 7507056511d40d84b5ce8c0dcce312f6113c4b9e Mon Sep 17 00:00:00 2001 From: Agata Drapala Date: Mon, 17 Feb 2025 11:42:48 +0100 Subject: [PATCH 2/3] testing two non-empty terms Signed-off-by: Agata Drapala --- .../src/context/SearchContext.test.tsx | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 796eca351f..6eb60e4d0c 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -445,31 +445,41 @@ describe('SearchContext', () => { .mockResolvedValueOnce({ results: [], numberOfResults: 3, + }) + .mockResolvedValueOnce({ + results: [], + numberOfResults: 1, }); + // first api call with empty term const { result } = renderHook(() => useSearch(), { wrapper: Wrapper, }); await waitFor(() => { expect(result.current).toEqual(expect.objectContaining(initialState)); + expect(searchApiMock.query).toHaveBeenLastCalledWith({ + term: '', + types: ['*'], + filters: {}, + }); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); - const term = 'term'; - + // second api call with term 'eva' await act(async () => { - result.current.setTerm(term); + result.current.setTerm('eva'); }); await waitFor(() => { expect(searchApiMock.query).toHaveBeenLastCalledWith({ - term: 'term', + term: 'eva', types: ['*'], filters: {}, }); expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'search', - subject: 'term', + subject: 'eva', value: 3, context: { extension: 'App', @@ -478,25 +488,28 @@ describe('SearchContext', () => { }, }); }); - }); - it('does not capture analytics events if term is empty', async () => { - searchApiMock.query.mockResolvedValue({ - results: [], - numberOfResults: 10, - }); - - renderHook(() => useSearch(), { - wrapper: Wrapper, + // third api call with term 'eva.m' + await act(async () => { + result.current.setTerm('eva.m'); }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenCalledWith({ - term: '', + expect(searchApiMock.query).toHaveBeenLastCalledWith({ + term: 'eva.m', types: ['*'], filters: {}, }); - expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ + action: 'search', + subject: 'eva.m', + value: 1, + context: { + extension: 'App', + pluginId: 'root', + routeRef: 'unknown', + }, + }); }); }); From cb070f7bd868df30eb930ed4b8f8a6c2641e16ee Mon Sep 17 00:00:00 2001 From: Agata Drapala Date: Mon, 17 Feb 2025 13:17:51 +0100 Subject: [PATCH 3/3] code clean-up Signed-off-by: Agata Drapala --- .../src/context/SearchContext.test.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 6eb60e4d0c..586c67a830 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -436,7 +436,7 @@ describe('SearchContext', () => { ); - it('captures analytics events if enabled in app', async () => { + it('captures analytics events for non-empty term', async () => { searchApiMock.query .mockResolvedValueOnce({ results: [], @@ -451,14 +451,13 @@ describe('SearchContext', () => { numberOfResults: 1, }); - // first api call with empty term + // search with empty term const { result } = renderHook(() => useSearch(), { wrapper: Wrapper, }); await waitFor(() => { - expect(result.current).toEqual(expect.objectContaining(initialState)); - expect(searchApiMock.query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenCalledWith({ term: '', types: ['*'], filters: {}, @@ -466,13 +465,13 @@ describe('SearchContext', () => { expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); - // second api call with term 'eva' + // search with term 'eva' await act(async () => { result.current.setTerm('eva'); }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenCalledWith({ term: 'eva', types: ['*'], filters: {}, @@ -489,13 +488,13 @@ describe('SearchContext', () => { }); }); - // third api call with term 'eva.m' + // search with new term 'eva.m' await act(async () => { result.current.setTerm('eva.m'); }); await waitFor(() => { - expect(searchApiMock.query).toHaveBeenLastCalledWith({ + expect(searchApiMock.query).toHaveBeenCalledWith({ term: 'eva.m', types: ['*'], filters: {},