From 2e61b6d87f98ef5dff166b11cabf3158a0d4fc23 Mon Sep 17 00:00:00 2001 From: Pamela Chia Date: Mon, 31 Aug 2026 21:32:03 +0800 Subject: [PATCH] refactor(telemetry): extract embedded agent posthog config builder --- .../common/embedded-agent-browser.test.ts | 46 ++++++++++++++++++- packages/common/embedded-agent-browser.ts | 25 ++++++++-- packages/common/posthog-client.ts | 20 ++------ 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/packages/common/embedded-agent-browser.test.ts b/packages/common/embedded-agent-browser.test.ts index a1598d9fe37..b44cb5131df 100644 --- a/packages/common/embedded-agent-browser.test.ts +++ b/packages/common/embedded-agent-browser.test.ts @@ -1,6 +1,10 @@ +import type { CaptureResult } from 'posthog-js' import { describe, expect, it } from 'vitest' -import { detectEmbeddedAgentBrowser } from './embedded-agent-browser' +import { + buildEmbeddedAgentBrowserConfig, + detectEmbeddedAgentBrowser, +} from './embedded-agent-browser' describe('detectEmbeddedAgentBrowser', () => { it('detects Claude Desktop on Mac with Electron token', () => { @@ -71,3 +75,43 @@ describe('detectEmbeddedAgentBrowser', () => { expect(detectEmbeddedAgentBrowser('')).toBeUndefined() }) }) + +describe('buildEmbeddedAgentBrowserConfig', () => { + const claudeUserAgent = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Claude/1.37937.3 Chrome/148.0.7778.280 Safari/537.36 MSIX' + + it('returns an empty config for non-agent user agents', () => { + expect( + buildEmbeddedAgentBrowserConfig( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36' + ) + ).toEqual({}) + }) + + it('returns an empty config when the user agent is unavailable', () => { + expect(buildEmbeddedAgentBrowserConfig(undefined)).toEqual({}) + }) + + it('annotates captured events with the detected agent browser', () => { + const config = buildEmbeddedAgentBrowserConfig(claudeUserAgent) + const beforeSend = config.before_send as (cr: CaptureResult | null) => CaptureResult | null + + const result = beforeSend({ + uuid: '00000000-0000-0000-0000-000000000000', + event: '$pageview', + properties: { $current_url: 'https://supabase.com/dashboard/projects' }, + } as CaptureResult) + + expect(result?.properties).toEqual({ + $current_url: 'https://supabase.com/dashboard/projects', + embedded_agent_browser: 'claude_desktop', + }) + }) + + it('passes through null capture results', () => { + const config = buildEmbeddedAgentBrowserConfig(claudeUserAgent) + const beforeSend = config.before_send as (cr: CaptureResult | null) => CaptureResult | null + + expect(beforeSend(null)).toBeNull() + }) +}) diff --git a/packages/common/embedded-agent-browser.ts b/packages/common/embedded-agent-browser.ts index f5dfae66c79..89e7cf6858e 100644 --- a/packages/common/embedded-agent-browser.ts +++ b/packages/common/embedded-agent-browser.ts @@ -1,11 +1,12 @@ +import type { PostHogConfig } from 'posthog-js' + export type EmbeddedAgentBrowser = 'claude_desktop' | 'chatgpt_desktop' const CLAUDE_DESKTOP_UA = /(^| )Claude\/\d/ const CHATGPT_DESKTOP_UA = /(^| )ChatGPT\/\d/ -// Claude Desktop >= ~1.37937 drops the Electron/ UA token on every platform, so Claude -// matches on its product token alone. ChatGPT's token also appears in mobile in-app -// WebView UAs (human browsing), so it stays gated on the Electron/ token. +// Newer Claude Desktop builds omit the Electron/ token, so Claude matches on its product +// token alone. ChatGPT's token also appears in mobile in-app WebViews, so it stays Electron-gated. export function detectEmbeddedAgentBrowser(userAgent: string): EmbeddedAgentBrowser | undefined { if (CLAUDE_DESKTOP_UA.test(userAgent)) return 'claude_desktop' if (CHATGPT_DESKTOP_UA.test(userAgent) && userAgent.includes('Electron/')) { @@ -13,3 +14,21 @@ export function detectEmbeddedAgentBrowser(userAgent: string): EmbeddedAgentBrow } return undefined } + +export function buildEmbeddedAgentBrowserConfig( + userAgent: string | undefined +): Partial { + const embeddedAgentBrowser = userAgent ? detectEmbeddedAgentBrowser(userAgent) : undefined + if (!embeddedAgentBrowser) return {} + + return { + before_send: (captureResult) => { + if (!captureResult) return captureResult + captureResult.properties = { + ...captureResult.properties, + embedded_agent_browser: embeddedAgentBrowser, + } + return captureResult + }, + } +} diff --git a/packages/common/posthog-client.ts b/packages/common/posthog-client.ts index 601a599199f..056e0e03188 100644 --- a/packages/common/posthog-client.ts +++ b/packages/common/posthog-client.ts @@ -4,7 +4,7 @@ import posthog, { type SessionRecordingOptions, } from 'posthog-js' -import { detectEmbeddedAgentBrowser } from './embedded-agent-browser' +import { buildEmbeddedAgentBrowserConfig } from './embedded-agent-browser' import { safeSessionStorage } from './safe-storage' export type { CapturedNetworkRequest, SessionRecordingOptions } @@ -89,27 +89,15 @@ class PostHogClient { return } - const embeddedAgentBrowser = - typeof navigator !== 'undefined' ? detectEmbeddedAgentBrowser(navigator.userAgent) : undefined - const config: Partial = { api_host: this.config.apiHost, ui_host: this.config.uiHost, autocapture: false, // We'll manually track events capture_pageview: false, // We'll manually track pageviews capture_pageleave: false, // We'll manually track page leaves - ...(embeddedAgentBrowser && { - before_send: (captureResult) => { - if (!captureResult) return captureResult - return { - ...captureResult, - properties: { - ...captureResult.properties, - embedded_agent_browser: embeddedAgentBrowser, - }, - } - }, - }), + ...buildEmbeddedAgentBrowserConfig( + typeof navigator !== 'undefined' ? navigator.userAgent : undefined + ), ...buildSessionRecordingConfig(sessionReplay), loaded: (posthog) => { // Apply pending properties that were set before PostHog