Skip to content

Commit d174a6a

Browse files
author
Theodore Li
committed
Add telemetry
1 parent 8a78f80 commit d174a6a

File tree

4 files changed

+97
-33
lines changed

4 files changed

+97
-33
lines changed

apps/sim/app/api/workspaces/[id]/byok-keys/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/per
1212

1313
const logger = createLogger('WorkspaceBYOKKeysAPI')
1414

15-
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'serper', 'exa'] as const
15+
const VALID_PROVIDERS = ['openai', 'anthropic', 'google', 'mistral', 'exa'] as const
1616

1717
const UpsertKeySchema = z.object({
1818
providerId: z.enum(VALID_PROVIDERS),

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ export const isTest = env.NODE_ENV === 'test'
2121
/**
2222
* Is this the hosted version of the application
2323
*/
24-
export const isHosted = true
25-
// getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
26-
// getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
24+
export const isHosted =
25+
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.sim.ai' ||
26+
getEnv('NEXT_PUBLIC_APP_URL') === 'https://www.staging.sim.ai'
2727

2828
/**
2929
* Is billing enforcement enabled

apps/sim/lib/core/telemetry.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,31 @@ export const PlatformEvents = {
934934
})
935935
},
936936

937+
/**
938+
* Track hosted key throttled (rate limited)
939+
*/
940+
hostedKeyThrottled: (attrs: {
941+
toolId: string
942+
envVarName: string
943+
attempt: number
944+
maxRetries: number
945+
delayMs: number
946+
userId?: string
947+
workspaceId?: string
948+
workflowId?: string
949+
}) => {
950+
trackPlatformEvent('platform.hosted_key.throttled', {
951+
'tool.id': attrs.toolId,
952+
'hosted_key.env_var': attrs.envVarName,
953+
'throttle.attempt': attrs.attempt,
954+
'throttle.max_retries': attrs.maxRetries,
955+
'throttle.delay_ms': attrs.delayMs,
956+
...(attrs.userId && { 'user.id': attrs.userId }),
957+
...(attrs.workspaceId && { 'workspace.id': attrs.workspaceId }),
958+
...(attrs.workflowId && { 'workflow.id': attrs.workflowId }),
959+
})
960+
},
961+
937962
/**
938963
* Track chat deployed (workflow deployed as chat interface)
939964
*/

apps/sim/tools/index.ts

Lines changed: 68 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,47 +29,61 @@ import {
2929
getToolAsync,
3030
validateRequiredParametersAfterMerge,
3131
} from '@/tools/utils'
32+
import { PlatformEvents } from '@/lib/core/telemetry'
3233

3334
const logger = createLogger('Tools')
3435

36+
/** Result from hosted key lookup */
37+
interface HostedKeyResult {
38+
key: string
39+
envVarName: string
40+
}
41+
3542
/**
3643
* Get a hosted API key from environment variables
3744
* Supports rotation when multiple keys are configured
45+
* Returns both the key and which env var it came from
3846
*/
39-
function getHostedKeyFromEnv(envKeys: string[]): string | null {
40-
const keys = envKeys
41-
.map((key) => env[key as keyof typeof env])
42-
.filter((value): value is string => Boolean(value))
47+
function getHostedKeyFromEnv(envKeys: string[]): HostedKeyResult | null {
48+
const keysWithNames = envKeys
49+
.map((envVarName) => ({ envVarName, key: env[envVarName as keyof typeof env] }))
50+
.filter((item): item is { envVarName: string; key: string } => Boolean(item.key))
4351

44-
if (keys.length === 0) return null
52+
if (keysWithNames.length === 0) return null
4553

4654
// Round-robin rotation based on current minute
4755
const currentMinute = Math.floor(Date.now() / 60000)
48-
const keyIndex = currentMinute % keys.length
56+
const keyIndex = currentMinute % keysWithNames.length
57+
58+
return keysWithNames[keyIndex]
59+
}
4960

50-
return keys[keyIndex]
61+
/** Result from hosted key injection */
62+
interface HostedKeyInjectionResult {
63+
isUsingHostedKey: boolean
64+
envVarName?: string
5165
}
5266

5367
/**
5468
* Inject hosted API key if tool supports it and user didn't provide one.
5569
* Checks BYOK workspace keys first, then falls back to hosted env keys.
56-
* Returns whether a hosted (billable) key was injected.
70+
* Returns whether a hosted (billable) key was injected and which env var it came from.
5771
*/
5872
async function injectHostedKeyIfNeeded(
5973
tool: ToolConfig,
6074
params: Record<string, unknown>,
6175
executionContext: ExecutionContext | undefined,
6276
requestId: string
63-
): Promise<boolean> {
64-
if (!tool.hosting) return false
65-
if (!isHosted) return false
77+
): Promise<HostedKeyInjectionResult> {
78+
if (!tool.hosting) return { isUsingHostedKey: false }
79+
if (!isHosted) return { isUsingHostedKey: false }
6680

6781
const { envKeys, apiKeyParam, byokProviderId } = tool.hosting
6882
const userProvidedKey = params[apiKeyParam]
6983

7084
if (userProvidedKey) {
7185
logger.debug(`[${requestId}] User provided API key for ${tool.id}, skipping hosted key`)
72-
return false
86+
return { isUsingHostedKey: false }
7387
}
7488

7589
// Check BYOK workspace key first
@@ -82,7 +96,7 @@ async function injectHostedKeyIfNeeded(
8296
if (byokResult) {
8397
params[apiKeyParam] = byokResult.apiKey
8498
logger.info(`[${requestId}] Using BYOK key for ${tool.id}`)
85-
return false // Don't bill - user's own key
99+
return { isUsingHostedKey: false } // Don't bill - user's own key
86100
}
87101
} catch (error) {
88102
logger.error(`[${requestId}] Failed to get BYOK key for ${tool.id}:`, error)
@@ -91,15 +105,15 @@ async function injectHostedKeyIfNeeded(
91105
}
92106

93107
// Fall back to hosted env key
94-
const hostedKey = getHostedKeyFromEnv(envKeys)
95-
if (!hostedKey) {
108+
const hostedKeyResult = getHostedKeyFromEnv(envKeys)
109+
if (!hostedKeyResult) {
96110
logger.debug(`[${requestId}] No hosted key available for ${tool.id}`)
97-
return false
111+
return { isUsingHostedKey: false }
98112
}
99113

100-
params[apiKeyParam] = hostedKey
101-
logger.info(`[${requestId}] Using hosted key for ${tool.id}`)
102-
return true // Bill the user
114+
params[apiKeyParam] = hostedKeyResult.key
115+
logger.info(`[${requestId}] Using hosted key for ${tool.id} (${hostedKeyResult.envVarName})`)
116+
return { isUsingHostedKey: true, envVarName: hostedKeyResult.envVarName }
103117
}
104118

105119
/**
@@ -114,17 +128,25 @@ function isRateLimitError(error: unknown): boolean {
114128
return false
115129
}
116130

131+
/** Context for retry with throttle tracking */
132+
interface RetryContext {
133+
requestId: string
134+
toolId: string
135+
envVarName: string
136+
executionContext?: ExecutionContext
137+
}
138+
117139
/**
118140
* Execute a function with exponential backoff retry for rate limiting errors.
119-
* Only used for hosted key requests.
141+
* Only used for hosted key requests. Tracks throttling events via telemetry.
120142
*/
121143
async function executeWithRetry<T>(
122144
fn: () => Promise<T>,
123-
requestId: string,
124-
toolId: string,
145+
context: RetryContext,
125146
maxRetries = 3,
126147
baseDelayMs = 1000
127148
): Promise<T> {
149+
const { requestId, toolId, envVarName, executionContext } = context
128150
let lastError: unknown
129151

130152
for (let attempt = 0; attempt <= maxRetries; attempt++) {
@@ -138,7 +160,20 @@ async function executeWithRetry<T>(
138160
}
139161

140162
const delayMs = baseDelayMs * Math.pow(2, attempt)
141-
logger.warn(`[${requestId}] Rate limited for ${toolId}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`)
163+
164+
// Track throttling event via telemetry
165+
PlatformEvents.hostedKeyThrottled({
166+
toolId,
167+
envVarName,
168+
attempt: attempt + 1,
169+
maxRetries,
170+
delayMs,
171+
userId: executionContext?.userId,
172+
workspaceId: executionContext?.workspaceId,
173+
workflowId: executionContext?.workflowId,
174+
})
175+
176+
logger.warn(`[${requestId}] Rate limited for ${toolId} (${envVarName}), retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})`)
142177
await new Promise((resolve) => setTimeout(resolve, delayMs))
143178
}
144179
}
@@ -480,7 +515,7 @@ export async function executeTool(
480515
}
481516

482517
// Inject hosted API key if tool supports it and user didn't provide one
483-
const isUsingHostedKey = await injectHostedKeyIfNeeded(
518+
const hostedKeyInfo = await injectHostedKeyIfNeeded(
484519
tool,
485520
contextParams,
486521
executionContext,
@@ -596,7 +631,7 @@ export async function executeTool(
596631
finalResult = await processFileOutputs(finalResult, tool, executionContext)
597632

598633
// Log usage for hosted key if execution was successful
599-
if (isUsingHostedKey && finalResult.success) {
634+
if (hostedKeyInfo.isUsingHostedKey && finalResult.success) {
600635
await logHostedToolUsage(tool, contextParams, finalResult.output, executionContext, requestId)
601636
}
602637

@@ -616,11 +651,15 @@ export async function executeTool(
616651

617652
// Execute the tool request directly (internal routes use regular fetch, external use SSRF-protected fetch)
618653
// Wrap with retry logic for hosted keys to handle rate limiting due to higher usage
619-
const result = isUsingHostedKey
654+
const result = hostedKeyInfo.isUsingHostedKey
620655
? await executeWithRetry(
621656
() => executeToolRequest(toolId, tool, contextParams),
622-
requestId,
623-
toolId
657+
{
658+
requestId,
659+
toolId,
660+
envVarName: hostedKeyInfo.envVarName!,
661+
executionContext,
662+
}
624663
)
625664
: await executeToolRequest(toolId, tool, contextParams)
626665

@@ -641,7 +680,7 @@ export async function executeTool(
641680
finalResult = await processFileOutputs(finalResult, tool, executionContext)
642681

643682
// Log usage for hosted key if execution was successful
644-
if (isUsingHostedKey && finalResult.success) {
683+
if (hostedKeyInfo.isUsingHostedKey && finalResult.success) {
645684
await logHostedToolUsage(tool, contextParams, finalResult.output, executionContext, requestId)
646685
}
647686

0 commit comments

Comments
 (0)