diff --git a/src/components/cards-grid-list/components/tutorial-cards-grid-list/index.tsx b/src/components/cards-grid-list/components/tutorial-cards-grid-list/index.tsx index 916caedb3b..b68dbd65df 100644 --- a/src/components/cards-grid-list/components/tutorial-cards-grid-list/index.tsx +++ b/src/components/cards-grid-list/components/tutorial-cards-grid-list/index.tsx @@ -10,35 +10,34 @@ import { TutorialCardWithAuthElements, } from 'components/tutorial-card' import s from './tutorial-cards.module.css' +import compactStyles from 'components/tutorial-card/tutorial-card-compact.module.css' interface TutorialCardsGridListProps extends CardsGridListProps { tutorials: TutorialCardPropsWithId[] + compact?: boolean } -/** - * Handles rendering a grid of Tutorial cards, and pre-fetching the - * `isBookmarked` state for each card. - */ -const TutorialCardsGridList = ({ tutorials, ...restProps }) => { - /** - * Collect the `tutorialIds` and React elements to render in separate arrays - * at the same time (to save on iterating over the same data twice). - */ +const TutorialCardsGridList = ({ + tutorials, + compact = false, + ...restProps +}) => { const tutorialIds = [] const cardsGridListItems = [] tutorials.forEach((tutorial: TutorialCardPropsWithId) => { tutorialIds.push(tutorial.id) cardsGridListItems.push( -
- +
+
) }) - /** - * Prime the `isBookmarked` queries for the tutorial cards we know we need to - * render via collected `tutorialIds` array. - */ const { isFetching, isRefetching } = useBookmarksByTutorialIds({ tutorialIds, }) diff --git a/src/components/cards-grid-list/components/tutorial-cards-grid-list/tutorial-cards.module.css b/src/components/cards-grid-list/components/tutorial-cards-grid-list/tutorial-cards.module.css index 50dec6afe5..8930af2f3c 100644 --- a/src/components/cards-grid-list/components/tutorial-cards-grid-list/tutorial-cards.module.css +++ b/src/components/cards-grid-list/components/tutorial-cards-grid-list/tutorial-cards.module.css @@ -1,5 +1,10 @@ -.sandboxCardBox { - min-height: 200px; +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +.tutorialCardBox { + min-height: 100px; height: 100%; display: flex; flex-direction: column; diff --git a/src/components/cards-grid-list/types.ts b/src/components/cards-grid-list/types.ts index 497119a611..4ce88419da 100644 --- a/src/components/cards-grid-list/types.ts +++ b/src/components/cards-grid-list/types.ts @@ -9,7 +9,7 @@ interface CardsGridListProps { children: ReactNode isOrdered?: boolean fixedColumns?: number - gridGap?: '16px' | '24px' + gridGap?: '8px' | '12px' | '16px' | '24px' } export type { CardsGridListProps } diff --git a/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx b/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx index a3c79a37cf..18d987f603 100644 --- a/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx +++ b/src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx @@ -11,6 +11,7 @@ import { IconAlertDiamond24 } from '@hashicorp/flight-icons/svg-react/alert-diam import { MdxHighlight, MdxTip, MdxNote, MdxWarning } from './variants' import { MdxInlineAlertData, MdxInlineAlertProps } from './types' import s from './mdx-inline-alert.module.css' +import posthog from 'posthog-js' const ALERT_DATA: MdxInlineAlertData = { tip: { title: 'Tip', icon: , color: 'neutral' }, @@ -73,8 +74,8 @@ export const MdxInlineAlert = withErrorBoundary( MdxInlineAlertBase, AlertErrorFallback, (error, errorInfo) => { - if (typeof window !== 'undefined' && window.posthog?.capture) { - window.posthog.capture('mdx_component_error', { + if (typeof window !== 'undefined' && posthog?.capture) { + posthog.capture('mdx_component_error', { component_name: 'MdxInlineAlert', error_message: error.message, error_stack: error.stack, diff --git a/src/components/interactive-lab-callout/index.tsx b/src/components/interactive-lab-callout/index.tsx index 2a273848cf..aca3378acb 100644 --- a/src/components/interactive-lab-callout/index.tsx +++ b/src/components/interactive-lab-callout/index.tsx @@ -10,6 +10,7 @@ import { useInstruqtEmbed } from 'contexts/instruqt-lab' import { FC } from 'react' import s from './interactive-lab-callout.module.css' import SANDBOX_CONFIG from 'content/sandbox/sandbox.json' assert { type: 'json' } +import { trackSandboxInteraction } from 'views/sandbox-view' interface InteractiveLabCalloutProps { labId?: string @@ -34,6 +35,9 @@ const InteractiveLabCallout: FC = ({ labId }) => { const handleStartLab = () => { if (effectiveLabId) { + trackSandboxInteraction('click', effectiveLabId, { + source: 'interactive-lab-callout', + }) ctx.openLab(effectiveLabId) ctx.setActive(true) } else { diff --git a/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx b/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx index 17881cb0e7..40668f213e 100644 --- a/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx +++ b/src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx @@ -22,6 +22,9 @@ vi.mock('next/router', () => ({ vi.mock('lib/posthog-events', () => ({ trackSandboxEvent: vi.fn(), SANDBOX_EVENT: { + SANDBOX_STARTED: 'sandbox_started', + SANDBOX_OPEN: 'sandbox_open', + SANDBOX_CLOSED: 'sandbox_closed', SANDBOX_LOADED: 'sandbox_loaded', SANDBOX_ERROR: 'sandbox_error', SANDBOX_RETRY: 'sandbox_retry', @@ -133,7 +136,7 @@ describe('EmbedElement', () => { screen.queryByText('Loading your sandbox...') ).not.toBeInTheDocument() - expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_loaded', { + expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_open', { labId: 'test-lab-id', page: '/test-path', }) diff --git a/src/components/lab-embed/embed-element/index.tsx b/src/components/lab-embed/embed-element/index.tsx index 6d08d61f8b..80b973a559 100644 --- a/src/components/lab-embed/embed-element/index.tsx +++ b/src/components/lab-embed/embed-element/index.tsx @@ -50,7 +50,7 @@ const EmbedElement = memo(function EmbedElement(): JSX.Element { })) if (labId) { - trackSandboxEvent(SANDBOX_EVENT.SANDBOX_LOADED, { + trackSandboxEvent(SANDBOX_EVENT.SANDBOX_OPEN, { labId, page: router.asPath, }) diff --git a/src/components/navigation-header/components/sandbox-dropdown/__tests__/sandbox-dropdown.test.tsx b/src/components/navigation-header/components/sandbox-dropdown/__tests__/sandbox-dropdown.test.tsx index 51a0b3c5e5..30b2cb5bfb 100644 --- a/src/components/navigation-header/components/sandbox-dropdown/__tests__/sandbox-dropdown.test.tsx +++ b/src/components/navigation-header/components/sandbox-dropdown/__tests__/sandbox-dropdown.test.tsx @@ -6,7 +6,7 @@ import { render, screen, fireEvent } from '@testing-library/react' import SandboxDropdown from '../index' -// Mock the hooks +// Mock the hooks and functions const mockUserRouter = vi.fn() vi.mock('next/router', () => ({ useRouter: () => mockUserRouter(), @@ -22,6 +22,20 @@ vi.mock('contexts/instruqt-lab', () => ({ useInstruqtEmbed: () => mockUseInstruqtEmbed(), })) +const mockTrackSandboxInteraction = vi.fn() +vi.mock('views/sandbox-view', () => ({ + trackSandboxInteraction: (...args: unknown[]) => + mockTrackSandboxInteraction(...args), +})) + +const mockTrackSandboxEvent = vi.fn() +vi.mock('lib/posthog-events', () => ({ + trackSandboxEvent: (...args: unknown[]) => mockTrackSandboxEvent(...args), + SANDBOX_EVENT: { + SANDBOX_OPEN: 'sandbox_open', + }, +})) + describe('SandboxDropdown', () => { beforeEach(() => { // Reset all mocks before each test @@ -135,7 +149,7 @@ describe('SandboxDropdown', () => { expect(mockOpenLab).toHaveBeenCalled() }) - it('tracks sandbox events when opening labs', () => { + it('tracks sandbox events and interactions when clicking a lab', () => { const mockOpenLab = vi.fn() const mockSetActive = vi.fn() mockUseInstruqtEmbed.mockImplementation(() => ({ @@ -155,5 +169,20 @@ describe('SandboxDropdown', () => { // Verify openLab was called expect(mockOpenLab).toHaveBeenCalled() + + // Verify tracking events were called + expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_open', { + labId: expect.any(String), + page: '/', + }) + + // Verify interaction tracking + expect(mockTrackSandboxInteraction).toHaveBeenCalledWith( + 'hover', + expect.any(String), + { + page: '/', + } + ) }) }) diff --git a/src/components/navigation-header/components/sandbox-dropdown/index.tsx b/src/components/navigation-header/components/sandbox-dropdown/index.tsx index 158ee78d6c..f0f3d73f13 100644 --- a/src/components/navigation-header/components/sandbox-dropdown/index.tsx +++ b/src/components/navigation-header/components/sandbox-dropdown/index.tsx @@ -23,6 +23,7 @@ import { SandboxLab } from 'types/sandbox' import { ProductSlug } from 'types/products' import { buildLabIdWithConfig } from 'lib/build-instruqt-url' import { useTheme } from 'next-themes' +import { trackSandboxInteraction } from 'views/sandbox-view' interface SandboxDropdownProps { ariaLabel: string @@ -122,9 +123,6 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => { } } - /** - * Handle lab selection - */ const handleLabClick = (lab: SandboxLab) => { const labWithTrack = { ...lab, @@ -133,16 +131,13 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => { const fullLabId = buildLabIdWithConfig(labWithTrack) openLab(fullLabId) setActive(true) - trackSandboxEvent(SANDBOX_EVENT.SANDBOX_STARTED, { + trackSandboxEvent(SANDBOX_EVENT.SANDBOX_OPEN, { labId: fullLabId, page: router.asPath, }) setIsOpen(false) } - /** - * Navigate to the sandbox page - */ const navigateToSandboxPage = (e: React.MouseEvent) => { e.preventDefault() router.push(`/${currentProduct.slug}/sandbox`) @@ -206,12 +201,12 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
@@ -220,7 +215,7 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
@@ -230,7 +225,6 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => { - {/* Available Product Sandboxes Section */} {
  • + const { openLab, labId, active } = useInstruqtEmbed() + return ( + <> + +
    {labId || 'no-lab'}
    +
    {active ? 'true' : 'false'}
    + + ) } - await act(async () => { - render( - - - - ) - }) + render( + + + + ) - await act(async () => { - fireEvent.click(await screen.findByText('Open Lab')) + fireEvent.click(screen.getByText('Open Lab')) + + await waitFor(() => { + expect(screen.getByTestId('lab-id')).toHaveTextContent('test-lab-id') + expect(screen.getByTestId('active')).toHaveTextContent('true') }) - expect(mockLocalStorage.setItem).toHaveBeenCalledWith( - 'instruqt-lab-state', - JSON.stringify({ - active: true, - storedLabId: 'test-lab-id', - }) + await waitFor( + () => { + expect(setItemSpy).toHaveBeenCalledWith( + 'instruqt-lab-state', + JSON.stringify({ + active: true, + storedLabId: 'test-lab-id', + }) + ) + }, + { timeout: 3000 } ) }) it('tracks sandbox events when opening a lab', async () => { const TestComponent = () => { - const { openLab } = useTestInstruqtEmbed() + const { openLab } = useInstruqtEmbed() return } - await act(async () => { - render( - - - - ) - }) + render( + + + + ) - await act(async () => { - fireEvent.click(await screen.findByText('Open Lab')) - }) + fireEvent.click(screen.getByText('Open Lab')) - expect(mockTrackSandboxEvent).toHaveBeenCalledWith( - SANDBOX_EVENT.SANDBOX_OPEN, - { - labId: 'test-lab-id', - page: '/test-path', - } + await waitFor( + () => { + expect(mockTrackSandboxEvent).toHaveBeenCalledWith( + SANDBOX_EVENT.SANDBOX_OPEN, + { + labId: 'test-lab-id', + page: '/test-path', + } + ) + }, + { timeout: 3000 } ) }) it('tracks sandbox events when closing a lab', async () => { const TestComponent = () => { - const { openLab, closeLab } = useTestInstruqtEmbed() + const { openLab, closeLab } = useInstruqtEmbed() return ( <>