Skip to content
Open
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
15 changes: 14 additions & 1 deletion components/AiChatButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
useState,
} from "react";

import * as posthog from "@/lib/posthog";

import useInkeepSettings from "../hooks/useInKeepSettings";

const ModalChat = dynamic(
Expand Down Expand Up @@ -47,6 +49,11 @@ export function InkeepModalProvider({ children }: { children: ReactNode }) {
} = useInkeepSettings();

const openWithPrompt = useCallback((prompt: string) => {
posthog.track("ask-ai-mobile-modal-opened-client", {
source: "search",
has_prompt: true,
prompt_length: prompt.length,
});
setInitialQuery(prompt);
setIsOpen(true);
}, []);
Expand Down Expand Up @@ -78,7 +85,13 @@ export function InkeepModalProvider({ children }: { children: ReactNode }) {
{children}
<div className="md-visible">
<button
onClick={() => setIsOpen(true)}
onClick={() => {
posthog.track("ask-ai-mobile-modal-opened-client", {
source: "floating_button",
has_prompt: false,
});
setIsOpen(true);
}}
className="fixed right-4 bottom-4 z-40 flex h-11 w-11 items-center justify-center rounded-full border shadow-lg"
style={{
borderColor: "var(--tgph-gray-4)",
Expand Down
40 changes: 37 additions & 3 deletions components/AskAiContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
useState,
} from "react";

import * as posthog from "@/lib/posthog";

const STORAGE_KEYS = {
sidebarWidth: "askAiSidebarWidth",
chatSessions: "askAiChatSessions",
Expand Down Expand Up @@ -233,25 +235,57 @@ export function AskAiProvider({ children }: { children: ReactNode }) {
return sessionId;
}, []);

const openSidebar = useCallback(() => setIsOpen(true), []);
const closeSidebar = useCallback(() => setIsOpen(false), []);
const toggleSidebar = useCallback(() => setIsOpen((prev) => !prev), []);
const openSidebar = useCallback(() => {
posthog.track("ask-ai-sidebar-opened-client", {
source: "button",
});
setIsOpen(true);
}, []);

const closeSidebar = useCallback(() => {
posthog.track("ask-ai-sidebar-closed-client");
setIsOpen(false);
}, []);

const toggleSidebar = useCallback(() => {
setIsOpen((prev) => {
if (!prev) {
posthog.track("ask-ai-sidebar-opened-client", {
source: "button",
});
} else {
posthog.track("ask-ai-sidebar-closed-client");
}
return !prev;
});
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side effect inside React state updater function

Low Severity

posthog.track is called inside the setIsOpen state updater function in toggleSidebar. React state updater functions are expected to be pure — in development with Strict Mode, React may invoke them twice, which would fire duplicate tracking events. The tracking call belongs outside the updater, using the previous state value from a ref or a separate check.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 81c0353. Configure here.


const openSidebarWithPrompt = useCallback(
(prompt: string) => {
posthog.track("ask-ai-sidebar-opened-client", {
source: "search",
has_prompt: true,
prompt_length: prompt.length,
});
// Create new session before setting prompt to ensure each query starts fresh
createNewSession();
setInitialPrompt(prompt);
setIsOpen(true);
},
[createNewSession],
);

const clearInitialPrompt = useCallback(() => setInitialPrompt(null), []);

// Select an existing chat session
const selectSession = useCallback(
(sessionId: string) => {
const session = chatSessions.find((s) => s.id === sessionId);
if (session) {
posthog.track("ask-ai-session-switched-client", {
message_count: session.messages.length,
});

// Abort any ongoing stream before switching sessions
// This saves partial content to the current session
beforeSessionChangeRef.current?.();
Expand Down
106 changes: 100 additions & 6 deletions components/ui/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import React, {
} from "react";
import { useHotkeys } from "react-hotkeys-hook";

import * as posthog from "@/lib/posthog";

import { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
Expand Down Expand Up @@ -136,6 +138,8 @@ const handleSearchNavigation = (
router,
itemUrl,
onSearch: () => void,
resultType: "doc" | "endpoint",
query?: string,
) => {
const pathname = router.asPath;
const isMapiReference =
Expand All @@ -146,6 +150,13 @@ const handleSearchNavigation = (
pathname.startsWith("/api-reference");
const isSamePageReferenceResult = isMapiReference || isApiReference;

posthog.track("docs-search-result-selected-client", {
result_type: resultType,
path: itemUrl,
query: query || "",
query_length: query?.length || 0,
});

// If the item is in the same reference, highlight the item, don't navigate
if (isSamePageReferenceResult) {
e.preventDefault();
Expand All @@ -167,9 +178,11 @@ const handleSearchNavigation = (
const DocsSearchResult = ({
item,
onClick,
query,
}: {
item: ResultItem;
onClick: () => void;
query: string;
}) => {
const router = useRouter();
const href = `/${item.path}`;
Expand Down Expand Up @@ -204,7 +217,9 @@ const DocsSearchResult = ({
return (
<a
href={href}
onClick={(e) => handleSearchNavigation(e, router, item.path, onClick)}
onClick={(e) =>
handleSearchNavigation(e, router, item.path, onClick, "doc", query)
}
>
{content}
</a>
Expand All @@ -214,7 +229,9 @@ const DocsSearchResult = ({
return (
<Link
href={href}
onClick={(e) => handleSearchNavigation(e, router, item.path, onClick)}
onClick={(e) =>
handleSearchNavigation(e, router, item.path, onClick, "doc", query)
}
>
{content}
</Link>
Expand All @@ -224,9 +241,11 @@ const DocsSearchResult = ({
const EndpointSearchResult = ({
item,
onClick,
query,
}: {
item: EndpointSearchItem;
onClick: () => void;
query: string;
}) => {
const router = useRouter();
const href = `/${item.path}`;
Expand Down Expand Up @@ -282,7 +301,16 @@ const EndpointSearchResult = ({
return (
<a
href={href}
onClick={(e) => handleSearchNavigation(e, router, item.path, onClick)}
onClick={(e) =>
handleSearchNavigation(
e,
router,
item.path,
onClick,
"endpoint",
query,
)
}
>
{content}
</a>
Expand All @@ -292,7 +320,9 @@ const EndpointSearchResult = ({
return (
<Link
href={href}
onClick={(e) => handleSearchNavigation(e, router, item.path, onClick)}
onClick={(e) =>
handleSearchNavigation(e, router, item.path, onClick, "endpoint", query)
}
>
{content}
</Link>
Expand Down Expand Up @@ -330,6 +360,14 @@ const Autocomplete = () => {
autocompleteInstance?: ReturnType<typeof createAutocomplete>,
) => {
const prompt = createAskAiPrompt(query);

posthog.track("docs-search-result-selected-client", {
result_type: "ask_ai",
query,
query_length: query.length,
device_type: isMobile ? "mobile" : "desktop",
});

if (isMobile) {
openInkeepModal(prompt);
} else {
Expand All @@ -346,12 +384,30 @@ const Autocomplete = () => {
[],
);

// Track search opened - use ref to track if we've already fired the event
const hasTrackedOpenRef = useRef(false);

const autocomplete = useMemo(
() =>
createAutocomplete({
onStateChange({ state }) {
onStateChange({ state, prevState }) {
setIsSearchOpen(state.isOpen);
setAutocompleteState(state);

// Track when search panel opens (query becomes non-empty and panel opens)
if (state.isOpen && !prevState.isOpen && state.query) {
if (!hasTrackedOpenRef.current) {
hasTrackedOpenRef.current = true;
posthog.track("docs-search-opened-client", {
trigger: "query",
});
}
}

// Reset tracking when search closes
if (!state.isOpen && prevState.isOpen) {
hasTrackedOpenRef.current = false;
}
},
getSources() {
return [
Expand Down Expand Up @@ -470,11 +526,21 @@ const Autocomplete = () => {
navigate({ itemUrl, item, state }) {
// Handle Ask AI navigation (check window.innerWidth inline since useMemo can't access isMobile state)
if ((item as any).__isAskAiItem) {
const isMobileDevice = window.innerWidth <= MOBILE_BREAKPOINT;

posthog.track("docs-search-result-selected-client", {
result_type: "ask_ai",
query: state.query,
query_length: state.query.length,
device_type: isMobileDevice ? "mobile" : "desktop",
selection_method: "keyboard",
});

closeAutocompleteRef.current?.();
// Defer opening sidebar/modal to next tick to avoid UI conflicts during close
setTimeout(() => {
const prompt = createAskAiPrompt(state.query);
if (window.innerWidth <= MOBILE_BREAKPOINT) {
if (isMobileDevice) {
openInkeepModal(prompt);
} else {
openSidebarWithPrompt(prompt);
Expand All @@ -483,6 +549,16 @@ const Autocomplete = () => {
return;
}

// Track doc/endpoint selection via keyboard
const isEndpoint = (item as any).index === "endpoints";
posthog.track("docs-search-result-selected-client", {
result_type: isEndpoint ? "endpoint" : "doc",
path: itemUrl,
query: state.query,
query_length: state.query.length,
selection_method: "keyboard",
});

// For API reference items, use window.location for full page reload
// to ensure the ApiReferenceProvider context is properly initialized
if (isApiReferencePath(itemUrl)) {
Expand Down Expand Up @@ -519,6 +595,12 @@ const Autocomplete = () => {
// adding small timeout so event doesn't get to the focused input resulting
// in "/" being displayed on the input
e.preventDefault();

posthog.track("docs-search-opened-client", {
trigger: "hotkey",
});
Comment thread
cursor[bot] marked this conversation as resolved.
hasTrackedOpenRef.current = true;

setTimeout(() => {
const ref = inputRef.current;

Expand Down Expand Up @@ -595,6 +677,16 @@ const Autocomplete = () => {
| ResultItem
| undefined;
if (firstItem?.path) {
const isEndpoint = (firstItem as any).index === "endpoints";

posthog.track("docs-search-result-selected-client", {
result_type: isEndpoint ? "endpoint" : "doc",
path: firstItem.path,
query: autocompleteState?.query || "",
query_length: autocompleteState?.query?.length || 0,
selection_method: "keyboard_enter",
});

// For API reference items, use window.location for full page reload
if (isApiReferencePath(firstItem.path)) {
window.location.href = `/${firstItem.path}`;
Expand Down Expand Up @@ -820,11 +912,13 @@ const Autocomplete = () => {
<EndpointSearchResult
item={item as EndpointSearchItem}
onClick={() => autocomplete.setQuery("")}
query={autocompleteState?.query || ""}
/>
) : (
<DocsSearchResult
item={item as DocsSearchItem}
onClick={() => autocomplete.setQuery("")}
query={autocompleteState?.query || ""}
/>
)}
</MenuItem>
Expand Down
15 changes: 15 additions & 0 deletions hooks/useChatStream.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useCallback, useRef, useEffect, MutableRefObject } from "react";

import * as posthog from "@/lib/posthog";

import {
useAskAi,
type Message,
Expand Down Expand Up @@ -182,6 +185,13 @@ export function useChatStream(): UseChatStreamReturn {
// A session is "new" if it was just created OR if it has no messages yet
const isNewSession = wasNewSession || isFirstMessage;

posthog.track("ask-ai-message-sent-client", {
message_length: content.trim().length,
is_first_message: isFirstMessage,
is_new_session: isNewSession,
conversation_length: messages.length,
});

// Add user message immediately
const userMessage: Message = {
id: `user-${Date.now()}`,
Expand Down Expand Up @@ -355,6 +365,11 @@ export function useChatStream(): UseChatStreamReturn {
);

const stopStream = useCallback(() => {
posthog.track("ask-ai-stream-stopped-client", {
content_length: contentBufferRef.current.length,
displayed_length: displayedLengthRef.current,
});

// Mark that user manually stopped the stream
userStoppedRef.current = true;

Expand Down
Loading