diff --git a/packages/@n8n/instance-ai/src/index.ts b/packages/@n8n/instance-ai/src/index.ts index 67553f5582f..f6b6bb45473 100644 --- a/packages/@n8n/instance-ai/src/index.ts +++ b/packages/@n8n/instance-ai/src/index.ts @@ -227,7 +227,10 @@ export type { Logger } from './logger'; export const createDomainAccessTracker: typeof DomainAccessMod.createDomainAccessTracker = lazyFunction(() => loadDomainAccess().createDomainAccessTracker); export type { DomainAccessTracker } from './domain-access'; -export type { SubmitLangsmithUserFeedbackOptions } from './tracing/langsmith-tracing'; +export type { + BrowserExtensionTraceContext, + SubmitLangsmithUserFeedbackOptions, +} from './tracing/langsmith-tracing'; export const emitAgentSnapshotTraceEvent: typeof AgentSnapshotEventMod.emitAgentSnapshotTraceEvent = lazyFunction(() => loadAgentSnapshotEvent().emitAgentSnapshotTraceEvent); diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/tracing-utils.test.ts b/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/tracing-utils.test.ts index 536e985f5e1..8eb50860b6c 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/tracing-utils.test.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/__tests__/tracing-utils.test.ts @@ -41,6 +41,8 @@ describe('createDetachedSubAgentTraceFactory', () => { agent_id: 'agent-1', n8n_version: '1.123.4', workflow_sdk_version: '0.13.0', + browser_connection_state: 'connected', + browser_extension_version: '0.0.6', }, }, }, @@ -61,7 +63,53 @@ describe('createDetachedSubAgentTraceFactory', () => { metadata: { task_id: 'task-1' }, n8nVersion: '1.123.4', workflowSdkVersion: '0.13.0', + browserExtension: { connectionState: 'connected', version: '0.0.6' }, }), ); }); + + it.each([ + [ + 'connected without a reported version', + { browser_connection_state: 'connected' }, + { connectionState: 'connected' }, + ], + [ + 'disconnected', + { browser_connection_state: 'disconnected' }, + { connectionState: 'disconnected' }, + ], + ['absent from the parent', {}, undefined], + ['not a known state', { browser_connection_state: 'sideways' }, undefined], + ])( + 'propagates the browser extension dimensions when the parent is %s', + async (_case, parentMetadata, expected) => { + mockCreateDetachedSubAgentTraceContext.mockResolvedValue({ + rootRun: { id: 'sub-root', traceId: 'sub-trace' }, + }); + const context = { + threadId: 'thread-1', + runId: 'run-1', + userId: 'user-1', + orchestratorAgentId: 'orchestrator-1', + tracing: { + projectName: 'project-1', + rootRun: { traceId: 'root-trace' }, + actorRun: { id: 'actor-run', metadata: parentMetadata }, + }, + } as unknown as OrchestrationContext; + + const createTrace = createDetachedSubAgentTraceFactory(context, { + agentId: 'sub-agent', + role: 'builder', + kind: 'background', + }); + + await createTrace(); + + expect(mockCreateDetachedSubAgentTraceContext).toHaveBeenCalledWith( + expect.objectContaining({ browserExtension: expected }), + ); + }, + ); }); diff --git a/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts b/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts index 638a301aeed..fad5b3575e1 100644 --- a/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts +++ b/packages/@n8n/instance-ai/src/tools/orchestration/tracing-utils.ts @@ -3,6 +3,7 @@ import { getCurrentOtelSpanContext, getCurrentTraceToolCallId, mergeCurrentTraceMetadata, + type BrowserExtensionTraceContext, } from '../../tracing/langsmith-tracing'; import type { InstanceAiTraceContext, @@ -11,6 +12,19 @@ import type { OrchestrationContext, } from '../../types'; +/** Rebuilds the parent's browser-extension dimensions so sub-agent tool calls stay sliceable. */ +function readBrowserExtensionFromMetadata( + metadata: Record | undefined, +): BrowserExtensionTraceContext | undefined { + const state = metadata?.browser_connection_state; + if (state !== 'connected' && state !== 'disconnected') return undefined; + const version = metadata?.browser_extension_version; + return { + connectionState: state, + ...(typeof version === 'string' ? { version } : {}), + }; +} + type ToolRegistry = OrchestrationContext['domainTools']; interface StartSubAgentTraceOptions { @@ -84,6 +98,9 @@ export function createDetachedSubAgentTraceFactory( typeof context.tracing.actorRun.metadata?.workflow_sdk_version === 'string' ? context.tracing.actorRun.metadata.workflow_sdk_version : undefined; + const parentBrowserExtension = readBrowserExtensionFromMetadata( + context.tracing.actorRun.metadata, + ); const activeSpanContext = getCurrentOtelSpanContext(); const spawnedByToolCallId = getCurrentTraceToolCallId(); @@ -102,6 +119,7 @@ export function createDetachedSubAgentTraceFactory( metadata: options.metadata, n8nVersion: parentN8nVersion, workflowSdkVersion: parentWorkflowSdkVersion, + browserExtension: parentBrowserExtension, agentId: options.agentId, role: options.role, kind: options.kind, diff --git a/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts b/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts index cba34a95f26..b6b816f8bc0 100644 --- a/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts +++ b/packages/@n8n/instance-ai/src/tracing/__tests__/langsmith-tracing.test.ts @@ -439,6 +439,7 @@ describe('createInstanceAiTraceContext', () => { userId: 'user-1', input: { message: 'What workflows do I have?' }, metadata: { n8n_version: '2.19.0' }, + browserExtension: { connectionState: 'connected', version: '0.0.7' }, }); expect(tracing?.getTelemetry).toBeDefined(); @@ -466,6 +467,8 @@ describe('createInstanceAiTraceContext', () => { user_id: 'user-1', agent_role: 'orchestrator', n8n_version: '2.19.0', + browser_extension_version: '0.0.7', + browser_connection_state: 'connected', execution_mode: 'foreground', trace_kind: 'message_turn', langsmith_trace_id: tracing?.rootRun.traceId, @@ -480,6 +483,36 @@ describe('createInstanceAiTraceContext', () => { await telemetry.provider?.shutdown(); }); + it('records a connected extension that reports no version distinctly from no extension', async () => { + const connected = await createInstanceAiTraceContext({ + threadId: 'thread-old-ext', + messageId: 'message-old-ext', + runId: 'run-old-ext', + userId: 'user-1', + input: { message: 'What workflows do I have?' }, + browserExtension: { connectionState: 'connected' }, + }); + + expect(connected?.rootRun.metadata).toEqual( + expect.objectContaining({ browser_connection_state: 'connected' }), + ); + expect(connected?.rootRun.metadata).not.toHaveProperty('browser_extension_version'); + + const absent = await createInstanceAiTraceContext({ + threadId: 'thread-no-ext', + messageId: 'message-no-ext', + runId: 'run-no-ext', + userId: 'user-1', + input: { message: 'What workflows do I have?' }, + browserExtension: { connectionState: 'disconnected' }, + }); + + expect(absent?.rootRun.metadata).toEqual( + expect.objectContaining({ browser_connection_state: 'disconnected' }), + ); + expect(absent?.rootRun.metadata).not.toHaveProperty('browser_extension_version'); + }); + it('does not mutate LangSmith tracing environment flags while creating a context', async () => { delete process.env.LANGSMITH_TRACING; delete process.env.LANGCHAIN_TRACING_V2; diff --git a/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts b/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts index c77d0a0c625..79a32c6f3c8 100644 --- a/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts +++ b/packages/@n8n/instance-ai/src/tracing/langsmith-tracing.ts @@ -685,6 +685,11 @@ function getOrCreateProxyClient(proxyConfig: ServiceProxyConfig): Client { return client; } +export interface BrowserExtensionTraceContext { + connectionState: 'connected' | 'disconnected'; + version?: string; +} + interface CreateInstanceAiTraceContextOptions { projectName?: string; threadId: string; @@ -698,6 +703,7 @@ interface CreateInstanceAiTraceContextOptions { metadata?: Record; n8nVersion?: string; workflowSdkVersion?: string; + browserExtension?: BrowserExtensionTraceContext; /** When set, traces are routed through the AI service proxy instead of directly to LangSmith. */ proxyConfig?: ServiceProxyConfig; } @@ -1637,6 +1643,14 @@ async function buildBaseMetadata( ...(options.workflowSdkVersion !== undefined ? { workflow_sdk_version: options.workflowSdkVersion } : {}), + ...(options.browserExtension + ? { + browser_connection_state: options.browserExtension.connectionState, + ...(options.browserExtension.version !== undefined + ? { browser_extension_version: options.browserExtension.version } + : {}), + } + : {}), ...(options.modelId !== undefined ? { model_id: serializeModelIdForTrace(options.modelId) } : {}), diff --git a/packages/@n8n/mcp-browser-extension/src/background.test.ts b/packages/@n8n/mcp-browser-extension/src/background.test.ts index 88ac0941055..0331557c7a5 100644 --- a/packages/@n8n/mcp-browser-extension/src/background.test.ts +++ b/packages/@n8n/mcp-browser-extension/src/background.test.ts @@ -368,3 +368,17 @@ describe('external messages (direct connect flow)', () => { expect(firstResult()).toEqual({ connected: false }); }); }); + +describe('buildRelayWsUrl', () => { + const RELAY_URL = 'wss://acme.app.n8n.cloud/browser-use/extension/abc?token=bu_x'; + + it('adds the extension version while preserving the existing query', async () => { + const { buildRelayWsUrl } = await import('./background'); + + const url = new URL(buildRelayWsUrl(RELAY_URL, '0.0.7')); + + expect(url.searchParams.get('extensionVersion')).toBe('0.0.7'); + expect(url.searchParams.get('token')).toBe('bu_x'); + expect(url.pathname).toBe('/browser-use/extension/abc'); + }); +}); diff --git a/packages/@n8n/mcp-browser-extension/src/background.ts b/packages/@n8n/mcp-browser-extension/src/background.ts index c2b0406489b..8b7bcc1129b 100644 --- a/packages/@n8n/mcp-browser-extension/src/background.ts +++ b/packages/@n8n/mcp-browser-extension/src/background.ts @@ -24,6 +24,13 @@ interface ConnectionState { let activeConnection: ConnectionState | null = null; +/** A query param rather than a header because `WebSocket` cannot set request headers. */ +export function buildRelayWsUrl(relayUrl: string, version: string): string { + const url = new URL(relayUrl); + url.searchParams.set('extensionVersion', version); + return url.toString(); +} + // --------------------------------------------------------------------------- // Relay URL storage (for deduplicating connect.html tabs) // --------------------------------------------------------------------------- @@ -424,7 +431,7 @@ async function connectToRelay( disconnect(); try { - const ws = new WebSocket(relayUrl); + const ws = new WebSocket(buildRelayWsUrl(relayUrl, chrome.runtime.getManifest().version)); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { diff --git a/packages/cli/src/modules/instance-ai/__tests__/instance-ai-browser-session.service.test.ts b/packages/cli/src/modules/instance-ai/__tests__/instance-ai-browser-session.service.test.ts index 35d448208d6..34fcdbfee2e 100644 --- a/packages/cli/src/modules/instance-ai/__tests__/instance-ai-browser-session.service.test.ts +++ b/packages/cli/src/modules/instance-ai/__tests__/instance-ai-browser-session.service.test.ts @@ -44,6 +44,7 @@ vi.mock('@n8n/mcp-browser', () => { const mcpBrowserMock: { __createdRelays: Array<{ onExtensionConnect?: () => void; + onExtensionDisconnect?: () => void; attachExtension: Mock; attachController: Mock; stop: Mock; @@ -52,6 +53,7 @@ const mcpBrowserMock: { } = mcpBrowser as unknown as { __createdRelays: Array<{ onExtensionConnect?: () => void; + onExtensionDisconnect?: () => void; attachExtension: Mock; attachController: Mock; stop: Mock; @@ -61,11 +63,14 @@ const mcpBrowserMock: { const USER_ID = 'user-1'; -function extensionRequest(sessionId: string, token: string | null) { +function extensionRequest(sessionId: string, token: string | null, extensionVersion?: unknown) { const ws = { close: vi.fn() }; const req = { params: { sessionId }, - query: token === null ? {} : { token }, + query: { + ...(token === null ? {} : { token }), + ...(extensionVersion === undefined ? {} : { extensionVersion }), + }, headers: {}, socket: { remoteAddress: '127.0.0.1' }, ws, @@ -281,6 +286,88 @@ describe('InstanceAiBrowserSessionService', () => { expect(telemetry.track).toHaveBeenCalledWith('Instance AI Browser connected', { user_id: USER_ID, + browser_extension_version: null, + }); + }); + }); + + describe('extension version', () => { + it('tracks the version reported on the upgrade', async () => { + const { sessionId, extToken, relay } = await createSession(service); + + service.handleExtensionUpgrade(extensionRequest(sessionId, extToken, '0.0.7').req); + relay.onExtensionConnect?.(); + + expect(telemetry.track).toHaveBeenCalledWith('Instance AI Browser connected', { + user_id: USER_ID, + browser_extension_version: '0.0.7', + }); + expect(service.getExtensionTraceContext(USER_ID)).toEqual({ + connectionState: 'connected', + version: '0.0.7', + }); + }); + + it.each([ + ['omitted, as pre-instrumentation extensions do', undefined], + ['not version-shaped', 'v0.0.7-beta'], + ['not a string', ['0.0.7']], + ['too long to be a Chrome version', '1.2.3.4.5'], + ])('records null when the version is %s', async (_case, reported) => { + const { sessionId, extToken, relay } = await createSession(service); + const { req, ws } = extensionRequest(sessionId, extToken, reported); + + service.handleExtensionUpgrade(req); + relay.onExtensionConnect?.(); + + expect(telemetry.track).toHaveBeenCalledWith('Instance AI Browser connected', { + user_id: USER_ID, + browser_extension_version: null, + }); + // An unusable version must not cost the user their connection. + expect(relay.attachExtension).toHaveBeenCalledWith(ws); + expect(ws.close).not.toHaveBeenCalled(); + }); + + it('re-reads the version on reconnect, so a silent auto-update is picked up', async () => { + const { sessionId, extToken, relay } = await createSession(service); + + service.handleExtensionUpgrade(extensionRequest(sessionId, extToken, '0.0.6').req); + relay.onExtensionConnect?.(); + relay.onExtensionDisconnect?.(); + service.handleExtensionUpgrade(extensionRequest(sessionId, extToken, '0.0.7').req); + relay.onExtensionConnect?.(); + + expect(service.getExtensionTraceContext(USER_ID)).toEqual({ + connectionState: 'connected', + version: '0.0.7', + }); + }); + + it('reports connected with no version for a pre-instrumentation extension', async () => { + const { sessionId, extToken, relay } = await createSession(service); + + service.handleExtensionUpgrade(extensionRequest(sessionId, extToken).req); + relay.onExtensionConnect?.(); + + expect(service.getExtensionTraceContext(USER_ID)).toEqual({ connectionState: 'connected' }); + }); + + it('reports disconnected for a user with no session', () => { + expect(service.getExtensionTraceContext('nobody')).toEqual({ + connectionState: 'disconnected', + }); + }); + + it('reports no version while disconnected', async () => { + const { sessionId, extToken, relay } = await createSession(service); + + service.handleExtensionUpgrade(extensionRequest(sessionId, extToken, '0.0.7').req); + relay.onExtensionConnect?.(); + relay.onExtensionDisconnect?.(); + + expect(service.getExtensionTraceContext(USER_ID)).toEqual({ + connectionState: 'disconnected', }); }); }); diff --git a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.service.test.ts b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.service.test.ts index 8d2cf2c6501..db45bd0c356 100644 --- a/packages/cli/src/modules/instance-ai/__tests__/instance-ai.service.test.ts +++ b/packages/cli/src/modules/instance-ai/__tests__/instance-ai.service.test.ts @@ -208,6 +208,7 @@ import { createOrchestratorRunControl, createSandbox, createWorkspace, + createInstanceAiTraceContext, loadInstanceAiRuntimeSkillSource, resumeAgentRun, setupSandboxWorkspace, @@ -1941,6 +1942,7 @@ type SuspendedRunResumeServiceInternals = { trackInFlightExecution: Mock; rebuildAgentForResume: Mock; threadPushRef: { get: Mock }; + browserSessionService: { getExtensionTraceContext: Mock }; }; function createSuspendedRunResumeService(): SuspendedRunResumeServiceInternals { @@ -1983,6 +1985,9 @@ function createSuspendedRunResumeService(): SuspendedRunResumeServiceInternals { service.processResumedStream = vi.fn(); service.rebuildAgentForResume = vi.fn(); service.threadPushRef = { get: vi.fn(() => undefined) }; + service.browserSessionService = { + getExtensionTraceContext: vi.fn(() => ({ connectionState: 'disconnected' })), + }; return service; } @@ -2463,6 +2468,23 @@ describe('InstanceAiService — suspended run user revalidation', () => { ); }); + it('stamps the browser extension dimensions on the approval-resume trace', async () => { + const service = createSuspendedRunResumeService(); + service.revalidateActiveUser.mockResolvedValue(fakeUser); + service.browserSessionService.getExtensionTraceContext.mockReturnValue({ + connectionState: 'connected', + version: '0.0.7', + }); + + await service.resumeSuspendedRun('user-1', 'req-1', { approved: true }); + + expect(service.tracing.createOrchestratorResumeTraceContext).toHaveBeenCalledWith( + expect.objectContaining({ + browserExtension: { connectionState: 'connected', version: '0.0.7' }, + }), + ); + }); + it('passes the revalidated user into the resumed stream', async () => { const service = createSuspendedRunResumeService(); const freshUser = { id: 'user-1', disabled: false } as User; @@ -4517,6 +4539,7 @@ describe('InstanceAiService — user message persistence on cancel', () => { instanceAiErrorReporter: { beginRun: Mock; endRun: Mock }; schedulePlannedTasks: Mock; taskProjector: { syncFromWorkflowLoop: Mock }; + browserSessionService: { getExtensionTraceContext: Mock }; }; function createCancelPersistenceService(): ExecuteRunInternals { @@ -4534,6 +4557,9 @@ describe('InstanceAiService — user message persistence on cancel', () => { beginRun: vi.fn(() => Symbol('error-reporter-execution')), endRun: vi.fn(), }; + service.browserSessionService = { + getExtensionTraceContext: vi.fn(() => ({ connectionState: 'disconnected' })), + }; return service; } @@ -4558,6 +4584,51 @@ describe('InstanceAiService — user message persistence on cancel', () => { ); }); + it('passes the connected extension version to the trace', async () => { + const service = createCancelPersistenceService(); + service.browserSessionService.getExtensionTraceContext.mockReturnValue({ + connectionState: 'connected', + version: '0.0.7', + }); + const abortController = new AbortController(); + abortController.abort(); + + await service.executeRun(fakeUser, 'thread-1', 'run-1', 'banana', abortController); + + expect(createInstanceAiTraceContext).toHaveBeenCalledWith( + expect.objectContaining({ + browserExtension: { connectionState: 'connected', version: '0.0.7' }, + }), + ); + }); + + it('separates a connected extension that reports no version from no extension at all', async () => { + const connected = createCancelPersistenceService(); + connected.browserSessionService.getExtensionTraceContext.mockReturnValue({ + connectionState: 'connected', + }); + const firstAbort = new AbortController(); + firstAbort.abort(); + + await connected.executeRun(fakeUser, 'thread-1', 'run-1', 'banana', firstAbort); + + expect(createInstanceAiTraceContext).toHaveBeenCalledWith( + expect.objectContaining({ browserExtension: { connectionState: 'connected' } }), + ); + }); + + it('leaves the trace version undefined when no extension is connected', async () => { + const service = createCancelPersistenceService(); + const abortController = new AbortController(); + abortController.abort(); + + await service.executeRun(fakeUser, 'thread-1', 'run-1', 'banana', abortController); + + expect(createInstanceAiTraceContext).toHaveBeenCalledWith( + expect.objectContaining({ browserExtension: { connectionState: 'disconnected' } }), + ); + }); + it('does not persist an empty user message', async () => { const service = createCancelPersistenceService(); const abortController = new AbortController(); diff --git a/packages/cli/src/modules/instance-ai/browser/browser-use-ws.constants.ts b/packages/cli/src/modules/instance-ai/browser/browser-use-ws.constants.ts index 7b8d7d5ef6d..1064c6c4cdf 100644 --- a/packages/cli/src/modules/instance-ai/browser/browser-use-ws.constants.ts +++ b/packages/cli/src/modules/instance-ai/browser/browser-use-ws.constants.ts @@ -5,6 +5,18 @@ export const BROWSER_USE_WS_NAMESPACE = '/browser-use'; export const CDP_TOKEN_HEADER = 'x-n8n-cdp-token'; +export const EXTENSION_VERSION_QUERY_PARAM = 'extensionVersion'; + +const EXTENSION_VERSION_PATTERN = /^\d{1,5}(\.\d{1,5}){0,3}$/; + +/** A malformed or absent value reads as "not reported" rather than failing the upgrade. */ +export function parseExtensionVersion(value: unknown): string | null { + if (typeof value !== 'string' || !EXTENSION_VERSION_PATTERN.test(value)) { + return null; + } + return value; +} + export interface BrowserUseUpgradeRequest extends Request { ws: WebSocket; } diff --git a/packages/cli/src/modules/instance-ai/browser/instance-ai-browser-session.service.ts b/packages/cli/src/modules/instance-ai/browser/instance-ai-browser-session.service.ts index 83f3d3bfbae..90330e6662d 100644 --- a/packages/cli/src/modules/instance-ai/browser/instance-ai-browser-session.service.ts +++ b/packages/cli/src/modules/instance-ai/browser/instance-ai-browser-session.service.ts @@ -7,6 +7,7 @@ import { Logger } from '@n8n/backend-common'; import { GlobalConfig } from '@n8n/config'; import { UserRepository } from '@n8n/db'; import { Service } from '@n8n/di'; +import type { BrowserExtensionTraceContext } from '@n8n/instance-ai'; import type { BrowserConnection, CDPRelayServer, @@ -30,6 +31,8 @@ import { BrowserLocalMcpServer } from './browser-local-mcp-server'; import { BROWSER_USE_WS_NAMESPACE, CDP_TOKEN_HEADER, + EXTENSION_VERSION_QUERY_PARAM, + parseExtensionVersion, type BrowserUseUpgradeRequest, } from './browser-use-ws.constants'; @@ -47,6 +50,7 @@ interface BrowserSession { hasConnectedOnce: boolean; connected: boolean; connectedAt: Date | null; + extensionVersion: string | null; connection: BrowserConnection; mcpServer: BrowserLocalMcpServer; } @@ -116,6 +120,15 @@ export class InstanceAiBrowserSessionService { return this.sessions.get(userId)?.connected ?? false; } + getExtensionTraceContext(userId: string): BrowserExtensionTraceContext { + const session = this.sessions.get(userId); + if (!session?.connected) return { connectionState: 'disconnected' }; + return { + connectionState: 'connected', + ...(session.extensionVersion !== null ? { version: session.extensionVersion } : {}), + }; + } + handleExtensionUpgrade(req: BrowserUseUpgradeRequest): void { const session = this.sessionsBySessionId.get(req.params.sessionId); const token = typeof req.query.token === 'string' ? req.query.token : null; @@ -123,6 +136,7 @@ export class InstanceAiBrowserSessionService { req.ws.close(4003, 'Invalid auth token'); return; } + session.extensionVersion = parseExtensionVersion(req.query[EXTENSION_VERSION_QUERY_PARAM]); session.relay.attachExtension(req.ws); } @@ -187,6 +201,7 @@ export class InstanceAiBrowserSessionService { hasConnectedOnce: false, connected: false, connectedAt: null, + extensionVersion: null, connection: toolkit.connection, mcpServer: new BrowserLocalMcpServer(toolkit, toolContext, this.logger), }; @@ -221,8 +236,14 @@ export class InstanceAiBrowserSessionService { name: 'browser_connect', arguments: {}, }); - this.telemetry.track('Instance AI Browser connected', { user_id: userId }); - this.logger.info('Browser Use extension connected', { userId }); + this.telemetry.track('Instance AI Browser connected', { + user_id: userId, + browser_extension_version: session.extensionVersion, + }); + this.logger.info('Browser Use extension connected', { + userId, + extensionVersion: session.extensionVersion, + }); this.pushState(userId); } diff --git a/packages/cli/src/modules/instance-ai/instance-ai.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.service.ts index 9955568986c..cfcbbfe9e7c 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.service.ts @@ -3685,6 +3685,9 @@ export class InstanceAiService { // Shared with createExecutionEnvironment so one ProxyTokenManager backs tracing + the run. const proxyRunConfig = await this.createProxyRunConfig(user); + // Read per run: Chrome auto-updates extensions silently. + const browserExtension = this.browserSessionService.getExtensionTraceContext(user.id); + // Create the trace before run-start so the SSE event carries traceId (modelId lands at finalization). if (resumeReason) { tracing = await this.tracing.createOrchestratorResumeTraceContext({ @@ -3703,6 +3706,7 @@ export class InstanceAiService { ? { build_task_id: plannedBuild.buildTaskId } : {}), }, + browserExtension, }); } else { tracing = await createInstanceAiTraceContext({ @@ -3715,6 +3719,7 @@ export class InstanceAiService { proxyConfig: proxyRunConfig.tracingProxyConfig, n8nVersion: N8N_VERSION, workflowSdkVersion: WORKFLOW_SDK_VERSION, + browserExtension, }); } @@ -5198,6 +5203,7 @@ export class InstanceAiService { ? { build_task_id: plannedBuild.buildTaskId } : {}), }, + browserExtension: this.browserSessionService.getExtensionTraceContext(activeUser.id), register: false, }); const effectiveTracing = resumeTracing ?? tracing; diff --git a/packages/cli/src/modules/instance-ai/tracing/instance-ai-tracing.service.ts b/packages/cli/src/modules/instance-ai/tracing/instance-ai-tracing.service.ts index 0e91d72b18b..2f25cb6b8cf 100644 --- a/packages/cli/src/modules/instance-ai/tracing/instance-ai-tracing.service.ts +++ b/packages/cli/src/modules/instance-ai/tracing/instance-ai-tracing.service.ts @@ -6,6 +6,7 @@ import { orchestratorAgentId, releaseTraceClient, submitLangsmithUserFeedback, + type BrowserExtensionTraceContext, type InstanceAiTraceContext, type ManagedBackgroundTask, type ModelConfig, @@ -202,6 +203,7 @@ export class InstanceAiTracingService { proxyConfig?: ServiceProxyConfig; resumeReason: OrchestratorResumeReason; metadata?: Record; + browserExtension?: BrowserExtensionTraceContext; /** Defer process-local registration until the durable resume claim succeeds. */ register?: boolean; }): Promise { @@ -226,6 +228,7 @@ export class InstanceAiTracingService { }, n8nVersion: N8N_VERSION, workflowSdkVersion: WORKFLOW_SDK_VERSION, + browserExtension: options.browserExtension, }); if (tracing) {