google-ads-api 19.0.0-rest-beta → 19.0.1
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/CHANGELOG.md +40 -0
- package/build/src/client.d.ts +0 -1
- package/build/src/client.js +0 -1
- package/build/src/customer.d.ts +0 -6
- package/build/src/customer.js +75 -202
- package/build/src/protos/autogen/fields.d.ts +0 -1
- package/build/src/protos/autogen/fields.js +1 -3
- package/build/src/protos/autogen/serviceFactory.js +1 -1
- package/build/src/protos/index.d.ts +1 -1
- package/build/src/protos/index.js +1 -2
- package/build/src/query.js +1 -1
- package/build/src/service.d.ts +0 -1
- package/build/src/service.js +14 -35
- package/build/src/testUtils.d.ts +0 -38
- package/build/src/testUtils.js +5 -76
- package/build/src/types.d.ts +1 -1
- package/build/src/utils.d.ts +5 -0
- package/build/src/utils.js +15 -0
- package/package.json +5 -15
- package/build/src/parserRest.d.ts +0 -4
- package/build/src/parserRest.js +0 -88
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
### Upcoming Breaking Changes
|
|
4
|
+
|
|
5
|
+
From v20 onwards this library will use the REST api for report, reportStream, search and searchStream. This will result in minor breaking changes to reporting calls:
|
|
6
|
+
|
|
7
|
+
- Fields with the enum value `0` will now be `undefined` instead
|
|
8
|
+
- Array-fields (such as `final_urls`) with the value `[]` will now be `undefined` instead
|
|
9
|
+
|
|
10
|
+
While these changes are inconvenient, the performance of the REST api is significantly better than the gRPC api, particularly for responses with many rows.
|
|
11
|
+
|
|
12
|
+
To prepare for this change, we recommend you use the install the `19.0.0-rest-beta` version of this library and test your application with it.
|
|
13
|
+
|
|
14
|
+
## 19.0.1
|
|
15
|
+
|
|
16
|
+
- Fix issue with type interface for search_settings not showing any valid fields.
|
|
17
|
+
|
|
18
|
+
## 19.0.0
|
|
19
|
+
|
|
20
|
+
### Version Upgrade
|
|
21
|
+
|
|
22
|
+
- Upgraded google-ads-api version to v19. Refer to Google ads release notes [here](https://developers.google.com/google-ads/api/docs/release-notes) for changes.
|
|
23
|
+
|
|
24
|
+
### Library Changes
|
|
25
|
+
|
|
26
|
+
- We currently do not support `return_total_results_count` on search_settings for SearchGoogleAdsRequest. Please use `reportCount()` instead. This has been reflected in the `RequestOptions` type:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export type RequestOptions = Omit<
|
|
30
|
+
services.ISearchGoogleAdsRequest,
|
|
31
|
+
"customer_id" | "query" | "search_settings"
|
|
32
|
+
> & {
|
|
33
|
+
search_settings?: Omit<
|
|
34
|
+
services.ISearchSettings,
|
|
35
|
+
"return_total_results_count"
|
|
36
|
+
>;
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
For more information please see the `SearchSettings` reference on the Google document [here](https://developers.google.com/google-ads/api/reference/rpc/v19/SearchSettings).
|
package/build/src/client.d.ts
CHANGED
package/build/src/client.js
CHANGED
package/build/src/customer.d.ts
CHANGED
|
@@ -52,10 +52,8 @@ export declare class Customer extends ServiceFactory {
|
|
|
52
52
|
reportStreamRaw(reportOptions: Readonly<ReportOptions>): Promise<CancellableStream | void>;
|
|
53
53
|
private search;
|
|
54
54
|
private paginatedSearch;
|
|
55
|
-
private useStreamToImitateRegularSearch;
|
|
56
55
|
private querier;
|
|
57
56
|
private streamer;
|
|
58
|
-
private handleStreamError;
|
|
59
57
|
/**
|
|
60
58
|
* @description Creates, updates, or removes resources. This method supports atomic transactions
|
|
61
59
|
* with multiple types of resources. For example, you can atomically create a campaign and a
|
|
@@ -64,8 +62,4 @@ export declare class Customer extends ServiceFactory {
|
|
|
64
62
|
*/
|
|
65
63
|
mutateResources<T>(mutations: MutateOperation<T>[], mutateOptions?: MutateOptions): Promise<services.MutateGoogleAdsResponse>;
|
|
66
64
|
private get googleAdsFields();
|
|
67
|
-
private prepareGoogleAdsServicePostRequestArgs;
|
|
68
|
-
private decamelizeKeysIfNeeded;
|
|
69
|
-
private gaqlQueryStringIncludesLimit;
|
|
70
|
-
private generateTooManyRowsError;
|
|
71
65
|
}
|
package/build/src/customer.js
CHANGED
|
@@ -4,19 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Customer = void 0;
|
|
7
|
-
const
|
|
8
|
-
const stream_chain_1 = require("stream-chain");
|
|
9
|
-
const stream_json_1 = require("stream-json");
|
|
10
|
-
const StreamArray_1 = require("stream-json/streamers/StreamArray");
|
|
11
|
-
const parserRest_1 = require("./parserRest");
|
|
12
|
-
const protos_1 = require("./protos");
|
|
7
|
+
const parser_1 = require("./parser");
|
|
13
8
|
const serviceFactory_1 = __importDefault(require("./protos/autogen/serviceFactory"));
|
|
14
9
|
const query_1 = require("./query");
|
|
15
|
-
const
|
|
16
|
-
const ROWS_PER_STREAMED_CHUNK = 10_000; // From experience, this is what can be expected from the API.
|
|
10
|
+
const utils_1 = require("./utils");
|
|
17
11
|
class Customer extends serviceFactory_1.default {
|
|
18
12
|
constructor(clientOptions, customerOptions, hooks) {
|
|
19
|
-
super(clientOptions, customerOptions, hooks
|
|
13
|
+
super(clientOptions, customerOptions, hooks !== null && hooks !== void 0 ? hooks : {});
|
|
20
14
|
}
|
|
21
15
|
/**
|
|
22
16
|
@description Single query using a raw GAQL string.
|
|
@@ -56,8 +50,8 @@ class Customer extends serviceFactory_1.default {
|
|
|
56
50
|
*/
|
|
57
51
|
async reportCount(options) {
|
|
58
52
|
const { gaqlQuery, requestOptions } = (0, query_1.buildQuery)({ ...options, limit: 1 }); // must get at least one row
|
|
59
|
-
//
|
|
60
|
-
requestOptions.
|
|
53
|
+
// @ts-expect-error we do not allow this field in reportOptions, however it is still a valid request option
|
|
54
|
+
requestOptions.return_total_results_count = true;
|
|
61
55
|
const useHooks = false; // to avoid cacheing conflicts
|
|
62
56
|
const { totalResultsCount } = await this.querier(gaqlQuery, requestOptions, options, useHooks);
|
|
63
57
|
return totalResultsCount;
|
|
@@ -117,123 +111,39 @@ class Customer extends serviceFactory_1.default {
|
|
|
117
111
|
});
|
|
118
112
|
}
|
|
119
113
|
async search(gaqlQuery, requestOptions) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const totalResultsCount = searchResponse.totalResultsCount
|
|
134
|
-
? +searchResponse.totalResultsCount
|
|
135
|
-
: undefined;
|
|
136
|
-
return { response, nextPageToken, totalResultsCount, summaryRow };
|
|
137
|
-
}
|
|
138
|
-
catch (e) {
|
|
139
|
-
if (e.response?.data.error.details[0]) {
|
|
140
|
-
throw new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(e.response.data.error.details[0]));
|
|
141
|
-
}
|
|
142
|
-
throw e;
|
|
114
|
+
const { service, request } = this.buildSearchRequestAndService(gaqlQuery, requestOptions);
|
|
115
|
+
const searchResponse = await service.search(request, {
|
|
116
|
+
otherArgs: { headers: this.callHeaders },
|
|
117
|
+
autoPaginate: false, // autoPaginate doesn't work
|
|
118
|
+
});
|
|
119
|
+
const response = searchResponse[0];
|
|
120
|
+
const summaryRow = searchResponse[2].summary_row;
|
|
121
|
+
const nextPageToken = searchResponse[2].next_page_token;
|
|
122
|
+
const totalResultsCount = searchResponse[2].total_results_count
|
|
123
|
+
? +searchResponse[2].total_results_count
|
|
124
|
+
: undefined;
|
|
125
|
+
if (summaryRow) {
|
|
126
|
+
response.unshift(summaryRow);
|
|
143
127
|
}
|
|
128
|
+
return { response, nextPageToken, totalResultsCount };
|
|
144
129
|
}
|
|
145
|
-
async paginatedSearch(gaqlQuery, requestOptions) {
|
|
146
|
-
/*
|
|
147
|
-
When possible, use the searchStream method to avoid the overhead of pagination.
|
|
148
|
-
*/
|
|
149
|
-
if (requestOptions.page_size === undefined &&
|
|
150
|
-
requestOptions.search_settings?.return_total_results_count === undefined) {
|
|
151
|
-
// If no pagination or summary options are set, we can use the non-paginated search method.
|
|
152
|
-
const { response } = await this.useStreamToImitateRegularSearch(gaqlQuery, requestOptions);
|
|
153
|
-
return { response };
|
|
154
|
-
}
|
|
130
|
+
async paginatedSearch(gaqlQuery, requestOptions, parser) {
|
|
155
131
|
const response = [];
|
|
156
132
|
let nextPageToken = undefined;
|
|
157
133
|
const initialSearch = await this.search(gaqlQuery, requestOptions);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// In this case, we set it to 0.
|
|
161
|
-
if (requestOptions.search_settings?.return_total_results_count &&
|
|
162
|
-
initialSearch.totalResultsCount === undefined) {
|
|
163
|
-
totalResultsCount = 0;
|
|
164
|
-
}
|
|
165
|
-
let summaryRow = initialSearch.summaryRow;
|
|
166
|
-
response.push(...initialSearch.response);
|
|
134
|
+
const totalResultsCount = initialSearch.totalResultsCount;
|
|
135
|
+
response.push(...parser(initialSearch.response));
|
|
167
136
|
nextPageToken = initialSearch.nextPageToken;
|
|
168
137
|
while (nextPageToken) {
|
|
169
138
|
const nextSearch = await this.search(gaqlQuery, {
|
|
170
139
|
...requestOptions,
|
|
171
140
|
page_token: nextPageToken,
|
|
172
141
|
});
|
|
173
|
-
response.push(...nextSearch.response);
|
|
142
|
+
response.push(...parser(nextSearch.response));
|
|
174
143
|
nextPageToken = nextSearch.nextPageToken;
|
|
175
|
-
if (nextSearch.summaryRow) {
|
|
176
|
-
summaryRow = nextSearch.summaryRow;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (summaryRow) {
|
|
180
|
-
response.unshift(summaryRow);
|
|
181
144
|
}
|
|
182
145
|
return { response, totalResultsCount };
|
|
183
146
|
}
|
|
184
|
-
// Google's searchStream method is faster than search, but it does not support all features.
|
|
185
|
-
// When report() is called, we use searchStream if possible, otherwise we use paginatedSearch.
|
|
186
|
-
// Note that just like `paginatedSearch`, this method accumulates results in memory. Use
|
|
187
|
-
// `reportStream` for a more memory-efficient alternative (at the cost of more CPU usage).
|
|
188
|
-
async useStreamToImitateRegularSearch(gaqlQuery, requestOptions) {
|
|
189
|
-
const accessToken = await this.getAccessToken();
|
|
190
|
-
try {
|
|
191
|
-
const args = this.prepareGoogleAdsServicePostRequestArgs("searchStream", accessToken, {
|
|
192
|
-
responseType: "stream",
|
|
193
|
-
data: {
|
|
194
|
-
query: gaqlQuery,
|
|
195
|
-
...requestOptions,
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
const response = await (0, axios_1.default)(args);
|
|
199
|
-
const stream = response.data;
|
|
200
|
-
const buffers = [];
|
|
201
|
-
let rowCount = -ROWS_PER_STREAMED_CHUNK;
|
|
202
|
-
for await (const data of stream) {
|
|
203
|
-
if (this.clientOptions.max_reporting_rows &&
|
|
204
|
-
!this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
|
|
205
|
-
// This is a quick-and-dirty way to count rows, but it's good enough for our purposes.
|
|
206
|
-
// We want to avoid using a proper JSON streamer here for performance reasons.
|
|
207
|
-
if (data.toString("utf-8").includes(`results":`)) {
|
|
208
|
-
rowCount += ROWS_PER_STREAMED_CHUNK;
|
|
209
|
-
}
|
|
210
|
-
if (rowCount > this.clientOptions.max_reporting_rows) {
|
|
211
|
-
throw this.generateTooManyRowsError();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
buffers.push(data);
|
|
215
|
-
}
|
|
216
|
-
const asString = Buffer.concat(buffers).toString("utf-8");
|
|
217
|
-
const accumulator = [];
|
|
218
|
-
let foundSummaryRow;
|
|
219
|
-
for (const { results, summaryRow } of JSON.parse(asString)) {
|
|
220
|
-
if (summaryRow) {
|
|
221
|
-
foundSummaryRow = this.decamelizeKeysIfNeeded(summaryRow);
|
|
222
|
-
}
|
|
223
|
-
accumulator.push(...(results ?? []).map((row) => {
|
|
224
|
-
return this.decamelizeKeysIfNeeded(row);
|
|
225
|
-
}));
|
|
226
|
-
if (foundSummaryRow) {
|
|
227
|
-
accumulator.unshift(foundSummaryRow);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return { response: accumulator };
|
|
231
|
-
}
|
|
232
|
-
catch (e) {
|
|
233
|
-
await this.handleStreamError(e);
|
|
234
|
-
throw e; // The line above should always throw.
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
147
|
async querier(gaqlQuery, requestOptions = {}, reportOptions, useHooks = true) {
|
|
238
148
|
const baseHookArguments = {
|
|
239
149
|
credentials: this.credentials,
|
|
@@ -260,7 +170,14 @@ class Customer extends serviceFactory_1.default {
|
|
|
260
170
|
}
|
|
261
171
|
}
|
|
262
172
|
try {
|
|
263
|
-
const
|
|
173
|
+
const parsingWapper = (rows) => {
|
|
174
|
+
return this.clientOptions.disable_parsing
|
|
175
|
+
? rows
|
|
176
|
+
: reportOptions
|
|
177
|
+
? (0, parser_1.parse)({ results: rows, reportOptions })
|
|
178
|
+
: (0, parser_1.parse)({ results: rows, gaqlString: gaqlQuery });
|
|
179
|
+
};
|
|
180
|
+
const { response, totalResultsCount } = await this.paginatedSearch(gaqlQuery, requestOptions, parsingWapper);
|
|
264
181
|
if (this.hooks.onQueryEnd && useHooks) {
|
|
265
182
|
const queryResolution = { resolved: false };
|
|
266
183
|
await this.hooks.onQueryEnd({
|
|
@@ -312,79 +229,58 @@ class Customer extends serviceFactory_1.default {
|
|
|
312
229
|
return;
|
|
313
230
|
}
|
|
314
231
|
}
|
|
232
|
+
const { service, request } = this.buildSearchStreamRequestAndService(gaqlQuery, requestOptions);
|
|
233
|
+
const stream = service.searchStream(request, {
|
|
234
|
+
otherArgs: { headers: this.callHeaders },
|
|
235
|
+
});
|
|
236
|
+
let streamFinished = false;
|
|
237
|
+
const accumulator = [];
|
|
238
|
+
let nextChunk = (0, utils_1.createNextChunkArrivedPromise)();
|
|
239
|
+
stream.on("data", (chunk) => {
|
|
240
|
+
const results = chunk.summary_row ? [chunk.summary_row] : chunk.results;
|
|
241
|
+
const parsedResponse = this.clientOptions.disable_parsing
|
|
242
|
+
? results
|
|
243
|
+
: reportOptions
|
|
244
|
+
? (0, parser_1.parse)({ results, reportOptions })
|
|
245
|
+
: (0, parser_1.parse)({ results, gaqlString: gaqlQuery });
|
|
246
|
+
accumulator.push(...parsedResponse);
|
|
247
|
+
nextChunk.resolve();
|
|
248
|
+
nextChunk = (0, utils_1.createNextChunkArrivedPromise)();
|
|
249
|
+
});
|
|
250
|
+
stream.on("error", (searchError) => {
|
|
251
|
+
nextChunk.reject(searchError);
|
|
252
|
+
});
|
|
253
|
+
stream.on("end", () => {
|
|
254
|
+
streamFinished = true;
|
|
255
|
+
nextChunk.resolve();
|
|
256
|
+
});
|
|
315
257
|
try {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
});
|
|
324
|
-
const response = await (0, axios_1.default)(args);
|
|
325
|
-
const stream = response.data;
|
|
326
|
-
// The options below help to make the stream less CPU intensive.
|
|
327
|
-
const parser = new stream_json_1.Parser({
|
|
328
|
-
streamValues: false,
|
|
329
|
-
streamKeys: false,
|
|
330
|
-
packValues: true,
|
|
331
|
-
packKeys: true,
|
|
332
|
-
});
|
|
333
|
-
const pipeline = (0, stream_chain_1.chain)([stream, parser, (0, StreamArray_1.streamArray)()]);
|
|
334
|
-
let count = 0;
|
|
335
|
-
for await (const data of pipeline) {
|
|
336
|
-
const results = data.value.results ?? [data.value.summaryRow];
|
|
337
|
-
count += results.length;
|
|
338
|
-
if (this.clientOptions.max_reporting_rows &&
|
|
339
|
-
count > this.clientOptions.max_reporting_rows &&
|
|
340
|
-
!this.gaqlQueryStringIncludesLimit(gaqlQuery)) {
|
|
341
|
-
throw this.generateTooManyRowsError();
|
|
258
|
+
while (!streamFinished || accumulator.length) {
|
|
259
|
+
if (accumulator.length > 0) {
|
|
260
|
+
const item = accumulator.shift();
|
|
261
|
+
if (item === undefined) {
|
|
262
|
+
throw new Error("UNDEFINED_STREAM_ERROR");
|
|
263
|
+
}
|
|
264
|
+
yield item;
|
|
342
265
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
yield parsed;
|
|
266
|
+
else {
|
|
267
|
+
await nextChunk.newPromise;
|
|
346
268
|
}
|
|
347
269
|
}
|
|
348
|
-
return;
|
|
349
270
|
}
|
|
350
|
-
catch (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
...baseHookArguments,
|
|
358
|
-
error: _e,
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
throw _e;
|
|
271
|
+
catch (searchError) {
|
|
272
|
+
const googleAdsError = this.getGoogleAdsError(searchError);
|
|
273
|
+
if (this.hooks.onStreamError) {
|
|
274
|
+
await this.hooks.onStreamError({
|
|
275
|
+
...baseHookArguments,
|
|
276
|
+
error: googleAdsError,
|
|
277
|
+
});
|
|
362
278
|
}
|
|
279
|
+
throw googleAdsError;
|
|
363
280
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (!e?.response?.data) {
|
|
367
|
-
throw e;
|
|
281
|
+
finally {
|
|
282
|
+
stream.destroy();
|
|
368
283
|
}
|
|
369
|
-
// The error is a stream, so some effort is required to parse it.
|
|
370
|
-
const stream = e.response.data;
|
|
371
|
-
const pipeline = (0, stream_chain_1.chain)([stream, (0, stream_json_1.parser)(), (0, StreamArray_1.streamArray)()]);
|
|
372
|
-
const defaultErrorMessage = "Unknown GoogleAdsFailure";
|
|
373
|
-
let googleAdsFailure = new Error(defaultErrorMessage);
|
|
374
|
-
// Only throw the first error.
|
|
375
|
-
pipeline.once("data", (data) => {
|
|
376
|
-
if (data?.value?.error?.details?.[0]) {
|
|
377
|
-
googleAdsFailure = new protos_1.errors.GoogleAdsFailure(this.decamelizeKeysIfNeeded(data.value.error.details[0]));
|
|
378
|
-
}
|
|
379
|
-
else {
|
|
380
|
-
googleAdsFailure = new Error(data?.value?.error?.message ?? defaultErrorMessage, { cause: data?.value?.error ?? data?.value });
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
// Must always reject.
|
|
384
|
-
await new Promise((_, reject) => {
|
|
385
|
-
pipeline.on("end", () => reject(googleAdsFailure));
|
|
386
|
-
pipeline.on("error", (err) => reject(err));
|
|
387
|
-
});
|
|
388
284
|
}
|
|
389
285
|
/**
|
|
390
286
|
* @description Creates, updates, or removes resources. This method supports atomic transactions
|
|
@@ -464,28 +360,5 @@ class Customer extends serviceFactory_1.default {
|
|
|
464
360
|
},
|
|
465
361
|
};
|
|
466
362
|
}
|
|
467
|
-
prepareGoogleAdsServicePostRequestArgs(functionName, accessToken, extra) {
|
|
468
|
-
return {
|
|
469
|
-
method: "POST",
|
|
470
|
-
url: `https://googleads.googleapis.com/${version_1.googleAdsVersion}/customers/${this.customerOptions.customer_id}/googleAds:${functionName}`,
|
|
471
|
-
headers: {
|
|
472
|
-
Authorization: `Bearer ${accessToken}`,
|
|
473
|
-
...this.callHeaders,
|
|
474
|
-
},
|
|
475
|
-
...extra,
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
|
-
decamelizeKeysIfNeeded(input) {
|
|
479
|
-
if (this.clientOptions.disable_parsing) {
|
|
480
|
-
return input;
|
|
481
|
-
}
|
|
482
|
-
return (0, parserRest_1.decamelizeKeys)(input);
|
|
483
|
-
}
|
|
484
|
-
gaqlQueryStringIncludesLimit(gaqlQuery) {
|
|
485
|
-
return gaqlQuery.toLowerCase().includes("limit ");
|
|
486
|
-
}
|
|
487
|
-
generateTooManyRowsError() {
|
|
488
|
-
return new Error(`Exceeded the maximum number of rows set by "max_reporting_rows" (${this.clientOptions.max_reporting_rows}).`);
|
|
489
|
-
}
|
|
490
363
|
}
|
|
491
364
|
exports.Customer = Customer;
|