From 491bc2628dc882b07d4924bd910814b35e356267 Mon Sep 17 00:00:00 2001 From: Antoine Clausse Date: Fri, 12 Apr 2024 11:50:48 +0200 Subject: [PATCH] Merge pull request #17899 from overleaf/revert-17700-ac-tear-down-compile-timeout-tests Revert "[web] Remove split-tests `compile-backend-class*` and `compile-timeout-20s*`" GitOrigin-RevId: d5070ced06adbd435e782a44b7ef767e395bd6a0 --- .../src/Features/Compile/CompileController.js | 9 + .../src/Features/Compile/CompileManager.js | 135 +++++++++++++-- .../Features/Editor/EditorHttpController.js | 58 +++++++ services/web/config/settings.defaults.js | 1 + .../web/frontend/extracted-translations.json | 21 +++ .../components/compile-time-warning.tsx | 116 +++++++++++++ .../compile-timeout-changing-soon.tsx | 122 ++++++++++++++ .../components/compile-timeout-messages.tsx | 72 ++++++-- .../components/compile-timeout-warning.tsx | 13 +- .../components/faster-compiles-feedback.tsx | 135 +++++++++++++++ .../components/pdf-logs-viewer.jsx | 9 +- .../components/pdf-preview-pane.jsx | 11 +- .../components/timeout-upgrade-prompt-new.tsx | 159 +++++++++++------- .../components/timeout-upgrade-prompt.jsx | 62 +++++++ .../js/features/pdf-preview/util/compiler.js | 8 + .../shared/context/detach-compile-context.tsx | 18 ++ .../shared/context/local-compile-context.tsx | 19 ++- .../js/shared/context/project-context.tsx | 17 ++ .../stories/pdf-preview-messages.stories.jsx | 4 +- .../stories/pdf-preview-messages.stories.tsx | 37 +++- services/web/locales/en.json | 21 +++ .../migration_compile_timeout_60s_to_20s.js | 57 ------- .../src/MigrateUserFeatureTimeoutTests.js | 134 --------------- .../pdf-preview/pdf-preview.spec.tsx | 2 +- .../test/unit/src/Compile/ClsiManagerTests.js | 1 + .../src/Compile/CompileControllerTests.js | 1 + .../unit/src/Compile/CompileManagerTests.js | 40 +++-- 27 files changed, 980 insertions(+), 302 deletions(-) create mode 100644 services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx create mode 100644 services/web/frontend/js/features/pdf-preview/components/compile-timeout-changing-soon.tsx create mode 100644 services/web/frontend/js/features/pdf-preview/components/faster-compiles-feedback.tsx create mode 100644 services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt.jsx delete mode 100644 services/web/scripts/migration_compile_timeout_60s_to_20s.js delete mode 100644 services/web/test/acceptance/src/MigrateUserFeatureTimeoutTests.js diff --git a/services/web/app/src/Features/Compile/CompileController.js b/services/web/app/src/Features/Compile/CompileController.js index 8515e430bb..99bf62728f 100644 --- a/services/web/app/src/Features/Compile/CompileController.js +++ b/services/web/app/src/Features/Compile/CompileController.js @@ -105,6 +105,15 @@ module.exports = CompileController = { 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) { options.rootDoc_id = req.body.rootDoc_id } else if ( diff --git a/services/web/app/src/Features/Compile/CompileManager.js b/services/web/app/src/Features/Compile/CompileManager.js index 63badf4dc7..c65c4fff9e 100644 --- a/services/web/app/src/Features/Compile/CompileManager.js +++ b/services/web/app/src/Features/Compile/CompileManager.js @@ -8,9 +8,18 @@ const UserGetter = require('../User/UserGetter') const ClsiManager = require('./ClsiManager') const Metrics = require('@overleaf/metrics') const { RateLimiter } = require('../../infrastructure/RateLimiter') +const SplitTestHandler = require('../SplitTests/SplitTestHandler') const UserAnalyticsIdCache = require('../Analytics/UserAnalyticsIdCache') +const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF = new Date('2023-09-18T11:00:00.000Z') +const NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE = new Date( + '2023-10-10T11:00:00.000Z' +) + module.exports = CompileManager = { + NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF, + NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE, + compile(projectId, userId, options = {}, _callback) { const timer = new Metrics.Timer('editor.compile') const callback = function (...args) { @@ -53,6 +62,16 @@ module.exports = CompileManager = { const value = limits[key] 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 CompileManager._checkCompileGroupAutoCompileLimit( options.isAutoCompile, @@ -165,25 +184,78 @@ module.exports = CompileManager = { if (err) { return callback(err) } - const compileGroup = - ownerFeatures.compileGroup || - Settings.defaultFeatures.compileGroup - const compileTimeout = - ownerFeatures.compileTimeout || - Settings.defaultFeatures.compileTimeout - const limits = { timeout: - // temporary override until users' compileTimeout is migrated - compileGroup === 'standard' && compileTimeout <= 60 - ? 20 - : compileTimeout, - compileGroup, - compileBackendClass: - compileGroup === 'standard' ? 'n2d' : 'c2d', + ownerFeatures.compileTimeout || + Settings.defaultFeatures.compileTimeout, + compileGroup: + ownerFeatures.compileGroup || + Settings.defaultFeatures.compileGroup, ownerAnalyticsId: analyticsId, } - callback(null, limits) + CompileManager._getCompileBackendClassDetails( + owner, + limits.compileGroup, + ( + err, + { compileBackendClass, showFasterCompilesFeedbackUI } + ) => { + if (err) return callback(err) + limits.compileBackendClass = compileBackendClass + limits.showFasterCompilesFeedbackUI = + showFasterCompilesFeedbackUI + 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) + // users who were on the 'default' servers at time of original rollout + // will have a later cutoff date for the 20s timeout in the next phase + // we check the backend class at version 8 (baseline) + const backendClassHistory = + owner.splitTests?.['compile-backend-class-n2d'] || + [] + const backendClassBaselineVariant = + backendClassHistory.find(version => { + return version.versionNumber === 8 + })?.variantName + const timeoutEnforcedCutoff = + backendClassBaselineVariant === 'default' + ? NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE + : NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF + if (assignment?.variant === '20s') { + if (owner.signUpDate > timeoutEnforcedCutoff) { + limits.timeout = 20 + callback(null, limits) + } else { + SplitTestHandler.getAssignmentForMongoUser( + owner, + 'compile-timeout-20s-existing-users', + (err, assignmentExistingUsers) => { + if (err) return callback(err) + if ( + assignmentExistingUsers?.variant === '20s' + ) { + limits.timeout = 20 + } + callback(null, limits) + } + ) + } + } else { + callback(null, limits) + } + } + ) + } else { + callback(null, limits) + } + } + ) } ) } @@ -250,6 +322,39 @@ module.exports = CompileManager = { }) }, + _getCompileBackendClassDetails(owner, compileGroup, callback) { + const { defaultBackendClass } = Settings.apis.clsi + if (compileGroup === 'standard') { + return SplitTestHandler.getAssignmentForMongoUser( + owner, + 'compile-backend-class-n2d', + (err, assignment) => { + if (err) return callback(err, {}) + const { variant } = assignment + callback(null, { + compileBackendClass: + variant === 'default' ? defaultBackendClass : variant, + showFasterCompilesFeedbackUI: false, + }) + } + ) + } + SplitTestHandler.getAssignmentForMongoUser( + owner, + 'compile-backend-class', + (err, assignment) => { + if (err) return callback(err, {}) + const { analytics, variant } = assignment + const activeForUser = analytics?.segmentation?.splitTest != null + callback(null, { + compileBackendClass: + variant === 'default' ? defaultBackendClass : variant, + showFasterCompilesFeedbackUI: activeForUser, + }) + } + ) + }, + wordCount(projectId, userId, file, clsiserverid, callback) { CompileManager.getProjectCompileLimits(projectId, function (error, limits) { if (error) { diff --git a/services/web/app/src/Features/Editor/EditorHttpController.js b/services/web/app/src/Features/Editor/EditorHttpController.js index 7f2d54167c..6e820c2fe4 100644 --- a/services/web/app/src/Features/Editor/EditorHttpController.js +++ b/services/web/app/src/Features/Editor/EditorHttpController.js @@ -13,6 +13,12 @@ const Errors = require('../Errors/Errors') const DocstoreManager = require('../Docstore/DocstoreManager') const logger = require('@overleaf/logger') const { expressify } = require('@overleaf/promise-utils') +const SplitTestHandler = require('../SplitTests/SplitTestHandler') +const { + NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF, + NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE, +} = require('../Compile/CompileManager') +const UserGetter = require('../User/UserGetter') module.exports = { joinProject: expressify(joinProject), @@ -66,6 +72,58 @@ async function joinProject(req, res, next) { if (!project) { return res.sendStatus(403) } + // Compile timeout 20s test + if (project.features?.compileTimeout <= 60) { + const compileAssignment = + await SplitTestHandler.promises.getAssignmentForUser( + project.owner._id, + 'compile-backend-class-n2d' + ) + if (compileAssignment?.variant === 'n2d') { + const timeoutAssignment = + await SplitTestHandler.promises.getAssignmentForUser( + project.owner._id, + 'compile-timeout-20s' + ) + if (timeoutAssignment?.variant === '20s') { + // users who were on the 'default' servers at time of original rollout + // will have a later cutoff date for the 20s timeout in the next phase + // we check the backend class at version 8 (baseline) + const owner = await UserGetter.promises.getUser(project.owner._id, { + _id: 1, + 'splitTests.compile-backend-class-n2d': 1, + }) + const backendClassHistory = + owner.splitTests?.['compile-backend-class-n2d'] || [] + const backendClassBaselineVariant = backendClassHistory.find( + version => { + return version.versionNumber === 8 + } + )?.variantName + const timeoutEnforcedCutoff = + backendClassBaselineVariant === 'default' + ? NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE + : NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF + if (project.owner.signUpDate > timeoutEnforcedCutoff) { + // New users will see a 10s warning and compile fail at 20s + project.showNewCompileTimeoutUI = 'active' + } else { + const existingUserTimeoutAssignment = + await SplitTestHandler.promises.getAssignmentForUser( + project.owner._id, + 'compile-timeout-20s-existing-users' + ) + if (existingUserTimeoutAssignment?.variant === '20s') { + // Older users in treatment see 10s warning and compile fail at 20s + project.showNewCompileTimeoutUI = 'active' + } else { + // Older users in control 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 if (isRestrictedUser) { project.owner = { _id: project.owner._id } diff --git a/services/web/config/settings.defaults.js b/services/web/config/settings.defaults.js index 5dfa6b59b6..956beb62c2 100644 --- a/services/web/config/settings.defaults.js +++ b/services/web/config/settings.defaults.js @@ -272,6 +272,7 @@ module.exports = { url: `http://${process.env.CLSI_HOST || 'localhost'}:3013`, // url: "http://#{process.env['CLSI_LB_HOST']}:3014" backendGroupName: undefined, + defaultBackendClass: process.env.CLSI_DEFAULT_BACKEND_CLASS || 'e2', submissionBackendClass: process.env.CLSI_SUBMISSION_BACKEND_CLASS || 'n2d', }, diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 0551e10657..4834acde80 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -70,9 +70,11 @@ "also": "", "an_email_has_already_been_sent_to": "", "an_error_occurred_when_verifying_the_coupon_code": "", + "and_you_can_upgrade_for_plenty_more_compile_time": "", "anonymous": "", "anyone_with_link_can_edit": "", "anyone_with_link_can_view": "", + "approaching_compile_timeout_limit_upgrade_for_more_compile_time": "", "archive": "", "archive_projects": "", "archived": "", @@ -84,6 +86,7 @@ "are_you_sure": "", "ask_proj_owner_to_unlink_from_current_github": "", "ask_proj_owner_to_upgrade_for_full_history": "", + "ask_proj_owner_to_upgrade_for_longer_compiles": "", "ask_proj_owner_to_upgrade_for_references_search": "", "ask_repo_owner_to_reconnect": "", "ask_repo_owner_to_renew_overleaf_subscription": "", @@ -189,6 +192,7 @@ "compile_mode": "", "compile_terminated_by_user": "", "compiler": "", + "compiles_on_our_free_plan_are_now_on_faster_servers": "", "compiling": "", "configure_sso": "", "confirm": "", @@ -281,6 +285,7 @@ "disabling": "", "disconnected": "", "discount_of": "", + "dismiss": "", "dismiss_error_popup": "", "display_deleted_user": "", "do_you_want_to_change_your_primary_email_address_to": "", @@ -381,6 +386,11 @@ "failed_to_send_managed_user_invite_to_email": "", "failed_to_send_sso_link_invite_to_email": "", "fast": "", + "faster_compiles_feedback_question": "", + "faster_compiles_feedback_seems_faster": "", + "faster_compiles_feedback_seems_same": "", + "faster_compiles_feedback_seems_slower": "", + "faster_compiles_feedback_thanks": "", "file_action_created": "", "file_action_deleted": "", "file_action_edited": "", @@ -416,6 +426,7 @@ "found_matching_deleted_users": "", "free_7_day_trial_billed_annually": "", "free_7_day_trial_billed_monthly": "", + "free_accounts_have_timeout_upgrade_to_increase": "", "free_plan_label": "", "free_plan_tooltip": "", "from_another_project": "", @@ -907,6 +918,7 @@ "please_wait": "", "plus_additional_collaborators_document_history_track_changes_and_more": "", "plus_more": "", + "plus_upgraded_accounts_receive": "", "postal_code": "", "premium_feature": "", "premium_plan_label": "", @@ -968,6 +980,7 @@ "react_history_tutorial_content": "", "react_history_tutorial_title": "", "reactivate_subscription": "", + "read_more_about_fix_prevent_timeout": "", "read_more_about_free_compile_timeouts_servers": "", "read_only": "", "read_only_token": "", @@ -1279,6 +1292,7 @@ "tc_switch_guests_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_description": "", "template_title_taken_from_project_title": "", @@ -1308,6 +1322,7 @@ "this_could_be_because_we_cant_support_some_elements_of_the_table": "", "this_field_is_required": "", "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_read_only": "", @@ -1449,6 +1464,8 @@ "upgrade": "", "upgrade_cc_btn": "", "upgrade_for_12x_more_compile_time": "", + "upgrade_for_longer_compiles": "", + "upgrade_for_plenty_more_compile_time": "", "upgrade_now": "", "upgrade_to_get_feature": "", "upgrade_to_track_changes": "", @@ -1502,6 +1519,8 @@ "we_sent_new_code": "", "wed_love_you_to_stay": "", "welcome_to_sl": "", + "were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "", + "were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "", "were_performing_maintenance": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "", @@ -1552,6 +1571,7 @@ "you_have_been_invited_to_transfer_management_of_your_account": "", "you_have_been_invited_to_transfer_management_of_your_account_to": "", "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "", + "you_may_be_able_to_fix_issues_to_speed_up_the_compile": "", "you_need_to_configure_your_sso_settings": "", "you_will_be_able_to_reassign_subscription": "", "youll_get_best_results_in_visual_but_can_be_used_in_source": "", @@ -1571,6 +1591,7 @@ "your_new_plan": "", "your_plan": "", "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": "", diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx new file mode 100644 index 0000000000..4f3ea2a23b --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/compile-time-warning.tsx @@ -0,0 +1,116 @@ +import { memo, useCallback, useEffect } from 'react' +import { Button } from 'react-bootstrap' +import { Trans, useTranslation } from 'react-i18next' +import * as eventTracking from '../../../infrastructure/event-tracking' +import StartFreeTrialButton from '../../../shared/components/start-free-trial-button' +import { useDetachCompileContext } from '../../../shared/context/detach-compile-context' +import usePersistedState from '../../../shared/hooks/use-persisted-state' +import { useSplitTestContext } from '@/shared/context/split-test-context' + +const TWENTY_FOUR_DAYS = 24 * 60 * 60 * 24 * 1000 + +function CompileTimeWarning() { + const { t } = useTranslation() + + const [displayStatus, setDisplayStatus] = usePersistedState( + 'compile-time-warning-display-status', + { lastDisplayTime: 0, dismissed: false }, + true + ) + + const { + showCompileTimeWarning, + setShowCompileTimeWarning, + deliveryLatencies, + isProjectOwner, + } = useDetachCompileContext() + + const { splitTestVariants } = useSplitTestContext() + const hasNewPaywallCta = splitTestVariants['paywall-cta'] === 'enabled' + + useEffect(() => { + if (deliveryLatencies && deliveryLatencies.compileTimeServerE2E) { + // compile-timeout-20s test + if (deliveryLatencies.compileTimeServerE2E > 10000) { + eventTracking.sendMB('compile-time-warning-would-display', { + time: 10, + newCompileTimeout: 'control', + isProjectOwner, + }) + } + } + }, [deliveryLatencies, isProjectOwner]) + + useEffect(() => { + if (showCompileTimeWarning) { + if ( + displayStatus && + Date.now() - displayStatus.lastDisplayTime < TWENTY_FOUR_DAYS + ) { + return + } + setDisplayStatus({ lastDisplayTime: Date.now(), dismissed: false }) + eventTracking.sendMB('compile-time-warning-displayed', { time: 30 }) + } + }, [showCompileTimeWarning, displayStatus, setDisplayStatus]) + + const getTimeSinceDisplayed = useCallback(() => { + return (Date.now() - displayStatus.lastDisplayTime) / 1000 + }, [displayStatus]) + + const closeWarning = useCallback(() => { + eventTracking.sendMB('compile-time-warning-dismissed', { + 'time-since-displayed': getTimeSinceDisplayed(), + time: 30, + }) + setShowCompileTimeWarning(false) + setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true })) + }, [getTimeSinceDisplayed, setShowCompileTimeWarning, setDisplayStatus]) + + const handleUpgradeClick = useCallback(() => { + eventTracking.sendMB('compile-time-warning-upgrade-click', { + 'time-since-displayed': getTimeSinceDisplayed(), + time: 30, + }) + setShowCompileTimeWarning(false) + setDisplayStatus(displayStatus => ({ ...displayStatus, dismissed: true })) + }, [getTimeSinceDisplayed, setShowCompileTimeWarning, setDisplayStatus]) + + if (!showCompileTimeWarning || displayStatus.dismissed) { + return null + } + + return ( +
+ +
+
+ , + }} + /> +
+
+ + {hasNewPaywallCta ? t('get_more_compile_time') : t('upgrade')} + +
+
+
+ ) +} + +export default memo(CompileTimeWarning) diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-timeout-changing-soon.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-changing-soon.tsx new file mode 100644 index 0000000000..9ca3135991 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-changing-soon.tsx @@ -0,0 +1,122 @@ +import Notification from '@/shared/components/notification' +import StartFreeTrialButton from '@/shared/components/start-free-trial-button' +import { Trans, useTranslation } from 'react-i18next' +import * as eventTracking from '@/infrastructure/event-tracking' +import { FC } from 'react' +import { useSplitTestContext } from '@/shared/context/split-test-context' + +const sendInfoClickEvent = () => { + eventTracking.sendMB('paywall-info-click', { + 'paywall-type': 'compile-time-warning', + content: 'blog', + }) +} + +export const CompileTimeoutChangingSoon: FC<{ + isProjectOwner?: boolean + handleDismissChangingSoon: () => void +}> = ({ isProjectOwner = false, handleDismissChangingSoon }) => { + const { t } = useTranslation() + + const { splitTestVariants } = useSplitTestContext() + const hasNewPaywallCta = splitTestVariants['paywall-cta'] === 'enabled' + + const compileTimeoutChangesBlogLink = ( + /* eslint-disable-next-line jsx-a11y/anchor-has-content */ + + ) + + const fixingCompileTimeoutsLearnLink = ( + /* eslint-disable-next-line jsx-a11y/anchor-has-content */ + + ) + + if (isProjectOwner) { + return ( + + {hasNewPaywallCta + ? t('get_more_compile_time') + : t('start_free_trial_without_exclamation')} + + } + ariaLive="polite" + content={ +
+

+ +

+

+ {' '} + }} + /> +

+
+ } + title={t('your_project_compiled_but_soon_might_not')} + type="warning" + isActionBelowContent + isDismissible + onDismiss={handleDismissChangingSoon} + /> + ) + } + + return ( + +

+ {' '} + +

+

+ }} + /> +

+ + } + title={t('this_project_compiled_but_soon_might_not')} + type="warning" + isDismissible + onDismiss={handleDismissChangingSoon} + /> + ) +} diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx index 577da2d186..fcb939a549 100644 --- a/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-messages.tsx @@ -1,23 +1,51 @@ -import { memo, useCallback, useEffect, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } 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 { CompileTimeoutWarning } from '@/features/pdf-preview/components/compile-timeout-warning' +import { CompileTimeoutChangingSoon } from '@/features/pdf-preview/components/compile-timeout-changing-soon' function CompileTimeoutMessages() { - const { isProjectOwner, deliveryLatencies, compiling, showLogs, error } = - useDetachCompileContext() + const { + showNewCompileTimeoutUI, + isProjectOwner, + deliveryLatencies, + compiling, + showLogs, + error, + } = useDetachCompileContext() 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, + isProjectOwner, + } + }, [showNewCompileTimeoutUI, isProjectOwner]) + const handleNewCompile = useCallback( compileTime => { setShowWarning(false) - if (compileTime > 10000) { - if (isProjectOwner) { + 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() @@ -25,25 +53,37 @@ function CompileTimeoutMessages() { setShowWarning(true) eventTracking.sendMB('compile-time-warning-displayed', { time: 10, - isProjectOwner, + ...segmentation, }) } } } }, - [isProjectOwner, dismissedUntilWarning] + [ + isProjectOwner, + showNewCompileTimeoutUI, + dismissedUntilWarning, + segmentation, + ] ) const handleDismissWarning = useCallback(() => { eventTracking.sendMB('compile-time-warning-dismissed', { time: 10, - isProjectOwner, + ...segmentation, }) setShowWarning(false) const until = new Date() until.setDate(until.getDate() + 1) // 1 day setDismissedUntilWarning(until) - }, [isProjectOwner, setDismissedUntilWarning]) + }, [setDismissedUntilWarning, segmentation]) + + const handleDismissChangingSoon = useCallback(() => { + eventTracking.sendMB('compile-time-warning-dismissed', { + time: 20, + ...segmentation, + }) + }, [segmentation]) useEffect(() => { if (compiling || error || showLogs) return @@ -58,16 +98,26 @@ function CompileTimeoutMessages() { return null } - if (!showWarning) { + if (!showWarning && !showChangingSoon) { return null } // if showWarning is true then the 10s warning is shown + // and if showChangingSoon is true then the 20s-60s should show return (
{showWarning && isProjectOwner && ( - + + )} + {showChangingSoon && ( + )}
) diff --git a/services/web/frontend/js/features/pdf-preview/components/compile-timeout-warning.tsx b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-warning.tsx index cbb80300e9..3ab2a235e5 100644 --- a/services/web/frontend/js/features/pdf-preview/components/compile-timeout-warning.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/compile-timeout-warning.tsx @@ -6,7 +6,8 @@ import { useSplitTestContext } from '@/shared/context/split-test-context' export const CompileTimeoutWarning: FC<{ handleDismissWarning: () => void -}> = ({ handleDismissWarning }) => { + showNewCompileTimeoutUI?: string +}> = ({ handleDismissWarning, showNewCompileTimeoutUI }) => { const { t } = useTranslation() const { splitTestVariants } = useSplitTestContext() @@ -33,8 +34,14 @@ export const CompileTimeoutWarning: FC<{
{t('your_project_near_compile_timeout_limit')}
- {t('upgrade_for_12x_more_compile_time')} - {'. '} + {showNewCompileTimeoutUI === 'active' ? ( + <> + {t('upgrade_for_12x_more_compile_time')} + {'. '} + + ) : ( + {t('upgrade_for_plenty_more_compile_time')} + )} } type="warning" diff --git a/services/web/frontend/js/features/pdf-preview/components/faster-compiles-feedback.tsx b/services/web/frontend/js/features/pdf-preview/components/faster-compiles-feedback.tsx new file mode 100644 index 0000000000..da89efc4d4 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/faster-compiles-feedback.tsx @@ -0,0 +1,135 @@ +import { memo, useEffect, useRef, useState } from 'react' +import { Button, Alert } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import Icon from '../../../shared/components/icon' +import { sendMB } from '../../../infrastructure/event-tracking' +import usePersistedState from '../../../shared/hooks/use-persisted-state' +import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' +import { useProjectContext } from '../../../shared/context/project-context' + +const SAY_THANKS_TIMEOUT = 10 * 1000 + +function FasterCompilesFeedbackContent() { + const { clsiServerId, deliveryLatencies, pdfFile, pdfUrl } = + useCompileContext() + const { _id: projectId } = useProjectContext() + + const [incrementalCompiles, setIncrementalCompiles] = useState(0) + const [hasRatedProject, setHasRatedProject] = usePersistedState( + `faster-compiles-feedback:${projectId}`, + false, + true + ) + const [dismiss, setDismiss] = usePersistedState( + 'faster-compiles-feedback:dismiss', + false, + true + ) + const [sayThanks, setSayThanks] = useState(false) + const lastClsiServerId = useRef(undefined) + const lastPdfUrl = useRef(undefined) + + useEffect(() => { + if ( + !pdfUrl || + !lastPdfUrl.current || + clsiServerId !== lastClsiServerId.current + ) { + // Reset history after + // - clearing cache / server error (both reset pdfUrl) + // - initial compile after reset of pdfUrl + // - switching the clsi server, aka we get a _slow_ full compile. + setIncrementalCompiles(0) + lastClsiServerId.current = clsiServerId + } else { + setIncrementalCompiles(n => n + 1) + } + lastPdfUrl.current = pdfUrl + }, [clsiServerId, lastPdfUrl, pdfUrl, setIncrementalCompiles]) + + function submitFeedback(feedback = '') { + sendMB('faster-compiles-feedback', { + projectId, + server: clsiServerId?.includes('-c2d-') ? 'faster' : 'normal', + feedback, + pdfSize: pdfFile?.size, + ...deliveryLatencies, + }) + setHasRatedProject(true) + setSayThanks(true) + window.setTimeout(() => { + setSayThanks(false) + }, SAY_THANKS_TIMEOUT) + } + + function dismissFeedback() { + sendMB('faster-compiles-feedback-dismiss') + setDismiss(true) + } + + const { t } = useTranslation() + + // Hide the feedback prompt in all these cases: + // - the initial compile (0), its always perceived as _slow_. + // - the first incremental compile (1), its always _faster_ than ^. + // - the user has dismissed the prompt + // - the user has rated compile speed already (say thanks if needed) + switch (true) { + case sayThanks: + return ( + setSayThanks(false)} + > + {t('faster_compiles_feedback_thanks')} + + ) + case dismiss || hasRatedProject: + return null + case incrementalCompiles > 1: + return ( + + + {t('faster_compiles_feedback_question')} +
+ {['slower', 'same', 'faster'].map(feedback => ( + + ))} +
+
+ ) + default: + return null + } +} + +function FasterCompilesFeedback() { + const { showFasterCompilesFeedbackUI } = useCompileContext() + + if (!showFasterCompilesFeedbackUI) { + return null + } + return +} + +export default memo(FasterCompilesFeedback) diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.jsx b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.jsx index 1275d135dd..47e652a143 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.jsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-logs-viewer.jsx @@ -3,6 +3,7 @@ import { memo } from 'react' import classnames from 'classnames' import PdfValidationIssue from './pdf-validation-issue' import StopOnFirstErrorPrompt from './stop-on-first-error-prompt' +import TimeoutUpgradePrompt from './timeout-upgrade-prompt' import TimeoutUpgradePromptNew from './timeout-upgrade-prompt-new' import PdfPreviewError from './pdf-preview-error' import PdfClearCacheButton from './pdf-clear-cache-button' @@ -24,6 +25,7 @@ function PdfLogsViewer() { validationIssues, showLogs, stoppedOnFirstError, + showNewCompileTimeoutUI, } = useCompileContext() const { loadingError } = usePdfPreviewContext() @@ -43,10 +45,13 @@ function PdfLogsViewer() { {loadingError && } - {error === 'timedout' ? ( + {showNewCompileTimeoutUI && error === 'timedout' ? ( ) : ( - <>{error && } + <> + {error && } + {error === 'timedout' && } + )} {validationIssues && diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx index bc0cb31005..2529309e6b 100644 --- a/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx +++ b/services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx @@ -5,12 +5,14 @@ import PdfViewer from './pdf-viewer' import { FullSizeLoadingSpinner } from '../../../shared/components/loading-spinner' import PdfHybridPreviewToolbar from './pdf-preview-hybrid-toolbar' import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context' +import FasterCompilesFeedback from './faster-compiles-feedback' import { PdfPreviewMessages } from './pdf-preview-messages' +import CompileTimeWarning from './compile-time-warning' import CompileTimeoutMessages from './compile-timeout-messages' import { PdfPreviewProvider } from './pdf-preview-provider' function PdfPreviewPane() { - const { pdfUrl } = useCompileContext() + const { pdfUrl, showNewCompileTimeoutUI } = useCompileContext() const classes = classNames('pdf', 'full-size', { 'pdf-empty': !pdfUrl, }) @@ -19,11 +21,16 @@ function PdfPreviewPane() { - + {showNewCompileTimeoutUI ? ( + + ) : ( + + )} }>
+
diff --git a/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx index 9999f55ec9..f46ac9dadc 100644 --- a/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt-new.tsx @@ -13,6 +13,7 @@ function TimeoutUpgradePromptNew() { startCompile, lastCompileOptions, setAnimateCompileDropdownArrow, + showNewCompileTimeoutUI, isProjectOwner, } = useDetachCompileContext() @@ -26,25 +27,35 @@ function TimeoutUpgradePromptNew() { setAnimateCompileDropdownArrow(true) }, [enableStopOnFirstError, startCompile, setAnimateCompileDropdownArrow]) + if (!window.ExposedSettings.enableSubscriptions) { + return null + } + + const compileTimeChanging = showNewCompileTimeoutUI === 'changing' + return ( <> - - {window.ExposedSettings.enableSubscriptions && ( - - )} + + ) } type CompileTimeoutProps = { + compileTimeChanging?: boolean isProjectOwner: boolean } const CompileTimeout = memo(function CompileTimeout({ + compileTimeChanging, isProjectOwner, }: CompileTimeoutProps) { const { t } = useTranslation() @@ -56,48 +67,58 @@ const CompileTimeout = memo(function CompileTimeout({ + <> +

+ {isProjectOwner + ? t('your_project_exceeded_compile_timeout_limit_on_free_plan') + : t('this_project_exceeded_compile_timeout_limit_on_free_plan')} +

+ {isProjectOwner ? (

- {isProjectOwner - ? t('your_project_exceeded_compile_timeout_limit_on_free_plan') - : t('this_project_exceeded_compile_timeout_limit_on_free_plan')} + {compileTimeChanging ? ( + <> + {t('upgrade_for_plenty_more_compile_time')}{' '} + {t( + 'plus_additional_collaborators_document_history_track_changes_and_more' + )} + + ) : ( + <> + {t('upgrade_for_12x_more_compile_time')}{' '} + {t( + 'plus_additional_collaborators_document_history_track_changes_and_more' + )} + + )}

- {isProjectOwner ? ( -

- {t('upgrade_for_12x_more_compile_time')}{' '} - {t( - 'plus_additional_collaborators_document_history_track_changes_and_more' - )} -

- ) : ( - , - ]} - /> - )} + ) : ( + , + ]} + /> + )} - {isProjectOwner && ( -

- - {hasNewPaywallCta - ? t('get_more_compile_time') - : t('start_a_free_trial')} - -

- )} - - ) + {isProjectOwner && ( +

+ + {hasNewPaywallCta + ? t('get_more_compile_time') + : t('start_a_free_trial')} + +

+ )} + } // @ts-ignore entryAriaLabel={t('your_compile_timed_out')} @@ -107,12 +128,14 @@ const CompileTimeout = memo(function CompileTimeout({ }) type PreventTimeoutHelpMessageProps = { + compileTimeChanging?: boolean lastCompileOptions: any handleEnableStopOnFirstErrorClick: () => void isProjectOwner: boolean } const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({ + compileTimeChanging, lastCompileOptions, handleEnableStopOnFirstErrorClick, isProjectOwner, @@ -204,19 +227,35 @@ const PreventTimeoutHelpMessage = memo(function PreventTimeoutHelpMessage({

- <> - {isProjectOwner ? ( - - ) : ( - - )} - + {compileTimeChanging ? ( + <> + {isProjectOwner ? ( + + ) : ( + + )} + + ) : ( + <> + {isProjectOwner ? ( + + ) : ( + + )} + + )}

diff --git a/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt.jsx b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt.jsx new file mode 100644 index 0000000000..30343a6792 --- /dev/null +++ b/services/web/frontend/js/features/pdf-preview/components/timeout-upgrade-prompt.jsx @@ -0,0 +1,62 @@ +import { useTranslation } from 'react-i18next' +import { useEditorContext } from '../../../shared/context/editor-context' +import StartFreeTrialButton from '../../../shared/components/start-free-trial-button' +import { memo } from 'react' +import PdfLogEntry from './pdf-log-entry' +import UpgradeBenefits from '../../../shared/components/upgrade-benefits' +import { useSplitTestContext } from '@/shared/context/split-test-context' + +function TimeoutUpgradePrompt() { + const { t } = useTranslation() + + const { hasPremiumCompile, isProjectOwner } = useEditorContext() + + const { splitTestVariants } = useSplitTestContext() + const hasNewPaywallCta = splitTestVariants['paywall-cta'] === 'enabled' + + if (!window.ExposedSettings.enableSubscriptions || hasPremiumCompile) { + return null + } + + return ( + +

{t('free_accounts_have_timeout_upgrade_to_increase')}

+

{t('plus_upgraded_accounts_receive')}:

+
+ +
+ {isProjectOwner && ( +

+ + {hasNewPaywallCta + ? t('get_more_compile_time') + : t('start_free_trial')} + +

+ )} + + } + entryAriaLabel={ + isProjectOwner + ? t('upgrade_for_longer_compiles') + : t('ask_proj_owner_to_upgrade_for_longer_compiles') + } + level="success" + /> + ) +} + +export default memo(TimeoutUpgradePrompt) diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.js b/services/web/frontend/js/features/pdf-preview/util/compiler.js index 6aaf362adc..b481625031 100644 --- a/services/web/frontend/js/features/pdf-preview/util/compiler.js +++ b/services/web/frontend/js/features/pdf-preview/util/compiler.js @@ -221,6 +221,14 @@ export default class DocumentCompiler { 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 } diff --git a/services/web/frontend/js/shared/context/detach-compile-context.tsx b/services/web/frontend/js/shared/context/detach-compile-context.tsx index 6dca121c0b..c63a23e29e 100644 --- a/services/web/frontend/js/shared/context/detach-compile-context.tsx +++ b/services/web/frontend/js/shared/context/detach-compile-context.tsx @@ -52,6 +52,8 @@ export const DetachCompileProvider: FC = ({ children }) => { setStopOnValidationError: _setStopOnValidationError, showLogs: _showLogs, showCompileTimeWarning: _showCompileTimeWarning, + showNewCompileTimeoutUI: _showNewCompileTimeoutUI, + showFasterCompilesFeedbackUI: _showFasterCompilesFeedbackUI, stopOnFirstError: _stopOnFirstError, stopOnValidationError: _stopOnValidationError, stoppedOnFirstError: _stoppedOnFirstError, @@ -190,6 +192,18 @@ export const DetachCompileProvider: FC = ({ children }) => { 'detacher', 'detached' ) + const [showNewCompileTimeoutUI] = useDetachStateWatcher( + 'showNewCompileTimeoutUI', + _showNewCompileTimeoutUI, + 'detacher', + 'detached' + ) + const [showFasterCompilesFeedbackUI] = useDetachStateWatcher( + 'showFasterCompilesFeedbackUI', + _showFasterCompilesFeedbackUI, + 'detacher', + 'detached' + ) const [stopOnFirstError] = useDetachStateWatcher( 'stopOnFirstError', _stopOnFirstError, @@ -399,6 +413,8 @@ export const DetachCompileProvider: FC = ({ children }) => { setStopOnValidationError, showLogs, showCompileTimeWarning, + showNewCompileTimeoutUI, + showFasterCompilesFeedbackUI, startCompile, stopCompile, stopOnFirstError, @@ -450,6 +466,8 @@ export const DetachCompileProvider: FC = ({ children }) => { setStopOnValidationError, showCompileTimeWarning, showLogs, + showNewCompileTimeoutUI, + showFasterCompilesFeedbackUI, startCompile, stopCompile, stopOnFirstError, diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx index 92371374f4..6fded1ce16 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.tsx +++ b/services/web/frontend/js/shared/context/local-compile-context.tsx @@ -72,6 +72,8 @@ export type CompileContext = { setStopOnValidationError: (value: boolean) => void showCompileTimeWarning: boolean showLogs: boolean + showNewCompileTimeoutUI?: string + showFasterCompilesFeedbackUI: boolean stopOnFirstError: boolean stopOnValidationError: boolean stoppedOnFirstError: boolean @@ -101,7 +103,11 @@ export const LocalCompileProvider: FC = ({ children }) => { const { hasPremiumCompile, isProjectOwner } = useEditorContext() - const { _id: projectId, rootDocId } = useProjectContext() + const { + _id: projectId, + rootDocId, + showNewCompileTimeoutUI, + } = useProjectContext() const { pdfPreviewOpen } = useLayoutContext() @@ -176,6 +182,10 @@ export const LocalCompileProvider: FC = ({ children }) => { // whether the logs should be visible const [showLogs, setShowLogs] = useState(false) + // whether the faster compiles feedback UI should be displayed + const [showFasterCompilesFeedbackUI, setShowFasterCompilesFeedbackUI] = + useState(false) + // whether the compile dropdown arrow should be animated const [animateCompileDropdownArrow, setAnimateCompileDropdownArrow] = useState(false) @@ -348,6 +358,9 @@ export const LocalCompileProvider: FC = ({ children }) => { if (data.clsiServerId) { setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController } + setShowFasterCompilesFeedbackUI( + Boolean(data.showFasterCompilesFeedbackUI) + ) if (data.outputFiles) { const outputFiles = new Map() @@ -620,6 +633,8 @@ export const LocalCompileProvider: FC = ({ children }) => { setStopOnFirstError, setStopOnValidationError, showLogs, + showNewCompileTimeoutUI, + showFasterCompilesFeedbackUI, startCompile, stopCompile, stopOnFirstError, @@ -668,6 +683,8 @@ export const LocalCompileProvider: FC = ({ children }) => { setStopOnValidationError, showCompileTimeWarning, showLogs, + showNewCompileTimeoutUI, + showFasterCompilesFeedbackUI, startCompile, stopCompile, stopOnFirstError, diff --git a/services/web/frontend/js/shared/context/project-context.tsx b/services/web/frontend/js/shared/context/project-context.tsx index 58f5398906..f016bc54b4 100644 --- a/services/web/frontend/js/shared/context/project-context.tsx +++ b/services/web/frontend/js/shared/context/project-context.tsx @@ -30,6 +30,7 @@ const ProjectContext = createContext< _id: UserId email: string } + showNewCompileTimeoutUI?: string tags: { _id: string name: string @@ -73,6 +74,7 @@ export const ProjectProvider: FC = ({ children }) => { features, publicAccesLevel: publicAccessLevel, owner, + showNewCompileTimeoutUI, trackChangesState, } = project || projectFallback @@ -84,6 +86,17 @@ export const ProjectProvider: FC = ({ children }) => { [] ) + // 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(() => { return { _id, @@ -94,6 +107,8 @@ export const ProjectProvider: FC = ({ children }) => { features, publicAccessLevel, owner, + showNewCompileTimeoutUI: + newCompileTimeoutOverride || showNewCompileTimeoutUI, tags, trackChangesState, } @@ -106,6 +121,8 @@ export const ProjectProvider: FC = ({ children }) => { features, publicAccessLevel, owner, + showNewCompileTimeoutUI, + newCompileTimeoutOverride, tags, trackChangesState, ]) diff --git a/services/web/frontend/stories/pdf-preview-messages.stories.jsx b/services/web/frontend/stories/pdf-preview-messages.stories.jsx index a5b736cb84..a857e61a05 100644 --- a/services/web/frontend/stories/pdf-preview-messages.stories.jsx +++ b/services/web/frontend/stories/pdf-preview-messages.stories.jsx @@ -2,7 +2,7 @@ import { ScopeDecorator } from './decorators/scope' import { useLocalCompileContext } from '../js/shared/context/local-compile-context' import { useEffect } from 'react' import { PdfPreviewMessages } from '../js/features/pdf-preview/components/pdf-preview-messages' -import CompileTimeoutMessages from '@/features/pdf-preview/components/compile-timeout-messages' +import CompileTimeWarning from '../js/features/pdf-preview/components/compile-time-warning' export default { title: 'Editor / PDF Preview / Messages', @@ -20,7 +20,7 @@ export const Dismissible = () => { return (
- +
) diff --git a/services/web/frontend/stories/pdf-preview-messages.stories.tsx b/services/web/frontend/stories/pdf-preview-messages.stories.tsx index 3cea716d26..87c0a49a2e 100644 --- a/services/web/frontend/stories/pdf-preview-messages.stories.tsx +++ b/services/web/frontend/stories/pdf-preview-messages.stories.tsx @@ -1,5 +1,9 @@ +import { useEffect } from 'react' import { ScopeDecorator } from './decorators/scope' +import { useLocalCompileContext } from '@/shared/context/local-compile-context' import { PdfPreviewMessages } from '@/features/pdf-preview/components/pdf-preview-messages' +import CompileTimeWarning from '@/features/pdf-preview/components/compile-time-warning' +import { CompileTimeoutChangingSoon } from '@/features/pdf-preview/components/compile-timeout-changing-soon' import { CompileTimeoutWarning } from '@/features/pdf-preview/components/compile-timeout-warning' export default { @@ -17,9 +21,40 @@ export default { ], } +export const CompileTime = () => { + const { setShowCompileTimeWarning } = useLocalCompileContext() + + useEffect(() => { + setShowCompileTimeWarning(true) + }, [setShowCompileTimeWarning]) + + return +} + +export const CompileTimeoutChangingSoonNotProjectOwner = (args: any) => { + return +} +CompileTimeoutChangingSoonNotProjectOwner.argTypes = { + handleDismissChangingSoon: { action: 'dismiss changing soon' }, +} + +export const CompileTimeoutChangingSoonProjectOwner = (args: any) => { + return +} +CompileTimeoutChangingSoonProjectOwner.argTypes = { + handleDismissChangingSoon: { action: 'dismiss changing soon' }, +} + export const CompileTimeoutWarningActive = (args: any) => { - return + return } CompileTimeoutWarningActive.argTypes = { handleDismissWarning: { action: 'dismiss warning' }, } + +export const CompileTimeoutWarningChanging = (args: any) => { + return +} +CompileTimeoutWarningChanging.argTypes = { + handleDismissWarning: { action: 'dismiss warning' }, +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index fd2b886c25..63b774346b 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -106,6 +106,7 @@ "an_email_has_already_been_sent_to": "An email has already been sent to <0>__email__. Please wait and try again later.", "an_error_occurred_when_verifying_the_coupon_code": "An error occurred when verifying the coupon code", "and": "and", + "and_you_can_upgrade_for_plenty_more_compile_time": "And you can upgrade to get plenty more compile time.", "annual": "Annual", "annual_billing_enabled": "Annual billing enabled", "anonymous": "Anonymous", @@ -114,6 +115,7 @@ "app_on_x": "__appName__ on __social__", "apply_educational_discount": "Apply educational discount", "apply_educational_discount_info": "Overleaf offers a 40% educational discount for groups of 10 or more. Applies to students or faculty using Overleaf for teaching.", + "approaching_compile_timeout_limit_upgrade_for_more_compile_time": "You are approaching your compile timeout limit. Upgrade to Overleaf Premium for <0>4x more compile time.", "april": "April", "archive": "Archive", "archive_projects": "Archive Projects", @@ -130,6 +132,7 @@ "ascending": "Ascending", "ask_proj_owner_to_unlink_from_current_github": "Ask the owner of the project (<0>__projectOwnerEmail__) to unlink the project from the current GitHub repository and create a connection to a different repository.", "ask_proj_owner_to_upgrade_for_full_history": "Please ask the project owner to upgrade to access this project’s full history.", + "ask_proj_owner_to_upgrade_for_longer_compiles": "Please ask the project owner to upgrade to increase the timeout limit.", "ask_proj_owner_to_upgrade_for_references_search": "Please ask the project owner to upgrade to use the References Search feature.", "ask_repo_owner_to_reconnect": "Ask the GitHub repository owner (<0>__repoOwnerEmail__) to reconnect the project.", "ask_repo_owner_to_renew_overleaf_subscription": "Ask the GitHub repository owner (<0>__repoOwnerEmail__) to renew their __appName__ subscription and reconnect the project.", @@ -290,6 +293,7 @@ "compile_timeout_short": "Compile timeout", "compile_timeout_short_info_basic": "This is how much time you get to compile your project on the Overleaf servers. You may need additional time for longer or more complex projects.", "compiler": "Compiler", + "compiles_on_our_free_plan_are_now_on_faster_servers": "Compiles on our free plan are now on faster servers, and we’re gradually <0>introducing a shorter compile timeout limit. This project currently exceeds the new limit, so it might not compile in future.", "compiling": "Compiling", "complete": "Complete", "compliance": "Compliance", @@ -416,6 +420,7 @@ "disabling": "Disabling", "disconnected": "Disconnected", "discount_of": "Discount of __amount__", + "dismiss": "Dismiss", "dismiss_error_popup": "Dismiss first error alert", "display_deleted_user": "Display deleted users", "do_not_have_acct_or_do_not_want_to_link": "If you don’t have an __appName__ account, or if you don’t want to link to your __institutionName__ account, please click __clickText__.", @@ -574,6 +579,11 @@ "faq_what_is_the_difference_between_users_and_collaborators_answer_second_paragraph": "In other words, collaborators are just other Overleaf users that you are working with on one of your projects.", "faq_what_is_the_difference_between_users_and_collaborators_question": "What’s the difference between users and collaborators?", "fast": "Fast", + "faster_compiles_feedback_question": "Was this compile different than usual?", + "faster_compiles_feedback_seems_faster": "Faster", + "faster_compiles_feedback_seems_same": "Same", + "faster_compiles_feedback_seems_slower": "Slower", + "faster_compiles_feedback_thanks": "Thanks for the feedback!", "fastest": "Fastest", "feature_included": "Feature included", "feature_not_included": "Feature not included", @@ -640,6 +650,7 @@ "free": "Free", "free_7_day_trial_billed_annually": "Free 7-day trial, then billed annually", "free_7_day_trial_billed_monthly": "Free 7-day trial, then billed monthly", + "free_accounts_have_timeout_upgrade_to_increase": "Free accounts have a one minute timeout, whereas upgraded accounts have a timeout of four minutes.", "free_dropbox_and_history": "Free Dropbox and History", "free_plan_label": "You’re on the free plan", "free_plan_tooltip": "Click to find out how you could benefit from Overleaf premium features.", @@ -1360,6 +1371,7 @@ "please_wait": "Please wait", "plus_additional_collaborators_document_history_track_changes_and_more": "(plus additional collaborators, document history, track changes, and more).", "plus_more": "plus more", + "plus_upgraded_accounts_receive": "Plus with an upgraded account you get", "popular_tags": "Popular Tags", "portal_add_affiliation_to_join": "It looks like you are already logged in to __appName__. If you have a __portalTitle__ email you can add it now.", "position": "Position", @@ -1442,6 +1454,7 @@ "react_history_tutorial_content": "To compare a range of versions, use the <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.", "react_history_tutorial_title": "History actions have a new home", "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_token": "Read-Only Token", @@ -1851,6 +1864,7 @@ "tc_switch_guests_tip": "Toggle track-changes for all link-sharing guests", "tc_switch_user_tip": "Toggle track-changes for this user", "tell_the_project_owner_and_ask_them_to_upgrade": "<0>Tell the project owner 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": "Tell the project owner and ask them to upgrade their Overleaf plan if you need more compile time.", "template": "Template", "template_approved_by_publisher": "This template has been approved by the publisher", "template_description": "Template Description", @@ -1904,6 +1918,7 @@ "this_field_is_required": "This field is required", "this_grants_access_to_features_2": "This grants you access to <0>__appName__ <0>__featureType__ features.", "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_read_only": "This project is public and can be viewed but not edited by anyone with the URL", @@ -2071,6 +2086,8 @@ "upgrade": "Upgrade", "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_plenty_more_compile_time": "Upgrade to get plenty more compile time.", "upgrade_now": "Upgrade Now", "upgrade_to_get_feature": "Upgrade to get __feature__, plus:", "upgrade_to_track_changes": "Upgrade to track changes", @@ -2138,6 +2155,8 @@ "website_status": "Website status", "wed_love_you_to_stay": "We’d love you to stay", "welcome_to_sl": "Welcome to __appName__", + "were_in_the_process_of_reducing_compile_timeout_which_may_affect_this_project": "We’re in the process of <0>reducing the compile timeout limit on our free plan, which may affect this project in future.", + "were_in_the_process_of_reducing_compile_timeout_which_may_affect_your_project": "We’re in the process of <0>reducing the compile timeout limit on our free plan, which may affect your project in future.", "were_performing_maintenance": "We’re performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_this_project": "We’ve recently <0>reduced the compile timeout limit on our free plan, which may have affected this project.", "weve_recently_reduced_the_compile_timeout_limit_which_may_have_affected_your_project": "We’ve recently <0>reduced the compile timeout limit on our free plan, which may have affected your project.", @@ -2208,6 +2227,7 @@ "you_have_been_removed_from_this_project_and_will_be_redirected_to_project_dashboard": "You have been removed from this project, and will no longer have access to it. You will be redirected to your project dashboard momentarily.", "you_introed_high_number": " You’ve introduced <0>__numberOfPeople__ people to __appName__. Good job!", "you_introed_small_number": " You’ve introduced <0>__numberOfPeople__ person to __appName__. Good job, but can you get some more?", + "you_may_be_able_to_fix_issues_to_speed_up_the_compile": "You may be able to fix issues to <0>speed up the compile.", "you_need_to_configure_your_sso_settings": "You need to configure and test your SSO settings before enabling SSO", "you_not_introed_anyone_to_sl": "You’ve not introduced anyone to __appName__ yet. Get sharing!", "you_plus_1": "You + 1", @@ -2235,6 +2255,7 @@ "your_password_has_been_successfully_changed": "Your password has been successfully changed", "your_plan": "Your plan", "your_plan_is_changing_at_term_end": "Your plan is changing to <0>__pendingPlanName__ 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", diff --git a/services/web/scripts/migration_compile_timeout_60s_to_20s.js b/services/web/scripts/migration_compile_timeout_60s_to_20s.js deleted file mode 100644 index 5f18f248fa..0000000000 --- a/services/web/scripts/migration_compile_timeout_60s_to_20s.js +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env node - -const minimist = require('minimist') -const { - db, - READ_PREFERENCE_SECONDARY, - waitForDb, -} = require('../app/src/infrastructure/mongodb') -const { batchedUpdate } = require('./helpers/batchedUpdate') - -async function logCount() { - const count60s = await db.users.countDocuments( - { 'features.compileTimeout': { $lte: 60, $ne: 20 } }, - { readPreference: READ_PREFERENCE_SECONDARY } - ) - const count20s = await db.users.countDocuments( - { 'features.compileTimeout': 20 }, - { readPreference: READ_PREFERENCE_SECONDARY } - ) - console.log(`Found ${count60s} users with compileTimeout <= 60s && != 20s`) - console.log(`Found ${count20s} users with compileTimeout == 20s`) -} - -const main = async ({ COMMIT }) => { - console.time('Script Duration') - - await waitForDb() - - await logCount() - - if (COMMIT) { - const nModified = await batchedUpdate( - 'users', - { 'features.compileTimeout': { $lte: 60, $ne: 20 } }, - { $set: { 'features.compileTimeout': 20 } } - ) - console.log(`Updated ${nModified} records`) - } - - console.timeEnd('Script Duration') -} - -const setup = () => { - const argv = minimist(process.argv.slice(2)) - const COMMIT = argv.commit !== undefined - if (!COMMIT) { - console.warn('Doing dry run. Add --commit to commit changes') - } - return { COMMIT } -} - -main(setup()) - .catch(err => { - console.error(err) - process.exit(1) - }) - .then(() => process.exit(0)) diff --git a/services/web/test/acceptance/src/MigrateUserFeatureTimeoutTests.js b/services/web/test/acceptance/src/MigrateUserFeatureTimeoutTests.js deleted file mode 100644 index a256ce8e78..0000000000 --- a/services/web/test/acceptance/src/MigrateUserFeatureTimeoutTests.js +++ /dev/null @@ -1,134 +0,0 @@ -const { expect } = require('chai') -const { promisify } = require('node:util') -const { exec } = require('node:child_process') -const logger = require('@overleaf/logger') -const { db } = require('../../../app/src/infrastructure/mongodb') - -async function runScript(args = []) { - try { - return await promisify(exec)( - ['node', 'scripts/migration_compile_timeout_60s_to_20s.js', ...args].join( - ' ' - ) - ) - } catch (error) { - logger.error({ error }, 'script failed') - throw error - } -} - -describe('MigrateUserFeatureTimeoutTests', function () { - const usersInput = { - noFeatures: {}, - noFeatureTimeout: { features: {} }, - timeout10s: { features: { compileTimeout: 10, other: 'val1' }, bar: '1' }, - timeout20s: { features: { compileTimeout: 20, other: 'val2' }, bar: '2' }, - timeout30s: { features: { compileTimeout: 30, other: 'val3' }, bar: '3' }, - timeout60s: { features: { compileTimeout: 60, other: 'val4' }, bar: '4' }, - timeout120s: { features: { compileTimeout: 120, other: 'val5' }, bar: '5' }, - timeout180s: { features: { compileTimeout: 180, other: 'val6' }, bar: '6' }, - } - - const usersKeys = Object.keys(usersInput) - const userIds = {} - - beforeEach('insert users', async function () { - const usersInsertedValues = await db.users.insertMany( - usersKeys.map(key => ({ - ...usersInput[key], - email: `${key}@example.com`, - })) - ) - usersKeys.forEach( - (key, index) => (userIds[key] = usersInsertedValues.insertedIds[index]) - ) - }) - - it('gives correct counts in dry mode', async function () { - const users = await db.users.find().toArray() - expect(users).to.have.lengthOf(usersKeys.length) - - const result = await runScript([]) - - expect(result.stderr).to.contain( - 'Doing dry run. Add --commit to commit changes' - ) - expect(result.stdout).to.contain( - 'Found 3 users with compileTimeout <= 60s && != 20s' - ) - expect(result.stdout).to.contain('Found 1 users with compileTimeout == 20s') - expect(result.stdout).not.to.contain('Updated') - - const usersAfter = await db.users.find().toArray() - - expect(usersAfter).to.deep.equal(users) - }) - - it("updates users compileTimeout when '--commit' is set", async function () { - const users = await db.users.find().toArray() - expect(users).to.have.lengthOf(usersKeys.length) - const result = await runScript(['--commit']) - - expect(result.stdout).to.contain( - 'Found 3 users with compileTimeout <= 60s && != 20s' - ) - expect(result.stdout).to.contain('Found 1 users with compileTimeout == 20s') - expect(result.stdout).to.contain('Updated 3 records') - - const usersAfter = await db.users.find().toArray() - - expect(usersAfter).to.deep.equal([ - { _id: userIds.noFeatures, email: 'noFeatures@example.com' }, - { - _id: userIds.noFeatureTimeout, - email: 'noFeatureTimeout@example.com', - features: {}, - }, - { - _id: userIds.timeout10s, - email: 'timeout10s@example.com', - features: { compileTimeout: 20, other: 'val1' }, - bar: '1', - }, - { - _id: userIds.timeout20s, - email: 'timeout20s@example.com', - features: { compileTimeout: 20, other: 'val2' }, - bar: '2', - }, - { - _id: userIds.timeout30s, - email: 'timeout30s@example.com', - features: { compileTimeout: 20, other: 'val3' }, - bar: '3', - }, - { - _id: userIds.timeout60s, - email: 'timeout60s@example.com', - features: { compileTimeout: 20, other: 'val4' }, - bar: '4', - }, - { - _id: userIds.timeout120s, - email: 'timeout120s@example.com', - features: { compileTimeout: 120, other: 'val5' }, - bar: '5', - }, - { - _id: userIds.timeout180s, - email: 'timeout180s@example.com', - features: { compileTimeout: 180, other: 'val6' }, - bar: '6', - }, - ]) - - const result2 = await runScript([]) - - expect(result2.stdout).to.contain( - 'Found 0 users with compileTimeout <= 60s && != 20s' - ) - expect(result2.stdout).to.contain( - 'Found 4 users with compileTimeout == 20s' - ) - }) -}) diff --git a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx index 59278a93dc..2295232019 100644 --- a/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx +++ b/services/web/test/frontend/components/pdf-preview/pdf-preview.spec.tsx @@ -314,7 +314,7 @@ describe('', function () { 'project-too-large': 'Project too large', 'rate-limited': 'Compile rate limit hit', terminated: 'Compilation cancelled', - timedout: 'Your compile timed out', + timedout: 'Timed out', 'too-recently-compiled': 'This project was compiled very recently, so this compile has been skipped.', unavailable: diff --git a/services/web/test/unit/src/Compile/ClsiManagerTests.js b/services/web/test/unit/src/Compile/ClsiManagerTests.js index 2abcfee70b..a0f58897d5 100644 --- a/services/web/test/unit/src/Compile/ClsiManagerTests.js +++ b/services/web/test/unit/src/Compile/ClsiManagerTests.js @@ -120,6 +120,7 @@ describe('ClsiManager', function () { }, clsi: { url: `http://${CLSI_HOST}`, + defaultBackendClass: 'e2', submissionBackendClass: 'n2d', }, clsi_priority: { diff --git a/services/web/test/unit/src/Compile/CompileControllerTests.js b/services/web/test/unit/src/Compile/CompileControllerTests.js index ed88c64abd..03de9fb465 100644 --- a/services/web/test/unit/src/Compile/CompileControllerTests.js +++ b/services/web/test/unit/src/Compile/CompileControllerTests.js @@ -32,6 +32,7 @@ describe('CompileController', function () { apis: { clsi: { url: 'http://clsi.example.com', + defaultBackendClass: 'e2', submissionBackendClass: 'n2d', }, clsi_priority: { diff --git a/services/web/test/unit/src/Compile/CompileManagerTests.js b/services/web/test/unit/src/Compile/CompileManagerTests.js index e9501c647a..2b49d6853f 100644 --- a/services/web/test/unit/src/Compile/CompileManagerTests.js +++ b/services/web/test/unit/src/Compile/CompileManagerTests.js @@ -23,7 +23,7 @@ describe('CompileManager', function () { requires: { '@overleaf/settings': (this.settings = { apis: { - clsi: { submissionBackendClass: 'n2d' }, + clsi: { defaultBackendClass: 'e2', submissionBackendClass: 'n2d' }, }, redis: { web: { host: 'localhost', port: 42 } }, rateLimit: { autoCompile: {} }, @@ -235,8 +235,9 @@ describe('CompileManager', function () { .calledWith(null, { timeout: this.timeout, compileGroup: this.group, - compileBackendClass: 'c2d', + compileBackendClass: 'e2', ownerAnalyticsId: 'abc', + showFasterCompilesFeedbackUI: false, }) .should.equal(true) }) @@ -302,7 +303,7 @@ describe('CompileManager', function () { this.callback ) this.callback - .calledWith(null, sinon.match({ timeout: 20 })) + .calledWith(null, sinon.match({ timeout: 60 })) .should.equal(true) }) describe('user is in the compile-timeout-20s-existing-users treatment', function () { @@ -387,12 +388,20 @@ describe('CompileManager', function () { this.callback ) this.callback - .calledWith(null, sinon.match({ timeout: 20 })) + .calledWith(null, sinon.match({ timeout: 60 })) .should.equal(true) }) }) describe('user signed up after the second phase rollout', function () { + beforeEach(function () { + const signUpDate = new Date( + this.CompileManager.NEW_COMPILE_TIMEOUT_ENFORCED_CUTOFF_DEFAULT_BASELINE + ) + signUpDate.setDate(signUpDate.getDate() + 1) + this.user.signUpDate = signUpDate + }) + it('should reduce compile timeout to 20s', function () { this.CompileManager.getProjectCompileLimits( this.project_id, @@ -432,12 +441,13 @@ describe('CompileManager', function () { variant: 'default', }) }) - it('should return the n2d class and disable the ui', function (done) { + it('should return the e2 class and disable the ui', function (done) { this.CompileManager.getProjectCompileLimits( this.project_id, - (err, { compileBackendClass }) => { + (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => { if (err) return done(err) - expect(compileBackendClass).to.equal('n2d') + expect(compileBackendClass).to.equal('e2') + expect(showFasterCompilesFeedbackUI).to.equal(false) done() } ) @@ -453,9 +463,10 @@ describe('CompileManager', function () { it('should return the n2d class and disable the ui', function (done) { this.CompileManager.getProjectCompileLimits( this.project_id, - (err, { compileBackendClass }) => { + (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => { if (err) return done(err) expect(compileBackendClass).to.equal('n2d') + expect(showFasterCompilesFeedbackUI).to.equal(false) done() } ) @@ -478,9 +489,10 @@ describe('CompileManager', function () { it('should return the default class and disable ui', function (done) { this.CompileManager.getProjectCompileLimits( this.project_id, - (err, { compileBackendClass }) => { + (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => { if (err) return done(err) - expect(compileBackendClass).to.equal('c2d') + expect(compileBackendClass).to.equal('e2') + expect(showFasterCompilesFeedbackUI).to.equal(false) done() } ) @@ -498,9 +510,10 @@ describe('CompileManager', function () { it('should return the default class and enable ui', function (done) { this.CompileManager.getProjectCompileLimits( this.project_id, - (err, { compileBackendClass }) => { + (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => { if (err) return done(err) - expect(compileBackendClass).to.equal('c2d') + expect(compileBackendClass).to.equal('e2') + expect(showFasterCompilesFeedbackUI).to.equal(true) done() } ) @@ -517,9 +530,10 @@ describe('CompileManager', function () { it('should return the c2d class and enable ui', function (done) { this.CompileManager.getProjectCompileLimits( this.project_id, - (err, { compileBackendClass }) => { + (err, { compileBackendClass, showFasterCompilesFeedbackUI }) => { if (err) return done(err) expect(compileBackendClass).to.equal('c2d') + expect(showFasterCompilesFeedbackUI).to.equal(true) done() } )