From f22fea3519c8b20cb559c394564878c4a5ab996a Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 17 Dec 2025 14:34:23 -0800 Subject: [PATCH] perf: optimize journey path resolver and middleware (#58955) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/journeys/lib/journey-path-resolver.ts | 90 +++++++++++++++++------ src/journeys/middleware/journey-track.ts | 10 ++- 2 files changed, 74 insertions(+), 26 deletions(-) diff --git a/src/journeys/lib/journey-path-resolver.ts b/src/journeys/lib/journey-path-resolver.ts index b1136fbdec..ec93200dfe 100644 --- a/src/journeys/lib/journey-path-resolver.ts +++ b/src/journeys/lib/journey-path-resolver.ts @@ -68,6 +68,13 @@ type ContentContext = { // Cache for journey pages so we only filter all pages once let cachedJourneyPages: JourneyPage[] | null = null +// Cache for guide paths to quickly check if a page is part of any journey +let cachedGuidePaths: Set | null = null +let hasDynamicGuides = false + +function needsRendering(str: string): boolean { + return str.includes('{{') || str.includes('{%') || str.includes('[') || str.includes('<') +} function getJourneyPages(pages: Pages): JourneyPage[] { if (!cachedJourneyPages) { @@ -78,6 +85,27 @@ function getJourneyPages(pages: Pages): JourneyPage[] { return cachedJourneyPages } +function getGuidePaths(pages: Pages): Set { + if (!cachedGuidePaths) { + cachedGuidePaths = new Set() + const journeyPages = getJourneyPages(pages) + for (const page of journeyPages) { + if (!page.journeyTracks) continue + for (const track of page.journeyTracks) { + if (!track.guides) continue + for (const guide of track.guides) { + if (needsRendering(guide.href)) { + hasDynamicGuides = true + } else { + cachedGuidePaths.add(normalizeGuidePath(guide.href)) + } + } + } + } + } + return cachedGuidePaths +} + function normalizeGuidePath(path: string): string { // First ensure we have a leading slash for consistent processing const pathWithSlash = path.startsWith('/') ? path : `/${path}` @@ -133,6 +161,16 @@ export async function resolveJourneyContext( ): Promise { const normalizedPath = normalizeGuidePath(articlePath) + // Optimization: Fast path check + // If we are not forcing a specific journey page, check our global cache + if (!currentJourneyPage) { + const guidePaths = getGuidePaths(pages) + // If we have no dynamic guides and this path isn't in our known guides, return null early. + if (!hasDynamicGuides && !guidePaths.has(normalizedPath)) { + return null + } + } + // Use the current journey page if provided, otherwise find all journey pages const journeyPages = currentJourneyPage ? [currentJourneyPage] : getJourneyPages(pages) @@ -165,15 +203,17 @@ export async function resolveJourneyContext( let renderedGuidePath = guidePath // Handle Liquid conditionals in guide paths - try { - renderedGuidePath = await executeWithFallback( - context, - () => renderContent(guidePath, context, { textOnly: true }), - () => guidePath, - ) - } catch { - // If rendering fails, use the original path rather than erroring - renderedGuidePath = guidePath + if (needsRendering(guidePath)) { + try { + renderedGuidePath = await executeWithFallback( + context, + () => renderContent(guidePath, context, { textOnly: true }), + () => guidePath, + ) + } catch { + // If rendering fails, use the original path rather than erroring + renderedGuidePath = guidePath + } } const normalizedGuidePath = normalizeGuidePath(renderedGuidePath) @@ -189,15 +229,17 @@ export async function resolveJourneyContext( let renderedAlternativeNextStep = alternativeNextStep // Handle Liquid conditionals in branching text which likely has links - try { - renderedAlternativeNextStep = await executeWithFallback( - context, - () => renderContent(alternativeNextStep, context), - () => alternativeNextStep, - ) - } catch { - // If rendering fails, use the original branching text rather than erroring - renderedAlternativeNextStep = alternativeNextStep + if (needsRendering(alternativeNextStep)) { + try { + renderedAlternativeNextStep = await executeWithFallback( + context, + () => renderContent(alternativeNextStep, context), + () => alternativeNextStep, + ) + } catch { + // If rendering fails, use the original branching text rather than erroring + renderedAlternativeNextStep = alternativeNextStep + } } result = { @@ -278,10 +320,14 @@ export async function resolveJourneyTracks( const result = await Promise.all( journeyTracks.map(async (track) => { // Render Liquid templates in title and description - const renderedTitle = await renderContent(track.title, context, { textOnly: true }) - const renderedDescription = track.description - ? await renderContent(track.description, context, { textOnly: true }) - : undefined + const renderedTitle = needsRendering(track.title) + ? await renderContent(track.title, context, { textOnly: true }) + : track.title + + const renderedDescription = + track.description && needsRendering(track.description) + ? await renderContent(track.description, context, { textOnly: true }) + : track.description const guides = await Promise.all( track.guides.map(async (guide: { href: string; alternativeNextStep?: string }) => { diff --git a/src/journeys/middleware/journey-track.ts b/src/journeys/middleware/journey-track.ts index 481ba58ca6..38ddfe618a 100644 --- a/src/journeys/middleware/journey-track.ts +++ b/src/journeys/middleware/journey-track.ts @@ -1,20 +1,22 @@ import type { Response, NextFunction } from 'express' import type { ExtendedRequest, Context } from '@/types' +import { resolveJourneyTracks, resolveJourneyContext } from '../lib/journey-path-resolver' + export default async function journeyTrack( req: ExtendedRequest & { context: Context }, res: Response, next: NextFunction, ) { + if (req.method !== 'GET' && req.method !== 'HEAD') return next() + if (!req.context) throw new Error('request is not contextualized') if (!req.context.page) return next() try { - const journeyResolver = await import('../lib/journey-path-resolver') - // If this page has journey tracks defined, resolve them for the landing page if ((req.context.page as any).journeyTracks) { - const resolvedTracks = await journeyResolver.resolveJourneyTracks( + const resolvedTracks = await resolveJourneyTracks( (req.context.page as any).journeyTracks, req.context, ) @@ -24,7 +26,7 @@ export default async function journeyTrack( } // Always try to resolve journey context (for navigation on guide articles) - const journeyContext = await journeyResolver.resolveJourneyContext( + const journeyContext = await resolveJourneyContext( req.pagePath || '', req.context.pages || {}, req.context,