diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 9e68974089..f8ffe44ffc 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -3464,6 +3464,27 @@ export const ResendIcon = (props: SVGProps) => ( ) +export const GoogleAdsIcon = (props: SVGProps) => ( + + + + + + + + + +) + export const GoogleBigQueryIcon = (props: SVGProps) => ( > @@ -192,6 +193,7 @@ export const blockTypeToIconMap: Record = { gitlab: GitLabIcon, gmail_v2: GmailIcon, gong: GongIcon, + google_ads: GoogleAdsIcon, google_bigquery: GoogleBigQueryIcon, google_books: GoogleBooksIcon, google_calendar_v2: GoogleCalendarIcon, diff --git a/apps/docs/content/docs/en/tools/google_ads.mdx b/apps/docs/content/docs/en/tools/google_ads.mdx new file mode 100644 index 0000000000..cadc9af6fe --- /dev/null +++ b/apps/docs/content/docs/en/tools/google_ads.mdx @@ -0,0 +1,192 @@ +--- +title: Google Ads +description: Query campaigns, ad groups, and performance metrics +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +{/* MANUAL-CONTENT-START:intro */} +[Google Ads](https://ads.google.com) is Google's online advertising platform that lets businesses create ads to reach customers across Google Search, YouTube, Gmail, and millions of partner websites. It supports campaign types including Search, Display, Video, Shopping, and Performance Max, with detailed targeting, bidding strategies, and performance analytics. + +In Sim, the Google Ads integration enables your agents to query campaign data, monitor ad group performance, and pull detailed metrics using the Google Ads Query Language (GAQL). This supports use cases such as automated performance reporting, budget monitoring, campaign health checks, and data-driven optimization workflows. By connecting Sim with Google Ads, your agents can retrieve real-time advertising data and act on insights without manual dashboard navigation. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Connect to Google Ads to list accessible accounts, list campaigns, view ad group details, get performance metrics, and run custom GAQL queries. + + + +## Tools + +### `google_ads_list_customers` + +List all Google Ads customer accounts accessible by the authenticated user + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `developerToken` | string | Yes | Google Ads API developer token | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `customerIds` | array | List of accessible customer IDs | +| `totalCount` | number | Total number of accessible customer accounts | + +### `google_ads_search` + +Run a custom Google Ads Query Language (GAQL) query + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) | +| `developerToken` | string | Yes | Google Ads API developer token | +| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) | +| `query` | string | Yes | GAQL query to execute | +| `pageToken` | string | No | Page token for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `results` | json | Array of result objects from the GAQL query | +| `totalResultsCount` | number | Total number of matching results | +| `nextPageToken` | string | Token for the next page of results | + +### `google_ads_list_campaigns` + +List campaigns in a Google Ads account with optional status filtering + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) | +| `developerToken` | string | Yes | Google Ads API developer token | +| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) | +| `status` | string | No | Filter by campaign status \(ENABLED, PAUSED, REMOVED\) | +| `limit` | number | No | Maximum number of campaigns to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `campaigns` | array | List of campaigns in the account | +| ↳ `id` | string | Campaign ID | +| ↳ `name` | string | Campaign name | +| ↳ `status` | string | Campaign status \(ENABLED, PAUSED, REMOVED\) | +| ↳ `channelType` | string | Advertising channel type \(SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX\) | +| ↳ `startDate` | string | Campaign start date \(YYYY-MM-DD\) | +| ↳ `endDate` | string | Campaign end date \(YYYY-MM-DD\) | +| ↳ `budgetAmountMicros` | string | Daily budget in micros \(divide by 1,000,000 for currency value\) | +| `totalCount` | number | Total number of campaigns returned | + +### `google_ads_campaign_performance` + +Get performance metrics for Google Ads campaigns over a date range + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) | +| `developerToken` | string | Yes | Google Ads API developer token | +| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) | +| `campaignId` | string | No | Filter by specific campaign ID | +| `dateRange` | string | No | Predefined date range \(LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY\) | +| `startDate` | string | No | Custom start date in YYYY-MM-DD format | +| `endDate` | string | No | Custom end date in YYYY-MM-DD format | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `campaigns` | array | Campaign performance data broken down by date | +| ↳ `id` | string | Campaign ID | +| ↳ `name` | string | Campaign name | +| ↳ `status` | string | Campaign status | +| ↳ `impressions` | string | Number of impressions | +| ↳ `clicks` | string | Number of clicks | +| ↳ `costMicros` | string | Cost in micros \(divide by 1,000,000 for currency value\) | +| ↳ `ctr` | number | Click-through rate \(0.0 to 1.0\) | +| ↳ `conversions` | number | Number of conversions | +| ↳ `date` | string | Date for this row \(YYYY-MM-DD\) | +| `totalCount` | number | Total number of result rows | + +### `google_ads_list_ad_groups` + +List ad groups in a Google Ads campaign + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) | +| `developerToken` | string | Yes | Google Ads API developer token | +| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) | +| `campaignId` | string | Yes | Campaign ID to list ad groups for | +| `status` | string | No | Filter by ad group status \(ENABLED, PAUSED, REMOVED\) | +| `limit` | number | No | Maximum number of ad groups to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `adGroups` | array | List of ad groups in the campaign | +| ↳ `id` | string | Ad group ID | +| ↳ `name` | string | Ad group name | +| ↳ `status` | string | Ad group status \(ENABLED, PAUSED, REMOVED\) | +| ↳ `type` | string | Ad group type \(SEARCH_STANDARD, DISPLAY_STANDARD, SHOPPING_PRODUCT_ADS\) | +| ↳ `campaignId` | string | Parent campaign ID | +| ↳ `campaignName` | string | Parent campaign name | +| `totalCount` | number | Total number of ad groups returned | + +### `google_ads_ad_performance` + +Get performance metrics for individual ads over a date range + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `customerId` | string | Yes | Google Ads customer ID \(numeric, no dashes\) | +| `developerToken` | string | Yes | Google Ads API developer token | +| `managerCustomerId` | string | No | Manager account customer ID \(if accessing via manager account\) | +| `campaignId` | string | No | Filter by campaign ID | +| `adGroupId` | string | No | Filter by ad group ID | +| `dateRange` | string | No | Predefined date range \(LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY\) | +| `startDate` | string | No | Custom start date in YYYY-MM-DD format | +| `endDate` | string | No | Custom end date in YYYY-MM-DD format | +| `limit` | number | No | Maximum number of results to return | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ads` | array | Ad performance data broken down by date | +| ↳ `adId` | string | Ad ID | +| ↳ `adGroupId` | string | Parent ad group ID | +| ↳ `adGroupName` | string | Parent ad group name | +| ↳ `campaignId` | string | Parent campaign ID | +| ↳ `campaignName` | string | Parent campaign name | +| ↳ `adType` | string | Ad type \(RESPONSIVE_SEARCH_AD, EXPANDED_TEXT_AD, etc.\) | +| ↳ `impressions` | string | Number of impressions | +| ↳ `clicks` | string | Number of clicks | +| ↳ `costMicros` | string | Cost in micros \(divide by 1,000,000 for currency value\) | +| ↳ `ctr` | number | Click-through rate \(0.0 to 1.0\) | +| ↳ `conversions` | number | Number of conversions | +| ↳ `date` | string | Date for this row \(YYYY-MM-DD\) | +| `totalCount` | number | Total number of result rows | + + diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index a089247e2e..c7d262ee9c 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -38,6 +38,7 @@ "gitlab", "gmail", "gong", + "google_ads", "google_bigquery", "google_books", "google_calendar", @@ -150,4 +151,4 @@ "zep", "zoom" ] -} +} \ No newline at end of file diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index c8146a2801..8c2599e231 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -45,6 +45,7 @@ const SCOPE_DESCRIPTIONS: Record = { 'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info', 'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms', 'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms', + 'https://www.googleapis.com/auth/adwords': 'Manage Google Ads campaigns and reporting', 'https://www.googleapis.com/auth/bigquery': 'View and manage data in Google BigQuery', 'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery', 'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage', diff --git a/apps/sim/blocks/blocks/google_ads.ts b/apps/sim/blocks/blocks/google_ads.ts new file mode 100644 index 0000000000..6e4539ad45 --- /dev/null +++ b/apps/sim/blocks/blocks/google_ads.ts @@ -0,0 +1,293 @@ +import { GoogleAdsIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const GoogleAdsBlock: BlockConfig = { + type: 'google_ads', + name: 'Google Ads', + description: 'Query campaigns, ad groups, and performance metrics', + longDescription: + 'Connect to Google Ads to list accessible accounts, list campaigns, view ad group details, get performance metrics, and run custom GAQL queries.', + docsLink: 'https://docs.sim.ai/tools/google_ads', + category: 'tools', + bgColor: '#E0E0E0', + icon: GoogleAdsIcon, + authMode: AuthMode.OAuth, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'List Customers', id: 'list_customers' }, + { label: 'List Campaigns', id: 'list_campaigns' }, + { label: 'Campaign Performance', id: 'campaign_performance' }, + { label: 'List Ad Groups', id: 'list_ad_groups' }, + { label: 'Ad Performance', id: 'ad_performance' }, + { label: 'Custom Query (GAQL)', id: 'search' }, + ], + value: () => 'list_campaigns', + }, + + { + id: 'credential', + title: 'Google Ads Account', + type: 'oauth-input', + canonicalParamId: 'oauthCredential', + mode: 'basic', + required: true, + serviceId: 'google-ads', + requiredScopes: ['https://www.googleapis.com/auth/adwords'], + placeholder: 'Select Google Ads account', + }, + { + id: 'manualCredential', + title: 'Google Ads Account', + type: 'short-input', + canonicalParamId: 'oauthCredential', + mode: 'advanced', + placeholder: 'Enter credential ID', + required: true, + }, + + { + id: 'developerToken', + title: 'Developer Token', + type: 'short-input', + placeholder: 'Enter your Google Ads API developer token', + required: true, + password: true, + }, + + { + id: 'customerId', + title: 'Customer ID', + type: 'short-input', + placeholder: 'Google Ads customer ID (no dashes)', + condition: { + field: 'operation', + value: 'list_customers', + not: true, + }, + required: { + field: 'operation', + value: 'list_customers', + not: true, + }, + }, + + { + id: 'managerCustomerId', + title: 'Manager Customer ID', + type: 'short-input', + placeholder: 'Manager account ID (optional)', + mode: 'advanced', + condition: { + field: 'operation', + value: 'list_customers', + not: true, + }, + }, + + { + id: 'query', + title: 'GAQL Query', + type: 'long-input', + placeholder: + "SELECT campaign.id, campaign.name, metrics.impressions FROM campaign WHERE campaign.status = 'ENABLED'", + condition: { field: 'operation', value: 'search' }, + required: { field: 'operation', value: 'search' }, + wandConfig: { + enabled: true, + prompt: `Generate a Google Ads Query Language (GAQL) query based on the user's description. +The query should: +- Use valid GAQL syntax +- Include relevant metrics when asking about performance +- Include segments.date with a date range when using metrics +- Be efficient and well-formatted + +Common resources: campaign, ad_group, ad_group_ad, keyword_view, search_term_view +Common metrics: metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions +Date ranges: LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, YESTERDAY + +Examples: +- "active campaigns" -> SELECT campaign.id, campaign.name, campaign.status FROM campaign WHERE campaign.status = 'ENABLED' +- "campaign spend last week" -> SELECT campaign.name, metrics.cost_micros, segments.date FROM campaign WHERE segments.date DURING LAST_7_DAYS AND campaign.status != 'REMOVED' + +Return ONLY the GAQL query - no explanations, no quotes, no extra text.`, + placeholder: 'Describe the query you want to run...', + }, + }, + + { + id: 'campaignId', + title: 'Campaign ID', + type: 'short-input', + placeholder: 'Campaign ID to filter by', + condition: { + field: 'operation', + value: ['campaign_performance', 'list_ad_groups', 'ad_performance'], + }, + required: { field: 'operation', value: 'list_ad_groups' }, + }, + + { + id: 'adGroupId', + title: 'Ad Group ID', + type: 'short-input', + placeholder: 'Ad group ID to filter by', + mode: 'advanced', + condition: { field: 'operation', value: 'ad_performance' }, + }, + + { + id: 'status', + title: 'Status Filter', + type: 'dropdown', + options: [ + { label: 'All (except removed)', id: '' }, + { label: 'Enabled', id: 'ENABLED' }, + { label: 'Paused', id: 'PAUSED' }, + ], + mode: 'advanced', + condition: { field: 'operation', value: ['list_campaigns', 'list_ad_groups'] }, + }, + + { + id: 'dateRange', + title: 'Date Range', + type: 'dropdown', + options: [ + { label: 'Last 30 Days', id: 'LAST_30_DAYS' }, + { label: 'Last 7 Days', id: 'LAST_7_DAYS' }, + { label: 'Today', id: 'TODAY' }, + { label: 'Yesterday', id: 'YESTERDAY' }, + { label: 'This Month', id: 'THIS_MONTH' }, + { label: 'Last Month', id: 'LAST_MONTH' }, + { label: 'Custom', id: 'CUSTOM' }, + ], + condition: { field: 'operation', value: ['campaign_performance', 'ad_performance'] }, + value: () => 'LAST_30_DAYS', + }, + + { + id: 'startDate', + title: 'Start Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'dateRange', value: 'CUSTOM' }, + required: { field: 'dateRange', value: 'CUSTOM' }, + }, + + { + id: 'endDate', + title: 'End Date', + type: 'short-input', + placeholder: 'YYYY-MM-DD', + condition: { field: 'dateRange', value: 'CUSTOM' }, + required: { field: 'dateRange', value: 'CUSTOM' }, + }, + + { + id: 'pageToken', + title: 'Page Token', + type: 'short-input', + placeholder: 'Pagination token', + mode: 'advanced', + condition: { field: 'operation', value: 'search' }, + }, + + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Maximum results to return', + mode: 'advanced', + condition: { + field: 'operation', + value: ['list_campaigns', 'list_ad_groups', 'ad_performance'], + }, + }, + ], + tools: { + access: [ + 'google_ads_list_customers', + 'google_ads_search', + 'google_ads_list_campaigns', + 'google_ads_campaign_performance', + 'google_ads_list_ad_groups', + 'google_ads_ad_performance', + ], + config: { + tool: (params) => `google_ads_${params.operation}`, + params: (params) => { + const { oauthCredential, dateRange, limit, ...rest } = params + + const result: Record = { + ...rest, + oauthCredential, + } + + if (dateRange && dateRange !== 'CUSTOM') { + result.dateRange = dateRange + } + + if (limit !== undefined && limit !== '') { + result.limit = Number(limit) + } + + return result + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + oauthCredential: { type: 'string', description: 'Google Ads OAuth credential' }, + developerToken: { type: 'string', description: 'Google Ads API developer token' }, + customerId: { type: 'string', description: 'Google Ads customer ID (numeric, no dashes)' }, + managerCustomerId: { type: 'string', description: 'Manager account customer ID' }, + query: { type: 'string', description: 'GAQL query to execute' }, + campaignId: { type: 'string', description: 'Campaign ID to filter by' }, + adGroupId: { type: 'string', description: 'Ad group ID to filter by' }, + status: { type: 'string', description: 'Status filter (ENABLED, PAUSED)' }, + dateRange: { type: 'string', description: 'Date range for performance queries' }, + startDate: { type: 'string', description: 'Custom start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'Custom end date (YYYY-MM-DD)' }, + pageToken: { type: 'string', description: 'Pagination token' }, + limit: { type: 'number', description: 'Maximum results to return' }, + }, + outputs: { + customerIds: { + type: 'json', + description: 'List of accessible customer IDs (list_customers)', + }, + results: { + type: 'json', + description: 'Query results (search)', + }, + campaigns: { + type: 'json', + description: 'Campaign data (list_campaigns, campaign_performance)', + }, + adGroups: { + type: 'json', + description: 'Ad group data (list_ad_groups)', + }, + ads: { + type: 'json', + description: 'Ad performance data (ad_performance)', + }, + totalCount: { + type: 'number', + description: 'Total number of results', + }, + totalResultsCount: { + type: 'number', + description: 'Total results count (search)', + }, + nextPageToken: { + type: 'string', + description: 'Token for next page of results', + }, + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index eff25ffb1d..358508aede 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -44,6 +44,7 @@ import { GitLabBlock } from '@/blocks/blocks/gitlab' import { GmailBlock, GmailV2Block } from '@/blocks/blocks/gmail' import { GongBlock } from '@/blocks/blocks/gong' import { GoogleSearchBlock } from '@/blocks/blocks/google' +import { GoogleAdsBlock } from '@/blocks/blocks/google_ads' import { GoogleBigQueryBlock } from '@/blocks/blocks/google_bigquery' import { GoogleBooksBlock } from '@/blocks/blocks/google_books' import { GoogleCalendarBlock, GoogleCalendarV2Block } from '@/blocks/blocks/google_calendar' @@ -233,6 +234,7 @@ export const registry: Record = { gmail_v2: GmailV2Block, google_calendar: GoogleCalendarBlock, google_calendar_v2: GoogleCalendarV2Block, + google_ads: GoogleAdsBlock, google_books: GoogleBooksBlock, google_docs: GoogleDocsBlock, google_drive: GoogleDriveBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 9e68974089..f8ffe44ffc 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3464,6 +3464,27 @@ export const ResendIcon = (props: SVGProps) => ( ) +export const GoogleAdsIcon = (props: SVGProps) => ( + + + + + + + + + +) + export const GoogleBigQueryIcon = (props: SVGProps) => ( { + try { + const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { + headers: { Authorization: `Bearer ${tokens.accessToken}` }, + }) + if (!response.ok) { + logger.error('Failed to fetch Google user info', { status: response.status }) + throw new Error(`Failed to fetch Google user info: ${response.statusText}`) + } + const profile = await response.json() + const now = new Date() + return { + id: `${profile.sub}-${crypto.randomUUID()}`, + name: profile.name || 'Google User', + email: profile.email, + image: profile.picture || undefined, + emailVerified: profile.email_verified || false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Google getUserInfo', { error }) + throw error + } + }, + }, { providerId: 'google-bigquery', clientId: env.GOOGLE_CLIENT_ID as string, diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index 2b9a96aca8..0eb7111be4 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -8,6 +8,7 @@ import { DropboxIcon, GithubIcon, GmailIcon, + GoogleAdsIcon, GoogleBigQueryIcon, GoogleCalendarIcon, GoogleDocsIcon, @@ -121,6 +122,14 @@ export const OAUTH_PROVIDERS: Record = { baseProviderIcon: GoogleIcon, scopes: ['https://www.googleapis.com/auth/calendar'], }, + 'google-ads': { + name: 'Google Ads', + description: 'Query campaigns, ad groups, and performance metrics in Google Ads.', + providerId: 'google-ads', + icon: GoogleAdsIcon, + baseProviderIcon: GoogleIcon, + scopes: ['https://www.googleapis.com/auth/adwords'], + }, 'google-bigquery': { name: 'Google BigQuery', description: 'Query, list, and insert data in Google BigQuery.', diff --git a/apps/sim/lib/oauth/types.ts b/apps/sim/lib/oauth/types.ts index 0da86f06fd..725b0f2c36 100644 --- a/apps/sim/lib/oauth/types.ts +++ b/apps/sim/lib/oauth/types.ts @@ -7,6 +7,7 @@ export type OAuthProvider = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-ads' | 'google-bigquery' | 'google-tasks' | 'google-vault' @@ -54,6 +55,7 @@ export type OAuthService = | 'google-docs' | 'google-sheets' | 'google-calendar' + | 'google-ads' | 'google-bigquery' | 'google-tasks' | 'google-vault' diff --git a/apps/sim/tools/google_ads/ad_performance.ts b/apps/sim/tools/google_ads/ad_performance.ts new file mode 100644 index 0000000000..337379298a --- /dev/null +++ b/apps/sim/tools/google_ads/ad_performance.ts @@ -0,0 +1,211 @@ +import type { + GoogleAdsAdPerformanceParams, + GoogleAdsAdPerformanceResponse, +} from '@/tools/google_ads/types' +import { validateDate, validateDateRange, validateNumericId } from '@/tools/google_ads/types' +import type { ToolConfig } from '@/tools/types' + +export const googleAdsAdPerformanceTool: ToolConfig< + GoogleAdsAdPerformanceParams, + GoogleAdsAdPerformanceResponse +> = { + id: 'google_ads_ad_performance', + name: 'Google Ads Ad Performance', + description: 'Get performance metrics for individual ads over a date range', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-ads', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for the Google Ads API', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Ads customer ID (numeric, no dashes)', + }, + developerToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Ads API developer token', + }, + managerCustomerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Manager account customer ID (if accessing via manager account)', + }, + campaignId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by campaign ID', + }, + adGroupId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by ad group ID', + }, + dateRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Predefined date range (LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY)', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom end date in YYYY-MM-DD format', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of results to return', + }, + }, + + request: { + url: (params) => { + const customerId = validateNumericId(params.customerId, 'customerId') + return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'developer-token': params.developerToken, + } + if (params.managerCustomerId) { + headers['login-customer-id'] = validateNumericId( + params.managerCustomerId, + 'managerCustomerId' + ) + } + return headers + }, + body: (params) => { + let query = + 'SELECT ad_group_ad.ad.id, ad_group.id, ad_group.name, campaign.id, campaign.name, ad_group_ad.ad.type, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions, segments.date FROM ad_group_ad' + + const conditions: string[] = ["ad_group_ad.status != 'REMOVED'"] + + if (params.campaignId) { + conditions.push(`campaign.id = ${validateNumericId(params.campaignId, 'campaignId')}`) + } + + if (params.adGroupId) { + conditions.push(`ad_group.id = ${validateNumericId(params.adGroupId, 'adGroupId')}`) + } + + if (params.startDate && params.endDate) { + const start = validateDate(params.startDate, 'startDate') + const end = validateDate(params.endDate, 'endDate') + conditions.push(`segments.date BETWEEN '${start}' AND '${end}'`) + } else { + const dateRange = validateDateRange(params.dateRange || 'LAST_30_DAYS') + conditions.push(`segments.date DURING ${dateRange}`) + } + + query += ` WHERE ${conditions.join(' AND ')}` + query += ' ORDER BY metrics.impressions DESC' + + if (params.limit) { + query += ` LIMIT ${params.limit}` + } + + return { query } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const errorMessage = + data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error' + return { + success: false, + output: { ads: [], totalCount: 0 }, + error: errorMessage, + } + } + + const results = data.results ?? [] + const ads = results.map((r: Record) => ({ + adId: r.adGroupAd?.ad?.id ?? '', + adGroupId: r.adGroup?.id ?? '', + adGroupName: r.adGroup?.name ?? null, + campaignId: r.campaign?.id ?? '', + campaignName: r.campaign?.name ?? null, + adType: r.adGroupAd?.ad?.type ?? null, + impressions: r.metrics?.impressions ?? '0', + clicks: r.metrics?.clicks ?? '0', + costMicros: r.metrics?.costMicros ?? '0', + ctr: r.metrics?.ctr ?? null, + conversions: r.metrics?.conversions ?? null, + date: r.segments?.date ?? null, + })) + + return { + success: true, + output: { + ads, + totalCount: ads.length, + }, + } + }, + + outputs: { + ads: { + type: 'array', + description: 'Ad performance data broken down by date', + items: { + type: 'object', + properties: { + adId: { type: 'string', description: 'Ad ID' }, + adGroupId: { type: 'string', description: 'Parent ad group ID' }, + adGroupName: { type: 'string', description: 'Parent ad group name' }, + campaignId: { type: 'string', description: 'Parent campaign ID' }, + campaignName: { type: 'string', description: 'Parent campaign name' }, + adType: { + type: 'string', + description: 'Ad type (RESPONSIVE_SEARCH_AD, EXPANDED_TEXT_AD, etc.)', + }, + impressions: { type: 'string', description: 'Number of impressions' }, + clicks: { type: 'string', description: 'Number of clicks' }, + costMicros: { + type: 'string', + description: 'Cost in micros (divide by 1,000,000 for currency value)', + }, + ctr: { type: 'number', description: 'Click-through rate (0.0 to 1.0)' }, + conversions: { type: 'number', description: 'Number of conversions' }, + date: { type: 'string', description: 'Date for this row (YYYY-MM-DD)' }, + }, + }, + }, + totalCount: { + type: 'number', + description: 'Total number of result rows', + }, + }, +} diff --git a/apps/sim/tools/google_ads/campaign_performance.ts b/apps/sim/tools/google_ads/campaign_performance.ts new file mode 100644 index 0000000000..3e4ec688b7 --- /dev/null +++ b/apps/sim/tools/google_ads/campaign_performance.ts @@ -0,0 +1,182 @@ +import type { + GoogleAdsCampaignPerformanceParams, + GoogleAdsCampaignPerformanceResponse, +} from '@/tools/google_ads/types' +import { validateDate, validateDateRange, validateNumericId } from '@/tools/google_ads/types' +import type { ToolConfig } from '@/tools/types' + +export const googleAdsCampaignPerformanceTool: ToolConfig< + GoogleAdsCampaignPerformanceParams, + GoogleAdsCampaignPerformanceResponse +> = { + id: 'google_ads_campaign_performance', + name: 'Google Ads Campaign Performance', + description: 'Get performance metrics for Google Ads campaigns over a date range', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-ads', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for the Google Ads API', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Ads customer ID (numeric, no dashes)', + }, + developerToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Ads API developer token', + }, + managerCustomerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Manager account customer ID (if accessing via manager account)', + }, + campaignId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by specific campaign ID', + }, + dateRange: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: + 'Predefined date range (LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH, TODAY, YESTERDAY)', + }, + startDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom start date in YYYY-MM-DD format', + }, + endDate: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Custom end date in YYYY-MM-DD format', + }, + }, + + request: { + url: (params) => { + const customerId = validateNumericId(params.customerId, 'customerId') + return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'developer-token': params.developerToken, + } + if (params.managerCustomerId) { + headers['login-customer-id'] = validateNumericId( + params.managerCustomerId, + 'managerCustomerId' + ) + } + return headers + }, + body: (params) => { + let query = + 'SELECT campaign.id, campaign.name, campaign.status, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.ctr, metrics.conversions, segments.date FROM campaign' + + const conditions: string[] = ["campaign.status != 'REMOVED'"] + + if (params.campaignId) { + conditions.push(`campaign.id = ${validateNumericId(params.campaignId, 'campaignId')}`) + } + + if (params.startDate && params.endDate) { + const start = validateDate(params.startDate, 'startDate') + const end = validateDate(params.endDate, 'endDate') + conditions.push(`segments.date BETWEEN '${start}' AND '${end}'`) + } else { + const dateRange = validateDateRange(params.dateRange || 'LAST_30_DAYS') + conditions.push(`segments.date DURING ${dateRange}`) + } + + query += ` WHERE ${conditions.join(' AND ')}` + query += ' ORDER BY metrics.impressions DESC' + + return { query } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const errorMessage = + data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error' + return { + success: false, + output: { campaigns: [], totalCount: 0 }, + error: errorMessage, + } + } + + const results = data.results ?? [] + const campaigns = results.map((r: Record) => ({ + id: r.campaign?.id ?? '', + name: r.campaign?.name ?? '', + status: r.campaign?.status ?? '', + impressions: r.metrics?.impressions ?? '0', + clicks: r.metrics?.clicks ?? '0', + costMicros: r.metrics?.costMicros ?? '0', + ctr: r.metrics?.ctr ?? null, + conversions: r.metrics?.conversions ?? null, + date: r.segments?.date ?? null, + })) + + return { + success: true, + output: { + campaigns, + totalCount: campaigns.length, + }, + } + }, + + outputs: { + campaigns: { + type: 'array', + description: 'Campaign performance data broken down by date', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + status: { type: 'string', description: 'Campaign status' }, + impressions: { type: 'string', description: 'Number of impressions' }, + clicks: { type: 'string', description: 'Number of clicks' }, + costMicros: { + type: 'string', + description: 'Cost in micros (divide by 1,000,000 for currency value)', + }, + ctr: { type: 'number', description: 'Click-through rate (0.0 to 1.0)' }, + conversions: { type: 'number', description: 'Number of conversions' }, + date: { type: 'string', description: 'Date for this row (YYYY-MM-DD)' }, + }, + }, + }, + totalCount: { + type: 'number', + description: 'Total number of result rows', + }, + }, +} diff --git a/apps/sim/tools/google_ads/index.ts b/apps/sim/tools/google_ads/index.ts new file mode 100644 index 0000000000..d61d7a6af2 --- /dev/null +++ b/apps/sim/tools/google_ads/index.ts @@ -0,0 +1,15 @@ +import { googleAdsAdPerformanceTool } from '@/tools/google_ads/ad_performance' +import { googleAdsCampaignPerformanceTool } from '@/tools/google_ads/campaign_performance' +import { googleAdsListAdGroupsTool } from '@/tools/google_ads/list_ad_groups' +import { googleAdsListCampaignsTool } from '@/tools/google_ads/list_campaigns' +import { googleAdsListCustomersTool } from '@/tools/google_ads/list_customers' +import { googleAdsSearchTool } from '@/tools/google_ads/search' + +export { + googleAdsAdPerformanceTool, + googleAdsCampaignPerformanceTool, + googleAdsListAdGroupsTool, + googleAdsListCampaignsTool, + googleAdsListCustomersTool, + googleAdsSearchTool, +} diff --git a/apps/sim/tools/google_ads/list_ad_groups.ts b/apps/sim/tools/google_ads/list_ad_groups.ts new file mode 100644 index 0000000000..8d8d243031 --- /dev/null +++ b/apps/sim/tools/google_ads/list_ad_groups.ts @@ -0,0 +1,167 @@ +import type { + GoogleAdsListAdGroupsParams, + GoogleAdsListAdGroupsResponse, +} from '@/tools/google_ads/types' +import { validateNumericId, validateStatus } from '@/tools/google_ads/types' +import type { ToolConfig } from '@/tools/types' + +export const googleAdsListAdGroupsTool: ToolConfig< + GoogleAdsListAdGroupsParams, + GoogleAdsListAdGroupsResponse +> = { + id: 'google_ads_list_ad_groups', + name: 'List Google Ads Ad Groups', + description: 'List ad groups in a Google Ads campaign', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-ads', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for the Google Ads API', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Ads customer ID (numeric, no dashes)', + }, + developerToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Ads API developer token', + }, + managerCustomerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Manager account customer ID (if accessing via manager account)', + }, + campaignId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Campaign ID to list ad groups for', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by ad group status (ENABLED, PAUSED, REMOVED)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of ad groups to return', + }, + }, + + request: { + url: (params) => { + const customerId = validateNumericId(params.customerId, 'customerId') + return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'developer-token': params.developerToken, + } + if (params.managerCustomerId) { + headers['login-customer-id'] = validateNumericId( + params.managerCustomerId, + 'managerCustomerId' + ) + } + return headers + }, + body: (params) => { + let query = + 'SELECT ad_group.id, ad_group.name, ad_group.status, ad_group.type, campaign.id, campaign.name FROM ad_group' + + const campaignId = validateNumericId(params.campaignId, 'campaignId') + const conditions: string[] = [`campaign.id = ${campaignId}`] + + if (params.status) { + conditions.push(`ad_group.status = '${validateStatus(params.status)}'`) + } else { + conditions.push("ad_group.status != 'REMOVED'") + } + + query += ` WHERE ${conditions.join(' AND ')}` + query += ' ORDER BY ad_group.name' + + if (params.limit) { + query += ` LIMIT ${params.limit}` + } + + return { query } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const errorMessage = + data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error' + return { + success: false, + output: { adGroups: [], totalCount: 0 }, + error: errorMessage, + } + } + + const results = data.results ?? [] + const adGroups = results.map((r: Record) => ({ + id: r.adGroup?.id ?? '', + name: r.adGroup?.name ?? '', + status: r.adGroup?.status ?? '', + type: r.adGroup?.type ?? null, + campaignId: r.campaign?.id ?? '', + campaignName: r.campaign?.name ?? null, + })) + + return { + success: true, + output: { + adGroups, + totalCount: adGroups.length, + }, + } + }, + + outputs: { + adGroups: { + type: 'array', + description: 'List of ad groups in the campaign', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Ad group ID' }, + name: { type: 'string', description: 'Ad group name' }, + status: { type: 'string', description: 'Ad group status (ENABLED, PAUSED, REMOVED)' }, + type: { + type: 'string', + description: 'Ad group type (SEARCH_STANDARD, DISPLAY_STANDARD, SHOPPING_PRODUCT_ADS)', + }, + campaignId: { type: 'string', description: 'Parent campaign ID' }, + campaignName: { type: 'string', description: 'Parent campaign name' }, + }, + }, + }, + totalCount: { + type: 'number', + description: 'Total number of ad groups returned', + }, + }, +} diff --git a/apps/sim/tools/google_ads/list_campaigns.ts b/apps/sim/tools/google_ads/list_campaigns.ts new file mode 100644 index 0000000000..ed738cc955 --- /dev/null +++ b/apps/sim/tools/google_ads/list_campaigns.ts @@ -0,0 +1,168 @@ +import type { + GoogleAdsListCampaignsParams, + GoogleAdsListCampaignsResponse, +} from '@/tools/google_ads/types' +import { validateNumericId, validateStatus } from '@/tools/google_ads/types' +import type { ToolConfig } from '@/tools/types' + +export const googleAdsListCampaignsTool: ToolConfig< + GoogleAdsListCampaignsParams, + GoogleAdsListCampaignsResponse +> = { + id: 'google_ads_list_campaigns', + name: 'List Google Ads Campaigns', + description: 'List campaigns in a Google Ads account with optional status filtering', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-ads', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for the Google Ads API', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Ads customer ID (numeric, no dashes)', + }, + developerToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Ads API developer token', + }, + managerCustomerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Manager account customer ID (if accessing via manager account)', + }, + status: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Filter by campaign status (ENABLED, PAUSED, REMOVED)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of campaigns to return', + }, + }, + + request: { + url: (params) => { + const customerId = validateNumericId(params.customerId, 'customerId') + return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'developer-token': params.developerToken, + } + if (params.managerCustomerId) { + headers['login-customer-id'] = validateNumericId( + params.managerCustomerId, + 'managerCustomerId' + ) + } + return headers + }, + body: (params) => { + let query = + 'SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.start_date, campaign.end_date, campaign_budget.amount_micros FROM campaign' + + const conditions: string[] = [] + if (params.status) { + conditions.push(`campaign.status = '${validateStatus(params.status)}'`) + } else { + conditions.push("campaign.status != 'REMOVED'") + } + + if (conditions.length > 0) { + query += ` WHERE ${conditions.join(' AND ')}` + } + + query += ' ORDER BY campaign.name' + + if (params.limit) { + query += ` LIMIT ${params.limit}` + } + + return { query } + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const errorMessage = + data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error' + return { + success: false, + output: { campaigns: [], totalCount: 0 }, + error: errorMessage, + } + } + + const results = data.results ?? [] + const campaigns = results.map((r: Record) => ({ + id: r.campaign?.id ?? '', + name: r.campaign?.name ?? '', + status: r.campaign?.status ?? '', + channelType: r.campaign?.advertisingChannelType ?? null, + startDate: r.campaign?.startDate ?? null, + endDate: r.campaign?.endDate ?? null, + budgetAmountMicros: r.campaignBudget?.amountMicros ?? null, + })) + + return { + success: true, + output: { + campaigns, + totalCount: campaigns.length, + }, + } + }, + + outputs: { + campaigns: { + type: 'array', + description: 'List of campaigns in the account', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Campaign ID' }, + name: { type: 'string', description: 'Campaign name' }, + status: { type: 'string', description: 'Campaign status (ENABLED, PAUSED, REMOVED)' }, + channelType: { + type: 'string', + description: + 'Advertising channel type (SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX)', + }, + startDate: { type: 'string', description: 'Campaign start date (YYYY-MM-DD)' }, + endDate: { type: 'string', description: 'Campaign end date (YYYY-MM-DD)' }, + budgetAmountMicros: { + type: 'string', + description: 'Daily budget in micros (divide by 1,000,000 for currency value)', + }, + }, + }, + }, + totalCount: { + type: 'number', + description: 'Total number of campaigns returned', + }, + }, +} diff --git a/apps/sim/tools/google_ads/list_customers.ts b/apps/sim/tools/google_ads/list_customers.ts new file mode 100644 index 0000000000..9d764b8691 --- /dev/null +++ b/apps/sim/tools/google_ads/list_customers.ts @@ -0,0 +1,84 @@ +import type { + GoogleAdsListCustomersParams, + GoogleAdsListCustomersResponse, +} from '@/tools/google_ads/types' +import type { ToolConfig } from '@/tools/types' + +export const googleAdsListCustomersTool: ToolConfig< + GoogleAdsListCustomersParams, + GoogleAdsListCustomersResponse +> = { + id: 'google_ads_list_customers', + name: 'List Google Ads Customers', + description: 'List all Google Ads customer accounts accessible by the authenticated user', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-ads', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for the Google Ads API', + }, + developerToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Ads API developer token', + }, + }, + + request: { + url: 'https://googleads.googleapis.com/v19/customers:listAccessibleCustomers', + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'developer-token': params.developerToken, + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const errorMessage = + data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error' + return { + success: false, + output: { customerIds: [], totalCount: 0 }, + error: errorMessage, + } + } + + const resourceNames: string[] = data.resourceNames ?? [] + const customerIds = resourceNames.map((rn: string) => rn.replace('customers/', '')) + + return { + success: true, + output: { + customerIds, + totalCount: customerIds.length, + }, + } + }, + + outputs: { + customerIds: { + type: 'array', + description: 'List of accessible customer IDs', + items: { + type: 'string', + description: 'Google Ads customer ID (numeric, no dashes)', + }, + }, + totalCount: { + type: 'number', + description: 'Total number of accessible customer accounts', + }, + }, +} diff --git a/apps/sim/tools/google_ads/search.ts b/apps/sim/tools/google_ads/search.ts new file mode 100644 index 0000000000..ffd497fc39 --- /dev/null +++ b/apps/sim/tools/google_ads/search.ts @@ -0,0 +1,130 @@ +import type { GoogleAdsSearchParams, GoogleAdsSearchResponse } from '@/tools/google_ads/types' +import { validateNumericId } from '@/tools/google_ads/types' +import type { ToolConfig } from '@/tools/types' + +export const googleAdsSearchTool: ToolConfig = { + id: 'google_ads_search', + name: 'Google Ads Search (GAQL)', + description: 'Run a custom Google Ads Query Language (GAQL) query', + version: '1.0.0', + + oauth: { + required: true, + provider: 'google-ads', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token for the Google Ads API', + }, + customerId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Google Ads customer ID (numeric, no dashes)', + }, + developerToken: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Google Ads API developer token', + }, + managerCustomerId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Manager account customer ID (if accessing via manager account)', + }, + query: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'GAQL query to execute', + }, + pageToken: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Page token for pagination', + }, + }, + + request: { + url: (params) => { + const customerId = validateNumericId(params.customerId, 'customerId') + return `https://googleads.googleapis.com/v19/customers/${customerId}/googleAds:search` + }, + method: 'POST', + headers: (params) => { + const headers: Record = { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + 'developer-token': params.developerToken, + } + if (params.managerCustomerId) { + headers['login-customer-id'] = validateNumericId( + params.managerCustomerId, + 'managerCustomerId' + ) + } + return headers + }, + body: (params) => { + const body: Record = { + query: params.query, + searchSettings: { + returnTotalResultsCount: true, + }, + } + if (params.pageToken) { + body.pageToken = params.pageToken + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + if (!response.ok) { + const errorMessage = + data?.error?.message ?? data?.error?.details?.[0]?.errors?.[0]?.message ?? 'Unknown error' + return { + success: false, + output: { + results: [], + totalResultsCount: null, + nextPageToken: null, + }, + error: errorMessage, + } + } + + return { + success: true, + output: { + results: data.results ?? [], + totalResultsCount: data.totalResultsCount ? Number(data.totalResultsCount) : null, + nextPageToken: data.nextPageToken ?? null, + }, + } + }, + + outputs: { + results: { + type: 'json', + description: 'Array of result objects from the GAQL query', + }, + totalResultsCount: { + type: 'number', + description: 'Total number of matching results', + }, + nextPageToken: { + type: 'string', + description: 'Token for the next page of results', + }, + }, +} diff --git a/apps/sim/tools/google_ads/types.ts b/apps/sim/tools/google_ads/types.ts new file mode 100644 index 0000000000..3f1a9df003 --- /dev/null +++ b/apps/sim/tools/google_ads/types.ts @@ -0,0 +1,187 @@ +import type { ToolResponse } from '@/tools/types' + +const NUMERIC_ID_REGEX = /^\d+$/ +const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/ +const VALID_STATUSES = new Set(['ENABLED', 'PAUSED', 'REMOVED']) +const VALID_DATE_RANGES = new Set([ + 'TODAY', + 'YESTERDAY', + 'LAST_7_DAYS', + 'LAST_14_DAYS', + 'LAST_30_DAYS', + 'LAST_BUSINESS_WEEK', + 'THIS_MONTH', + 'LAST_MONTH', + 'THIS_WEEK_SUN_TODAY', + 'THIS_WEEK_MON_TODAY', + 'LAST_WEEK_SUN_SAT', + 'LAST_WEEK_MON_SUN', +]) + +/** Validates that a value is a numeric ID (digits only). */ +export function validateNumericId(value: string, fieldName: string): string { + const cleaned = value.replace(/-/g, '') + if (!NUMERIC_ID_REGEX.test(cleaned)) { + throw new Error(`${fieldName} must be numeric (digits only), got: ${value}`) + } + return cleaned +} + +/** Validates that a status value is a known Google Ads status. */ +export function validateStatus(value: string): string { + if (!VALID_STATUSES.has(value)) { + throw new Error(`Invalid status: ${value}. Must be one of: ${[...VALID_STATUSES].join(', ')}`) + } + return value +} + +/** Validates a date string is in YYYY-MM-DD format. */ +export function validateDate(value: string, fieldName: string): string { + if (!DATE_REGEX.test(value)) { + throw new Error(`${fieldName} must be in YYYY-MM-DD format, got: ${value}`) + } + return value +} + +/** Validates a date range is a known Google Ads predefined range. */ +export function validateDateRange(value: string): string { + if (!VALID_DATE_RANGES.has(value)) { + throw new Error( + `Invalid date range: ${value}. Must be one of: ${[...VALID_DATE_RANGES].join(', ')}` + ) + } + return value +} + +export interface GoogleAdsBaseParams { + accessToken: string + customerId: string + developerToken: string + managerCustomerId?: string +} + +export interface GoogleAdsListCustomersParams { + accessToken: string + developerToken: string +} + +export interface GoogleAdsSearchParams extends GoogleAdsBaseParams { + query: string + pageToken?: string +} + +export interface GoogleAdsListCampaignsParams extends GoogleAdsBaseParams { + status?: string + limit?: number +} + +export interface GoogleAdsCampaignPerformanceParams extends GoogleAdsBaseParams { + campaignId?: string + dateRange?: string + startDate?: string + endDate?: string +} + +export interface GoogleAdsListAdGroupsParams extends GoogleAdsBaseParams { + campaignId: string + status?: string + limit?: number +} + +export interface GoogleAdsAdPerformanceParams extends GoogleAdsBaseParams { + campaignId?: string + adGroupId?: string + dateRange?: string + startDate?: string + endDate?: string + limit?: number +} + +export interface GoogleAdsListCustomersResponse extends ToolResponse { + output: { + customerIds: string[] + totalCount: number + } +} + +export interface GoogleAdsSearchResponse extends ToolResponse { + output: { + results: Record[] + totalResultsCount: number | null + nextPageToken: string | null + } +} + +export interface GoogleAdsCampaign { + id: string + name: string + status: string + channelType: string | null + startDate: string | null + endDate: string | null + budgetAmountMicros: string | null +} + +export interface GoogleAdsListCampaignsResponse extends ToolResponse { + output: { + campaigns: GoogleAdsCampaign[] + totalCount: number + } +} + +export interface GoogleAdsCampaignPerformance { + id: string + name: string + status: string + impressions: string + clicks: string + costMicros: string + ctr: number | null + conversions: number | null + date: string | null +} + +export interface GoogleAdsCampaignPerformanceResponse extends ToolResponse { + output: { + campaigns: GoogleAdsCampaignPerformance[] + totalCount: number + } +} + +export interface GoogleAdsAdGroup { + id: string + name: string + status: string + type: string | null + campaignId: string + campaignName: string | null +} + +export interface GoogleAdsListAdGroupsResponse extends ToolResponse { + output: { + adGroups: GoogleAdsAdGroup[] + totalCount: number + } +} + +export interface GoogleAdsAdPerformance { + adId: string + adGroupId: string + adGroupName: string | null + campaignId: string + campaignName: string | null + adType: string | null + impressions: string + clicks: string + costMicros: string + ctr: number | null + conversions: number | null + date: string | null +} + +export interface GoogleAdsAdPerformanceResponse extends ToolResponse { + output: { + ads: GoogleAdsAdPerformance[] + totalCount: number + } +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index d8302770c3..1102239711 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -650,6 +650,14 @@ import { gongLookupPhoneTool, } from '@/tools/gong' import { googleSearchTool } from '@/tools/google' +import { + googleAdsAdPerformanceTool, + googleAdsCampaignPerformanceTool, + googleAdsListAdGroupsTool, + googleAdsListCampaignsTool, + googleAdsListCustomersTool, + googleAdsSearchTool, +} from '@/tools/google_ads' import { googleBigQueryGetTableTool, googleBigQueryInsertRowsTool, @@ -3652,6 +3660,12 @@ export const tools: Record = { wordpress_list_users: wordpressListUsersTool, wordpress_get_user: wordpressGetUserTool, wordpress_search_content: wordpressSearchContentTool, + google_ads_list_customers: googleAdsListCustomersTool, + google_ads_search: googleAdsSearchTool, + google_ads_list_campaigns: googleAdsListCampaignsTool, + google_ads_campaign_performance: googleAdsCampaignPerformanceTool, + google_ads_list_ad_groups: googleAdsListAdGroupsTool, + google_ads_ad_performance: googleAdsAdPerformanceTool, google_bigquery_query: googleBigQueryQueryTool, google_bigquery_list_datasets: googleBigQueryListDatasetsTool, google_bigquery_list_tables: googleBigQueryListTablesTool,