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
20 changes: 18 additions & 2 deletions docs/studio-tea-telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,17 @@ Studio 的产品行为数据统一上报到 TEA App `1050062`。火山引擎和
## 数据边界

只允许上报已登记的扁平 string/number 字段。禁止上报 Prompt、消息正文、模型响应、
源码、文件路径、自由文本错误、错误堆栈、Cookie、Token、AK/SK 或其他密钥。错误只保留
稳定的 `error_kind`、可选 `error_code` 和适用事件的 `failed_phase`。
源码、文件路径、错误堆栈、Cookie、Token、AK/SK 或其他密钥。错误默认保留稳定的
`error_kind`、可选 `error_code` 和适用事件的 `failed_phase`。
`studio_agent_deploy` 失败事件额外允许上报经过前端脱敏与长度限制的 `error_message`,
用于定位部署、构建和 Runtime 初始化失败原因。构建阶段失败时,`error_message` 优先来自
前端已收到的 CodePipeline 构建日志文本;没有可用构建日志时再回退到部署接口返回的
错误摘要。构建日志较长时保留末尾内容,避免截掉通常位于日志尾部的真实失败行。
后端在构建失败的最终同步中会有限重试拉取 CodePipeline 日志,直到保留的日志尾部包含
构建错误 marker,降低日志服务延迟导致埋点只拿到镜像下载/解压进度的概率。
部署主阶段按 `prepare -> upload -> build -> deploy -> publish -> update -> evaluation`
单调推进;CodePipeline 日志事件只更新构建日志,不应把已经进入部署阶段的任务回退为
构建阶段。

`user_unique_id` 通过 TEA `config` 设置,不作为自定义事件属性重复发送。所有资源 ID
在模块边界转换为字符串;布尔属性使用 `0/1`。
Expand All @@ -58,3 +67,10 @@ Studio 的产品行为数据统一上报到 TEA App `1050062`。火山引擎和
本迁移仅删除 Studio 产品行为埋点使用的前端 APMPlus Web SDK 和配置传递链路。
`veadk/tracing/telemetry/`、APMPlus OpenTelemetry exporter、Runtime trace 和问题反馈中的
APMPlus 查询能力不在迁移范围内并继续保留。

## 开发排障记录

- 检查 Studio 后端使用的 AgentKit SDK 时,应优先使用仓库虚拟环境
`.venv/bin/python`。当前全局 `python` 命令可能不存在,且全局 `python3` 环境可能未安装
`agentkit` 包,直接执行 `python3 -c 'import agentkit'` 会得到
`ModuleNotFoundError: No module named 'agentkit'`。
28 changes: 24 additions & 4 deletions frontend/server/studio_update_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
from typing import Any, Literal

from frontend.server.storage.provisioning import resolve_studio_storage_for_deploy
from veadk.cli.studio_account_id import (
resolve_studio_account_id_metadata,
studio_account_id_environment,
)
from veadk.utils.cloud_provider import CloudProvider

SnapshotKind = Literal["codex", "openclaw", "hermes"]
Expand All @@ -31,10 +35,10 @@
)


def _function_config(
def _function_state(
function_client: Any,
function_id: str,
) -> dict[str, str]:
) -> tuple[object, dict[str, str]]:
import volcenginesdkvefaas

function = function_client.get_function(
Expand All @@ -45,7 +49,7 @@ def _function_config(
for item in (getattr(function, "envs", None) or [])
if getattr(item, "key", None)
}
return environment
return function, environment


def _provision_snapshot_tool(
Expand Down Expand Up @@ -135,7 +139,7 @@ def reconcile_studio_update_resources(
session_token: str,
) -> dict[str, str]:
"""Return environment overrides for resources missing from an older Studio."""
environment = _function_config(function_client, function_id)
function, environment = _function_state(function_client, function_id)
overrides: dict[str, str] = {}

from veadk.cli.studio_knowledge_signing import (
Expand Down Expand Up @@ -167,6 +171,22 @@ def reconcile_studio_update_resources(
}
)

account_resolution = resolve_studio_account_id_metadata(
environment={**environment, **overrides},
remote_function=function,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
region=region,
provider=provider,
)
overrides.update(
studio_account_id_environment(
account_resolution,
clear_error_on_success=True,
)
)

missing_snapshot_tools = [
item for item in _SNAPSHOT_ENVIRONMENTS if not environment.get(item[0])
]
Expand Down
1 change: 1 addition & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2937,6 +2937,7 @@ export default function App() {
environment,
cloudProvider: cfg.provider,
accountId: studio?.accountId ?? "",
accountIdResolutionError: studio?.accountIdResolutionError ?? "",
});
trackStudioEntryViewed({ authState: "anonymous" });
setFeatures(cfg.features);
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/adk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2637,6 +2637,7 @@ export interface StudioTelemetryContext {
project: string;
version: string;
accountId?: string;
accountIdResolutionError?: string;
}

export interface StudioTelemetryConfig {
Expand Down Expand Up @@ -2715,6 +2716,10 @@ function normalizeStudioTelemetryConfig(value: unknown): StudioTelemetryConfig {
project: typeof studio.project === "string" ? studio.project : "",
version: typeof studio.version === "string" ? studio.version : "",
accountId: typeof studio.accountId === "string" ? studio.accountId : "",
accountIdResolutionError:
typeof studio.accountIdResolutionError === "string"
? studio.accountIdResolutionError
: "",
},
};
}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/automations/feishu/FeishuBotIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import {
beginAgentDeploy,
classifyTelemetryError,
safeTelemetryErrorMessage,
type AgentDeployFailedProps,
} from "../../telemetry";
import feishuLogo from "../../assets/feishu-logo.svg";
Expand Down Expand Up @@ -227,6 +228,7 @@ export function FeishuBotIntegration({ onBack }: FeishuBotIntegrationProps) {
operation.fail({
failedPhase: telemetryDeployPhase(latestPhaseRef.current),
errorKind: "abort",
errorMessage: safeTelemetryErrorMessage("用户取消部署"),
});
return;
}
Expand All @@ -242,6 +244,7 @@ export function FeishuBotIntegration({ onBack }: FeishuBotIntegrationProps) {
...(cancelledRef.current
? { errorKind: "abort" as const }
: classifyTelemetryError(error, { phase: latestPhaseRef.current })),
errorMessage: safeTelemetryErrorMessage(error),
});
if (!mountedRef.current || cancelledRef.current) return;
setDeploymentStatus("failed");
Expand All @@ -268,6 +271,7 @@ export function FeishuBotIntegration({ onBack }: FeishuBotIntegrationProps) {
deploymentOperationRef.current?.fail({
failedPhase: telemetryDeployPhase(latestPhaseRef.current),
errorKind: "abort",
errorMessage: safeTelemetryErrorMessage("用户取消部署"),
});
if (mountedRef.current) setDeploymentStatus("cancelled");
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TeaClient, type TeaClientConfig } from "./client";
import { TelemetryRuntime } from "./runtime";
export { classifyTelemetryError } from "./privacy";
export { classifyTelemetryError, safeTelemetryErrorMessage } from "./privacy";
export type {
ClassifiedTelemetryError,
TelemetryErrorContext,
Expand Down
74 changes: 73 additions & 1 deletion frontend/src/telemetry/privacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@ export interface TelemetryErrorContext {

interface ErrorShape {
code?: unknown;
message?: unknown;
name?: unknown;
status?: unknown;
}

interface TelemetryMessageOptions {
preserveEnd?: boolean;
}

const DEFAULT_STRING_MAX_LENGTH = 256;
const ERROR_MESSAGE_MAX_LENGTH = 1024;
const REDACTED = "[REDACTED]";

function stableErrorCode(value: unknown): string | undefined {
if (typeof value !== "string" && typeof value !== "number") return undefined;
const code = String(value).trim();
Expand All @@ -33,6 +42,58 @@ function classifiedTelemetryError(
return errorCode === undefined ? { errorKind } : { errorKind, errorCode };
}

function truncateTelemetryString(
value: string,
maxLength: number,
options: TelemetryMessageOptions = {},
): string {
if (value.length <= maxLength) return value;
if (options.preserveEnd) {
const prefix = "[truncated] ...";
return `${prefix}${value.slice(-Math.max(0, maxLength - prefix.length))}`;
}
const suffix = "... [truncated]";
return `${value.slice(0, Math.max(0, maxLength - suffix.length))}${suffix}`;
}

function redactTelemetryMessage(value: string): string {
return value
.replace(
/\b(Authorization\s*[:=]\s*)(Bearer\s+)?[^\s"',;&]+/gi,
(_match, prefix: string, bearer: string | undefined) =>
`${prefix}${bearer ?? ""}${REDACTED}`,
)
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED}`)
.replace(
/\b([\w.-]*(?:token|password|passwd|secret|api[_-]?key|access[_-]?key|secret[_-]?key|cookie)[\w.-]*\s*[:=]\s*)(["']?)[^\s"',;&]+/gi,
(_match, prefix: string, quote: string) => `${prefix}${quote}${REDACTED}`,
);
}

/** Returns a compact, redacted error message suitable for product telemetry. */
export function safeTelemetryErrorMessage(
error: unknown,
options: TelemetryMessageOptions = {},
): string | undefined {
const shape = error !== null && typeof error === "object"
? error as ErrorShape
: {};
const raw = typeof shape.message === "string"
? shape.message
: typeof error === "string" ||
typeof error === "number" ||
typeof error === "boolean"
? String(error)
: "";
const normalized = raw.replace(/\s+/g, " ").trim();
if (!normalized) return undefined;
return truncateTelemetryString(
redactTelemetryMessage(normalized),
ERROR_MESSAGE_MAX_LENGTH,
options,
);
}

/** Converts an unknown failure to an approved category without reading its text. */
export function classifyTelemetryError(
error: unknown,
Expand Down Expand Up @@ -97,6 +158,7 @@ const COMMON_KEYS = [
"environment",
"cloud_provider",
"account_id",
"account_id_resolution_error",
"user_role",
"user_source",
"page_instance_id",
Expand All @@ -120,6 +182,7 @@ const EVENT_KEYS: Record<StudioTelemetryEventName, readonly string[]> = {
"failed_phase",
"error_kind",
"error_code",
"error_message",
],
studio_sandbox_create: [
"status",
Expand Down Expand Up @@ -194,7 +257,16 @@ export function sanitizeTelemetryPayload(
const payload: TelemetryPayload = {};
for (const [key, value] of Object.entries(input)) {
if (!allowed.has(key) || !isTelemetryValue(value)) continue;
payload[key] = typeof value === "string" ? value.slice(0, 256) : value;
if (typeof value === "string") {
payload[key] = truncateTelemetryString(
value,
key === "error_message"
? ERROR_MESSAGE_MAX_LENGTH
: DEFAULT_STRING_MAX_LENGTH,
);
} else {
payload[key] = value;
}
}
return payload;
}
6 changes: 6 additions & 0 deletions frontend/src/telemetry/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class TelemetryRuntime {
this.context = {
...context,
accountId: context.accountId?.trim() ?? "",
accountIdResolutionError: context.accountIdResolutionError?.trim() ?? "",
};
}

Expand Down Expand Up @@ -124,6 +125,8 @@ export class TelemetryRuntime {
environment: this.context.environment,
cloud_provider: this.context.cloudProvider,
account_id: this.context.accountId,
account_id_resolution_error:
this.context.accountIdResolutionError || undefined,
page_instance_id: this.pageInstanceId,
auth_state: props.authState,
}));
Expand All @@ -146,6 +149,7 @@ export class TelemetryRuntime {
failed_phase: result.failedPhase,
error_kind: result.errorKind,
error_code: result.errorCode,
error_message: result.errorMessage,
}));
}

Expand Down Expand Up @@ -289,6 +293,8 @@ export class TelemetryRuntime {
environment: this.context.environment,
cloud_provider: this.context.cloudProvider,
account_id: this.identity.accountId,
account_id_resolution_error:
this.context.accountIdResolutionError || undefined,
user_role: this.identity.userRole,
user_source: this.identity.userSource,
page_instance_id: this.pageInstanceId,
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/telemetry/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface StudioTelemetryContext {
environment: TelemetryEnvironment;
cloudProvider: "volcengine" | "byteplus";
accountId?: string;
accountIdResolutionError?: string;
}

export interface TelemetryIdentity {
Expand Down Expand Up @@ -105,6 +106,7 @@ export interface AgentDeployFailedProps {
| "unknown";
errorKind: ErrorKind;
errorCode?: string;
errorMessage?: string;
}

export type SandboxKind = "codex" | "deepseek-harness" | "openclaw" | "hermes";
Expand Down
Loading
Loading