Skip to content

Commit d538b76

Browse files
authored
feat(copilot): server-side mothership tool/vfs/file metrics (#5071)
1 parent 3e2b641 commit d538b76

14 files changed

Lines changed: 518 additions & 125 deletions

apps/sim/instrumentation-node.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,17 @@ function normalizeOtlpMetricsUrl(url: string): string {
8383
}
8484
}
8585

86+
// deployment.environment in the GO value space (dev | staging | prod) without
87+
// any new infra env var. Every deployed Sim tier already gets
88+
// APPCONFIG_ENVIRONMENT = the infra env name (dev | staging | production), so we
89+
// reuse it and map production -> prod to match Go's `OTEL_DEPLOYMENT_ENVIRONMENT`
90+
// (and thus a single Grafana $env filter spans Sim + Go). Returns undefined when
91+
// unset (local dev) so the OTEL_/NODE_ENV fallbacks still apply.
92+
function deploymentEnvFromAppConfig(v: string | undefined): string | undefined {
93+
if (!v) return undefined
94+
return v === 'production' ? 'prod' : v
95+
}
96+
8697
// Sampling ratio from env (mirrors Go's `samplerFromEnv`); fallback
8798
// is 100% everywhere. Retention caps cost, not sampling.
8899
function resolveSamplingRatio(_isLocalEndpoint: boolean): number {
@@ -270,11 +281,15 @@ async function initializeOpenTelemetry() {
270281
resourceFromAttributes({
271282
[ATTR_SERVICE_NAME]: telemetryConfig.serviceName,
272283
[ATTR_SERVICE_VERSION]: telemetryConfig.serviceVersion,
273-
// OTEL_ → DEPLOYMENT_ENVIRONMENT → NODE_ENV; matches Go's
274-
// `resourceEnvFromEnv()` so both halves tag the same value.
284+
// OTEL_ → DEPLOYMENT_ENVIRONMENT → APPCONFIG_ENVIRONMENT (mapped to the
285+
// Go value space) → NODE_ENV. Matches Go's `resourceEnvFromEnv()` so a
286+
// single $env spans Sim + Go. APPCONFIG_ENVIRONMENT (already set on every
287+
// deployed tier) is the fix that stops deployed Sim tagging everything
288+
// "production" via the NODE_ENV fallback — no new infra env var needed.
275289
[ATTR_DEPLOYMENT_ENVIRONMENT]:
276290
process.env.OTEL_DEPLOYMENT_ENVIRONMENT ||
277291
process.env.DEPLOYMENT_ENVIRONMENT ||
292+
deploymentEnvFromAppConfig(process.env.APPCONFIG_ENVIRONMENT) ||
278293
env.NODE_ENV ||
279294
'development',
280295
'service.namespace': 'mothership',

apps/sim/lib/copilot/chat/payload.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface BuildPayloadParams {
3737
userTimezone?: string
3838
userMetadata?: {
3939
name?: string
40+
email?: string
4041
timezone?: string
4142
}
4243
includeMothershipTools?: boolean
@@ -367,7 +368,8 @@ export async function buildCopilotRequestPayload(
367368
...(params.workspaceContext ? { workspaceContext: params.workspaceContext } : {}),
368369
...(params.userPermission ? { userPermission: params.userPermission } : {}),
369370
...(params.userTimezone ? { userTimezone: params.userTimezone } : {}),
370-
...(params.userMetadata && (params.userMetadata.name || params.userMetadata.timezone)
371+
...(params.userMetadata &&
372+
(params.userMetadata.name || params.userMetadata.email || params.userMetadata.timezone)
371373
? { userMetadata: params.userMetadata }
372374
: {}),
373375
// Tell the copilot file subagent which document toolchain to write. Emitted

apps/sim/lib/copilot/chat/post.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ type UnifiedChatBranch =
169169
fileAttachments?: UnifiedChatRequest['fileAttachments']
170170
userPermission?: string
171171
userTimezone?: string
172-
userMetadata?: { name?: string; timezone?: string }
172+
userMetadata?: { name?: string; email?: string; timezone?: string }
173173
workflowId: string
174174
workflowName?: string
175175
workspaceId?: string
@@ -204,7 +204,7 @@ type UnifiedChatBranch =
204204
fileAttachments?: UnifiedChatRequest['fileAttachments']
205205
userPermission?: string
206206
userTimezone?: string
207-
userMetadata?: { name?: string; timezone?: string }
207+
userMetadata?: { name?: string; email?: string; timezone?: string }
208208
workspaceContext?: string
209209
}) => Promise<Record<string, unknown>>
210210
buildExecutionContext: (params: {
@@ -722,6 +722,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
722722
const body = ChatMessageSchema.parse(await req.json())
723723
const userMetadata = {
724724
...(authenticatedUserName ? { name: authenticatedUserName } : {}),
725+
...(authenticatedUserEmail ? { email: authenticatedUserEmail } : {}),
725726
...(body.userTimezone ? { timezone: body.userTimezone } : {}),
726727
}
727728
const normalizedContexts = normalizeContexts(body.contexts) ?? []
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// AUTO-GENERATED FILE. DO NOT EDIT.
2+
//
3+
// Source: copilot/copilot/contracts/metrics-v1.schema.json
4+
// Regenerate with: bun run metrics-contract:generate
5+
//
6+
// Canonical mothership OTel metric names. Call sites should reference
7+
// `Metric.<Identifier>` (e.g. `Metric.CopilotToolDuration`) rather than raw
8+
// string literals, so the Go-side contract is the single source of truth and
9+
// typos become compile errors.
10+
//
11+
// NAMES ONLY. Label keys and histogram bucket boundaries are NOT in this
12+
// contract — Go owns the label-cardinality allowlist and the shared bucket
13+
// constant, and the Sim emitter MUST mirror those by hand so the Go∪Sim metric
14+
// union is queryable as one series set.
15+
16+
export const Metric = {
17+
CopilotCacheAttempted: 'copilot.cache.attempted',
18+
CopilotCacheHit: 'copilot.cache.hit',
19+
CopilotCacheWrite: 'copilot.cache.write',
20+
CopilotFileReadDuration: 'copilot.file.read.duration',
21+
CopilotFileReadSize: 'copilot.file.read.size',
22+
CopilotMessagesSerializeDuration: 'copilot.messages.serialize.duration',
23+
CopilotRequestCount: 'copilot.request.count',
24+
CopilotRequestDuration: 'copilot.request.duration',
25+
CopilotToolCalls: 'copilot.tool.calls',
26+
CopilotToolDuration: 'copilot.tool.duration',
27+
CopilotVfsMaterializeDuration: 'copilot.vfs.materialize.duration',
28+
GenAiClientCacheTokenUsage: 'gen_ai.client.cache.token.usage',
29+
GenAiClientTokenUsage: 'gen_ai.client.token.usage',
30+
LlmClientErrors: 'llm.client.errors',
31+
LlmClientOutputCutoff: 'llm.client.output_cutoff',
32+
LlmClientStreamDuration: 'llm.client.stream.duration',
33+
LlmClientTimeToFirstToken: 'llm.client.time_to_first_token',
34+
} as const
35+
36+
export type MetricKey = keyof typeof Metric
37+
export type MetricValue = (typeof Metric)[MetricKey]
38+
39+
/** Readonly sorted list of every canonical mothership metric name. */
40+
export const MetricValues: readonly MetricValue[] = [
41+
'copilot.cache.attempted',
42+
'copilot.cache.hit',
43+
'copilot.cache.write',
44+
'copilot.file.read.duration',
45+
'copilot.file.read.size',
46+
'copilot.messages.serialize.duration',
47+
'copilot.request.count',
48+
'copilot.request.duration',
49+
'copilot.tool.calls',
50+
'copilot.tool.duration',
51+
'copilot.vfs.materialize.duration',
52+
'gen_ai.client.cache.token.usage',
53+
'gen_ai.client.token.usage',
54+
'llm.client.errors',
55+
'llm.client.output_cutoff',
56+
'llm.client.stream.duration',
57+
'llm.client.time_to_first_token',
58+
] as const

0 commit comments

Comments
 (0)