Skip to content

Commit

Permalink
New "Promotion Mode"
Browse files Browse the repository at this point in the history
- Implement config switching based on promotion flag
- Modify image services to use CONFIG or PROMOTION_CONFIG as appropriate
- Skip asset deletion in promotion mode for ImageExtensionService
- Add new ImagePauseService for pausing uploaded assets
  • Loading branch information
halelidan committed Oct 8, 2024
1 parent e1c51b4 commit fef66a3
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 130 deletions.
19 changes: 18 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ interface Config {
'VertexAI Api Domain Part'?: string;
'Gemini Model'?: string;
'Image Generation Model'?: string;
'Is Promotion Mode': string;
}

export const sheet =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Config')!;
const DEFAULT_CONFIG = {
export const promotionSheet =
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Promotion_Config')!;

const DEFAULT_CONFIG: Config = {
'Ads API Key': '',
'Manager ID': '',
'Account ID': '',
Expand Down Expand Up @@ -79,6 +83,7 @@ const DEFAULT_CONFIG = {
'VertexAI Api Domain Part': undefined,
'Gemini Model': undefined,
'Image Generation Model': undefined,
'Is Promotion Mode': 'no',
};

export const ADIOS_MODES = {
Expand All @@ -95,3 +100,15 @@ export const CONFIG: Config =
.reduce((res, e) => {
return { ...res, [e[0]]: e[1] };
}, DEFAULT_CONFIG) ?? DEFAULT_CONFIG;

export const PROMOTION_CONFIG: Config =
{
...promotionSheet
?.getRange('A2:B')
.getDisplayValues()
.filter(e => e[0])
.reduce((res, e) => {
return { ...res, [e[0]]: e[1] };
}, DEFAULT_CONFIG),
'Is Promotion Mode': 'yes', // Set to 'true' for promotion config
} ?? DEFAULT_CONFIG;
33 changes: 17 additions & 16 deletions src/frontend-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CONFIG } from './config';
import { CONFIG, PROMOTION_CONFIG } from './config';
import { GcsApi } from './gcs-api';
import {
ImagePolicyViolations,
PolicyViolation,
POLICY_VIOLATIONS_FILE,
PolicyViolation,
} from './gemini-validation-service';
import { GoogleAdsApiFactory } from './google-ads-api-mock';

Expand Down Expand Up @@ -51,8 +51,9 @@ export interface Image {
selected?: boolean;
issues?: ImageIssue[];
}

const gcsApi = new GcsApi(CONFIG['GCS Bucket']);
const isPromotionMode = CONFIG['Is Promotion Mode'] === 'yes';
const config = isPromotionMode ? PROMOTION_CONFIG : CONFIG;
const gcsApi = new GcsApi(config['GCS Bucket']);

const getData = () => {
const gcsImages = gcsApi.listAllImages(GoogleAdsApiFactory.getAdsAccountId());
Expand All @@ -65,22 +66,22 @@ const getData = () => {
const statusFolder = e.name.split('/')[2];
let status: IMAGE_STATUS | null = null;
switch (statusFolder) {
case CONFIG['Generated DIR']:
case config['Generated DIR']:
status = IMAGE_STATUS.GENERATED;
break;
case CONFIG['Uploaded DIR']:
case config['Uploaded DIR']:
status = IMAGE_STATUS.UPLOADED;
break;
case CONFIG['Validated DIR']:
case config['Validated DIR']:
status = IMAGE_STATUS.VALIDATED;
break;
case CONFIG['Disapproved DIR']:
case config['Disapproved DIR']:
status = IMAGE_STATUS.DISAPPROVED;
break;
case CONFIG['Bad performance DIR']:
case config['Bad performance DIR']:
status = IMAGE_STATUS.BAD_PERFORMANCE;
break;
case CONFIG['Rejected DIR']:
case config['Rejected DIR']:
status = IMAGE_STATUS.REJECTED;
break;
default:
Expand All @@ -99,7 +100,7 @@ const getData = () => {

adGroups[adGroupId].push({
filename,
url: `https://storage.mtls.cloud.google.com/${CONFIG['GCS Bucket']}/${e.name}`,
url: `https://storage.mtls.cloud.google.com/${config['GCS Bucket']}/${e.name}`,
status,
issues,
});
Expand All @@ -116,17 +117,17 @@ const getData = () => {
};

const setImageStatus = (images: Image[], status: IMAGE_STATUS) => {
const gcsApi = new GcsApi(CONFIG['GCS Bucket']);
const gcsApi = new GcsApi(config['GCS Bucket']);
images.forEach(image => {
const adGroupId = image.filename.split('|')[0];
gcsApi.moveImage(
GoogleAdsApiFactory.getAdsAccountId(),
adGroupId,
image.filename,
CONFIG['Generated DIR'],
config['Generated DIR'],
status === IMAGE_STATUS.VALIDATED
? CONFIG['Validated DIR']
: CONFIG['Rejected DIR']
? config['Validated DIR']
: config['Rejected DIR']
);
});
};
Expand Down Expand Up @@ -164,7 +165,7 @@ class PolicyStatusByAdGroup {
[filename: string]: PolicyViolation[];
} {
const fullName = `${GoogleAdsApiFactory.getAdsAccountId()}/${adGroupId}/${
CONFIG['Generated DIR']
config['Generated DIR']
}/${POLICY_VIOLATIONS_FILE}`;

try {
Expand Down
16 changes: 16 additions & 0 deletions src/google-ads-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ export class GoogleAdsApi implements GoogleAdsApiInterface {
});
}

pauseAdGroupAssets(resourceNames: string[]) {
const operations = resourceNames.map(resourceName => ({
update: {
resource_name: resourceName,
status: 'PAUSED',
},
update_mask: {
paths: ['status'],
},
}));
return this.post('/adGroupAssets:mutate', {
customer_id: this._customerId,
operations,
});
}

deleteExtensionFeedItem(resourceName?: string) {
if (!resourceName) {
return;
Expand Down
88 changes: 54 additions & 34 deletions src/image-extension-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { CONFIG } from './config';
import { CONFIG, PROMOTION_CONFIG } from './config';
import { GcsApi } from './gcs-api';
import { GoogleAdsApi } from './google-ads-api';
import { Triggerable } from './triggerable';
Expand All @@ -23,24 +23,34 @@ export class ImageExtensionService extends Triggerable {

private readonly _gcsApi;
private readonly _googleAdsApi;
private readonly _config;
private readonly _isPromotionMode: boolean;

constructor() {
constructor(isPromotionMode: boolean) {
super();
this._gcsApi = new GcsApi(CONFIG['GCS Bucket']);
this._isPromotionMode = isPromotionMode;
this._config = this._isPromotionMode ? PROMOTION_CONFIG : CONFIG;
Logger.log(
`Config sheet chosen is: ${
this._isPromotionMode ? 'PROMOTION_CONFIG' : 'CONFIG'
}`
);
this._gcsApi = new GcsApi(this._config['GCS Bucket']);
this._googleAdsApi = new GoogleAdsApi(
CONFIG['Ads API Key'],
CONFIG['Manager ID'],
CONFIG['Account ID']
this._config['Ads API Key'],
this._config['Manager ID'],
this._config['Account ID']
);
}

run() {
this.deleteTrigger();
const adGroups = this._googleAdsApi.getAdGroups();
const lastProcessedKey = this._isPromotionMode
? 'lastPromotionImageExtensionProcessedAdGroupId'
: 'lastImageExtensionProcessedAdGroupId';
const lastImageExtensionProcessedAdGroupId =
PropertiesService.getScriptProperties().getProperty(
'lastImageExtensionProcessedAdGroupId'
);
PropertiesService.getScriptProperties().getProperty(lastProcessedKey);
let startIndex = 0;
if (lastImageExtensionProcessedAdGroupId) {
const lastIndex = adGroups.findIndex(
Expand All @@ -52,10 +62,10 @@ export class ImageExtensionService extends Triggerable {
const adGroup = adGroups[i];
if (this.shouldTerminate()) {
Logger.log(
`The function is reaching the 6 minute timeout, and therfore will create a trigger to rerun from this ad group: ${adGroup.adGroup.name} and then self terminate.`
`The function is reaching the 6 minute timeout, and therefore will create a trigger to rerun from this ad group: ${adGroup.adGroup.name} and then self terminate.`
);
PropertiesService.getScriptProperties().setProperty(
'lastImageExtensionProcessedAdGroupId',
lastProcessedKey,
adGroup.adGroup.id
);
this.createTriggerForNextRun();
Expand All @@ -65,8 +75,8 @@ export class ImageExtensionService extends Triggerable {
`Processing Ad Group ${adGroup.adGroup.name} (${adGroup.adGroup.id})...`
);
const uploadedToGcsImages = this._gcsApi
.listImages(CONFIG['Account ID'], adGroup.adGroup.id, [
CONFIG['Uploaded DIR'],
.listImages(this._config['Account ID'], adGroup.adGroup.id, [
this._config['Uploaded DIR'],
])
?.items?.map(e => e.name.split('/').slice(-1)[0]);
const notLinkedAssets = this._googleAdsApi
Expand All @@ -92,54 +102,64 @@ export class ImageExtensionService extends Triggerable {
notLinkedAssets
);
}
const adGroupAssetsToDelete = this._googleAdsApi
.getAdGroupAssetsForAdGroup(adGroup.adGroup.id)
.filter(e => !uploadedToGcsImages?.includes(e.asset.name))
.map(e => e.adGroupAsset.resourceName);
if (adGroupAssetsToDelete?.length > 0) {
Logger.log(
`Deleting ${adGroupAssetsToDelete.length} ad group assets for ad group ${adGroup.adGroup.id}...`
);
this._googleAdsApi.deleteAdGroupAssets(adGroupAssetsToDelete);
// Only delete assets if not in promotion mode
if (!this._isPromotionMode) {
const adGroupAssetsToDelete = this._googleAdsApi
.getAdGroupAssetsForAdGroup(adGroup.adGroup.id)
.filter(e => !uploadedToGcsImages?.includes(e.asset.name))
.map(e => e.adGroupAsset.resourceName);
if (adGroupAssetsToDelete?.length > 0) {
Logger.log(
`Deleting ${adGroupAssetsToDelete.length} ad group assets for ad group ${adGroup.adGroup.id}...`
);
this._googleAdsApi.deleteAdGroupAssets(adGroupAssetsToDelete);
}
} else {
Logger.log('Skipping deletion of existing assets in promotion mode.');
}
PropertiesService.getScriptProperties().setProperty(
'lastImageExtensionProcessedAdGroupId',
lastProcessedKey,
adGroup.adGroup.id
);
}
Logger.log('Finished Extension Process.');
//If script completes without timing out, clear the stored ad group ID and any triggers
PropertiesService.getScriptProperties().deleteProperty(
'lastImageExtensionProcessedAdGroupId'
);
PropertiesService.getScriptProperties().deleteProperty(lastProcessedKey);
this.deleteTrigger();
}

static triggeredRun() {
const isPromotionMode = CONFIG['Is Promotion Mode'] === 'yes';
Logger.log(`triggeredRun method:`);
Logger.log(`Is Promotion Mode: ${isPromotionMode}`);

PropertiesService.getScriptProperties().setProperty(
`${ImageExtensionService.name}StartTime`,
new Date().getTime().toString()
);
const imageExtensionService = new ImageExtensionService();
const imageExtensionService = new ImageExtensionService(isPromotionMode);
imageExtensionService.run();
}

static manuallyRun() {
const isPromotionMode = CONFIG['Is Promotion Mode'] === 'yes';
Logger.log(`manuallyRun method:`);
Logger.log(`Is Promotion Mode: ${isPromotionMode}`);

PropertiesService.getScriptProperties().setProperty(
`${ImageExtensionService.name}StartTime`,
new Date().getTime().toString()
);
const lastProcessedKey = isPromotionMode
? 'lastPromotionImageExtensionProcessedAdGroupId'
: 'lastImageExtensionProcessedAdGroupId';
const lastImageExtensionProcessedAdGroupId =
PropertiesService.getScriptProperties().getProperty(
'lastImageExtensionProcessedAdGroupId'
);
PropertiesService.getScriptProperties().getProperty(lastProcessedKey);
if (lastImageExtensionProcessedAdGroupId) {
PropertiesService.getScriptProperties().deleteProperty(
'lastImageExtensionProcessedAdGroupId'
);
PropertiesService.getScriptProperties().deleteProperty(lastProcessedKey);
Logger.log('Cleared last processed Ad Group ID for a fresh manual run.');
}
const imageExtensionService = new ImageExtensionService();
const imageExtensionService = new ImageExtensionService(isPromotionMode);
imageExtensionService.run();
}
}
Loading

0 comments on commit fef66a3

Please sign in to comment.