@google-cloud/nodejs-common 2.0.5-alpha → 2.0.7-alpha
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.
- package/package.json +2 -1
- package/src/apis/analytics.js +1 -1
- package/src/apis/auth_client.js +33 -23
- package/src/apis/google_ads.js +1 -0
- package/src/apis/google_ads_api.js +1550 -0
- package/src/apis/index.js +12 -0
- package/src/components/utils.js +23 -0
|
@@ -0,0 +1,1550 @@
|
|
|
1
|
+
// Copyright 2024 Google Inc.
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @fileoverview Google Ads API Wrapper based on the NodeJS library complied
|
|
17
|
+
* from the Bazel generated files.
|
|
18
|
+
* @see https://github.com/googleapis/googleapis
|
|
19
|
+
* @see https://github.com/lushu/googleads-nodejs
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { Transform } = require('stream');
|
|
24
|
+
const lodash = require('lodash');
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
ConversionAdjustmentUploadServiceClient,
|
|
28
|
+
ConversionUploadServiceClient,
|
|
29
|
+
GoogleAdsServiceClient,
|
|
30
|
+
GoogleAdsFieldServiceClient,
|
|
31
|
+
OfflineUserDataJobServiceClient,
|
|
32
|
+
UserDataServiceClient,
|
|
33
|
+
UserListServiceClient,
|
|
34
|
+
protos: { google: { ads: { googleads } } },
|
|
35
|
+
} = require('google-ads-nodejs-client');
|
|
36
|
+
const {
|
|
37
|
+
common: {
|
|
38
|
+
Consent,
|
|
39
|
+
CustomerMatchUserListMetadata,
|
|
40
|
+
StoreSalesMetadata,
|
|
41
|
+
TransactionAttribute,
|
|
42
|
+
UserAttribute,
|
|
43
|
+
UserData,
|
|
44
|
+
UserIdentifier,
|
|
45
|
+
},
|
|
46
|
+
resources: {
|
|
47
|
+
GoogleAdsField,
|
|
48
|
+
OfflineUserDataJob,
|
|
49
|
+
UserList,
|
|
50
|
+
},
|
|
51
|
+
services: {
|
|
52
|
+
AddOfflineUserDataJobOperationsRequest,
|
|
53
|
+
AddOfflineUserDataJobOperationsResponse,
|
|
54
|
+
CallConversion,
|
|
55
|
+
ClickConversion,
|
|
56
|
+
ConversionAdjustment,
|
|
57
|
+
CreateOfflineUserDataJobRequest,
|
|
58
|
+
CustomVariable,
|
|
59
|
+
GoogleAdsRow,
|
|
60
|
+
MutateUserListsRequest,
|
|
61
|
+
MutateUserListsResponse,
|
|
62
|
+
OfflineUserDataJobOperation,
|
|
63
|
+
RunOfflineUserDataJobRequest,
|
|
64
|
+
SearchGoogleAdsFieldsRequest,
|
|
65
|
+
SearchGoogleAdsRequest,
|
|
66
|
+
SearchGoogleAdsResponse,
|
|
67
|
+
UploadCallConversionsRequest,
|
|
68
|
+
UploadCallConversionsResponse,
|
|
69
|
+
UploadClickConversionsRequest,
|
|
70
|
+
UploadClickConversionsResponse,
|
|
71
|
+
UploadConversionAdjustmentsRequest,
|
|
72
|
+
UploadConversionAdjustmentsResponse,
|
|
73
|
+
UploadUserDataRequest,
|
|
74
|
+
UploadUserDataResponse,
|
|
75
|
+
UserDataOperation,
|
|
76
|
+
},
|
|
77
|
+
errors: {
|
|
78
|
+
GoogleAdsError,
|
|
79
|
+
GoogleAdsFailure,
|
|
80
|
+
},
|
|
81
|
+
enums: {
|
|
82
|
+
ConsentStatusEnum: { ConsentStatus },
|
|
83
|
+
OfflineUserDataJobFailureReasonEnum: { OfflineUserDataJobFailureReason },
|
|
84
|
+
OfflineUserDataJobTypeEnum: { OfflineUserDataJobType },
|
|
85
|
+
OfflineUserDataJobStatusEnum: { OfflineUserDataJobStatus },
|
|
86
|
+
UserIdentifierSourceEnum: { UserIdentifierSource },
|
|
87
|
+
UserListMembershipStatusEnum: { UserListMembershipStatus },
|
|
88
|
+
UserListTypeEnum: { UserListType },
|
|
89
|
+
CustomerMatchUploadKeyTypeEnum: { CustomerMatchUploadKeyType },
|
|
90
|
+
},
|
|
91
|
+
} = googleads.v16;
|
|
92
|
+
|
|
93
|
+
const AuthClient = require('./auth_client.js');
|
|
94
|
+
const {
|
|
95
|
+
getLogger,
|
|
96
|
+
BatchResult,
|
|
97
|
+
extractObject,
|
|
98
|
+
changeObjectNamingFromSnakeToLowerCamel,
|
|
99
|
+
} = require('../components/utils.js');
|
|
100
|
+
|
|
101
|
+
/** @type {!ReadonlyArray<string>} */
|
|
102
|
+
const API_SCOPES = Object.freeze(['https://www.googleapis.com/auth/adwords']);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* List of properties that will be taken from the data file as elements of a
|
|
106
|
+
* conversion or a conversion adjustment.
|
|
107
|
+
* @const {string: Array<string>}
|
|
108
|
+
*/
|
|
109
|
+
const CONVERSION_FIELDS = {};
|
|
110
|
+
/**
|
|
111
|
+
* @see CallConversion
|
|
112
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CallConversion
|
|
113
|
+
*/
|
|
114
|
+
CONVERSION_FIELDS.CALL = [
|
|
115
|
+
'callerId',
|
|
116
|
+
'consent',
|
|
117
|
+
'callStartDateTime',
|
|
118
|
+
'conversionAction',
|
|
119
|
+
'conversionDateTime',
|
|
120
|
+
'conversionValue',
|
|
121
|
+
'currencyCode',
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* @see ClickConversion
|
|
125
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
126
|
+
*/
|
|
127
|
+
CONVERSION_FIELDS.CLICK = [
|
|
128
|
+
'gbraid',
|
|
129
|
+
'wbraid',
|
|
130
|
+
'gclid',
|
|
131
|
+
'externalAttributionData',
|
|
132
|
+
'conversionEnvironment',
|
|
133
|
+
'consent',
|
|
134
|
+
'gclid',
|
|
135
|
+
'conversionAction',
|
|
136
|
+
'conversionDateTime',
|
|
137
|
+
'conversionValue',
|
|
138
|
+
'currencyCode',
|
|
139
|
+
'orderId',
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* @see ConversionAdjustment
|
|
143
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ConversionAdjustment
|
|
144
|
+
*/
|
|
145
|
+
CONVERSION_FIELDS.ADJUSTMENT = [
|
|
146
|
+
'gclidDateTimePair',
|
|
147
|
+
'adjustmentType',
|
|
148
|
+
'restatementValue',
|
|
149
|
+
'orderId',
|
|
150
|
+
'conversionAction',
|
|
151
|
+
'adjustmentDateTime',
|
|
152
|
+
'userAgent',
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Additional attributes in user data for store sales data or customer match.
|
|
157
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
158
|
+
* @type {Array<string>}
|
|
159
|
+
*/
|
|
160
|
+
const USERDATA_ADDITIONAL_ATTRIBUTES = [
|
|
161
|
+
'transactionAttribute', // Attribute of the store sales transaction.
|
|
162
|
+
'userAttribute', // Only be used with CUSTOMER_MATCH_WITH_ATTRIBUTES job type.
|
|
163
|
+
'consent',
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Kinds of UserIdentifier for different services.
|
|
168
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
169
|
+
* @see UserIdentifier
|
|
170
|
+
* @type {Array<string>}
|
|
171
|
+
*/
|
|
172
|
+
const IDENTIFIERS = {};
|
|
173
|
+
IDENTIFIERS.CUSTOMER_MATCH = [
|
|
174
|
+
'hashedEmail',
|
|
175
|
+
'hashedPhoneNumber',
|
|
176
|
+
'mobileId',
|
|
177
|
+
'thirdPartyUserId',
|
|
178
|
+
'addressInfo',
|
|
179
|
+
];
|
|
180
|
+
IDENTIFIERS.STORE_SALES = [
|
|
181
|
+
'hashedEmail',
|
|
182
|
+
'hashedPhoneNumber',
|
|
183
|
+
'thirdPartyUserId',
|
|
184
|
+
'addressInfo',
|
|
185
|
+
];
|
|
186
|
+
IDENTIFIERS.CLICK_CONVERSION = [
|
|
187
|
+
'hashedEmail',
|
|
188
|
+
'hashedPhoneNumber',
|
|
189
|
+
];
|
|
190
|
+
IDENTIFIERS.CONVERSION_ADJUSTMENT = [
|
|
191
|
+
'hashedEmail',
|
|
192
|
+
'hashedPhoneNumber',
|
|
193
|
+
'addressInfo',
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Maximum number of user identifiers in single UserData.
|
|
198
|
+
* @see https://ads-developers.googleblog.com/2021/10/userdata-enforcement-in-google-ads-api.html
|
|
199
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/v15/ClickConversion
|
|
200
|
+
* @type {number}
|
|
201
|
+
*/
|
|
202
|
+
const MAX_IDENTIFIERS = {
|
|
203
|
+
USER_DATA: 20,
|
|
204
|
+
CONVERSION: 5,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Configuration for uploading click conversions, call converions or conversion
|
|
209
|
+
* adjustments for Google Ads, includes:
|
|
210
|
+
* gclid, conversionAction, conversionDateTime, conversionValue,
|
|
211
|
+
* currencyCode, orderId, externalAttributionData,
|
|
212
|
+
* callerId, callStartDateTime,
|
|
213
|
+
* adjustmentType, adjustmentDateTime, userAgent, gclidDateTimePair, etc.
|
|
214
|
+
* @see CONVERSION_FIELDS
|
|
215
|
+
*
|
|
216
|
+
* Other properties that will be used to build the conversions but not picked by
|
|
217
|
+
* the value directly including:
|
|
218
|
+
* 1. 'userIdentifierSource', source of the user identifier. If there is user
|
|
219
|
+
* identifiers information in the conversion, this property should be set as
|
|
220
|
+
* 'FIRST_PARTY'.
|
|
221
|
+
* @see IDENTIFIERS
|
|
222
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier?hl=en
|
|
223
|
+
* 2. 'customVariableTags', the tags of conversion custom variables. To upload
|
|
224
|
+
* custom variables, 'conversionCustomVariableId' is required rather than the
|
|
225
|
+
* 'tag'. So the invoker is expected to use the function
|
|
226
|
+
* 'getConversionCustomVariableId' to get the ids and pass in as a
|
|
227
|
+
* map(customVariables) of <tag, id> pairs before uploading conversions.
|
|
228
|
+
*
|
|
229
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/ClickConversion
|
|
230
|
+
* @typedef {{
|
|
231
|
+
* externalAttributionData: (GoogleAdsApi.ExternalAttributionData|undefined),
|
|
232
|
+
* cartData: (object|undefined),
|
|
233
|
+
* gclid: (string|undefined),
|
|
234
|
+
* callerId: (string|undefined),
|
|
235
|
+
* callStartDateTime: (string|undefined),
|
|
236
|
+
* conversionAction: string,
|
|
237
|
+
* conversionDateTime: string,
|
|
238
|
+
* conversionValue: number,
|
|
239
|
+
* currencyCode:(string|undefined),
|
|
240
|
+
* orderId: (string|undefined),
|
|
241
|
+
* adjustmentType: (string|undefined),
|
|
242
|
+
* adjustmentDateTime: (!ConversionAdjustmentType|undefined),
|
|
243
|
+
* userAgent: (string|undefined),
|
|
244
|
+
* userIdentifierSource:(!UserIdentifierSource|undefined),
|
|
245
|
+
* customVariableTags:(!Array<string>|undefined),
|
|
246
|
+
* customVariables:(!Object<string,string>|undefined),
|
|
247
|
+
* consent: (!Consent),
|
|
248
|
+
* }}
|
|
249
|
+
*/
|
|
250
|
+
let ConversionConfig;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Configuration for uploading customer match to Google Ads, includes:
|
|
254
|
+
* customerId, loginCustomerId, listId and operation.
|
|
255
|
+
* If audience listId is not present, 'listName' and 'uploadKeyType' need to
|
|
256
|
+
* be there so they can be used to create a customer match user list.
|
|
257
|
+
* operation must be one of 'create' or 'remove'.
|
|
258
|
+
* Should not include `userIdentifierSource` based on:
|
|
259
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
260
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
261
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUploadKeyTypeEnum.CustomerMatchUploadKeyType
|
|
262
|
+
* @typedef {{
|
|
263
|
+
* customerId: (string|number),
|
|
264
|
+
* loginCustomerId: (string|number),
|
|
265
|
+
* listId: (string|undefined),
|
|
266
|
+
* listName: (string|undefined),
|
|
267
|
+
* uploadKeyType: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
|
|
268
|
+
* operation: ('create'|'remove'),
|
|
269
|
+
* consent: (!Consent),
|
|
270
|
+
* }}
|
|
271
|
+
*/
|
|
272
|
+
let CustomerMatchConfig;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Configuration for offline user data job, includes:
|
|
276
|
+
* customerId, loginCustomerId, listId, operation and type.
|
|
277
|
+
* 'operation' should be one of the two: 'create' or 'remove',
|
|
278
|
+
* 'type' is OfflineUserDataJobType, it can be 'CUSTOMER_MATCH_USER_LIST',
|
|
279
|
+
* 'CUSTOMER_MATCH_WITH_ATTRIBUTES' or 'STORE_SALES_UPLOAD_FIRST_PARTY'.
|
|
280
|
+
* For job type 'CUSTOMER_MATCH_USER_LIST', if `listId` is not present,
|
|
281
|
+
* 'listName' and 'uploadKeyType' need to be there so they can be used to
|
|
282
|
+
* create a customer match user list.
|
|
283
|
+
* For job type 'CUSTOMER_MATCH_WITH_ATTRIBUTES', 'user_attribute' can be used
|
|
284
|
+
* to store shared additional user attributes.
|
|
285
|
+
* For job type 'STORE_SALES_UPLOAD_FIRST_PARTY', `storeSalesMetadata` is
|
|
286
|
+
* required to offer StoreSalesMetadata. Besides that, for the store sales data,
|
|
287
|
+
* common data (e.g. `currencyCode`, `conversionAction`) in
|
|
288
|
+
* `transactionAttribute` can be put here as well.
|
|
289
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/OfflineUserDataJob
|
|
290
|
+
* @typedef {{
|
|
291
|
+
* customerId: (string|number),
|
|
292
|
+
* loginCustomerId: (string|number),
|
|
293
|
+
* listId: (string|undefined),
|
|
294
|
+
* listName: (string|undefined),
|
|
295
|
+
* uploadKeyType: ('CONTACT_INFO'|'CRM_ID'|'MOBILE_ADVERTISING_ID'|undefined),
|
|
296
|
+
* operation: ('create'|'remove'),
|
|
297
|
+
* type: !OfflineUserDataJobType,
|
|
298
|
+
* storeSalesMetadata: (undefined|StoreSalesMetadata),
|
|
299
|
+
* transactionAttribute: (undefined|TransactionAttribute),
|
|
300
|
+
* userAttribute: (undefined|UserAttribute),
|
|
301
|
+
* userIdentifierSource: (!UserIdentifierSource|undefined),
|
|
302
|
+
* consent: (!Consent),
|
|
303
|
+
* }}
|
|
304
|
+
*/
|
|
305
|
+
let OfflineUserDataJobConfig;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Configuration for uploading customer match data for Google Ads.
|
|
309
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
310
|
+
* @typedef {{
|
|
311
|
+
* hashedEmail: (string|Array<string>|undefined),
|
|
312
|
+
* hashedPhoneNumber: (string|Array<string>|undefined),
|
|
313
|
+
* mobileId: (string|Array<string>|undefined),
|
|
314
|
+
* thirdPartyUserId: (string|Array<string>|undefined),
|
|
315
|
+
* addressInfo: (GoogleAdsApi.OfflineUserAddressInfo|undefined),
|
|
316
|
+
* userIdentifierSource: (!UserIdentifierSource|undefined),
|
|
317
|
+
* }}
|
|
318
|
+
*/
|
|
319
|
+
let CustomerMatchRecord;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Google Ads API class based on Google Ads API library.
|
|
323
|
+
*/
|
|
324
|
+
class GoogleAdsApi {
|
|
325
|
+
/**
|
|
326
|
+
* Note: Rate limits is set by the access level of Developer token.
|
|
327
|
+
* @param {string} developerToken Developer token to access the API.
|
|
328
|
+
* @param {boolean=} debugMode This is used to set ONLY validate conversions
|
|
329
|
+
* but not real uploading.
|
|
330
|
+
* @param {!Object<string,string>=} env The environment object to hold env
|
|
331
|
+
* variables.
|
|
332
|
+
*/
|
|
333
|
+
constructor(developerToken, debugMode = false, env = process.env) {
|
|
334
|
+
this.developerToken = developerToken;
|
|
335
|
+
this.debugMode = debugMode;
|
|
336
|
+
this.authClient = new AuthClient(API_SCOPES, env);
|
|
337
|
+
this.logger = getLogger('API.ADS.N');
|
|
338
|
+
this.logger.info(
|
|
339
|
+
`Init ${this.constructor.name} with Debug Mode?`, this.debugMode);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Gets a Google Ads report based on a Google Ads Query Language(GAQL) query
|
|
344
|
+
* with pagination automatically handled. It returns an array of
|
|
345
|
+
* `GoogleAdsRow` without other fields in the object
|
|
346
|
+
* `SearchGoogleAdsResponse`, e.g.`fieldMask`.
|
|
347
|
+
* The `GoogleAdsRow` objects have `null` value for those unselected fields.
|
|
348
|
+
* The enum fields in the query are present as index number.
|
|
349
|
+
* @param {string} customerId
|
|
350
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
351
|
+
* @param {string} query A Google Ads Query Language query.
|
|
352
|
+
* @return {!Array<!GoogleAdsRow>}
|
|
353
|
+
*/
|
|
354
|
+
async getReport(customerId, loginCustomerId, query) {
|
|
355
|
+
const request = new SearchGoogleAdsRequest({
|
|
356
|
+
query,
|
|
357
|
+
customerId: this.getCleanCid_(customerId),
|
|
358
|
+
});
|
|
359
|
+
return this.getReport_(request, loginCustomerId, true);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Gets a page of Google Ads report based on a GAQL query.
|
|
364
|
+
* It returns a `SearchGoogleAdsResponse` object with the property `results`
|
|
365
|
+
* which contains an array of `GoogleAdsRow`. The returned object also
|
|
366
|
+
* contains `nextPageToken`.
|
|
367
|
+
* Note, the `GoogleAdsRow` objects have `null` value for those unselected
|
|
368
|
+
* fields. The `fieldMask` in the response CAN be used to clear the result
|
|
369
|
+
* objects.
|
|
370
|
+
* The enum fields in the query are present as index number.
|
|
371
|
+
* @param {string} customerId
|
|
372
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
373
|
+
* @param {string} query A Google Ads Query Language query.
|
|
374
|
+
* @param {object=} options Options for `SearchGoogleAdsRequest`. It can
|
|
375
|
+
* contain `pageSize` whose default value is 10000 and `pageToken` if it
|
|
376
|
+
* is going to get the next page.
|
|
377
|
+
* @return {!SearchGoogleAdsResponse}
|
|
378
|
+
*/
|
|
379
|
+
async getPaginatedReport(customerId, loginCustomerId, query, options = {}) {
|
|
380
|
+
const request = new SearchGoogleAdsRequest(
|
|
381
|
+
Object.assign({
|
|
382
|
+
query,
|
|
383
|
+
customerId: this.getCleanCid_(customerId),
|
|
384
|
+
pageSize: 10000,
|
|
385
|
+
}, options)
|
|
386
|
+
);
|
|
387
|
+
return this.getReport_(request, loginCustomerId);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Gets Google Ads report based on the parameters. The Google Ads API returns
|
|
392
|
+
* an array with three element:
|
|
393
|
+
* 1. Array<GoogleAdsRow>
|
|
394
|
+
* 2. SearchGoogleAdsRequest | null,
|
|
395
|
+
* 3. SearchGoogleAdsResponse
|
|
396
|
+
* When the `autoPaginate` is set to be true, only the first element is
|
|
397
|
+
* available, so the returned value is an array of GoogleAdsRow, otherwise
|
|
398
|
+
* the returned value is a `SearchGoogleAdsResponse` object.
|
|
399
|
+
* @param {!SearchGoogleAdsRequest} request
|
|
400
|
+
* @param {string} loginCustomerId
|
|
401
|
+
* @param {boolean=} autoPaginate
|
|
402
|
+
* @return {!Array<(!GoogleAdsRow>|!SearchGoogleAdsResponse)}
|
|
403
|
+
* @private
|
|
404
|
+
*/
|
|
405
|
+
async getReport_(request, loginCustomerId, autoPaginate = false) {
|
|
406
|
+
const client = await this.getGoogleAdsServiceClient_();
|
|
407
|
+
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
408
|
+
callOptions.autoPaginate = autoPaginate;
|
|
409
|
+
const response = await client.search(request, callOptions);
|
|
410
|
+
const result = response[autoPaginate ? 0 : 2];
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
//TODO: Test a big report see how the event 'data' is triggered
|
|
415
|
+
/**
|
|
416
|
+
* Gets stream report of a given Customer account. The stream will send
|
|
417
|
+
* `SearchGoogleAdsResponse` objects with the event 'data'.
|
|
418
|
+
* @param {string} customerId
|
|
419
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
420
|
+
* @param {string} query
|
|
421
|
+
* @return {!StreamProxy}
|
|
422
|
+
*/
|
|
423
|
+
async streamReport(customerId, loginCustomerId, query) {
|
|
424
|
+
const client = await this.getGoogleAdsServiceClient_();
|
|
425
|
+
const request = new SearchGoogleAdsRequest({
|
|
426
|
+
query,
|
|
427
|
+
customerId: this.getCleanCid_(customerId),
|
|
428
|
+
});
|
|
429
|
+
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
430
|
+
const response = await client.searchStream(request, callOptions);
|
|
431
|
+
return response;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Gets the report with only selected fields.
|
|
436
|
+
* Based on the `fieldMask` in the `SearchGoogleAdsResponse` to filter out
|
|
437
|
+
* selected fields of the report and returns the `GoogleAdsRows` in the
|
|
438
|
+
* `results` in JSON format strings with the delimit of a line breaker.
|
|
439
|
+
* @param {string} customerId
|
|
440
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
441
|
+
* @param {string} query
|
|
442
|
+
* @return {!Promise<string>}
|
|
443
|
+
*/
|
|
444
|
+
async cleanedStreamReport(customerId, loginCustomerId, query) {
|
|
445
|
+
const cleanReportStream = new Transform({
|
|
446
|
+
writableObjectMode: true,
|
|
447
|
+
transform(chunk, encoding, callback) {
|
|
448
|
+
const { fieldMask: { paths } } = chunk;
|
|
449
|
+
const extractor = extractObject(paths);
|
|
450
|
+
// Add a line break after each chunk to keep files in proper format.
|
|
451
|
+
const data = chunk.results.map(extractor).map(JSON.stringify).join('\n')
|
|
452
|
+
+ '\n';
|
|
453
|
+
callback(null, data);
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
const stream = await this.streamReport(customerId, loginCustomerId, query);
|
|
457
|
+
return stream.pipe(cleanReportStream);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Returns resources information from Google Ads API. see:
|
|
462
|
+
* https://developers.google.com/google-ads/api/docs/concepts/field-service
|
|
463
|
+
* Note, it looks like this function doesn't check the CID, just using
|
|
464
|
+
* developer token and OAuth.
|
|
465
|
+
* @param {string|number} loginCustomerId Login customer account ID.
|
|
466
|
+
* @param {Array<string>} adFields Array of Ad fields.
|
|
467
|
+
* @param {Array<string>} metadata Select fields, default values are:
|
|
468
|
+
* name, data_type, is_repeated, type_url.
|
|
469
|
+
* @return {!Promise<!Array<GoogleAdsField>>}
|
|
470
|
+
*/
|
|
471
|
+
async searchMetaData(loginCustomerId, adFields, metadata = [
|
|
472
|
+
'name', 'data_type', 'is_repeated', 'type_url',]) {
|
|
473
|
+
const client = await this.getGoogleAdsFieldServiceClient_();
|
|
474
|
+
const selectClause = metadata.join(',');
|
|
475
|
+
const fields = adFields.join('","');
|
|
476
|
+
const query = `SELECT ${selectClause} WHERE name IN ("${fields}")`;
|
|
477
|
+
const request = new SearchGoogleAdsFieldsRequest({ query });
|
|
478
|
+
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
479
|
+
const [results] = await client.searchGoogleAdsFields(request, callOptions);
|
|
480
|
+
return results;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Returns the function to send out a request to Google Ads API with a batch
|
|
485
|
+
* of call conversions.
|
|
486
|
+
* @param {string} customerId
|
|
487
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
488
|
+
* @param {!ConversionConfig} adsConfig Default call conversion params
|
|
489
|
+
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
490
|
+
* Google Ads API.
|
|
491
|
+
*/
|
|
492
|
+
getUploadCallConversionFn(customerId, loginCustomerId, adsConfig) {
|
|
493
|
+
return this.getUploadConversionFnBase_(customerId, loginCustomerId,
|
|
494
|
+
adsConfig, 'uploadCallConversions', 'callerId');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Returns the function to send out a request to Google Ads API with a batch
|
|
499
|
+
* of click conversions.
|
|
500
|
+
* @param {string} customerId
|
|
501
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
502
|
+
* @param {!ConversionConfig} adsConfig Default click conversion params
|
|
503
|
+
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
504
|
+
* Google Ads API.
|
|
505
|
+
*/
|
|
506
|
+
getUploadClickConversionFn(customerId, loginCustomerId, adsConfig) {
|
|
507
|
+
return this.getUploadConversionFnBase_(customerId, loginCustomerId,
|
|
508
|
+
adsConfig, 'uploadClickConversions', 'gclid');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Returns the function to send out a request to Google Ads API with a batch
|
|
513
|
+
* of conversion adjustments.
|
|
514
|
+
* @param {string} customerId
|
|
515
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
516
|
+
* @param {!ConversionConfig} adsConfig Default conversion adjustments
|
|
517
|
+
* params.
|
|
518
|
+
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
519
|
+
* Google Ads API.
|
|
520
|
+
*/
|
|
521
|
+
getUploadConversionAdjustmentFn(customerId, loginCustomerId, adsConfig) {
|
|
522
|
+
return this.getUploadConversionFnBase_(customerId, loginCustomerId,
|
|
523
|
+
adsConfig, 'uploadConversionAdjustments', 'orderId');
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Returns the function to send call conversions, click conversions or
|
|
528
|
+
* conversion adjustment (enhanced conversions).
|
|
529
|
+
*
|
|
530
|
+
* Google Ads API conversion related services (ConversionUploadService,
|
|
531
|
+
* ConversionAdjustmentUploadService) support `partial_failure` which will
|
|
532
|
+
* return `partialFailureError`(Status) when 'some kinds of errors' happen,
|
|
533
|
+
* e.g. wrong conversion Id, wrong gclid, etc.
|
|
534
|
+
* `Status` has a property `message` that has the general information of the
|
|
535
|
+
* error, however, the detailed information (e.g. the failed conversions and
|
|
536
|
+
* their reasons) lies in the property named `datails` (an array of
|
|
537
|
+
* `GoogleAdsFailure`). Each `GoogleAdsFailure` is related to a failed
|
|
538
|
+
* conversion. The function `extraFailedLines_` is used to extract the
|
|
539
|
+
* details.
|
|
540
|
+
*
|
|
541
|
+
* There is also another kind of error that will be throw directly. Sometimes,
|
|
542
|
+
* this kind of error is a wrapped `GoogleAdsFailure`, e.g. wrong CID.
|
|
543
|
+
* The function `updateBatchResultWithError` for this kind of error.
|
|
544
|
+
* @see extraFailedLines_
|
|
545
|
+
* @see updateBatchResultWithError
|
|
546
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/google.rpc#google.rpc.Status
|
|
547
|
+
* @param {string} customerId
|
|
548
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
549
|
+
* @param {!ConversionConfig} conversionConfig Default conversion parameters.
|
|
550
|
+
* @param {string} functionName The name of sending converions function, could
|
|
551
|
+
* be `uploadClickConversions`, `uploadCallConversions` or
|
|
552
|
+
* `uploadConversionAdjustments`.
|
|
553
|
+
* @param {string} propertyForDebug The name of property for debug info.
|
|
554
|
+
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
555
|
+
* Google Ads API.
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
getUploadConversionFnBase_(customerId, loginCustomerId, conversionConfig,
|
|
559
|
+
functionName, propertyForDebug) {
|
|
560
|
+
/** @type {!ConversionConfig} */
|
|
561
|
+
const adsConfig = this.getCamelConfig_(conversionConfig);
|
|
562
|
+
adsConfig.customerId = this.getCleanCid_(customerId);
|
|
563
|
+
adsConfig.loginCustomerId = this.getCleanCid_(loginCustomerId);
|
|
564
|
+
/**
|
|
565
|
+
* Sends a batch of hits to Google Ads API.
|
|
566
|
+
* @param {!Array<string>} lines Data for single request. It should be
|
|
567
|
+
* guaranteed that it doesn't exceed quota limitation.
|
|
568
|
+
* @param {string} batchId The tag for log.
|
|
569
|
+
* @return {!BatchResult}
|
|
570
|
+
*/
|
|
571
|
+
return async (lines, batchId) => {
|
|
572
|
+
/** @const {BatchResult} */
|
|
573
|
+
const batchResult = {
|
|
574
|
+
result: true,
|
|
575
|
+
numberOfLines: lines.length,
|
|
576
|
+
};
|
|
577
|
+
try {
|
|
578
|
+
const response = await this[functionName](lines, adsConfig);
|
|
579
|
+
const { results, partialFailureError: failed, jobId } = response;
|
|
580
|
+
if (this.logger.isDebugEnabled()) {
|
|
581
|
+
const id = results.map((conversion) => conversion[propertyForDebug]);
|
|
582
|
+
this.logger.debug(`Uploaded ${propertyForDebug}:`, id);
|
|
583
|
+
}
|
|
584
|
+
if (failed) {
|
|
585
|
+
this.logger.info(`Job[${jobId}] partialFailureError:`, failed.message);
|
|
586
|
+
const failures = failed.details.map(
|
|
587
|
+
({ value }) => GoogleAdsFailure.decode(value));
|
|
588
|
+
this.extraFailedLines_(batchResult, failures, lines, 0);
|
|
589
|
+
}
|
|
590
|
+
return batchResult;
|
|
591
|
+
} catch (error) {
|
|
592
|
+
this.logger.error(
|
|
593
|
+
`Error in ${functionName} batch: ${batchId}`, error);
|
|
594
|
+
this.updateBatchResultWithError(batchResult, error, lines, 0);
|
|
595
|
+
return batchResult;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Updates the BatchResult based on errors.
|
|
602
|
+
*
|
|
603
|
+
* There are 2 types of errors here:
|
|
604
|
+
* 1. Normal JavaScript Error object. It happens outside of the communication
|
|
605
|
+
* with Google Ads API and fails the whole process, so there is no detailed
|
|
606
|
+
* failed lines.
|
|
607
|
+
* 2. GoogleAdsFailure (which is wrapped in error.metadata in this library).
|
|
608
|
+
* GoogleAdsFailure is a Google Ads' own error object which has an array of
|
|
609
|
+
* GoogleAdsError (property name 'errors'). GoogleAdsError contains
|
|
610
|
+
* the detailed failed data if it is a "partial failure". For example, a wrong
|
|
611
|
+
* encoded user identifier is a "partial failure", while a wrong user list id
|
|
612
|
+
* is not.
|
|
613
|
+
* GoogleAdsFailure: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsFailure
|
|
614
|
+
* GoogleAdsError: https://developers.google.com/google-ads/api/reference/rpc/latest/GoogleAdsError
|
|
615
|
+
*
|
|
616
|
+
* For Customer Match data uploading, there is not partial failure, so the
|
|
617
|
+
* result can be either succeeded or a thrown error. The thrown error will be
|
|
618
|
+
* used to build the returned result here.
|
|
619
|
+
* For Conversions uploading (partial failure enabled), if there is an error
|
|
620
|
+
* fails the whole process, the error will also be thrown and handled here.
|
|
621
|
+
* Otherwise, the errors will be wrapped in the response as the property named
|
|
622
|
+
* 'partial_failure_error' which contains an array of GoogleAdsFailure. This
|
|
623
|
+
* kind of failure doesn't fail the process, while line-errors can be
|
|
624
|
+
* extracted from it.
|
|
625
|
+
* For more information, see the function `extraFailedLines_`.
|
|
626
|
+
*
|
|
627
|
+
* An example of 'GoogleAdsFailure' is:
|
|
628
|
+
* GoogleAdsFailure {
|
|
629
|
+
* errors: [
|
|
630
|
+
* GoogleAdsError {
|
|
631
|
+
* error_code: ErrorCode { offline_user_data_job_error: 25 },
|
|
632
|
+
* message: 'The SHA256 encoded value is malformed.',
|
|
633
|
+
* location: ErrorLocation {
|
|
634
|
+
* fieldPathElements: [
|
|
635
|
+
* FieldPathElement { fieldName: 'operations', index: 0 },
|
|
636
|
+
* FieldPathElement { fieldName: 'create' },
|
|
637
|
+
* FieldPathElement { fieldName: 'user_identifiers', index: 0 },
|
|
638
|
+
* FieldPathElement { fieldName: 'hashed_email' }
|
|
639
|
+
* ]
|
|
640
|
+
* }
|
|
641
|
+
* }
|
|
642
|
+
* ],
|
|
643
|
+
* requestId: 'xxxxxxxxxxxxxxx'
|
|
644
|
+
* }
|
|
645
|
+
*
|
|
646
|
+
* @param {!BatchResult} batchResult
|
|
647
|
+
* @param {(!GoogleAdsFailure|!Error)} error
|
|
648
|
+
* @param {!Array<string>} lines The original input data.
|
|
649
|
+
* @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
|
|
650
|
+
* 'field_path_elements'. This is used to get the original line related to
|
|
651
|
+
* this GoogleAdsError.
|
|
652
|
+
*/
|
|
653
|
+
updateBatchResultWithError(batchResult, error, lines, fieldPathIndex) {
|
|
654
|
+
batchResult.result = false;
|
|
655
|
+
if (error.metadata) {
|
|
656
|
+
const failures = this.getGoogleAdsFailures_(error.metadata);
|
|
657
|
+
if (failures.length > 0) {
|
|
658
|
+
debugGoogleAdsFailure(failures, lines)
|
|
659
|
+
this.extraFailedLines_(batchResult, failures, lines, fieldPathIndex);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.logger.warn(
|
|
663
|
+
'Got an error with metadata but no GoogleAdsFailure in it', error);
|
|
664
|
+
}
|
|
665
|
+
this.logger.warn('Got an error without metadata', error);
|
|
666
|
+
batchResult.errors = [error.message || error.toString()];
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Returns the GoogleAdsFailure from a wrapped gRPC error's metadata.
|
|
671
|
+
* @param {Metadata} metadata
|
|
672
|
+
* @return {!Array<!GoogleAdsFailure>}
|
|
673
|
+
*/
|
|
674
|
+
getGoogleAdsFailures_(metadata) {
|
|
675
|
+
const errors = metadata.getMap();
|
|
676
|
+
const googleAdsFailures = Object.keys(errors)
|
|
677
|
+
.filter((key) => key.indexOf('googleadsfailure') > -1)
|
|
678
|
+
.map((key) => GoogleAdsFailure.decode(errors[key]));
|
|
679
|
+
return googleAdsFailures;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Extras failed lines based on the Array of GoogleAdsFailure.
|
|
685
|
+
*
|
|
686
|
+
* Different errors have different 'fieldPathIndex' which is the index of
|
|
687
|
+
* failed lines in original input data (an array of a string).
|
|
688
|
+
*
|
|
689
|
+
* For conversions, the ErrorLocation is like:
|
|
690
|
+
* ErrorLocation {
|
|
691
|
+
* fieldPathElements: [
|
|
692
|
+
* FieldPathElement { fieldName: 'operations', index: 0 },
|
|
693
|
+
* FieldPathElement { fieldName: 'create' }
|
|
694
|
+
* ]
|
|
695
|
+
* }
|
|
696
|
+
*
|
|
697
|
+
* For customer match upload, the ErrorLocation is like:
|
|
698
|
+
* ErrorLocation {
|
|
699
|
+
* fieldPathElements: [
|
|
700
|
+
* FieldPathElement { fieldName: 'operations', index: 0 },
|
|
701
|
+
* FieldPathElement { fieldName: 'create' },
|
|
702
|
+
* FieldPathElement { fieldName: 'userIdentifiers', index: 0 },
|
|
703
|
+
* FieldPathElement { fieldName: 'hashedEmail' }
|
|
704
|
+
* ]
|
|
705
|
+
* }
|
|
706
|
+
*
|
|
707
|
+
* The fieldPathElements can help to locate the details of the data cause the
|
|
708
|
+
* error.
|
|
709
|
+
* Currently, just leverage the first `FieldPathElement` as it is enough to
|
|
710
|
+
* locate the origin line in the input data.
|
|
711
|
+
* Note, this can be used to extract the precise information of the root cause
|
|
712
|
+
* of the error.
|
|
713
|
+
*
|
|
714
|
+
* With this we can get errors and failed lines. The function will set
|
|
715
|
+
* following for the given BatchResult object:
|
|
716
|
+
* result - false
|
|
717
|
+
* errors - de-duplicated error reasons
|
|
718
|
+
* failedLines - failed lines, an array of string. Without the reason of
|
|
719
|
+
* failure.
|
|
720
|
+
* groupedFailed - a hashmap of failed the lines. The key is the reason, the
|
|
721
|
+
* value is the array of failed lines due to this reason.
|
|
722
|
+
* @param {!BatchResult} batchResult
|
|
723
|
+
* @param {!Array<!GoogleAdsFailure>} failures
|
|
724
|
+
* @param {!Array<string>} lines The original input data.
|
|
725
|
+
* @param {number} fieldPathIndex The index of 'FieldPathElement' in the array
|
|
726
|
+
* 'fieldPathElements'. This is used to get the original line related to
|
|
727
|
+
* this GoogleAdsError.
|
|
728
|
+
* @private
|
|
729
|
+
*/
|
|
730
|
+
extraFailedLines_(batchResult, failures, lines, fieldPathIndex) {
|
|
731
|
+
batchResult.result = false;
|
|
732
|
+
batchResult.failedLines = [];
|
|
733
|
+
batchResult.groupedFailed = {};
|
|
734
|
+
const errors = new Set();
|
|
735
|
+
failures.forEach((failure) => {
|
|
736
|
+
this.logger.error(`API requestId[${failure.requestId}]`,
|
|
737
|
+
failure.errors.map(({ message }) => message));
|
|
738
|
+
failure.errors.forEach(({ message, location }) => {
|
|
739
|
+
errors.add(message);
|
|
740
|
+
if (location && location.fieldPathElements[fieldPathIndex]) {
|
|
741
|
+
const { index } = location.fieldPathElements[fieldPathIndex];
|
|
742
|
+
if (index === null) {
|
|
743
|
+
this.logger.warn(`Unknown field path index: ${fieldPathIndex}`,
|
|
744
|
+
location.fieldPathElements);
|
|
745
|
+
} else {
|
|
746
|
+
const groupedFailed = batchResult.groupedFailed[message] || [];
|
|
747
|
+
const failedLine = lines[index];
|
|
748
|
+
batchResult.failedLines.push(failedLine);
|
|
749
|
+
groupedFailed.push(failedLine);
|
|
750
|
+
if (groupedFailed.length === 1) {
|
|
751
|
+
batchResult.groupedFailed[message] = groupedFailed;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
batchResult.errors = Array.from(errors);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Uploads call conversions to google ads account.
|
|
762
|
+
* In DEBUG mode, this function will only validate the conversions.
|
|
763
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/v15/CallConversion
|
|
764
|
+
* @param {!Array<string>} lines Data for single request. It should be
|
|
765
|
+
* guaranteed that it doesn't exceed quota limitation.
|
|
766
|
+
* @param {!ConversionConfig} config Default conversion parameters.
|
|
767
|
+
* @return {!Promise<!UploadCallConversionsResponse>}
|
|
768
|
+
*/
|
|
769
|
+
async uploadCallConversions(lines, config) {
|
|
770
|
+
const { customerId, loginCustomerId } = config;
|
|
771
|
+
this.logger.debug('Upload call conversions for:', config);
|
|
772
|
+
const conversions =
|
|
773
|
+
buildConversionJsonList(lines, config, CONVERSION_FIELDS.CALL);
|
|
774
|
+
const callConversions =
|
|
775
|
+
conversions.map((conversion) => new CallConversion(conversion));
|
|
776
|
+
const client = await this.getConversionUploadServiceClient_();
|
|
777
|
+
const request = new UploadCallConversionsRequest({
|
|
778
|
+
conversions: callConversions,
|
|
779
|
+
customerId,
|
|
780
|
+
validateOnly: this.debugMode, // when true makes no changes
|
|
781
|
+
partialFailure: true, // Will still create the non-failed entities
|
|
782
|
+
});
|
|
783
|
+
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
784
|
+
const [response] = await client.uploadCallConversions(request, callOptions);
|
|
785
|
+
return response;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Uploads click conversions to google ads account.
|
|
790
|
+
* In DEBUG mode, this function will only validate the conversions.
|
|
791
|
+
* @param {!Array<string>} lines Data for single request. It should be
|
|
792
|
+
* guaranteed that it doesn't exceed quota limitation.
|
|
793
|
+
* @param {!ConversionConfig} config Default conversion parameters.
|
|
794
|
+
* @return {!Promise<!UploadClickConversionsResponse>}
|
|
795
|
+
*/
|
|
796
|
+
async uploadClickConversions(lines, config) {
|
|
797
|
+
const { customerId, loginCustomerId } = config;
|
|
798
|
+
this.logger.debug('Upload click conversions for:', config);
|
|
799
|
+
const conversions = buildConversionJsonList(lines, config,
|
|
800
|
+
CONVERSION_FIELDS.CLICK, IDENTIFIERS.CLICK_CONVERSION, MAX_IDENTIFIERS.CONVERSION);
|
|
801
|
+
const clickConversions =
|
|
802
|
+
conversions.map((conversion) => new ClickConversion(conversion));
|
|
803
|
+
const client = await this.getConversionUploadServiceClient_();
|
|
804
|
+
const request = new UploadClickConversionsRequest({
|
|
805
|
+
conversions: clickConversions,
|
|
806
|
+
customerId,
|
|
807
|
+
validateOnly: this.debugMode, // when true makes no changes
|
|
808
|
+
partialFailure: true, // Will still create the non-failed entities
|
|
809
|
+
});
|
|
810
|
+
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
811
|
+
const [response] = await client.uploadClickConversions(request, callOptions);
|
|
812
|
+
return response;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Uploads conversion adjustments to google ads account.
|
|
817
|
+
* In DEBUG mode, this function will only validate the conversion adjustments.
|
|
818
|
+
* @param {!Array<string>} lines Data for single request. It should be
|
|
819
|
+
* guaranteed that it doesn't exceed quota limitation.
|
|
820
|
+
* @param {!ConversionConfig} config Default conversion parameters.
|
|
821
|
+
* @return {!Promise<!UploadConversionAdjustmentsResponse>}
|
|
822
|
+
*/
|
|
823
|
+
async uploadConversionAdjustments(lines, config) {
|
|
824
|
+
const { customerId, loginCustomerId } = config;
|
|
825
|
+
this.logger.debug('Upload conversion adjustments for:', config);
|
|
826
|
+
const conversions = buildConversionJsonList(
|
|
827
|
+
lines, config, IDENTIFIERS.CONVERSION_ADJUSTMENT, MAX_IDENTIFIERS.CONVERSION);
|
|
828
|
+
const conversionAdjustments =
|
|
829
|
+
conversions.map((conversion) => new ConversionAdjustment(conversion));
|
|
830
|
+
const client = await this.getConversionAdjustmentUploadServiceClient_();
|
|
831
|
+
const request = new UploadConversionAdjustmentsRequest({
|
|
832
|
+
conversionAdjustments,
|
|
833
|
+
customerId,
|
|
834
|
+
validateOnly: this.debugMode, // when true makes no changes
|
|
835
|
+
partialFailure: true, // Will still create the non-failed entities
|
|
836
|
+
});
|
|
837
|
+
const callOptions = this.getCallOptions_(loginCustomerId);
|
|
838
|
+
const [response] =
|
|
839
|
+
await client.uploadConversionAdjustments(request, callOptions);
|
|
840
|
+
return response;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Returns the id of Conversion Custom Variable with the given tag.
|
|
845
|
+
* @param {string} tag Custom Variable tag.
|
|
846
|
+
* @param {string} customerId
|
|
847
|
+
* @param {string} loginCustomerId Login customer account ID (Mcc Account id).
|
|
848
|
+
* @return {Promise<string|undefined>} Returns undefined if can't find tag.
|
|
849
|
+
*/
|
|
850
|
+
async getConversionCustomVariableId(tag, customerId, loginCustomerId) {
|
|
851
|
+
const query = `
|
|
852
|
+
SELECT conversion_custom_variable.id,
|
|
853
|
+
conversion_custom_variable.tag
|
|
854
|
+
FROM conversion_custom_variable
|
|
855
|
+
WHERE conversion_custom_variable.tag = "${tag}" LIMIT 1
|
|
856
|
+
`;
|
|
857
|
+
const customVariables =
|
|
858
|
+
await this.getReport(customerId, loginCustomerId, query);
|
|
859
|
+
if (customVariables.length > 0) {
|
|
860
|
+
return customVariables[0].conversionCustomVariable.id;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Gets the user listId of a given list name and upload key type. It
|
|
866
|
+
* only looks for a CRM_BASED and OPEN list.
|
|
867
|
+
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
868
|
+
* @return {number|undefined} User list_id if it exists.
|
|
869
|
+
*/
|
|
870
|
+
async getCustomerMatchUserListId(customerMatchConfig) {
|
|
871
|
+
const config = this.getCamelConfig_(customerMatchConfig);
|
|
872
|
+
const { customerId, loginCustomerId, listName, uploadKeyType, } = config;
|
|
873
|
+
const query = `
|
|
874
|
+
SELECT user_list.id, user_list.resource_name
|
|
875
|
+
FROM user_list
|
|
876
|
+
WHERE user_list.name = '${listName}'
|
|
877
|
+
AND customer.id = ${this.getCleanCid_(customerId)}
|
|
878
|
+
AND user_list.type = CRM_BASED
|
|
879
|
+
AND user_list.membership_status = OPEN
|
|
880
|
+
AND user_list.crm_based_user_list.upload_key_type = ${uploadKeyType}
|
|
881
|
+
`;
|
|
882
|
+
const userlists = await this.getReport(customerId, loginCustomerId, query);
|
|
883
|
+
return userlists.length === 0 ? undefined : userlists[0].userList.id;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Creates the user list based on a given customerMatchConfig and returns the
|
|
888
|
+
* Id. The user list would be a CRM_BASED type.
|
|
889
|
+
* Trying to create a list with an used name will fail.
|
|
890
|
+
* The Google Ads service behind this function (UserListService) supports
|
|
891
|
+
* `partial_failure`. Here, we only create one userlist at one time, so
|
|
892
|
+
* `partial_failure` is disabled to simplified the error handleing process.
|
|
893
|
+
* @see getUploadConversionFnBase_ for more details of error handling.
|
|
894
|
+
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
895
|
+
* @return {number} The created user list id. Note this is not the resource
|
|
896
|
+
* name.
|
|
897
|
+
*/
|
|
898
|
+
async createCustomerMatchUserList(customerMatchConfig) {
|
|
899
|
+
const config = this.getCamelConfig_(customerMatchConfig);
|
|
900
|
+
const { customerId, loginCustomerId, listName, uploadKeyType, } = config;
|
|
901
|
+
const userList = new UserList({
|
|
902
|
+
name: listName,
|
|
903
|
+
type: UserListType.CRM_BASED,
|
|
904
|
+
crmBasedUserList: { uploadKeyType },
|
|
905
|
+
});
|
|
906
|
+
const request = new MutateUserListsRequest({
|
|
907
|
+
customerId: this.getCleanCid_(customerId),
|
|
908
|
+
operations: [{ create: userList }],
|
|
909
|
+
validateOnly: this.debugMode, // when true makes no changes
|
|
910
|
+
partialFailure: false, // Simplify error handling in creating userlist
|
|
911
|
+
});
|
|
912
|
+
const client = await this.getUserListServiceClient_();
|
|
913
|
+
const options = this.getCallOptions_(loginCustomerId);
|
|
914
|
+
try {
|
|
915
|
+
/**
|
|
916
|
+
* @type {!MutateUserListsResponse}
|
|
917
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/MutateUserListsResponse
|
|
918
|
+
*/
|
|
919
|
+
const [response] = await client.mutateUserLists(request, options);
|
|
920
|
+
const { results } = response; // No `partialFailureError` here.
|
|
921
|
+
this.logger.debug(`Created crm userlist from`, customerMatchConfig);
|
|
922
|
+
if (!results[0]) {
|
|
923
|
+
if (this.debugMode) {
|
|
924
|
+
throw new Error('No UserList was created in DEBUG mode.');
|
|
925
|
+
} else {
|
|
926
|
+
throw new Error('No UserList was created.');
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const { resourceName } = results[0];
|
|
930
|
+
const splitted = resourceName.split('/');
|
|
931
|
+
return splitted[splitted.length - 1];
|
|
932
|
+
} catch (error) {
|
|
933
|
+
// Get the details from a wrapped GoogleAdsFailure
|
|
934
|
+
if (error.metadata) {
|
|
935
|
+
const failures = this.getGoogleAdsFailures_(error.metadata);
|
|
936
|
+
if (failures.length > 0) {
|
|
937
|
+
if (this.logger.isDebugEnabled())
|
|
938
|
+
debugGoogleAdsFailure(failures, request);
|
|
939
|
+
const message = failures.map((failure) => {
|
|
940
|
+
return failure.errors.map(({ message }) => message).join(' ');
|
|
941
|
+
}).join(';');
|
|
942
|
+
throw new Error(message);
|
|
943
|
+
}
|
|
944
|
+
this.logger.warn(
|
|
945
|
+
'Got an error with metadata but no GoogleAdsFailure in it', error);
|
|
946
|
+
}
|
|
947
|
+
throw error;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Returns the function to send out a request to Google Ads API with
|
|
953
|
+
* user ids for Customer Match upload.
|
|
954
|
+
* This API does NOT support the field 'userIdentifierSource'.
|
|
955
|
+
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
956
|
+
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
957
|
+
* Google Ads API.
|
|
958
|
+
*/
|
|
959
|
+
getUploadCustomerMatchFn(customerMatchConfig) {
|
|
960
|
+
/**
|
|
961
|
+
* Sends a batch of hits to Google Ads API.
|
|
962
|
+
* @param {!Array<string>} lines Data for single request. It should be
|
|
963
|
+
* guaranteed that it doesn't exceed quota limitation.
|
|
964
|
+
* @param {string} batchId The tag for log.
|
|
965
|
+
* @return {!Promise<BatchResult>}
|
|
966
|
+
*/
|
|
967
|
+
return async (lines, batchId) => {
|
|
968
|
+
/** @const {BatchResult} */ const batchResult = {
|
|
969
|
+
result: true,
|
|
970
|
+
numberOfLines: lines.length,
|
|
971
|
+
};
|
|
972
|
+
try {
|
|
973
|
+
const response =
|
|
974
|
+
await this.uploadUserDataToUserList(lines, customerMatchConfig);
|
|
975
|
+
this.logger.debug(`Customer Match upload batch[${batchId}]`, response);
|
|
976
|
+
return batchResult;
|
|
977
|
+
} catch (error) {
|
|
978
|
+
this.logger.error(
|
|
979
|
+
`Error in Customer Match upload batch[${batchId}]`, error);
|
|
980
|
+
this.updateBatchResultWithError(batchResult, error, lines, 0);
|
|
981
|
+
return batchResult;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Uploads a user data to a user list (aka customer match). The service
|
|
988
|
+
* doesn't support partial failure. So it would throw out a wrapped
|
|
989
|
+
* GoogleAdsFailure if error happens. The error will be handled by function
|
|
990
|
+
* `getUploadCustomerMatchFn`.
|
|
991
|
+
* @see getUploadCustomerMatchFn
|
|
992
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataService
|
|
993
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserDataOperation
|
|
994
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
995
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier
|
|
996
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
997
|
+
* Please note: The UserDataService has a limit of 10 UserDataOperations
|
|
998
|
+
* and 100 user IDs per request
|
|
999
|
+
* @see https://developers.google.com/google-ads/api/docs/migration/user-data-service#rate_limits
|
|
1000
|
+
* @param {!Array<string>} lines An array of JSON string for user Ids
|
|
1001
|
+
* @param {!CustomerMatchConfig} customerMatchConfig
|
|
1002
|
+
* @return {!Promise<UploadUserDataResponse>}
|
|
1003
|
+
*/
|
|
1004
|
+
async uploadUserDataToUserList(lines, customerMatchConfig) {
|
|
1005
|
+
const config = this.getCamelConfig_(customerMatchConfig);
|
|
1006
|
+
const { customerId, loginCustomerId, listId, operation, } = config;
|
|
1007
|
+
const client = await this.getUserDataServiceClient_();
|
|
1008
|
+
const userDataList = buildUserDataList(
|
|
1009
|
+
lines, config, IDENTIFIERS.CUSTOMER_MATCH, MAX_IDENTIFIERS.USER_DATA);
|
|
1010
|
+
const operations = userDataList.map(
|
|
1011
|
+
(userData) => new UserDataOperation({ [operation]: new UserData(userData) })
|
|
1012
|
+
);
|
|
1013
|
+
const metadata = this.buildCustomerMatchUserListMetadata_(customerId, listId);
|
|
1014
|
+
const request = new UploadUserDataRequest({
|
|
1015
|
+
customerId: this.getCleanCid_(customerId),
|
|
1016
|
+
operations,
|
|
1017
|
+
customerMatchUserListMetadata: metadata,
|
|
1018
|
+
});
|
|
1019
|
+
const options = this.getCallOptions_(loginCustomerId);
|
|
1020
|
+
const [response] = await client.uploadUserData(request, options);
|
|
1021
|
+
return response;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Creates CustomerMatchUserListMetadata.
|
|
1026
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CustomerMatchUserListMetadata
|
|
1027
|
+
* @param {string} customerId part of the ResourceName to be mutated
|
|
1028
|
+
* @param {string} userListId part of the ResourceName to be mutated
|
|
1029
|
+
* @return {!CustomerMatchUserListMetadata}
|
|
1030
|
+
* @private
|
|
1031
|
+
*/
|
|
1032
|
+
buildCustomerMatchUserListMetadata_(customerId, userListId) {
|
|
1033
|
+
const resourceName = `customers/${customerId}/userLists/${userListId}`;
|
|
1034
|
+
return new CustomerMatchUserListMetadata({
|
|
1035
|
+
userList: resourceName,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* Get OfflineUserDataJob status.
|
|
1041
|
+
* @param {OfflineUserDataJobConfig} offlineUserDataJobConfig
|
|
1042
|
+
* @param {string} resourceName
|
|
1043
|
+
* @return {string} Job status @see OfflineUserDataJobStatus
|
|
1044
|
+
*/
|
|
1045
|
+
async getOfflineUserDataJob(offlineUserDataJobConfig, resourceName) {
|
|
1046
|
+
const config = this.getCamelConfig_(offlineUserDataJobConfig);
|
|
1047
|
+
const { customerId, loginCustomerId, } = config;
|
|
1048
|
+
const query = `
|
|
1049
|
+
SELECT offline_user_data_job.id,
|
|
1050
|
+
offline_user_data_job.status,
|
|
1051
|
+
offline_user_data_job.type,
|
|
1052
|
+
offline_user_data_job.customer_match_user_list_metadata.user_list,
|
|
1053
|
+
offline_user_data_job.failure_reason
|
|
1054
|
+
FROM offline_user_data_job
|
|
1055
|
+
WHERE offline_user_data_job.resource_name = '${resourceName}'
|
|
1056
|
+
`;
|
|
1057
|
+
const jobs = await this.getReport(customerId, loginCustomerId, query);
|
|
1058
|
+
if (jobs.length === 0) {
|
|
1059
|
+
throw new Error(`Can't find the OfflineUserDataJob: ${resourceName}`);
|
|
1060
|
+
}
|
|
1061
|
+
const { failureReason, status } = jobs[0].offlineUserDataJob;
|
|
1062
|
+
if (OfflineUserDataJobFailureReason[failureReason] > 0) {
|
|
1063
|
+
this.logger.warn(
|
|
1064
|
+
`OfflineUserDataJob [${resourceName}] failed: `, failureReason);
|
|
1065
|
+
}
|
|
1066
|
+
return status;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* Creates a OfflineUserDataJob and returns resource name.
|
|
1071
|
+
* @param {OfflineUserDataJobConfig} offlineUserDataJobConfig
|
|
1072
|
+
* @return {string} The resouce name of the creaed job.
|
|
1073
|
+
*/
|
|
1074
|
+
async createOfflineUserDataJob(offlineUserDataJobConfig) {
|
|
1075
|
+
const config = this.getCamelConfig_(offlineUserDataJobConfig);
|
|
1076
|
+
const { customerId, loginCustomerId, listId, type, } = config;
|
|
1077
|
+
this.logger.debug('Creating OfflineUserDataJob from:',
|
|
1078
|
+
offlineUserDataJobConfig);
|
|
1079
|
+
const jobData = { type };
|
|
1080
|
+
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJobs?hl=en#CustomerMatchUserListMetadata
|
|
1081
|
+
if (type.startsWith('CUSTOMER_MATCH')) {
|
|
1082
|
+
const metadata = this.buildCustomerMatchUserListMetadata_(customerId,
|
|
1083
|
+
listId);
|
|
1084
|
+
jobData.customerMatchUserListMetadata = metadata;
|
|
1085
|
+
// https://developers.google.com/google-ads/api/rest/reference/rest/latest/OfflineUserDataJob?hl=en#StoreSalesMetadata
|
|
1086
|
+
} else if (type.startsWith('STORE_SALES')) {
|
|
1087
|
+
// Support previous property 'StoreSalesMetadata' for compatibility.
|
|
1088
|
+
if (config.storeSalesMetadata || config.StoreSalesMetadata) {
|
|
1089
|
+
jobData.storeSalesMetadata = config.storeSalesMetadata
|
|
1090
|
+
|| config.StoreSalesMetadata;
|
|
1091
|
+
}
|
|
1092
|
+
} else {
|
|
1093
|
+
throw new Error(`UNSUPPORTED OfflineUserDataJobType: ${type}.`);
|
|
1094
|
+
}
|
|
1095
|
+
const job = new OfflineUserDataJob(jobData);
|
|
1096
|
+
const request = new CreateOfflineUserDataJobRequest({
|
|
1097
|
+
customerId: this.getCleanCid_(customerId),
|
|
1098
|
+
job,
|
|
1099
|
+
validateOnly: this.debugMode, // when true makes no changes
|
|
1100
|
+
enableMatchRateRangePreview: true,
|
|
1101
|
+
});
|
|
1102
|
+
const client = await this.getOfflineUserDataJobServiceClient_();
|
|
1103
|
+
const options = this.getCallOptions_(loginCustomerId);
|
|
1104
|
+
try {
|
|
1105
|
+
/**
|
|
1106
|
+
* @type {!CreateOfflineUserDataJobResponse}
|
|
1107
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/CreateOfflineUserDataJobResponse
|
|
1108
|
+
*/
|
|
1109
|
+
const [response] = await client.createOfflineUserDataJob(request, options);
|
|
1110
|
+
const { resourceName } = response;
|
|
1111
|
+
if (!resourceName) {
|
|
1112
|
+
if (this.debugMode) {
|
|
1113
|
+
throw new Error('No offlineUserDataJob was created in DEBUG mode.');
|
|
1114
|
+
} else {
|
|
1115
|
+
throw new Error('No offlineUserDataJob was created.');
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
this.logger.info('Created OfflineUserDataJob:', resourceName);
|
|
1119
|
+
return resourceName;
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
if (error.metadata) {
|
|
1122
|
+
const failures = this.getGoogleAdsFailures_(error.metadata);
|
|
1123
|
+
if (failures.length > 0) {
|
|
1124
|
+
if (this.logger.isDebugEnabled())
|
|
1125
|
+
debugGoogleAdsFailure(failures, request);
|
|
1126
|
+
const message = failures.map((failure) => {
|
|
1127
|
+
return failure.errors.map(({ message }) => message).join(' ');
|
|
1128
|
+
}).join(';');
|
|
1129
|
+
throw new Error(message);
|
|
1130
|
+
}
|
|
1131
|
+
this.logger.warn(
|
|
1132
|
+
'Got an error with metadata but no GoogleAdsFailure in it', error);
|
|
1133
|
+
}
|
|
1134
|
+
throw error;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Returns the function to send out a request to Google Ads API with
|
|
1140
|
+
* user data as operations in OfflineUserDataJob.
|
|
1141
|
+
* @param {!OfflineUserDataJobConfig} config
|
|
1142
|
+
* @param {string} jobResourceName
|
|
1143
|
+
* @return {!SendSingleBatch} Function which can send a batch of hits to
|
|
1144
|
+
* Google Ads API.
|
|
1145
|
+
*/
|
|
1146
|
+
getAddOperationsToOfflineUserDataJobFn(config, jobResourceName) {
|
|
1147
|
+
/**
|
|
1148
|
+
* Sends a batch of hits to Google Ads API.
|
|
1149
|
+
* @param {!Array<string>} lines Data for single request. It should be
|
|
1150
|
+
* guaranteed that it doesn't exceed quota limitation.
|
|
1151
|
+
* @param {string} batchId The tag for log.
|
|
1152
|
+
* @return {!Promise<BatchResult>}
|
|
1153
|
+
*/
|
|
1154
|
+
return async (lines, batchId) => {
|
|
1155
|
+
/** @const {BatchResult} */ const batchResult = {
|
|
1156
|
+
result: true,
|
|
1157
|
+
numberOfLines: lines.length,
|
|
1158
|
+
};
|
|
1159
|
+
try {
|
|
1160
|
+
const response = await this.addOperationsToOfflineUserDataJob(
|
|
1161
|
+
lines, config, jobResourceName);
|
|
1162
|
+
this.logger.debug(`Add operation to job batch[${batchId}]`, response);
|
|
1163
|
+
const { partialFailureError: failed } = response;
|
|
1164
|
+
if (failed) {
|
|
1165
|
+
this.logger.info(`Job[${jobResourceName}] faile:`, failed.message);
|
|
1166
|
+
const failures = failed.details.map(
|
|
1167
|
+
({ value }) => GoogleAdsFailure.decode(value));
|
|
1168
|
+
this.extraFailedLines_(batchResult, failures, lines, 0);
|
|
1169
|
+
}
|
|
1170
|
+
return batchResult;
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
this.logger.error(
|
|
1173
|
+
`Error in OfflineUserDataJob add operations batch[${batchId}]`, error);
|
|
1174
|
+
console.log(batchResult);
|
|
1175
|
+
this.updateBatchResultWithError(batchResult, error, lines, 0);
|
|
1176
|
+
return batchResult;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Adds user data in to the OfflineUserDataJob.
|
|
1183
|
+
* @param {!Array<string>} lines An array of JSON string for user Ids
|
|
1184
|
+
* @param {OfflineUserDataJobConfig} config Offline user data job config.
|
|
1185
|
+
* @param {string} jobResourceName
|
|
1186
|
+
* @return {!Promise<!AddOfflineUserDataJobOperationsResponse>}
|
|
1187
|
+
*/
|
|
1188
|
+
async addOperationsToOfflineUserDataJob(lines, offlineUserDataJobConfig,
|
|
1189
|
+
jobResourceName) {
|
|
1190
|
+
const config = this.getCamelConfig_(offlineUserDataJobConfig);
|
|
1191
|
+
const { loginCustomerId, operation, type } = config;
|
|
1192
|
+
const client = await this.getOfflineUserDataJobServiceClient_();
|
|
1193
|
+
const identifierTypes = type.startsWith('CUSTOMER_MATCH')
|
|
1194
|
+
? IDENTIFIERS.CUSTOMER_MATCH : IDENTIFIERS.STORE_SALES;
|
|
1195
|
+
const userDataList = buildUserDataList(
|
|
1196
|
+
lines, config, identifierTypes, MAX_IDENTIFIERS.USER_DATA);
|
|
1197
|
+
const operations = userDataList.map((userData) => {
|
|
1198
|
+
return new OfflineUserDataJobOperation(
|
|
1199
|
+
{ [operation]: new UserData(userData) });
|
|
1200
|
+
});
|
|
1201
|
+
const request = new AddOfflineUserDataJobOperationsRequest({
|
|
1202
|
+
resourceName: jobResourceName,
|
|
1203
|
+
operations,
|
|
1204
|
+
validateOnly: this.debugMode,
|
|
1205
|
+
enablePartialFailure: true,
|
|
1206
|
+
enableWarnings: true,
|
|
1207
|
+
});
|
|
1208
|
+
const options = this.getCallOptions_(loginCustomerId);
|
|
1209
|
+
const [response] =
|
|
1210
|
+
await client.addOfflineUserDataJobOperations(request, options);
|
|
1211
|
+
return response;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Starts the OfflineUserDataJob.
|
|
1216
|
+
* @param {OfflineUserDataJobConfig} offlineUserDataJobConfig Offline user data job config.
|
|
1217
|
+
* @param {string} jobResourceName
|
|
1218
|
+
* @return {!Promise<!Operation>}
|
|
1219
|
+
*/
|
|
1220
|
+
async runOfflineUserDataJob(offlineUserDataJobConfig, jobResourceName) {
|
|
1221
|
+
const config = this.getCamelConfig_(offlineUserDataJobConfig);
|
|
1222
|
+
const { loginCustomerId } = config;
|
|
1223
|
+
const client = await this.getOfflineUserDataJobServiceClient_();
|
|
1224
|
+
const request = new RunOfflineUserDataJobRequest({
|
|
1225
|
+
resourceName: jobResourceName,
|
|
1226
|
+
validateOnly: this.debugMode,
|
|
1227
|
+
});
|
|
1228
|
+
const options = this.getCallOptions_(loginCustomerId);
|
|
1229
|
+
try {
|
|
1230
|
+
const [response] = await client.runOfflineUserDataJob(request, options);
|
|
1231
|
+
this.logger.debug('runOfflineUserDataJob response: ', response);
|
|
1232
|
+
return response;
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
if (error.metadata) {
|
|
1235
|
+
const failures = this.getGoogleAdsFailures_(error.metadata);
|
|
1236
|
+
if (failures.length > 0) {
|
|
1237
|
+
if (this.logger.isDebugEnabled())
|
|
1238
|
+
debugGoogleAdsFailure(failures, request);
|
|
1239
|
+
const message = failures.map((failure) => {
|
|
1240
|
+
return failure.errors.map(({ message }) => message).join(' ');
|
|
1241
|
+
}).join(';');
|
|
1242
|
+
throw new Error(message);
|
|
1243
|
+
}
|
|
1244
|
+
this.logger.warn(
|
|
1245
|
+
'Got an error with metadata but no GoogleAdsFailure in it', error);
|
|
1246
|
+
}
|
|
1247
|
+
throw error;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/**
|
|
1252
|
+
* Returns a integer format CID by removing dashes.
|
|
1253
|
+
* @param {string} cid
|
|
1254
|
+
* @return {string}
|
|
1255
|
+
* @private
|
|
1256
|
+
*/
|
|
1257
|
+
getCleanCid_(cid) {
|
|
1258
|
+
return cid.toString().replace(/-/g, '');
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Historically, we used a 3rd party library that adopted snake naming
|
|
1263
|
+
* convention as the protobuf files and API documents. However, the
|
|
1264
|
+
* automatically generated NodeJS library requires lowerCamel naming
|
|
1265
|
+
* convention. To reduce the impact to existing users, the function can
|
|
1266
|
+
* convert a 'snake' object to a 'lowerCamel' object.
|
|
1267
|
+
* @param {Object} config
|
|
1268
|
+
* @return {object}
|
|
1269
|
+
*/
|
|
1270
|
+
getCamelConfig_(config) {
|
|
1271
|
+
return changeObjectNamingFromSnakeToLowerCamel(config);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* Returns a HTTP header object contains the authentication information for
|
|
1276
|
+
* Google Ads API, include: `developer-token` and `ogin-customer-id`.
|
|
1277
|
+
* @param {string} loginCustomerId
|
|
1278
|
+
* @return {object} The HTTP header object.
|
|
1279
|
+
* @private
|
|
1280
|
+
*/
|
|
1281
|
+
getGoogleAdsHeaders_(loginCustomerId) {
|
|
1282
|
+
return {
|
|
1283
|
+
"developer-token": this.developerToken,
|
|
1284
|
+
"login-customer-id": loginCustomerId,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* Returns an option object as gRPC call options. This is used to add Google
|
|
1290
|
+
* Ads API required authentication information to requests.
|
|
1291
|
+
* @param {string} loginCustomerId
|
|
1292
|
+
* @return {{otherArgs:object}}
|
|
1293
|
+
* @private
|
|
1294
|
+
*/
|
|
1295
|
+
getCallOptions_(loginCustomerId) {
|
|
1296
|
+
return {
|
|
1297
|
+
otherArgs: {
|
|
1298
|
+
headers: this.getGoogleAdsHeaders_(this.getCleanCid_(loginCustomerId)),
|
|
1299
|
+
},
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Prepares the feach data service client instance.
|
|
1305
|
+
* @return {!GoogleAdsServiceClient}
|
|
1306
|
+
* @private
|
|
1307
|
+
*/
|
|
1308
|
+
async getGoogleAdsServiceClient_() {
|
|
1309
|
+
if (this.googleAdsClient) return this.googleAdsClient;
|
|
1310
|
+
await this.authClient.prepareCredentials();
|
|
1311
|
+
this.googleAdsClient = new GoogleAdsServiceClient({
|
|
1312
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1313
|
+
});
|
|
1314
|
+
return this.googleAdsClient;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/**
|
|
1318
|
+
* Prepares the Google Ads field service client instance.
|
|
1319
|
+
* @return {!GoogleAdsFieldServiceClient}
|
|
1320
|
+
* @private
|
|
1321
|
+
*/
|
|
1322
|
+
async getGoogleAdsFieldServiceClient_() {
|
|
1323
|
+
await this.authClient.prepareCredentials();
|
|
1324
|
+
return new GoogleAdsFieldServiceClient({
|
|
1325
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* Prepares the conversion upload service client instance.
|
|
1331
|
+
* @return {!ConversionUploadServiceClient}
|
|
1332
|
+
* @private
|
|
1333
|
+
*/
|
|
1334
|
+
async getConversionUploadServiceClient_() {
|
|
1335
|
+
await this.authClient.prepareCredentials();
|
|
1336
|
+
return new ConversionUploadServiceClient({
|
|
1337
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Prepares the conversion adjustment upload service client instance.
|
|
1343
|
+
* @return {!ConversionAdjustmentUploadServiceClient}
|
|
1344
|
+
* @private
|
|
1345
|
+
*/
|
|
1346
|
+
async getConversionAdjustmentUploadServiceClient_() {
|
|
1347
|
+
await this.authClient.prepareCredentials();
|
|
1348
|
+
return new ConversionAdjustmentUploadServiceClient({
|
|
1349
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Prepares the user list manage service client instance.
|
|
1355
|
+
* @return {!UserListServiceClient}
|
|
1356
|
+
* @private
|
|
1357
|
+
*/
|
|
1358
|
+
async getUserListServiceClient_() {
|
|
1359
|
+
await this.authClient.prepareCredentials();
|
|
1360
|
+
return new UserListServiceClient({
|
|
1361
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Prepares the user data upload service client instance.
|
|
1367
|
+
* @return {!UserDataServiceClient}
|
|
1368
|
+
* @private
|
|
1369
|
+
*/
|
|
1370
|
+
async getUserDataServiceClient_() {
|
|
1371
|
+
await this.authClient.prepareCredentials();
|
|
1372
|
+
return new UserDataServiceClient({
|
|
1373
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Prepares the offline user data job manage service client instance.
|
|
1379
|
+
* @return {!OfflineUserDataJobServiceClient}
|
|
1380
|
+
* @private
|
|
1381
|
+
*/
|
|
1382
|
+
async getOfflineUserDataJobServiceClient_() {
|
|
1383
|
+
await this.authClient.prepareCredentials();
|
|
1384
|
+
return new OfflineUserDataJobServiceClient({
|
|
1385
|
+
authClient: this.authClient.getDefaultAuth(),
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Returns an arrary of UserIdentifier object based the given JSON object.
|
|
1392
|
+
* @param {Object} record An object contains user indentifier information.
|
|
1393
|
+
* @param {!Array<string>} identifierTypes An list of user identifier types that
|
|
1394
|
+
* are supported by the target service.
|
|
1395
|
+
* @param {number} maximumNumOfIdentifiers The maximum number of user
|
|
1396
|
+
* identifiers in a conversion or a UserData.
|
|
1397
|
+
* @param {!UserIdentifierSource} defaultSource The value of `
|
|
1398
|
+
* UserIdentifierSource` in config. This is not supported by UserDataService
|
|
1399
|
+
* or 'CUSTOMER_MATCH' type OfflineUserDataJob.
|
|
1400
|
+
* @return {!Array<!UserData>}
|
|
1401
|
+
* @see UserData
|
|
1402
|
+
*/
|
|
1403
|
+
const buildUserIdentifierList = (record, identifierTypes,
|
|
1404
|
+
maximumNumOfIdentifiers, defaultSource) => {
|
|
1405
|
+
const { userIdentifierSource = defaultSource } = record;
|
|
1406
|
+
const userIdentifiers = [];
|
|
1407
|
+
identifierTypes.forEach((identifierType) => {
|
|
1408
|
+
const identifierValue = record[identifierType];
|
|
1409
|
+
if (identifierValue) {
|
|
1410
|
+
if (Array.isArray(identifierValue)) {
|
|
1411
|
+
identifierValue.forEach((id) => {
|
|
1412
|
+
userIdentifiers.push(new UserIdentifier({
|
|
1413
|
+
[identifierType]: id,
|
|
1414
|
+
userIdentifierSource,
|
|
1415
|
+
}));
|
|
1416
|
+
});
|
|
1417
|
+
} else {
|
|
1418
|
+
userIdentifiers.push(new UserIdentifier({
|
|
1419
|
+
[identifierType]: identifierValue,
|
|
1420
|
+
userIdentifierSource,
|
|
1421
|
+
}));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
if (userIdentifiers.length <= maximumNumOfIdentifiers) {
|
|
1426
|
+
return userIdentifiers;
|
|
1427
|
+
} else {
|
|
1428
|
+
this.logger.warn(
|
|
1429
|
+
`Too many user identifiers, will only send ${maximumNumOfIdentifiers}:`,
|
|
1430
|
+
JSON.stringify(record));
|
|
1431
|
+
return userIdentifiers.slice(0, MAX_IDENTIFIERS_PER_USER);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Returns an array of conversion JSON objects based on the given arran of JSON
|
|
1437
|
+
* strings. The result can be used to create objects of ClickConversion,
|
|
1438
|
+
* CallConversion or ConversionAdjustment.
|
|
1439
|
+
* @param {!Array<string>} lines An array of JSON strings of conversions.
|
|
1440
|
+
* @param {ConversionConfig} config Conversion configuration.
|
|
1441
|
+
* @param {!Array<string>} conversionFields An list of user identifier types that
|
|
1442
|
+
* are supported by the target service.
|
|
1443
|
+
* @param {!Array<string>} identifierTypes An list of user identifier types that
|
|
1444
|
+
* are supported by the target service.
|
|
1445
|
+
* @param {number} maximumNumOfIdentifiers The maximum number of user
|
|
1446
|
+
* identifiers in a conversion or a UserData.
|
|
1447
|
+
* @return {!Array<object>} An array of objects to create ClickConversion,
|
|
1448
|
+
* CallConversion or ConversionAdjustment
|
|
1449
|
+
*/
|
|
1450
|
+
const buildConversionJsonList = (lines, config, conversionFields,
|
|
1451
|
+
identifierTypes = [], maximumNumOfIdentifiers = 0) => {
|
|
1452
|
+
const { customerId, customVariables, userIdentifierSource } = config;
|
|
1453
|
+
const configProperties = lodash.pick(config, conversionFields);
|
|
1454
|
+
return lines.map((line) => JSON.parse(line))
|
|
1455
|
+
.map(changeObjectNamingFromSnakeToLowerCamel)
|
|
1456
|
+
//XXX: extra steps here to keep compatibility for snake convention
|
|
1457
|
+
.map((record) => {
|
|
1458
|
+
const conversion = lodash.merge({}, configProperties,
|
|
1459
|
+
lodash.pick(record, conversionFields));
|
|
1460
|
+
if (customVariables) {
|
|
1461
|
+
const variables = Object.keys(customVariables);
|
|
1462
|
+
conversion.customVariables = variables.map((variable) => {
|
|
1463
|
+
return new CustomVariable({
|
|
1464
|
+
conversionCustomVariable:
|
|
1465
|
+
`customers/${customerId}/conversionCustomVariables/${customVariables[variable]}`,
|
|
1466
|
+
value: record[variable],
|
|
1467
|
+
});
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
if (identifierTypes.length > 0) {
|
|
1471
|
+
const userIdentifiers = buildUserIdentifierList(
|
|
1472
|
+
record,
|
|
1473
|
+
identifierTypes,
|
|
1474
|
+
maximumNumOfIdentifiers,
|
|
1475
|
+
userIdentifierSource
|
|
1476
|
+
);
|
|
1477
|
+
if (userIdentifiers.length > 0) {
|
|
1478
|
+
conversion.userIdentifiers = userIdentifiers;
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return conversion;
|
|
1482
|
+
})
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Returns an arrary of UserData object based on the given arran of JSON strings.
|
|
1486
|
+
* @param {!Array<string>} lines An array of JSON strings of UserData.
|
|
1487
|
+
* @param {{
|
|
1488
|
+
* additionalAttributes: (!UserIdentifierSource|undefined)
|
|
1489
|
+
* transactionAttribute: (!TransactionAttribute|undefined),
|
|
1490
|
+
* userAttribute: (!UserAttribute|undefined),
|
|
1491
|
+
* consent: (!Consent|undefined),
|
|
1492
|
+
* }} config
|
|
1493
|
+
* @see USERDATA_ADDITIONAL_ATTRIBUTES
|
|
1494
|
+
* @see UserIdentifierSource Not available in UserDataService or 'CUSTOMER_MATCH'
|
|
1495
|
+
* type OfflineUserDataJob.
|
|
1496
|
+
* @return {!Array<!UserData>}
|
|
1497
|
+
* @see https://developers.google.com/google-ads/api/reference/rpc/latest/UserData
|
|
1498
|
+
*/
|
|
1499
|
+
const buildUserDataList = (lines, config, identifierTypes,
|
|
1500
|
+
maximumNumOfIdentifiers) => {
|
|
1501
|
+
const configAttributes = lodash.pick(config, USERDATA_ADDITIONAL_ATTRIBUTES);
|
|
1502
|
+
const { userIdentifierSource } = config;
|
|
1503
|
+
return lines.map((line) => JSON.parse(line))
|
|
1504
|
+
//XXX: extra steps here to keep compatibility for snake conversion
|
|
1505
|
+
.map(changeObjectNamingFromSnakeToLowerCamel)
|
|
1506
|
+
.map((record) => {
|
|
1507
|
+
const userData = lodash.merge({}, configAttributes,
|
|
1508
|
+
lodash.pick(record, USERDATA_ADDITIONAL_ATTRIBUTES));
|
|
1509
|
+
userData.userIdentifiers = buildUserIdentifierList(
|
|
1510
|
+
record,
|
|
1511
|
+
identifierTypes,
|
|
1512
|
+
maximumNumOfIdentifiers,
|
|
1513
|
+
userIdentifierSource
|
|
1514
|
+
);
|
|
1515
|
+
return new UserData(userData);
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Prints the information of GoogleAdsFailure from a request.
|
|
1521
|
+
* @param {!Array<!GoogleAdsFailure>} failures
|
|
1522
|
+
* @param {(object|undefined)} request
|
|
1523
|
+
*/
|
|
1524
|
+
const debugGoogleAdsFailure = (failures, request) => {
|
|
1525
|
+
failures.forEach(({ requestId, errors }) => {
|
|
1526
|
+
console.log('GoogleAdsFailure request id:', requestId);
|
|
1527
|
+
errors.forEach(({ message, location }, index) => {
|
|
1528
|
+
console.log(` GoogleAdsError[${index}]:`, message);
|
|
1529
|
+
console.log(` location:`, location);
|
|
1530
|
+
});
|
|
1531
|
+
});
|
|
1532
|
+
if (request) {
|
|
1533
|
+
console.log('Original request:',
|
|
1534
|
+
request.toJSON ? request.toJSON() : request);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
module.exports = {
|
|
1539
|
+
ConversionConfig,
|
|
1540
|
+
CustomerMatchRecord,
|
|
1541
|
+
CustomerMatchConfig,
|
|
1542
|
+
OfflineUserDataJobConfig,
|
|
1543
|
+
GoogleAdsApi,
|
|
1544
|
+
buildUserIdentifierList,
|
|
1545
|
+
buildConversionJsonList,
|
|
1546
|
+
buildUserDataList,
|
|
1547
|
+
CONVERSION_FIELDS,
|
|
1548
|
+
IDENTIFIERS,
|
|
1549
|
+
MAX_IDENTIFIERS,
|
|
1550
|
+
};
|