Skip to content

Commit 481fcbe

Browse files
authored
Feature: sandbox page and tutorials (#2870)
* Improves PostHog error tracking on sandbox page Updates error tracking logic to use the imported `posthog` instance instead of relying on `window.posthog`. This ensures that the PostHog instance is correctly referenced, improving the reliability of error tracking on the sandbox page. * Makes bookmark optional on tutorial cards Allows tutorial cards to be displayed without a bookmark button. This change provides the flexibility to display tutorial cards in contexts where bookmarking functionality is not desired or applicable. Specifically, it disables the bookmark button within the tutorial cards grid list. * Fixes tutorial card props and testing context Corrects the type definition for the TutorialCardWithAuthElements component to properly handle optional properties. Removes commented-out code in the instruqt-lab context test file, cleaning up the codebase and removing distractions. * Reduces card and badge sizes for a denser UI. Reduces padding and margins in cards and badges to create a more compact and information-dense user interface. This aims to improve content presentation by minimizing whitespace and making more content visible at a glance. * Refactors Instruqt lab context tests Updates tests for the Instruqt lab context to improve reliability and reduce duplication. - Mocks localStorage directly instead of creating a mock object. - Uses vi.spyOn for getItem and setItem to improve test isolation. - Adds mock for validateSandboxConfigWithDetailedErrors to isolate config validation. - Updates mock for next/router to simplify it. * Refactors PostHog capture calls Simplifies PostHog event capture calls by directly importing the `posthog` instance. This change improves code readability and reduces redundancy across multiple components and contexts. * Improves card layouts and adds sandbox card Refactors card layouts for better spacing and responsiveness. Introduces a new sandbox card component for displaying sandbox environments. Updates tutorial card list to support compact cards. * Reduces sandbox dropdown intro size matches figma. Reduces the size of the product icon and intro text in the sandbox dropdown for a more visually appealing presentation. * Increases padding in sandbox card component Improves visual spacing within the sandbox card component by increasing the padding and margins to create a more balanced and appealing user interface. * Improves Instruqt lab context tests Updates tests to use `waitFor` for asynchronous state updates, ensuring accurate state validation and preventing race conditions. Also, fixes a bug where the `active` state was not being set correctly when a new lab is opened. * Improves sandbox card layout and styling Removes !important overrides from tutorial card styles for better CSS specificity management. Increases the grid gap in the sandbox view to improve visual spacing. * Tracks sandbox interactions for analytics Implements event tracking for user interactions within the sandbox environment. This change captures key interactions, such as clicks and hovers, to provide insights into user behavior and improve the sandbox experience. The data collected will inform decisions on sandbox discoverability, engagement, and overall user satisfaction. * Updates sandbox event tracking Updates the sandbox event that is tracked when the iframe loads to `sandbox_open`. This more accurately reflects the user's action of opening the sandbox and aligns with the new event naming conventions. * Adds sandbox event tracking to dropdown Adds tracking for sandbox events and interactions when a user clicks on a lab within the sandbox dropdown. This provides insights into user engagement with sandboxes.
1 parent b567730 commit 481fcbe

File tree

24 files changed

+431
-455
lines changed

24 files changed

+431
-455
lines changed

src/components/cards-grid-list/components/tutorial-cards-grid-list/index.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,34 @@ import {
1010
TutorialCardWithAuthElements,
1111
} from 'components/tutorial-card'
1212
import s from './tutorial-cards.module.css'
13+
import compactStyles from 'components/tutorial-card/tutorial-card-compact.module.css'
1314

1415
interface TutorialCardsGridListProps extends CardsGridListProps {
1516
tutorials: TutorialCardPropsWithId[]
17+
compact?: boolean
1618
}
1719

18-
/**
19-
* Handles rendering a grid of Tutorial cards, and pre-fetching the
20-
* `isBookmarked` state for each card.
21-
*/
22-
const TutorialCardsGridList = ({ tutorials, ...restProps }) => {
23-
/**
24-
* Collect the `tutorialIds` and React elements to render in separate arrays
25-
* at the same time (to save on iterating over the same data twice).
26-
*/
20+
const TutorialCardsGridList = ({
21+
tutorials,
22+
compact = false,
23+
...restProps
24+
}) => {
2725
const tutorialIds = []
2826
const cardsGridListItems = []
2927
tutorials.forEach((tutorial: TutorialCardPropsWithId) => {
3028
tutorialIds.push(tutorial.id)
3129
cardsGridListItems.push(
32-
<div className={s.sandboxCardBox} key={tutorial.id}>
33-
<TutorialCardWithAuthElements {...tutorial} />
30+
<div
31+
className={`${s.tutorialCardBox} ${
32+
compact ? compactStyles.compactTutorialCard : ''
33+
}`}
34+
key={tutorial.id}
35+
>
36+
<TutorialCardWithAuthElements {...tutorial} hasBookmark={false} />
3437
</div>
3538
)
3639
})
3740

38-
/**
39-
* Prime the `isBookmarked` queries for the tutorial cards we know we need to
40-
* render via collected `tutorialIds` array.
41-
*/
4241
const { isFetching, isRefetching } = useBookmarksByTutorialIds({
4342
tutorialIds,
4443
})

src/components/cards-grid-list/components/tutorial-cards-grid-list/tutorial-cards.module.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
.sandboxCardBox {
2-
min-height: 200px;
1+
/**
2+
* Copyright (c) HashiCorp, Inc.
3+
* SPDX-License-Identifier: MPL-2.0
4+
*/
5+
6+
.tutorialCardBox {
7+
min-height: 100px;
38
height: 100%;
49
display: flex;
510
flex-direction: column;

src/components/cards-grid-list/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ interface CardsGridListProps {
99
children: ReactNode
1010
isOrdered?: boolean
1111
fixedColumns?: number
12-
gridGap?: '16px' | '24px'
12+
gridGap?: '8px' | '12px' | '16px' | '24px'
1313
}
1414

1515
export type { CardsGridListProps }

src/components/dev-dot-content/mdx-components/mdx-alert/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { IconAlertDiamond24 } from '@hashicorp/flight-icons/svg-react/alert-diam
1111
import { MdxHighlight, MdxTip, MdxNote, MdxWarning } from './variants'
1212
import { MdxInlineAlertData, MdxInlineAlertProps } from './types'
1313
import s from './mdx-inline-alert.module.css'
14+
import posthog from 'posthog-js'
1415

1516
const ALERT_DATA: MdxInlineAlertData = {
1617
tip: { title: 'Tip', icon: <IconInfo24 />, color: 'neutral' },
@@ -73,8 +74,8 @@ export const MdxInlineAlert = withErrorBoundary(
7374
MdxInlineAlertBase,
7475
AlertErrorFallback,
7576
(error, errorInfo) => {
76-
if (typeof window !== 'undefined' && window.posthog?.capture) {
77-
window.posthog.capture('mdx_component_error', {
77+
if (typeof window !== 'undefined' && posthog?.capture) {
78+
posthog.capture('mdx_component_error', {
7879
component_name: 'MdxInlineAlert',
7980
error_message: error.message,
8081
error_stack: error.stack,

src/components/interactive-lab-callout/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useInstruqtEmbed } from 'contexts/instruqt-lab'
1010
import { FC } from 'react'
1111
import s from './interactive-lab-callout.module.css'
1212
import SANDBOX_CONFIG from 'content/sandbox/sandbox.json' assert { type: 'json' }
13+
import { trackSandboxInteraction } from 'views/sandbox-view'
1314

1415
interface InteractiveLabCalloutProps {
1516
labId?: string
@@ -34,6 +35,9 @@ const InteractiveLabCallout: FC<InteractiveLabCalloutProps> = ({ labId }) => {
3435

3536
const handleStartLab = () => {
3637
if (effectiveLabId) {
38+
trackSandboxInteraction('click', effectiveLabId, {
39+
source: 'interactive-lab-callout',
40+
})
3741
ctx.openLab(effectiveLabId)
3842
ctx.setActive(true)
3943
} else {

src/components/lab-embed/embed-element/__tests__/embed-element.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ vi.mock('next/router', () => ({
2222
vi.mock('lib/posthog-events', () => ({
2323
trackSandboxEvent: vi.fn(),
2424
SANDBOX_EVENT: {
25+
SANDBOX_STARTED: 'sandbox_started',
26+
SANDBOX_OPEN: 'sandbox_open',
27+
SANDBOX_CLOSED: 'sandbox_closed',
2528
SANDBOX_LOADED: 'sandbox_loaded',
2629
SANDBOX_ERROR: 'sandbox_error',
2730
SANDBOX_RETRY: 'sandbox_retry',
@@ -133,7 +136,7 @@ describe('EmbedElement', () => {
133136
screen.queryByText('Loading your sandbox...')
134137
).not.toBeInTheDocument()
135138

136-
expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_loaded', {
139+
expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_open', {
137140
labId: 'test-lab-id',
138141
page: '/test-path',
139142
})

src/components/lab-embed/embed-element/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const EmbedElement = memo(function EmbedElement(): JSX.Element {
5050
}))
5151

5252
if (labId) {
53-
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_LOADED, {
53+
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_OPEN, {
5454
labId,
5555
page: router.asPath,
5656
})

src/components/navigation-header/components/sandbox-dropdown/__tests__/sandbox-dropdown.test.tsx

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { render, screen, fireEvent } from '@testing-library/react'
77
import SandboxDropdown from '../index'
88

9-
// Mock the hooks
9+
// Mock the hooks and functions
1010
const mockUserRouter = vi.fn()
1111
vi.mock('next/router', () => ({
1212
useRouter: () => mockUserRouter(),
@@ -22,6 +22,20 @@ vi.mock('contexts/instruqt-lab', () => ({
2222
useInstruqtEmbed: () => mockUseInstruqtEmbed(),
2323
}))
2424

25+
const mockTrackSandboxInteraction = vi.fn()
26+
vi.mock('views/sandbox-view', () => ({
27+
trackSandboxInteraction: (...args: unknown[]) =>
28+
mockTrackSandboxInteraction(...args),
29+
}))
30+
31+
const mockTrackSandboxEvent = vi.fn()
32+
vi.mock('lib/posthog-events', () => ({
33+
trackSandboxEvent: (...args: unknown[]) => mockTrackSandboxEvent(...args),
34+
SANDBOX_EVENT: {
35+
SANDBOX_OPEN: 'sandbox_open',
36+
},
37+
}))
38+
2539
describe('SandboxDropdown', () => {
2640
beforeEach(() => {
2741
// Reset all mocks before each test
@@ -135,7 +149,7 @@ describe('SandboxDropdown', () => {
135149
expect(mockOpenLab).toHaveBeenCalled()
136150
})
137151

138-
it('tracks sandbox events when opening labs', () => {
152+
it('tracks sandbox events and interactions when clicking a lab', () => {
139153
const mockOpenLab = vi.fn()
140154
const mockSetActive = vi.fn()
141155
mockUseInstruqtEmbed.mockImplementation(() => ({
@@ -155,5 +169,20 @@ describe('SandboxDropdown', () => {
155169

156170
// Verify openLab was called
157171
expect(mockOpenLab).toHaveBeenCalled()
172+
173+
// Verify tracking events were called
174+
expect(mockTrackSandboxEvent).toHaveBeenCalledWith('sandbox_open', {
175+
labId: expect.any(String),
176+
page: '/',
177+
})
178+
179+
// Verify interaction tracking
180+
expect(mockTrackSandboxInteraction).toHaveBeenCalledWith(
181+
'hover',
182+
expect.any(String),
183+
{
184+
page: '/',
185+
}
186+
)
158187
})
159188
})

src/components/navigation-header/components/sandbox-dropdown/index.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { SandboxLab } from 'types/sandbox'
2323
import { ProductSlug } from 'types/products'
2424
import { buildLabIdWithConfig } from 'lib/build-instruqt-url'
2525
import { useTheme } from 'next-themes'
26+
import { trackSandboxInteraction } from 'views/sandbox-view'
2627

2728
interface SandboxDropdownProps {
2829
ariaLabel: string
@@ -122,9 +123,6 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
122123
}
123124
}
124125

125-
/**
126-
* Handle lab selection
127-
*/
128126
const handleLabClick = (lab: SandboxLab) => {
129127
const labWithTrack = {
130128
...lab,
@@ -133,16 +131,13 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
133131
const fullLabId = buildLabIdWithConfig(labWithTrack)
134132
openLab(fullLabId)
135133
setActive(true)
136-
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_STARTED, {
134+
trackSandboxEvent(SANDBOX_EVENT.SANDBOX_OPEN, {
137135
labId: fullLabId,
138136
page: router.asPath,
139137
})
140138
setIsOpen(false)
141139
}
142140

143-
/**
144-
* Navigate to the sandbox page
145-
*/
146141
const navigateToSandboxPage = (e: React.MouseEvent) => {
147142
e.preventDefault()
148143
router.push(`/${currentProduct.slug}/sandbox`)
@@ -206,12 +201,12 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
206201
<div className={s.introSandboxRow}>
207202
<ProductIcon
208203
productSlug={currentProduct.slug as ProductSlug}
209-
size={24}
204+
size={16}
210205
className={s.productIcon}
211206
/>
212207
<Text
213208
asElement="span"
214-
className={`${s.sectionTitle} ${s.introSandboxTitle}`}
209+
className={`${s.sectionTitle} ${s.introSandboxTitle} ${s.title}`}
215210
size={200}
216211
weight="semibold"
217212
>
@@ -220,7 +215,7 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
220215
</div>
221216
<Text
222217
asElement="span"
223-
className={`${s.introText} ${s.introSandboxText}`}
218+
className={`${s.introText} ${s.introSandboxText} ${s.description}`}
224219
size={100}
225220
weight="regular"
226221
>
@@ -230,7 +225,6 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
230225
</Text>
231226
</button>
232227

233-
{/* Available Product Sandboxes Section */}
234228
<Text
235229
asElement="p"
236230
className={s.sectionTitle}
@@ -245,7 +239,12 @@ const SandboxDropdown = ({ ariaLabel, label }: SandboxDropdownProps) => {
245239
<li key={lab.labId || index} className={s.itemContainer}>
246240
<button
247241
className={s.sandboxItem}
248-
onClick={() => handleLabClick(lab)}
242+
onClick={() => {
243+
handleLabClick(lab)
244+
trackSandboxInteraction('hover', lab.labId, {
245+
page: router.asPath,
246+
})
247+
}}
249248
onKeyDown={handleKeyDown}
250249
>
251250
<div className={s.content}>

src/components/navigation-header/components/sandbox-dropdown/sandbox-dropdown.module.css

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,13 +167,16 @@
167167
.productIcon {
168168
flex-shrink: 0;
169169
color: var(--token-color-foreground-faint);
170+
width: 16px;
171+
height: 16px;
170172
}
171173

172174
.description {
173175
color: var(--token-color-foreground-faint);
174176
display: block;
175177
line-height: 1.5;
176-
font-size: 14px;
178+
font-size: 13px;
179+
font-weight: 400;
177180
}
178181

179182
.learnMoreLink {

0 commit comments

Comments
 (0)