Skip to content

Commit

Permalink
Merge pull request #25 from Nosto/CFE-301-utilize-new-is-keyword-flag…
Browse files Browse the repository at this point in the history
…-in-search-analytics-call

feat: Implement isKeyword support for analytics
  • Loading branch information
rokbar-nosto committed Apr 25, 2024
2 parents e2196d4 + aca61f6 commit 1016f48
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 20 deletions.
75 changes: 73 additions & 2 deletions spec/suites/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
NostoClient,
autocomplete,
} from "../../src/entries/base"
import { getDefaultConfig } from "../../src/config"

interface WindowWithNostoJS extends Window {
nostojs: jest.Mock<
Expand All @@ -29,7 +30,7 @@ interface WindowWithNostoJS extends Window {

export const handleAutocomplete = async (
render: AutocompleteConfig<DefaultState>["render"],
submit: AutocompleteConfig<DefaultState>["submit"] = () => ({})
submit?: AutocompleteConfig<DefaultState>["submit"]
) => {
autocomplete({
fetch: {
Expand All @@ -56,7 +57,7 @@ export const handleAutocomplete = async (
inputSelector: "#search",
dropdownSelector: "#search-results",
render,
submit,
submit: submit ?? getDefaultConfig<DefaultState>().submit,
})
}

Expand Down Expand Up @@ -349,6 +350,23 @@ export function autocompleteSuite({
)
})

it("should call search with isKeyword=false", async () => {
const user = userEvent.setup()

await waitFor(() => handleAutocomplete(render()))

await user.type(screen.getByTestId("input"), "black")
await user.click(screen.getByTestId("search-button"))

await waitFor(() =>
expect(searchSpy).toHaveBeenCalledWith(expect.anything(), {
track: "serp",
redirect: true,
isKeyword: false,
})
)
})

it("should record search submit with keyboard", async () => {
const user = userEvent.setup()

Expand All @@ -361,6 +379,22 @@ export function autocompleteSuite({
})
})

it("should call search with keyboard with isKeyword=false", async () => {
const user = userEvent.setup()

await waitFor(() => handleAutocomplete(render()))
await user.type(screen.getByTestId("input"), "black")

await waitFor(async () => {
await user.keyboard("{enter}")
expect(searchSpy).toHaveBeenCalledWith(expect.anything(), {
track: "serp",
redirect: true,
isKeyword: false,
})
})
})

it("should record search click on keyword click", async () => {
const user = userEvent.setup()

Expand All @@ -376,6 +410,23 @@ export function autocompleteSuite({
})
})

it("should call search on keyword click with isKeyword=true", async () => {
const user = userEvent.setup()

await waitFor(() => handleAutocomplete(render()))
await user.type(screen.getByTestId("input"), "black")

await waitFor(async () => {
await user.click(screen.getAllByTestId("keyword")?.[0])

expect(searchSpy).toHaveBeenCalledWith(expect.anything(), {
track: "serp",
redirect: false,
isKeyword: true,
})
})
})

it("should record search click on product click", async () => {
const user = userEvent.setup()

Expand Down Expand Up @@ -448,5 +499,25 @@ export function autocompleteSuite({

assignMock.mockClear()
})

it("should call search when keyword is submitted with keyboard, with isKeyword=true", async () => {
const user = userEvent.setup()

await waitFor(() => handleAutocomplete(render()))

await user.type(screen.getByTestId("input"), "black")

await waitFor(async () => {
await user.keyboard("{arrowdown}")
await user.keyboard("{arrowdown}")
await user.keyboard("{enter}")

expect(searchSpy).toHaveBeenCalledWith(expect.anything(), {
track: "serp",
redirect: false,
isKeyword: true,
})
})
})
})
}
2 changes: 1 addition & 1 deletion src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export function log(
(async () => {
const api = await getNostoClient()
api.captureError(error, "nostoAutocomplete", level)
})()
})()
}
console[level](...[msg, error].filter(Boolean))
}
Expand Down
8 changes: 8 additions & 0 deletions src/api/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export interface InputSearchQueryWithFields extends InputSearchQuery {
} & InputSearchQuery["keywords"]
}

/**
* @group Nosto Client
* @category Core
*/
export interface SearchOptions {
/**
* Enabled Nosto tracking. The source of search request must be provided, e.g. if request made from autocomplete, track should be set to "autocomplete".
Expand All @@ -28,6 +32,10 @@ export interface SearchOptions {
* Automatically handle redirect when received from search.
*/
redirect?: boolean
/**
* Marks that search was done by clicking a keyword
*/
isKeyword?: boolean
}

export * from "./generated"
8 changes: 6 additions & 2 deletions src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { LimiterError, createLimiter } from "./utils/limiter"
import { CancellableError } from "./utils/promise"
import { getGaTrackUrl, isGaEnabled, trackGaPageView } from "./utils/ga"
import { createHistory } from "./utils/history"
import { SearchOptions } from "./api/search"

export type AutocompleteInstance = {
/**
Expand All @@ -24,6 +25,8 @@ export type AutocompleteInstance = {
destroy(): void
}

export type SearchAutocompleteOptions = Pick<SearchOptions, "isKeyword" | "redirect">

/**
* @param config Autocomplete configuration.
* @returns Autocomplete instance.
Expand Down Expand Up @@ -338,8 +341,9 @@ function submitWithContext<State>(context: {
config: AutocompleteConfig<State>
actions: StateActions<State>
}) {
return async (value: string, redirect: boolean = false) => {
return async (value: string, options?: SearchAutocompleteOptions) => {
const { config, actions } = context
const { redirect = false } = options ?? {}

if (value.length > 0) {
if (config.historyEnabled) {
Expand All @@ -359,7 +363,7 @@ function submitWithContext<State>(context: {
}

if (!redirect && typeof config?.submit === "function") {
config.submit(value, config)
config.submit(value, config, options)
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InputSearchQueryWithFields } from "./api/search"
import { SearchAutocompleteOptions } from "./autocomplete"
import { search } from "./search"

/**
Expand Down Expand Up @@ -55,7 +56,11 @@ export interface AutocompleteConfig<State> {
/**
* The function to use to submit the search
*/
submit?: (query: string, config: AutocompleteConfig<State>) => void
submit?: (
query: string,
config: AutocompleteConfig<State>,
options?: SearchAutocompleteOptions
) => void
/**
* Enable history
*/
Expand Down Expand Up @@ -87,7 +92,7 @@ export function getDefaultConfig<State>() {
historySize: 5,
nostoAnalytics: true,
googleAnalytics: defaultGaConfig,
submit: (query, config) => {
submit: (query, config, options) => {
if (
query.length >=
(config.minQueryLength ??
Expand All @@ -100,6 +105,7 @@ export function getDefaultConfig<State>() {
{
redirect: true,
track: config.nostoAnalytics ? "serp" : undefined,
...options,
}
)
}
Expand Down
10 changes: 6 additions & 4 deletions src/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,27 @@ export async function search(
query: InputSearchQueryWithFields,
options?: SearchOptions
) {
const { redirect, track } = options ?? { redirect: false, track: undefined }
const { redirect = false, track, isKeyword = false } = options ?? {}

const fields = query.products?.fields ?? defaultProductFields
const facets = query.products?.facets ?? ["*"]
const size = query.products?.size ?? 20
const from = query.products?.from ?? 0

const api = await getNostoClient()
const response = await api.search({
const response = await api.search(
{
...query,
products: {
...query.products,
fields,
facets,
size,
from,
}
},
},
{ redirect, track })
{ redirect, track, isKeyword }
)

return { query, response }
}
10 changes: 7 additions & 3 deletions src/utils/dropdown.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { log } from "../api/client"
import { SearchAutocompleteOptions } from "../autocomplete"

type OnClickBindings<State> = {
[key: string]: (obj: {
Expand All @@ -12,7 +13,10 @@ export function createDropdown<State>(
container: HTMLElement,
initialState: PromiseLike<State>,
render: (container: HTMLElement, state: State) => void | PromiseLike<void>,
submit: (inputValue: string, redirect?: boolean) => unknown,
submit: (
inputValue: string,
options?: SearchAutocompleteOptions
) => unknown,
updateInput: (inputValue: string) => void,
onClickBindings?: OnClickBindings<State>
) {
Expand All @@ -35,7 +39,7 @@ export function createDropdown<State>(
}

if (parsedHit?.keyword) {
submit(parsedHit.keyword, !!parsedHit?._redirect)
submit(parsedHit.keyword, { redirect: !!parsedHit?._redirect, isKeyword: true })

if (parsedHit?._redirect) {
location.href = parsedHit._redirect
Expand Down Expand Up @@ -230,7 +234,7 @@ export function createDropdown<State>(
async function init() {
const state = await Promise.resolve(initialState)
await Promise.resolve(render(container, state))

// Without setTimeout React does not have committed DOM changes yet, so we don't have the correct elements.
setTimeout(() => {
loadElements()
Expand Down
18 changes: 12 additions & 6 deletions src/utils/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AutocompleteConfig } from "../config"
import { History } from "./history"
import { Cancellable, makeCancellable } from "./promise"
import { search } from "../search"
import { SearchAutocompleteOptions } from "../autocomplete"

/**
* @group Autocomplete
Expand Down Expand Up @@ -41,7 +42,11 @@ export const getStateActions = <State>({
}): StateActions<State> => {
let cancellable: Cancellable<State> | undefined

const fetchState = (value: string, config: AutocompleteConfig<State>): PromiseLike<State> => {
const fetchState = (
value: string,
config: AutocompleteConfig<State>,
options?: SearchAutocompleteOptions
): PromiseLike<State> => {
if (typeof config.fetch === "function") {
return config.fetch(value)
} else {
Expand All @@ -54,27 +59,28 @@ export const getStateActions = <State>({
{
track: config.nostoAnalytics ? "autocomplete" : undefined,
redirect: false,
...options,
}
)
}
}

function getHistoryState(query: string): PromiseLike<State> {
// @ts-expect-error type mismatch
// @ts-expect-error type mismatch
return Promise.resolve({
query: {
query,
},
history: history?.getItems()
history: history?.getItems(),
})
}

return {
updateState: (inputValue?: string): PromiseLike<State> => {
updateState: (inputValue?: string, options?: SearchAutocompleteOptions): PromiseLike<State> => {
cancellable?.cancel()

if (inputValue && inputValue.length >= config.minQueryLength) {
cancellable = makeCancellable(fetchState(inputValue, config))
cancellable = makeCancellable(fetchState(inputValue, config, options))
return cancellable.promise
} else if (history) {
return getHistoryState(inputValue ?? "")
Expand Down

0 comments on commit 1016f48

Please sign in to comment.