Skip to content

Commit cec141b

Browse files
committed
fix(amplitude): send both saved keys to User Profile over Basic auth
Amplitude's User Profile API documents only Basic authentication with the project API key and secret key. A saved Amplitude credential now fills the API key too, and the tool sends Basic auth whenever an API key is present. Inline calls that pass only the secret key keep the Api-Key header they sent before.
1 parent 42d8761 commit cec141b

6 files changed

Lines changed: 70 additions & 6 deletions

File tree

‎apps/docs/content/docs/integrations/amplitude.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ReferenceDetails } from "@/components/ui/reference-details"
1212
{/* MANUAL-CONTENT-START:intro */}
1313
Use [Amplitude](https://amplitude.com/) to send events, update user and group properties, and query product analytics from a workflow. The HTTP and Dashboard REST APIs use API-key and secret-key authentication.
1414

15-
Connect an Amplitude project once as an Amplitude credential: in Amplitude, open **Settings > Projects**, choose the project, and copy the **API key** and **secret key** from its **General** tab. Enter `eu` as the data residency only for a project hosted in Amplitude's EU data center. Sim checks the pair by listing the project's events, a read with no billing cost. Event and identify operations send the API key, analytics operations send both keys, and **User Profile** sends the secret key; User Profile is not available for EU projects. Workflows built with the earlier Amplitude block keep the keys they entered in the block.
15+
Connect an Amplitude project once as an Amplitude credential: in Amplitude, open **Settings > Projects**, choose the project, and copy the **API key** and **secret key** from its **General** tab. Enter `eu` as the data residency only for a project hosted in Amplitude's EU data center. Sim checks the pair by listing the project's events, a read with no billing cost. Event and identify operations send the API key, analytics operations and **User Profile** send both keys; User Profile is not available for EU projects. Workflows built with the earlier Amplitude block keep the keys they entered in the block.
1616
{/* MANUAL-CONTENT-END */}
1717

1818
## Use the block [#usage-instructions]

‎apps/sim/tools/amplitude/types.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export interface AmplitudeUserActivityResponse extends ToolResponse {
140140
* User Profile params (User Profile API).
141141
*/
142142
export interface AmplitudeUserProfileParams {
143+
apiKey?: string
143144
secretKey: string
144145
userId?: string
145146
deviceId?: string

‎apps/sim/tools/amplitude/user_profile.ts‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ export const userProfileTool: ToolConfig<AmplitudeUserProfileParams, AmplitudeUs
1515
oauth: AMPLITUDE_PROFILE_CREDENTIAL_OAUTH,
1616

1717
params: {
18+
apiKey: {
19+
type: 'string',
20+
required: false,
21+
visibility: 'user-only',
22+
description:
23+
'Amplitude API Key. With it, the request authenticates with both keys over Basic auth',
24+
},
1825
secretKey: {
1926
type: 'string',
2027
required: true,
@@ -64,9 +71,15 @@ export const userProfileTool: ToolConfig<AmplitudeUserProfileParams, AmplitudeUs
6471
return url.toString()
6572
},
6673
method: 'GET',
67-
headers: (params) => ({
68-
Authorization: `Api-Key ${params.secretKey}`,
69-
}),
74+
headers: (params) => {
75+
const apiKey = params.apiKey?.trim()
76+
// Without an API key (an inline block that never asked for one), keep the secret-key-only header.
77+
return {
78+
Authorization: apiKey
79+
? `Basic ${btoa(`${apiKey}:${params.secretKey}`)}`
80+
: `Api-Key ${params.secretKey}`,
81+
}
82+
},
7083
},
7184

7285
transformResponse: async (response: Response) => {

‎apps/sim/tools/generated/tool-metadata.ts‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

‎apps/sim/tools/index.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
ResolvedSecretTraceRegistry,
8080
} from '@/executor/utils/resolved-secret-trace-registry'
8181
import { prepareToolExecution, transformBlockTool } from '@/providers/utils'
82+
import { userProfileTool as amplitudeUserProfileTool } from '@/tools/amplitude/user_profile'
8283
import { bitbucketGetPipelineStepLogTool } from '@/tools/bitbucket/get_pipeline_step_log'
8384
import { docusignGetEnvelopeTool } from '@/tools/docusign/get_envelope'
8485
import { ErrorExtractorId } from '@/tools/error-extractors'
@@ -245,6 +246,7 @@ vi.mock('@/executor/handlers/workflow/custom-block-tool-runner', () => ({
245246
// Mock the tools registry to avoid loading the full 4500+ line registry file.
246247
// Only the tools actually exercised in tests are provided.
247248
const mockRegistryTools: Record<string, any> = {
249+
amplitude_user_profile: amplitudeUserProfileTool,
248250
stripe_list_subscriptions: stripeListSubscriptionsTool,
249251
stripe_search_subscriptions: stripeSearchSubscriptionsTool,
250252
slack_lists_items_list: slackListsItemsListTool,
@@ -4171,6 +4173,53 @@ describe('Internal Route Trust', () => {
41714173
expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled()
41724174
})
41734175

4176+
it('reads an Amplitude profile with Basic auth over both keys of a saved credential', async () => {
4177+
mockResolveExecutorCredentialToken.mockResolvedValue({
4178+
accessToken: 'saved-secret-key',
4179+
credentialType: 'service_account',
4180+
providerWorkspaceId: 'saved-api-key',
4181+
})
4182+
mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
4183+
toSecureFetchResponse(Response.json({ userData: {} }))
4184+
)
4185+
4186+
const result = await executeTool('amplitude_user_profile', {
4187+
credential: 'saved-credential',
4188+
userId: 'user-1',
4189+
})
4190+
4191+
expect(result.success).toBe(true)
4192+
expect(mockSecureFetchWithPinnedIP).toHaveBeenLastCalledWith(
4193+
'https://profile-api.amplitude.com/v1/userprofile?user_id=user-1',
4194+
expect.anything(),
4195+
expect.objectContaining({
4196+
headers: expect.objectContaining({
4197+
authorization: `Basic ${Buffer.from('saved-api-key:saved-secret-key').toString('base64')}`,
4198+
}),
4199+
})
4200+
)
4201+
})
4202+
4203+
it('keeps the secret-key-only Api-Key header for an inline Amplitude profile read', async () => {
4204+
mockSecureFetchWithPinnedIP.mockResolvedValueOnce(
4205+
toSecureFetchResponse(Response.json({ userData: {} }))
4206+
)
4207+
4208+
const result = await executeTool('amplitude_user_profile', {
4209+
secretKey: 'inline-secret-key',
4210+
userId: 'user-1',
4211+
})
4212+
4213+
expect(result.success).toBe(true)
4214+
expect(mockSecureFetchWithPinnedIP).toHaveBeenLastCalledWith(
4215+
'https://profile-api.amplitude.com/v1/userprofile?user_id=user-1',
4216+
expect.anything(),
4217+
expect.objectContaining({
4218+
headers: expect.objectContaining({ authorization: 'Api-Key inline-secret-key' }),
4219+
})
4220+
)
4221+
})
4222+
41744223
it('makes copilot pass credentialId or the inline values, and honors credentialId alone', async () => {
41754224
const _context = {
41764225
workspaceId: 'workspace-456',

‎apps/sim/tools/shared/saved-credentials.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ export const ALGOLIA_CREDENTIAL_OAUTH = tokenCredentialOAuth('algolia', {
201201
* project API key (as its account id), the secret key (as its token), and the data residency
202202
* region (as its domain). Each policy maps only the params its tools declare: the Dashboard REST
203203
* tools send both keys to the region's host, the ingestion tools send the API key to the
204-
* region's host, and the User Profile tool sends the secret key to its single US host.
204+
* region's host, and the User Profile tool sends both keys to its single US host.
205205
*/
206206
export const AMPLITUDE_CREDENTIAL_OAUTH = tokenCredentialOAuth('amplitude', {
207207
apiKey: 'providerWorkspaceId',
@@ -213,6 +213,7 @@ export const AMPLITUDE_INGESTION_CREDENTIAL_OAUTH = tokenCredentialOAuth('amplit
213213
dataResidency: 'domain',
214214
})
215215
export const AMPLITUDE_PROFILE_CREDENTIAL_OAUTH = tokenCredentialOAuth('amplitude', {
216+
apiKey: 'providerWorkspaceId',
216217
secretKey: 'accessToken',
217218
})
218219
export const APIFY_CREDENTIAL_OAUTH = tokenCredentialOAuth('apify', { apiKey: 'accessToken' })

0 commit comments

Comments
 (0)