Skip to content

Commit

Permalink
feat(telemetry): add telemetry for Trial Dialogs (#5643)
Browse files Browse the repository at this point in the history
* feat: ✨ add telemetry for trial dialogs

* feat: add ability to test dialogs using query params

* fix: 🐛 add spaces between tracking event names

* fix: 🐛 fix bug where incorrect tracking object was being passed when closing popover

* refactor: ♻️ follow naming convention for prop vs internal component functions

* refactor: ♻️ remove mixing of UI types from data types

* fix: camelCase all the strings

* refactor: add comment and toLowerCase handler for dialog id

* chore(core): format files

---------

Co-authored-by: Ash <[email protected]>
  • Loading branch information
drewlyton and juice49 authored Feb 13, 2024
1 parent b999da0 commit ad971f8
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import styled from 'styled-components'
import {Button, Dialog} from '../../../../../ui-components'
import {useColorSchemeValue} from '../../../colorScheme'
import {UpsellDescriptionSerializer} from '../../../upsell'
import {type TrialDialogDismissedInfo} from './__telemetry__/trialDialogEvents.telemetry'
import {type FreeTrialDialog} from './types'

/**
Expand Down Expand Up @@ -39,19 +40,35 @@ const StyledDialog = styled(Dialog)`
`
interface ModalContentProps {
content: FreeTrialDialog
handleClose: () => void
handleOpenNext: () => void
onClose: (action?: TrialDialogDismissedInfo['dialogDismissAction']) => void
onOpenNext: () => void
onOpenUrlCallback: () => void
open: boolean
}

export function DialogContent({handleClose, handleOpenNext, content, open}: ModalContentProps) {
export function DialogContent({
onClose,
onOpenNext,
onOpenUrlCallback,
content,
open,
}: ModalContentProps) {
function handleClose() {
onClose('xClick')
}
function handleClickOutside() {
onClose('outsideClick')
}
function handleCTAClose() {
onClose('ctaClicked')
}
const schemeValue = useColorSchemeValue()
if (!open) return null
return (
<StyledDialog
id="free-trial-modal"
onClose={handleClose}
onClickOutside={handleClose}
onClose={onClose}
onClickOutside={handleClickOutside}
padding={false}
__unstable_hideCloseButton
scheme={schemeValue}
Expand All @@ -74,9 +91,10 @@ export function DialogContent({handleClose, handleOpenNext, content, open}: Moda
target: '_blank',
rel: 'noopener noreferrer',
as: 'a',
onClick: onOpenUrlCallback,
}
: {
onClick: content.ctaButton?.action === 'openNext' ? handleOpenNext : handleClose,
onClick: content.ctaButton?.action === 'openNext' ? onOpenNext : handleCTAClose,
}),
},
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {useTelemetry} from '@sanity/telemetry/react'
import {useCallback, useEffect, useState} from 'react'

import {Popover} from '../../../../../ui-components'
import {useColorSchemeValue} from '../../../colorScheme'
import {
getTrialStage,
TrialDialogCTAClicked,
TrialDialogDismissed,
type TrialDialogDismissedInfo,
TrialDialogViewed,
} from './__telemetry__/trialDialogEvents.telemetry'
import {DialogContent} from './DialogContent'
import {FreeTrialButtonSidebar, FreeTrialButtonTopbar} from './FreeTrialButton'
import {useFreeTrialContext} from './FreeTrialContext'
Expand All @@ -14,6 +22,7 @@ interface FreeTrialProps {
export function FreeTrial({type}: FreeTrialProps) {
const {data, showDialog, showOnLoad, toggleShowContent} = useFreeTrialContext()
const scheme = useColorSchemeValue()
const telemetry = useTelemetry()

// Use callback refs to get the element handle when it's ready/changed
const [ref, setRef] = useState<HTMLButtonElement | null>(null)
Expand All @@ -32,20 +41,90 @@ export function FreeTrial({type}: FreeTrialProps) {
toggleShowContent(false)
}, [toggleShowContent, ref])

const handleClose = useCallback(
(dialogType?: 'modal' | 'popover') => {
return (action?: TrialDialogDismissedInfo['dialogDismissAction']) => {
const dialog = data?.showOnLoad || data?.showOnClick

if (dialog)
telemetry.log(TrialDialogDismissed, {
dialogId: dialog.id,
dialogRevision: dialog._rev,
dialogType,
source: 'studio',
trialDaysLeft: data.daysLeft,
dialogTrialStage: getTrialStage({showOnLoad, dialogId: dialog.id}),
dialogDismissAction: action,
})

toggleDialog()
}
},
[data, toggleDialog, showOnLoad, telemetry],
)

const handleDialogCTAClick = useCallback(
(action?: 'openURL' | 'openNext') => {
return () => {
const dialog = data?.showOnLoad || data?.showOnClick
if (dialog)
telemetry.log(TrialDialogCTAClicked, {
dialogId: dialog.id,
dialogRevision: dialog._rev,
dialogType: 'modal',
source: 'studio',
trialDaysLeft: data.daysLeft,
dialogTrialStage: getTrialStage({showOnLoad, dialogId: dialog.id}),
dialogCtaType: action === 'openURL' ? 'upgrade' : 'learnMore',
})
closeAndReOpen()
}
},
[data, closeAndReOpen, telemetry, showOnLoad],
)

const handlePopoverCTAClick = useCallback(() => {
if (data?.showOnLoad)
telemetry.log(TrialDialogCTAClicked, {
dialogId: data.showOnLoad.id,
dialogRevision: data.showOnLoad._rev,
dialogType: 'popover',
source: 'studio',
trialDaysLeft: data.daysLeft,
dialogTrialStage: getTrialStage({showOnLoad: true, dialogId: data.showOnLoad.id}),
dialogCtaType: 'learnMore',
})
closeAndReOpen()
}, [data?.showOnLoad, data?.daysLeft, closeAndReOpen, telemetry])

const handleOnTrialButtonClick = useCallback(() => {
if (data?.showOnClick)
telemetry.log(TrialDialogViewed, {
dialogId: data.showOnClick.id,
dialogRevision: data.showOnClick._rev,
dialogTrigger: 'fromClick',
dialogType: 'modal',
source: 'studio',
trialDaysLeft: data.daysLeft,
dialogTrialStage: getTrialStage({showOnLoad: true, dialogId: data.showOnClick.id}),
})
closeAndReOpen()
}, [data?.showOnClick, data?.daysLeft, telemetry, closeAndReOpen])

if (!data?.id) return null
const dialogToRender = showOnLoad ? data.showOnLoad : data.showOnClick
if (!dialogToRender) return null

const button =
type === 'sidebar' ? (
<FreeTrialButtonSidebar
toggleShowContent={closeAndReOpen}
toggleShowContent={handleOnTrialButtonClick}
daysLeft={data.daysLeft}
ref={setRef}
/>
) : (
<FreeTrialButtonTopbar
toggleShowContent={closeAndReOpen}
toggleShowContent={handleOnTrialButtonClick}
daysLeft={data.daysLeft}
totalDays={data.totalDays}
ref={setRef}
Expand All @@ -64,8 +143,8 @@ export function FreeTrial({type}: FreeTrialProps) {
content={
<PopoverContent
content={dialogToRender}
handleClose={toggleDialog}
handleOpenNext={closeAndReOpen}
handleClose={handleClose('popover')}
handleOpenNext={handlePopoverCTAClick}
/>
}
>
Expand All @@ -79,8 +158,9 @@ export function FreeTrial({type}: FreeTrialProps) {
{button}
<DialogContent
content={dialogToRender}
handleClose={toggleDialog}
handleOpenNext={closeAndReOpen}
onClose={handleClose('modal')}
onOpenNext={handleDialogCTAClick('openNext')}
onOpenUrlCallback={handleDialogCTAClick('openURL')}
open={showDialog}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import {useTelemetry} from '@sanity/telemetry/react'
import {type ReactNode, useCallback, useEffect, useState} from 'react'
import {useRouter} from 'sanity/router'

import {useClient} from '../../../../hooks'
import {SANITY_VERSION} from '../../../../version'
import {getTrialStage, TrialDialogViewed} from './__telemetry__/trialDialogEvents.telemetry'
import {FreeTrialContext} from './FreeTrialContext'
import {type FreeTrialResponse} from './types'

/**
* @internal
*/
Expand All @@ -16,15 +18,52 @@ export interface FreeTrialProviderProps {
* @internal
*/
export const FreeTrialProvider = ({children}: FreeTrialProviderProps) => {
const router = useRouter()
const [data, setData] = useState<FreeTrialResponse | null>(null)
const [showDialog, setShowDialog] = useState(false)
const [showOnLoad, setShowOnLoad] = useState(false)
const client = useClient({apiVersion: '2023-12-11'})
const telemetry = useTelemetry()

// Whenever showDialog changes, run effect to track
// the dialog view
useEffect(() => {
const dialog = data?.showOnLoad
if (showDialog && showOnLoad && dialog) {
telemetry.log(TrialDialogViewed, {
dialogId: dialog.id,
dialogRevision: dialog._rev,
dialogTrialStage: getTrialStage({showOnLoad, dialogId: dialog.id}),
dialogTrigger: showOnLoad ? 'auto' : 'fromClick',
dialogType: dialog.dialogType,
source: 'studio',
trialDaysLeft: data.daysLeft,
})
}
}, [showDialog, data, showOnLoad, telemetry])

useEffect(() => {
// See if we have any parameters from the current route
// to pass onto our query
const searchParams = new URLSearchParams(router.state._searchParams)

const queryParams = new URLSearchParams()
queryParams.append('studioVersion', SANITY_VERSION)
// Allows us to override the current state of the trial to
// get back certain modals based on the current experience
// can be 'growth-trial', 'growth-trial-ending', or 'post-growth-trial'
const trialState = searchParams.get('trialState')
if (trialState) queryParams.append('trialState', trialState)
// Allows us to set whether we've seen the modals before
// or whether this is our first time seeing them (i.e. show a popup)
const seenBefore = searchParams.get('seenBefore')
if (seenBefore) queryParams.append('seenBefore', seenBefore)
// If we have trialState, query the override endpoint so that we
// get back trial modals for that state
const queryURL = queryParams.get('trialState') ? `/journey/trial/override` : `/journey/trial`
const request = client.observable
.request<FreeTrialResponse | null>({
url: `/journey/trial?studioVersion=${SANITY_VERSION}`,
url: `${queryURL}?${queryParams.toString()}`,
})
.subscribe(
(response) => {
Expand All @@ -42,7 +81,7 @@ export const FreeTrialProvider = ({children}: FreeTrialProviderProps) => {
return () => {
request.unsubscribe()
}
}, [client])
}, [client, router])

const toggleShowContent = useCallback(
(closeAndReOpen = false) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from 'styled-components'

import {Button} from '../../../../../ui-components'
import {UpsellDescriptionSerializer} from '../../../upsell'
import {type TrialDialogDismissedInfo} from './__telemetry__/trialDialogEvents.telemetry'
import {type FreeTrialDialog} from './types'

const Image = styled.img`
Expand All @@ -14,7 +15,7 @@ const Image = styled.img`

interface PopoverContentProps {
content: FreeTrialDialog
handleClose: () => void
handleClose: (action?: TrialDialogDismissedInfo['dialogDismissAction']) => void
handleOpenNext: () => void
}

Expand All @@ -39,7 +40,7 @@ export function PopoverContent({content, handleClose, handleOpenNext}: PopoverCo
mode="bleed"
text={content.secondaryButton.text}
tone="default"
onClick={handleClose}
onClick={() => handleClose('xClick')}
/>
)}
<Button
Expand All @@ -56,7 +57,10 @@ export function PopoverContent({content, handleClose, handleOpenNext}: PopoverCo
as: 'a',
}
: {
onClick: content.ctaButton?.action === 'openNext' ? handleOpenNext : handleClose,
onClick:
content.ctaButton?.action === 'openNext'
? handleOpenNext
: () => handleClose('ctaClicked'),
})}
/>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {defineEvent} from '@sanity/telemetry'

type TrialStage = 'trialStarted' | 'trialActive' | 'trialEndingSoon' | 'trialEnded' | 'postTrial'

type BaseDialogEventAttributes = {
source: 'studio'
trialDaysLeft: number
dialogType: 'modal' | 'popover'
dialogId: string
dialogRevision: string
dialogTrialStage: TrialStage
}

export interface TrialDialogViewedInfo extends BaseDialogEventAttributes {
dialogTrigger: 'fromClick' | 'auto'
}

export const TrialDialogViewed = defineEvent<TrialDialogViewedInfo>({
name: 'Trial Dialog Viewed',
version: 1,
description: 'User viewed a dialog or popover related to free trial',
})

export interface TrialDialogDismissedInfo extends BaseDialogEventAttributes {
dialogDismissAction: 'ctaClicked' | 'xClick' | 'outsideClick'
}

export const TrialDialogDismissed = defineEvent<TrialDialogDismissedInfo>({
name: 'Trial Dialog Dismissed',
version: 1,
description: 'User dismissed a dialog or popover related to free trial',
})

export interface TrialDialogCTAClickedInfo extends BaseDialogEventAttributes {
dialogCtaType: 'upgrade' | 'learnMore'
}

export const TrialDialogCTAClicked = defineEvent<TrialDialogCTAClickedInfo>({
name: 'Trial Dialog CTA Clicked',
version: 1,
description: 'User clicked a CTA in a dialog or popover related to free trial',
})

export function getTrialStage({
showOnLoad,
dialogId,
}: {
showOnLoad: boolean
dialogId: string
}): TrialStage {
// Note: some of the ids in the trial experience studio have uppercase letters
// so the toLowerCase is important here
if (showOnLoad && dialogId.toLowerCase() === 'free-upgrade-popover') return 'trialStarted'
if (showOnLoad && dialogId.toLowerCase() === 'trial-ending-popover') return 'trialEndingSoon'
if (showOnLoad && dialogId.toLowerCase() === 'project-downgraded-to-free') return 'trialEnded'
if (!showOnLoad && dialogId.toLowerCase() === 'after-trial-upgrade') return 'postTrial'
return 'trialActive'
}

0 comments on commit ad971f8

Please sign in to comment.