@pipedream/google_ads 0.7.0 → 1.0.0

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.
Files changed (54) hide show
  1. package/README.md +4 -4
  2. package/actions/add-contact-to-list-by-email/add-contact-to-list-by-email.mjs +1 -1
  3. package/actions/common/bidding.mjs +196 -0
  4. package/actions/common/conversion-upload.mjs +131 -0
  5. package/actions/common/props.mjs +1 -1
  6. package/actions/create-ad-group-report/create-ad-group-report.mjs +2 -2
  7. package/actions/create-ad-report/create-ad-report.mjs +2 -2
  8. package/actions/create-campaign-report/create-campaign-report.mjs +2 -2
  9. package/actions/create-customer-list/common-constants.mjs +10 -10
  10. package/actions/create-customer-list/create-customer-list.mjs +3 -3
  11. package/actions/create-customer-report/create-customer-report.mjs +2 -2
  12. package/actions/create-or-remove-campaign-criteria/create-or-remove-campaign-criteria.mjs +214 -0
  13. package/actions/create-or-remove-campaign-shared-set/create-or-remove-campaign-shared-set.mjs +155 -0
  14. package/actions/create-or-remove-shared-criteria/create-or-remove-shared-criteria.mjs +180 -0
  15. package/actions/create-or-update-ad-group/create-or-update-ad-group.mjs +315 -0
  16. package/actions/create-or-update-ad-group-ad/create-or-update-ad-group-ad.mjs +203 -0
  17. package/actions/create-or-update-bidding-strategy/create-or-update-bidding-strategy.mjs +185 -0
  18. package/actions/create-or-update-campaign/create-or-update-campaign.mjs +300 -0
  19. package/actions/create-or-update-campaign-budget/create-or-update-campaign-budget.mjs +212 -0
  20. package/actions/create-or-update-keywords/create-or-update-keywords.mjs +275 -0
  21. package/actions/create-or-update-shared-sets/create-or-update-shared-sets.mjs +180 -0
  22. package/actions/create-report/create-report.mjs +5 -7
  23. package/actions/create-responsive-search-ad/create-responsive-search-ad.mjs +202 -0
  24. package/actions/generate-keyword-ideas/generate-keyword-ideas.mjs +3 -3
  25. package/actions/get-keyword-quality-scores/get-keyword-quality-scores.mjs +107 -0
  26. package/actions/list-account-id-options/list-account-id-options.mjs +1 -1
  27. package/actions/list-ad-group-ads/list-ad-group-ads.mjs +55 -0
  28. package/actions/list-ad-group-criteria/list-ad-group-criteria.mjs +55 -0
  29. package/actions/list-ad-groups/list-ad-groups.mjs +55 -0
  30. package/actions/list-bidding-strategies/list-bidding-strategies.mjs +55 -0
  31. package/actions/list-campaign-budgets/list-campaign-budgets.mjs +55 -0
  32. package/actions/list-campaign-criteria/list-campaign-criteria.mjs +55 -0
  33. package/actions/list-campaign-shared-sets/list-campaign-shared-sets.mjs +55 -0
  34. package/actions/list-campaigns/list-campaigns.mjs +55 -0
  35. package/actions/list-customer-clients/list-customer-clients.mjs +2 -2
  36. package/actions/list-keywords/list-keywords.mjs +55 -0
  37. package/actions/list-shared-criteria/list-shared-criteria.mjs +55 -0
  38. package/actions/list-shared-sets/list-shared-sets.mjs +55 -0
  39. package/actions/send-offline-conversion/send-offline-conversion.mjs +4 -4
  40. package/actions/update-campaign-ai-max-settings/update-campaign-ai-max-settings.mjs +167 -0
  41. package/actions/upload-call-conversion/upload-call-conversion.mjs +65 -0
  42. package/actions/upload-click-conversion/upload-click-conversion.mjs +121 -0
  43. package/common/common-report.mjs +5 -7
  44. package/common/constants.mjs +369 -0
  45. package/common/queries.mjs +4 -2
  46. package/common/resources/ad.mjs +25 -70
  47. package/common/resources/adGroup.mjs +2 -3
  48. package/common/resources/campaign.mjs +7 -10
  49. package/common/resources/customer.mjs +3 -4
  50. package/common/utils.mjs +4 -0
  51. package/google_ads.app.mjs +657 -19
  52. package/package.json +1 -1
  53. package/sources/new-campaign-created/new-campaign-created.mjs +3 -3
  54. package/sources/new-lead-form-entry/new-lead-form-entry.mjs +3 -3
@@ -0,0 +1,275 @@
1
+ import { ConfigurationError } from "@pipedream/platform";
2
+ import googleAds from "../../google_ads.app.mjs";
3
+ import {
4
+ AD_GROUP_STATUSES,
5
+ CAMPAIGN_OPERATION_TYPES,
6
+ KEYWORD_MATCH_TYPES,
7
+ } from "../../common/constants.mjs";
8
+ import { parseObject } from "../../common/utils.mjs";
9
+ import { getAdditionalFields } from "../common/props.mjs";
10
+
11
+ const docLink =
12
+ "https://developers.google.com/google-ads/api/reference/rpc/v25/AdGroupCriterionService/MutateAdGroupCriteria?transport=rest";
13
+
14
+ export default {
15
+ key: "google_ads-create-or-update-keywords",
16
+ name: "Create, Update, or Remove Keywords",
17
+ description: `Creates, updates, or removes keyword criteria for an ad group. [See the documentation](${docLink})`,
18
+ version: "0.0.2",
19
+ type: "action",
20
+ annotations: {
21
+ destructiveHint: true,
22
+ openWorldHint: true,
23
+ readOnlyHint: false,
24
+ },
25
+ props: {
26
+ googleAds,
27
+ accountId: {
28
+ propDefinition: [
29
+ googleAds,
30
+ "accountId",
31
+ ],
32
+ },
33
+ customerClientId: {
34
+ propDefinition: [
35
+ googleAds,
36
+ "customerClientId",
37
+ ({ accountId }) => ({
38
+ accountId,
39
+ }),
40
+ ],
41
+ optional: true,
42
+ },
43
+ operationType: {
44
+ type: "string",
45
+ label: "Operation Type",
46
+ description: "Whether to create, update, or remove a keyword.",
47
+ options: CAMPAIGN_OPERATION_TYPES,
48
+ },
49
+ adGroupCriterionId: {
50
+ propDefinition: [
51
+ googleAds,
52
+ "adGroupCriterionId",
53
+ ({
54
+ accountId, customerClientId,
55
+ }) => ({
56
+ accountId,
57
+ customerClientId,
58
+ }),
59
+ ],
60
+ description: "The keyword to update or remove. Required for **Update** and **Remove** operations.",
61
+ optional: true,
62
+ },
63
+ adGroupId: {
64
+ propDefinition: [
65
+ googleAds,
66
+ "adGroupId",
67
+ ({
68
+ accountId, customerClientId,
69
+ }) => ({
70
+ accountId,
71
+ customerClientId,
72
+ }),
73
+ ],
74
+ description: "The ad group this keyword belongs to. Required for **Create** operations.",
75
+ optional: true,
76
+ },
77
+ updateMask: {
78
+ type: "string",
79
+ label: "Update Mask",
80
+ description: "Comma-separated list of fields to update (e.g., `status,cpcBidMicros`). Required for **Update** operations.",
81
+ optional: true,
82
+ },
83
+ keywordText: {
84
+ type: "string",
85
+ label: "Keyword Text",
86
+ description: "The keyword text (max 80 characters, max 10 words). Required for **Create** operations.",
87
+ optional: true,
88
+ },
89
+ keywordMatchType: {
90
+ type: "string",
91
+ label: "Keyword Match Type",
92
+ description: "The match type for the keyword. Required for **Create** operations.",
93
+ options: KEYWORD_MATCH_TYPES,
94
+ optional: true,
95
+ },
96
+ negative: {
97
+ type: "boolean",
98
+ label: "Negative Keyword",
99
+ description: "Whether this is a negative keyword. Immutable after creation.",
100
+ optional: true,
101
+ },
102
+ status: {
103
+ type: "string",
104
+ label: "Status",
105
+ description: "The status of the keyword criterion.",
106
+ options: AD_GROUP_STATUSES,
107
+ optional: true,
108
+ },
109
+ cpcBidMicros: {
110
+ type: "integer",
111
+ label: "CPC Bid (Micros)",
112
+ description: "The maximum CPC (cost-per-click) bid in micros (e.g., `1000000` = $1.00).",
113
+ optional: true,
114
+ },
115
+ trackingUrlTemplate: {
116
+ type: "string",
117
+ label: "Tracking URL Template",
118
+ description: "The URL template for constructing a tracking URL.",
119
+ optional: true,
120
+ },
121
+ finalUrls: {
122
+ type: "string[]",
123
+ label: "Final URLs",
124
+ description: "The list of final URLs for the keyword.",
125
+ optional: true,
126
+ },
127
+ finalMobileUrls: {
128
+ type: "string[]",
129
+ label: "Final Mobile URLs",
130
+ description: "The list of final mobile URLs for the keyword.",
131
+ optional: true,
132
+ },
133
+ finalUrlSuffix: {
134
+ type: "string",
135
+ label: "Final URL Suffix",
136
+ description: "The suffix used to append query parameters to landing page URLs.",
137
+ optional: true,
138
+ },
139
+ additionalFields: getAdditionalFields(docLink),
140
+ },
141
+ async run({ $ }) {
142
+ const {
143
+ googleAds,
144
+ accountId,
145
+ customerClientId,
146
+ operationType,
147
+ adGroupCriterionId,
148
+ adGroupId,
149
+ updateMask,
150
+ keywordText,
151
+ keywordMatchType,
152
+ negative,
153
+ status,
154
+ cpcBidMicros,
155
+ trackingUrlTemplate,
156
+ finalUrls,
157
+ finalMobileUrls,
158
+ finalUrlSuffix,
159
+ additionalFields,
160
+ } = this;
161
+
162
+ if ((operationType === "update" || operationType === "remove") && !adGroupCriterionId) {
163
+ throw new ConfigurationError(
164
+ "**Keyword** is required for Update and Remove operations.",
165
+ );
166
+ }
167
+
168
+ if (operationType === "update" && !updateMask) {
169
+ throw new ConfigurationError(
170
+ "**Update Mask** is required for Update operations.",
171
+ );
172
+ }
173
+
174
+ if (operationType === "create") {
175
+ if (!adGroupId) {
176
+ throw new ConfigurationError(
177
+ "**Ad Group** is required for Create operations.",
178
+ );
179
+ }
180
+ if (!keywordText) {
181
+ throw new ConfigurationError(
182
+ "**Keyword Text** is required for Create operations.",
183
+ );
184
+ }
185
+ if (!keywordMatchType) {
186
+ throw new ConfigurationError(
187
+ "**Keyword Match Type** is required for Create operations.",
188
+ );
189
+ }
190
+ }
191
+
192
+ const customerId = customerClientId ?? accountId;
193
+ const adGroup = adGroupId
194
+ ? `customers/${customerId}/adGroups/${adGroupId}`
195
+ : undefined;
196
+
197
+ let operation;
198
+
199
+ if (operationType === "remove") {
200
+ operation = {
201
+ remove: adGroupCriterionId,
202
+ };
203
+ } else {
204
+ const criterionData = {
205
+ ...(adGroupCriterionId && {
206
+ resourceName: adGroupCriterionId,
207
+ }),
208
+ ...(adGroup && {
209
+ adGroup,
210
+ }),
211
+ ...(keywordText && keywordMatchType && {
212
+ keyword: {
213
+ text: keywordText,
214
+ matchType: keywordMatchType,
215
+ },
216
+ }),
217
+ ...(negative !== undefined && {
218
+ negative,
219
+ }),
220
+ ...(status && {
221
+ status,
222
+ }),
223
+ ...(cpcBidMicros !== undefined && {
224
+ cpcBidMicros,
225
+ }),
226
+ ...(trackingUrlTemplate && {
227
+ trackingUrlTemplate,
228
+ }),
229
+ ...(finalUrls?.length && {
230
+ finalUrls,
231
+ }),
232
+ ...(finalMobileUrls?.length && {
233
+ finalMobileUrls,
234
+ }),
235
+ ...(finalUrlSuffix && {
236
+ finalUrlSuffix,
237
+ }),
238
+ ...parseObject(additionalFields),
239
+ };
240
+
241
+ if (operationType === "update") {
242
+ operation = {
243
+ update: criterionData,
244
+ updateMask,
245
+ };
246
+ } else {
247
+ operation = {
248
+ create: criterionData,
249
+ };
250
+ }
251
+ }
252
+
253
+ const response = await googleAds.mutateAdGroupCriteria({
254
+ $,
255
+ accountId,
256
+ customerClientId,
257
+ data: {
258
+ operations: [
259
+ operation,
260
+ ],
261
+ },
262
+ });
263
+
264
+ const result = response?.results?.[0];
265
+ const id = result?.resourceName?.split("~").pop();
266
+
267
+ $.export(
268
+ "$summary",
269
+ `Successfully ${operationType}d keyword${id
270
+ ? ` with criterion ID \`${id}\``
271
+ : ""}.`,
272
+ );
273
+ return response;
274
+ },
275
+ };
@@ -0,0 +1,180 @@
1
+ import { ConfigurationError } from "@pipedream/platform";
2
+ import googleAds from "../../google_ads.app.mjs";
3
+ import {
4
+ CAMPAIGN_OPERATION_TYPES,
5
+ SHARED_SET_TYPES,
6
+ } from "../../common/constants.mjs";
7
+ import { parseObject } from "../../common/utils.mjs";
8
+ import { getAdditionalFields } from "../common/props.mjs";
9
+
10
+ const docLink =
11
+ "https://developers.google.com/google-ads/api/reference/rpc/v25/SharedSetService/MutateSharedSets?transport=rest";
12
+
13
+ export default {
14
+ key: "google_ads-create-or-update-shared-sets",
15
+ name: "Create or Update Shared Sets",
16
+ description: `Creates or updates shared sets (reusable lists of negative keywords or placements). [See the documentation](${docLink})`,
17
+ version: "0.0.2",
18
+ type: "action",
19
+ annotations: {
20
+ destructiveHint: true,
21
+ openWorldHint: true,
22
+ readOnlyHint: false,
23
+ },
24
+ props: {
25
+ googleAds,
26
+ accountId: {
27
+ propDefinition: [
28
+ googleAds,
29
+ "accountId",
30
+ ],
31
+ },
32
+ customerClientId: {
33
+ propDefinition: [
34
+ googleAds,
35
+ "customerClientId",
36
+ ({ accountId }) => ({
37
+ accountId,
38
+ }),
39
+ ],
40
+ optional: true,
41
+ },
42
+ operationType: {
43
+ type: "string",
44
+ label: "Operation Type",
45
+ description: "Whether to create, update, or remove a shared set.",
46
+ options: CAMPAIGN_OPERATION_TYPES,
47
+ },
48
+ sharedSetId: {
49
+ propDefinition: [
50
+ googleAds,
51
+ "sharedSetId",
52
+ ({
53
+ accountId, customerClientId,
54
+ }) => ({
55
+ accountId,
56
+ customerClientId,
57
+ }),
58
+ ],
59
+ description: "The shared set to update or remove. Required for **Update** and **Remove** operations.",
60
+ optional: true,
61
+ },
62
+ updateMask: {
63
+ type: "string",
64
+ label: "Update Mask",
65
+ description: "Comma-separated list of fields to update (e.g., `name`). Required for **Update** operations.",
66
+ optional: true,
67
+ },
68
+ name: {
69
+ type: "string",
70
+ label: "Name",
71
+ description: "The name of the shared set. Required for **Create** operations.",
72
+ optional: true,
73
+ },
74
+ type: {
75
+ type: "string",
76
+ label: "Type",
77
+ description: "The type of the shared set. Required for **Create** operations. Immutable after creation.",
78
+ options: SHARED_SET_TYPES,
79
+ optional: true,
80
+ },
81
+ additionalFields: getAdditionalFields(docLink),
82
+ },
83
+ async run({ $ }) {
84
+ const {
85
+ googleAds,
86
+ accountId,
87
+ customerClientId,
88
+ operationType,
89
+ sharedSetId,
90
+ updateMask,
91
+ name,
92
+ type,
93
+ additionalFields,
94
+ } = this;
95
+
96
+ if ((operationType === "update" || operationType === "remove") && !sharedSetId) {
97
+ throw new ConfigurationError(
98
+ "**Shared Set** is required for Update and Remove operations.",
99
+ );
100
+ }
101
+
102
+ if (operationType === "update" && !updateMask) {
103
+ throw new ConfigurationError(
104
+ "**Update Mask** is required for Update operations.",
105
+ );
106
+ }
107
+
108
+ if (operationType === "create") {
109
+ if (!name) {
110
+ throw new ConfigurationError(
111
+ "**Name** is required for Create operations.",
112
+ );
113
+ }
114
+ if (!type) {
115
+ throw new ConfigurationError(
116
+ "**Type** is required for Create operations.",
117
+ );
118
+ }
119
+ }
120
+
121
+ const customerId = customerClientId ?? accountId;
122
+ const resourceName = sharedSetId
123
+ ? `customers/${customerId}/sharedSets/${sharedSetId}`
124
+ : undefined;
125
+
126
+ let operation;
127
+
128
+ if (operationType === "remove") {
129
+ operation = {
130
+ remove: resourceName,
131
+ };
132
+ } else {
133
+ const sharedSetData = {
134
+ ...(resourceName && {
135
+ resourceName,
136
+ }),
137
+ ...(name && {
138
+ name,
139
+ }),
140
+ ...(type && {
141
+ type,
142
+ }),
143
+ ...parseObject(additionalFields),
144
+ };
145
+
146
+ if (operationType === "update") {
147
+ operation = {
148
+ update: sharedSetData,
149
+ updateMask,
150
+ };
151
+ } else {
152
+ operation = {
153
+ create: sharedSetData,
154
+ };
155
+ }
156
+ }
157
+
158
+ const response = await googleAds.mutateSharedSets({
159
+ $,
160
+ accountId,
161
+ customerClientId,
162
+ data: {
163
+ operations: [
164
+ operation,
165
+ ],
166
+ },
167
+ });
168
+
169
+ const result = response?.results?.[0];
170
+ const id = result?.resourceName?.split("/").pop();
171
+
172
+ $.export(
173
+ "$summary",
174
+ `Successfully ${operationType}d shared set${id
175
+ ? ` with ID \`${id}\``
176
+ : ""}.`,
177
+ );
178
+ return response;
179
+ },
180
+ };
@@ -19,8 +19,8 @@ export default {
19
19
  ...common,
20
20
  key: "google_ads-create-report",
21
21
  name: "Create Report",
22
- description: "Generates a report from your Google Ads data. [See the documentation](https://developers.google.com/google-ads/api/reference/rpc/v21/GoogleAdsService/Search?transport=rest)",
23
- version: "0.2.0",
22
+ description: "Generates a report from your Google Ads data. [See the documentation](https://developers.google.com/google-ads/api/reference/rpc/v25/GoogleAdsService/Search?transport=rest)",
23
+ version: "0.3.0",
24
24
  annotations: {
25
25
  destructiveHint: false,
26
26
  openWorldHint: true,
@@ -99,7 +99,7 @@ export default {
99
99
  segments: {
100
100
  type: "string[]",
101
101
  label: "Segments",
102
- description: "Segment field names to include (e.g. `[\"date\"]` or `[\"segments.date\"]`). See the documentation [here](https://developers.google.com/google-ads/api/reference/rpc/v21/Segments).",
102
+ description: "Segment field names to include (e.g. `[\"date\"]` or `[\"segments.date\"]`). See the documentation [here](https://developers.google.com/google-ads/api/reference/rpc/v25/Segments).",
103
103
  options() {
104
104
  const resource = RESOURCES.find((r) => r.resourceOption.value === this.resource);
105
105
  if (!resource) throw new ConfigurationError("Select one of the available resources.");
@@ -113,7 +113,7 @@ export default {
113
113
  metrics: {
114
114
  type: "string[]",
115
115
  label: "Metrics",
116
- description: "Metric field names to include (e.g. `[\"impressions\", \"clicks\"]` or `[\"metrics.impressions\", \"metrics.clicks\"]`). See the documentation [here](https://developers.google.com/google-ads/api/reference/rpc/v21/Metrics).",
116
+ description: "Metric field names to include (e.g. `[\"impressions\", \"clicks\"]` or `[\"metrics.impressions\", \"metrics.clicks\"]`). See the documentation [here](https://developers.google.com/google-ads/api/reference/rpc/v25/Metrics).",
117
117
  options() {
118
118
  const resource = RESOURCES.find((r) => r.resourceOption.value === this.resource);
119
119
  if (!resource) throw new ConfigurationError("Select one of the available resources.");
@@ -256,9 +256,7 @@ export default {
256
256
  $,
257
257
  accountId: this.accountId,
258
258
  customerClientId: this.customerClientId,
259
- data: {
260
- query,
261
- },
259
+ query,
262
260
  })) ?? [];
263
261
 
264
262
  const { length } = results;
@@ -0,0 +1,202 @@
1
+ import { ConfigurationError } from "@pipedream/platform";
2
+ import googleAds from "../../google_ads.app.mjs";
3
+ import { AD_GROUP_STATUSES } from "../../common/constants.mjs";
4
+
5
+ const docLink =
6
+ "https://developers.google.com/google-ads/api/docs/ads/ad-types";
7
+
8
+ function parseAsset(value) {
9
+ try {
10
+ const parsed = JSON.parse(value);
11
+ if (typeof parsed === "object" && parsed !== null) {
12
+ if (!parsed.text) {
13
+ throw new ConfigurationError(
14
+ `Malformed JSON asset: missing required 'text' field in: \`${value}\``,
15
+ );
16
+ }
17
+ return parsed;
18
+ }
19
+ } catch (err) {
20
+ if (err instanceof ConfigurationError) throw err;
21
+ // not JSON, treat as plain text
22
+ }
23
+ return {
24
+ text: value,
25
+ };
26
+ }
27
+
28
+ export default {
29
+ key: "google_ads-create-responsive-search-ad",
30
+ name: "Create Responsive Search Ad",
31
+ description: `Creates a Responsive Search Ad (RSA) in an ad group. RSA copy (headlines, descriptions) cannot be edited in-place after creation — to change ad copy, remove the existing ad and create a new one. URL fields (\`finalUrls\`, \`trackingUrlTemplate\`, etc.) can be updated in-place via the **Create or Update Ad Group Ad** action. [See the documentation](${docLink})`,
32
+ version: "0.0.2",
33
+ type: "action",
34
+ annotations: {
35
+ destructiveHint: false,
36
+ openWorldHint: true,
37
+ readOnlyHint: false,
38
+ },
39
+ props: {
40
+ googleAds,
41
+ accountId: {
42
+ propDefinition: [
43
+ googleAds,
44
+ "accountId",
45
+ ],
46
+ },
47
+ customerClientId: {
48
+ propDefinition: [
49
+ googleAds,
50
+ "customerClientId",
51
+ ({ accountId }) => ({
52
+ accountId,
53
+ }),
54
+ ],
55
+ optional: true,
56
+ },
57
+ adGroupId: {
58
+ propDefinition: [
59
+ googleAds,
60
+ "adGroupId",
61
+ ({
62
+ accountId, customerClientId,
63
+ }) => ({
64
+ accountId,
65
+ customerClientId,
66
+ }),
67
+ ],
68
+ description: "The ad group to create this ad in.",
69
+ },
70
+ status: {
71
+ type: "string",
72
+ label: "Status",
73
+ description: "The initial status of the ad. Defaults to `ENABLED`.",
74
+ options: AD_GROUP_STATUSES,
75
+ optional: true,
76
+ },
77
+ headlines: {
78
+ type: "string[]",
79
+ label: "Headlines",
80
+ description: "3–15 headline assets. Each value is either plain text (max 30 characters) or a JSON object to pin to a specific slot: `{\"text\": \"Buy Now\", \"pinnedField\": \"HEADLINE_1\"}`. Valid `pinnedField` values: `HEADLINE_1`, `HEADLINE_2`, `HEADLINE_3`.",
81
+ },
82
+ descriptions: {
83
+ type: "string[]",
84
+ label: "Descriptions",
85
+ description: "2–4 description assets. Each value is either plain text (max 90 characters) or a JSON object to pin to a specific slot: `{\"text\": \"Shop today\", \"pinnedField\": \"DESCRIPTION_1\"}`. Valid `pinnedField` values: `DESCRIPTION_1`, `DESCRIPTION_2`.",
86
+ },
87
+ finalUrls: {
88
+ type: "string[]",
89
+ label: "Final URLs",
90
+ description: "The landing page URL(s). At least one is required.",
91
+ },
92
+ finalMobileUrls: {
93
+ type: "string[]",
94
+ label: "Final Mobile URLs",
95
+ description: "The landing page URL(s) for mobile devices.",
96
+ optional: true,
97
+ },
98
+ trackingUrlTemplate: {
99
+ type: "string",
100
+ label: "Tracking URL Template",
101
+ description: "The URL template for constructing a tracking URL.",
102
+ optional: true,
103
+ },
104
+ finalUrlSuffix: {
105
+ type: "string",
106
+ label: "Final URL Suffix",
107
+ description: "The suffix appended to landing page URLs when serving.",
108
+ optional: true,
109
+ },
110
+ },
111
+ async run({ $ }) {
112
+ const {
113
+ googleAds,
114
+ accountId,
115
+ customerClientId,
116
+ adGroupId,
117
+ status,
118
+ headlines,
119
+ descriptions,
120
+ finalUrls,
121
+ finalMobileUrls,
122
+ trackingUrlTemplate,
123
+ finalUrlSuffix,
124
+ } = this;
125
+
126
+ if (!headlines || headlines.length < 3) {
127
+ throw new ConfigurationError(
128
+ "At least 3 **Headlines** are required for a Responsive Search Ad.",
129
+ );
130
+ }
131
+ if (headlines.length > 15) {
132
+ throw new ConfigurationError(
133
+ "A Responsive Search Ad supports a maximum of 15 **Headlines**.",
134
+ );
135
+ }
136
+ if (!descriptions || descriptions.length < 2) {
137
+ throw new ConfigurationError(
138
+ "At least 2 **Descriptions** are required for a Responsive Search Ad.",
139
+ );
140
+ }
141
+ if (descriptions.length > 4) {
142
+ throw new ConfigurationError(
143
+ "A Responsive Search Ad supports a maximum of 4 **Descriptions**.",
144
+ );
145
+ }
146
+ if (!finalUrls || finalUrls.length === 0) {
147
+ throw new ConfigurationError(
148
+ "At least one **Final URL** is required.",
149
+ );
150
+ }
151
+
152
+ const customerId = customerClientId ?? accountId;
153
+ const adGroup = `customers/${customerId}/adGroups/${adGroupId}`;
154
+
155
+ const operation = {
156
+ create: {
157
+ adGroup,
158
+ ...(status && {
159
+ status,
160
+ }),
161
+ ad: {
162
+ responsiveSearchAd: {
163
+ headlines: headlines.map(parseAsset),
164
+ descriptions: descriptions.map(parseAsset),
165
+ },
166
+ finalUrls,
167
+ ...(finalMobileUrls?.length && {
168
+ finalMobileUrls,
169
+ }),
170
+ ...(trackingUrlTemplate && {
171
+ trackingUrlTemplate,
172
+ }),
173
+ ...(finalUrlSuffix && {
174
+ finalUrlSuffix,
175
+ }),
176
+ },
177
+ },
178
+ };
179
+
180
+ const response = await googleAds.mutateAdGroupAd({
181
+ $,
182
+ accountId,
183
+ customerClientId,
184
+ data: {
185
+ operations: [
186
+ operation,
187
+ ],
188
+ },
189
+ });
190
+
191
+ const result = response?.results?.[0];
192
+ const id = result?.resourceName?.split("/").pop();
193
+
194
+ $.export(
195
+ "$summary",
196
+ `Successfully created responsive search ad${id
197
+ ? ` with ID \`${id}\``
198
+ : ""}.`,
199
+ );
200
+ return response;
201
+ },
202
+ };