Skip to content
Open
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
443 changes: 437 additions & 6 deletions apps/sim/blocks/blocks/amplitude.ts

Large diffs are not rendered by default.

63 changes: 60 additions & 3 deletions apps/sim/tools/amplitude/event_segmentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
AmplitudeEventSegmentationParams,
AmplitudeEventSegmentationResponse,
} from '@/tools/amplitude/types'
import { getDashboardHost } from '@/tools/amplitude/utils'
import type { ToolConfig } from '@/tools/types'

export const eventSegmentationTool: ToolConfig<
Expand Down Expand Up @@ -64,25 +65,81 @@ export const eventSegmentationTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Property name to group by (prefix custom user properties with "gp:")',
},
groupBy2: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Second property name to group by (prefix custom user properties with "gp:")',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of group-by values (max 1000)',
},
filters: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'JSON array of filter objects applied to the event, e.g. [{"subprop_type":"event","subprop_key":"city","subprop_op":"is","subprop_value":["San Francisco"]}]',
},
formula: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Required when metric is "formula", e.g. "UNIQUES(A)/UNIQUES(B)"',
},
segment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON segment definition(s) applied to the query',
},
dataResidency: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Data residency region: "us" (default) or "eu"',
},
},

request: {
url: (params) => {
const url = new URL('https://amplitude.com/api/2/events/segmentation')
const eventObj = JSON.stringify({ event_type: params.eventType })
url.searchParams.set('e', eventObj)
const url = new URL(`${getDashboardHost(params.dataResidency)}/api/2/events/segmentation`)
const event: Record<string, unknown> = { event_type: params.eventType }

if (params.filters) {
let parsedFilters: unknown
try {
parsedFilters = JSON.parse(params.filters)
} catch {
parsedFilters = undefined
}
if (!Array.isArray(parsedFilters)) {
throw new Error(
'Amplitude Event Segmentation: "filters" must be a valid JSON array of filter objects'
)
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.
event.filters = parsedFilters
}

if (params.metric === 'formula' && !params.formula) {
throw new Error(
'Amplitude Event Segmentation: "formula" is required when metric is "formula"'
)
}

url.searchParams.set('e', JSON.stringify(event))
url.searchParams.set('start', params.start)
url.searchParams.set('end', params.end)
if (params.metric) url.searchParams.set('m', params.metric)
if (params.interval) url.searchParams.set('i', params.interval)
if (params.groupBy) url.searchParams.set('g', params.groupBy)
if (params.groupBy2) url.searchParams.set('g2', params.groupBy2)
if (params.limit) url.searchParams.set('limit', params.limit)
if (params.formula) url.searchParams.set('formula', params.formula)
Comment thread
waleedlatif1 marked this conversation as resolved.
if (params.segment) url.searchParams.set('s', params.segment)
return url.toString()
},
method: 'GET',
Expand Down
195 changes: 195 additions & 0 deletions apps/sim/tools/amplitude/funnels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import type { AmplitudeFunnelsParams, AmplitudeFunnelsResponse } from '@/tools/amplitude/types'
import { getDashboardHost } from '@/tools/amplitude/utils'
import type { ToolConfig } from '@/tools/types'

export const funnelsTool: ToolConfig<AmplitudeFunnelsParams, AmplitudeFunnelsResponse> = {
id: 'amplitude_funnels',
name: 'Amplitude Funnels',
description: 'Analyze conversion rates and drop-off between a sequence of events.',
version: '1.0.0',

params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Amplitude API Key',
},
secretKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Amplitude Secret Key',
},
events: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON array of event objects, one per funnel step in order, e.g. [{"event_type":"signup"},{"event_type":"purchase"}]',
},
start: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Start date in YYYYMMDD format',
},
end: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'End date in YYYYMMDD format',
},
mode: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Funnel ordering: "ordered", "unordered", or "sequential" (default: ordered)',
},
userType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'User type: "new" or "active" (default: active)',
},
interval: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Time interval: -300000 (real-time), -3600000 (hourly), 1 (daily), 7 (weekly), or 30 (monthly)',
},
conversionWindowSeconds: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Conversion window in seconds (default: 2592000, i.e. 30 days)',
},
groupBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Property to group by (limit: one; prefix custom properties with "gp:")',
},
limit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of group-by values (default: 100, max: 1000)',
},
segment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON segment definition(s) applied to the query',
},
dataResidency: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Data residency region: "us" (default) or "eu"',
},
},

request: {
url: (params) => {
const url = new URL(`${getDashboardHost(params.dataResidency)}/api/2/funnels`)
let parsed: unknown
try {
parsed = JSON.parse(params.events)
} catch {
throw new Error('Amplitude Funnels: "events" must be a valid JSON array of event objects')
}
Comment thread
waleedlatif1 marked this conversation as resolved.
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
Boolean(value) && typeof value === 'object' && !Array.isArray(value)

if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every(isPlainObject)) {
throw new Error(
'Amplitude Funnels: "events" must be a non-empty JSON array of event objects'
)
Comment thread
waleedlatif1 marked this conversation as resolved.
}
Comment thread
waleedlatif1 marked this conversation as resolved.
for (const step of parsed) {
url.searchParams.append('e', JSON.stringify(step))
}
Comment thread
waleedlatif1 marked this conversation as resolved.
Comment thread
waleedlatif1 marked this conversation as resolved.
url.searchParams.set('start', params.start)
url.searchParams.set('end', params.end)
if (params.mode) url.searchParams.set('mode', params.mode)
if (params.userType) url.searchParams.set('n', params.userType)
if (params.interval) url.searchParams.set('i', params.interval)
if (params.conversionWindowSeconds) url.searchParams.set('cs', params.conversionWindowSeconds)
if (params.groupBy) url.searchParams.set('g', params.groupBy)
if (params.limit) url.searchParams.set('limit', params.limit)
if (params.segment) url.searchParams.set('s', params.segment)
return url.toString()
},
method: 'GET',
headers: (params) => ({
Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`,
}),
},

transformResponse: async (response: Response) => {
const data = await response.json()

if (!response.ok) {
throw new Error(data.error || `Amplitude Funnels API error: ${response.status}`)
}

const results = (Array.isArray(data.data) ? data.data : []) as Array<Record<string, unknown>>

const funnels = results.map((r) => {
Comment thread
waleedlatif1 marked this conversation as resolved.
const dayFunnels = r.dayFunnels as Record<string, unknown> | undefined
return {
stepByStep: (r.stepByStep as number[]) ?? [],
cumulative: (r.cumulative as number[]) ?? [],
cumulativeRaw: (r.cumulativeRaw as number[]) ?? [],
medianTransTimes: (r.medianTransTimes as number[]) ?? [],
avgTransTimes: (r.avgTransTimes as number[]) ?? [],
events: (r.events as string[]) ?? [],
dayFunnels: dayFunnels
? {
series: (dayFunnels.series as number[][]) ?? [],
xValues: (dayFunnels.xValues as string[]) ?? [],
}
: null,
}
})

return {
success: true,
output: { funnels },
}
},

outputs: {
funnels: {
type: 'array',
description: 'Funnel results, one entry per segment',
items: {
type: 'object',
properties: {
stepByStep: { type: 'json', description: 'Conversion count at each step' },
cumulative: {
type: 'json',
description: 'Cumulative conversion percentage at each step',
},
cumulativeRaw: { type: 'json', description: 'Cumulative conversion count at each step' },
medianTransTimes: {
type: 'json',
description: 'Median transition time between steps (ms)',
},
avgTransTimes: {
type: 'json',
description: 'Average transition time between steps (ms)',
},
events: { type: 'json', description: 'Event names for each funnel step' },
dayFunnels: {
type: 'json',
description: 'Daily funnel breakdown {series, xValues}',
optional: true,
},
},
},
},
},
}
23 changes: 22 additions & 1 deletion apps/sim/tools/amplitude/get_active_users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
AmplitudeGetActiveUsersParams,
AmplitudeGetActiveUsersResponse,
} from '@/tools/amplitude/types'
import { getDashboardHost } from '@/tools/amplitude/utils'
import type { ToolConfig } from '@/tools/types'

export const getActiveUsersTool: ToolConfig<
Expand Down Expand Up @@ -50,15 +51,35 @@ export const getActiveUsersTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Time interval: 1 (daily), 7 (weekly), or 30 (monthly)',
},
groupBy: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Property name to group by',
},
segment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON segment definition(s) applied to the query',
},
dataResidency: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Data residency region: "us" (default) or "eu"',
},
},

request: {
url: (params) => {
const url = new URL('https://amplitude.com/api/2/users')
const url = new URL(`${getDashboardHost(params.dataResidency)}/api/2/users`)
url.searchParams.set('start', params.start)
url.searchParams.set('end', params.end)
if (params.metric) url.searchParams.set('m', params.metric)
if (params.interval) url.searchParams.set('i', params.interval)
if (params.groupBy) url.searchParams.set('g', params.groupBy)
Comment thread
waleedlatif1 marked this conversation as resolved.
if (params.segment) url.searchParams.set('s', params.segment)
return url.toString()
},
method: 'GET',
Expand Down
Loading
Loading