chore(common): analytics on spotlight (#3727)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
This commit is contained in:
@@ -23,7 +23,7 @@
|
|||||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||||
<button
|
<button
|
||||||
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle', undefined, 'mouseclick')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex flex-1 items-center">
|
<span class="inline-flex flex-1 items-center">
|
||||||
<icon-lucide-search class="svg-icons mr-2" />
|
<icon-lucide-search class="svg-icons mr-2" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-if="show"
|
v-if="show"
|
||||||
styles="sm:max-w-lg"
|
styles="sm:max-w-lg"
|
||||||
full-width
|
full-width
|
||||||
@close="emit('hide-modal')"
|
@close="closeSpotlightModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col border-b border-divider transition">
|
<div class="flex flex-col border-b border-divider transition">
|
||||||
@@ -86,35 +86,36 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue"
|
|
||||||
import { useService } from "dioc/vue"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { isEqual } from "lodash-es"
|
||||||
|
import { computed, ref, watch } from "vue"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
|
||||||
import {
|
import {
|
||||||
SpotlightService,
|
|
||||||
SpotlightSearchState,
|
SpotlightSearchState,
|
||||||
SpotlightSearcherResult,
|
SpotlightSearcherResult,
|
||||||
|
SpotlightService,
|
||||||
} from "~/services/spotlight"
|
} from "~/services/spotlight"
|
||||||
import { isEqual } from "lodash-es"
|
|
||||||
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
|
||||||
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
|
||||||
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
|
||||||
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
|
||||||
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
import { CollectionsSpotlightSearcherService } from "~/services/spotlight/searchers/collections.searcher"
|
||||||
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
|
||||||
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
|
||||||
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
|
||||||
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
|
||||||
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
|
||||||
import {
|
import {
|
||||||
EnvironmentsSpotlightSearcherService,
|
EnvironmentsSpotlightSearcherService,
|
||||||
SwitchEnvSpotlightSearcherService,
|
SwitchEnvSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/environment.searcher"
|
} from "~/services/spotlight/searchers/environment.searcher"
|
||||||
|
import { GeneralSpotlightSearcherService } from "~/services/spotlight/searchers/general.searcher"
|
||||||
|
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
|
||||||
|
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
||||||
|
import { MiscellaneousSpotlightSearcherService } from "~/services/spotlight/searchers/miscellaneous.searcher"
|
||||||
|
import { NavigationSpotlightSearcherService } from "~/services/spotlight/searchers/navigation.searcher"
|
||||||
|
import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/request.searcher"
|
||||||
|
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
|
||||||
|
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
|
||||||
|
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
|
||||||
|
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
|
||||||
import {
|
import {
|
||||||
SwitchWorkspaceSpotlightSearcherService,
|
SwitchWorkspaceSpotlightSearcherService,
|
||||||
WorkspaceSpotlightSearcherService,
|
WorkspaceSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -290,4 +291,17 @@ function newUseArrowKeysForNavigation() {
|
|||||||
|
|
||||||
return { selectedEntry }
|
return { selectedEntry }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeSpotlightModal() {
|
||||||
|
const analyticsData: HoppSpotlightSessionEventData = {
|
||||||
|
action: "close",
|
||||||
|
searcherID: null,
|
||||||
|
rank: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the action indicating `close` and rank as `null` in the state for analytics event logging
|
||||||
|
spotlightService.setAnalyticsData(analyticsData)
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -331,7 +331,8 @@ const deleteHistory = (entry: HistoryEntry) => {
|
|||||||
const addToCollection = (entry: HistoryEntry) => {
|
const addToCollection = (entry: HistoryEntry) => {
|
||||||
if (props.page === "rest") {
|
if (props.page === "rest") {
|
||||||
invokeAction("request.save-as", {
|
invokeAction("request.save-as", {
|
||||||
request: entry.request,
|
requestType: "rest",
|
||||||
|
request: entry.request as HoppRESTRequest,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ import IconShare2 from "~icons/lucide/share-2"
|
|||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InspectionService } from "~/services/inspection"
|
import { InspectionService } from "~/services/inspection"
|
||||||
import { InterceptorService } from "~/services/interceptor.service"
|
import { InterceptorService } from "~/services/interceptor.service"
|
||||||
@@ -577,25 +577,12 @@ defineActionHandler("request.share-request", shareRequest)
|
|||||||
defineActionHandler("request.method.next", cycleDownMethod)
|
defineActionHandler("request.method.next", cycleDownMethod)
|
||||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||||
defineActionHandler("request.save", saveRequest)
|
defineActionHandler("request.save", saveRequest)
|
||||||
defineActionHandler(
|
defineActionHandler("request.save-as", (req) => {
|
||||||
"request.save-as",
|
showSaveRequestModal.value = true
|
||||||
(
|
if (req?.requestType === "rest") {
|
||||||
req:
|
request.value = req.request
|
||||||
| {
|
|
||||||
requestType: "rest"
|
|
||||||
request: HoppRESTRequest
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
requestType: "gql"
|
|
||||||
request: HoppGQLRequest
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
showSaveRequestModal.value = true
|
|
||||||
if (req && req.requestType === "rest") {
|
|
||||||
request.value = req.request
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
defineActionHandler("request.method.get", () => updateMethod("GET"))
|
||||||
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
defineActionHandler("request.method.post", () => updateMethod("POST"))
|
||||||
defineActionHandler("request.method.put", () => updateMethod("PUT"))
|
defineActionHandler("request.method.put", () => updateMethod("PUT"))
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ export type HoppAction =
|
|||||||
| "user.login" // Login to Hoppscotch
|
| "user.login" // Login to Hoppscotch
|
||||||
| "user.logout" // Log out of Hoppscotch
|
| "user.logout" // Log out of Hoppscotch
|
||||||
| "editor.format" // Format editor content
|
| "editor.format" // Format editor content
|
||||||
|
| "modals.team.delete" // Delete team
|
||||||
|
| "workspace.switch" // Switch workspace
|
||||||
|
| "rest.request.open" // Open REST request
|
||||||
|
| "request.open-tab" // Open REST request
|
||||||
|
| "share.request" // Share REST request
|
||||||
|
| "tab.duplicate-tab" // Duplicate REST request
|
||||||
|
| "gql.request.open" // Open GraphQL request
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the arguments, if present for a given type that is required to be passed on
|
* Defines the arguments, if present for a given type that is required to be passed on
|
||||||
@@ -112,6 +119,7 @@ type HoppActionArgsMap = {
|
|||||||
requestType: "gql"
|
requestType: "gql"
|
||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
}
|
}
|
||||||
|
| undefined
|
||||||
"request.open-tab": {
|
"request.open-tab": {
|
||||||
tab: RESTOptionTabs | GQLOptionTabs
|
tab: RESTOptionTabs | GQLOptionTabs
|
||||||
}
|
}
|
||||||
@@ -121,7 +129,6 @@ type HoppActionArgsMap = {
|
|||||||
"tab.duplicate-tab": {
|
"tab.duplicate-tab": {
|
||||||
tabID?: string
|
tabID?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
"gql.request.open": {
|
"gql.request.open": {
|
||||||
request: HoppGQLRequest
|
request: HoppGQLRequest
|
||||||
saveContext?: HoppGQLSaveContext
|
saveContext?: HoppGQLSaveContext
|
||||||
@@ -132,11 +139,23 @@ type HoppActionArgsMap = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KeysWithValueUndefined<T> = {
|
||||||
|
[K in keyof T]: undefined extends T[K] ? K : never
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HoppActions which require arguments for their invocation
|
* HoppActions which require arguments for their invocation
|
||||||
*/
|
*/
|
||||||
export type HoppActionWithArgs = keyof HoppActionArgsMap
|
export type HoppActionWithArgs = keyof HoppActionArgsMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HoppActions which optionally takes in arguments for their invocation
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type HoppActionWithOptionalArgs =
|
||||||
|
| HoppActionWithNoArgs
|
||||||
|
| KeysWithValueUndefined<HoppActionArgsMap>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HoppActions which do not require arguments for their invocation
|
* HoppActions which do not require arguments for their invocation
|
||||||
*/
|
*/
|
||||||
@@ -145,27 +164,26 @@ export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
|||||||
/**
|
/**
|
||||||
* Resolves the argument type for a given HoppAction
|
* Resolves the argument type for a given HoppAction
|
||||||
*/
|
*/
|
||||||
type ArgOfHoppAction<A extends HoppAction | HoppActionWithArgs> =
|
type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
|
||||||
A extends HoppActionWithArgs ? HoppActionArgsMap[A] : undefined
|
? HoppActionArgsMap[A]
|
||||||
|
: undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the action function for a given HoppAction, used by action handler function defs
|
* Resolves the action function for a given HoppAction, used by action handler function defs
|
||||||
*/
|
*/
|
||||||
type ActionFunc<A extends HoppAction | HoppActionWithArgs> =
|
type ActionFunc<A extends HoppAction> = A extends HoppActionWithArgs
|
||||||
A extends HoppActionWithArgs ? (arg: ArgOfHoppAction<A>) => void : () => void
|
? (arg: ArgOfHoppAction<A>, trigger?: InvocationTriggers) => void
|
||||||
|
: (_?: undefined, trigger?: InvocationTriggers) => void
|
||||||
|
|
||||||
type BoundActionList = {
|
type BoundActionList = {
|
||||||
// eslint-disable-next-line no-unused-vars
|
[A in HoppAction]?: Array<ActionFunc<A>>
|
||||||
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundActions: BoundActionList = reactive({})
|
const boundActions: BoundActionList = reactive({})
|
||||||
|
|
||||||
export const activeActions$ = new BehaviorSubject<
|
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||||
(HoppAction | HoppActionWithArgs)[]
|
|
||||||
>([])
|
|
||||||
|
|
||||||
export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
export function bindAction<A extends HoppAction>(
|
||||||
action: A,
|
action: A,
|
||||||
handler: ActionFunc<A>
|
handler: ActionFunc<A>
|
||||||
) {
|
) {
|
||||||
@@ -179,27 +197,33 @@ export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
|||||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InvocationTriggers = "keypress" | "mouseclick"
|
||||||
|
|
||||||
type InvokeActionFunc = {
|
type InvokeActionFunc = {
|
||||||
(action: HoppActionWithNoArgs, args?: undefined): void
|
(
|
||||||
|
action: HoppActionWithOptionalArgs,
|
||||||
|
args?: undefined,
|
||||||
|
trigger?: InvocationTriggers
|
||||||
|
): void
|
||||||
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
|
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invokes a action, triggering action handlers if any registered.
|
* Invokes an action, triggering action handlers if any registered.
|
||||||
* The second argument parameter is optional if your action has no args required
|
* The second and third arguments are optional
|
||||||
* @param action The action to fire
|
* @param action The action to fire
|
||||||
* @param args The argument passed to the action handler. Optional if action has no args required
|
* @param args The argument passed to the action handler. Optional if action has no args required
|
||||||
|
* @param trigger Optionally supply the trigger that invoked the action (keypress/mouseclick)
|
||||||
*/
|
*/
|
||||||
export const invokeAction: InvokeActionFunc = <
|
export const invokeAction: InvokeActionFunc = <A extends HoppAction>(
|
||||||
A extends HoppAction | HoppActionWithArgs,
|
|
||||||
>(
|
|
||||||
action: A,
|
action: A,
|
||||||
args: ArgOfHoppAction<A>
|
args?: ArgOfHoppAction<A>,
|
||||||
|
trigger?: InvocationTriggers
|
||||||
) => {
|
) => {
|
||||||
boundActions[action]?.forEach((handler) => handler(args! as any))
|
boundActions[action]?.forEach((handler) => handler(args! as any, trigger))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unbindAction<A extends HoppAction | HoppActionWithArgs>(
|
export function unbindAction<A extends HoppAction>(
|
||||||
action: A,
|
action: A,
|
||||||
handler: ActionFunc<A>
|
handler: ActionFunc<A>
|
||||||
) {
|
) {
|
||||||
@@ -232,7 +256,7 @@ export function isActionBound(action: HoppAction): Ref<boolean> {
|
|||||||
* @param handler The function to be called when the action is invoked
|
* @param handler The function to be called when the action is invoked
|
||||||
* @param isActive A ref that indicates whether the action is active
|
* @param isActive A ref that indicates whether the action is active
|
||||||
*/
|
*/
|
||||||
export function defineActionHandler<A extends HoppAction | HoppActionWithArgs>(
|
export function defineActionHandler<A extends HoppAction>(
|
||||||
action: A,
|
action: A,
|
||||||
handler: ActionFunc<A>,
|
handler: ActionFunc<A>,
|
||||||
isActive: Ref<boolean> | undefined = undefined
|
isActive: Ref<boolean> | undefined = undefined
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { onBeforeUnmount, onMounted } from "vue"
|
import { onBeforeUnmount, onMounted } from "vue"
|
||||||
import { HoppActionWithNoArgs, invokeAction } from "./actions"
|
import { HoppActionWithOptionalArgs, invokeAction } from "./actions"
|
||||||
import { isAppleDevice } from "./platformutils"
|
import { isAppleDevice } from "./platformutils"
|
||||||
import { isDOMElement, isTypableElement } from "./utils/dom"
|
import { isDOMElement, isTypableElement } from "./utils/dom"
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ type SingleCharacterShortcutKey = `${Key}`
|
|||||||
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
|
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
|
||||||
|
|
||||||
export const bindings: {
|
export const bindings: {
|
||||||
[_ in ShortcutKey]?: HoppActionWithNoArgs
|
[_ in ShortcutKey]?: HoppActionWithOptionalArgs
|
||||||
} = {
|
} = {
|
||||||
"ctrl-enter": "request.send-cancel",
|
"ctrl-enter": "request.send-cancel",
|
||||||
"ctrl-i": "request.reset",
|
"ctrl-i": "request.reset",
|
||||||
@@ -96,7 +96,7 @@ function handleKeyDown(ev: KeyboardEvent) {
|
|||||||
if (!boundAction) return
|
if (!boundAction) return
|
||||||
|
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
invokeAction(boundAction)
|
invokeAction(boundAction, undefined, "keypress")
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||||
|
|||||||
@@ -69,13 +69,15 @@ import "splitpanes/dist/splitpanes.css"
|
|||||||
import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
|
import { computed, onBeforeMount, onMounted, ref, watch } from "vue"
|
||||||
import { RouterView, useRouter } from "vue-router"
|
import { RouterView, useRouter } from "vue-router"
|
||||||
|
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { InvocationTriggers, defineActionHandler } from "~/helpers/actions"
|
||||||
import { hookKeybindingsListener } from "~/helpers/keybindings"
|
import { hookKeybindingsListener } from "~/helpers/keybindings"
|
||||||
import { applySetting } from "~/newstore/settings"
|
import { applySetting } from "~/newstore/settings"
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
|
import { HoppSpotlightSessionEventData } from "~/platform/analytics"
|
||||||
import { PersistenceService } from "~/services/persistence"
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
import { SpotlightService } from "~/services/spotlight"
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
@@ -93,6 +95,7 @@ const toast = useToast()
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const persistenceService = useService(PersistenceService)
|
const persistenceService = useService(PersistenceService)
|
||||||
|
const spotlightService = useService(SpotlightService)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
if (!mdAndLarger.value) {
|
if (!mdAndLarger.value) {
|
||||||
@@ -144,7 +147,18 @@ const spacerClass = computed(() =>
|
|||||||
expandNavigation.value ? "spacer-small" : "spacer-expand"
|
expandNavigation.value ? "spacer-small" : "spacer-expand"
|
||||||
)
|
)
|
||||||
|
|
||||||
defineActionHandler("modals.search.toggle", () => {
|
defineActionHandler("modals.search.toggle", (_, trigger) => {
|
||||||
|
const triggerMethodMap: Record<
|
||||||
|
InvocationTriggers,
|
||||||
|
HoppSpotlightSessionEventData["method"]
|
||||||
|
> = {
|
||||||
|
keypress: "keyboard-shortcut",
|
||||||
|
mouseclick: "click-spotlight-bar",
|
||||||
|
}
|
||||||
|
spotlightService.setAnalyticsData({
|
||||||
|
method: triggerMethodMap[trigger as InvocationTriggers],
|
||||||
|
})
|
||||||
|
|
||||||
showSearch.value = !showSearch.value
|
showSearch.value = !showSearch.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ export type HoppRequestEvent =
|
|||||||
}
|
}
|
||||||
| { platform: "wss" | "sse" | "socketio" | "mqtt" }
|
| { platform: "wss" | "sse" | "socketio" | "mqtt" }
|
||||||
|
|
||||||
|
export type HoppSpotlightSessionEventData = {
|
||||||
|
action?: "success" | "close"
|
||||||
|
inputLength?: number
|
||||||
|
method?: "keyboard-shortcut" | "click-spotlight-bar"
|
||||||
|
rank?: string | null
|
||||||
|
searcherID?: string | null
|
||||||
|
sessionDuration?: string
|
||||||
|
}
|
||||||
|
|
||||||
export type AnalyticsEvent =
|
export type AnalyticsEvent =
|
||||||
| ({ type: "HOPP_REQUEST_RUN" } & HoppRequestEvent)
|
| ({ type: "HOPP_REQUEST_RUN" } & HoppRequestEvent)
|
||||||
| {
|
| {
|
||||||
@@ -46,6 +55,9 @@ export type AnalyticsEvent =
|
|||||||
| { type: "HOPP_EXPORT_ENVIRONMENT"; platform: "rest" | "gql" }
|
| { type: "HOPP_EXPORT_ENVIRONMENT"; platform: "rest" | "gql" }
|
||||||
| { type: "HOPP_REST_CODEGEN_OPENED" }
|
| { type: "HOPP_REST_CODEGEN_OPENED" }
|
||||||
| { type: "HOPP_REST_IMPORT_CURL" }
|
| { type: "HOPP_REST_IMPORT_CURL" }
|
||||||
|
| ({
|
||||||
|
type: "HOPP_SPOTLIGHT_SESSION"
|
||||||
|
} & HoppSpotlightSessionEventData)
|
||||||
|
|
||||||
export type AnalyticsPlatformDef = {
|
export type AnalyticsPlatformDef = {
|
||||||
initAnalytics: () => void
|
initAnalytics: () => void
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
SpotlightSearcher,
|
SpotlightSearcher,
|
||||||
SpotlightSearcherSessionState,
|
|
||||||
SpotlightSearcherResult,
|
SpotlightSearcherResult,
|
||||||
|
SpotlightSearcherSessionState,
|
||||||
SpotlightService,
|
SpotlightService,
|
||||||
} from "../"
|
} from "../"
|
||||||
import { Ref, computed, nextTick, ref, watch } from "vue"
|
|
||||||
import { TestContainer } from "dioc/testing"
|
|
||||||
|
|
||||||
const echoSearcher: SpotlightSearcher = {
|
const echoSearcher: SpotlightSearcher = {
|
||||||
searcherID: "echo-searcher",
|
searcherID: "echo-searcher",
|
||||||
@@ -78,6 +80,15 @@ const emptySearcher: SpotlightSearcher = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("SpotlightService", () => {
|
describe("SpotlightService", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
setPlatformDef({
|
||||||
|
// @ts-expect-error We're mocking the platform
|
||||||
|
analytics: {
|
||||||
|
logEvent: vi.fn(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("registerSearcher", () => {
|
describe("registerSearcher", () => {
|
||||||
it("registers a searcher with a given ID", () => {
|
it("registers a searcher with a given ID", () => {
|
||||||
const container = new TestContainer()
|
const container = new TestContainer()
|
||||||
@@ -387,16 +398,14 @@ describe("SpotlightService", () => {
|
|||||||
searcherID: "test-searcher",
|
searcherID: "test-searcher",
|
||||||
searcherSectionTitle: "Test Searcher",
|
searcherSectionTitle: "Test Searcher",
|
||||||
createSearchSession: (query) => {
|
createSearchSession: (query) => {
|
||||||
watch(query, notifiedFn, { immediate: true })
|
const dispose = watch(query, notifiedFn, { immediate: true })
|
||||||
|
|
||||||
return [
|
return [
|
||||||
computed<SpotlightSearcherSessionState>(() => ({
|
computed<SpotlightSearcherSessionState>(() => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
results: [],
|
results: [],
|
||||||
})),
|
})),
|
||||||
() => {
|
dispose,
|
||||||
/* noop */
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
onResultSelect: () => {
|
onResultSelect: () => {
|
||||||
@@ -420,7 +429,7 @@ describe("SpotlightService", () => {
|
|||||||
query.value = "test3"
|
query.value = "test3"
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
expect(notifiedFn).toHaveBeenCalledTimes(3)
|
expect(notifiedFn).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("selectSearchResult", () => {
|
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 { 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
|
* Defines how to render the entry text in a Spotlight Search Result
|
||||||
@@ -115,6 +118,7 @@ export type SpotlightSearchState = {
|
|||||||
export class SpotlightService extends Service {
|
export class SpotlightService extends Service {
|
||||||
public static readonly ID = "SPOTLIGHT_SERVICE"
|
public static readonly ID = "SPOTLIGHT_SERVICE"
|
||||||
|
|
||||||
|
private analyticsData: HoppSpotlightSessionEventData = {}
|
||||||
private searchers: Map<string, SpotlightSearcher> = new Map()
|
private searchers: Map<string, SpotlightSearcher> = new Map()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,6 +144,8 @@ export class SpotlightService extends Service {
|
|||||||
public createSearchSession(
|
public createSearchSession(
|
||||||
query: Ref<string>
|
query: Ref<string>
|
||||||
): [Ref<SpotlightSearchState>, () => void] {
|
): [Ref<SpotlightSearchState>, () => void] {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
const searchSessions = Array.from(this.searchers.values()).map(
|
const searchSessions = Array.from(this.searchers.values()).map(
|
||||||
(x) => [x, ...x.createSearchSession(query)] as const
|
(x) => [x, ...x.createSearchSession(query)] as const
|
||||||
)
|
)
|
||||||
@@ -183,6 +189,16 @@ export class SpotlightService extends Service {
|
|||||||
onSessionEndList.push(onSessionEnd)
|
onSessionEndList.push(onSessionEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
query,
|
||||||
|
(newQuery) => {
|
||||||
|
this.setAnalyticsData({
|
||||||
|
inputLength: newQuery.length,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
loadingSearchers,
|
loadingSearchers,
|
||||||
(set) => {
|
(set) => {
|
||||||
@@ -198,6 +214,18 @@ export class SpotlightService extends Service {
|
|||||||
for (const onEnd of onSessionEndList) {
|
for (const onEnd of onSessionEndList) {
|
||||||
onEnd()
|
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]
|
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
|
* 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 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(
|
public selectSearchResult(
|
||||||
searcherID: string,
|
searcherID: string,
|
||||||
result: SpotlightSearcherResult
|
result: SpotlightSearcherResult
|
||||||
) {
|
) {
|
||||||
this.searchers.get(searcherID)?.onResultSelect(result)
|
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