Implement test to reduce compile timeout to 20 seconds (#14705)

Compile timeout reduction to 20s for treatment users

Co-authored-by: Rebeka <rebeka.dekany@overleaf.com>
GitOrigin-RevId: 54f70fe4b1fc631cef966deb0c1d28c904dd3a44
This commit is contained in:
Thomas 2023-09-18 13:35:43 +02:00 committed by Copybot
parent d04a1d3767
commit 31cb9e336b
22 changed files with 838 additions and 8 deletions

View file

@ -114,6 +114,15 @@ module.exports = CompileController = {
stopOnFirstError, stopOnFirstError,
} }
// temporary override to force the new compile timeout
const forceNewCompileTimeout = req.query.force_new_compile_timeout
if (
forceNewCompileTimeout === 'active' ||
forceNewCompileTimeout === 'changing'
) {
options.forceNewCompileTimeout = forceNewCompileTimeout
}
if (req.body.rootDoc_id) { if (req.body.rootDoc_id) {
options.rootDoc_id = req.body.rootDoc_id options.rootDoc_id = req.body.rootDoc_id
} else if ( } else if (

View file

@ -11,7 +11,11 @@ const { RateLimiter } = require('../../infrastructure/RateLimiter')
const SplitTestHandler = require('../SplitTests/SplitTestHandler') const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const { getAnalyticsIdFromMongoUser } = require('../Analytics/AnalyticsHelper') const { getAnalyticsIdFromMongoUser } = require('../Analytics/AnalyticsHelper')
const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF = new Date('2023-09-18T11:00:00.000Z')
module.exports = CompileManager = { module.exports = CompileManager = {
NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF,
compile(projectId, userId, options = {}, _callback) { compile(projectId, userId, options = {}, _callback) {
const timer = new Metrics.Timer('editor.compile') const timer = new Metrics.Timer('editor.compile')
const callback = function (...args) { const callback = function (...args) {
@ -54,6 +58,16 @@ module.exports = CompileManager = {
const value = limits[key] const value = limits[key]
options[key] = value options[key] = value
} }
if (options.timeout !== 20) {
// temporary override to force the new compile timeout
if (options.forceNewCompileTimeout === 'active') {
options.timeout = 20
} else if (
options.forceNewCompileTimeout === 'changing'
) {
options.timeout = 60
}
}
// Put a lower limit on autocompiles for free users, based on compileGroup // Put a lower limit on autocompiles for free users, based on compileGroup
CompileManager._checkCompileGroupAutoCompileLimit( CompileManager._checkCompileGroupAutoCompileLimit(
options.isAutoCompile, options.isAutoCompile,
@ -149,6 +163,7 @@ module.exports = CompileManager = {
betaProgram: 1, betaProgram: 1,
features: 1, features: 1,
splitTests: 1, splitTests: 1,
signUpDate: 1, // for compile-timeout-20s
}, },
function (err, owner) { function (err, owner) {
if (err) { if (err) {
@ -176,7 +191,28 @@ module.exports = CompileManager = {
limits.compileBackendClass = compileBackendClass limits.compileBackendClass = compileBackendClass
limits.showFasterCompilesFeedbackUI = limits.showFasterCompilesFeedbackUI =
showFasterCompilesFeedbackUI showFasterCompilesFeedbackUI
callback(null, limits) if (compileBackendClass === 'n2d' && limits.timeout <= 60) {
// project owners with faster compiles but with <= 60 compile timeout (default)
// will have a 20s compile timeout
// The compile-timeout-20s split test exists to enable a gradual rollout
SplitTestHandler.getAssignmentForMongoUser(
owner,
'compile-timeout-20s',
(err, assignment) => {
if (err) return callback(err)
if (assignment?.variant === '20s') {
if (
owner.signUpDate > NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF
) {
limits.timeout = 20
}
}
callback(null, limits)
}
)
} else {
callback(null, limits)
}
} }
) )
} }

View file

@ -14,6 +14,10 @@ const Errors = require('../Errors/Errors')
const DocstoreManager = require('../Docstore/DocstoreManager') const DocstoreManager = require('../Docstore/DocstoreManager')
const logger = require('@overleaf/logger') const logger = require('@overleaf/logger')
const { expressify } = require('../../util/promises') const { expressify } = require('../../util/promises')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
const {
NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF,
} = require('../Compile/CompileManager')
module.exports = { module.exports = {
joinProject: expressify(joinProject), joinProject: expressify(joinProject),
@ -67,6 +71,30 @@ async function joinProject(req, res, next) {
if (!project) { if (!project) {
return res.sendStatus(403) return res.sendStatus(403)
} }
// Compile timeout 20s test
if (project.features?.compileTimeout <= 60) {
const compileAssignment =
await SplitTestHandler.promises.getAssignmentForMongoUser(
project.owner._id,
'compile-backend-class-n2d'
)
if (compileAssignment?.variant === 'n2d') {
const timeoutAssignment =
await SplitTestHandler.promises.getAssignmentForMongoUser(
project.owner._id,
'compile-timeout-20s'
)
if (timeoutAssignment?.variant === '20s') {
if (project.owner.signUpDate > NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF) {
// New users will see a 10s warning and compile fail at 20s
project.showNewCompileTimeoutUI = 'active'
} else {
// Older users aren't limited to 20s, but will see a notice of upcoming changes if compile >20s
project.showNewCompileTimeoutUI = 'changing'
}
}
}
}
// Hide sensitive data if the user is restricted // Hide sensitive data if the user is restricted
if (isRestrictedUser) { if (isRestrictedUser) {
project.owner = { _id: project.owner._id } project.owner = { _id: project.owner._id }

View file

@ -3,6 +3,7 @@
"1_4_width": "", "1_4_width": "",
"3_4_width": "", "3_4_width": "",
"a_custom_size_has_been_used_in_the_latex_code": "", "a_custom_size_has_been_used_in_the_latex_code": "",
"a_fatal_compile_error_that_completely_blocks_compilation": "",
"a_file_with_that_name_already_exists_and_will_be_overriden": "", "a_file_with_that_name_already_exists_and_will_be_overriden": "",
"a_more_comprehensive_list_of_keyboard_shortcuts": "", "a_more_comprehensive_list_of_keyboard_shortcuts": "",
"about_to_archive_projects": "", "about_to_archive_projects": "",
@ -63,6 +64,7 @@
"also": "", "also": "",
"an_email_has_already_been_sent_to": "", "an_email_has_already_been_sent_to": "",
"an_error_occurred_when_verifying_the_coupon_code": "", "an_error_occurred_when_verifying_the_coupon_code": "",
"and_you_can_upgrade_for_plenty_more_compile_time": "",
"anonymous": "", "anonymous": "",
"anyone_with_link_can_edit": "", "anyone_with_link_can_edit": "",
"anyone_with_link_can_view": "", "anyone_with_link_can_view": "",
@ -102,6 +104,7 @@
"browser": "", "browser": "",
"bulk_accept_confirm": "", "bulk_accept_confirm": "",
"bulk_reject_confirm": "", "bulk_reject_confirm": "",
"but_note_that_free_compile_timeout_limit_will_be_reduced_on_x_date": "",
"by_subscribing_you_agree_to_our_terms_of_service": "", "by_subscribing_you_agree_to_our_terms_of_service": "",
"can_edit": "", "can_edit": "",
"can_link_institution_email_acct_to_institution_acct": "", "can_link_institution_email_acct_to_institution_acct": "",
@ -167,6 +170,7 @@
"comment": "", "comment": "",
"commit": "", "commit": "",
"common": "", "common": "",
"common_causes_of_compile_timeouts_are": "",
"commons_plan_tooltip": "", "commons_plan_tooltip": "",
"compact": "", "compact": "",
"company_name": "", "company_name": "",
@ -177,6 +181,7 @@
"compile_larger_projects": "", "compile_larger_projects": "",
"compile_mode": "", "compile_mode": "",
"compile_terminated_by_user": "", "compile_terminated_by_user": "",
"compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile": "",
"compiler": "", "compiler": "",
"compiling": "", "compiling": "",
"configure_sso": "", "configure_sso": "",
@ -311,6 +316,7 @@
"emails_and_affiliations_explanation": "", "emails_and_affiliations_explanation": "",
"emails_and_affiliations_title": "", "emails_and_affiliations_title": "",
"enable_managed_users": "", "enable_managed_users": "",
"enable_stop_on_first_error_under_recompile_dropdown_menu": "",
"enabled_managed_users_set_up_sso": "", "enabled_managed_users_set_up_sso": "",
"enabling": "", "enabling": "",
"end_of_document": "", "end_of_document": "",
@ -549,6 +555,7 @@
"labs_program_already_participating": "", "labs_program_already_participating": "",
"labs_program_benefits": "<0></0>", "labs_program_benefits": "<0></0>",
"labs_program_not_participating": "", "labs_program_not_participating": "",
"large_or_high-resolution_images_taking_too_long": "",
"last_active": "", "last_active": "",
"last_active_description": "", "last_active_description": "",
"last_modified": "", "last_modified": "",
@ -565,6 +572,7 @@
"learn_more": "", "learn_more": "",
"learn_more_about_link_sharing": "", "learn_more_about_link_sharing": "",
"learn_more_about_managed_users": "", "learn_more_about_managed_users": "",
"learn_more_about_other_causes_of_compile_timeouts": "",
"leave": "", "leave": "",
"leave_any_group_subscriptions": "", "leave_any_group_subscriptions": "",
"leave_group": "", "leave_group": "",
@ -737,6 +745,7 @@
"organize_projects": "", "organize_projects": "",
"other_logs_and_files": "", "other_logs_and_files": "",
"other_output_files": "", "other_output_files": "",
"other_ways_to_prevent_compile_timeouts": "",
"output_file": "", "output_file": "",
"overall_theme": "", "overall_theme": "",
"overleaf": "", "overleaf": "",
@ -789,6 +798,7 @@
"please_select_a_project": "", "please_select_a_project": "",
"please_select_an_output_file": "", "please_select_an_output_file": "",
"please_set_main_file": "", "please_set_main_file": "",
"plus_additional_collaborators_document_history_track_changes_and_more": "",
"plus_more": "", "plus_more": "",
"plus_upgraded_accounts_receive": "", "plus_upgraded_accounts_receive": "",
"postal_code": "", "postal_code": "",
@ -841,6 +851,8 @@
"react_history_tutorial_content": "", "react_history_tutorial_content": "",
"react_history_tutorial_title": "", "react_history_tutorial_title": "",
"reactivate_subscription": "", "reactivate_subscription": "",
"read_more_about_fix_prevent_timeout": "",
"read_more_about_free_compile_timeouts_servers": "",
"read_only": "", "read_only": "",
"read_only_token": "", "read_only_token": "",
"read_write_token": "", "read_write_token": "",
@ -1022,8 +1034,10 @@
"sso_config_prop_help_user_id": "", "sso_config_prop_help_user_id": "",
"sso_explanation": "", "sso_explanation": "",
"sso_link_error": "", "sso_link_error": "",
"start_a_free_trial": "",
"start_by_adding_your_email": "", "start_by_adding_your_email": "",
"start_free_trial": "", "start_free_trial": "",
"start_free_trial_without_exclamation": "",
"stop_compile": "", "stop_compile": "",
"stop_on_first_error": "", "stop_on_first_error": "",
"stop_on_first_error_enabled_description": "", "stop_on_first_error_enabled_description": "",
@ -1065,6 +1079,8 @@
"tc_switch_everyone_tip": "", "tc_switch_everyone_tip": "",
"tc_switch_guests_tip": "", "tc_switch_guests_tip": "",
"tc_switch_user_tip": "", "tc_switch_user_tip": "",
"tell_the_project_owner_and_ask_them_to_upgrade": "",
"tell_the_project_owner_to_upgrade_plan_for_more_compile_time": "",
"template_approved_by_publisher": "", "template_approved_by_publisher": "",
"template_description": "", "template_description": "",
"template_title_taken_from_project_title": "", "template_title_taken_from_project_title": "",
@ -1088,6 +1104,8 @@
"this_could_be_because_we_cant_support_some_elements_of_the_table": "", "this_could_be_because_we_cant_support_some_elements_of_the_table": "",
"this_field_is_required": "", "this_field_is_required": "",
"this_grants_access_to_features_2": "", "this_grants_access_to_features_2": "",
"this_project_compiled_but_soon_might_not": "",
"this_project_exceeded_compile_timeout_limit_on_free_plan": "",
"this_project_is_public": "", "this_project_is_public": "",
"this_project_is_public_read_only": "", "this_project_is_public_read_only": "",
"this_project_will_appear_in_your_dropbox_folder_at": "", "this_project_will_appear_in_your_dropbox_folder_at": "",
@ -1112,6 +1130,7 @@
"too_many_requests": "", "too_many_requests": "",
"too_many_search_results": "", "too_many_search_results": "",
"too_recently_compiled": "", "too_recently_compiled": "",
"took_a_while": "",
"toolbar_add_comment": "", "toolbar_add_comment": "",
"toolbar_bullet_list": "", "toolbar_bullet_list": "",
"toolbar_choose_section_heading_level": "", "toolbar_choose_section_heading_level": "",
@ -1203,7 +1222,9 @@
"updating": "", "updating": "",
"upgrade": "", "upgrade": "",
"upgrade_cc_btn": "", "upgrade_cc_btn": "",
"upgrade_for_12x_more_compile_time": "",
"upgrade_for_longer_compiles": "", "upgrade_for_longer_compiles": "",
"upgrade_for_plenty_more_compile_time": "",
"upgrade_now": "", "upgrade_now": "",
"upgrade_to_get_feature": "", "upgrade_to_get_feature": "",
"upgrade_to_track_changes": "", "upgrade_to_track_changes": "",
@ -1275,10 +1296,12 @@
"you_have_added_x_of_group_size_y": "", "you_have_added_x_of_group_size_y": "",
"you_have_been_invited_to_transfer_management_of_your_account": "", "you_have_been_invited_to_transfer_management_of_your_account": "",
"you_have_been_invited_to_transfer_management_of_your_account_to": "", "you_have_been_invited_to_transfer_management_of_your_account_to": "",
"you_may_be_able_to_prevent_a_compile_timeout": "",
"you_will_be_able_to_reassign_subscription": "", "you_will_be_able_to_reassign_subscription": "",
"youll_get_best_results_in_visual_but_can_be_used_in_source": "", "youll_get_best_results_in_visual_but_can_be_used_in_source": "",
"your_affiliation_is_confirmed": "", "your_affiliation_is_confirmed": "",
"your_browser_does_not_support_this_feature": "", "your_browser_does_not_support_this_feature": "",
"your_compile_timed_out": "",
"your_git_access_info": "", "your_git_access_info": "",
"your_git_access_info_bullet_1": "", "your_git_access_info_bullet_1": "",
"your_git_access_info_bullet_2": "", "your_git_access_info_bullet_2": "",
@ -1290,6 +1313,9 @@
"your_new_plan": "", "your_new_plan": "",
"your_plan": "", "your_plan": "",
"your_plan_is_changing_at_term_end": "", "your_plan_is_changing_at_term_end": "",
"your_project_compiled_but_soon_might_not": "",
"your_project_exceeded_compile_timeout_limit_on_free_plan": "",
"your_project_near_compile_timeout_limit": "",
"your_projects": "", "your_projects": "",
"your_subscription": "", "your_subscription": "",
"your_subscription_has_expired": "", "your_subscription_has_expired": "",

View file

@ -29,7 +29,7 @@ function CompileTimeWarning() {
return return
} }
setDisplayStatus({ lastDisplayTime: Date.now(), dismissed: false }) setDisplayStatus({ lastDisplayTime: Date.now(), dismissed: false })
eventTracking.sendMB('compile-time-warning-displayed', {}) eventTracking.sendMB('compile-time-warning-displayed', { time: 30 })
} }
}, [showCompileTimeWarning, displayStatus, setDisplayStatus]) }, [showCompileTimeWarning, displayStatus, setDisplayStatus])
@ -40,6 +40,7 @@ function CompileTimeWarning() {
const closeWarning = useCallback(() => { const closeWarning = useCallback(() => {
eventTracking.sendMB('compile-time-warning-dismissed', { eventTracking.sendMB('compile-time-warning-dismissed', {
'time-since-displayed': getTimeSinceDisplayed(), 'time-since-displayed': getTimeSinceDisplayed(),
time: 30,
}) })
setShowCompileTimeWarning(false) setShowCompileTimeWarning(false)
setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true })) setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true }))
@ -48,6 +49,7 @@ function CompileTimeWarning() {
const handleUpgradeClick = useCallback(() => { const handleUpgradeClick = useCallback(() => {
eventTracking.sendMB('compile-time-warning-upgrade-click', { eventTracking.sendMB('compile-time-warning-upgrade-click', {
'time-since-displayed': getTimeSinceDisplayed(), 'time-since-displayed': getTimeSinceDisplayed(),
time: 30,
}) })
setShowCompileTimeWarning(false) setShowCompileTimeWarning(false)
setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true })) setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true }))

View file

@ -0,0 +1,250 @@
import { memo, useCallback, useEffect, useState, useMemo } from 'react'
import * as eventTracking from '../../../infrastructure/event-tracking'
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context'
import usePersistedState from '../../../shared/hooks/use-persisted-state'
import Notification from '../../../shared/components/notification'
import { Trans, useTranslation } from 'react-i18next'
import StartFreeTrialButton from '@/shared/components/start-free-trial-button'
function CompileTimeoutMessages() {
const {
showNewCompileTimeoutUI,
isProjectOwner,
deliveryLatencies,
compiling,
showLogs,
error,
} = useDetachCompileContext()
const { t } = useTranslation()
const [showWarning, setShowWarning] = useState(false)
const [showChangingSoon, setShowChangingSoon] = useState(false)
const [dismissedUntilWarning, setDismissedUntilWarning] = usePersistedState<
Date | undefined
>(`has-dismissed-10s-compile-time-warning-until`)
const segmentation = useMemo(() => {
return {
newCompileTimeout: showNewCompileTimeoutUI || 'control',
isProjectOwner,
}
}, [showNewCompileTimeoutUI, isProjectOwner])
const handleNewCompile = useCallback(
compileTime => {
setShowWarning(false)
setShowChangingSoon(false)
if (compileTime > 20000) {
if (showNewCompileTimeoutUI === 'changing') {
setShowChangingSoon(true)
eventTracking.sendMB('compile-time-warning-displayed', {
time: 20,
...segmentation,
})
}
} else if (compileTime > 10000) {
setShowChangingSoon(false)
if (
(isProjectOwner && showNewCompileTimeoutUI === 'active') ||
showNewCompileTimeoutUI === 'changing'
) {
if (
!dismissedUntilWarning ||
new Date(dismissedUntilWarning) < new Date()
) {
setShowWarning(true)
eventTracking.sendMB('compile-time-warning-displayed', {
time: 10,
...segmentation,
})
}
} else {
eventTracking.sendMB('compile-time-warning-would-display', {
time: 10,
...segmentation,
})
}
}
},
[
isProjectOwner,
showNewCompileTimeoutUI,
dismissedUntilWarning,
segmentation,
]
)
const handleDismissWarning = useCallback(() => {
eventTracking.sendMB('compile-time-warning-dismissed', {
time: 10,
...segmentation,
})
setShowWarning(false)
const until = new Date()
until.setDate(until.getDate() + 1) // 1 day
setDismissedUntilWarning(until)
}, [setDismissedUntilWarning, segmentation])
const handleDismissChangingSoon = useCallback(() => {
eventTracking.sendMB('compile-time-warning-dismissed', {
time: 20,
...segmentation,
})
}, [segmentation])
useEffect(() => {
if (compiling || error || showLogs) return
window.sl_console.log(
`[compileTimeout] compiledTimeServerE2E ${
deliveryLatencies.compileTimeServerE2E / 1000
}s`
)
handleNewCompile(deliveryLatencies.compileTimeServerE2E)
}, [compiling, error, showLogs, deliveryLatencies, handleNewCompile])
if (!window.ExposedSettings.enableSubscriptions) {
return null
}
if (compiling || error || showLogs) {
return null
}
if (!showWarning && !showChangingSoon) {
return null
}
function sendInfoClickEvent() {
eventTracking.sendMB('paywall-info-click', {
'paywall-type': 'compile-time-warning',
content: 'blog',
})
}
const compileTimeoutBlogLinks = [
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
<a
aria-label={t('read_more_about_free_compile_timeouts_servers')}
href="/blog/changes-to-free-compile-timeouts-and-servers"
key="compileTimeoutBlogLink1"
rel="noopener noreferrer"
target="_blank"
onClick={sendInfoClickEvent}
/>,
/* eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key */
<a
aria-label={t('read_more_about_fix_prevent_timeout')}
href="/learn/how-to/Fixing_and_preventing_compile_timeouts"
key="compileTimeoutBlogLink2"
target="_blank"
rel="noopener noreferrer"
/>,
]
// if showWarning is true then the 10s warning is shown
// and if showChangingSoon is true then the 20s-60s should show
return (
<div>
{showWarning && isProjectOwner && (
<Notification
action={
<StartFreeTrialButton
source="compile-time-warning-new-10s"
buttonProps={{
className: 'btn-secondary-compile-timeout-override',
}}
>
{t('start_free_trial_without_exclamation')}
</StartFreeTrialButton>
}
ariaLive="polite"
content={
<div>
<div>
<span>
<Trans i18nKey="your_project_near_compile_timeout_limit" />
</span>
</div>
{showNewCompileTimeoutUI === 'active' ? (
<>
<strong>
<Trans i18nKey="upgrade_for_12x_more_compile_time" />
</strong>
{'. '}
</>
) : (
<strong>
<Trans i18nKey="upgrade_for_plenty_more_compile_time" />
</strong>
)}
</div>
}
type="warning"
title={t('took_a_while')}
isActionBelowContent
isDismissible
onDismiss={handleDismissWarning}
/>
)}
{showChangingSoon &&
(isProjectOwner ? (
<Notification
action={
<StartFreeTrialButton
source="compile-time-warning-new-changing-soon"
buttonProps={{
className: 'btn-secondary-compile-timeout-override',
}}
>
{t('start_free_trial_without_exclamation')}
</StartFreeTrialButton>
}
ariaLive="polite"
content={
<div>
<p>
<Trans
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
components={compileTimeoutBlogLinks}
values={{ date: 'October 6 2023' }}
/>{' '}
<Trans i18nKey="and_you_can_upgrade_for_plenty_more_compile_time" />
</p>
</div>
}
title={t('your_project_compiled_but_soon_might_not')}
type="warning"
isActionBelowContent
isDismissible
onDismiss={handleDismissChangingSoon}
/>
) : (
<Notification
ariaLive="polite"
content={
<div>
<p>
<Trans
i18nKey="compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile"
components={compileTimeoutBlogLinks}
values={{ date: 'October 6 2023' }}
/>
</p>
<p className="row-spaced">
<Trans i18nKey="tell_the_project_owner_to_upgrade_plan_for_more_compile_time" />
</p>
</div>
}
title={t('this_project_compiled_but_soon_might_not')}
type="warning"
isDismissible
onDismiss={handleDismissChangingSoon}
/>
))}
</div>
)
}
export default memo(CompileTimeoutMessages)

View file

@ -4,6 +4,7 @@ import classnames from 'classnames'
import PdfValidationIssue from './pdf-validation-issue' import PdfValidationIssue from './pdf-validation-issue'
import StopOnFirstErrorPrompt from './stop-on-first-error-prompt' import StopOnFirstErrorPrompt from './stop-on-first-error-prompt'
import TimeoutUpgradePrompt from './timeout-upgrade-prompt' import TimeoutUpgradePrompt from './timeout-upgrade-prompt'
import TimeoutUpgradePromptNew from './timeout-upgrade-prompt-new'
import PdfPreviewError from './pdf-preview-error' import PdfPreviewError from './pdf-preview-error'
import PdfClearCacheButton from './pdf-clear-cache-button' import PdfClearCacheButton from './pdf-clear-cache-button'
import PdfDownloadFilesButton from './pdf-download-files-button' import PdfDownloadFilesButton from './pdf-download-files-button'
@ -23,6 +24,7 @@ function PdfLogsViewer() {
validationIssues, validationIssues,
showLogs, showLogs,
stoppedOnFirstError, stoppedOnFirstError,
showNewCompileTimeoutUI,
} = useCompileContext() } = useCompileContext()
const { t } = useTranslation() const { t } = useTranslation()
@ -34,9 +36,14 @@ function PdfLogsViewer() {
{stoppedOnFirstError && <StopOnFirstErrorPrompt />} {stoppedOnFirstError && <StopOnFirstErrorPrompt />}
{error && <PdfPreviewError error={error} />} {showNewCompileTimeoutUI && error === 'timedout' ? (
<TimeoutUpgradePromptNew />
{error === 'timedout' && <TimeoutUpgradePrompt />} ) : (
<>
{error && <PdfPreviewError error={error} />}
{error === 'timedout' && <TimeoutUpgradePrompt />}
</>
)}
{validationIssues && {validationIssues &&
Object.entries(validationIssues).map(([name, issue]) => ( Object.entries(validationIssues).map(([name, issue]) => (

View file

@ -8,9 +8,10 @@ import { useDetachCompileContext as useCompileContext } from '../../../shared/co
import FasterCompilesFeedback from './faster-compiles-feedback' import FasterCompilesFeedback from './faster-compiles-feedback'
import { PdfPreviewMessages } from './pdf-preview-messages' import { PdfPreviewMessages } from './pdf-preview-messages'
import CompileTimeWarning from './compile-time-warning' import CompileTimeWarning from './compile-time-warning'
import CompileTimeoutMessages from './compile-timeout-messages'
function PdfPreviewPane() { function PdfPreviewPane() {
const { pdfUrl } = useCompileContext() const { pdfUrl, showNewCompileTimeoutUI } = useCompileContext()
const classes = classNames('pdf', 'full-size', { const classes = classNames('pdf', 'full-size', {
'pdf-empty': !pdfUrl, 'pdf-empty': !pdfUrl,
}) })
@ -18,7 +19,11 @@ function PdfPreviewPane() {
<div className={classes}> <div className={classes}>
<PdfHybridPreviewToolbar /> <PdfHybridPreviewToolbar />
<PdfPreviewMessages> <PdfPreviewMessages>
<CompileTimeWarning /> {showNewCompileTimeoutUI ? (
<CompileTimeoutMessages />
) : (
<CompileTimeWarning />
)}
</PdfPreviewMessages> </PdfPreviewMessages>
<Suspense fallback={<LoadingPreview />}> <Suspense fallback={<LoadingPreview />}>
<div className="pdf-viewer"> <div className="pdf-viewer">

View file

@ -0,0 +1,241 @@
import { Trans, useTranslation } from 'react-i18next'
import { useDetachCompileContext } from '../../../shared/context/detach-compile-context'
import StartFreeTrialButton from '../../../shared/components/start-free-trial-button'
import { memo, useCallback } from 'react'
import PdfLogEntry from './pdf-log-entry'
import { useStopOnFirstError } from '../../../shared/hooks/use-stop-on-first-error'
import { Button } from 'react-bootstrap'
import * as eventTracking from '../../../infrastructure/event-tracking'
function TimeoutUpgradePromptNew() {
const {
startCompile,
lastCompileOptions,
setAnimateCompileDropdownArrow,
showNewCompileTimeoutUI,
isProjectOwner,
} = useDetachCompileContext()
const { enableStopOnFirstError } = useStopOnFirstError({
eventSource: 'timeout-new',
})
const handleEnableStopOnFirstErrorClick = useCallback(() => {
enableStopOnFirstError()
startCompile({ stopOnFirstError: true })
setAnimateCompileDropdownArrow(true)
}, [enableStopOnFirstError, startCompile, setAnimateCompileDropdownArrow])
if (!window.ExposedSettings.enableSubscriptions) {
return null
}
const compileTimeChanging = showNewCompileTimeoutUI === 'changing'
return (
<>
<CompileTimeout
compileTimeChanging={compileTimeChanging}
isProjectOwner={isProjectOwner}
/>
<PreventTimeoutHelpMessage
compileTimeChanging={compileTimeChanging}
handleEnableStopOnFirstErrorClick={handleEnableStopOnFirstErrorClick}
lastCompileOptions={lastCompileOptions}
/>
</>
)
}
type CompileTimeoutProps = {
compileTimeChanging?: boolean
isProjectOwner: boolean
}
function CompileTimeout({
compileTimeChanging,
isProjectOwner,
}: CompileTimeoutProps) {
const { t } = useTranslation()
return (
<PdfLogEntry
headerTitle={t('your_compile_timed_out')}
formattedContent={
<>
<p>
{isProjectOwner
? t('your_project_exceeded_compile_timeout_limit_on_free_plan')
: t('this_project_exceeded_compile_timeout_limit_on_free_plan')}
</p>
{isProjectOwner ? (
<p>
{compileTimeChanging ? (
<>
<strong>{t('upgrade_for_plenty_more_compile_time')}</strong>{' '}
{t(
'plus_additional_collaborators_document_history_track_changes_and_more'
)}
</>
) : (
<>
<strong>
<Trans i18nKey="upgrade_for_12x_more_compile_time" />
</strong>{' '}
<Trans i18nKey="plus_additional_collaborators_document_history_track_changes_and_more" />
</>
)}
</p>
) : (
<Trans
i18nKey="tell_the_project_owner_and_ask_them_to_upgrade"
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
)}
{isProjectOwner && (
<p className="text-center">
<StartFreeTrialButton
source={
compileTimeChanging
? 'compile-timeout-new-changing'
: 'compile-timeout-new-active'
}
buttonProps={{
bsStyle: 'success',
className: 'row-spaced-small',
block: true,
}}
>
{t('start_a_free_trial')}
</StartFreeTrialButton>
</p>
)}
</>
}
entryAriaLabel={t('your_compile_timed_out')}
level="error"
/>
)
}
type PreventTimeoutHelpMessageProps = {
compileTimeChanging?: boolean
lastCompileOptions: any
handleEnableStopOnFirstErrorClick: () => void
}
function PreventTimeoutHelpMessage({
compileTimeChanging,
lastCompileOptions,
handleEnableStopOnFirstErrorClick,
}: PreventTimeoutHelpMessageProps) {
const { t } = useTranslation()
function sendInfoClickEvent() {
eventTracking.sendMB('paywall-info-click', {
'paywall-type': 'compile-timeout',
content: 'blog',
})
}
return (
<PdfLogEntry
headerTitle={t('other_ways_to_prevent_compile_timeouts')}
formattedContent={
<>
<p>
{t('you_may_be_able_to_prevent_a_compile_timeout')}
{compileTimeChanging && (
<>
{' '}
<Trans
i18nKey="but_note_that_free_compile_timeout_limit_will_be_reduced_on_x_date"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a
aria-label={t(
'read_more_about_free_compile_timeouts_servers'
)}
href="/blog/changes-to-free-compile-timeouts-and-servers"
rel="noopener noreferrer"
target="_blank"
onClick={sendInfoClickEvent}
/>,
]}
values={{ date: 'October 6 2023' }}
/>
</>
)}
</p>
<p>{t('common_causes_of_compile_timeouts_are')}:</p>
<ul>
<li>
<Trans
i18nKey="large_or_high-resolution_images_taking_too_long"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a
href="/learn/how-to/Optimising_very_large_image_files"
rel="noopener noreferrer"
target="_blank"
/>,
]}
/>
</li>
<li>
<Trans
i18nKey="a_fatal_compile_error_that_completely_blocks_compilation"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a
href="/learn/how-to/Fixing_and_preventing_compile_timeouts#Step_3:_Assess_your_project_for_time-consuming_tasks_and_fatal_errors"
rel="noopener noreferrer"
target="_blank"
/>,
]}
/>
{!lastCompileOptions.stopOnFirstError && (
<>
{' '}
<Trans
i18nKey="enable_stop_on_first_error_under_recompile_dropdown_menu"
components={[
// eslint-disable-next-line react/jsx-key
<Button
bsSize="xs"
bsStyle="info-ghost-inline"
onClick={handleEnableStopOnFirstErrorClick}
/>,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>{' '}
</>
)}
</li>
</ul>
<p>
<Trans
i18nKey="learn_more_about_other_causes_of_compile_timeouts"
components={[
// eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
<a
href="/learn/how-to/Fixing_and_preventing_compile_timeouts"
rel="noopener noreferrer"
target="_blank"
/>,
]}
/>
</p>
</>
}
entryAriaLabel={t('other_ways_to_prevent_compile_timeouts')}
level="raw"
/>
)
}
export default memo(TimeoutUpgradePromptNew)

View file

@ -184,6 +184,14 @@ export default class DocumentCompiler {
params.file_line_errors = 'true' params.file_line_errors = 'true'
} }
// temporary override to force the new compile timeout
const newCompileTimeoutOverride = new URLSearchParams(
window.location.search
).get('force_new_compile_timeout')
if (newCompileTimeoutOverride) {
params.set('force_new_compile_timeout', newCompileTimeoutOverride)
}
return params return params
} }

View file

@ -34,6 +34,7 @@ export function DetachCompileProvider({ children }) {
fileList: _fileList, fileList: _fileList,
hasChanges: _hasChanges, hasChanges: _hasChanges,
highlights: _highlights, highlights: _highlights,
isProjectOwner: _isProjectOwner,
lastCompileOptions: _lastCompileOptions, lastCompileOptions: _lastCompileOptions,
logEntries: _logEntries, logEntries: _logEntries,
logEntryAnnotations: _logEntryAnnotations, logEntryAnnotations: _logEntryAnnotations,
@ -55,6 +56,7 @@ export function DetachCompileProvider({ children }) {
setStopOnValidationError: _setStopOnValidationError, setStopOnValidationError: _setStopOnValidationError,
showLogs: _showLogs, showLogs: _showLogs,
showCompileTimeWarning: _showCompileTimeWarning, showCompileTimeWarning: _showCompileTimeWarning,
showNewCompileTimeoutUI: _showNewCompileTimeoutUI,
showFasterCompilesFeedbackUI: _showFasterCompilesFeedbackUI, showFasterCompilesFeedbackUI: _showFasterCompilesFeedbackUI,
stopOnFirstError: _stopOnFirstError, stopOnFirstError: _stopOnFirstError,
stopOnValidationError: _stopOnValidationError, stopOnValidationError: _stopOnValidationError,
@ -135,6 +137,12 @@ export function DetachCompileProvider({ children }) {
'detacher', 'detacher',
'detached' 'detached'
) )
const [isProjectOwner] = useDetachStateWatcher(
'isProjectOwner',
_isProjectOwner,
'detacher',
'detached'
)
const [lastCompileOptions] = useDetachStateWatcher( const [lastCompileOptions] = useDetachStateWatcher(
'lastCompileOptions', 'lastCompileOptions',
_lastCompileOptions, _lastCompileOptions,
@ -189,6 +197,12 @@ export function DetachCompileProvider({ children }) {
'detacher', 'detacher',
'detached' 'detached'
) )
const [showNewCompileTimeoutUI] = useDetachStateWatcher(
'showNewCompileTimeoutUI',
_showNewCompileTimeoutUI,
'detacher',
'detached'
)
const [showFasterCompilesFeedbackUI] = useDetachStateWatcher( const [showFasterCompilesFeedbackUI] = useDetachStateWatcher(
'showFasterCompilesFeedbackUI', 'showFasterCompilesFeedbackUI',
_showFasterCompilesFeedbackUI, _showFasterCompilesFeedbackUI,
@ -384,6 +398,7 @@ export function DetachCompileProvider({ children }) {
fileList, fileList,
hasChanges, hasChanges,
highlights, highlights,
isProjectOwner,
lastCompileOptions, lastCompileOptions,
logEntryAnnotations, logEntryAnnotations,
logEntries, logEntries,
@ -409,6 +424,7 @@ export function DetachCompileProvider({ children }) {
setStopOnValidationError, setStopOnValidationError,
showLogs, showLogs,
showCompileTimeWarning, showCompileTimeWarning,
showNewCompileTimeoutUI,
showFasterCompilesFeedbackUI, showFasterCompilesFeedbackUI,
startCompile, startCompile,
stopCompile, stopCompile,
@ -437,6 +453,7 @@ export function DetachCompileProvider({ children }) {
fileList, fileList,
hasChanges, hasChanges,
highlights, highlights,
isProjectOwner,
lastCompileOptions, lastCompileOptions,
logEntryAnnotations, logEntryAnnotations,
logEntries, logEntries,
@ -460,6 +477,7 @@ export function DetachCompileProvider({ children }) {
setStopOnValidationError, setStopOnValidationError,
showCompileTimeWarning, showCompileTimeWarning,
showLogs, showLogs,
showNewCompileTimeoutUI,
showFasterCompilesFeedbackUI, showFasterCompilesFeedbackUI,
startCompile, startCompile,
stopCompile, stopCompile,

View file

@ -66,6 +66,7 @@ export const CompileContextPropTypes = {
setStopOnValidationError: PropTypes.func.isRequired, setStopOnValidationError: PropTypes.func.isRequired,
showCompileTimeWarning: PropTypes.bool.isRequired, showCompileTimeWarning: PropTypes.bool.isRequired,
showLogs: PropTypes.bool.isRequired, showLogs: PropTypes.bool.isRequired,
showNewCompileTimeoutUI: PropTypes.string,
showFasterCompilesFeedbackUI: PropTypes.bool.isRequired, showFasterCompilesFeedbackUI: PropTypes.bool.isRequired,
stopOnFirstError: PropTypes.bool.isRequired, stopOnFirstError: PropTypes.bool.isRequired,
stopOnValidationError: PropTypes.bool.isRequired, stopOnValidationError: PropTypes.bool.isRequired,
@ -84,7 +85,11 @@ export function LocalCompileProvider({ children }) {
const { hasPremiumCompile, isProjectOwner } = useEditorContext() const { hasPremiumCompile, isProjectOwner } = useEditorContext()
const { _id: projectId, rootDocId } = useProjectContext() const {
_id: projectId,
rootDocId,
showNewCompileTimeoutUI,
} = useProjectContext()
const { pdfPreviewOpen } = useLayoutContext() const { pdfPreviewOpen } = useLayoutContext()
@ -555,6 +560,7 @@ export function LocalCompileProvider({ children }) {
fileList, fileList,
hasChanges, hasChanges,
highlights, highlights,
isProjectOwner,
lastCompileOptions, lastCompileOptions,
logEntryAnnotations, logEntryAnnotations,
logEntries, logEntries,
@ -580,6 +586,7 @@ export function LocalCompileProvider({ children }) {
setStopOnFirstError, setStopOnFirstError,
setStopOnValidationError, setStopOnValidationError,
showLogs, showLogs,
showNewCompileTimeoutUI,
showFasterCompilesFeedbackUI, showFasterCompilesFeedbackUI,
startCompile, startCompile,
stopCompile, stopCompile,
@ -609,6 +616,7 @@ export function LocalCompileProvider({ children }) {
fileList, fileList,
hasChanges, hasChanges,
highlights, highlights,
isProjectOwner,
lastCompileOptions, lastCompileOptions,
logEntries, logEntries,
logEntryAnnotations, logEntryAnnotations,
@ -629,6 +637,7 @@ export function LocalCompileProvider({ children }) {
setStopOnValidationError, setStopOnValidationError,
showCompileTimeWarning, showCompileTimeWarning,
showLogs, showLogs,
showNewCompileTimeoutUI,
showFasterCompilesFeedbackUI, showFasterCompilesFeedbackUI,
startCompile, startCompile,
stopCompile, stopCompile,

View file

@ -33,6 +33,7 @@ export const projectShape = {
_id: PropTypes.string.isRequired, _id: PropTypes.string.isRequired,
email: PropTypes.string.isRequired, email: PropTypes.string.isRequired,
}), }),
useNewCompileTimeoutUI: PropTypes.string,
} }
ProjectContext.Provider.propTypes = { ProjectContext.Provider.propTypes = {
@ -79,8 +80,20 @@ export function ProjectProvider({ children }) {
features, features,
publicAccesLevel: publicAccessLevel, publicAccesLevel: publicAccessLevel,
owner, owner,
showNewCompileTimeoutUI,
} = project || projectFallback } = project || projectFallback
// temporary override for new compile timeout
const forceNewCompileTimeout = new URLSearchParams(
window.location.search
).get('force_new_compile_timeout')
const newCompileTimeoutOverride =
forceNewCompileTimeout === 'active'
? 'active'
: forceNewCompileTimeout === 'changing'
? 'changing'
: undefined
const value = useMemo(() => { const value = useMemo(() => {
return { return {
_id, _id,
@ -91,6 +104,8 @@ export function ProjectProvider({ children }) {
features, features,
publicAccessLevel, publicAccessLevel,
owner, owner,
showNewCompileTimeoutUI:
newCompileTimeoutOverride || showNewCompileTimeoutUI,
} }
}, [ }, [
_id, _id,
@ -101,6 +116,8 @@ export function ProjectProvider({ children }) {
features, features,
publicAccessLevel, publicAccessLevel,
owner, owner,
showNewCompileTimeoutUI,
newCompileTimeoutOverride,
]) ])
return ( return (

View file

@ -756,6 +756,7 @@ CodeMirror
.pdf-preview-messages { .pdf-preview-messages {
position: absolute; position: absolute;
right: @margin-sm; right: @margin-sm;
left: @margin-sm;
top: @margin-xl; top: @margin-xl;
z-index: @zindex-popover; z-index: @zindex-popover;
} }

View file

@ -589,3 +589,10 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.btn-secondary-compile-timeout-override {
color: #1b222c;
background-color: #ffffff;
border-color: #677283;
border-width: 1px;
}

View file

@ -156,6 +156,35 @@
background-color: @red-10; background-color: @red-10;
} }
} }
.btn-info-ghost when (@is-new-css = true) {
.btn-borderless(@blue-50, @btn-info-ghost-bg, @blue-10);
}
// Info Ghost appear as info blue with no border
.btn-info-ghost when (@is-new-css = false) {
.button-variant(@btn-info-ghost-color; @btn-info-ghost-bg; @btn-info-ghost-border);
// hover for ghost acts different from typical variants, as it's default state has no bg
&:hover {
background-color: @blue-10;
}
}
// Inline button to fit text, without link styling.
// TODO: generic class for other styles
.btn-info-ghost-inline when (@is-new-css = true) {
.btn-borderless(@blue-50, @btn-info-ghost-bg, @blue-10);
padding: 0 !important;
font-size: inherit !important;
vertical-align: inherit;
}
.btn-info-ghost-inline when (@is-new-css = false) {
.button-variant(@btn-info-ghost-color; @btn-info-ghost-bg; @btn-info-ghost-border);
// hover for ghost acts different from typical variants, as it's default state has no bg
&:hover {
background-color: @blue-10;
}
padding: 0 !important;
font-size: inherit !important;
vertical-align: inherit;
}
.btn-danger-ghost when (@is-new-css = true) { .btn-danger-ghost when (@is-new-css = true) {
.btn-borderless(@red-50, @btn-danger-ghost-bg, @red-10); .btn-borderless(@red-50, @btn-danger-ghost-bg, @red-10);
} }

View file

@ -241,6 +241,10 @@
@btn-info-bg: @ol-blue; @btn-info-bg: @ol-blue;
@btn-info-border: transparent; @btn-info-border: transparent;
@btn-info-ghost-color: @blue-50;
@btn-info-ghost-bg: transparent;
@btn-info-ghost-border: transparent;
@btn-warning-color: #fff; @btn-warning-color: #fff;
@btn-warning-bg: @orange; @btn-warning-bg: @orange;
@btn-warning-border: transparent; @btn-warning-border: transparent;

View file

@ -171,6 +171,10 @@
@btn-info-bg: @blue; @btn-info-bg: @blue;
@btn-info-border: transparent; @btn-info-border: transparent;
@btn-info-ghost-color: @blue-50;
@btn-info-ghost-bg: transparent;
@btn-info-ghost-border: transparent;
@btn-warning-color: #fff; @btn-warning-color: #fff;
@btn-warning-bg: @orange; @btn-warning-bg: @orange;
@btn-warning-border: transparent; @btn-warning-border: transparent;

View file

@ -12,6 +12,7 @@
"Terms": "Terms", "Terms": "Terms",
"Universities": "Universities", "Universities": "Universities",
"a_custom_size_has_been_used_in_the_latex_code": "A custom size has been used in the LaTeX code.", "a_custom_size_has_been_used_in_the_latex_code": "A custom size has been used in the LaTeX code.",
"a_fatal_compile_error_that_completely_blocks_compilation": "A <0>fatal compile error</0> that completely blocks the compilation.",
"a_file_with_that_name_already_exists_and_will_be_overriden": "A file with that name already exists. That file will be overwritten.", "a_file_with_that_name_already_exists_and_will_be_overriden": "A file with that name already exists. That file will be overwritten.",
"a_more_comprehensive_list_of_keyboard_shortcuts": "A more comprehensive list of keyboard shortcuts can be found in <0>this __appName__ project template</0>", "a_more_comprehensive_list_of_keyboard_shortcuts": "A more comprehensive list of keyboard shortcuts can be found in <0>this __appName__ project template</0>",
"about": "About", "about": "About",
@ -100,6 +101,7 @@
"an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__</0>. Please wait and try again later.", "an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__</0>. Please wait and try again later.",
"an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code", "an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code",
"and": "and", "and": "and",
"and_you_can_upgrade_for_plenty_more_compile_time": "And you can <strong>upgrade to get plenty more compile time.</strong>",
"annual": "Annual", "annual": "Annual",
"annual_billing_enabled": "Annual billing enabled", "annual_billing_enabled": "Annual billing enabled",
"anonymous": "Anonymous", "anonymous": "Anonymous",
@ -178,6 +180,7 @@
"built_in": "Built-In", "built_in": "Built-In",
"bulk_accept_confirm": "Are you sure you want to accept the selected __nChanges__ changes?", "bulk_accept_confirm": "Are you sure you want to accept the selected __nChanges__ changes?",
"bulk_reject_confirm": "Are you sure you want to reject the selected __nChanges__ changes?", "bulk_reject_confirm": "Are you sure you want to reject the selected __nChanges__ changes?",
"but_note_that_free_compile_timeout_limit_will_be_reduced_on_x_date": "But note that the free compile timeout limit <0>will be reduced on __date__</0>.",
"buy_now_no_exclamation_mark": "Buy now", "buy_now_no_exclamation_mark": "Buy now",
"by": "by", "by": "by",
"by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service</0>.", "by_registering_you_agree_to_our_terms_of_service": "By registering, you agree to our <0>terms of service</0>.",
@ -270,6 +273,7 @@
"comment": "Comment", "comment": "Comment",
"commit": "Commit", "commit": "Commit",
"common": "Common", "common": "Common",
"common_causes_of_compile_timeouts_are": "Common causes of compile timeouts are",
"commons_plan_tooltip": "Youre on the __plan__ plan because of your affiliation with __institution__. Click to find out how to make the most of your Overleaf premium features.", "commons_plan_tooltip": "Youre on the __plan__ plan because of your affiliation with __institution__. Click to find out how to make the most of your Overleaf premium features.",
"compact": "Compact", "compact": "Compact",
"company_name": "Company Name", "company_name": "Company Name",
@ -284,6 +288,7 @@
"compile_timeout": "Compile timeout (minutes)", "compile_timeout": "Compile timeout (minutes)",
"compile_timeout_short": "Compile timeout", "compile_timeout_short": "Compile timeout",
"compile_timeout_short_info": "This is how much time you get to compile your project on the Overleaf servers. For short and simple projects, 1 minute should be enough, but you may need longer for complex or longer projects", "compile_timeout_short_info": "This is how much time you get to compile your project on the Overleaf servers. For short and simple projects, 1 minute should be enough, but you may need longer for complex or longer projects",
"compile_timeout_will_be_reduced_project_exceeds_limit_speed_up_compile": "The compile timeout limit on our free plan <0>will be reduced on __date__</0> and this project currently exceeds the new limit. You may be able to fix issues to <1>speed up the compile</1>.",
"compiler": "Compiler", "compiler": "Compiler",
"compiling": "Compiling", "compiling": "Compiling",
"complete": "Complete", "complete": "Complete",
@ -485,6 +490,7 @@
"empty_zip_file": "Zip doesnt contain any file", "empty_zip_file": "Zip doesnt contain any file",
"en": "English", "en": "English",
"enable_managed_users": "Enable Managed Users", "enable_managed_users": "Enable Managed Users",
"enable_stop_on_first_error_under_recompile_dropdown_menu": "Enable <0>“Stop on first error”</0> under the <1>Recompile</1> drop-down menu to help you find and fix errors right away.",
"enabled_managed_users_set_up_sso": "You need to enable Managed Users to set up SSO.", "enabled_managed_users_set_up_sso": "You need to enable Managed Users to set up SSO.",
"enabling": "Enabling", "enabling": "Enabling",
"end_of_document": "End of document", "end_of_document": "End of document",
@ -886,6 +892,7 @@
"labs_program_benefits": "__appName__ is always looking for new ways to help users work more quickly and effectively. By joining Overleaf Labs, you can participate in experiments that explore innovative ideas in the space of collaborative writing and publishing.", "labs_program_benefits": "__appName__ is always looking for new ways to help users work more quickly and effectively. By joining Overleaf Labs, you can participate in experiments that explore innovative ideas in the space of collaborative writing and publishing.",
"labs_program_not_participating": "You are not enrolled in Labs", "labs_program_not_participating": "You are not enrolled in Labs",
"language": "Language", "language": "Language",
"large_or_high-resolution_images_taking_too_long": "Large or high-resolution images taking too long to process. You may be able to <0>optimize them</0>.",
"last_active": "Last Active", "last_active": "Last Active",
"last_active_description": "Last time a project was opened.", "last_active_description": "Last time a project was opened.",
"last_modified": "Last Modified", "last_modified": "Last Modified",
@ -915,6 +922,7 @@
"learn_more_about_emails": "<0>Learn more</0> about managing your __appName__ emails.", "learn_more_about_emails": "<0>Learn more</0> about managing your __appName__ emails.",
"learn_more_about_link_sharing": "Learn more about Link Sharing", "learn_more_about_link_sharing": "Learn more about Link Sharing",
"learn_more_about_managed_users": "Learn more about Managed Users.", "learn_more_about_managed_users": "Learn more about Managed Users.",
"learn_more_about_other_causes_of_compile_timeouts": "<0>Learn more</0> about other causes of compile timeouts and how to fix them.",
"learn_more_lowercase": "learn more", "learn_more_lowercase": "learn more",
"leave": "Leave", "leave": "Leave",
"leave_any_group_subscriptions": "Leave any group subscriptions other than the one that will be managing your account. <0>Leave them from the Subscription page.</0>", "leave_any_group_subscriptions": "Leave any group subscriptions other than the one that will be managing your account. <0>Leave them from the Subscription page.</0>",
@ -1186,6 +1194,7 @@
"other_logs_and_files": "Other logs and files", "other_logs_and_files": "Other logs and files",
"other_output_files": "Download other output files", "other_output_files": "Download other output files",
"other_sessions": "Other Sessions", "other_sessions": "Other Sessions",
"other_ways_to_prevent_compile_timeouts": "Other ways to prevent compile timeouts",
"our_values": "Our values", "our_values": "Our values",
"output_file": "Output file", "output_file": "Output file",
"over": "over", "over": "over",
@ -1269,6 +1278,7 @@
"please_select_an_output_file": "Please Select an Output File", "please_select_an_output_file": "Please Select an Output File",
"please_set_a_password": "Please set a password", "please_set_a_password": "Please set a password",
"please_set_main_file": "Please choose the main file for this project in the project menu. ", "please_set_main_file": "Please choose the main file for this project in the project menu. ",
"plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).",
"plus_more": "plus more", "plus_more": "plus more",
"plus_upgraded_accounts_receive": "Plus with an upgraded account you get", "plus_upgraded_accounts_receive": "Plus with an upgraded account you get",
"popular_tags": "Popular Tags", "popular_tags": "Popular Tags",
@ -1342,6 +1352,8 @@
"react_history_tutorial_content": "To compare a range of versions, use the <0></0> on the versions you want at the start and end of the range. To add a label or to download a version use the options in the three-dot menu. <1>Learn more about using Overleaf History.</1>", "react_history_tutorial_content": "To compare a range of versions, use the <0></0> on the versions you want at the start and end of the range. To add a label or to download a version use the options in the three-dot menu. <1>Learn more about using Overleaf History.</1>",
"react_history_tutorial_title": "History actions have a new home", "react_history_tutorial_title": "History actions have a new home",
"reactivate_subscription": "Reactivate your subscription", "reactivate_subscription": "Reactivate your subscription",
"read_more_about_fix_prevent_timeout": "Read more about how to fix and prevent timeouts",
"read_more_about_free_compile_timeouts_servers": "Read more about changes to free compile timeouts and servers",
"read_only": "Read Only", "read_only": "Read Only",
"read_only_token": "Read-Only Token", "read_only_token": "Read-Only Token",
"read_write_token": "Read-Write Token", "read_write_token": "Read-Write Token",
@ -1612,8 +1624,10 @@
"sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.", "sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.",
"sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.", "sso_user_denied_access": "Cannot log in because __appName__ was not granted access to your __provider__ account. Please try again.",
"standard": "Standard", "standard": "Standard",
"start_a_free_trial": "Start a free trial",
"start_by_adding_your_email": "Start by adding your email address.", "start_by_adding_your_email": "Start by adding your email address.",
"start_free_trial": "Start Free Trial!", "start_free_trial": "Start Free Trial!",
"start_free_trial_without_exclamation": "Start Free Trial",
"start_using_latex_now": "start using LaTeX right now", "start_using_latex_now": "start using LaTeX right now",
"start_using_sl_now": "Start using __appName__ now", "start_using_sl_now": "Start using __appName__ now",
"state": "State", "state": "State",
@ -1674,6 +1688,8 @@
"tc_switch_everyone_tip": "Toggle track-changes for everyone", "tc_switch_everyone_tip": "Toggle track-changes for everyone",
"tc_switch_guests_tip": "Toggle track-changes for all link-sharing guests", "tc_switch_guests_tip": "Toggle track-changes for all link-sharing guests",
"tc_switch_user_tip": "Toggle track-changes for this user", "tc_switch_user_tip": "Toggle track-changes for this user",
"tell_the_project_owner_and_ask_them_to_upgrade": "<0>Tell the project owner</0> and ask them to upgrade their Overleaf plan if you need more compile time.",
"tell_the_project_owner_to_upgrade_plan_for_more_compile_time": "<strong>Tell the project owner</strong> and ask them to upgrade their Overleaf plan if you need more compile time.",
"template": "Template", "template": "Template",
"template_approved_by_publisher": "This template has been approved by the publisher", "template_approved_by_publisher": "This template has been approved by the publisher",
"template_description": "Template Description", "template_description": "Template Description",
@ -1722,6 +1738,8 @@
"this_field_is_required": "This field is required", "this_field_is_required": "This field is required",
"this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.", "this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.",
"this_is_your_template": "This is your template from your project", "this_is_your_template": "This is your template from your project",
"this_project_compiled_but_soon_might_not": "This project compiled, but soon it might not",
"this_project_exceeded_compile_timeout_limit_on_free_plan": "This project exceeded the compile timeout limit on our free plan.",
"this_project_is_public": "This project is public and can be edited by anyone with the URL.", "this_project_is_public": "This project is public and can be edited by anyone with the URL.",
"this_project_is_public_read_only": "This project is public and can be viewed but not edited by anyone with the URL", "this_project_is_public_read_only": "This project is public and can be viewed but not edited by anyone with the URL",
"this_project_will_appear_in_your_dropbox_folder_at": "This project will appear in your Dropbox folder at ", "this_project_will_appear_in_your_dropbox_folder_at": "This project will appear in your Dropbox folder at ",
@ -1751,6 +1769,7 @@
"too_many_requests": "Too many requests were received in a short space of time. Please wait for a few moments and try again.", "too_many_requests": "Too many requests were received in a short space of time. Please wait for a few moments and try again.",
"too_many_search_results": "There are more than 100 results. Please refine your search.", "too_many_search_results": "There are more than 100 results. Please refine your search.",
"too_recently_compiled": "This project was compiled very recently, so this compile has been skipped.", "too_recently_compiled": "This project was compiled very recently, so this compile has been skipped.",
"took_a_while": "That took a while...",
"toolbar_add_comment": "Add Comment", "toolbar_add_comment": "Add Comment",
"toolbar_bullet_list": "Bullet List", "toolbar_bullet_list": "Bullet List",
"toolbar_choose_section_heading_level": "Choose section heading level", "toolbar_choose_section_heading_level": "Choose section heading level",
@ -1867,7 +1886,9 @@
"updating_site": "Updating Site", "updating_site": "Updating Site",
"upgrade": "Upgrade", "upgrade": "Upgrade",
"upgrade_cc_btn": "Upgrade now, pay after 7 days", "upgrade_cc_btn": "Upgrade now, pay after 7 days",
"upgrade_for_12x_more_compile_time": "Upgrade to get 12x more compile time",
"upgrade_for_longer_compiles": "Upgrade to increase your timeout limit.", "upgrade_for_longer_compiles": "Upgrade to increase your timeout limit.",
"upgrade_for_plenty_more_compile_time": "Upgrade to get plenty more compile time.",
"upgrade_now": "Upgrade Now", "upgrade_now": "Upgrade Now",
"upgrade_to_get_feature": "Upgrade to get __feature__, plus:", "upgrade_to_get_feature": "Upgrade to get __feature__, plus:",
"upgrade_to_track_changes": "Upgrade to Track Changes", "upgrade_to_track_changes": "Upgrade to Track Changes",
@ -1977,6 +1998,7 @@
"you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.", "you_have_been_invited_to_transfer_management_of_your_account_to": "You have been invited to transfer management of your account to __groupName__.",
"you_introed_high_number": " Youve introduced <0>__numberOfPeople__</0> people to __appName__. Good job!", "you_introed_high_number": " Youve introduced <0>__numberOfPeople__</0> people to __appName__. Good job!",
"you_introed_small_number": " Youve introduced <0>__numberOfPeople__</0> person to __appName__. Good job, but can you get some more?", "you_introed_small_number": " Youve introduced <0>__numberOfPeople__</0> person to __appName__. Good job, but can you get some more?",
"you_may_be_able_to_prevent_a_compile_timeout": "You may be able to prevent a compile timeout using the following tips.",
"you_not_introed_anyone_to_sl": "Youve not introduced anyone to __appName__ yet. Get sharing!", "you_not_introed_anyone_to_sl": "Youve not introduced anyone to __appName__ yet. Get sharing!",
"you_plus_1": "You + 1", "you_plus_1": "You + 1",
"you_plus_10": "You + 10", "you_plus_10": "You + 10",
@ -1987,6 +2009,7 @@
"your_account_is_managed_by_admin_cant_join_additional_group": "Your __appName__ account is managed by your current group admin (__admin__). This means you cant join additional group subscriptions. <0>Read more about Managed Users.</0>", "your_account_is_managed_by_admin_cant_join_additional_group": "Your __appName__ account is managed by your current group admin (__admin__). This means you cant join additional group subscriptions. <0>Read more about Managed Users.</0>",
"your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.", "your_affiliation_is_confirmed": "Your <0>__institutionName__</0> affiliation is confirmed.",
"your_browser_does_not_support_this_feature": "Sorry, your browser doesnt support this feature. Please update your browser to its latest version.", "your_browser_does_not_support_this_feature": "Sorry, your browser doesnt support this feature. Please update your browser to its latest version.",
"your_compile_timed_out": "Your compile timed out",
"your_git_access_info": "Your Git authentication tokens should be entered whenever youre prompted for a password.", "your_git_access_info": "Your Git authentication tokens should be entered whenever youre prompted for a password.",
"your_git_access_info_bullet_1": "You can have up to 10 tokens.", "your_git_access_info_bullet_1": "You can have up to 10 tokens.",
"your_git_access_info_bullet_2": "If you reach the maximum limit, youll need to delete a token before you can generate a new one.", "your_git_access_info_bullet_2": "If you reach the maximum limit, youll need to delete a token before you can generate a new one.",
@ -1999,6 +2022,9 @@
"your_password_has_been_successfully_changed": "Your password has been successfully changed", "your_password_has_been_successfully_changed": "Your password has been successfully changed",
"your_plan": "Your plan", "your_plan": "Your plan",
"your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__</0> at the end of the current billing period.", "your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__</0> at the end of the current billing period.",
"your_project_compiled_but_soon_might_not": "Your project compiled, but soon it might not",
"your_project_exceeded_compile_timeout_limit_on_free_plan": "Your project exceeded the compile timeout limit on our free plan.",
"your_project_near_compile_timeout_limit": "Your project is near the compile timeout limit for our free plan.",
"your_projects": "Your Projects", "your_projects": "Your Projects",
"your_sessions": "Your Sessions", "your_sessions": "Your Sessions",
"your_subscription": "Your Subscription", "your_subscription": "Your Subscription",

View file

@ -215,6 +215,7 @@ describe('CompileManager', function () {
betaProgram: 1, betaProgram: 1,
features: 1, features: 1,
splitTests: 1, splitTests: 1,
signUpDate: 1,
}) })
.should.equal(true) .should.equal(true)
}) })
@ -232,6 +233,96 @@ describe('CompileManager', function () {
}) })
}) })
describe('getProjectCompileLimits with reduced compile timeout', function () {
beforeEach(function () {
this.getAssignmentForMongoUser.callsFake((user, test, cb) => {
if (test === 'compile-backend-class-n2d') {
cb(null, { variant: 'n2d' })
}
if (test === 'compile-timeout-20s') {
cb(null, { variant: '20s' })
}
})
this.features = {
compileTimeout: (this.timeout = 60),
compileGroup: (this.group = 'standard'),
}
this.ProjectGetter.getProject = sinon
.stub()
.callsArgWith(
2,
null,
(this.project = { owner_ref: (this.owner_id = 'owner-id-123') })
)
this.UserGetter.getUser = sinon
.stub()
.callsArgWith(
2,
null,
(this.user = { features: this.features, analyticsId: 'abc' })
)
this.CompileManager.getProjectCompileLimits(
this.project_id,
this.callback
)
})
describe('user is in the n2d group and compile-timeout-20s split test variant', function () {
describe('user has a timeout of more than 60s', function () {
beforeEach(function () {
this.features.compileTimeout = 120
})
it('should keep the users compile timeout', function () {
this.CompileManager.getProjectCompileLimits(
this.project_id,
this.callback
)
this.callback
.calledWith(null, sinon.match({ timeout: 120 }))
.should.equal(true)
})
})
describe('user registered before the cut off date', function () {
beforeEach(function () {
this.features.compileTimeout = 60
const signUpDate = new Date(
this.CompileManager.NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF
)
signUpDate.setDate(signUpDate.getDate() - 1)
this.user.signUpDate = signUpDate
})
it('should keep the users compile timeout', function () {
this.CompileManager.getProjectCompileLimits(
this.project_id,
this.callback
)
this.callback
.calledWith(null, sinon.match({ timeout: 60 }))
.should.equal(true)
})
})
describe('user registered after the cut off date', function () {
beforeEach(function () {
this.timeout = 60
const signUpDate = new Date(
this.CompileManager.NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF
)
signUpDate.setDate(signUpDate.getDate() + 1)
this.user.signUpDate = signUpDate
})
it('should reduce compile timeout to 20s', function () {
this.CompileManager.getProjectCompileLimits(
this.project_id,
this.callback
)
this.callback
.calledWith(null, sinon.match({ timeout: 20 }))
.should.equal(true)
})
})
})
})
describe('compileBackendClass', function () { describe('compileBackendClass', function () {
beforeEach(function () { beforeEach(function () {
this.features = { this.features = {

View file

@ -131,6 +131,13 @@ describe('EditorHttpController', function () {
notFound: sinon.stub(), notFound: sinon.stub(),
unprocessableEntity: sinon.stub(), unprocessableEntity: sinon.stub(),
} }
this.SplitTestHandler = {
promises: {
getAssignmentForMongoUser: sinon
.stub()
.resolves({ variant: 'default' }),
},
}
this.EditorHttpController = SandboxedModule.require(MODULE_PATH, { this.EditorHttpController = SandboxedModule.require(MODULE_PATH, {
requires: { requires: {
'../Project/ProjectDeleter': this.ProjectDeleter, '../Project/ProjectDeleter': this.ProjectDeleter,
@ -150,6 +157,8 @@ describe('EditorHttpController', function () {
this.ProjectEntityUpdateHandler, this.ProjectEntityUpdateHandler,
'../Docstore/DocstoreManager': this.DocstoreManager, '../Docstore/DocstoreManager': this.DocstoreManager,
'../Errors/HttpErrorHandler': this.HttpErrorHandler, '../Errors/HttpErrorHandler': this.HttpErrorHandler,
'../SplitTests/SplitTestHandler': this.SplitTestHandler,
'../Compile/CompileManager': {},
}, },
}) })
}) })

View file

@ -8,6 +8,9 @@ declare global {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
interface Window { interface Window {
csrfToken: string csrfToken: string
sl_console: {
log: (message: string) => void
}
sl_debugging: boolean sl_debugging: boolean
user: User user: User
user_id?: string user_id?: string