Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/studio-tea-telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Studio 的产品行为数据统一上报到 TEA App `1050062`。火山引擎和

| 事件 | 含义 |
| --- | --- |
| `studio_entry_viewed` | 用户打开 Studio 前端页面后的一次匿名入口访问;不要求登录。 |
| `studio_session_started` | 用户身份确认且页面可用后开始的一次 Studio 访问;不是 Agent 对话会话。 |
| `studio_agent_deploy` | 部署或更新 Agent。 |
| `studio_sandbox_create` | 创建 Sandbox。 |
Expand All @@ -22,8 +23,9 @@ Studio 的产品行为数据统一上报到 TEA App `1050062`。火山引擎和

## 指标口径

- Studio 匿名入口访问次数:`studio_entry_viewed` 去重 `page_instance_id`。
- Studio 使用人数:`studio_session_started` 去重 `user_unique_id`。
- Studio 访问次数:去重 `page_instance_id`。
- Studio 登录访问次数:`studio_session_started` 去重 `page_instance_id`。
- 活跃用户池数:过滤空值后去重 `user_pool_id`。
- 每池活跃人数:按 `user_pool_id` 分组后去重 `user_unique_id`。
- 操作尝试数:过滤 `status = started` 后去重 `operation_id`。
Expand All @@ -38,6 +40,10 @@ Studio 的产品行为数据统一上报到 TEA App `1050062`。火山引擎和
的用户池、池内全部成员或当前 Agent/Sandbox 存量。这些资源存量必须来自后端管理接口
或定期快照。

`account_id` 表示部署当前 Studio 的云账号 ID。它在 `veadk studio deploy/update`
时解析并保存到 Studio 运行时环境中,前端通过匿名可访问的 `/web/ui-config` 获取该值,
用于 `studio_entry_viewed` 和后续登录会话埋点,不应作为登录用户身份使用。

## 数据边界

只允许上报已登记的扁平 string/number 字段。禁止上报 Prompt、消息正文、模型响应、
Expand Down
48 changes: 42 additions & 6 deletions frontend/server/storage/provisioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import re
from collections.abc import Mapping
from contextlib import nullcontext
from typing import Any

from . import StudioProvider, StudioStorageConfig
Expand All @@ -35,20 +36,53 @@ def _resolve_account_id(
secret_key: str,
session_token: str,
region: str,
provider: StudioProvider | None = None,
) -> str:
from agentkit.toolkit.volcengine.sts import VeSTS

account_id = VeSTS(
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
region=region,
).get_account_id()
context = nullcontext()
if provider is not None:
from agentkit.platform.context import default_cloud_provider

context = default_cloud_provider(provider)

with context:
account_id = VeSTS(
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
region=region,
).get_account_id()
if account_id is None:
raise StudioStorageProvisioningError("无法获取当前云账号 ID。")
return str(account_id).strip()


def resolve_studio_account_id_for_deploy(
*,
access_key: str,
secret_key: str,
session_token: str,
region: str,
provider: StudioProvider | None = None,
) -> str:
"""Resolve the deployer's cloud account id for Studio runtime metadata."""
try:
return _resolve_account_id(
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
region=region,
provider=provider,
)
except StudioStorageProvisioningError:
raise
except Exception as error:
raise StudioStorageProvisioningError(
f"无法获取当前云账号 ID:{error}"
) from error


def _create_tos_client(
*,
provider: StudioProvider,
Expand Down Expand Up @@ -129,6 +163,7 @@ def resolve_studio_storage_for_deploy(
secret_key=secret_key,
session_token=session_token,
region=region,
provider=provider,
)
except StudioStorageProvisioningError:
raise
Expand Down Expand Up @@ -181,5 +216,6 @@ def resolve_studio_storage_for_deploy(

__all__ = [
"StudioStorageProvisioningError",
"resolve_studio_account_id_for_deploy",
"resolve_studio_storage_for_deploy",
]
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ import {
identifyTelemetryUser,
initTelemetry,
setTelemetryContext,
trackStudioEntryViewed,
trackStudioSessionStarted,
type AgentConnectStartedProps,
type AgentConnectSucceededProps,
Expand Down Expand Up @@ -2386,7 +2387,9 @@ export default function App() {
studioVersion: studio?.version || cfg.version,
environment,
cloudProvider: cfg.provider,
accountId: studio?.accountId ?? "",
});
trackStudioEntryViewed({ authState: "anonymous" });
setFeatures(cfg.features);
setAgentsSource(cfg.agentsSource);
setCloudProvider(cfg.provider);
Expand All @@ -2408,6 +2411,7 @@ export default function App() {
if (!userUniqueId) return;
identifyTelemetryUser({
userUniqueId,
accountId: access.telemetry.accountId ?? "",
userRole: access.role === "admin" ? "admin" : "member",
userSource: localMode ? "local" : "sso",
});
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/adk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2393,6 +2393,7 @@ export interface StudioTelemetryContext {
region: string;
project: string;
version: string;
accountId?: string;
}

export interface StudioTelemetryConfig {
Expand Down Expand Up @@ -2470,6 +2471,7 @@ function normalizeStudioTelemetryConfig(value: unknown): StudioTelemetryConfig {
region: typeof studio.region === "string" ? studio.region : "",
project: typeof studio.project === "string" ? studio.project : "",
version: typeof studio.version === "string" ? studio.version : "",
accountId: typeof studio.accountId === "string" ? studio.accountId : "",
},
};
}
Expand Down Expand Up @@ -2514,6 +2516,7 @@ export interface StudioAccess {
role: StudioRole;
telemetry: {
userId: string;
accountId?: string;
};
capabilities: {
createAgents: boolean;
Expand All @@ -2527,6 +2530,7 @@ export const DEFAULT_STUDIO_ACCESS: StudioAccess = {
role: "user",
telemetry: {
userId: "",
accountId: "",
},
capabilities: {
createAgents: false,
Expand All @@ -2543,6 +2547,10 @@ export async function getStudioAccess(): Promise<StudioAccess> {
if (
!["admin", "developer", "user"].includes(access.role) ||
typeof access.telemetry?.userId !== "string" ||
(
access.telemetry.accountId !== undefined &&
typeof access.telemetry.accountId !== "string"
) ||
typeof access.capabilities?.createAgents !== "boolean" ||
typeof access.capabilities?.manageAgents !== "boolean" ||
!["all", "mine"].includes(access.capabilities?.runtimeScope)
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type {
AgentSourceDownloadFailedProps,
AgentSourceDownloadStartedProps,
AgentSourceDownloadSucceededProps,
EntryViewedProps,
SandboxCreateFailedProps,
SandboxCreateStartedProps,
SandboxCreateSucceededProps,
Expand All @@ -45,6 +46,11 @@ export function identifyTelemetryUser(identity: TelemetryIdentity): void {
runtime.identify(identity);
}

/** Tracks an anonymous Studio page entry before user identity is known. */
export function trackStudioEntryViewed(props: EntryViewedProps): void {
runtime.trackStudioEntryViewed(props);
}

/** Tracks an authenticated, page-ready Studio visit, not an Agent chat session. */
export function trackStudioSessionStarted(props: SessionStartedProps): void {
runtime.trackStudioSessionStarted(props);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/telemetry/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,14 @@ const COMMON_KEYS = [
"studio_version",
"environment",
"cloud_provider",
"account_id",
"user_role",
"user_source",
"page_instance_id",
] as const;

const EVENT_KEYS: Record<StudioTelemetryEventName, readonly string[]> = {
studio_entry_viewed: ["auth_state"],
studio_session_started: ["agents_source"],
studio_agent_deploy: [
"status",
Expand Down
42 changes: 39 additions & 3 deletions frontend/src/telemetry/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type AgentSourceDownloadFailedProps,
type AgentSourceDownloadStartedProps,
type AgentSourceDownloadSucceededProps,
type EntryViewedProps,
type SandboxCreateFailedProps,
type SandboxCreateStartedProps,
type SandboxCreateSucceededProps,
Expand Down Expand Up @@ -62,6 +63,7 @@ export class TelemetryRuntime {
private pageInstanceId: string;
private context: StudioTelemetryContext | undefined;
private identity: TelemetryIdentity | undefined;
private entryViewed = false;
private sessionStarted = false;

constructor(dependencies: TelemetryRuntimeDependencies) {
Expand All @@ -72,7 +74,10 @@ export class TelemetryRuntime {
}

setContext(context: StudioTelemetryContext): void {
this.context = context;
this.context = {
...context,
accountId: context.accountId?.trim() ?? "",
};
}

identify(identity: TelemetryIdentity): void {
Expand All @@ -85,7 +90,11 @@ export class TelemetryRuntime {
this.pageInstanceId = this.createId();
this.sessionStarted = false;
}
this.identity = { ...identity, userUniqueId };
this.identity = {
...identity,
userUniqueId,
accountId: identity.accountId?.trim() ?? "",
};
this.sink.identify?.(userUniqueId);
}

Expand All @@ -98,6 +107,29 @@ export class TelemetryRuntime {
});
}

/** Records one anonymous Studio page entry as soon as the SPA is loaded. */
trackStudioEntryViewed(props: EntryViewedProps): void {
if (this.entryViewed || !this.context) return;
this.entryViewed = true;
const payload = sanitizeTelemetryPayload("studio_entry_viewed", compact({
schema_version: TELEMETRY_SCHEMA_VERSION,
event_id: this.createId(),
user_pool_id: this.context.userPoolId,
studio_deploy_id: this.context.studioDeployId,
vefaas_application_id: this.context.applicationId,
vefaas_function_id: this.context.functionId,
studio_region: this.context.studioRegion,
studio_project: this.context.studioProject,
studio_version: this.context.studioVersion,
environment: this.context.environment,
cloud_provider: this.context.cloudProvider,
account_id: this.context.accountId,
page_instance_id: this.pageInstanceId,
auth_state: props.authState,
}));
this.sink.emit("studio_entry_viewed", payload);
}

beginAgentDeploy(
props: AgentDeployStartedProps,
): TelemetryOperation<AgentDeploySucceededProps, AgentDeployFailedProps> {
Expand Down Expand Up @@ -199,7 +231,10 @@ export class TelemetryRuntime {
}

private beginOperation<Succeeded, Failed>(
name: Exclude<StudioTelemetryEventName, "studio_session_started">,
name: Exclude<
StudioTelemetryEventName,
"studio_entry_viewed" | "studio_session_started"
>,
startedProps: Record<string, unknown>,
successProps: (props: Succeeded) => Record<string, unknown>,
failureProps: (props: Failed) => Record<string, unknown>,
Expand Down Expand Up @@ -253,6 +288,7 @@ export class TelemetryRuntime {
studio_version: this.context.studioVersion,
environment: this.context.environment,
cloud_provider: this.context.cloudProvider,
account_id: this.identity.accountId,
user_role: this.identity.userRole,
user_source: this.identity.userSource,
page_instance_id: this.pageInstanceId,
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/telemetry/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type TelemetryValue = string | number;
export type TelemetryPayload = Record<string, TelemetryValue>;

export type StudioTelemetryEventName =
| "studio_entry_viewed"
| "studio_session_started"
| "studio_agent_deploy"
| "studio_sandbox_create"
Expand All @@ -27,10 +28,12 @@ export interface StudioTelemetryContext {
studioVersion: string;
environment: TelemetryEnvironment;
cloudProvider: "volcengine" | "byteplus";
accountId?: string;
}

export interface TelemetryIdentity {
userUniqueId: string;
accountId?: string;
userRole: UserRole;
userSource: UserSource;
}
Expand All @@ -52,6 +55,10 @@ export interface SessionStartedProps {
agentsSource: "local" | "cloud";
}

export interface EntryViewedProps {
authState: "anonymous";
}

export type DeploySource =
| "scratch"
| "code_package"
Expand Down
11 changes: 9 additions & 2 deletions frontend/tests/studioAccess.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@ const cliFrontendSource = readFileSync(

test("Studio access fails closed until the server-derived role is known", () => {
assert.match(clientSource, /export type StudioRole = "admin" \| "developer" \| "user"/);
assert.match(clientSource, /telemetry:\s*\{\s*userId: string;\s*\}/);
assert.match(clientSource, /export const DEFAULT_STUDIO_ACCESS[\s\S]*?userId: ""[\s\S]*?createAgents: false[\s\S]*?manageAgents: false[\s\S]*?runtimeScope: "mine"/);
assert.match(clientSource, /telemetry:\s*\{\s*userId: string;\s*accountId\?: string;\s*\}/);
assert.match(clientSource, /export const DEFAULT_STUDIO_ACCESS[\s\S]*?userId: ""[\s\S]*?accountId: ""[\s\S]*?createAgents: false[\s\S]*?manageAgents: false[\s\S]*?runtimeScope: "mine"/);
assert.match(clientSource, /typeof access\.telemetry\?\.userId !== "string"/);
assert.match(appSource, /accountId: access\.telemetry\.accountId \?\? ""/);
assert.match(clientSource, /apiFetch\("\/web\/access"\)/);
assert.match(appSource, /if \(!access\) \{\s*return <div className="boot" \/>;\s*\}/);
assert.match(appSource, /setAccess\(DEFAULT_STUDIO_ACCESS\)/);
});

test("Studio entry telemetry uses anonymous UI config metadata", () => {
assert.match(clientSource, /accountId: typeof studio\.accountId === "string"/);
assert.match(appSource, /accountId: studio\?\.accountId \?\? ""/);
assert.match(appSource, /trackStudioEntryViewed\(\{ authState: "anonymous" \}\)/);
});

test("Agent workspace creation and update actions obey Studio access", () => {
assert.doesNotMatch(sidebarSource, /access\.capabilities\.createAgents && show\("addAgent"\)/);
assert.doesNotMatch(sidebarSource, /access\.capabilities\.manageAgents && show\("manageAgents"\)/);
Expand Down
Loading
Loading