Opteo / google-ads-api

Google Ads API client library for Node.js
https://opteo.com
MIT License
270 stars 90 forks source link

Just looking for some guidance: How would I create a new negative keyword list, or view a search terms array? #405

Closed sgpascoe closed 1 year ago

sgpascoe commented 2 years ago

I don't see anything in the documentation that touches on search terms reports or negative keyword lists.

Is there an example of how to simply export 30 days of search terms, or create a new negative keyword list with one keyword?

wcoots commented 2 years ago

Hello @sgpascoe. To query the last 30 days of search terms I would recommend using the report method with a query like so:

const search_terms = customer.report({
    entity: 'search_term_view',
    attributes: ['search_term_view.search_term', ... ],
    metrics: ['metrics.clicks', ... ],
    from_date: '2022-05-25', // 30 days ago
})

If you are getting very large numbers of search terms then you may be better of streaming the results in using the reportStream method. For more information on the search term view resource see here.

To create a new negative keyword list containing a keyword, you will need to perform 2 separate mutations. First a create operation on the shared set service. This will return the resource name of the new shared set. You will then need to perform a second create operation on the shared criteria service using your new shared set's resource name.

const { results: [{ resource_name }] } = await customer.sharedSets.create([
    {
        name: 'new shared set',
        type: enums.SharedSetType.NEGATIVE_KEYWORDS,
        status: enums.SharedSetStatus.ENABLED,
    },
])

await customer.sharedCriteria.create([
    {
        shared_set: resource_name,
        keyword: {
            text: 'new keyword',
            match_type: enums.KeywordMatchType.EXACT, // or BROAD or PHRASE
        },
    },
])
sgpascoe commented 2 years ago

That's fantastic!

I've got the first example up and running now, but struggling with only returning the actual search term.

I've tried search_terms.attributes or search_terms.search_term_view or search_terms.search_term_view.search_term but I can't seem to find the right locator - am I misunderstanding how this object is being returned?

Apologies, I know this is probably very basic stuff

wcoots commented 2 years ago

The search_term_view.search_term attribute should be the actual search term. The returned object will always have the attributes, metrics and segments that you have specified in your query/report.

sgpascoe commented 2 years ago

This did the job!

var stfinaldata = []; const stimportdata = Object.values(result); for(let i = 0; i < stimportdata.length; i++){ let stnarrowdata = Object.values(stimportdata[i]); let stsinglefinal = stnarrowdata[0].search_term; stfinaldata.push(stsinglefinal); }

sgpascoe commented 2 years ago

@WillCooter It currently says message: 'A shared set with this name already exists.',

I'm looking for any reference to customer.sharedSets.create within Google's API documentation and trying to tie it to the links above but I can't see anything that fits. How would I create an if statement to see if this shared set already exists?

also, even when there is no existing shared set, the keyword creation is returning

GoogleAdsError { error_code: ErrorCode { field_error: 2 }, message: 'The required field was not present.', location: ErrorLocation { field_path_elements: [Array] } }

sgpascoe commented 2 years ago

OK, So I've figured the Second error is due to me trying to use a variable for the match type

let matchType = BROAD;
await customer.sharedCriteria.create([
    {
        shared_set: resource_name,
        keyword: {
            text: 'new keyword',
            match_type: enums.KeywordMatchType.matchType , 
        },
    },
])

Does not work, so instead I have

if (matchtype == 'BROAD'){
    await customer.sharedCriteria.create([
      {
          shared_set: resource_name,
          keyword: {
              text: negativeword,
              match_type: enums.KeywordMatchType.BROAD, 
          },
      },
  ])
  }
  else if (matchtype == 'EXACT'){
    await customer.sharedCriteria.create([
      {
          shared_set: resource_name,
          keyword: {
              text: negativeword,
              match_type: enums.KeywordMatchType.EXACT, 
          },
      },
  ])
  }
  else if (matchtype == 'PHRASE'){
    await customer.sharedCriteria.create([
      {
          shared_set: resource_name,
          keyword: {
              text: negativeword,
              match_type: enums.KeywordMatchType.PHRASE, 
          },
      },
  ])
  }

and although not as elegant, it does the job.

I think for the first error I'm just getting hung up on the line const { results: [{ resource_name }] } = await customer.sharedSets.create([ as I can't seem to use it in a separate try/catch function to the keyword creation, as the keyword creation then can't find resource_name.

I either need a way to define it in a way that can be called from a separate function (and ignore the shared set error in the first function, continuing with the keyword creation function seperately) or a way to check that the shared set is not already created.

I've been doing a lot of trial and error and reading within the API using the links you provided, and solved the majority of my issues, but I feel this is a little obfuscated for me to work out unfortunately.

Thanks so much for your guidance so far, it's really helping me to learn, but I feel I am a long way from understanding how the API works.

sgpascoe commented 2 years ago

@WillCooter I'm now returning resource_name successfully from my first function, to create the shared set.

However I'm still stuck on the last hurdle. If the set already exists, it catches the error and does not return a value for resource_name. Is there a method for simply checking to see if a shared set exists? I imagine I need to define the sharedset without using a create operation, and then somehow return the sharedset.status, but I'm unsure how to do that. I've been experimenting with various things, but the segmented nature of the Ads API reference is making it difficult for me to understand how to turn single references into full code!

I imagine to check if it already exists, I essentially want to say:

SELECT
shared_set.status
FROM shared_set
WHERE
shared_set.name = 'General Negatives'
AND shared_set.type = 'NEGATIVE_KEYWORDS'
sgpascoe commented 2 years ago

ok... This is how I now check if a list exists, create it if it doesn't, and return it if it does.

async function createnewlist(){
  console.log('Checking for negative list "General Negatives"')
  var newset = await customer.query(`
    SELECT 
      shared_set.status, 
      shared_set.name, 
      shared_set.resource_name, 
      shared_set.type, 
      shared_set.member_count, 
      shared_set.id, 
      shared_set.reference_count 
  FROM shared_set 
  WHERE 
    shared_set.name = 'General Negatives' 
    AND shared_set.status = 'ENABLED' 
    AND shared_set.type = 'NEGATIVE_KEYWORDS' 
  `)

  if (typeof newset[0] === "undefined") {

    console.log('No Negative list found. Creating one now.')

    try {
      const { results: [{ resource_name }] } = await customer.sharedSets.create([
        {
          name: 'General Negatives',
          type: enums.SharedSetType.NEGATIVE_KEYWORDS,
          status: enums.SharedSetStatus.ENABLED,
        }
      ])

      console.log('resource name is',resource_name);
      return resource_name;

    } catch(err) {
      if (err instanceof errors.GoogleAdsFailure) {
        console.log('Ads/Adding Keyword to Negative List - ERROR',err.errors); // Array of errors.GoogleAdsError instances
        // Get the first one and explicitly check for a certain error type
        const [firstError] = err.errors;
        if (firstError.error_code === errors.QueryErrorEnum.QueryError.UNRECOGNIZED_FIELD) {
          console.log(`Error: using invalid field "${firstError.trigger}" in query`);
        } else if (firstError.error_code === errors.QueryErrorEnum.DUPLICATE_NAME) {
          console.log('duplicate name error: shared set exists already');
        }
    }
  } else { 
    existList = newset[0].shared_set.resource_name
    console.log('newset 0:',await existList)
    console.log("There is an existing list :",existList,', sending it to the negative keyword importer function')}
    return existList
  }
}

Today I have a whole new error, I didn't change anything since I had it working last night:

  code: 16,
  details: 'Failed to retrieve auth metadata with error: invalid_grant',
  metadata: Metadata { internalRepr: Map(0) {}, options: {} },
  note: 'Exception occurred in retry method that was not classified as transient'
wcoots commented 2 years ago

@sgpascoe This error is coming from Google rather from this library, I am not sure what it means. You will have to consult Google's API docs or ask the folks over on the Google Ads API forum.

sgpascoe commented 2 years ago

@sgpascoe This error is coming from Google rather from this library, I am not sure what it means. You will have to consult Google's API docs or ask the folks over on the Google Ads API forum.

Thanks @WillCooter. Looking closer it is an error that flags an issue with the refresh token. I've been using this: https://refresh-token-helper.opteo.com/ is this not correct, or do they expire?

Does my code above look sound, or is there a quicker way in place to make this check?

sgpascoe commented 2 years ago

@WillCooter After attaining a new refresh token and fixing the issue a week ago, I am today getting the same error. Is there a problem with the refresh token generator above?