chore(common): analytics on spotlight (#3727)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { TestContainer } from "dioc/testing"
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { Ref, computed, nextTick, ref, watch } from "vue"
|
||||
import { setPlatformDef } from "~/platform"
|
||||
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
|
||||
import {
|
||||
SpotlightSearcher,
|
||||
SpotlightSearcherSessionState,
|
||||
SpotlightSearcherResult,
|
||||
SpotlightSearcherSessionState,
|
||||
SpotlightService,
|
||||
} from "../"
|
||||
import { Ref, computed, nextTick, ref, watch } from "vue"
|
||||
import { TestContainer } from "dioc/testing"
|
||||
|
||||
const echoSearcher: SpotlightSearcher = {
|
||||
searcherID: "echo-searcher",
|
||||
@@ -78,6 +80,15 @@ const emptySearcher: SpotlightSearcher = {
|
||||
}
|
||||
|
||||
describe("SpotlightService", () => {
|
||||
beforeAll(() => {
|
||||
setPlatformDef({
|
||||
// @ts-expect-error We're mocking the platform
|
||||
analytics: {
|
||||
logEvent: vi.fn(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe("registerSearcher", () => {
|
||||
it("registers a searcher with a given ID", () => {
|
||||
const container = new TestContainer()
|
||||
@@ -387,16 +398,14 @@ describe("SpotlightService", () => {
|
||||
searcherID: "test-searcher",
|
||||
searcherSectionTitle: "Test Searcher",
|
||||
createSearchSession: (query) => {
|
||||
watch(query, notifiedFn, { immediate: true })
|
||||
const dispose = watch(query, notifiedFn, { immediate: true })
|
||||
|
||||
return [
|
||||
computed<SpotlightSearcherSessionState>(() => ({
|
||||
loading: false,
|
||||
results: [],
|
||||
})),
|
||||
() => {
|
||||
/* noop */
|
||||
},
|
||||
dispose,
|
||||
]
|
||||
},
|
||||
onResultSelect: () => {
|
||||
@@ -420,7 +429,7 @@ describe("SpotlightService", () => {
|
||||
query.value = "test3"
|
||||
await nextTick()
|
||||
|
||||
expect(notifiedFn).toHaveBeenCalledTimes(3)
|
||||
expect(notifiedFn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
describe("selectSearchResult", () => {
|
||||
@@ -547,4 +556,83 @@ describe("SpotlightService", () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getAnalyticsData", () => {
|
||||
const analyticsData: HoppSpotlightSessionEventData = {
|
||||
method: "click-spotlight-bar",
|
||||
inputLength: 0,
|
||||
action: "close",
|
||||
rank: null,
|
||||
searcherID: null,
|
||||
sessionDuration: "0.9s",
|
||||
}
|
||||
|
||||
it("returns the initial state of `analyticsData` in a spotlight session", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const spotlight = container.bind(SpotlightService)
|
||||
|
||||
expect(spotlight.getAnalyticsData()).toEqual({})
|
||||
})
|
||||
|
||||
it("returns the current state of `analyticsData` in a spotlight session", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const spotlight = container.bind(SpotlightService)
|
||||
spotlight.setAnalyticsData(analyticsData)
|
||||
|
||||
expect(spotlight.getAnalyticsData()).toEqual(analyticsData)
|
||||
})
|
||||
})
|
||||
|
||||
describe("setAnalyticsData", () => {
|
||||
const analyticsData: HoppSpotlightSessionEventData = {
|
||||
method: "click-spotlight-bar",
|
||||
inputLength: 0,
|
||||
action: "close",
|
||||
rank: null,
|
||||
searcherID: null,
|
||||
sessionDuration: "0.9s",
|
||||
}
|
||||
|
||||
it("sets analytics data for the current spotlight session by merging with existing data", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const spotlight = container.bind(SpotlightService)
|
||||
|
||||
// Session data, maintained outside and communicated to the service
|
||||
spotlight.setAnalyticsData({
|
||||
method: "click-spotlight-bar",
|
||||
action: "close",
|
||||
rank: null,
|
||||
searcherID: null,
|
||||
})
|
||||
|
||||
// Session duration and input length are computed at the service level
|
||||
const analyticsDataComputedInService: Partial<HoppSpotlightSessionEventData> =
|
||||
{
|
||||
inputLength: 0,
|
||||
sessionDuration: "0.9s",
|
||||
}
|
||||
spotlight.setAnalyticsData(analyticsDataComputedInService)
|
||||
|
||||
expect(spotlight.getAnalyticsData()).toEqual(analyticsData)
|
||||
})
|
||||
|
||||
it("resets analytics data after a spotlight session", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const spotlight = container.bind(SpotlightService)
|
||||
|
||||
// Populate `analyticsData` in the service context with sample data
|
||||
spotlight.setAnalyticsData(analyticsData)
|
||||
|
||||
const analyticsDataEmptyState = {}
|
||||
|
||||
// Resets the state with the supplied data by specifying `false` for the `merge` argument
|
||||
spotlight.setAnalyticsData(analyticsDataEmptyState, false)
|
||||
|
||||
expect(spotlight.getAnalyticsData()).toEqual(analyticsDataEmptyState)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { Service } from "dioc"
|
||||
import { watch, type Ref, ref, reactive, effectScope, Component } from "vue"
|
||||
import { Component, effectScope, reactive, ref, watch, type Ref } from "vue"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
|
||||
|
||||
/**
|
||||
* Defines how to render the entry text in a Spotlight Search Result
|
||||
@@ -115,6 +118,7 @@ export type SpotlightSearchState = {
|
||||
export class SpotlightService extends Service {
|
||||
public static readonly ID = "SPOTLIGHT_SERVICE"
|
||||
|
||||
private analyticsData: HoppSpotlightSessionEventData = {}
|
||||
private searchers: Map<string, SpotlightSearcher> = new Map()
|
||||
|
||||
/**
|
||||
@@ -140,6 +144,8 @@ export class SpotlightService extends Service {
|
||||
public createSearchSession(
|
||||
query: Ref<string>
|
||||
): [Ref<SpotlightSearchState>, () => void] {
|
||||
const startTime = Date.now()
|
||||
|
||||
const searchSessions = Array.from(this.searchers.values()).map(
|
||||
(x) => [x, ...x.createSearchSession(query)] as const
|
||||
)
|
||||
@@ -183,6 +189,16 @@ export class SpotlightService extends Service {
|
||||
onSessionEndList.push(onSessionEnd)
|
||||
}
|
||||
|
||||
watch(
|
||||
query,
|
||||
(newQuery) => {
|
||||
this.setAnalyticsData({
|
||||
inputLength: newQuery.length,
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
loadingSearchers,
|
||||
(set) => {
|
||||
@@ -198,6 +214,18 @@ export class SpotlightService extends Service {
|
||||
for (const onEnd of onSessionEndList) {
|
||||
onEnd()
|
||||
}
|
||||
|
||||
// Sets the session duration in the state for analytics event logging
|
||||
const sessionDuration = `${((Date.now() - startTime) / 1000).toFixed(2)}s`
|
||||
this.setAnalyticsData({ sessionDuration })
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SPOTLIGHT_SESSION",
|
||||
...this.analyticsData,
|
||||
})
|
||||
|
||||
// Reset the state
|
||||
this.setAnalyticsData({}, false)
|
||||
}
|
||||
|
||||
return [resultObj, onSearchEnd]
|
||||
@@ -206,12 +234,35 @@ export class SpotlightService extends Service {
|
||||
/**
|
||||
* Selects a search result. To be called when the user selects a result
|
||||
* @param searcherID The ID of the searcher that the result belongs to
|
||||
* @param result The resuklt to look at
|
||||
* @param result The result to look at
|
||||
*/
|
||||
public selectSearchResult(
|
||||
searcherID: string,
|
||||
result: SpotlightSearcherResult
|
||||
) {
|
||||
this.searchers.get(searcherID)?.onResultSelect(result)
|
||||
|
||||
// Sets the action indicating `success` and selected result score in the state for analytics event logging
|
||||
this.setAnalyticsData({
|
||||
action: "success",
|
||||
rank: result.score.toFixed(2),
|
||||
searcherID,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the analytics data for the current search session
|
||||
*/
|
||||
public getAnalyticsData(): HoppSpotlightSessionEventData {
|
||||
return this.analyticsData
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets Analytics data for the current search session
|
||||
* @param data The data to set
|
||||
* @param merge Whether to merge the data with the existing data or replace it
|
||||
*/
|
||||
public setAnalyticsData(data: HoppSpotlightSessionEventData, merge = true) {
|
||||
this.analyticsData = merge ? { ...this.analyticsData, ...data } : data
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user