diff --git a/apps/sim/blocks/blocks/amplitude.ts b/apps/sim/blocks/blocks/amplitude.ts index a05f9ed5a0c..71ac94a19b1 100644 --- a/apps/sim/blocks/blocks/amplitude.ts +++ b/apps/sim/blocks/blocks/amplitude.ts @@ -6,12 +6,12 @@ export const AmplitudeBlock: BlockConfig = { name: 'Amplitude', description: 'Track events and query analytics from Amplitude', longDescription: - 'Integrate Amplitude into your workflow to track events, identify users and groups, search for users, query analytics, and retrieve revenue data.', + 'Integrate Amplitude into your workflow to track events, identify users and groups, search for users, query analytics, analyze funnels and retention, and retrieve revenue data.', docsLink: 'https://docs.sim.ai/integrations/amplitude', category: 'tools', integrationType: IntegrationType.Analytics, - bgColor: '#1B1F3B', - iconColor: '#1F77E0', + bgColor: '#13294B', + iconColor: '#1E61F0', icon: AmplitudeIcon, authMode: AuthMode.ApiKey, @@ -32,6 +32,8 @@ export const AmplitudeBlock: BlockConfig = { { label: 'Real-time Active Users', id: 'realtime_active_users' }, { label: 'List Events', id: 'list_events' }, { label: 'Get Revenue', id: 'get_revenue' }, + { label: 'Funnels', id: 'funnels' }, + { label: 'Retention', id: 'retention' }, ], value: () => 'send_event', }, @@ -70,6 +72,8 @@ export const AmplitudeBlock: BlockConfig = { 'realtime_active_users', 'list_events', 'get_revenue', + 'funnels', + 'retention', ], }, placeholder: 'Enter your Amplitude Secret Key', @@ -85,10 +89,26 @@ export const AmplitudeBlock: BlockConfig = { 'realtime_active_users', 'list_events', 'get_revenue', + 'funnels', + 'retention', ], }, }, + // Data Residency (all operations except User Profile, which is US-only) + { + id: 'dataResidency', + title: 'Data Residency', + type: 'dropdown', + options: [ + { label: 'US (default)', id: 'us' }, + { label: 'EU', id: 'eu' }, + ], + value: () => 'us', + condition: { field: 'operation', value: 'user_profile', not: true }, + mode: 'advanced', + }, + // --- Send Event fields --- { id: 'eventType', @@ -460,6 +480,8 @@ export const AmplitudeBlock: BlockConfig = { title: 'Interval', type: 'dropdown', options: [ + { label: 'Real-time', id: '-300000' }, + { label: 'Hourly', id: '-3600000' }, { label: 'Daily', id: '1' }, { label: 'Weekly', id: '7' }, { label: 'Monthly', id: '30' }, @@ -476,6 +498,14 @@ export const AmplitudeBlock: BlockConfig = { condition: { field: 'operation', value: 'event_segmentation' }, mode: 'advanced', }, + { + id: 'segmentationGroupBy2', + title: 'Group By (2nd Property)', + type: 'short-input', + placeholder: 'Second property name (prefix custom with "gp:")', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + }, { id: 'segmentationLimit', title: 'Limit', @@ -484,6 +514,46 @@ export const AmplitudeBlock: BlockConfig = { condition: { field: 'operation', value: 'event_segmentation' }, mode: 'advanced', }, + { + id: 'segmentationFilters', + title: 'Filters', + type: 'long-input', + placeholder: + '[{"subprop_type":"event","subprop_key":"city","subprop_op":"is","subprop_value":["San Francisco"]}]', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of Amplitude event segmentation filter objects, each with subprop_type ("event" or "user"), subprop_key, subprop_op (e.g. "is", "is not", "contains"), and subprop_value (array of strings). Return ONLY the JSON array - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'segmentationFormula', + title: 'Formula', + type: 'short-input', + placeholder: 'e.g., UNIQUES(A)/UNIQUES(B) — required when Metric is Formula', + condition: { + field: 'operation', + value: 'event_segmentation', + and: { field: 'segmentationMetric', value: 'formula' }, + }, + required: { + field: 'operation', + value: 'event_segmentation', + and: { field: 'segmentationMetric', value: 'formula' }, + }, + mode: 'advanced', + }, + { + id: 'segmentationSegment', + title: 'Segment Definition', + type: 'long-input', + placeholder: 'JSON segment definition(s)', + condition: { field: 'operation', value: 'event_segmentation' }, + mode: 'advanced', + }, // --- Get Active Users fields --- { @@ -539,6 +609,22 @@ export const AmplitudeBlock: BlockConfig = { condition: { field: 'operation', value: 'get_active_users' }, mode: 'advanced', }, + { + id: 'activeUsersGroupBy', + title: 'Group By', + type: 'short-input', + placeholder: 'Property name', + condition: { field: 'operation', value: 'get_active_users' }, + mode: 'advanced', + }, + { + id: 'activeUsersSegment', + title: 'Segment Definition', + type: 'long-input', + placeholder: 'JSON segment definition(s)', + condition: { field: 'operation', value: 'get_active_users' }, + mode: 'advanced', + }, // --- Get Revenue fields --- { @@ -596,6 +682,243 @@ export const AmplitudeBlock: BlockConfig = { condition: { field: 'operation', value: 'get_revenue' }, mode: 'advanced', }, + { + id: 'revenueGroupBy', + title: 'Group By', + type: 'short-input', + placeholder: 'Property name (limit: one)', + condition: { field: 'operation', value: 'get_revenue' }, + mode: 'advanced', + }, + { + id: 'revenueSegment', + title: 'Segment Definition', + type: 'long-input', + placeholder: 'JSON segment definition(s)', + condition: { field: 'operation', value: 'get_revenue' }, + mode: 'advanced', + }, + + // --- Funnels fields --- + { + id: 'funnelEvents', + title: 'Funnel Steps', + type: 'long-input', + required: { field: 'operation', value: 'funnels' }, + placeholder: '[{"event_type":"signup"},{"event_type":"purchase"}]', + condition: { field: 'operation', value: 'funnels' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a JSON array of Amplitude event objects, one per funnel step in order, each with an "event_type" key. Return ONLY the JSON array - no explanations, no extra text.', + generationType: 'json-object', + }, + }, + { + id: 'funnelStart', + title: 'Start Date', + type: 'short-input', + required: { field: 'operation', value: 'funnels' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'funnels' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'funnelEnd', + title: 'End Date', + type: 'short-input', + required: { field: 'operation', value: 'funnels' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'funnels' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'funnelMode', + title: 'Funnel Mode', + type: 'dropdown', + options: [ + { label: 'Ordered', id: 'ordered' }, + { label: 'Unordered', id: 'unordered' }, + { label: 'Sequential', id: 'sequential' }, + ], + value: () => 'ordered', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + { + id: 'funnelUserType', + title: 'User Type', + type: 'dropdown', + options: [ + { label: 'Active', id: 'active' }, + { label: 'New', id: 'new' }, + ], + value: () => 'active', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + { + id: 'funnelInterval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: 'Real-time', id: '-300000' }, + { label: 'Hourly', id: '-3600000' }, + { label: 'Daily', id: '1' }, + { label: 'Weekly', id: '7' }, + { label: 'Monthly', id: '30' }, + ], + value: () => '1', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + { + id: 'funnelConversionWindowSeconds', + title: 'Conversion Window (seconds)', + type: 'short-input', + placeholder: '2592000 (30 days)', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + { + id: 'funnelGroupBy', + title: 'Group By', + type: 'short-input', + placeholder: 'Property name (limit: one)', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + { + id: 'funnelLimit', + title: 'Limit', + type: 'short-input', + placeholder: 'Max group-by values (max 1000)', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + { + id: 'funnelSegment', + title: 'Segment Definition', + type: 'long-input', + placeholder: 'JSON segment definition(s)', + condition: { field: 'operation', value: 'funnels' }, + mode: 'advanced', + }, + + // --- Retention fields --- + { + id: 'retentionStartEvent', + title: 'Starting Event', + type: 'short-input', + required: { field: 'operation', value: 'retention' }, + placeholder: '{"event_type":"_new"}', + condition: { field: 'operation', value: 'retention' }, + }, + { + id: 'retentionReturnEvent', + title: 'Returning Event', + type: 'short-input', + required: { field: 'operation', value: 'retention' }, + placeholder: '{"event_type":"_all"}', + condition: { field: 'operation', value: 'retention' }, + }, + { + id: 'retentionStart', + title: 'Start Date', + type: 'short-input', + required: { field: 'operation', value: 'retention' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'retention' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'retentionEnd', + title: 'End Date', + type: 'short-input', + required: { field: 'operation', value: 'retention' }, + placeholder: 'YYYYMMDD', + condition: { field: 'operation', value: 'retention' }, + wandConfig: { + enabled: true, + prompt: + 'Generate a date in YYYYMMDD format. Return ONLY the date string - no explanations, no extra text.', + generationType: 'timestamp', + }, + }, + { + id: 'retentionMode', + title: 'Retention Mode', + type: 'dropdown', + options: [ + { label: 'N-Day', id: 'n-day' }, + { label: 'Rolling', id: 'rolling' }, + { label: 'Bracket', id: 'bracket' }, + ], + value: () => 'n-day', + condition: { field: 'operation', value: 'retention' }, + mode: 'advanced', + }, + { + id: 'retentionBrackets', + title: 'Retention Brackets', + type: 'short-input', + placeholder: '[[0,4]] — required when Retention Mode is Bracket', + condition: { + field: 'operation', + value: 'retention', + and: { field: 'retentionMode', value: 'bracket' }, + }, + required: { + field: 'operation', + value: 'retention', + and: { field: 'retentionMode', value: 'bracket' }, + }, + mode: 'advanced', + }, + { + id: 'retentionInterval', + title: 'Interval', + type: 'dropdown', + options: [ + { label: 'Daily', id: '1' }, + { label: 'Weekly', id: '7' }, + { label: 'Monthly', id: '30' }, + ], + value: () => '1', + condition: { field: 'operation', value: 'retention' }, + mode: 'advanced', + }, + { + id: 'retentionGroupBy', + title: 'Group By', + type: 'short-input', + placeholder: 'Property name (limit: one)', + condition: { field: 'operation', value: 'retention' }, + mode: 'advanced', + }, + { + id: 'retentionSegment', + title: 'Segment Definition', + type: 'long-input', + placeholder: 'JSON segment definition(s)', + condition: { field: 'operation', value: 'retention' }, + mode: 'advanced', + }, ], tools: { @@ -611,6 +934,8 @@ export const AmplitudeBlock: BlockConfig = { 'amplitude_realtime_active_users', 'amplitude_list_events', 'amplitude_get_revenue', + 'amplitude_funnels', + 'amplitude_retention', ], config: { tool: (params) => `amplitude_${params.operation}`, @@ -649,7 +974,11 @@ export const AmplitudeBlock: BlockConfig = { if (params.segmentationMetric) result.metric = params.segmentationMetric if (params.segmentationInterval) result.interval = params.segmentationInterval if (params.segmentationGroupBy) result.groupBy = params.segmentationGroupBy + if (params.segmentationGroupBy2) result.groupBy2 = params.segmentationGroupBy2 if (params.segmentationLimit) result.limit = params.segmentationLimit + if (params.segmentationFilters) result.filters = params.segmentationFilters + if (params.segmentationFormula) result.formula = params.segmentationFormula + if (params.segmentationSegment) result.segment = params.segmentationSegment break case 'get_active_users': @@ -657,6 +986,8 @@ export const AmplitudeBlock: BlockConfig = { if (params.activeUsersEnd) result.end = params.activeUsersEnd if (params.activeUsersMetric) result.metric = params.activeUsersMetric if (params.activeUsersInterval) result.interval = params.activeUsersInterval + if (params.activeUsersGroupBy) result.groupBy = params.activeUsersGroupBy + if (params.activeUsersSegment) result.segment = params.activeUsersSegment break case 'get_revenue': @@ -664,6 +995,34 @@ export const AmplitudeBlock: BlockConfig = { if (params.revenueEnd) result.end = params.revenueEnd if (params.revenueMetric) result.metric = params.revenueMetric if (params.revenueInterval) result.interval = params.revenueInterval + if (params.revenueGroupBy) result.groupBy = params.revenueGroupBy + if (params.revenueSegment) result.segment = params.revenueSegment + break + + case 'funnels': + if (params.funnelEvents) result.events = params.funnelEvents + if (params.funnelStart) result.start = params.funnelStart + if (params.funnelEnd) result.end = params.funnelEnd + if (params.funnelMode) result.mode = params.funnelMode + if (params.funnelUserType) result.userType = params.funnelUserType + if (params.funnelInterval) result.interval = params.funnelInterval + if (params.funnelConversionWindowSeconds) + result.conversionWindowSeconds = params.funnelConversionWindowSeconds + if (params.funnelGroupBy) result.groupBy = params.funnelGroupBy + if (params.funnelLimit) result.limit = params.funnelLimit + if (params.funnelSegment) result.segment = params.funnelSegment + break + + case 'retention': + if (params.retentionStartEvent) result.startEvent = params.retentionStartEvent + if (params.retentionReturnEvent) result.returnEvent = params.retentionReturnEvent + if (params.retentionStart) result.start = params.retentionStart + if (params.retentionEnd) result.end = params.retentionEnd + if (params.retentionMode) result.retentionMode = params.retentionMode + if (params.retentionBrackets) result.retentionBrackets = params.retentionBrackets + if (params.retentionInterval) result.interval = params.retentionInterval + if (params.retentionGroupBy) result.groupBy = params.retentionGroupBy + if (params.retentionSegment) result.segment = params.retentionSegment break } @@ -696,6 +1055,32 @@ export const AmplitudeBlock: BlockConfig = { activeUsersEnd: { type: 'string', description: 'Active users end date' }, revenueStart: { type: 'string', description: 'Revenue start date' }, revenueEnd: { type: 'string', description: 'Revenue end date' }, + dataResidency: { type: 'string', description: 'Data residency region: "us" or "eu"' }, + segmentationFilters: { type: 'string', description: 'Event segmentation filters JSON' }, + segmentationFormula: { type: 'string', description: 'Event segmentation formula expression' }, + segmentationGroupBy2: { + type: 'string', + description: 'Event segmentation second group-by property', + }, + segmentationSegment: { + type: 'string', + description: 'Event segmentation segment definition JSON', + }, + activeUsersGroupBy: { type: 'string', description: 'Active users group-by property' }, + activeUsersSegment: { type: 'string', description: 'Active users segment definition JSON' }, + revenueGroupBy: { type: 'string', description: 'Revenue group-by property' }, + revenueSegment: { type: 'string', description: 'Revenue segment definition JSON' }, + funnelEvents: { type: 'string', description: 'Funnel step event objects JSON array' }, + funnelStart: { type: 'string', description: 'Funnel analysis start date' }, + funnelEnd: { type: 'string', description: 'Funnel analysis end date' }, + funnelGroupBy: { type: 'string', description: 'Funnel group-by property' }, + funnelSegment: { type: 'string', description: 'Funnel segment definition JSON' }, + retentionStartEvent: { type: 'string', description: 'Retention starting event JSON object' }, + retentionReturnEvent: { type: 'string', description: 'Retention returning event JSON object' }, + retentionStart: { type: 'string', description: 'Retention analysis start date' }, + retentionEnd: { type: 'string', description: 'Retention analysis end date' }, + retentionGroupBy: { type: 'string', description: 'Retention group-by property' }, + retentionSegment: { type: 'string', description: 'Retention segment definition JSON' }, }, outputs: { @@ -711,10 +1096,43 @@ export const AmplitudeBlock: BlockConfig = { type: 'number', description: 'Number of events ingested (send_event)', }, + payloadSizeBytes: { + type: 'number', + description: 'Size of the ingested payload in bytes (send_event)', + }, + serverUploadTime: { + type: 'number', + description: 'Server-side upload timestamp (send_event)', + }, matches: { type: 'json', description: 'User search matches (amplitudeId, userId)', }, + type: { + type: 'string', + description: 'Match type, e.g. match_user_or_device_id (user_search)', + }, + userId: { + type: 'string', + description: 'External user ID (user_profile)', + }, + deviceId: { + type: 'string', + description: 'Device ID (user_profile)', + }, + ampProps: { + type: 'json', + description: + 'Amplitude user properties (library, first_used, last_used, custom) (user_profile)', + }, + cohortIds: { + type: 'json', + description: 'Cohort IDs the user belongs to (user_profile)', + }, + computations: { + type: 'json', + description: 'Computed user properties (user_profile)', + }, events: { type: 'json', description: 'Event list (list_events, user_activity)', @@ -725,7 +1143,8 @@ export const AmplitudeBlock: BlockConfig = { }, series: { type: 'json', - description: 'Time-series data (segmentation, active_users, revenue, realtime)', + description: + 'Time-series data (segmentation, active_users, realtime: number[][]; revenue: [{dates, values}]; retention: [{dates, values, combined}])', }, seriesLabels: { type: 'json', @@ -733,7 +1152,7 @@ export const AmplitudeBlock: BlockConfig = { }, seriesMeta: { type: 'json', - description: 'Metadata labels for data series (active_users)', + description: 'Metadata labels for data series (active_users, retention)', }, seriesCollapsed: { type: 'json', @@ -741,7 +1160,12 @@ export const AmplitudeBlock: BlockConfig = { }, xValues: { type: 'json', - description: 'X-axis date/time values for chart data', + description: 'X-axis date/time values for chart data (segmentation, active_users, realtime)', + }, + funnels: { + type: 'json', + description: + 'Funnel results per segment (stepByStep, cumulative, medianTransTimes, dayFunnels, etc.) (funnels)', }, }, } @@ -849,5 +1273,12 @@ export const AmplitudeBlockMeta = { content: '# Lookup User Activity\n\nInvestigate a single user in Amplitude for support or debugging.\n\n## Steps\n1. Search for the user by user ID, device ID, or Amplitude ID to resolve their Amplitude ID.\n2. Pull the user activity stream for that Amplitude ID, ordered latest first.\n3. Optionally fetch the user profile to see their current properties.\n\n## Output\nA timeline of the user recent events plus key profile properties. Note the time range covered.', }, + { + name: 'analyze-conversion-funnel', + description: + 'Run an Amplitude funnel across a sequence of events to find conversion rates and drop-off points.', + content: + '# Analyze Conversion Funnel\n\nMeasure how users progress through a multi-step flow in Amplitude.\n\n## Steps\n1. Define the ordered sequence of events that make up the funnel (e.g., signup, activation, purchase).\n2. Pick the date range and, if useful, a property to group by.\n3. Run the funnel query and read the step-by-step and cumulative conversion numbers.\n\n## Output\nConversion counts and rates at each step, the biggest drop-off point, and any group-by breakdown.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/tools/amplitude/event_segmentation.ts b/apps/sim/tools/amplitude/event_segmentation.ts index d5cbab3357a..394adf07ccf 100644 --- a/apps/sim/tools/amplitude/event_segmentation.ts +++ b/apps/sim/tools/amplitude/event_segmentation.ts @@ -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< @@ -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 = { 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' + ) + } + 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) + if (params.segment) url.searchParams.set('s', params.segment) return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/amplitude/funnels.ts b/apps/sim/tools/amplitude/funnels.ts new file mode 100644 index 00000000000..7eed8cdc883 --- /dev/null +++ b/apps/sim/tools/amplitude/funnels.ts @@ -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 = { + 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') + } + const isPlainObject = (value: unknown): value is Record => + 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' + ) + } + for (const step of parsed) { + url.searchParams.append('e', JSON.stringify(step)) + } + 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> + + const funnels = results.map((r) => { + const dayFunnels = r.dayFunnels as Record | 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, + }, + }, + }, + }, + }, +} diff --git a/apps/sim/tools/amplitude/get_active_users.ts b/apps/sim/tools/amplitude/get_active_users.ts index 6670e5ab150..a935d9919be 100644 --- a/apps/sim/tools/amplitude/get_active_users.ts +++ b/apps/sim/tools/amplitude/get_active_users.ts @@ -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< @@ -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) + if (params.segment) url.searchParams.set('s', params.segment) return url.toString() }, method: 'GET', diff --git a/apps/sim/tools/amplitude/get_revenue.ts b/apps/sim/tools/amplitude/get_revenue.ts index 264eb1e0f95..5f1861bdd86 100644 --- a/apps/sim/tools/amplitude/get_revenue.ts +++ b/apps/sim/tools/amplitude/get_revenue.ts @@ -2,6 +2,7 @@ import type { AmplitudeGetRevenueParams, AmplitudeGetRevenueResponse, } from '@/tools/amplitude/types' +import { getDashboardHost } from '@/tools/amplitude/utils' import type { ToolConfig } from '@/tools/types' export const getRevenueTool: ToolConfig = { @@ -47,15 +48,35 @@ export const getRevenueTool: ToolConfig { - const url = new URL('https://amplitude.com/api/2/revenue/ltv') + const url = new URL(`${getDashboardHost(params.dataResidency)}/api/2/revenue/ltv`) 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.segment) url.searchParams.set('s', params.segment) return url.toString() }, method: 'GET', @@ -78,25 +99,35 @@ export const getRevenueTool: ToolConfig: {r1d..r90d, count, paid, total_amount}}}]', + items: { + type: 'json', + properties: { + dates: { + type: 'array', + description: 'Dates covered by this series', + items: { type: 'string' }, + }, + values: { + type: 'json', + description: + 'Per-date metric values keyed by date (r1d..r90d, count, paid, total_amount)', + }, + }, + }, }, seriesLabels: { type: 'array', description: 'Labels for each data series', items: { type: 'string' }, }, - xValues: { - type: 'array', - description: 'Date values for the x-axis', - items: { type: 'string' }, - }, }, } diff --git a/apps/sim/tools/amplitude/group_identify.ts b/apps/sim/tools/amplitude/group_identify.ts index b0bd548c49d..6cdefbb320f 100644 --- a/apps/sim/tools/amplitude/group_identify.ts +++ b/apps/sim/tools/amplitude/group_identify.ts @@ -2,6 +2,7 @@ import type { AmplitudeGroupIdentifyParams, AmplitudeGroupIdentifyResponse, } from '@/tools/amplitude/types' +import { getIngestionHost } from '@/tools/amplitude/utils' import type { ToolConfig } from '@/tools/types' export const groupIdentifyTool: ToolConfig< @@ -40,13 +41,19 @@ export const groupIdentifyTool: ToolConfig< description: 'JSON object of group properties. Use operations like $set, $setOnce, $add, $append, $unset.', }, + dataResidency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Data residency region: "us" (default) or "eu"', + }, }, request: { - url: 'https://api2.amplitude.com/groupidentify', + url: (params) => `${getIngestionHost(params.dataResidency)}/groupidentify`, method: 'POST', headers: () => ({ - 'Content-Type': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', }), body: (params) => { let groupProperties: Record = {} @@ -56,16 +63,20 @@ export const groupIdentifyTool: ToolConfig< groupProperties = {} } - return { + const identification = [ + { + group_type: params.groupType, + group_value: params.groupValue, + group_properties: groupProperties, + }, + ] + + const body = new URLSearchParams({ api_key: params.apiKey, - identification: [ - { - group_type: params.groupType, - group_value: params.groupValue, - group_properties: groupProperties, - }, - ], - } + identification: JSON.stringify(identification), + }) + + return body.toString() }, }, diff --git a/apps/sim/tools/amplitude/identify_user.ts b/apps/sim/tools/amplitude/identify_user.ts index a0cb0316805..754316dbea1 100644 --- a/apps/sim/tools/amplitude/identify_user.ts +++ b/apps/sim/tools/amplitude/identify_user.ts @@ -2,6 +2,7 @@ import type { AmplitudeIdentifyUserParams, AmplitudeIdentifyUserResponse, } from '@/tools/amplitude/types' +import { getIngestionHost } from '@/tools/amplitude/utils' import type { ToolConfig } from '@/tools/types' export const identifyUserTool: ToolConfig< @@ -40,13 +41,19 @@ export const identifyUserTool: ToolConfig< description: 'JSON object of user properties. Use operations like $set, $setOnce, $add, $append, $unset.', }, + dataResidency: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Data residency region: "us" (default) or "eu"', + }, }, request: { - url: 'https://api2.amplitude.com/identify', + url: (params) => `${getIngestionHost(params.dataResidency)}/identify`, method: 'POST', headers: () => ({ - 'Content-Type': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', }), body: (params) => { const identification: Record = {} @@ -60,10 +67,12 @@ export const identifyUserTool: ToolConfig< identification.user_properties = {} } - return { + const body = new URLSearchParams({ api_key: params.apiKey, - identification: [identification], - } + identification: JSON.stringify([identification]), + }) + + return body.toString() }, }, diff --git a/apps/sim/tools/amplitude/index.ts b/apps/sim/tools/amplitude/index.ts index b0f1aab792f..6aca5c1ff9f 100644 --- a/apps/sim/tools/amplitude/index.ts +++ b/apps/sim/tools/amplitude/index.ts @@ -1,10 +1,12 @@ import { eventSegmentationTool } from '@/tools/amplitude/event_segmentation' +import { funnelsTool } from '@/tools/amplitude/funnels' import { getActiveUsersTool } from '@/tools/amplitude/get_active_users' import { getRevenueTool } from '@/tools/amplitude/get_revenue' import { groupIdentifyTool } from '@/tools/amplitude/group_identify' import { identifyUserTool } from '@/tools/amplitude/identify_user' import { listEventsTool } from '@/tools/amplitude/list_events' import { realtimeActiveUsersTool } from '@/tools/amplitude/realtime_active_users' +import { retentionTool } from '@/tools/amplitude/retention' import { sendEventTool } from '@/tools/amplitude/send_event' import { userActivityTool } from '@/tools/amplitude/user_activity' import { userProfileTool } from '@/tools/amplitude/user_profile' @@ -21,3 +23,5 @@ export const amplitudeGetActiveUsersTool = getActiveUsersTool export const amplitudeRealtimeActiveUsersTool = realtimeActiveUsersTool export const amplitudeListEventsTool = listEventsTool export const amplitudeGetRevenueTool = getRevenueTool +export const amplitudeFunnelsTool = funnelsTool +export const amplitudeRetentionTool = retentionTool diff --git a/apps/sim/tools/amplitude/list_events.ts b/apps/sim/tools/amplitude/list_events.ts index fa89d3ddf11..a1685bf0ee6 100644 --- a/apps/sim/tools/amplitude/list_events.ts +++ b/apps/sim/tools/amplitude/list_events.ts @@ -2,6 +2,7 @@ import type { AmplitudeListEventsParams, AmplitudeListEventsResponse, } from '@/tools/amplitude/types' +import { getDashboardHost } from '@/tools/amplitude/utils' import type { ToolConfig } from '@/tools/types' export const listEventsTool: ToolConfig = { @@ -24,10 +25,16 @@ export const listEventsTool: ToolConfig `${getDashboardHost(params.dataResidency)}/api/2/events/list`, method: 'GET', headers: (params) => ({ Authorization: `Basic ${btoa(`${params.apiKey}:${params.secretKey}`)}`, @@ -49,6 +56,8 @@ export const listEventsTool: ToolConfig