Skip to content

fix: analytics data loss + add live Chrome support#68

Open
aidenybai wants to merge 15 commits intomainfrom
fix/analytics-data-loss
Open

fix: analytics data loss + add live Chrome support#68
aidenybai wants to merge 15 commits intomainfrom
fix/analytics-data-loss

Conversation

@aidenybai
Copy link
Copy Markdown
Member

@aidenybai aidenybai commented Apr 2, 2026

Why

Agents couldn't test against authenticated state. When an agent runs open_browser, it launches Playwright's bundled Chromium — a clean browser with no cookies, sessions, or extensions. If you're testing a change behind auth (e.g. a dashboard, settings page, internal tool), the agent sees a login wall and can't proceed. The user's real Chrome already has all the sessions and state needed.

What changed

Live Chrome support:

  • New liveChrome option on open_browser MCP tool — connects to the user's already-running Chrome via CDP auto-discovery
  • Two clear modes only: bundled Chromium (default) or live Chrome (connect to running browser). No launching a separate Chrome process as a middle ground.
  • If no running Chrome is found or connection fails, falls back to bundled Chromium silently
  • When connected to live Chrome, close only closes the page (not the whole browser)

Other:

  • Chrome launcher utility with spawn error handling, OS-specific path discovery, and proper cleanup
  • Shared @expect/shared/machine-id module used by analytics and CLI version ping
  • Gate /api/version ping to interactive sessions only (skip CI/agent), send machine ID for correlation

Note

Medium Risk
Touches browser session creation/teardown and CDP connection behavior, which can cause flaky automation or unexpected tab/browser lifecycle effects across platforms despite added tests and fallbacks.

Overview
Adds live Chrome support to the browser MCP open flow via a new liveChrome option, which auto-discovers a running Chrome CDP endpoint, connects via chromium.connectOverCDP, and adjusts lifecycle handling (don’t reuse existing tabs; close only closes the page and skips closing the external browser).

Hardens CDP discovery by parsing DevToolsActivePort via a shared utility, expands supported user-data dirs, and introduces a chrome-launcher utility (system Chrome discovery/launch + cleanup) plus new error types and timeouts. Separately, analytics/version pings now use a cached shared @expect/shared/machine-id module and the CLI gates version pings to interactive runs while including mid; the website llms.txt route now serves combined repo README.md + skill content.

Written by Cursor Bugbot for commit f348d3e. This will update automatically on new commits. Configure here.

- Gate /api/version ping to interactive-only (skip CI/agent), send machine
  ID as query param for correlation with PostHog distinctId
- Fix captureImmediate: Effect.sync → Effect.tryPromise so the HTTP promise
  is properly awaited instead of fire-and-forget
- Fix flush: remove Effect.ignore, handle errors at Analytics service layer
  with logging instead of silent swallow
- Fix session-analytics: use ManagedRuntime so machineId() runs once and
  distinctId stays stable across calls, remove redundant error swallowing
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
expect Ready Ready Preview, Comment Apr 2, 2026 9:13am

@aidenybai aidenybai changed the title fix: prevent PostHog analytics data loss fix: add system chrome Apr 2, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/expect-cli@68

commit: f348d3e

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

Test Results

❌ Website Test: failed

8 passed, 3 failed out of 11 steps — 602s

Step Status Duration
Homepage loads — hero section and install commands visible ✅ passed 14s
View demo — navigate to /replay?demo=true and verify replay player loads ✅ passed 45s
Replay controls — play/pause, speed selector, and step list visible ❌ failed 255s
Copy button — clipboard contains expected install command ✅ passed 70s
Theme toggle — dark mode switches background, back to light mode ✅ passed 26s
Footer links — GitHub and X links with correct href and target="_blank" ✅ passed 22s
Legal pages — /terms, /privacy, /security each load with text content ✅ passed 31s
Mobile viewport 375×812 — no horizontal scroll, key content visible ✅ passed 29s
Accessibility audit — WCAG violations ❌ failed 46s
Performance metrics ✅ passed 9s
Project healthcheck — pnpm check ❌ failed 50s

Session Recording

https://github.com/millionco/expect/releases/download/ci-pr-68/2a919a4baed09c26223889391a69a92f.webm


Workflow run #180 | 📎 Download all recordings

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 13 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/browser/src/mcp/server.ts">

<violation number="1" location="packages/browser/src/mcp/server.ts:118">
P2: The open-tool response can incorrectly claim "(system Chrome)" even when system Chrome was not used.</violation>
</file>

<file name="packages/browser/src/chrome-launcher.ts">

<violation number="1" location="packages/browser/src/chrome-launcher.ts:203">
P2: The `Effect.callback` polling loop doesn't return a cleanup finalizer, so fiber interruption leaks the `setTimeout` chain (up to 30 seconds of unnecessary file I/O). Capture the timer handle and return a cleanup `Effect` to cancel it on interruption.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

- Fix systemChrome suffix showing when CDP URL was provided (system
  Chrome is ignored in that case)
- Add cleanup finalizer to Effect.callback polling loop in
  chrome-launcher to prevent setTimeout leak on fiber interruption
- Add Effect.catchCause error handling to analytics trackEvent/flush
  to prevent unhandled promise rejections from fire-and-forget calls
Add Microsoft Edge executable paths (LOCALAPPDATA, PROGRAMFILES,
PROGRAMFILES(X86)) to Windows system Chrome discovery, matching parity
with macOS and Linux which already include Edge.
import { usePreferencesStore } from "../stores/use-preferences";

const analyticsLayer = Analytics.layerPostHog;
const analyticsRuntime = ManagedRuntime.make(Analytics.layerPostHog);
Copy link
Copy Markdown
Contributor

@vercel vercel bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Effect.catchCause instead of Effect.catchTag("UnknownError") for consistency in error handling

Fix on Vercel

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use ManagedRuntime

The error cleanup path skipped browser.close() whenever cdpEndpoint was
truthy, which was designed for user-provided CDP endpoints (externally
managed Chrome). System Chrome sets cdpEndpoint from its own launch,
so the Playwright CDP client was never released on failure. Now only
skip browser.close() for user-provided CDP (when no chromeProcess).
The @expect/browser package had no testTimeout config, defaulting to
5000ms. Cross-browser E2E tests (webkit, firefox launches) and cookie
injection tests exceed this on Windows CI. Set testTimeout: 0 to match
the project convention documented in AGENTS.md.
import { usePreferencesStore } from "../stores/use-preferences";

const analyticsLayer = Analytics.layerPostHog;
const analyticsRuntime = ManagedRuntime.make(Analytics.layerPostHog);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use ManagedRuntime

- Early return in Analytics when telemetry disabled (skip machineId, capture, track)
- Replace ternary with early return in Browser.createPage
- Use async fs (fs.promises) in chrome-launcher instead of sync
- Namespace fs/path imports in llms.txt route
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/browser/src/chrome-launcher.ts">

<violation number="1" location="packages/browser/src/chrome-launcher.ts:197">
P1: Do not ignore all errors when deleting `DevToolsActivePort`; swallowing removal failures can allow stale CDP port data to be reused.</violation>
</file>

<file name="apps/cli/src/utils/session-analytics.ts">

<violation number="1">
P2: Using `Effect.runPromise` with `Effect.provide(analyticsLayer)` per call rebuilds the Analytics layer each invocation; this can change `distinctId` across events on fallback paths and breaks stable session analytics identity.</violation>
</file>

<file name="packages/shared/src/analytics/analytics.ts">

<violation number="1">
P2: `flush` is piped through `Effect.ignore`, so flush failures are silently swallowed. This hides analytics delivery problems.</violation>

<violation number="2">
P1: `capture` wraps `captureImmediate` in `Effect.sync`, which drops async completion/failure tracking. Use a promise-aware Effect so capture is actually awaited.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


yield* Effect.tryPromise(() =>
fs.promises.rm(path.join(userDataDir, "DevToolsActivePort"), { force: true }),
).pipe(Effect.catchTag("UnknownError", () => Effect.void));
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Do not ignore all errors when deleting DevToolsActivePort; swallowing removal failures can allow stale CDP port data to be reused.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/browser/src/chrome-launcher.ts, line 197:

<comment>Do not ignore all errors when deleting `DevToolsActivePort`; swallowing removal failures can allow stale CDP port data to be reused.</comment>

<file context>
@@ -183,13 +183,18 @@ export const launchSystemChrome = Effect.fn("launchSystemChrome")(function* (opt
-  fs.rmSync(path.join(userDataDir, "DevToolsActivePort"), { force: true });
+  yield* Effect.tryPromise(() =>
+    fs.promises.rm(path.join(userDataDir, "DevToolsActivePort"), { force: true }),
+  ).pipe(Effect.catchTag("UnknownError", () => Effect.void));
 
   const args = buildLaunchArgs({ headless: options.headless, userDataDir });
</file context>
Suggested change
).pipe(Effect.catchTag("UnknownError", () => Effect.void));
).pipe(Effect.catchTag("UnknownError", Effect.die));
Fix with Cubic

@@ -1,6 +1,6 @@
import { Config, Effect, Layer, Option, ServiceMap } from "effect";
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: capture wraps captureImmediate in Effect.sync, which drops async completion/failure tracking. Use a promise-aware Effect so capture is actually awaited.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/shared/src/analytics/analytics.ts, line 40:

<comment>`capture` wraps `captureImmediate` in `Effect.sync`, which drops async completion/failure tracking. Use a promise-aware Effect so capture is actually awaited.</comment>

<file context>
@@ -37,13 +37,13 @@ export class AnalyticsProvider extends ServiceMap.Service<
   static layerPostHog = Layer.succeed(this)({
     capture: (event) =>
-      Effect.tryPromise(() =>
+      Effect.sync(() => {
         posthogClient.captureImmediate({
           event: event.eventName,
</file context>
Fix with Cubic

@@ -1,6 +1,6 @@
import { Config, Effect, Layer, Option, ServiceMap } from "effect";
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: flush is piped through Effect.ignore, so flush failures are silently swallowed. This hides analytics delivery problems.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/shared/src/analytics/analytics.ts, line 60:

<comment>`flush` is piped through `Effect.ignore`, so flush failures are silently swallowed. This hides analytics delivery problems.</comment>

<file context>
@@ -54,7 +54,10 @@ export class AnalyticsProvider extends ServiceMap.Service<
+    flush: Effect.tryPromise({
+      try: () => posthogClient.flush(),
+      catch: (cause) => cause,
+    }).pipe(Effect.ignore),
   });
 
</file context>
Fix with Cubic

Simplifies browser connection to two clear modes: Playwright bundled
Chromium (default) or connect to an already-running Chrome (liveChrome).
Removes the confusing middle ground of launching a fresh system Chrome
binary as a fallback.
sourcemap: true,
},
test: {
testTimeout: 0,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test timeout disabled globally in committed config

Medium Severity

testTimeout: 0 disables test timeouts entirely for the browser package. Any test that hangs (common with browser/CDP tests) will block CI indefinitely instead of failing after a reasonable threshold.

Fix in Cursor Fix in Web

if (!cached) {
cached = getRawMachineId();
}
return cached;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rejected machineId promise is permanently cached

Low Severity

If getRawMachineId() rejects (e.g., transient filesystem error), the rejected promise is stored in cached and returned on every subsequent call. There's no retry path — the module is permanently broken for the process lifetime.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 9 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/browser/src/browser.ts">

<violation number="1" location="packages/browser/src/browser.ts:351">
P2: Avoid closing externally supplied CDP sessions on setup errors. With `options.cdpUrl`, `isExternalBrowser` is false, so this cleanup now calls `browser.close()` and can shut down a user-provided Chrome instance.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

return yield* setupPage.pipe(
Effect.tapError(() => {
if (cdpEndpoint) return Effect.void;
if (isExternalBrowser) return Effect.void;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid closing externally supplied CDP sessions on setup errors. With options.cdpUrl, isExternalBrowser is false, so this cleanup now calls browser.close() and can shut down a user-provided Chrome instance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/browser/src/browser.ts, line 351:

<comment>Avoid closing externally supplied CDP sessions on setup errors. With `options.cdpUrl`, `isExternalBrowser` is false, so this cleanup now calls `browser.close()` and can shut down a user-provided Chrome instance.</comment>

<file context>
@@ -322,20 +343,16 @@ export class Browser extends ServiceMap.Service<Browser>()("@browser/Browser", {
-          ),
-        ),
+        Effect.tapError(() => {
+          if (isExternalBrowser) return Effect.void;
+          return Effect.tryPromise(() => browser.close()).pipe(
+            Effect.catchTag("UnknownError", () => Effect.void),
</file context>
Suggested change
if (isExternalBrowser) return Effect.void;
if (isExternalBrowser || cdpEndpoint) return Effect.void;
Fix with Cubic

Copy link
Copy Markdown
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestions:

  1. flush operation silently swallows all failures due to Effect.ignore, preventing error logging and hiding analytics delivery problems
  1. capture function uses Effect.sync which doesn't wait for PostHog's captureImmediate Promise to complete, causing analytics events to be fire-and-forget

Fix on Vercel

@aidenybai aidenybai changed the title fix: add system chrome fix: analytics data loss + add live Chrome support Apr 2, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Copy link
Copy Markdown

@JiwaniZakir JiwaniZakir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In apps/website/app/llms.txt/route.ts, both readFileSync calls happen at module load time rather than inside the GET handler. In a serverless/edge environment, process.cwd() may not resolve to the monorepo root as expected, and any missing file will crash the module on cold start instead of returning a graceful HTTP error response — wrapping these reads inside the handler (or using try/catch with a fallback NextResponse) would be more resilient.

In apps/cli/src/index.tsx, the promise chain .catch(() => "unknown").then((mid) => { fetch(...) }) is functionally correct but the ordering is unconventional — typically .then() precedes .catch(), so this reads as if the fetch only fires on failure. Reversing to .then((mid) => fetch(...)).catch(() => {}) would be clearer and also silences the dangling fetch rejection in one step.

For the live Chrome support in browser.ts, the isExternalBrowser flag is set from Option.isSome(liveBrowser) but the diff is truncated before showing how it's used. It's worth confirming that browser cleanup differentiates between browser.disconnect() (for externally-connected CDP sessions) and browser.close() (for launched browsers) — conflating these can leave the user's live Chrome instance in a closed state unexpectedly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants