Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add APPLY_FILTER analytics event #2423

Merged
merged 2 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions frontend/src/components/VFilters/VSearchGridFilter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{{ $t("filterList.clear") }}
</VButton>
</header>
<form ref="filtersFormRef" class="filters-form">
<form class="filters-form">
<VFilterChecklist
v-for="filterType in filterTypes"
:key="filterType"
Expand All @@ -32,7 +32,8 @@
</template>

<script lang="ts">
import { computed, defineComponent, ref } from "vue"
import { computed, defineComponent } from "vue"
import { storeToRefs } from "pinia"

import { useRouter } from "@nuxtjs/composition-api"

Expand All @@ -43,6 +44,7 @@ import { areQueriesEqual, ApiQueryParams } from "~/utils/search-query-transform"
import type { NonMatureFilterCategory } from "~/constants/filters"
import { defineEvent } from "~/types/emits"
import { useI18n } from "~/composables/use-i18n"
import { useAnalytics } from "~/composables/use-analytics"

import VFilterChecklist from "~/components/VFilters/VFilterChecklist.vue"
import VButton from "~/components/VButton.vue"
Expand Down Expand Up @@ -81,14 +83,20 @@ export default defineComponent({
const i18n = useI18n()
const router = useRouter()

const filtersFormRef = ref<HTMLFormElement | null>(null)
const { sendCustomEvent } = useAnalytics()

const {
isAnyFilterApplied,
searchQueryParams,
searchTerm,
searchType,
searchFilters: filters,
} = storeToRefs(searchStore)

const isAnyFilterApplied = computed(() => searchStore.isAnyFilterApplied)
const filters = computed(() => searchStore.searchFilters)
const filterTypes = computed(
() => Object.keys(filters.value) as NonMatureFilterCategory[]
)
const filterTypeTitle = (filterType: string) => {
const filterTypeTitle = (filterType: NonMatureFilterCategory) => {
return i18n.t(`filters.${filterType}.title`).toString()
}

Expand All @@ -97,7 +105,7 @@ export default defineComponent({
* when the queries change.
*/
watchDebounced(
() => searchStore.searchQueryParams,
searchQueryParams,
(newQuery: ApiQueryParams, oldQuery: ApiQueryParams) => {
if (!areQueriesEqual(newQuery, oldQuery)) {
router.push(searchStore.getSearchPath())
Expand All @@ -106,14 +114,30 @@ export default defineComponent({
{ debounce: 800, maxWait: 5000 }
)

const toggleFilter = ({
filterType,
code,
}: {
filterType: NonMatureFilterCategory
code: string
}) => {
const checked = searchStore.toggleFilter({ filterType, code })
sendCustomEvent("APPLY_FILTER", {
category: filterType,
key: code,
checked,
searchType: searchType.value,
query: searchTerm.value,
})
}

return {
filtersFormRef,
isAnyFilterApplied,
filters,
filterTypes,
filterTypeTitle,
clearFilters: searchStore.clearFilters,
toggleFilter: searchStore.toggleFilter,
toggleFilter,
}
},
})
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/stores/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ export const useSearchStore = defineStore("search", {
},
/**
* Toggles a filter's checked parameter. Requires either codeIdx or code.
* Returns the new checked value.
*/
toggleFilter({
filterType,
Expand All @@ -342,7 +343,7 @@ export const useSearchStore = defineStore("search", {
filterType: FilterCategory
codeIdx?: number
code?: string
}) {
}): boolean {
if (typeof codeIdx === "undefined" && typeof code === "undefined") {
throw new Error(
`Cannot toggle filter of type ${filterType}. Use code or codeIdx parameter`
Expand All @@ -351,6 +352,7 @@ export const useSearchStore = defineStore("search", {
const filterItems = this.filters[filterType]
const idx = codeIdx ?? filterItems.findIndex((f) => f.code === code)
this.filters[filterType][idx].checked = !filterItems[idx].checked
return this.filters[filterType][idx].checked
},

/**
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/types/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { MediaType, SearchType } from "~/constants/media"
import type { ReportReason } from "~/constants/content-report"
import type { NonMatureFilterCategory } from "~/constants/filters"

/**
* Compound type of all custom events sent from the site; Index with `EventName`
Expand Down Expand Up @@ -190,6 +191,26 @@ export type Events = {
/** The search term */
query: string
}
/**
* Description: Whenever the user sets a filter. Filter category and key are the values used in code, not the user-facing filter labels.
* Questions:
* - Do most users filter their searches?
* - What % of users use filtering?
* - Which filters are most popular? Least popular?
* - Are any filters so commonly applied they should become defaults?
*/
APPLY_FILTER: {
/** The filter category, e.g. `license` */
category: NonMatureFilterCategory
/** The filter key, e.g. `by` */
key: string
/** Whether the filter is checked or unchecked */
checked: boolean
/** The media type being searched, can include All content */
searchType: SearchType
/** The search term */
query: string
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,26 @@ import { render } from "~~/test/unit/test-utils/render"

import { useSearchStore } from "~/stores/search"

import { useAnalytics } from "~/composables/use-analytics"
import { ALL_MEDIA } from "~/constants/media"

import VSearchGridFilter from "~/components/VFilters/VSearchGridFilter.vue"

jest.mock("~/composables/use-analytics")
describe("VSearchGridFilter", () => {
let options = {}
let searchStore
let configureStoreCb
const routerMock = { push: jest.fn() }
const routeMock = { path: jest.fn() }
const sendCustomEventMock = jest.fn()
useAnalytics.mockImplementation(() => ({
sendCustomEvent: sendCustomEventMock,
}))

beforeEach(() => {
sendCustomEventMock.mockClear()

options = {
mocks: {
$route: routeMock,
Expand All @@ -36,6 +46,25 @@ describe("VSearchGridFilter", () => {
screen.getByRole("checkbox", { checked: true, name: /use commercially/i })
})

it("sends APPLY_FILTER event when filter is toggled", async () => {
configureStoreCb = (localVue, options) => {
searchStore = useSearchStore(options.pinia)
searchStore.setSearchTerm("cat")
}
render(VSearchGridFilter, options, configureStoreCb)

await fireEvent.click(
screen.queryByRole("checkbox", { name: /use commercially/i })
)
expect(sendCustomEventMock).toHaveBeenCalledWith("APPLY_FILTER", {
category: "licenseTypes",
key: "commercial",
checked: true,
query: "cat",
searchType: ALL_MEDIA,
})
})

it("clears filters", async () => {
configureStoreCb = (localVue, options) => {
searchStore = useSearchStore(options.pinia)
Expand Down