From 138cb5af4c84061c90c9906f08c608e8bdc01df9 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 11:25:34 -0400 Subject: [PATCH 01/11] fix(orchestrator): handle missing framework before skill preflight Stop the main integration flow with DetectNoFramework when detection finds no framework, before it can surface a misleading missing-skill error. Include Tal Gluck's replay-vision fix and actionable guidance from https://github.com/PostHog/wizard/pull/1205. Thank you, Tal, for identifying and fixing the replay-vision path. Carry that work in this signed commit alongside the main integration fix and regression coverage for both flows. Co-authored-by: Tal Gluck --- .../programs/__tests__/no-framework.test.ts | 90 +++++++++++++++++++ .../__tests__/detect.test.ts | 4 +- .../programs/posthog-integration/detect.ts | 9 ++ src/lib/programs/replay-vision/index.ts | 20 ++++- 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/lib/programs/__tests__/no-framework.test.ts diff --git a/src/lib/programs/__tests__/no-framework.test.ts b/src/lib/programs/__tests__/no-framework.test.ts new file mode 100644 index 000000000..045a1efa2 --- /dev/null +++ b/src/lib/programs/__tests__/no-framework.test.ts @@ -0,0 +1,90 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { WizardStore } from '@ui/tui/store'; +import { InkUI } from '@ui/tui/ink-ui'; +import { setUI } from '@ui/index'; +import { buildSession, OutroKind } from '@lib/wizard-session'; +import { ErrorCodes } from '@shared/errors'; +import { Integration } from '@shared/constants'; + +vi.mock('@utils/analytics', () => ({ + analytics: { + capture: vi.fn(), + wizardCapture: vi.fn(), + setTag: vi.fn(), + captureException: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }, + sessionProperties: vi.fn(() => ({})), +})); + +describe.each([ + { + program: 'posthog-integration' as const, + integration: Integration.javascriptNode, + dependencies: {}, + message: 'Could not auto-detect your framework', + }, + { + program: 'replay-vision' as const, + integration: Integration.nextjs, + dependencies: { next: '^15.0.0' }, + message: "Replay vision couldn't detect a framework", + }, +])( + '$program framework detection', + ({ program, integration, dependencies, message }) => { + let installDir: string; + let store: WizardStore; + const exit = new Error('wizard exited'); + + beforeEach(() => { + installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-framework-')); + store = new WizardStore(program); + store.session = buildSession({ installDir }); + const ui = new InkUI(store); + vi.spyOn(ui, 'waitForOutroDismissed').mockResolvedValue(undefined); + setUI(ui); + vi.spyOn(process, 'exit').mockImplementation(() => { + throw exit; + }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(installDir, { recursive: true, force: true }); + }); + + it('shows the no-framework error before completing detection in an empty project', async () => { + await expect(store.runReadyHooks()).rejects.toBe(exit); + + expect(store.session.outroData).toEqual( + expect.objectContaining({ + kind: OutroKind.Error, + errorCode: ErrorCodes.DetectNoFramework, + message: expect.stringContaining(message), + }), + ); + expect(store.session.detectionComplete).toBe(false); + expect(store.session.outroData?.message).toContain( + "app's root directory", + ); + expect(process.exit).toHaveBeenCalledWith(1); + }); + + it('completes detection and selects the skill for a recognized project', async () => { + fs.writeFileSync( + path.join(installDir, 'package.json'), + JSON.stringify({ dependencies }), + ); + + await store.runReadyHooks(); + + expect(store.session.integration).toBe(integration); + expect(store.session.skillId).toBe(integration); + expect(store.session.detectionComplete).toBe(true); + expect(process.exit).not.toHaveBeenCalled(); + }); + }, +); diff --git a/src/lib/programs/posthog-integration/__tests__/detect.test.ts b/src/lib/programs/posthog-integration/__tests__/detect.test.ts index 456abdfd8..5f7b9f38f 100644 --- a/src/lib/programs/posthog-integration/__tests__/detect.test.ts +++ b/src/lib/programs/posthog-integration/__tests__/detect.test.ts @@ -46,7 +46,9 @@ import { import type { DetectedSource } from '@lib/warehouse-sources/types'; function makeTmpDir(): string { - return fs.mkdtempSync(path.join(os.tmpdir(), 'warehouse-reporting-')); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'warehouse-reporting-')); + fs.writeFileSync(path.join(dir, 'package.json'), '{}'); + return dir; } function cleanup(dir: string): void { diff --git a/src/lib/programs/posthog-integration/detect.ts b/src/lib/programs/posthog-integration/detect.ts index f27faa6a9..b29676e2f 100644 --- a/src/lib/programs/posthog-integration/detect.ts +++ b/src/lib/programs/posthog-integration/detect.ts @@ -24,6 +24,8 @@ import { checkFrameworkVersion, } from '@lib/detection/index'; import { analytics } from '@utils/analytics'; +import { wizardAbort } from '@utils/wizard-abort'; +import { ErrorCodes } from '@shared/errors'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import { AI_SOURCE_KINDS } from '@lib/warehouse-sources/registry'; import type { DetectedSource } from '@lib/warehouse-sources/types'; @@ -77,6 +79,13 @@ export async function detectPostHogIntegration( if (versionResult.supported !== true) { ctx.setUnsupportedVersion(versionResult.supported); } + } else { + await wizardAbort({ + code: ErrorCodes.DetectNoFramework, + message: + "Could not auto-detect your framework for this project.\n\nRun the wizard from your app's root directory (where its package.json or equivalent project file lives). See supported frameworks and manual setup instructions at:\n https://posthog.com/docs/libraries", + }); + return; } // Feature discovery diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index ef01e6a9d..f8307ac10 100644 --- a/src/lib/programs/replay-vision/index.ts +++ b/src/lib/programs/replay-vision/index.ts @@ -74,6 +74,19 @@ async function abortUnsupportedPlatform( }); } +async function abortNoFrameworkDetected(): Promise { + await wizardAbort({ + code: ErrorCodes.DetectNoFramework, + message: + "Replay vision couldn't detect a framework here, so it has nothing " + + 'to scope its scanners to.\n\n' + + "Make sure you're running this from your app's root directory " + + '(where its package.json or framework-equivalent lives), not an ' + + 'empty or unrelated folder. See what replay supports at:\n' + + ' https://posthog.com/docs/session-replay', + }); +} + /** * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. * Kept in sync with the stop conditions in the skill's `description.md` @@ -108,7 +121,12 @@ const DETECT_STEP: ProgramStep = { // be the stale pre-copy object (see the warning in detect.ts). onReady: async (ctx: ProgramReadyContext) => { const integration = await detectFramework(ctx.session.installDir); - if (integration && !REPLAY_VISION_SUPPORTED.has(integration)) { + if (!integration) { + // Stop before skill preflight, matching the CI path below. + await abortNoFrameworkDetected(); + return; + } + if (!REPLAY_VISION_SUPPORTED.has(integration)) { await abortUnsupportedPlatform(integration); return; } From 56595cb0fc1f904a66827ec9c28cee4ef1e2c0c0 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 11:35:31 -0400 Subject: [PATCH 02/11] fix(tui): show early aborts before incomplete setup screens Route early error outros before walking the program's incomplete screens. Framework-detection aborts otherwise wait for dismissal while the intro keeps displaying its detection spinner. Exit after dismissal and preserve the existing failed-run handoff. Exercise the real outro dismissal wait for both integration and replay vision, and check early-error routing across registered programs. --- .../programs/__tests__/no-framework.test.ts | 35 +++++++++++-------- src/ui/tui/__tests__/router.test.ts | 17 +++++++++ src/ui/tui/router.ts | 25 ++++++------- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/lib/programs/__tests__/no-framework.test.ts b/src/lib/programs/__tests__/no-framework.test.ts index 045a1efa2..07bcb7661 100644 --- a/src/lib/programs/__tests__/no-framework.test.ts +++ b/src/lib/programs/__tests__/no-framework.test.ts @@ -1,7 +1,7 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { WizardStore } from '@ui/tui/store'; +import { WizardStore, ScreenId } from '@ui/tui/store'; import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui/index'; import { buildSession, OutroKind } from '@lib/wizard-session'; @@ -44,7 +44,6 @@ describe.each([ store = new WizardStore(program); store.session = buildSession({ installDir }); const ui = new InkUI(store); - vi.spyOn(ui, 'waitForOutroDismissed').mockResolvedValue(undefined); setUI(ui); vi.spyOn(process, 'exit').mockImplementation(() => { throw exit; @@ -57,19 +56,27 @@ describe.each([ }); it('shows the no-framework error before completing detection in an empty project', async () => { - await expect(store.runReadyHooks()).rejects.toBe(exit); + const finished = expect(store.runReadyHooks()).rejects.toBe(exit); + try { + await vi.waitFor(() => expect(store.session.outroData).not.toBeNull()); - expect(store.session.outroData).toEqual( - expect.objectContaining({ - kind: OutroKind.Error, - errorCode: ErrorCodes.DetectNoFramework, - message: expect.stringContaining(message), - }), - ); - expect(store.session.detectionComplete).toBe(false); - expect(store.session.outroData?.message).toContain( - "app's root directory", - ); + expect(store.session.outroData).toEqual( + expect.objectContaining({ + kind: OutroKind.Error, + errorCode: ErrorCodes.DetectNoFramework, + message: expect.stringContaining(message), + }), + ); + expect(store.session.detectionComplete).toBe(false); + expect(store.router.resolve(store.session)).toBe(ScreenId.Outro); + expect(store.session.outroData?.message).toContain( + "app's root directory", + ); + expect(process.exit).not.toHaveBeenCalled(); + } finally { + store.setOutroDismissed(); + await finished; + } expect(process.exit).toHaveBeenCalledWith(1); }); diff --git a/src/ui/tui/__tests__/router.test.ts b/src/ui/tui/__tests__/router.test.ts index 89b94150f..62c1d49fc 100644 --- a/src/ui/tui/__tests__/router.test.ts +++ b/src/ui/tui/__tests__/router.test.ts @@ -29,6 +29,23 @@ function failedRunSession() { } describe('WizardRouter', () => { + it.each(PROGRAM_REGISTRY.map((program) => program.id))( + 'shows and dismisses an early error before setup completes in %s', + (program) => { + const router = new WizardRouter(program); + const session = baseWizardSession(); + session.runPhase = RunPhase.Error; + session.outroData = { + kind: OutroKind.Error, + message: 'detection failed', + }; + + expect(router.resolve(session)).toBe(ScreenId.Outro); + session.outroDismissed = true; + expect(router.resolve(session)).toBe(ScreenId.Exit); + }, + ); + it.each(PROGRAM_REGISTRY.map((program) => program.id))( 'shows a failed run over every step and overlay in %s', (program) => { diff --git a/src/ui/tui/router.ts b/src/ui/tui/router.ts index 8c2dc9836..5b17e5e6a 100644 --- a/src/ui/tui/router.ts +++ b/src/ui/tui/router.ts @@ -12,7 +12,7 @@ * No switch statements, no hardcoded transitions in business logic. */ -import { RunPhase, type WizardSession } from '@lib/wizard-session'; +import { OutroKind, RunPhase, type WizardSession } from '@lib/wizard-session'; import { isRunFailure } from '@ui/mint-failure'; import { Program, type ProgramId } from '@lib/programs/program-registry'; import { @@ -78,24 +78,19 @@ export class WizardRouter { return this.overlays[this.overlays.length - 1]; } + // Early aborts must be dismissible even when intro, setup, or auth is incomplete. + if ( + !runFailed && + session.runPhase === RunPhase.Error && + session.outroData?.kind === OutroKind.Error + ) { + return session.outroDismissed ? ScreenId.Exit : ScreenId.Outro; + } + const sequence = runFailed ? MINT_HANDOFF_SEQUENCE : this.sequence; for (const entry of sequence) { if (entry.show && !entry.show(session)) continue; if (entry.isComplete && entry.isComplete(session)) continue; - // A failed login aborts the run: wizardAbort renders the error outro - // and then waits for its dismissal. But the auth step only completes - // on credentials — which an aborted login never set — so the walk - // would park here forever: auth spinner up, outro unreachable, and - // that wait deadlocked. Route to the outro so the error can be read - // and dismissed. Auth only: the run steps already complete on - // RunPhase.Error, so later aborts reach their program's own outro. - if ( - entry.id === ScreenId.Auth && - session.runPhase === RunPhase.Error && - session.outroData - ) { - return ScreenId.Outro; - } return entry.id; } From 4ba6804865027842199e2854ba096170795d8831 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 11:40:54 -0400 Subject: [PATCH 03/11] refactor(programs): share the no-framework abort Use one abort helper for integration and replay-vision detection in both interactive and CI paths. Keep replay-specific guidance with its program and provide the same actionable message to CI runs. Extend the no-framework regression coverage to both CI pre-run hooks. --- .../programs/__tests__/no-framework.test.ts | 62 ++++++++++++------- .../programs/posthog-integration/detect.ts | 9 +-- src/lib/programs/posthog-integration/index.ts | 8 +-- src/lib/programs/replay-vision/index.ts | 27 +++----- src/lib/programs/shared/abort-no-framework.ts | 14 +++++ 5 files changed, 67 insertions(+), 53 deletions(-) create mode 100644 src/lib/programs/shared/abort-no-framework.ts diff --git a/src/lib/programs/__tests__/no-framework.test.ts b/src/lib/programs/__tests__/no-framework.test.ts index 07bcb7661..217f2bd83 100644 --- a/src/lib/programs/__tests__/no-framework.test.ts +++ b/src/lib/programs/__tests__/no-framework.test.ts @@ -7,6 +7,11 @@ import { setUI } from '@ui/index'; import { buildSession, OutroKind } from '@lib/wizard-session'; import { ErrorCodes } from '@shared/errors'; import { Integration } from '@shared/constants'; +import { getProgramConfig } from '@lib/programs/program-registry'; + +vi.mock('@lib/detection/project-scope', () => ({ + scopeInstallDirToProject: vi.fn().mockResolvedValue(undefined), +})); vi.mock('@utils/analytics', () => ({ analytics: { @@ -55,30 +60,41 @@ describe.each([ fs.rmSync(installDir, { recursive: true, force: true }); }); - it('shows the no-framework error before completing detection in an empty project', async () => { - const finished = expect(store.runReadyHooks()).rejects.toBe(exit); - try { - await vi.waitFor(() => expect(store.session.outroData).not.toBeNull()); + it.each(['interactive', 'ci'] as const)( + 'shows the no-framework error for an empty project in %s mode', + async (mode) => { + store.session = buildSession({ installDir, ci: mode === 'ci' }); + const detection = + mode === 'ci' + ? getProgramConfig(program).ciPreRun?.(store.session) + : store.runReadyHooks(); + if (!detection) throw new Error('expected a detection hook'); + const finished = expect(detection).rejects.toBe(exit); + try { + await vi.waitFor(() => + expect(store.session.outroData).not.toBeNull(), + ); - expect(store.session.outroData).toEqual( - expect.objectContaining({ - kind: OutroKind.Error, - errorCode: ErrorCodes.DetectNoFramework, - message: expect.stringContaining(message), - }), - ); - expect(store.session.detectionComplete).toBe(false); - expect(store.router.resolve(store.session)).toBe(ScreenId.Outro); - expect(store.session.outroData?.message).toContain( - "app's root directory", - ); - expect(process.exit).not.toHaveBeenCalled(); - } finally { - store.setOutroDismissed(); - await finished; - } - expect(process.exit).toHaveBeenCalledWith(1); - }); + expect(store.session.outroData).toEqual( + expect.objectContaining({ + kind: OutroKind.Error, + errorCode: ErrorCodes.DetectNoFramework, + message: expect.stringContaining(message), + }), + ); + expect(store.session.detectionComplete).toBe(false); + expect(store.router.resolve(store.session)).toBe(ScreenId.Outro); + expect(store.session.outroData?.message).toContain( + "app's root directory", + ); + expect(process.exit).not.toHaveBeenCalled(); + } finally { + store.setOutroDismissed(); + await finished; + } + expect(process.exit).toHaveBeenCalledWith(1); + }, + ); it('completes detection and selects the skill for a recognized project', async () => { fs.writeFileSync( diff --git a/src/lib/programs/posthog-integration/detect.ts b/src/lib/programs/posthog-integration/detect.ts index b29676e2f..a1302678a 100644 --- a/src/lib/programs/posthog-integration/detect.ts +++ b/src/lib/programs/posthog-integration/detect.ts @@ -24,8 +24,7 @@ import { checkFrameworkVersion, } from '@lib/detection/index'; import { analytics } from '@utils/analytics'; -import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@shared/errors'; +import { abortNoFrameworkDetected } from '@lib/programs/shared/abort-no-framework'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import { AI_SOURCE_KINDS } from '@lib/warehouse-sources/registry'; import type { DetectedSource } from '@lib/warehouse-sources/types'; @@ -80,11 +79,7 @@ export async function detectPostHogIntegration( ctx.setUnsupportedVersion(versionResult.supported); } } else { - await wizardAbort({ - code: ErrorCodes.DetectNoFramework, - message: - "Could not auto-detect your framework for this project.\n\nRun the wizard from your app's root directory (where its package.json or equivalent project file lives). See supported frameworks and manual setup instructions at:\n https://posthog.com/docs/libraries", - }); + await abortNoFrameworkDetected(); return; } diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index 5bfed9021..e0a0da6f3 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -13,8 +13,7 @@ import { analytics } from '@utils/analytics'; import { detectFramework, gatherFrameworkContext } from '@lib/detection/index'; import { scopeInstallDirToProject } from '@lib/detection/project-scope'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; -import { wizardAbort } from '@utils/wizard-abort'; -import { ErrorCodes } from '@shared/errors'; +import { abortNoFrameworkDetected } from '@lib/programs/shared/abort-no-framework'; import { WIZARD_DEFAULT_AIO_LOGS_FLAG_KEY, WIZARD_INTERACTION_EVENT_NAME, @@ -264,10 +263,7 @@ export const posthogIntegrationConfig: ProgramConfig = { const integration = await detectFramework(session.installDir); if (!integration) { - await wizardAbort({ - code: ErrorCodes.DetectNoFramework, - message: 'Could not auto-detect your framework for this project.', - }); + await abortNoFrameworkDetected(); return; } session.integration = integration; diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index f8307ac10..50310eed9 100644 --- a/src/lib/programs/replay-vision/index.ts +++ b/src/lib/programs/replay-vision/index.ts @@ -15,6 +15,7 @@ import type { WizardSession } from '@lib/wizard-session'; import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; +import { abortNoFrameworkDetected } from '@lib/programs/shared/abort-no-framework'; const REPLAY_VISION_REPORT_FILE = 'posthog-replay-vision-report.md'; @@ -74,18 +75,13 @@ async function abortUnsupportedPlatform( }); } -async function abortNoFrameworkDetected(): Promise { - await wizardAbort({ - code: ErrorCodes.DetectNoFramework, - message: - "Replay vision couldn't detect a framework here, so it has nothing " + - 'to scope its scanners to.\n\n' + - "Make sure you're running this from your app's root directory " + - '(where its package.json or framework-equivalent lives), not an ' + - 'empty or unrelated folder. See what replay supports at:\n' + - ' https://posthog.com/docs/session-replay', - }); -} +const NO_FRAMEWORK_MESSAGE = + "Replay vision couldn't detect a framework here, so it has nothing " + + 'to scope its scanners to.\n\n' + + "Make sure you're running this from your app's root directory " + + '(where its package.json or framework-equivalent lives), not an ' + + 'empty or unrelated folder. See what replay supports at:\n' + + ' https://posthog.com/docs/session-replay'; /** * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. @@ -123,7 +119,7 @@ const DETECT_STEP: ProgramStep = { const integration = await detectFramework(ctx.session.installDir); if (!integration) { // Stop before skill preflight, matching the CI path below. - await abortNoFrameworkDetected(); + await abortNoFrameworkDetected(NO_FRAMEWORK_MESSAGE); return; } if (!REPLAY_VISION_SUPPORTED.has(integration)) { @@ -191,10 +187,7 @@ export const replayVisionConfig: ProgramConfig = { const integration = await detectFramework(session.installDir); if (!integration) { - await wizardAbort({ - code: ErrorCodes.DetectNoFramework, - message: 'Could not auto-detect your framework for this project.', - }); + await abortNoFrameworkDetected(NO_FRAMEWORK_MESSAGE); return; } if (!REPLAY_VISION_SUPPORTED.has(integration)) { diff --git a/src/lib/programs/shared/abort-no-framework.ts b/src/lib/programs/shared/abort-no-framework.ts new file mode 100644 index 000000000..7a7e6c49c --- /dev/null +++ b/src/lib/programs/shared/abort-no-framework.ts @@ -0,0 +1,14 @@ +import { ErrorCodes } from '@shared/errors'; +import { wizardAbort } from '@utils/wizard-abort'; + +const NO_FRAMEWORK_MESSAGE = + 'Could not auto-detect your framework for this project.\n\n' + + "Run the wizard from your app's root directory (where its package.json or " + + 'equivalent project file lives). See supported frameworks and manual setup instructions at:\n' + + ' https://posthog.com/docs/libraries'; + +export function abortNoFrameworkDetected( + message = NO_FRAMEWORK_MESSAGE, +): Promise { + return wizardAbort({ code: ErrorCodes.DetectNoFramework, message }); +} From 19e327805792258ad39ddac0beea21defcb901fa Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 11:57:07 -0400 Subject: [PATCH 04/11] fix(tui): space out framework detection error guidance Separate the short error heading, recovery instruction, supporting detail, and labeled documentation link. Give the terminal exit hint extra space and name the actual exit action. Preserve recovery instructions in CI logs. Use structured outro data for the shared no-framework abort and keep replay-specific headings and links in the replay program. --- src/agent/progress.ts | 4 +++ .../programs/__tests__/no-framework.test.ts | 4 +-- src/lib/programs/replay-vision/index.ts | 16 +++++----- src/lib/programs/shared/abort-no-framework.ts | 24 ++++++++++----- src/ui/logging-ui.ts | 4 ++- src/ui/tui/screens/OutroScreen.tsx | 30 ++++++++++++++----- 6 files changed, 55 insertions(+), 27 deletions(-) diff --git a/src/agent/progress.ts b/src/agent/progress.ts index 83e4c4551..6c5cdb44b 100644 --- a/src/agent/progress.ts +++ b/src/agent/progress.ts @@ -24,6 +24,8 @@ export interface OutroData { kind: OutroKind; /** Main headline (green check for Success, red X for Error, etc.) */ message?: string; + /** Error-only: the primary recovery instruction, above the supporting body. */ + instruction?: string; /** Free-form body text shown under the headline. Use \n for paragraph breaks. */ body?: string; /** Success-only: bulleted list of "what the agent did" */ @@ -42,6 +44,8 @@ export interface OutroData { */ nextSteps?: { heading: string; items: string[] }; docsUrl?: string; + /** Optional label above an error's documentation link. */ + docsLabel?: string; continueUrl?: string; /** Report file the agent wrote (e.g. "posthog-setup-report.md") */ reportFile?: string; diff --git a/src/lib/programs/__tests__/no-framework.test.ts b/src/lib/programs/__tests__/no-framework.test.ts index 217f2bd83..c657a7e6a 100644 --- a/src/lib/programs/__tests__/no-framework.test.ts +++ b/src/lib/programs/__tests__/no-framework.test.ts @@ -29,7 +29,7 @@ describe.each([ program: 'posthog-integration' as const, integration: Integration.javascriptNode, dependencies: {}, - message: 'Could not auto-detect your framework', + message: 'Could not detect a framework', }, { program: 'replay-vision' as const, @@ -84,7 +84,7 @@ describe.each([ ); expect(store.session.detectionComplete).toBe(false); expect(store.router.resolve(store.session)).toBe(ScreenId.Outro); - expect(store.session.outroData?.message).toContain( + expect(store.session.outroData?.instruction).toContain( "app's root directory", ); expect(process.exit).not.toHaveBeenCalled(); diff --git a/src/lib/programs/replay-vision/index.ts b/src/lib/programs/replay-vision/index.ts index 50310eed9..de5b07257 100644 --- a/src/lib/programs/replay-vision/index.ts +++ b/src/lib/programs/replay-vision/index.ts @@ -75,13 +75,11 @@ async function abortUnsupportedPlatform( }); } -const NO_FRAMEWORK_MESSAGE = - "Replay vision couldn't detect a framework here, so it has nothing " + - 'to scope its scanners to.\n\n' + - "Make sure you're running this from your app's root directory " + - '(where its package.json or framework-equivalent lives), not an ' + - 'empty or unrelated folder. See what replay supports at:\n' + - ' https://posthog.com/docs/session-replay'; +const NO_FRAMEWORK_GUIDANCE = { + message: "Replay vision couldn't detect a framework", + docsLabel: 'Supported replay platforms:', + docsUrl: 'https://posthog.com/docs/session-replay', +}; /** * `[ABORT]` reasons the replay-vision skill emits when the run can't proceed. @@ -119,7 +117,7 @@ const DETECT_STEP: ProgramStep = { const integration = await detectFramework(ctx.session.installDir); if (!integration) { // Stop before skill preflight, matching the CI path below. - await abortNoFrameworkDetected(NO_FRAMEWORK_MESSAGE); + await abortNoFrameworkDetected(NO_FRAMEWORK_GUIDANCE); return; } if (!REPLAY_VISION_SUPPORTED.has(integration)) { @@ -187,7 +185,7 @@ export const replayVisionConfig: ProgramConfig = { const integration = await detectFramework(session.installDir); if (!integration) { - await abortNoFrameworkDetected(NO_FRAMEWORK_MESSAGE); + await abortNoFrameworkDetected(NO_FRAMEWORK_GUIDANCE); return; } if (!REPLAY_VISION_SUPPORTED.has(integration)) { diff --git a/src/lib/programs/shared/abort-no-framework.ts b/src/lib/programs/shared/abort-no-framework.ts index 7a7e6c49c..061a454c7 100644 --- a/src/lib/programs/shared/abort-no-framework.ts +++ b/src/lib/programs/shared/abort-no-framework.ts @@ -1,14 +1,22 @@ import { ErrorCodes } from '@shared/errors'; import { wizardAbort } from '@utils/wizard-abort'; - -const NO_FRAMEWORK_MESSAGE = - 'Could not auto-detect your framework for this project.\n\n' + - "Run the wizard from your app's root directory (where its package.json or " + - 'equivalent project file lives). See supported frameworks and manual setup instructions at:\n' + - ' https://posthog.com/docs/libraries'; +import { OutroKind, type OutroData } from '@lib/wizard-session'; export function abortNoFrameworkDetected( - message = NO_FRAMEWORK_MESSAGE, + overrides: Pick = {}, ): Promise { - return wizardAbort({ code: ErrorCodes.DetectNoFramework, message }); + const message = overrides.message ?? 'Could not detect a framework'; + return wizardAbort({ + code: ErrorCodes.DetectNoFramework, + message, + outroData: { + kind: OutroKind.Error, + message, + instruction: "Run the wizard from your app's root directory.", + body: "That's the folder containing package.json or an equivalent project file.", + docsLabel: 'Supported frameworks and manual setup:', + docsUrl: 'https://posthog.com/docs/libraries', + ...overrides, + }, + }); } diff --git a/src/ui/logging-ui.ts b/src/ui/logging-ui.ts index 3ddb26b92..c275cf8fa 100644 --- a/src/ui/logging-ui.ts +++ b/src/ui/logging-ui.ts @@ -38,8 +38,10 @@ export class LoggingUI implements WizardUI { outroError(data: OutroData): void { console.log(`✖ ${data.message ?? 'Wizard aborted'}`); + if (data.instruction) console.log(`│ ${data.instruction}`); if (data.body) console.log(`│ ${data.body}`); - if (data.docsUrl) console.log(`│ Docs: ${data.docsUrl}`); + if (data.docsUrl) + console.log(`│ ${data.docsLabel ?? 'Docs:'} ${data.docsUrl}`); } waitForOutroDismissed(): Promise { diff --git a/src/ui/tui/screens/OutroScreen.tsx b/src/ui/tui/screens/OutroScreen.tsx index 0ce4ca01e..59bdc44b1 100644 --- a/src/ui/tui/screens/OutroScreen.tsx +++ b/src/ui/tui/screens/OutroScreen.tsx @@ -176,17 +176,30 @@ export const OutroScreen = ({ store }: OutroScreenProps) => { ✘ {outroData.message || 'An error occurred'} - {outroData.body && ( + {outroData.instruction && ( + {outroData.instruction} + + )} + + {outroData.body && ( + {outroData.body} )} {outroData.docsUrl && ( - - - Docs: {outroData.docsUrl} - + + {outroData.docsLabel ? ( + <> + {outroData.docsLabel} + {outroData.docsUrl} + + ) : ( + + Docs: {outroData.docsUrl} + + )} )} @@ -198,8 +211,11 @@ export const OutroScreen = ({ store }: OutroScreenProps) => { )} - - Press any key to continue + + + Press any key to{' '} + {outroData.kind === OutroKind.Error ? 'exit' : 'continue'} + ); From be6ea8dd15464e38fac37a6803ce68a9a015d7cc Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 12:07:29 -0400 Subject: [PATCH 05/11] fix(integration): preserve manual framework selection Let interactive detection finish without a framework so users can select one manually. Guard the integration run entry before constructing a run when no framework configuration exists. Keep CI and replay guards. Cover detection miss, keyboard selection, and missing-framework run abort. --- .../programs/__tests__/no-framework.test.ts | 64 +++++++++++++++++-- .../__tests__/detect.test.ts | 4 +- .../programs/posthog-integration/detect.ts | 4 -- src/lib/programs/posthog-integration/index.ts | 5 +- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/lib/programs/__tests__/no-framework.test.ts b/src/lib/programs/__tests__/no-framework.test.ts index c657a7e6a..23f2c8be3 100644 --- a/src/lib/programs/__tests__/no-framework.test.ts +++ b/src/lib/programs/__tests__/no-framework.test.ts @@ -1,3 +1,7 @@ +import { createElement } from 'react'; +import { render, cleanup } from 'ink-testing-library'; +import * as detection from '@lib/detection/index'; +import { PostHogIntegrationIntroScreen } from '@ui/tui/screens/PostHogIntegrationIntroScreen'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -7,14 +11,20 @@ import { setUI } from '@ui/index'; import { buildSession, OutroKind } from '@lib/wizard-session'; import { ErrorCodes } from '@shared/errors'; import { Integration } from '@shared/constants'; +import { analytics } from '@utils/analytics'; import { getProgramConfig } from '@lib/programs/program-registry'; +vi.mock('ink', () => + vi.importActual('../../../../node_modules/ink/build/index.js'), +); + vi.mock('@lib/detection/project-scope', () => ({ scopeInstallDirToProject: vi.fn().mockResolvedValue(undefined), })); vi.mock('@utils/analytics', () => ({ analytics: { + getAllFlagsForWizard: vi.fn().mockResolvedValue({}), capture: vi.fn(), wizardCapture: vi.fn(), setTag: vi.fn(), @@ -30,21 +40,24 @@ describe.each([ integration: Integration.javascriptNode, dependencies: {}, message: 'Could not detect a framework', + modes: ['ci', 'run'] as const, }, { program: 'replay-vision' as const, integration: Integration.nextjs, dependencies: { next: '^15.0.0' }, message: "Replay vision couldn't detect a framework", + modes: ['interactive', 'ci'] as const, }, ])( '$program framework detection', - ({ program, integration, dependencies, message }) => { + ({ program, integration, dependencies, message, modes }) => { let installDir: string; let store: WizardStore; const exit = new Error('wizard exited'); beforeEach(() => { + vi.mocked(analytics.getAllFlagsForWizard).mockResolvedValue({}); installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'no-framework-')); store = new WizardStore(program); store.session = buildSession({ installDir }); @@ -56,17 +69,21 @@ describe.each([ }); afterEach(() => { + cleanup(); vi.restoreAllMocks(); fs.rmSync(installDir, { recursive: true, force: true }); }); - it.each(['interactive', 'ci'] as const)( + it.each(modes)( 'shows the no-framework error for an empty project in %s mode', async (mode) => { store.session = buildSession({ installDir, ci: mode === 'ci' }); + const config = getProgramConfig(program); const detection = - mode === 'ci' - ? getProgramConfig(program).ciPreRun?.(store.session) + mode === 'run' && typeof config.run === 'function' + ? config.run(store.session) + : mode === 'ci' + ? config.ciPreRun?.(store.session) : store.runReadyHooks(); if (!detection) throw new Error('expected a detection hook'); const finished = expect(detection).rejects.toBe(exit); @@ -96,6 +113,45 @@ describe.each([ }, ); + if (program === 'posthog-integration') { + it('lets users pick a supported framework after auto-detection misses their project', async () => { + fs.writeFileSync( + path.join(installDir, 'package.json'), + JSON.stringify({ dependencies: { next: '^15.0.0' } }), + ); + vi.spyOn(detection, 'detectFramework').mockResolvedValueOnce(undefined); + + await store.runReadyHooks(); + + expect(store.session.detectionComplete).toBe(true); + expect(store.session.frameworkConfig).toBeNull(); + expect(store.session.outroData).toBeNull(); + expect(store.router.resolve(store.session)).toBe(ScreenId.Intro); + const screen = render( + createElement(PostHogIntegrationIntroScreen, { store }), + ); + await vi.waitFor(() => + expect(screen.lastFrame()).toContain('Select your framework'), + ); + + screen.stdin.write('\r'); + + await vi.waitFor(() => + expect(store.session.integration).toBe(Integration.nextjs), + ); + await vi.waitFor(() => + expect(screen.lastFrame()).toContain('Continue'), + ); + const config = getProgramConfig(program); + if (typeof config.run !== 'function') + throw new Error('expected run hook'); + const run = await config.run(store.session); + expect(run.integrationLabel).toBe(Integration.nextjs); + expect(store.session.outroData).toBeNull(); + expect(process.exit).not.toHaveBeenCalled(); + }); + } + it('completes detection and selects the skill for a recognized project', async () => { fs.writeFileSync( path.join(installDir, 'package.json'), diff --git a/src/lib/programs/posthog-integration/__tests__/detect.test.ts b/src/lib/programs/posthog-integration/__tests__/detect.test.ts index 5f7b9f38f..456abdfd8 100644 --- a/src/lib/programs/posthog-integration/__tests__/detect.test.ts +++ b/src/lib/programs/posthog-integration/__tests__/detect.test.ts @@ -46,9 +46,7 @@ import { import type { DetectedSource } from '@lib/warehouse-sources/types'; function makeTmpDir(): string { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'warehouse-reporting-')); - fs.writeFileSync(path.join(dir, 'package.json'), '{}'); - return dir; + return fs.mkdtempSync(path.join(os.tmpdir(), 'warehouse-reporting-')); } function cleanup(dir: string): void { diff --git a/src/lib/programs/posthog-integration/detect.ts b/src/lib/programs/posthog-integration/detect.ts index a1302678a..f27faa6a9 100644 --- a/src/lib/programs/posthog-integration/detect.ts +++ b/src/lib/programs/posthog-integration/detect.ts @@ -24,7 +24,6 @@ import { checkFrameworkVersion, } from '@lib/detection/index'; import { analytics } from '@utils/analytics'; -import { abortNoFrameworkDetected } from '@lib/programs/shared/abort-no-framework'; import { detectWarehouseSources } from '@lib/warehouse-sources/detect'; import { AI_SOURCE_KINDS } from '@lib/warehouse-sources/registry'; import type { DetectedSource } from '@lib/warehouse-sources/types'; @@ -78,9 +77,6 @@ export async function detectPostHogIntegration( if (versionResult.supported !== true) { ctx.setUnsupportedVersion(versionResult.supported); } - } else { - await abortNoFrameworkDetected(); - return; } // Feature discovery diff --git a/src/lib/programs/posthog-integration/index.ts b/src/lib/programs/posthog-integration/index.ts index e0a0da6f3..08f30f2e9 100644 --- a/src/lib/programs/posthog-integration/index.ts +++ b/src/lib/programs/posthog-integration/index.ts @@ -288,7 +288,10 @@ export const posthogIntegrationConfig: ProgramConfig = { }, run: async (session: WizardSession): Promise => { - const config = session.frameworkConfig!; + const config = session.frameworkConfig; + if (!config) { + return abortNoFrameworkDetected(); + } const typeScriptDetected = isUsingTypeScript({ installDir: session.installDir, From d8c09f26ea21b8ddbf9a976de33bb12bf0c67ef5 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Thu, 24 Sep 2026 12:16:57 -0400 Subject: [PATCH 06/11] fix(ui): clarify app root recovery copy --- src/lib/programs/__tests__/no-framework.test.ts | 2 +- src/lib/programs/shared/abort-no-framework.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/programs/__tests__/no-framework.test.ts b/src/lib/programs/__tests__/no-framework.test.ts index 23f2c8be3..34895e16f 100644 --- a/src/lib/programs/__tests__/no-framework.test.ts +++ b/src/lib/programs/__tests__/no-framework.test.ts @@ -102,7 +102,7 @@ describe.each([ expect(store.session.detectionComplete).toBe(false); expect(store.router.resolve(store.session)).toBe(ScreenId.Outro); expect(store.session.outroData?.instruction).toContain( - "app's root directory", + 'an app root directory', ); expect(process.exit).not.toHaveBeenCalled(); } finally { diff --git a/src/lib/programs/shared/abort-no-framework.ts b/src/lib/programs/shared/abort-no-framework.ts index 061a454c7..e2200f5c7 100644 --- a/src/lib/programs/shared/abort-no-framework.ts +++ b/src/lib/programs/shared/abort-no-framework.ts @@ -12,7 +12,7 @@ export function abortNoFrameworkDetected( outroData: { kind: OutroKind.Error, message, - instruction: "Run the wizard from your app's root directory.", + instruction: 'Run the wizard from an app root directory.', body: "That's the folder containing package.json or an equivalent project file.", docsLabel: 'Supported frameworks and manual setup:', docsUrl: 'https://posthog.com/docs/libraries', From f681acb96e4e66516ccfe489a41e360843e97d76 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:02:34 -0400 Subject: [PATCH 07/11] refactor(error-tracking): reuse the no-framework abort (#1349) --- .../programs/__tests__/error-tracking.test.ts | 17 +++++++++++++++++ src/lib/programs/error-tracking/index.ts | 6 ++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/lib/programs/__tests__/error-tracking.test.ts b/src/lib/programs/__tests__/error-tracking.test.ts index bf49fb9c5..64ee0f26c 100644 --- a/src/lib/programs/__tests__/error-tracking.test.ts +++ b/src/lib/programs/__tests__/error-tracking.test.ts @@ -162,6 +162,23 @@ describe('error-tracking project picker report', () => { }); describe('error-tracking ciPreRun', () => { + test('stops when no framework is detected', async () => { + vi.mocked(detectFramework).mockResolvedValue(undefined); + const session = { + installDir: '/tmp/error-tracking-ci', + frameworkContext: {}, + } as unknown as WizardSession; + + await errorTrackingConfig.ciPreRun?.(session); + + expect(wizardAbort).toHaveBeenCalledWith( + expect.objectContaining({ code: ErrorCodes.DetectNoFramework }), + ); + expect(session.integration).toBeUndefined(); + expect(session.frameworkConfig).toBeUndefined(); + expect(session.skillId).toBeUndefined(); + }); + test('stops KMP before it sets the framework', async () => { vi.mocked(detectFramework).mockResolvedValue(Integration.kmp); const session = { diff --git a/src/lib/programs/error-tracking/index.ts b/src/lib/programs/error-tracking/index.ts index b28ae779d..9aba3a5b8 100644 --- a/src/lib/programs/error-tracking/index.ts +++ b/src/lib/programs/error-tracking/index.ts @@ -17,6 +17,7 @@ import { preinstallPostHogCliOnce } from '@lib/programs/shared/posthog-cli-prein import { analytics } from '@utils/analytics'; import { wizardAbort } from '@utils/wizard-abort'; import { ErrorCodes } from '@shared/errors'; +import { abortNoFrameworkDetected } from '@lib/programs/shared/abort-no-framework'; const ERROR_TRACKING_REPORT_FILE = 'posthog-error-tracking-report.md'; const ERROR_TRACKING_DOCS_URL = 'https://posthog.com/docs/error-tracking'; @@ -184,10 +185,7 @@ export const errorTrackingConfig: ProgramConfig = { const integration = await detectFramework(session.installDir); if (!integration) { - await wizardAbort({ - code: ErrorCodes.DetectNoFramework, - message: 'Could not auto-detect your framework for this project.', - }); + await abortNoFrameworkDetected(); return; } if (ERROR_TRACKING_UNSUPPORTED.has(integration)) { From dbbd1356e481b4f9505524799cbdd82d054a1dce Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 7 Oct 2026 10:03:07 -0400 Subject: [PATCH 08/11] test(tui): pin the exit code when an aborted run's outro is dismissed Co-Authored-By: Claude Opus 5.5 --- src/tui/__tests__/abort-exit-code.test.tsx | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/tui/__tests__/abort-exit-code.test.tsx diff --git a/src/tui/__tests__/abort-exit-code.test.tsx b/src/tui/__tests__/abort-exit-code.test.tsx new file mode 100644 index 000000000..28ed473d0 --- /dev/null +++ b/src/tui/__tests__/abort-exit-code.test.tsx @@ -0,0 +1,52 @@ +/** + * An aborted run ends the process with the abort's code. Dismissing the error + * outro routes the router to the Exit screen, which asks to end the run with 0; + * the abort must already have asked for 1, because the first request wins. + */ +import { vi, it, expect, afterEach } from 'vitest'; +import { render, cleanup } from 'ink-testing-library'; + +vi.mock(import('ink'), () => + vi.importActual('ink-actual'), +); +vi.mock(import('@utils/analytics'), () => ({ + analytics: { + capture: vi.fn(), + wizardCapture: vi.fn(), + captureException: vi.fn(), + setTag: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + } as never, + sessionProperties: vi.fn(() => ({})), +})); + +import { abortOnScreens } from '@tui/abort'; +import { ScreenId } from '@tui/router'; +import { ExitScreen } from '@tui/screens/ExitScreen'; +import { WizardStore } from '@tui/store'; +import { ErrorCodes } from '@shared/errors'; +import { Program } from '@programs'; + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +it('exits 1, not 0, when an aborted run is dismissed from its outro', async () => { + const store = new WizardStore(Program.PostHogIntegration); + void abortOnScreens(store, { + code: ErrorCodes.DetectNoFramework, + message: 'Could not detect a framework', + }); + await vi.waitFor(() => expect(store.session.outroData).not.toBeNull()); + expect(store.exitRequest).toBeNull(); + + store.setOutroDismissed(); + expect(store.router.resolve(store)).toBe(ScreenId.Exit); + await vi.waitFor(() => expect(store.exitRequest).toBe(1)); + + // The Exit screen that follows must not overwrite the abort's code. + render(); + await new Promise((resolve) => setImmediate(resolve)); + expect(store.exitRequest).toBe(1); +}); From 89ebdf5bc263f1e608f3b19b32b419830eb6a218 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 7 Oct 2026 10:21:29 -0400 Subject: [PATCH 09/11] fix(replay-vision): say the setup couldn't find a compatible framework Co-Authored-By: Claude Opus 5.5 --- src/programs/replay-vision/__tests__/no-framework.test.ts | 2 +- src/programs/replay-vision/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/programs/replay-vision/__tests__/no-framework.test.ts b/src/programs/replay-vision/__tests__/no-framework.test.ts index 2ad3fe6a9..bad1cd0e4 100644 --- a/src/programs/replay-vision/__tests__/no-framework.test.ts +++ b/src/programs/replay-vision/__tests__/no-framework.test.ts @@ -51,7 +51,7 @@ describe.each([ program: replayVision, integration: Integration.nextjs, dependencies: { next: '^15.0.0' }, - message: "Replay vision couldn't detect a framework", + message: "The Replay Vision setup couldn't find a compatible framework", }, ])( '$name framework detection', diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index dd2b395de..c7ffb5c5c 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -44,7 +44,7 @@ function abortUnsupportedPlatform(integration: Integration): never { } const NO_FRAMEWORK_GUIDANCE = { - message: "Replay vision couldn't detect a framework", + message: "The Replay Vision setup couldn't find a compatible framework", docsLabel: 'Supported replay platforms:', docsUrl: 'https://posthog.com/docs/session-replay', }; From 5c88d3df1771745dc3f12da87922a9b58dcd31fb Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" <29069505+gewenyu99@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:25:54 -0400 Subject: [PATCH 10/11] Apply suggestion from @gewenyu99 --- src/programs/replay-vision/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index c7ffb5c5c..0f677070c 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -44,7 +44,7 @@ function abortUnsupportedPlatform(integration: Integration): never { } const NO_FRAMEWORK_GUIDANCE = { - message: "The Replay Vision setup couldn't find a compatible framework", + message: "Could not find a Replay Vision compatible framework", docsLabel: 'Supported replay platforms:', docsUrl: 'https://posthog.com/docs/session-replay', }; From 690ed4dc3ac5b27a209d9adda41802479131a02e Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 7 Oct 2026 10:41:20 -0400 Subject: [PATCH 11/11] fix(replay-vision): match the no-framework message in its test and quote style Co-Authored-By: Claude Opus 5.5 --- src/programs/replay-vision/__tests__/no-framework.test.ts | 2 +- src/programs/replay-vision/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/programs/replay-vision/__tests__/no-framework.test.ts b/src/programs/replay-vision/__tests__/no-framework.test.ts index bad1cd0e4..ee90fef95 100644 --- a/src/programs/replay-vision/__tests__/no-framework.test.ts +++ b/src/programs/replay-vision/__tests__/no-framework.test.ts @@ -51,7 +51,7 @@ describe.each([ program: replayVision, integration: Integration.nextjs, dependencies: { next: '^15.0.0' }, - message: "The Replay Vision setup couldn't find a compatible framework", + message: 'Could not find a Replay Vision compatible framework', }, ])( '$name framework detection', diff --git a/src/programs/replay-vision/index.ts b/src/programs/replay-vision/index.ts index 0f677070c..0d6e3baf1 100644 --- a/src/programs/replay-vision/index.ts +++ b/src/programs/replay-vision/index.ts @@ -44,7 +44,7 @@ function abortUnsupportedPlatform(integration: Integration): never { } const NO_FRAMEWORK_GUIDANCE = { - message: "Could not find a Replay Vision compatible framework", + message: 'Could not find a Replay Vision compatible framework', docsLabel: 'Supported replay platforms:', docsUrl: 'https://posthog.com/docs/session-replay', };