Merge pull request #18071 from overleaf/jdt-bib-events

Bibliography events for 3rd party integrations

GitOrigin-RevId: d8d7f4378d75166481d5265d2e8bef72d75968c3
This commit is contained in:
Jimmy Domagala-Tang 2024-04-23 09:49:11 -07:00 committed by Copybot
parent 898acab307
commit 2c11ad84e0
8 changed files with 99 additions and 15 deletions

View file

@ -18,7 +18,9 @@ const ProjectLocator = require('../Project/ProjectLocator')
const Settings = require('@overleaf/settings') const Settings = require('@overleaf/settings')
const logger = require('@overleaf/logger') const logger = require('@overleaf/logger')
const _ = require('lodash') const _ = require('lodash')
const AnalyticsManager = require('../../../../app/src/Features/Analytics/AnalyticsManager')
const LinkedFilesHandler = require('./LinkedFilesHandler') const LinkedFilesHandler = require('./LinkedFilesHandler')
const { const {
CompileFailedError, CompileFailedError,
UrlFetchFailedError, UrlFetchFailedError,
@ -91,6 +93,11 @@ module.exports = LinkedFilesController = {
if (err != null) { if (err != null) {
return LinkedFilesController.handleError(err, req, res, next) return LinkedFilesController.handleError(err, req, res, next)
} }
if (name.endsWith('.bib')) {
AnalyticsManager.recordEventForUser(userId, 'linked-bib-file', {
integration: provider,
})
}
return res.json({ new_file_id: newFileId }) return res.json({ new_file_id: newFileId })
} }
) )

View file

@ -22,7 +22,10 @@ export default function FileTreeCreateNewDoc() {
event.preventDefault() event.preventDefault()
finishCreatingDoc({ name }) finishCreatingDoc({ name })
eventTracking.sendMB('new-file-created', { method: 'doc' }) eventTracking.sendMB('new-file-created', {
method: 'doc',
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
})
}, },
[finishCreatingDoc, name] [finishCreatingDoc, name]
) )

View file

@ -95,7 +95,10 @@ export default function FileTreeImportFromProject() {
// form submission: create a linked file with this name, from this entity or output file // form submission: create a linked file with this name, from this entity or output file
const handleSubmit: FormEventHandler = event => { const handleSubmit: FormEventHandler = event => {
event.preventDefault() event.preventDefault()
eventTracking.sendMB('new-file-created', { method: 'project' }) eventTracking.sendMB('new-file-created', {
method: 'project',
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
})
if (isOutputFilesMode) { if (isOutputFilesMode) {
finishCreatingLinkedFile({ finishCreatingLinkedFile({

View file

@ -36,7 +36,10 @@ export default function FileTreeImportFromUrl() {
// form submission: create a linked file with this name, from this URL // form submission: create a linked file with this name, from this URL
const handleSubmit = event => { const handleSubmit = event => {
event.preventDefault() event.preventDefault()
eventTracking.sendMB('new-file-created', { method: 'url' }) eventTracking.sendMB('new-file-created', {
method: 'url',
extension: name.split('.').length > 1 ? name.split('.').pop() : '',
})
finishCreatingLinkedFile({ finishCreatingLinkedFile({
name, name,
provider: 'url', provider: 'url',

View file

@ -145,7 +145,13 @@ export default function FileTreeUploadDoc() {
}) })
// broadcast doc metadata after each successful upload // broadcast doc metadata after each successful upload
.on('upload-success', (file, response) => { .on('upload-success', (file, response) => {
eventTracking.sendMB('new-file-created', { method: 'upload' }) eventTracking.sendMB('new-file-created', {
method: 'upload',
extension:
file?.name && file?.name.split('.').length > 1
? file?.name.split('.').pop()
: '',
})
if (response.body.entity_type === 'doc') { if (response.body.entity_type === 'doc') {
window.setTimeout(() => { window.setTimeout(() => {
refreshProjectMetadata(projectId, response.body.entity_id) refreshProjectMetadata(projectId, response.body.entity_id)

View file

@ -1,12 +1,30 @@
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import * as eventTracking from '../../../../infrastructure/event-tracking' import * as eventTracking from '../../../../infrastructure/event-tracking'
import { useProjectContext } from '@/shared/context/project-context'
import { MenuItem } from 'react-bootstrap' import { MenuItem } from 'react-bootstrap'
import { useFileTreeActionable } from '../../contexts/file-tree-actionable' import { useFileTreeActionable } from '../../contexts/file-tree-actionable'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useFileTreeSelectable } from '../../contexts/file-tree-selectable'
import { findInTree } from '../../util/find-in-tree'
function FileTreeItemMenuItems() { function FileTreeItemMenuItems() {
const { t } = useTranslation() const { t } = useTranslation()
const { fileTreeData } = useFileTreeData()
const { selectedEntityIds } = useFileTreeSelectable()
// return the name of the selected file or doc if there is only one selected
const selectedFileName = useMemo(() => {
if (selectedEntityIds.size === 1) {
const [selectedEntityId] = selectedEntityIds
const selectedEntity = findInTree(fileTreeData, selectedEntityId)
return selectedEntity?.entity?.name
}
return null
}, [fileTreeData, selectedEntityIds])
const { const {
canRename, canRename,
canDelete, canDelete,
@ -19,15 +37,24 @@ function FileTreeItemMenuItems() {
downloadPath, downloadPath,
} = useFileTreeActionable() } = useFileTreeActionable()
const createWithAnalytics = () => { const { owner } = useProjectContext()
const downloadWithAnalytics = useCallback(() => {
// we are only interested in downloads of bib files WRT analytics, for the purposes of promoting the tpr integrations
if (selectedFileName?.endsWith('.bib')) {
eventTracking.sendMB('download-bib-file', { projectOwner: owner._id })
}
}, [selectedFileName, owner])
const createWithAnalytics = useCallback(() => {
eventTracking.sendMB('new-file-click', { location: 'file-menu' }) eventTracking.sendMB('new-file-click', { location: 'file-menu' })
startCreatingDocOrFile() startCreatingDocOrFile()
} }, [startCreatingDocOrFile])
const uploadWithAnalytics = () => { const uploadWithAnalytics = useCallback(() => {
eventTracking.sendMB('upload-click', { location: 'file-menu' }) eventTracking.sendMB('upload-click', { location: 'file-menu' })
startUploadingDocOrFile() startUploadingDocOrFile()
} }, [startUploadingDocOrFile])
return ( return (
<> <>
@ -35,7 +62,7 @@ function FileTreeItemMenuItems() {
<MenuItem onClick={startRenaming}>{t('rename')}</MenuItem> <MenuItem onClick={startRenaming}>{t('rename')}</MenuItem>
) : null} ) : null}
{downloadPath ? ( {downloadPath ? (
<MenuItem href={downloadPath} download> <MenuItem href={downloadPath} onClick={downloadWithAnalytics} download>
{t('download')} {t('download')}
</MenuItem> </MenuItem>
) : null} ) : null}

View file

@ -21,6 +21,8 @@ import {
} from '@/features/ide-react/types/file-tree' } from '@/features/ide-react/types/file-tree'
import { debugConsole } from '@/utils/debugging' import { debugConsole } from '@/utils/debugging'
import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view' import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
import { sendMB } from '@/infrastructure/event-tracking'
import { FileRef } from '../../../../../types/file-ref'
const FileTreeOpenContext = createContext< const FileTreeOpenContext = createContext<
| { | {
@ -34,7 +36,7 @@ const FileTreeOpenContext = createContext<
>(undefined) >(undefined)
export const FileTreeOpenProvider: FC = ({ children }) => { export const FileTreeOpenProvider: FC = ({ children }) => {
const { rootDocId } = useProjectContext() const { rootDocId, owner } = useProjectContext()
const { eventEmitter, projectJoined } = useIdeReactContext() const { eventEmitter, projectJoined } = useIdeReactContext()
const { const {
openDocId: openDocWithId, openDocId: openDocWithId,
@ -71,8 +73,14 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
setOpenEntity(selected) setOpenEntity(selected)
if (selected.type === 'doc' && fileTreeReady) { if (selected.type === 'doc' && fileTreeReady) {
openDocWithId(selected.entity._id) openDocWithId(selected.entity._id)
if (selected.entity.name.endsWith('.bib')) {
sendMB('open-bib-file', {
projectOwner: owner._id,
isSampleFile: selected.entity.name === 'sample.bib',
linkedFileProvider: null,
})
}
} }
// Keep openFile scope value in sync with the file tree // Keep openFile scope value in sync with the file tree
const openFile = const openFile =
selected.type === 'fileRef' selected.type === 'fileRef'
@ -80,10 +88,18 @@ export const FileTreeOpenProvider: FC = ({ children }) => {
: null : null
setOpenFile(openFile) setOpenFile(openFile)
if (openFile) { if (openFile) {
if (selected?.entity?.name?.endsWith('.bib')) {
sendMB('open-bib-file', {
projectOwner: owner._id,
isSampleFile: false,
linkedFileProvider: (selected.entity as FileRef).linkedFileData
?.provider,
})
}
window.dispatchEvent(new CustomEvent('file-view:file-opened')) window.dispatchEvent(new CustomEvent('file-view:file-opened'))
} }
}, },
[fileTreeReady, setOpenFile, openDocWithId] [fileTreeReady, setOpenFile, openDocWithId, owner]
) )
const handleFileTreeDelete = useCallback( const handleFileTreeDelete = useCallback(

View file

@ -8,8 +8,12 @@ import { sendMB } from '../../../../infrastructure/event-tracking'
import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper' import ButtonWrapper from '@/features/ui/components/bootstrap-5/wrappers/button-wrapper'
import { bsVersion } from '@/features/utils/bootstrap-5' import { bsVersion } from '@/features/utils/bootstrap-5'
function trackUpgradeClick() { function trackUpgradeClick(integration: string) {
sendMB('settings-upgrade-click') sendMB('settings-upgrade-click', { integration })
}
function trackLinkingClick(integration: string) {
sendMB('link-integration-click', { integration, location: 'Settings' })
} }
type IntegrationLinkingWidgetProps = { type IntegrationLinkingWidgetProps = {
@ -73,6 +77,7 @@ export function IntegrationLinkingWidget({
</div> </div>
<div> <div>
<ActionButton <ActionButton
integration={title}
hasFeature={hasFeature} hasFeature={hasFeature}
linked={linked} linked={linked}
handleUnlinkClick={handleUnlinkClick} handleUnlinkClick={handleUnlinkClick}
@ -81,6 +86,7 @@ export function IntegrationLinkingWidget({
/> />
</div> </div>
<UnlinkConfirmationModal <UnlinkConfirmationModal
integration={title}
show={showModal} show={showModal}
title={unlinkConfirmationTitle} title={unlinkConfirmationTitle}
content={unlinkConfirmationText} content={unlinkConfirmationText}
@ -92,6 +98,7 @@ export function IntegrationLinkingWidget({
} }
type ActionButtonProps = { type ActionButtonProps = {
integration: string
hasFeature?: boolean hasFeature?: boolean
linked?: boolean linked?: boolean
handleUnlinkClick: () => void handleUnlinkClick: () => void
@ -105,6 +112,7 @@ function ActionButton({
handleUnlinkClick, handleUnlinkClick,
linkPath, linkPath,
disabled, disabled,
integration,
}: ActionButtonProps) { }: ActionButtonProps) {
const { t } = useTranslation() const { t } = useTranslation()
if (!hasFeature) { if (!hasFeature) {
@ -112,7 +120,7 @@ function ActionButton({
<ButtonWrapper <ButtonWrapper
variant="primary" variant="primary"
href="/user/subscription/plans" href="/user/subscription/plans"
onClick={trackUpgradeClick} onClick={() => trackUpgradeClick(integration)}
bs3Props={{ bsStyle: null, className: 'btn-primary' }} bs3Props={{ bsStyle: null, className: 'btn-primary' }}
> >
<span className="text-capitalize">{t('upgrade')}</span> <span className="text-capitalize">{t('upgrade')}</span>
@ -152,6 +160,7 @@ function ActionButton({
bs5: 'text-capitalize', bs5: 'text-capitalize',
})} })}
bs3Props={{ bsStyle: null }} bs3Props={{ bsStyle: null }}
onClick={() => trackLinkingClick(integration)}
> >
{t('link')} {t('link')}
</ButtonWrapper> </ButtonWrapper>
@ -164,6 +173,7 @@ function ActionButton({
type UnlinkConfirmModalProps = { type UnlinkConfirmModalProps = {
show: boolean show: boolean
title: string title: string
integration: string
content: string content: string
unlinkPath: string unlinkPath: string
handleHide: () => void handleHide: () => void
@ -172,6 +182,7 @@ type UnlinkConfirmModalProps = {
function UnlinkConfirmationModal({ function UnlinkConfirmationModal({
show, show,
title, title,
integration,
content, content,
unlinkPath, unlinkPath,
handleHide, handleHide,
@ -182,6 +193,13 @@ function UnlinkConfirmationModal({
event.preventDefault() event.preventDefault()
handleHide() handleHide()
} }
const handleConfirm = () => {
sendMB('unlink-integration-click', {
integration,
})
}
return ( return (
<AccessibleModal show={show} onHide={handleHide}> <AccessibleModal show={show} onHide={handleHide}>
<Modal.Header closeButton> <Modal.Header closeButton>
@ -209,6 +227,7 @@ function UnlinkConfirmationModal({
type="submit" type="submit"
variant="danger-ghost" variant="danger-ghost"
bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }} bs3Props={{ bsStyle: null, className: 'btn-danger-ghost' }}
onClick={handleConfirm}
> >
{t('unlink')} {t('unlink')}
</ButtonWrapper> </ButtonWrapper>