Skip to content

Commit

Permalink
Tentacles v4.3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
lushu committed Mar 5, 2024
1 parent 598dca9 commit e52bbe5
Show file tree
Hide file tree
Showing 49 changed files with 3,282 additions and 1,210 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ and [Data Tasks Coordinator]. This library includes:
- Google Ads click conversions upload
- Google Ads customer match upload
- Google Ads enhanced conversions upload
- Google Ads offline userdata job data upload
- Google Ads conversions scheduled uploads based on Google Sheets
- Measurement Protocol Google Analytics 4

1. Wrapper for some Google APIs for reporting, mainly
2. Wrapper for some Google APIs for reporting, mainly
for [Data Tasks Coordinator]:

- Google Ads reporting
Expand All @@ -31,7 +32,7 @@ and [Data Tasks Coordinator]. This library includes:
- YouTube Data API
- Ads Data Hub querying

1. Utilities wrapper class for Google Cloud Products:
3. Utilities wrapper class for Google Cloud Products:

- **Firestore Access Object**: Firestore has two modes[[comparison]] which
have different API. This class, with its two successors, offer a unified
Expand Down Expand Up @@ -66,7 +67,7 @@ and [Data Tasks Coordinator]. This library includes:
an adapter to wrap a Node8 Cloud Functions into Node6 and Node8 compatible
functions.~~ (This has been removed since v1.9.0)

1. A share library for [Bash] to facilitate installation tasks.
4. A share library for [Bash] to facilitate installation tasks.

[gmp and google ads connector]: https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/gmp-googleads-connector
[data tasks coordinator]: https://github.com/GoogleCloudPlatform/cloud-for-marketing/tree/master/marketing-analytics/activation/data-tasks-coordinator
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Release Notes

## 2.1.0 (2024-03-05)

### Update to the v3 of the DV360 API
### Update to the v16 of the Google Ads API

## 2.0.0 (2023-11-24)

### Firestore's non-default database is supported
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# limitations under the License.

# Google Ads API version
GOOGLE_ADS_API_VERSION=13
GOOGLE_ADS_API_VERSION=16

#######################################
# Verify whether the current OAuth token, CID and developer token can work.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@google-cloud/nodejs-common",
"version": "2.0.0",
"version": "2.1.0",
"description": "A NodeJs common library for solutions based on Cloud Functions",
"author": "Google Inc.",
"license": "Apache-2.0",
Expand All @@ -16,23 +16,24 @@
},
"homepage": "https://github.com/GoogleCloudPlatform/cloud-for-marketing/blob/master/marketing-analytics/activation/common-libs/nodejs-common/README.md",
"dependencies": {
"@google-cloud/aiplatform": "^3.0.0",
"@google-cloud/automl": "^4.0.0",
"@google-cloud/bigquery": "^7.2.0",
"@google-cloud/datastore": "^8.2.2",
"@google-cloud/firestore": "^6.7.0",
"@google-cloud/aiplatform": "^3.10.0",
"@google-cloud/automl": "^4.0.1",
"@google-cloud/bigquery": "^7.3.0",
"@google-cloud/datastore": "^8.4.0",
"@google-cloud/firestore": "^7.2.0",
"@google-cloud/logging-winston": "^6.0.0",
"@google-cloud/pubsub": "^4.0.2",
"@google-cloud/storage": "^7.0.1",
"@google-cloud/scheduler": "^4.0.0",
"@google-cloud/secret-manager": "^5.0.0",
"gaxios": "^6.1.0",
"@google-cloud/pubsub": "^4.1.1",
"@google-cloud/storage": "^7.7.0",
"@google-cloud/scheduler": "^4.0.1",
"@google-cloud/secret-manager": "^5.0.1",
"gaxios": "^6.1.1",
"google-ads-nodejs-client": "16.0.0",
"google-ads-api": "^14.1.0",
"google-ads-node": "^12.0.2",
"google-auth-library": "^9.0.0",
"googleapis": "^126.0.1",
"google-auth-library": "^9.4.2",
"googleapis": "^131.0.0",
"winston": "^3.10.0",
"@grpc/grpc-js": "1.9.5",
"@grpc/grpc-js": "1.9.14",
"lodash": "^4.17.21"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class Analytics {
}

/**
* Prepares the Google Analytics instace.
* Prepares the Google Analytics instance.
* @return {!google.analytics}
* @private
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@

const fs = require('fs');
const path = require('path');
const {GoogleAuth, OAuth2Client, JWT, Compute} = require('google-auth-library');
const {
GoogleAuth,
OAuth2Client,
UserRefreshClient,
JWT,
Compute,
} = require('google-auth-library');
const { SecretManager } = require('../components/secret_manager.js');
const { getLogger } = require('../components/utils.js');

Expand Down Expand Up @@ -77,6 +83,7 @@ class AuthClient {
this.logger = getLogger('AUTH');
this.scopes = scopes;
this.env = Object.assign({}, process.env, overwrittenEnv);
this.initialized = false;
}

/**
Expand All @@ -88,33 +95,35 @@ class AuthClient {
* and service account key file if there is no secret name was set in the env.
*/
async prepareCredentials() {
if (this.initialized === true) {
this.logger.info(`This authClient has been initialized.`);
return;
}
if (this.env[DEFAULT_ENV_SECRET]) {
const secretmanager = new SecretManager({
projectId: this.env.GCP_PROJECT,
});
const secret = await secretmanager.access(this.env[DEFAULT_ENV_SECRET]);
if (secret) {
const secretObj = JSON.parse(secret);
if (secretObj.token) {
this.oauthToken = secretObj;
} else {
this.serviceAccountKey = secretObj;
}
if (secretObj.token) this.oauthToken = secretObj;
else this.serviceAccountKey = secretObj;
this.logger.info(`Get secret from SM ${this.env[DEFAULT_ENV_SECRET]}.`);
return;
} else {
this.logger.warn(`Cannot find SM ${this.env[DEFAULT_ENV_SECRET]}.`);
}
} else {// To be compatible with previous solution.
const oauthTokenFile = this.getContentFromEnvVar(DEFAULT_ENV_OAUTH);
if (oauthTokenFile) {
this.oauthToken = JSON.parse(oauthTokenFile);
}
const serviceAccountKeyFile =
this.getContentFromEnvVar(DEFAULT_ENV_KEYFILE);
if (serviceAccountKeyFile) {
this.serviceAccountKey = JSON.parse(serviceAccountKeyFile);
}
this.logger.warn(`Cannot find SM ${this.env[DEFAULT_ENV_SECRET]}.`);
}
// To be compatible with previous solution.
const oauthTokenFile = this.getContentFromEnvVar(DEFAULT_ENV_OAUTH);
if (oauthTokenFile) {
this.oauthToken = JSON.parse(oauthTokenFile);
}
const serviceAccountKeyFile =
this.getContentFromEnvVar(DEFAULT_ENV_KEYFILE);
if (serviceAccountKeyFile) {
this.serviceAccountKey = JSON.parse(serviceAccountKeyFile);
}
this.initialized = true;
}

/**
Expand All @@ -137,7 +146,7 @@ class AuthClient {
* 1. OAuth, return an OAuth token if available.
* 2. JWT, return JWT client if a service account key is available.
* 3. ADC if none of these files exists.
* @return {!OAuth2Client|!JWT|!Compute}
* @return {!UserRefreshClient|!JWT|!Compute}
*/
getDefaultAuth() {
if (typeof this.oauthToken !== 'undefined') {
Expand Down Expand Up @@ -167,13 +176,14 @@ class AuthClient {

/**
* Returns an OAuth2 client based on the given key file.
* @return {!OAuth2Client}
* @return {!UserRefreshClient}
*/
getOAuth2Client() {
this.ensureCredentialExists_(this.oauthToken, 'OAuth token');
const { client_id, client_secret, token } = this.oauthToken;
const oAuth2Client = new OAuth2Client(client_id, client_secret);
oAuth2Client.setCredentials({ refresh_token: token.refresh_token });
const { client_id, client_secret, token: { refresh_token } }
= this.oauthToken;
const oAuth2Client =
new UserRefreshClient(client_id, client_secret, refresh_token);
return oAuth2Client;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2024 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this fileAccessObject except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Common functions for Google Ads and Search Ads API classes.
*/

'use strict';

const { Transform } = require('stream');
const {
extractObject,
changeObjectNamingFromLowerCamelToSnake,
getLogger,
} = require('../../components/utils.js');

/**
* Returns a integer format CID by removing dashes and spaces.
* @param {string} cid
* @return {string}
*/
function getCleanCid(cid) {
return cid.toString().trim().replace(/-/g, '');
}

const START_TAG = '"results":';
const FIELD_MASK_TAG = '"fieldMask"';
const END_TAG = '"requestId"';

/**
* Generates a function that can convert a given JSON object to a JSON string
* with only specified fields(fieldMask), in specified naming convention.
* @param {string} fieldMask The 'fieldMask' string from response.
* @param {boolean=} snakeCase Whether or not output JSON in snake naming.
*/
function generateProcessFn(fieldMask, snakeCase = false) {
const extractor = extractObject(fieldMask.split(','));
return (originalObject) => {
const extracted = extractor(originalObject);
const generatedObject = snakeCase
? changeObjectNamingFromLowerCamelToSnake(extracted) : extracted;
return JSON.stringify(generatedObject);
};
};

/**
* A stream.Transform that can extract properties and convert naming of the
* reponse of Google/Search Ads report from REST interface.
*/
class RestSearchStreamTransform extends Transform {

/**
* @constructor
* @param {boolean=} snakeCase Whether or not output JSON in snake naming.
*/
constructor(snakeCase = false) {
super({ objectMode: true });
this.snakeCase = snakeCase;
this.chunks = [Buffer.from('')];
this.processFn; // The function to process a row of the report.
this.logger = getLogger('ADS.STREAM.T');
this.stopwatch = Date.now();
}

_transform(chunk, encoding, callback) {
const latest = Buffer.concat([this.chunks[this.chunks.length - 1], chunk]);
const endIndex = latest.indexOf(END_TAG);
if (endIndex > -1) {
this.chunks.push(chunk);
const rawString = Buffer.concat(this.chunks).toString();
const startIndex = rawString.indexOf(START_TAG) + START_TAG.length;
const maskIndex = rawString.lastIndexOf(FIELD_MASK_TAG);
if (!this.processFn) {
const fieldMask = rawString
.substring(maskIndex + FIELD_MASK_TAG.length, rawString.indexOf(END_TAG))
.split('"')[1];
this.logger.debug(`Got fieldMask: ${fieldMask}`);
this.processFn = generateProcessFn(fieldMask, this.snakeCase);
}
const resultsWithTailing = rawString.substring(startIndex, maskIndex);
const results = resultsWithTailing.substring(
0, resultsWithTailing.lastIndexOf(','));
const rows = JSON.parse(results);
const data = rows.map(this.processFn).join('\n') + '\n';
// Clear cached chunks.
this.chunks = [latest.subarray(latest.indexOf(END_TAG) + END_TAG.length)];

this.logger.debug(`Got ${rows.length} rows. Process time:`,
Date.now() - this.stopwatch);
this.stopwatch = Date.now();
callback(null, data);
} else {
if (chunk.length < END_TAG.length) {// Update latest chunk for short chunk
this.chunks[this.chunks.length - 1] = latest;
} else {
this.chunks.push(chunk);
}
callback();
}
}
}

module.exports = {
getCleanCid,
RestSearchStreamTransform,
};
Loading

0 comments on commit e52bbe5

Please sign in to comment.