diff --git a/examples/sandbox/package.json b/examples/sandbox/package.json
index e67bfe9bd..fbf9a9fca 100644
--- a/examples/sandbox/package.json
+++ b/examples/sandbox/package.json
@@ -14,6 +14,7 @@
"dependencies": {
"@elastic/apm-rum": "^5.12.0",
"@elastic/apm-rum-react": "^1.4.2",
+ "@elastic/behavioral-analytics-javascript-tracker": "^2.1.3",
"@elastic/datemath": "^5.0.3",
"@elastic/eui": "^49.0.0",
"@elastic/react-search-ui": "1.19.1",
diff --git a/examples/sandbox/src/Router.js b/examples/sandbox/src/Router.js
index 5853dae10..1bad99b15 100644
--- a/examples/sandbox/src/Router.js
+++ b/examples/sandbox/src/Router.js
@@ -2,6 +2,7 @@ import * as React from "react";
import { Switch } from "react-router-dom";
import Root from "./pages/root";
import Elasticsearch from "./pages/elasticsearch";
+import ElasticsearchWithAnalytics from "./pages/elasticsearch-with-analytics";
import Engines from "./pages/engines";
import AppSearch from "./pages/app-search";
import SiteSearch from "./pages/site-search";
@@ -50,6 +51,11 @@ export default function Router() {
path="/search-bar-in-header/search"
component={SearchBarInHeaderSearch}
/>
+
{/* Use cases */}
diff --git a/examples/sandbox/src/pages/elasticsearch-with-analytics/index.js b/examples/sandbox/src/pages/elasticsearch-with-analytics/index.js
new file mode 100644
index 000000000..7b584b60b
--- /dev/null
+++ b/examples/sandbox/src/pages/elasticsearch-with-analytics/index.js
@@ -0,0 +1,320 @@
+import React from "react";
+import "@elastic/eui/dist/eui_theme_light.css";
+import AnalyticsPlugin from "@elastic/search-ui-analytics-plugin";
+import {
+ createTracker,
+ getTracker
+} from "@elastic/behavioral-analytics-javascript-tracker";
+
+import ElasticSearchAPIConnector from "@elastic/search-ui-elasticsearch-connector";
+import moment from "moment";
+
+import {
+ ErrorBoundary,
+ Facet,
+ SearchProvider,
+ SearchBox,
+ Results,
+ PagingInfo,
+ ResultsPerPage,
+ Paging,
+ Sorting,
+ WithSearch
+} from "@elastic/react-search-ui";
+import {
+ BooleanFacet,
+ Layout,
+ SingleLinksFacet,
+ SingleSelectFacet
+} from "@elastic/react-search-ui-views";
+import "@elastic/react-search-ui-views/lib/styles/styles.css";
+
+createTracker({
+ collectionName: "search-ui",
+ endpoint: "http://localhost:9200"
+});
+const connector = new ElasticSearchAPIConnector({
+ host:
+ process.env.REACT_ELASTICSEARCH_HOST ||
+ "https://search-ui-sandbox.es.us-central1.gcp.cloud.es.io:9243",
+ index: process.env.REACT_ELASTICSEARCH_INDEX || "national-parks",
+ apiKey:
+ process.env.REACT_ELASTICSEARCH_API_KEY ||
+ "SlUzdWE0QUJmN3VmYVF2Q0F6c0I6TklyWHFIZ3lTbHF6Yzc2eEtyeWFNdw=="
+});
+
+const config = {
+ debug: true,
+ alwaysSearchOnInitialLoad: true,
+ apiConnector: connector,
+ hasA11yNotifications: true,
+ plugins: [AnalyticsPlugin({ client: getTracker() })],
+ searchQuery: {
+ filters: [],
+ search_fields: {
+ title: {
+ weight: 3
+ },
+ description: {},
+ states: {}
+ },
+ result_fields: {
+ visitors: { raw: {} },
+ world_heritage_site: { raw: {} },
+ location: { raw: {} },
+ acres: { raw: {} },
+ square_km: { raw: {} },
+ title: {
+ snippet: {
+ size: 100,
+ fallback: true
+ }
+ },
+ nps_link: { raw: {} },
+ states: { raw: {} },
+ date_established: { raw: {} },
+ image_url: { raw: {} },
+ description: {
+ snippet: {
+ size: 100,
+ fallback: true
+ }
+ }
+ },
+ disjunctiveFacets: [
+ "acres",
+ "states.keyword",
+ "date_established",
+ "location"
+ ],
+ facets: {
+ "world_heritage_site.keyword": { type: "value" },
+ "states.keyword": { type: "value", size: 30, sort: "count" },
+ acres: {
+ type: "range",
+ ranges: [
+ { from: -1, name: "Any" },
+ { from: 0, to: 1000, name: "Small" },
+ { from: 1001, to: 100000, name: "Medium" },
+ { from: 100001, name: "Large" }
+ ]
+ },
+ location: {
+ // San Francisco. In the future, make this the user's current position
+ center: "37.7749, -122.4194",
+ type: "range",
+ unit: "mi",
+ ranges: [
+ { from: 0, to: 100, name: "Nearby" },
+ { from: 100, to: 500, name: "A longer drive" },
+ { from: 500, name: "Perhaps fly?" }
+ ]
+ },
+ date_established: {
+ type: "range",
+ ranges: [
+ {
+ from: moment().subtract(50, "years").toISOString(),
+ name: "Within the last 50 years"
+ },
+ {
+ from: moment().subtract(100, "years").toISOString(),
+ to: moment().subtract(50, "years").toISOString(),
+ name: "50 - 100 years ago"
+ },
+ {
+ to: moment().subtract(100, "years").toISOString(),
+ name: "More than 100 years ago"
+ }
+ ]
+ },
+ visitors: {
+ type: "range",
+ ranges: [
+ { from: 0, to: 10000, name: "0 - 10000" },
+ { from: 10001, to: 100000, name: "10001 - 100000" },
+ { from: 100001, to: 500000, name: "100001 - 500000" },
+ { from: 500001, to: 1000000, name: "500001 - 1000000" },
+ { from: 1000001, to: 5000000, name: "1000001 - 5000000" },
+ { from: 5000001, to: 10000000, name: "5000001 - 10000000" },
+ { from: 10000001, name: "10000001+" }
+ ]
+ }
+ }
+ },
+ autocompleteQuery: {
+ results: {
+ search_fields: {
+ parks_search_as_you_type: {}
+ },
+ resultsPerPage: 5,
+ result_fields: {
+ title: {
+ snippet: {
+ size: 100,
+ fallback: true
+ }
+ },
+ nps_link: {
+ raw: {}
+ }
+ }
+ },
+ suggestions: {
+ types: {
+ documents: {
+ fields: ["parks_completion"]
+ }
+ },
+ size: 4
+ }
+ }
+};
+
+const SORT_OPTIONS = [
+ {
+ name: "Relevance",
+ value: []
+ },
+ {
+ name: "Title",
+ value: [
+ {
+ field: "title.keyword",
+ direction: "asc"
+ }
+ ]
+ },
+ {
+ name: "State",
+ value: [
+ {
+ field: "states.keyword",
+ direction: "asc"
+ }
+ ]
+ },
+ {
+ name: "State -> Title",
+ value: [
+ {
+ field: "states.keyword",
+ direction: "asc"
+ },
+ {
+ field: "title.keyword",
+ direction: "asc"
+ }
+ ]
+ },
+ {
+ name: "Heritage Site -> State -> Title",
+ value: [
+ {
+ field: "world_heritage_site.keyword",
+ direction: "asc"
+ },
+ {
+ field: "states.keyword",
+ direction: "asc"
+ },
+ {
+ field: "title.keyword",
+ direction: "asc"
+ }
+ ]
+ }
+];
+
+export default function App() {
+ return (
+
+ ({
+ wasSearched
+ })}
+ >
+ {({ wasSearched }) => {
+ return (
+
+
+
+ }
+ sideContent={
+
+ {wasSearched && (
+
+ )}
+
+
+
+
+
+
+
+
+ }
+ bodyContent={
+
+ }
+ bodyHeader={
+
+ {wasSearched && }
+ {wasSearched && }
+
+ }
+ bodyFooter={}
+ />
+
+
+ );
+ }}
+
+
+ );
+}
diff --git a/examples/sandbox/src/pages/root.js b/examples/sandbox/src/pages/root.js
index 95f77b0e4..c31ea6011 100644
--- a/examples/sandbox/src/pages/root.js
+++ b/examples/sandbox/src/pages/root.js
@@ -35,6 +35,11 @@ export default function Root() {
Search bar in header
+
+
+ Elasticsearch with analytics plugin
+
+
Explore use cases:
diff --git a/packages/search-ui-analytics-plugin/package.json b/packages/search-ui-analytics-plugin/package.json
index d1a2f7aba..f4b6e5d6c 100644
--- a/packages/search-ui-analytics-plugin/package.json
+++ b/packages/search-ui-analytics-plugin/package.json
@@ -40,5 +40,8 @@
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
+ },
+ "dependencies": {
+ "@elastic/behavioral-analytics-tracker-core": "^2.0.5"
}
}
diff --git a/packages/search-ui-analytics-plugin/src/__tests__/index.test.ts b/packages/search-ui-analytics-plugin/src/__tests__/index.test.ts
index 1657247f6..6d7a16d55 100644
--- a/packages/search-ui-analytics-plugin/src/__tests__/index.test.ts
+++ b/packages/search-ui-analytics-plugin/src/__tests__/index.test.ts
@@ -24,7 +24,8 @@ describe("analytics plugin", () => {
eaPlugin.subscribe({
type: "SearchQuery",
query: "test",
- totalResults: 0
+ totalResults: 0,
+ filters: []
});
expect(window["elasticAnalytics"].trackEvent).not.toBeCalled();
expect(internalClient.trackEvent).toBeCalled();
@@ -36,12 +37,23 @@ describe("analytics plugin", () => {
eaPlugin.subscribe({
type: "SearchQuery",
query: "test",
- totalResults: 0
+ totalResults: 0,
+ filters: []
});
expect(window["elasticAnalytics"].trackEvent).toBeCalledWith("search", {
- type: "SearchQuery",
- query: "test",
- totalResults: 0
+ search: {
+ filters: {},
+ page: {
+ current: undefined,
+ size: undefined
+ },
+ results: {
+ items: [],
+ total_results: 0
+ },
+ sort: undefined,
+ query: "test"
+ }
});
});
});
diff --git a/packages/search-ui-analytics-plugin/src/index.ts b/packages/search-ui-analytics-plugin/src/index.ts
index 64e8e5f58..082ef0912 100644
--- a/packages/search-ui-analytics-plugin/src/index.ts
+++ b/packages/search-ui-analytics-plugin/src/index.ts
@@ -1,17 +1,104 @@
-import type { Event } from "@elastic/search-ui";
+import {
+ Event,
+ BaseEvent,
+ ResultSelectedEvent,
+ SearchQueryEvent,
+ FilterValueRange,
+ FilterValue
+} from "@elastic/search-ui";
+import type {
+ Tracker,
+ TrackerEventType,
+ EventInputProperties,
+ SearchEventInputProperties,
+ SearchClickEventInputProperties
+} from "@elastic/behavioral-analytics-tracker-core";
-export type EventType = "search" | "click" | "pageview";
+export interface AnalyticsPluginOptions {
+ client?: Pick;
+}
+
+const transformSearchQueryEvent = (event: Omit) => {
+ const transformFilterValues = (values: FilterValue[]): string[] => {
+ const transformBasicValue = (value: string | boolean | number) =>
+ value.toString();
+ const transformRangeValue = (value: FilterValueRange) =>
+ `${value.from || "*"}-${value.to || "*"}`;
+
+ return values.reduce((res, value) => {
+ if (Array.isArray(value)) {
+ return [...res, ...value.map(transformBasicValue)];
+ }
-export type AnalyticsClient = {
- trackEvent: (eventType: EventType, payload: Record) => void;
+ return [
+ ...res,
+ typeof value === "object"
+ ? transformRangeValue(value)
+ : transformBasicValue(value)
+ ];
+ }, []);
+ };
+
+ return {
+ search: {
+ query: event.query,
+ filters: event.filters.reduce(
+ (res, filter) => ({
+ ...res,
+ [filter.field]: transformFilterValues(filter.values)
+ }),
+ {}
+ ),
+ page: {
+ current: event.currentPage,
+ size: event.resultsPerPage
+ },
+ results: {
+ items: [],
+ total_results: event.totalResults
+ },
+ sort: event.sort
+ ?.filter(
+ (sort) => sort.direction === "desc" || sort.direction === "asc"
+ )
+ .map((sort) => ({
+ name: sort.field,
+ direction: sort.direction as "asc" | "desc"
+ }))
+ }
+ };
};
-export interface AnalyticsPluginOptions {
- client?: AnalyticsClient;
-}
+type TrackerParams<
+ T extends TrackerEventType,
+ K extends EventInputProperties
+> = [T, K];
+
+const mapEventToTrackerParams: Record<
+ ResultSelectedEvent["type"] | SearchQueryEvent["type"],
+ (
+ event: BaseEvent
+ ) =>
+ | TrackerParams<"search_click", SearchClickEventInputProperties>
+ | TrackerParams<"search", SearchEventInputProperties>
+> = {
+ ResultSelected: (event: ResultSelectedEvent) => [
+ "search_click",
+ {
+ ...transformSearchQueryEvent(event),
+ document: { id: event.documentId, index: event.origin }
+ }
+ ],
+ SearchQuery: (event: SearchQueryEvent) => [
+ "search",
+ transformSearchQueryEvent(event)
+ ]
+};
-export default function AnalyticsPlugin(options = { client: undefined }) {
- const client: AnalyticsClient =
+export default function AnalyticsPlugin(
+ options: AnalyticsPluginOptions = { client: undefined }
+) {
+ const client: Tracker =
options.client ||
(typeof window !== "undefined" && window["elasticAnalytics"]);
if (!client) {
@@ -22,15 +109,14 @@ export default function AnalyticsPlugin(options = { client: undefined }) {
return {
subscribe: (event: Event) => {
- const eventTypeMap: Record = {
- AutocompleteSuggestionSelected: "click",
- FacetFilterRemoved: "click",
- FacetFilterSelected: "click",
- ResultSelected: "click",
- SearchQuery: "search"
- };
-
- client.trackEvent(eventTypeMap[event.type], event);
+ const [eventType, payload] =
+ mapEventToTrackerParams[event.type](event) || [];
+
+ if (!eventType || !payload) {
+ return;
+ }
+
+ client.trackEvent(eventType, payload);
}
};
}
diff --git a/packages/search-ui/src/SearchDriver.ts b/packages/search-ui/src/SearchDriver.ts
index 4c1287c60..34ca31220 100644
--- a/packages/search-ui/src/SearchDriver.ts
+++ b/packages/search-ui/src/SearchDriver.ts
@@ -451,7 +451,11 @@ class SearchDriver {
this.events.emit({
type: "SearchQuery",
+ filters: this.state.filters,
query: this.state.searchTerm,
+ sort: requestState.sort,
+ currentPage: requestState.current,
+ resultsPerPage: requestState.resultsPerPage,
totalResults: totalResults
});
diff --git a/packages/search-ui/src/__tests__/Events.test.ts b/packages/search-ui/src/__tests__/Events.test.ts
index 296aca822..670a0684a 100644
--- a/packages/search-ui/src/__tests__/Events.test.ts
+++ b/packages/search-ui/src/__tests__/Events.test.ts
@@ -139,7 +139,9 @@ describe("when an API connector and handler are both provided", () => {
origin: "autocomplete",
position: 1,
query: "test",
- tags: []
+ tags: [],
+ filters: [],
+ totalResults: 0
};
events.emit(event);
diff --git a/packages/search-ui/src/__tests__/SearchDriver.test.ts b/packages/search-ui/src/__tests__/SearchDriver.test.ts
index d2a228a3f..9fe129568 100644
--- a/packages/search-ui/src/__tests__/SearchDriver.test.ts
+++ b/packages/search-ui/src/__tests__/SearchDriver.test.ts
@@ -521,8 +521,12 @@ describe("_updateSearchResults", () => {
expect(stateAfterCreation.pagingStart).toEqual(21);
expect(stateAfterCreation.pagingEnd).toEqual(40);
expect(mockPlugin.subscribe).toBeCalledWith({
+ currentPage: 1,
query: "test",
+ resultsPerPage: 20,
+ sort: undefined,
totalResults: 1000,
+ filters: [],
type: "SearchQuery"
});
});
diff --git a/packages/search-ui/src/__tests__/actions/trackAutocompleteClickThrough.test.ts b/packages/search-ui/src/__tests__/actions/trackAutocompleteClickThrough.test.ts
index 246c2b9b3..27ff1c2c5 100644
--- a/packages/search-ui/src/__tests__/actions/trackAutocompleteClickThrough.test.ts
+++ b/packages/search-ui/src/__tests__/actions/trackAutocompleteClickThrough.test.ts
@@ -55,11 +55,16 @@ describe("#trackAutocompleteClickThrough", () => {
expect(requestId).toEqual("6789");
expect(result._meta.content_source_id).toEqual("621581b6174a804659f9dc16");
expect(mockPlugin.subscribe).toBeCalledWith({
+ currentPage: 1,
documentId: "park_great-smoky-mountains",
+ filters: [],
origin: "autocomplete",
position: 0,
query: "search terms",
+ resultsPerPage: 20,
+ sort: undefined,
tags: ["test"],
+ totalResults: 0,
type: "ResultSelected"
});
});
diff --git a/packages/search-ui/src/__tests__/actions/trackClickThrough.test.ts b/packages/search-ui/src/__tests__/actions/trackClickThrough.test.ts
index b2c5d8018..40a58ee9a 100644
--- a/packages/search-ui/src/__tests__/actions/trackClickThrough.test.ts
+++ b/packages/search-ui/src/__tests__/actions/trackClickThrough.test.ts
@@ -48,12 +48,17 @@ describe("#trackClickThrough", () => {
expect(page).toBe(1);
expect(mockPlugin.subscribe).toBeCalledWith({
+ currentPage: 1,
documentId: "park_great-smoky-mountains",
+ filters: [],
origin: "results",
position: 0,
query: "search terms",
+ resultsPerPage: 20,
+ sort: undefined,
tags: ["test"],
- type: "ResultSelected"
+ type: "ResultSelected",
+ totalResults: 1000
});
});
});
diff --git a/packages/search-ui/src/actions/trackAutocompleteClickThrough.ts b/packages/search-ui/src/actions/trackAutocompleteClickThrough.ts
index a911b7f1b..6ff14d1bf 100644
--- a/packages/search-ui/src/actions/trackAutocompleteClickThrough.ts
+++ b/packages/search-ui/src/actions/trackAutocompleteClickThrough.ts
@@ -22,8 +22,16 @@ export default function trackAutocompleteClickThrough(
);
}
- const { autocompletedResultsRequestId, searchTerm, autocompletedResults } =
- this.state;
+ const {
+ autocompletedResultsRequestId,
+ searchTerm,
+ autocompletedResults,
+ current,
+ resultsPerPage,
+ totalResults,
+ filters,
+ sort
+ } = this.state;
const resultIndex = autocompletedResults.findIndex(
(result) => result._meta.id === documentId
);
@@ -46,6 +54,11 @@ export default function trackAutocompleteClickThrough(
query: searchTerm,
position: resultIndex,
origin: "autocomplete",
- tags
+ tags,
+ totalResults,
+ filters,
+ sort,
+ currentPage: current,
+ resultsPerPage: resultsPerPage
});
}
diff --git a/packages/search-ui/src/actions/trackClickThrough.ts b/packages/search-ui/src/actions/trackClickThrough.ts
index fa1c332e8..c952832e4 100644
--- a/packages/search-ui/src/actions/trackClickThrough.ts
+++ b/packages/search-ui/src/actions/trackClickThrough.ts
@@ -1,4 +1,4 @@
-import { SearchState } from "..";
+import { Event, SearchResult, SearchState } from "..";
import Events from "../Events";
/**
@@ -23,7 +23,10 @@ export default function trackClickThrough(
searchTerm,
results,
current,
- resultsPerPage
+ resultsPerPage,
+ totalResults,
+ filters,
+ sort
}: SearchState = this.state;
const resultIndexOnPage = results.findIndex(
@@ -47,8 +50,13 @@ export default function trackClickThrough(
type: "ResultSelected",
documentId,
query: searchTerm,
- position: resultIndexOnPage,
origin: "results",
- tags
+ position: resultIndexOnPage,
+ tags,
+ totalResults,
+ filters,
+ sort,
+ currentPage: current,
+ resultsPerPage: resultsPerPage
});
}
diff --git a/packages/search-ui/src/types/index.ts b/packages/search-ui/src/types/index.ts
index ded003dea..76937c866 100644
--- a/packages/search-ui/src/types/index.ts
+++ b/packages/search-ui/src/types/index.ts
@@ -237,10 +237,14 @@ export interface BaseEvent {
tags?: string[];
}
-interface SearchQueryEvent extends BaseEvent {
+export interface SearchQueryEvent extends BaseEvent {
type: "SearchQuery";
+ filters: Filter[];
query: string;
totalResults: number;
+ sort?: SortOption[];
+ currentPage?: number;
+ resultsPerPage?: number;
}
interface AutocompleteSuggestionSelectedEvent extends BaseEvent {
@@ -250,9 +254,8 @@ interface AutocompleteSuggestionSelectedEvent extends BaseEvent {
position: number;
}
-interface ResultSelectedEvent extends BaseEvent {
+export interface ResultSelectedEvent extends Omit {
type: "ResultSelected";
- query: string;
documentId: string;
position: number;
origin: "autocomplete" | "results";
diff --git a/yarn.lock b/yarn.lock
index ed22f8f93..a6534c801 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1621,6 +1621,18 @@
dependencies:
object-hash "^1.3.0"
+"@elastic/behavioral-analytics-javascript-tracker@^2.1.3":
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/@elastic/behavioral-analytics-javascript-tracker/-/behavioral-analytics-javascript-tracker-2.1.3.tgz#53942a286c84ecc357dea0263f18882898a416e4"
+ integrity sha512-imProiNHyD04SLk8CqELv1E5quHnttoP7TWRzc94m0c8SxhU5vDn/5Af6JrRQ6QlGjc1+8ENckogcS1L9qGqaw==
+ dependencies:
+ "@elastic/behavioral-analytics-tracker-core" "2.0.5"
+
+"@elastic/behavioral-analytics-tracker-core@2.0.5", "@elastic/behavioral-analytics-tracker-core@^2.0.5":
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/@elastic/behavioral-analytics-tracker-core/-/behavioral-analytics-tracker-core-2.0.5.tgz#831dfa87b01ba8808cd843c60fd49f909b915ad2"
+ integrity sha512-SEtxpTnnWz/Etve2cdwMyVEv1C67lrVX38wv1hvg6L6Vu7bbSQdjs4b2KruswTA1CtJq8fngWSdACkIZn0scwQ==
+
"@elastic/datemath@^5.0.3":
version "5.0.3"
resolved "https://registry.yarnpkg.com/@elastic/datemath/-/datemath-5.0.3.tgz#7baccdab672b9a3ecb7fe8387580670936b58573"