@slates-integrations/google-ads 0.2.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ import { SlateTool } from 'slates';
2
+ import { googleAdsActionScopes } from '../scopes';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { z } from 'zod';
6
+
7
+ export let searchReports = SlateTool.create(spec, {
8
+ name: 'Run GAQL Query',
9
+ key: 'search_reports',
10
+ description: `Executes a Google Ads Query Language (GAQL) query to retrieve reporting data, resource details, or metrics from a Google Ads account. Supports querying any resource type including campaigns, ad groups, ads, keywords, conversions, and more.
11
+
12
+ Use this tool to build custom reports, fetch performance metrics, or look up specific resources. The query follows the GAQL syntax: \`SELECT fields FROM resource WHERE conditions ORDER BY field LIMIT n\`.`,
13
+ instructions: [
14
+ 'GAQL queries follow the format: SELECT <fields> FROM <resource> [WHERE <conditions>] [ORDER BY <field> [ASC|DESC]] [LIMIT <n>]',
15
+ 'Common resources: campaign, ad_group, ad_group_ad, ad_group_criterion, keyword_view, campaign_budget, bidding_strategy, conversion_action',
16
+ 'Use segments like segments.date to break down metrics by date',
17
+ 'Date ranges can be filtered with segments.date BETWEEN "YYYY-MM-DD" AND "YYYY-MM-DD" or using DURING LAST_30_DAYS, THIS_MONTH, etc.'
18
+ ],
19
+ tags: {
20
+ readOnly: true
21
+ }
22
+ })
23
+ .scopes(googleAdsActionScopes.searchReports)
24
+ .input(
25
+ z.object({
26
+ customerId: z.string().describe('The Google Ads customer account ID (without hyphens)'),
27
+ query: z.string().describe('The GAQL query string to execute'),
28
+ pageSize: z
29
+ .number()
30
+ .optional()
31
+ .describe('Maximum number of results per page (default: 10000)'),
32
+ pageToken: z.string().optional().describe('Page token for pagination')
33
+ })
34
+ )
35
+ .output(
36
+ z.object({
37
+ results: z
38
+ .array(z.any())
39
+ .describe('Array of result rows, each containing the requested fields'),
40
+ totalResultsCount: z
41
+ .string()
42
+ .optional()
43
+ .describe('Total number of results matching the query'),
44
+ nextPageToken: z
45
+ .string()
46
+ .optional()
47
+ .describe('Token to retrieve the next page of results')
48
+ })
49
+ )
50
+ .handleInvocation(async ctx => {
51
+ let client = createClient(ctx.auth, ctx.config);
52
+
53
+ let response = await client.search(
54
+ ctx.input.customerId,
55
+ ctx.input.query,
56
+ ctx.input.pageSize,
57
+ ctx.input.pageToken
58
+ );
59
+
60
+ let resultCount = response.results?.length ?? 0;
61
+
62
+ return {
63
+ output: {
64
+ results: response.results || [],
65
+ totalResultsCount: response.totalResultsCount,
66
+ nextPageToken: response.nextPageToken
67
+ },
68
+ message: `Query returned **${resultCount}** result(s)${response.nextPageToken ? ' (more pages available)' : ''}.`
69
+ };
70
+ })
71
+ .build();
@@ -0,0 +1,93 @@
1
+ import { SlateTool } from 'slates';
2
+ import { googleAdsActionScopes } from '../scopes';
3
+ import { spec } from '../spec';
4
+ import { createClient } from '../lib/helpers';
5
+ import { z } from 'zod';
6
+
7
+ export let uploadOfflineConversions = SlateTool.create(spec, {
8
+ name: 'Upload Offline Conversions',
9
+ key: 'upload_offline_conversions',
10
+ description: `Upload offline click conversions to Google Ads. Imports real-world transaction data like in-store purchases, qualified phone leads, or CRM events to measure full-funnel conversion impact.
11
+
12
+ Each conversion requires a Google Click ID (gclid) to link the offline event back to the original ad click.`,
13
+ instructions: [
14
+ 'The gclid is captured when a user clicks on an ad and visits your site. Store it for later upload.',
15
+ 'Conversion date/time should be in the format "yyyy-mm-dd hh:mm:ss+|-hh:mm" (e.g., "2024-01-15 14:30:00-05:00").',
16
+ 'The conversion action resource name must match an existing conversion action in the account.',
17
+ 'Conversions can take up to 3 hours to appear in Google Ads reporting.'
18
+ ],
19
+ constraints: [
20
+ 'Click conversions must be uploaded within 90 days of the click.',
21
+ 'Duplicate conversions (same gclid + conversion action + conversion date) are rejected.'
22
+ ]
23
+ })
24
+ .scopes(googleAdsActionScopes.uploadOfflineConversions)
25
+ .input(
26
+ z.object({
27
+ customerId: z.string().describe('The Google Ads customer account ID'),
28
+ conversions: z
29
+ .array(
30
+ z.object({
31
+ gclid: z.string().describe('Google Click ID from the original ad click'),
32
+ conversionAction: z
33
+ .string()
34
+ .describe(
35
+ 'Conversion action resource name (e.g., customers/{id}/conversionActions/{id})'
36
+ ),
37
+ conversionDateTime: z
38
+ .string()
39
+ .describe('When the conversion occurred, e.g., "2024-01-15 14:30:00-05:00"'),
40
+ conversionValue: z
41
+ .number()
42
+ .optional()
43
+ .describe('Monetary value of the conversion'),
44
+ currencyCode: z
45
+ .string()
46
+ .optional()
47
+ .describe('Currency code for the conversion value (e.g., USD)'),
48
+ orderId: z
49
+ .string()
50
+ .optional()
51
+ .describe('Unique order/transaction ID for deduplication')
52
+ })
53
+ )
54
+ .describe('List of conversions to upload'),
55
+ partialFailure: z
56
+ .boolean()
57
+ .optional()
58
+ .describe('If true, valid conversions are uploaded even if some fail (default: true)')
59
+ })
60
+ )
61
+ .output(
62
+ z.object({
63
+ uploadResults: z.any().describe('Results for each uploaded conversion'),
64
+ partialFailureError: z.any().optional().describe('Details of any partial failures')
65
+ })
66
+ )
67
+ .handleInvocation(async ctx => {
68
+ let client = createClient(ctx.auth, ctx.config);
69
+
70
+ let conversions = ctx.input.conversions.map(c => ({
71
+ gclid: c.gclid,
72
+ conversionAction: c.conversionAction,
73
+ conversionDateTime: c.conversionDateTime,
74
+ conversionValue: c.conversionValue,
75
+ currencyCode: c.currencyCode,
76
+ orderId: c.orderId
77
+ }));
78
+
79
+ let result = await client.uploadClickConversions(
80
+ ctx.input.customerId,
81
+ conversions,
82
+ ctx.input.partialFailure
83
+ );
84
+
85
+ return {
86
+ output: {
87
+ uploadResults: result.results || result,
88
+ partialFailureError: result.partialFailureError
89
+ },
90
+ message: `Uploaded **${conversions.length}** offline conversion(s).${result.partialFailureError ? ' Some conversions had errors.' : ''}`
91
+ };
92
+ })
93
+ .build();
@@ -0,0 +1 @@
1
+ export * from './lead-form-submit';
@@ -0,0 +1,135 @@
1
+ import { SlateTrigger } from 'slates';
2
+ import { googleAdsActionScopes } from '../scopes';
3
+ import { spec } from '../spec';
4
+ import { z } from 'zod';
5
+
6
+ export let leadFormSubmit = SlateTrigger.create(spec, {
7
+ name: 'Lead Form Submission',
8
+ key: 'lead_form_submit',
9
+ description: `Receives lead form submissions from Google Ads campaigns via webhook. Triggers when a user submits a lead form extension in a Search, Display, YouTube, or Performance Max campaign.
10
+
11
+ The webhook URL must be configured in the lead form extension settings within Google Ads. Each lead includes the submitted user data, campaign/ad group context, and a Google Click ID.`,
12
+ instructions: [
13
+ 'Configure the webhook URL in your Google Ads lead form extension settings.',
14
+ 'Use the lead_id for deduplication as Google Ads may retry delivery.',
15
+ 'The is_test field indicates whether the lead was submitted using the "Test" button in Google Ads.'
16
+ ]
17
+ })
18
+ .scopes(googleAdsActionScopes.leadFormSubmit)
19
+ .input(
20
+ z.object({
21
+ leadId: z.string().describe('Unique lead identifier for deduplication'),
22
+ campaignId: z.string().optional().describe('Campaign that generated the lead'),
23
+ adGroupId: z.string().optional().describe('Ad group that generated the lead'),
24
+ creativeId: z.string().optional().describe('Creative/ad that generated the lead'),
25
+ assetGroupId: z
26
+ .string()
27
+ .optional()
28
+ .describe('Asset group ID (only for Performance Max campaigns)'),
29
+ gclid: z.string().optional().describe('Google Click ID'),
30
+ isTest: z.boolean().optional().describe('Whether this is a test lead'),
31
+ userColumnData: z
32
+ .array(
33
+ z.object({
34
+ columnId: z
35
+ .string()
36
+ .describe(
37
+ 'Data type identifier (e.g., FULL_NAME, EMAIL, PHONE_NUMBER, POSTAL_CODE, CITY, COUNTRY)'
38
+ ),
39
+ stringValue: z.string().describe('The submitted value')
40
+ })
41
+ )
42
+ .optional()
43
+ .describe('User-submitted form data'),
44
+ apiVersion: z.string().optional().describe('API version of the webhook payload'),
45
+ formId: z.string().optional().describe('Lead form ID'),
46
+ gclidCreatedAt: z.string().optional().describe('Timestamp when the gclid was created')
47
+ })
48
+ )
49
+ .output(
50
+ z.object({
51
+ leadId: z.string().describe('Unique lead identifier'),
52
+ campaignId: z.string().optional().describe('Campaign ID'),
53
+ adGroupId: z.string().optional().describe('Ad group ID'),
54
+ creativeId: z.string().optional().describe('Creative/ad ID'),
55
+ assetGroupId: z.string().optional().describe('Asset group ID (Performance Max only)'),
56
+ gclid: z.string().optional().describe('Google Click ID for conversion tracking'),
57
+ isTest: z.boolean().optional().describe('Whether this is a test lead'),
58
+ formId: z.string().optional().describe('Lead form ID'),
59
+ userData: z
60
+ .record(z.string(), z.string())
61
+ .optional()
62
+ .describe(
63
+ 'User-submitted data as key-value pairs (e.g., { "FULL_NAME": "John Doe", "EMAIL": "john@example.com" })'
64
+ )
65
+ })
66
+ )
67
+ .webhook({
68
+ handleRequest: async ctx => {
69
+ let data = (await ctx.request.json()) as Record<string, any>;
70
+
71
+ let input: {
72
+ leadId: string;
73
+ campaignId?: string;
74
+ adGroupId?: string;
75
+ creativeId?: string;
76
+ assetGroupId?: string;
77
+ gclid?: string;
78
+ isTest?: boolean;
79
+ userColumnData?: { columnId: string; stringValue: string }[];
80
+ apiVersion?: string;
81
+ formId?: string;
82
+ gclidCreatedAt?: string;
83
+ } = {
84
+ leadId: data.lead_id || data.leadId || '',
85
+ campaignId: data.campaign_id?.toString() || data.campaignId?.toString(),
86
+ adGroupId: data.ad_group_id?.toString() || data.adGroupId?.toString(),
87
+ creativeId: data.creative_id?.toString() || data.creativeId?.toString(),
88
+ assetGroupId: data.asset_group_id?.toString() || data.assetGroupId?.toString(),
89
+ gclid: data.gcl_id || data.gclid,
90
+ isTest: data.is_test ?? data.isTest,
91
+ apiVersion: data.api_version || data.apiVersion,
92
+ formId: data.form_id?.toString() || data.formId?.toString(),
93
+ gclidCreatedAt: data.gclid_created_at || data.gclidCreatedAt
94
+ };
95
+
96
+ if (Array.isArray(data.user_column_data || data.userColumnData)) {
97
+ input.userColumnData = (data.user_column_data || data.userColumnData).map(
98
+ (col: any) => ({
99
+ columnId: col.column_id || col.columnId,
100
+ stringValue: col.string_value || col.stringValue || ''
101
+ })
102
+ );
103
+ }
104
+
105
+ return {
106
+ inputs: [input]
107
+ };
108
+ },
109
+
110
+ handleEvent: async ctx => {
111
+ let userData: Record<string, string> = {};
112
+ if (ctx.input.userColumnData) {
113
+ for (let col of ctx.input.userColumnData) {
114
+ userData[col.columnId] = col.stringValue;
115
+ }
116
+ }
117
+
118
+ return {
119
+ type: 'lead_form.submitted',
120
+ id: ctx.input.leadId,
121
+ output: {
122
+ leadId: ctx.input.leadId,
123
+ campaignId: ctx.input.campaignId,
124
+ adGroupId: ctx.input.adGroupId,
125
+ creativeId: ctx.input.creativeId,
126
+ assetGroupId: ctx.input.assetGroupId,
127
+ gclid: ctx.input.gclid,
128
+ isTest: ctx.input.isTest,
129
+ formId: ctx.input.formId,
130
+ userData: Object.keys(userData).length > 0 ? userData : undefined
131
+ }
132
+ };
133
+ }
134
+ })
135
+ .build();
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "types": ["node"],
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+ "moduleResolution": "bundler",
11
+
12
+ "noEmit": true,
13
+ "strict": true,
14
+ "skipLibCheck": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "noImplicitOverride": true,
18
+ "noUnusedLocals": false,
19
+ "noUnusedParameters": false,
20
+ "noPropertyAccessFromIndexSignature": false
21
+ },
22
+ "include": ["src"]
23
+ }
@@ -0,0 +1,7 @@
1
+ import { createSlatesVitestConfig } from '@slates/test/config';
2
+
3
+ export default createSlatesVitestConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts', 'src/**/*.e2e.ts']
6
+ }
7
+ });