mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 13:39:44 +02:00
feat: Add Browser Use extension version to telemetry (no-changelog) (#36602)
This commit is contained in:
parent
53f2750b3f
commit
f7ddca72bf
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> | 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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
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) }
|
||||
: {}),
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>;
|
||||
browserExtension?: BrowserExtensionTraceContext;
|
||||
/** Defer process-local registration until the durable resume claim succeeds. */
|
||||
register?: boolean;
|
||||
}): Promise<InstanceAiTraceContext | undefined> {
|
||||
|
|
@ -226,6 +228,7 @@ export class InstanceAiTracingService {
|
|||
},
|
||||
n8nVersion: N8N_VERSION,
|
||||
workflowSdkVersion: WORKFLOW_SDK_VERSION,
|
||||
browserExtension: options.browserExtension,
|
||||
});
|
||||
|
||||
if (tracing) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user