Skip to content

Commit

Permalink
feat(core): add telemetry logs to announcement viewed and resources m…
Browse files Browse the repository at this point in the history
…enu clicked
  • Loading branch information
pedrobonamin committed Sep 17, 2024
1 parent 4d740f2 commit 8eecf04
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
import {CloseIcon} from '@sanity/icons'
import {useTelemetry} from '@sanity/telemetry/react'
import {Box, Flex, Grid, Text} from '@sanity/ui'
import {Fragment, useCallback, useMemo, useRef} from 'react'
import {Fragment, useCallback, useEffect, useMemo, useRef} from 'react'
import {useTranslation} from 'react-i18next'
import {styled} from 'styled-components'

import {Button, Dialog} from '../../../ui-components'
import {useDateTimeFormat, type UseDateTimeFormatOptions} from '../../hooks'
import {SANITY_VERSION} from '../../version'
import {UpsellDescriptionSerializer} from '../upsell'
import {ProductAnnouncementLinkClicked} from './__telemetry__/studioAnnouncements.telemetry'
import {
ProductAnnouncementLinkClicked,
ProductAnnouncementViewed,
} from './__telemetry__/studioAnnouncements.telemetry'
import {Divider} from './Divider'
import {type DialogMode, type StudioAnnouncementDocument} from './types'

Expand Down Expand Up @@ -40,24 +43,26 @@ const FloatingButton = styled(Button)`
z-index: 2;
`

interface UnseenDocumentProps {
interface AnnouncementProps {
announcement: StudioAnnouncementDocument
mode: DialogMode
isFirst: boolean
parentRef: React.RefObject<HTMLDivElement>
}

/**
* Renders the unseen document in the dialog.
* Has a sticky header with the date and title, and a body with the content.
*/
function UnseenDocument({announcement, mode}: UnseenDocumentProps) {
function Announcement({announcement, mode, isFirst, parentRef}: AnnouncementProps) {
const telemetry = useTelemetry()
const dateFormatter = useDateTimeFormat(DATE_FORMAT_OPTIONS)
const {publishedDate, title, body} = announcement
const logViewedItemRef = useRef<HTMLDivElement | null>(null)

const formattedDate = useMemo(() => {
if (!publishedDate) return ''
return dateFormatter.format(new Date(publishedDate))
}, [publishedDate, dateFormatter])
if (!announcement.publishedDate) return ''
return dateFormatter.format(new Date(announcement.publishedDate))
}, [announcement.publishedDate, dateFormatter])

const handleLinkClick = useCallback(
({url, linkTitle}: {url: string; linkTitle: string}) => {
Expand All @@ -73,6 +78,49 @@ function UnseenDocument({announcement, mode}: UnseenDocumentProps) {
},
[telemetry, announcement, mode],
)
const logViewed = useCallback(
(scrolledIntoView: boolean) => {
telemetry.log(ProductAnnouncementViewed, {
announcement_id: announcement._id,
announcement_title: announcement.title,
source: 'studio',
studio_version: SANITY_VERSION,
scrolled_into_view: scrolledIntoView,
origin: mode,
})
},
[announcement._id, announcement.title, mode, telemetry],
)

useEffect(() => {
if (isFirst) {
// If it's the first announcement we want to log that the user has seen it.
// The rest will be logged when they scroll into view.
logViewed(false)
return
}
const item = logViewedItemRef.current
const parent = parentRef.current

if (!item || !parent) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
logViewed(true)
// Disconnect the observer after it's been viewed
observer.disconnect()
}
},
{root: parent, threshold: 1, rootMargin: '0px 0px -100px 0px'},
)

observer.observe(item)

// eslint-disable-next-line consistent-return
return () => {
observer.disconnect()
}
}, [logViewed, parentRef, isFirst])

return (
<Box>
Expand All @@ -84,21 +132,21 @@ function UnseenDocument({announcement, mode}: UnseenDocumentProps) {
</Text>
</Box>
</Box>
<Flex flex={1} padding={2} justify="center">
<Flex flex={1} padding={2} justify="center" ref={logViewedItemRef}>
<Text size={1} weight="semibold">
{title}
{announcement.title}
</Text>
</Flex>
</DialogHeader>
<Box padding={4}>
<UpsellDescriptionSerializer blocks={body} onLinkClick={handleLinkClick} />
<UpsellDescriptionSerializer blocks={announcement.body} onLinkClick={handleLinkClick} />
</Box>
</Box>
)
}

interface StudioAnnouncementDialogProps {
unseenDocuments: StudioAnnouncementDocument[]
announcements: StudioAnnouncementDocument[]
onClose: () => void
mode: DialogMode
}
Expand All @@ -109,7 +157,7 @@ interface StudioAnnouncementDialogProps {
* @hidden
*/
export function StudioAnnouncementsDialog({
unseenDocuments = [],
announcements = [],
onClose,
mode,
}: StudioAnnouncementDialogProps) {
Expand All @@ -127,11 +175,16 @@ export function StudioAnnouncementsDialog({
__unstable_autoFocus={false}
>
<Root ref={dialogRef} height="fill">
{unseenDocuments.map((unseenDocument, index) => (
<Fragment key={unseenDocument._id}>
<UnseenDocument announcement={unseenDocument} mode={mode} />
{announcements.map((announcement, index) => (
<Fragment key={announcement._id}>
<Announcement
announcement={announcement}
mode={mode}
isFirst={index === 0}
parentRef={dialogRef}
/>
{/* Add a divider between each dialog if it's not the last one */}
{index < unseenDocuments.length - 1 && <Divider parentRef={dialogRef} />}
{index < announcements.length - 1 && <Divider parentRef={dialogRef} />}
</Fragment>
))}
<FloatingButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
/* eslint-disable camelcase */
import {useTelemetry} from '@sanity/telemetry/react'
import {useCallback} from 'react'

import {MenuItem} from '../../../ui-components'
import {SANITY_VERSION} from '../../version'
import {WhatsNewHelpMenuItemClicked} from './__telemetry__/studioAnnouncements.telemetry'
import {useStudioAnnouncements} from './useStudioAnnouncements'

export function StudioAnnouncementsMenuItem({text}: {text: string}) {
const {onDialogOpen, studioAnnouncements} = useStudioAnnouncements()
const telemetry = useTelemetry()

const handleOpenDialog = useCallback(() => {
onDialogOpen('help_menu')
}, [onDialogOpen])
telemetry.log(WhatsNewHelpMenuItemClicked, {
source: 'studio',
announcement_id: studioAnnouncements[0]?._id,
announcement_title: studioAnnouncements[0]?.title,
studio_version: SANITY_VERSION,
})
}, [onDialogOpen, studioAnnouncements, telemetry])

if (studioAnnouncements.length === 0) return null
return <MenuItem tone="default" text={text} onClick={handleOpenDialog} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export function StudioAnnouncementsProvider({children}: StudioAnnouncementsProvi
{dialogMode && (
<StudioAnnouncementsDialog
mode={dialogMode}
unseenDocuments={dialogMode === 'help_menu' ? studioAnnouncements : unseenAnnouncements}
announcements={dialogMode === 'help_menu' ? studioAnnouncements : unseenAnnouncements}
onClose={handleDialogClose}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ interface ProductAnnouncementSharedProperties {
announcement_title: string
source: 'studio'
studio_version?: string
// TODO: Aren't this added automatically?
project_id?: string
organization_id?: string
}

type origin = 'card' | 'help_menu'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {StudioAnnouncementsDialog} from '../StudioAnnouncementsDialog'
import {type StudioAnnouncementDocument} from '../types'

jest.mock('@sanity/telemetry/react', () => ({
useTelemetry: jest.fn(),
useTelemetry: jest.fn().mockReturnValue({
log: jest.fn(),
}),
}))

jest.mock('../../../version', () => ({
Expand Down Expand Up @@ -102,7 +104,7 @@ describe('StudioAnnouncementsCard', () => {
const wrapper = await createAnnouncementWrapper()
await render(
<StudioAnnouncementsDialog
unseenDocuments={MOCKED_ANNOUNCEMENTS}
announcements={MOCKED_ANNOUNCEMENTS}
onClose={onCloseMock}
mode="card"
/>,
Expand Down Expand Up @@ -135,7 +137,7 @@ describe('StudioAnnouncementsCard', () => {
const wrapper = await createAnnouncementWrapper()
await render(
<StudioAnnouncementsDialog
unseenDocuments={MOCKED_ANNOUNCEMENTS}
announcements={MOCKED_ANNOUNCEMENTS}
onClose={onCloseMock}
mode="card"
/>,
Expand All @@ -148,18 +150,16 @@ describe('StudioAnnouncementsCard', () => {
expect(onCloseMock).toHaveBeenCalled()
})

test('logs telemetry when link is clicked', async () => {
test('logs telemetry when link is clicked and announcement viewed', async () => {
const onCloseMock = jest.fn()
const mockLog = jest.fn()
const {useTelemetry} = require('@sanity/telemetry/react')
// Set up the mock return value
useTelemetry.mockReturnValue({
log: mockLog,
})
useTelemetry.mockReturnValue({log: mockLog})
const wrapper = await createAnnouncementWrapper()
await render(
<StudioAnnouncementsDialog
unseenDocuments={MOCKED_ANNOUNCEMENTS}
announcements={MOCKED_ANNOUNCEMENTS}
onClose={onCloseMock}
mode="card"
/>,
Expand All @@ -169,7 +169,24 @@ describe('StudioAnnouncementsCard', () => {
// Simulate clicking on a link
const link = screen.getByText('Content with a link')
fireEvent.click(link)

expect(mockLog).toHaveBeenCalledTimes(2)
expect(mockLog).toHaveBeenCalledWith(
{
description: 'User viewed the product announcement',
name: 'Product Announcement Viewed',
schema: undefined,
type: 'log',
version: 1,
},
{
announcement_id: 'studioAnnouncement-1',
announcement_title: 'Announcement 1',
origin: 'card',
scrolled_into_view: false,
source: 'studio',
studio_version: '3.57.0',
},
)
expect(mockLog).toHaveBeenCalledWith(
{
description: 'User clicked the link in the product announcement ',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import {afterEach, describe, expect, jest, test} from '@jest/globals'
import {Menu} from '@sanity/ui'
import {fireEvent, render, screen} from '@testing-library/react'
Expand All @@ -11,6 +12,16 @@ import {type StudioAnnouncementDocument} from '../types'
import {useStudioAnnouncements} from '../useStudioAnnouncements'

jest.mock('../useStudioAnnouncements')
jest.mock('@sanity/telemetry/react', () => ({
useTelemetry: jest.fn().mockReturnValue({
log: jest.fn(),
}),
}))

jest.mock('../../../version', () => ({
SANITY_VERSION: '3.57.0',
}))

const MOCKED_ANNOUNCEMENT: StudioAnnouncementDocument = {
_id: 'studioAnnouncement-1',
_type: 'productAnnouncement',
Expand Down Expand Up @@ -79,6 +90,10 @@ describe('StudioAnnouncementsMenuItem', () => {

test('clicking on MenuItem calls onDialogOpen with "all"', async () => {
const onDialogOpenMock = jest.fn()
const mockLog = jest.fn()
const {useTelemetry} = require('@sanity/telemetry/react')
// Set up the mock return value
useTelemetry.mockReturnValue({log: mockLog})

useStudioAnnouncementsMock.mockReturnValue({
studioAnnouncements: [MOCKED_ANNOUNCEMENT],
Expand All @@ -95,5 +110,21 @@ describe('StudioAnnouncementsMenuItem', () => {
fireEvent.click(screen.getByText('Announcements'))

expect(onDialogOpenMock).toHaveBeenCalledWith('help_menu')
expect(mockLog).toHaveBeenCalledTimes(1)
expect(mockLog).toHaveBeenCalledWith(
{
description: 'User clicked the "Whats new" help menu item',
name: 'Whats New Help Menu Item Clicked',
schema: undefined,
type: 'log',
version: 1,
},
{
announcement_id: 'studioAnnouncement-1',
announcement_title: 'Announcement 1',
source: 'studio',
studio_version: '3.57.0',
},
)
})
})

0 comments on commit 8eecf04

Please sign in to comment.