From 29ebc43a0b3b0246225e32525cc1be422cddc9a5 Mon Sep 17 00:00:00 2001 From: Phil Berryman Date: Tue, 22 Nov 2022 15:29:56 -0800 Subject: [PATCH 1/2] feat: add number of searches Signed-off-by: Phil Berryman --- .changeset/clean-seals-whisper.md | 5 +++++ .changeset/dry-melons-listen.md | 8 ++++++++ .changeset/silver-rats-rest.md | 5 +++++ docs/plugins/analytics.md | 14 +++++++------- .../src/engines/ElasticSearchSearchEngine.ts | 1 + .../src/PgSearchEngine/PgSearchEngine.test.ts | 3 +++ .../src/PgSearchEngine/PgSearchEngine.ts | 3 ++- .../src/engines/LunrSearchEngine.ts | 1 + .../src/service/AuthorizedSearchEngine.ts | 1 + plugins/search-common/api-report.md | 2 ++ plugins/search-common/src/types.ts | 1 + .../src/components/SearchTracker/SearchTracker.tsx | 10 +++++++--- 12 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 .changeset/clean-seals-whisper.md create mode 100644 .changeset/dry-melons-listen.md create mode 100644 .changeset/silver-rats-rest.md diff --git a/.changeset/clean-seals-whisper.md b/.changeset/clean-seals-whisper.md new file mode 100644 index 0000000000..8891d75f8f --- /dev/null +++ b/.changeset/clean-seals-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-react': minor +--- + +The `value` of a search analytics event is now set as the total number of search results (when available) diff --git a/.changeset/dry-melons-listen.md b/.changeset/dry-melons-listen.md new file mode 100644 index 0000000000..1e09619060 --- /dev/null +++ b/.changeset/dry-melons-listen.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-search-backend': minor +'@backstage/plugin-search-backend-module-elasticsearch': minor +'@backstage/plugin-search-backend-node': minor +'@backstage/plugin-search-backend-module-pg': minor +--- + +numberOfResults is now provided alongside the query result diff --git a/.changeset/silver-rats-rest.md b/.changeset/silver-rats-rest.md new file mode 100644 index 0000000000..52bb9acd6f --- /dev/null +++ b/.changeset/silver-rats-rest.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-common': minor +--- + +numberOfResults (total number of results for a given query) can now be provided by each search engine and consumed as part of the search results response diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 46e1d354df..e04cb9c5c6 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -52,13 +52,13 @@ 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 | | -| `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 | -| `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 | | +| `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 | 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-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index a33d8939eb..72bab5b3b6 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -335,6 +335,7 @@ export class ElasticSearchSearchEngine implements SearchEngine { ), nextPageCursor, previousPageCursor, + numberOfResults: result.body.hits.total.value, }; } catch (error) { if (error.meta?.body?.error?.type === 'index_not_found_exception') { diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts index 23f8983074..c1d73c618d 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts @@ -231,6 +231,7 @@ describe('PgSearchEngine', () => { }, ], nextPageCursor: undefined, + numberOfResults: 1, }); expect(database.transaction).toHaveBeenCalledTimes(1); expect(database.query).toHaveBeenCalledWith(tx, { @@ -288,6 +289,7 @@ describe('PgSearchEngine', () => { }, })), nextPageCursor: 'MQ==', + numberOfResults: 30, }); expect(database.transaction).toHaveBeenCalledTimes(1); expect(database.query).toHaveBeenCalledWith(tx, { @@ -348,6 +350,7 @@ describe('PgSearchEngine', () => { })) .slice(25), previousPageCursor: 'MA==', + numberOfResults: 5, }); expect(database.transaction).toHaveBeenCalledTimes(1); expect(database.query).toHaveBeenCalledWith(tx, { diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index f53c433c2f..b0e8d0c6d2 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -194,6 +194,7 @@ export class PgSearchEngine implements SearchEngine { const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : undefined; + const numberOfResults = rows.length; const results = pageRows.map( ({ type, document, highlight }, index): IndexableResult => ({ @@ -215,7 +216,7 @@ export class PgSearchEngine implements SearchEngine { }), ); - return { results, nextPageCursor, previousPageCursor }; + return { results, nextPageCursor, previousPageCursor, numberOfResults }; } } diff --git a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts index d60f9951f4..ff2c828402 100644 --- a/plugins/search-backend-node/src/engines/LunrSearchEngine.ts +++ b/plugins/search-backend-node/src/engines/LunrSearchEngine.ts @@ -238,6 +238,7 @@ export class LunrSearchEngine implements SearchEngine { }), }, })), + numberOfResults: results.length, nextPageCursor, previousPageCursor, }; diff --git a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts index ade43280be..52c87d76c7 100644 --- a/plugins/search-backend/src/service/AuthorizedSearchEngine.ts +++ b/plugins/search-backend/src/service/AuthorizedSearchEngine.ts @@ -205,6 +205,7 @@ export class AuthorizedSearchEngine implements SearchEngine { (nextPageCursor || filteredResults.length > targetResults) ? encodePageCursor({ page: page + 1 }) : undefined, + numberOfResults: undefined, }; } diff --git a/plugins/search-common/api-report.md b/plugins/search-common/api-report.md index 61e5d22257..2006689ab9 100644 --- a/plugins/search-common/api-report.md +++ b/plugins/search-common/api-report.md @@ -73,6 +73,8 @@ export interface ResultSet { // (undocumented) nextPageCursor?: string; // (undocumented) + numberOfResults?: number; + // (undocumented) previousPageCursor?: string; // (undocumented) results: Result[]; diff --git a/plugins/search-common/src/types.ts b/plugins/search-common/src/types.ts index 54abc9e1b1..2ea4a490ca 100644 --- a/plugins/search-common/src/types.ts +++ b/plugins/search-common/src/types.ts @@ -87,6 +87,7 @@ export interface ResultSet { results: Result[]; nextPageCursor?: string; previousPageCursor?: string; + numberOfResults?: number; } /** diff --git a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index f900e45d45..3b758a0c47 100644 --- a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -23,14 +23,18 @@ import { useSearch } from '../../context'; */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { const analytics = useAnalytics(); - const { term } = useSearch(); + const { term, result } = useSearch(); + + const numberOfResults = result.value?.numberOfResults ?? undefined; useEffect(() => { if (term) { // Capture analytics search event with search term provided as value - analytics.captureEvent('search', term); + analytics.captureEvent('search', term, { + value: numberOfResults, + }); } - }, [analytics, term]); + }, [analytics, term, numberOfResults]); return <>{children}; }; From 38f46e6d5c938dbdfdf083998dc41a0b6562cc39 Mon Sep 17 00:00:00 2001 From: Phil Berryman Date: Fri, 25 Nov 2022 12:15:27 -0800 Subject: [PATCH 2/2] chore: remove addition of numberOfResults from PgSearchEngine Signed-off-by: Phil Berryman --- .changeset/dry-melons-listen.md | 1 - .../src/PgSearchEngine/PgSearchEngine.test.ts | 3 --- .../src/PgSearchEngine/PgSearchEngine.ts | 3 +-- .../components/SearchTracker/SearchTracker.tsx | 16 +++++++++++++--- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.changeset/dry-melons-listen.md b/.changeset/dry-melons-listen.md index 1e09619060..07342fb88e 100644 --- a/.changeset/dry-melons-listen.md +++ b/.changeset/dry-melons-listen.md @@ -2,7 +2,6 @@ '@backstage/plugin-search-backend': minor '@backstage/plugin-search-backend-module-elasticsearch': minor '@backstage/plugin-search-backend-node': minor -'@backstage/plugin-search-backend-module-pg': minor --- numberOfResults is now provided alongside the query result diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts index c1d73c618d..23f8983074 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.test.ts @@ -231,7 +231,6 @@ describe('PgSearchEngine', () => { }, ], nextPageCursor: undefined, - numberOfResults: 1, }); expect(database.transaction).toHaveBeenCalledTimes(1); expect(database.query).toHaveBeenCalledWith(tx, { @@ -289,7 +288,6 @@ describe('PgSearchEngine', () => { }, })), nextPageCursor: 'MQ==', - numberOfResults: 30, }); expect(database.transaction).toHaveBeenCalledTimes(1); expect(database.query).toHaveBeenCalledWith(tx, { @@ -350,7 +348,6 @@ describe('PgSearchEngine', () => { })) .slice(25), previousPageCursor: 'MA==', - numberOfResults: 5, }); expect(database.transaction).toHaveBeenCalledTimes(1); expect(database.query).toHaveBeenCalledWith(tx, { diff --git a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts index b0e8d0c6d2..f53c433c2f 100644 --- a/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts +++ b/plugins/search-backend-module-pg/src/PgSearchEngine/PgSearchEngine.ts @@ -194,7 +194,6 @@ export class PgSearchEngine implements SearchEngine { const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : undefined; - const numberOfResults = rows.length; const results = pageRows.map( ({ type, document, highlight }, index): IndexableResult => ({ @@ -216,7 +215,7 @@ export class PgSearchEngine implements SearchEngine { }), ); - return { results, nextPageCursor, previousPageCursor, numberOfResults }; + return { results, nextPageCursor, previousPageCursor }; } } diff --git a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx index 3b758a0c47..b19af6eef8 100644 --- a/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx +++ b/plugins/search-react/src/components/SearchTracker/SearchTracker.tsx @@ -17,24 +17,34 @@ import React, { useEffect } from 'react'; import { useAnalytics } from '@backstage/core-plugin-api'; import { useSearch } from '../../context'; +import usePrevious from 'react-use/lib/usePrevious'; /** * Capture search event on term change. */ export const TrackSearch = ({ children }: { children: React.ReactChild }) => { + const useHasChanged = (value: any) => { + const previousVal = usePrevious(value); + return previousVal !== value; + }; + const analytics = useAnalytics(); const { term, result } = useSearch(); const numberOfResults = result.value?.numberOfResults ?? undefined; + // Stops the analtyics event from firing before the new search engine response is returned + const hasStartedLoading = useHasChanged(result.loading); + const hasFinishedLoading = hasStartedLoading && !result.loading; + useEffect(() => { - if (term) { - // Capture analytics search event with search term provided as value + if (term && hasFinishedLoading) { + // Capture analytics search event with search term and numberOfResults (provided as value) analytics.captureEvent('search', term, { value: numberOfResults, }); } - }, [analytics, term, numberOfResults]); + }, [analytics, term, numberOfResults, hasFinishedLoading]); return <>{children}; };