Repository: PipedreamHQ/pipedream
Pull request #16378: [Components] google_search_console : add “Retrieve Site Performance” & “Submit URL for Indexing” actions
Author: login: SokolovskyiK
Labels: name: User submitted
description: Submitted by a user
Created: 2025-04-21T18:41:29Z | Merged: 2025-04-25T14:56:42Z

Fixes #15016
 
###  Note

I’d love to contribute more in the future — so if anything I’ve done here is non-idiomatic or misaligned with Pipedream practices, , I’m genuinely happy to adjust and improve.

---

##  What’s Included

This PR introduces two new actions for the Google Search Console integration:

- **Retrieve Site Performance Data**  
  Fetches search analytics with flexible filtering, dimensions, and pagination.

- **Submit URL for Indexing**  
  Notifies Google that a page's content has been updated.  
  ⚠️ _Note: The standard Google Search Console app does **not** include the required [Indexing API scope](https://www.googleapis.com/auth/indexing) so this action was tested using a manually generated token with appropriate permissions._

---

##  Input Validation Philosophy

These actions include input validation before sending requests to the external API, and they clearly identify **which field** caused the error when one is detected.  
While some users may expect to receive raw API errors (e.g. 400, 403, etc.), this validation is intentionally scoped to catch only **definitely invalid inputs** — not to override meaningful errors from the external service.

For inputs that appear **suspicious but are not clearly invalid**, the action does **not block execution**. Instead, it issues a **warning** in the UI to help users catch potential issues early without interrupting the workflow.

---

### Examples of Validation Behavior

#### 🛑 Inputs That **Throw an Error**
These inputs are considered definitely invalid, and the action will block execution:

- Malformed URLs (e.g., http://exa mple.com — contains spaces)
- Strings consisting only of whitespace
- Incorrect types (e.g., passing an array when a string is expected)
- Invalid date formats (must follow YYYY-MM-DD)

#### ⚠️ Inputs That **Trigger a Warning**
These inputs are suspicious but not clearly invalid. The action continues execution, but a warning is shown in the $summary:

- URLs containing backslashes (e.g., https:\\example.com)
- Zero-width or non-printable characters, or dubious characters like <, >, ", etc.
- Unusual formatting that may lead to API-side rejection

---

These issues are often mishandled by APIs or lead to unclear responses like "invalid url" or "bad request".  
Catching them early improves clarity, avoids unnecessary requests, and helps users debug faster.

I’m aware this introduces a layer of abstraction over the API’s native error handling, and I’m happy to revisit this if it becomes a problem.  
However, I believe the trade-off is justified — especially in light of the Pipedream documentation:

> _“Typically, integrating with these services requires custom code to manage connection logic, error handling, and more. Actions handle that for you. You only need to specify the parameters required for the action.”_

---

## ✅ Testing Notes

These actions were tested both **locally** (via custom test runners in the /tests folder) and through the **Pipedream UI** using real authenticated accounts and verified sites.

Test coverage includes:
- Valid requests with all optional fields
- Suspicious but non-blocking input (triggering warnings)
- Validation failures with malformed or incorrect data

---

## 📁 About the /tests Folder

You’ll notice a /tests folder included in this PR.

It contains local test runners and helper scripts used to simulate the $ context and invoke the actions outside the Pipedream platform.

While not required for production, it:
- Documents how the components were tested locally
- Provides a reproducible setup for future debugging
- May be helpful to other contributors or reviewers

> 🧹 **It’s 100% safe to delete the folder** if you prefer a cleaner repo — it has no effect on runtime or deployment.

---

Thanks again for the awesome platform!

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced actions for retrieving Google Search Console site performance data and submitting URLs for indexing via Google APIs.
- **Documentation**
  - Added and updated README files detailing available actions, usage instructions, authentication requirements, and example payloads for new features.
- **Tests**
  - Added local test scripts and data sets for validating input handling, URL and date validation, and simulating API interactions outside the production environment.
- **Chores**
  - Updated package version to 0.7.0.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #15016: [Components] google_search_console
# google_search_console
## URLs
- https://developers.google.com/webmaster-tools
## Actions
### submit-url-for-indexing
#### Description
Submit a URL for indexing in the specified site. Requires the user to provide the site property and the URL.
### retrieve-site-performance-data
#### Description
Retrieve performance data (e.g., clicks, impressions, CTR) for a specified site. The user can configure optional filters including start date, end date, and query strings.

Linked issue comments: user: login: vunguyenhung
created_at: 2025-04-25T03:07:06Z
body: Hi everyone, all test cases are passed! Ready for release! 

Test report
https://vunguyenhung.notion.site/Components-google_search_console-1e0bf548bb5e8192a154e3c61ea1fad2
author_association: CONTRIBUTOR

Diff:
diff --git a/components/google_search_console/README.md b/components/google_search_console/README.md
index cada189f1f1ec..a36ac08199d6a 100644
--- a/components/google_search_console/README.md
+++ b/components/google_search_console/README.md
@@ -9,3 +9,10 @@ The Google Search Console API opens a treasure trove of data and insights about
 - **Sync Search Results with Google Sheets**: Create a workflow that periodically pulls data from the Google Search Console API and adds it to a Google Sheet. This is useful for maintaining an evolving dataset for deeper analysis, historical reference, or sharing insights across teams without giving direct access to the Search Console.
 
 - **Automatic Sitemap Submission**: Set up a Pipedream workflow that triggers whenever a new sitemap is generated in your content management system (CMS). The workflow can then automatically submit the sitemap to Google Search Console via API, ensuring Google has the latest structure of your site for crawling and indexing.
+
+## Available Actions
+
+[Retrieve Site Performance Data](./actions/retrieve-site-performance-data/README.md)
+[Submit URL for Indexing](./actions/submit-url-for-indexing/README.md)
+
+
diff --git a/components/google_search_console/actions/retrieve-site-performance-data/README.md b/components/google_search_console/actions/retrieve-site-performance-data/README.md
new file mode 100644
index 0000000000000..a6ed4ab12ceb8
--- /dev/null
+++ b/components/google_search_console/actions/retrieve-site-performance-data/README.md
@@ -0,0 +1,108 @@
+# Google Search Console – Site Performance (Analytics) Action
+
+This action queries **search performance data** for a verified website using the [Google Search Console API](https://developers.google.com/webmaster-tools/search-console-api-original/v3/searchanalytics/query). It allows you to extract insights like:
+
+- Top search queries  
+- Click-through rates  
+- Page impressions  
+- Device types, countries, and more  
+
+---
+
+## Use Cases
+
+- Automate SEO reporting  
+- Analyze organic search trends  
+- Filter and break down traffic by dimensions (e.g. query, device, country)  
+
+---
+
+## Internals
+
+- Supports all relevant props from the [Search Analytics Query API](https://developers.google.com/webmaster-tools/search-console-api-original/v3/searchanalytics/query)  
+- Trims and validates all input props  
+- Accepts optional `dimensionFilterGroups` either as an object or JSON string  
+- Automatically builds the POST request body using `propsMeta` metadata  
+- Returns the raw Search Console API response for full access to metrics  
+- Accumulates and displays warnings for non-blocking input issues  
+
+---
+
+## Auth
+
+Requires OAuth 2.0 with the following scope: `https://www.googleapis.com/auth/webmasters.readonly`
+
+
+## Endpoint
+`https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query`
+
+
+## 📦 Example Request Payload
+
+
+
+```json
+{
+
+  "siteUrl": "https://falc1.com/page",
+
+  "startDate": "2025-12-22",
+
+  "endDate": "2025-12-31",
+
+  "dimensions": ["query", "page", "country", "device"],
+
+  "searchType": "web",
+
+  "rowLimit": 10,
+
+  "startRow": 0,
+
+  "aggregationType": "auto",
+
+  "dataState": "final",
+
+  "dimensionFilterGroups": [
+    {
+
+      "groupType": "and",
+
+      "filters": [
+        {
+
+          "dimension": "query",
+
+          "operator": "contains",
+
+          "expression": "example"
+        },
+        {
+          "dimension": "country",
+
+          "operator": "equals",
+
+          "expression": "USA"
+        }
+      ]
+    }
+  ]
+}
+```
+
+### Field Descriptions
+
+ **siteUrl**: The site you want to query data for. Must be verified in your Google Search Console account.
+ **startDate**: The start date of the reporting period (inclusive), in YYYY-MM-DD format.
+ **endDate**: The end date of the reporting period (inclusive), in YYYY-MM-DD format.
+ **dimensions**: The dimensions you want to break down the data by. Valid values: "query", "page", "country", "device", "searchAppearance", "date". Order matters — it affects how rows are grouped in the response.
+ **searchType**: The type of search data to include. Valid values: "web", "image", "video", "news", "googleNews", "discover"
+ **rowLimit**: Maximum number of rows to return (1–25,000)
+ **startRow**: Optional: Skips the first N rows — used for pagination.
+ **aggregationType**: Optional: How to group data. "auto" = Google's default grouping. "byPage" = Group by page (useful for getting per-page breakdowns).
+ **dataState**: Optional: Data freshness filter. "final" = Only finalized data (more accurate). "all" = Includes fresh but possibly incomplete data.
+ **dimensionFilterGroups**: Optional filter group(s) to restrict which rows are returned. Each group applies logical AND/OR across its filters.
+   **groupType**: Logical grouping operator for the filters inside this group. "and" = all filters must match, "or" = any filter can match
+   **filters**: List of individual filters to apply within the group
+      **dimension**: Which dimension to filter by (must match a dimension in your request)
+      **operator**: Filter operator — e.g., "equals", "contains", "notEquals", etc.
+      **expression**: Value to match against
\ No newline at end of file
diff --git a/components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs b/components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs
new file mode 100644
index 0000000000000..36b3f65858c2d
--- /dev/null
+++ b/components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs
@@ -0,0 +1,200 @@
+/**
+ * Fetches performance data (clicks, impressions, etc.) for a verified site
+ * via the Google Search Console Search Analytics API.
+ *
+ * Full usage docs in README.md
+ */
+
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../google_search_console.app.mjs";
+import {
+  removeCustomPropFields, trimIfString,
+} from "../../common/utils.mjs";
+
+/*
+  Define prop metadata separately, including custom fields used for extended validation
+  and runtime behavior.
+
+  These extended fields (like `extendedType`, `postBody`, etc.) are not part of the standard
+  Pipedream prop schema.
+
+  A helper function (`removeCustomPropFields`) will later strip these non-standard fields,
+  returning only valid Pipedream props for use in the UI.
+
+  Keeping the full metadata in closure allows access to helpful context (e.g. validation rules)
+  during runtime.
+*/
+const propsMeta = {
+
+  siteUrl: {
+    type: "string",
+    extendedType: "url",
+    label: "Verified Site URL",
+    description: "Including https:// is strongly advised",
+  },
+  startDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "Start Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  endDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "End Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  dimensions: {
+    type: "string[]",
+    label: "Dimensions",
+    optional: true,
+    description: "e.g. ['query', 'page', 'country', 'device']",
+    postBody: true,
+  },
+  searchType: {
+    type: "string",
+    label: "Search Type",
+    optional: true,
+    options: [
+      "web",
+      "image",
+      "video",
+      "news",
+      "googleNews",
+      "discover",
+    ],
+    default: "web",
+    postBody: true,
+  },
+  aggregationType: {
+    type: "string",
+    label: "Aggregation Type",
+    optional: true,
+    options: [
+      "auto",
+      "byPage",
+    ],
+    postBody: true,
+  },
+  rowLimit: {
+    type: "integer",
+    label: "Max rows to return",
+    default: 10,
+    postBody: true,
+  },
+  startRow: {
+    type: "integer",
+    label: "Start row (for pagination)",
+    optional: true,
+    postBody: true,
+  },
+  dimensionFilterGroups: {
+    type: "object",
+    label: "Dimension Filters",
+    optional: true,
+    description: "Follow Search Console API structure for filters",
+    postBody: true,
+  },
+  dataState: {
+    type: "string",
+    label: "Data State",
+    optional: true,
+    options: [
+      "all",
+      "final",
+    ],
+    default: "final",
+    postBody: true,
+  },
+};
+
+export default {
+  name: "Retrieve Site Performance Data",
+  description: "Fetches search analytics from Google Search Console for a verified site.",
+  key: "google_search_console-retrieve-site-performance-data",
+  version: "0.0.1",
+  type: "action",
+  props: {
+    gsConsole,
+    // Remove non-standard fields and expose only valid props to Pipedream UI
+    ...removeCustomPropFields(propsMeta),
+  },
+
+  //=================== RUN ==============================
+  //======================================================
+
+  async run({ $ }) {
+
+    /*
+      `dimensionFilterGroups` is expected to be an object.
+      If a JSON string is passed instead (e.g. from UI input), attempt to parse it.
+      - Returns parsed object if successful
+      - Returns original input if not a string
+      - Throws a descriptive error if JSON parsing fails
+    */
+    this.dimensionFilterGroups = this.gsConsole.parseIfJsonString(this.dimensionFilterGroups);
+
+    // Prepare the POST request payload
+    const body = {};
+
+    // Accumulator for non-blocking input warnings
+    const warnings = [];
+
+    /*
+      This loop:
+      - Trims and validates all defined props
+      - Skips empty optional fields
+      - Accumulates non-blocking warnings
+      - Adds valid props to the POST request payload (`body`) if marked with `postBody: true`
+    */
+    for (let propName in propsMeta) {
+
+      // Just for convenience.
+      const meta = propsMeta[propName];
+
+      // Trim the value if it's a string
+      this[propName] = trimIfString(this[propName]);
+
+      // Skip if the prop is optional and empty (null, undefined, or blank string)
+      if (meta.optional === true && ((this[propName] ?? "") === "")) continue;
+
+      // Validate input (may throw or return warning messages)
+      const validationResult = this.gsConsole.validateUserInput(meta, this[propName]);
+
+      // Push the warnings into warnings accumulator if any.
+      if (validationResult.warnings) warnings.push(...validationResult.warnings);
+
+      // Include prop in the request body if marked as postBody
+      if (meta.postBody === true) body[propName] = this[propName];
+    };
+
+    // Already trimmed earlier
+    const url = this.siteUrl;
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: `https://searchconsole.googleapis.com/webmasters/v3/sites/${encodeURIComponent(url)}/searchAnalytics/query`,
+        headers: {
+          "Authorization": `Bearer ${this.gsConsole.$auth.oauth_access_token}`,
+          "Content-Type": "application/json",
+        },
+        data: body,
+      });
+
+    } catch (error) {
+      // Identify if the error was thrown by internal validation or by the API call
+      const thrower = this.gsConsole.checkWhoThrewError(error);
+
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("\n- "));
+    };
+
+    // Output summary and any warnings for the user
+    $.export("$summary", ` Fetched ${response.rows?.length || 0} rows of data. ` + warnings.join("\n- "));
+    return response;
+  },
+};
+
diff --git a/components/google_search_console/actions/submit-url-for-indexing/README.md b/components/google_search_console/actions/submit-url-for-indexing/README.md
new file mode 100644
index 0000000000000..303cb9e052e69
--- /dev/null
+++ b/components/google_search_console/actions/submit-url-for-indexing/README.md
@@ -0,0 +1,31 @@
+# 🚀 Google Indexing API – URL Submission Action
+
+This action submits a specific URL to the **Google Indexing API** to notify Google that the content has been **updated** and should be re-crawled.
+
+---
+
+## ✅ Use Case
+
+Use this action when:
+- You’ve updated content on a page
+- You want to request Google to reindex that URL as soon as possible
+
+---
+
+## 🧠 Internals
+
+- Validates and trims the input URL  
+- Uses the `URL_UPDATED` type to inform Google of a content change  
+- Returns the raw API response  
+- Displays a user-friendly summary  
+- Accumulates and returns non-blocking warnings (e.g. unusual characters in the URL)
+
+---
+
+## 🔐 Auth
+
++Requires OAuth 2.0 with [indexing](https://www.googleapis.com/auth/indexing) scope: 
+
+## 🔗 Endpoint
+
+[https://indexing.googleapis.com/v3/urlNotifications:publish](https://www.googleapis.com/auth/indexing)
\ No newline at end of file
diff --git a/components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs b/components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs
new file mode 100644
index 0000000000000..52722f9534e37
--- /dev/null
+++ b/components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs
@@ -0,0 +1,80 @@
+/**
+ * Submits a URL to the Google Indexing API to notify that content was updated.
+ * Uses `URL_UPDATED` type. Full docs in README.md
+ */
+
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../google_search_console.app.mjs";
+import { trimIfString } from "../../common/utils.mjs";
+
+export default {
+
+  name: "Submit URL for Indexing",
+  description: "Sends a URL update notification to the Google Indexing API",
+  key: "google_search_console-submit-url-for-indexing",
+  version: "0.0.1",
+  type: "action",
+  props: {
+    gsConsole,
+    siteUrl: {
+      type: "string",
+      label: "URL for indexing",
+      description: "URL to be submitted for indexing",
+    },
+  },
+
+  //=================== RUN ==============================
+  //======================================================
+
+  async run({ $ }) {
+
+    this.siteUrl = trimIfString(this.siteUrl);
+
+    // Accumulator for non-blocking warnings
+    let warnings = [];
+
+    /*
+      Validate the submitted site URL.
+      - May throw if invalid
+      - May return warnings for issues like suspicious characters
+    */
+    const urlCheck = this.gsConsole.checkIfUrlValid(this.siteUrl);
+
+    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+
+    // Prepare the API response object
+    let response;
+
+    try {
+      // Submit the URL to the Google Indexing API
+      response = await axios($, {
+        method: "POST",
+        url: "https://indexing.googleapis.com/v3/urlNotifications:publish",
+        headers: {
+          "Authorization": `Bearer ${this.gsConsole.$auth.oauth_access_token}`,
+          "Content-Type": "application/json",
+        },
+        data: {
+          url: this.siteUrl,
+          type: "URL_UPDATED", // Notifies Google that the content at this URL has been updated
+        },
+      });
+    } catch (error) {
+      /*
+        Determine whether the error originated from:
+        - Internal validation logic
+        - The external API call
+
+        Helps distinguish between coding issues vs API issues.
+      */
+      const thrower = gsConsole.methods.checkWhoThrewError(error);
+
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("\n- "));
+
+    };
+    // Output a summary message and any accumulated warnings
+    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + warnings.join("\n- "));
+    // Return the raw API response
+    return response;
+  },
+};
diff --git a/components/google_search_console/common/methods.mjs b/components/google_search_console/common/methods.mjs
new file mode 100644
index 0000000000000..5952273c822f7
--- /dev/null
+++ b/components/google_search_console/common/methods.mjs
@@ -0,0 +1,312 @@
+export default {
+
+  /* ============================================================================================
+    Throws a custom error and marks it with a flag (`isCustom`) for easier debugging and testing.
+================================================================================================ */
+
+  throwCustomError(msg) {
+    const err = new Error(msg);
+    err.isCustom = true;
+    throw err;
+  },
+
+  /* ============================================================================================
+    Determines whether an error originated from your own validation code or from the API request.
+    Useful for debugging and crafting more helpful error messages.
+=============================================================================================== */
+
+  // =====================================================================
+  checkWhoThrewError(error) {
+    return {
+      whoThrew: error?.response?.status
+        ? "API response"
+        : "Internal Code",
+      error,
+    };
+  },
+
+  /* ==========================================================================================
+    Throws if the input is not a string or is a blank string (only whitespace, tabs,
+    newlines, etc.).
+============================================================================================= */
+
+  throwIfBlankOrNotString(input, reason) {
+
+    if (typeof input !== "string") {
+      this.throwCustomError(
+        `Expected a string, but got ${typeof input}` + this._reasonMsg(reason),
+      );
+    }
+
+    if (input.trim() === "") {
+      this.throwCustomError(
+        "Expected a non-empty string with visible characters." + this._reasonMsg(reason),
+      );
+    }
+
+    return input;
+  },
+
+  /* ==============================================================================================
+    Throws if the input is not a whole number (non-negative integer).
+================================================================================================= */
+
+  throwIfNotWholeNumber(input, reason) {
+    if (!Number.isInteger(input) || input < 0) {
+      this.throwCustomError( `Expected a whole number (0 or positive integer), but got ${typeof input}` + this._reasonMsg(reason));
+
+    }
+
+    return input;
+  },
+
+  /* =============================================================================================
+    Throws if the input is not an array.
+================================================================================================= */
+
+  throwIfNotArray(input, reason) {
+
+    if (!Array.isArray(input)) {
+      this.throwCustomError("Invalid argument type. Expected an array " +  this._reasonMsg(reason));
+    };
+
+    return input;
+  },
+
+  /* =============================================================================================
+    Throws if the input is not an array of strings.
+================================================================================================ */
+
+  throwIfNotArrayOfStrings(input, reason) {
+
+    this.throwIfNotArray(input, reason);
+
+    for (let i = 0; i < input.length; i++) {
+      if (typeof input[i] !== "string") {
+        this.throwCustomError("Expected an array of strings " +  this._reasonMsg(reason));
+      }
+    };
+
+    return input;
+  },
+
+  /* ==============================================================================================
+    Throws if the input is not a plain object (arrays are excluded).
+================================================================================================= */
+
+  throwIfNotObject(input, reason) {
+
+    if ( typeof input === "object" && input !== null && !Array.isArray(input)) {
+      return input;
+    };
+
+    this.throwCustomError("Invalid argument type. Expected an object " +  this._reasonMsg(reason));
+
+  },
+
+  /* ==============================================================================================
+    Throws if the input is neither an object nor an array.
+================================================================================================= */
+
+  throwIfNotObjectOrArray(input, reason) {
+
+    if ( typeof input === "object" && input !== null) {
+      return input;
+    };
+
+    this.throwCustomError("Invalid argument type. Expected an object or array" +  this._reasonMsg(reason));
+
+  },
+
+  /* ==============================================================================================
+    Validates that a string follows the YYYY-MM-DD date format.
+    Throws if the format or any individual part (year, month, day) is invalid.
+================================================================================================= */
+
+  throwIfNotYMDDashDate(input, reason) {
+
+    this.throwIfBlankOrNotString(input, reason);
+
+    const parts = input.trim().split("-");
+
+    if (parts.length !== 3) {
+      this.throwCustomError("Date must be in format YYYY-MM-DD"  +  this._reasonMsg(reason));
+    };
+
+    let [
+      year,
+      month,
+      day,
+    ] = parts;
+
+    // --- YEAR ---
+    year = +year;
+    if (!Number.isInteger(year) || year < 1990 || year > 2100) {
+      this.throwCustomError("Year must be between 1990 and 2100" +  this._reasonMsg(reason));
+    };
+
+    const monthNum = +month;
+    if (month.length !== 2 || Number.isNaN(monthNum) || monthNum < 1 || monthNum > 12 ) {
+      this.throwCustomError(`Month must be between 01 and 12. Got: '${month}'` +  this._reasonMsg(reason));
+    };
+
+    const dayNum = +day;
+    if (day.length !== 2 || Number.isNaN(dayNum) || dayNum < 1 || dayNum > 31 ) {
+      this.throwCustomError(`Day must be between 01 and 31. Got: '${day}'` +  this._reasonMsg(reason));
+    };
+
+    return input;
+  },
+
+  /* ==============================================================================================
+    Validates a URL string:
+    - Rejects blank strings, spaces, tabs, and newlines
+    - Warns about suspicious or unusual characters
+    - Adds a warning if protocol is missing or malformed
+    Returns:
+      {
+        warnings: string[],
+        url: string (trimmed original input)
+      }
+================================================================================================= */
+
+  // =====================================================================
+  checkIfUrlValid(input, reason) {
+
+    // Throws an error if the input is not a string or if its a blank string;
+    this.throwIfBlankOrNotString(input);
+
+    // Warnin accumulator
+    let warnings = [];
+    ;
+    // Trim the input (already checked for string);
+    const trimmedInput = input.trim();
+
+    // Reject if spaces, tabs, or newlines are present
+    if ((/[ \t\n]/.test(trimmedInput))) {
+      this.throwCustomError( "Url contains invalid characters like space, backslash etc., please check."  +  this._reasonMsg(reason));
+    };
+
+    // Warn about suspicious characters
+    const dubiousCharRegex = /[\\<>{}|"`^]|\u200B|\u200C|\u200D|\u2060|\uFEFF|\u00A0/g;
+
+    const dubiousMatches = trimmedInput.match(dubiousCharRegex);
+
+    if (dubiousMatches) {
+      const uniqueChars = [
+        ...new Set(dubiousMatches),
+      ].join(" ");
+
+      warnings.push(` URL contains dubious or non-standard characters " ${uniqueChars} " ` +
+          `that may be rejected by Google. Proceed only if you know what you are doing.  ${this._reasonMsg(reason)}`) ;
+    };
+
+    // urlObject for further use if the next check passes.
+    let urlObject;
+    // Tries to create a new URL object with the input string;
+    try {
+      urlObject =  new URL(trimmedInput); // throws if invalid or has no protocol
+
+      // Warn if user typed only one slash (e.g., https:/)
+      if (/^(https?):\/(?!\/)/.test(input)) {
+
+        warnings.push(` It looks like you're missing one slash after "${urlObject.protocol}".` +
+        `Did you mean "${urlObject.protocol}//..."? ${this._reasonMsg(reason)} `);
+
+      };
+
+    } catch (err) {
+      // If the URL is invalid, try to create a new URL object with "http://" in case user forgot to add it;
+      try  {
+        // If it works then there was no protocol in the input string;
+        urlObject = new URL("http://" + trimmedInput);
+
+        warnings.push(" URL does not have http or https protocol \"");
+
+      } catch (err) {
+        // If after all checks we are here that means that the url
+        // contain potentially unacceptable characters.
+        warnings.push(` URL contains potentionally unacceptable characters"  ${this._reasonMsg(reason)}`);
+
+      };
+
+    };
+
+    // Here I wanted to check for "ftp" and other protocols but if user
+    // uses them they know what they are doing.
+
+    return {
+      warnings: warnings,
+      url: input,
+    };
+
+  },
+
+  /* ==============================================================================================
+    Appends a reason string to error messages for additional context.
+================================================================================================= */
+
+  _reasonMsg(reason) {
+
+    return (reason && typeof reason === "string")
+      ? ` Reason: ${reason} `
+      : "";
+  },
+
+  /* ==============================================================================================
+    Calls the appropriate validation method based on the input's type.
+    Uses either the standard `type` or an extended override via `extendedType`.
+    Throws if no validator is found or if the value fails validation.
+================================================================================================= */
+
+  validateUserInput(propMeta, value) {
+
+    this.throwIfNotObject(propMeta, propMeta.label);
+
+    // Check if there is extra type in prop meta. Like string can be also date type
+    const type = ("extendedType" in propMeta)
+      ? propMeta.extendedType
+      : propMeta.type;
+
+    // Decide which method to call based on the input we want to check.
+
+    const validators = {
+      "string": this.throwIfBlankOrNotString,
+      "integer": this.throwIfNotWholeNumber,
+      "string[]": this.throwIfNotArrayOfStrings,
+      "object": this.throwIfNotObjectOrArray,
+      "array": this.throwIfNotArray,
+      "YYYY-MM-DD": this.throwIfNotYMDDashDate,
+      "url": this.checkIfUrlValid,
+    };
+
+    const validator = validators[type];
+
+    if (!validator) {
+      this.throwCustomError(`No validator found for type "${type}"`, propMeta.label);
+    };
+
+    // Calls the correct function and passes label
+    // to show as a reason if it throws an error or issues a warning.
+    return validator.call(this, value, propMeta.label);
+
+  },
+
+  /* ==============================================================================================
+    Attempts to parse a string as JSON.
+    - Returns the parsed object if valid
+    - Returns the original value if not a string
+    - Throws a custom error if parsing fails
+================================================================================================= */
+
+  parseIfJsonString(input, reason) {
+    if (typeof input !== "string") return input;
+
+    try {
+      return JSON.parse(input);
+    } catch (err) {
+      this.throwCustomError( `Can not parse JSON. Error : ${err}`  +  this._reasonMsg(reason));
+    };
+  },
+
+};
diff --git a/components/google_search_console/common/tests/README.md b/components/google_search_console/common/tests/README.md
new file mode 100644
index 0000000000000..d234532b520e0
--- /dev/null
+++ b/components/google_search_console/common/tests/README.md
@@ -0,0 +1,44 @@
+# Local Tests Directory
+
+This folder contains **non-production test scripts** used for developing and validating Pipedream components locally.  
+**You can safely delete the entire folder** — it does not affect runtime or deployment.
+
+---
+
+## Folder Structure
+
+### `action-tests/`
+Component-level test runners that simulate a real Pipedream `$` context using `mockery-dollar.mjs`.
+
+- `test-retrieve-site-performance-data.mjs` – Tests the Search Analytics action
+- `test-submit-url-for-indexing.mjs` – Tests the Indexing API action
+
+---
+
+### `methods/`
+Unit tests for reusable validation and utility methods found in `.app.mjs`.
+
+- `test-checkIfUrlValid.mjs` – Tests URL validation helper
+- `test-throwIfNotYMDDashDate.mjs` – Tests strict date validation
+
+#### `bogus-data/`
+Mocked data used to simulate edge-case user inputs and trigger validations:
+- `bogus-data-url.mjs` – Invalid or suspicious URLs
+- `bogus-data-google-date.mjs` – Date values to test against expected format
+
+---
+
+### Root-level Utilities
+
+- `mockery-dollar.mjs` – Mocks the `$` object Pipedream injects into actions
+- `get-token.mjs` – Script for manually supplying a Google OAuth token during local testing
+
+---
+
+## ⚠️ Notes
+
+- Some files may contain **hardcoded tokens** — be sure to exclude them from commits.
+- All files here are meant for **local testing only**.
+- Delete this folder any time before publishing — it's safe and has no impact on your app.
+
+---
\ No newline at end of file
diff --git a/components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs b/components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs
new file mode 100644
index 0000000000000..aee46bc08b14b
--- /dev/null
+++ b/components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs
@@ -0,0 +1,251 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects mocked `$` object (logs, summary)
+ * - Bypasses OAuth by using hardcoded access token (get-token.mjs)
+ * - Validates, builds, and sends the Search Console request
+ *
+ *   You MUST:
+ * - Replace `Authorization` token with a valid one manually
+ * - Ensure `siteUrl` is verified in your Search Console
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs";
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import {
+  removeCustomPropFields, trimIfString,
+} from "../../../common/utils.mjs";
+
+// Define prop metadata separately and spread it into the props object.
+// Useful for accessing extended metadata during runtime — available because it stays in closure.
+
+// ====================== TEST PREPARATION =====================================================
+
+// TEST ONLY
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+
+  // Required
+  startDate: "2025-12-22",
+  endDate: "2025-12-31",
+
+  // Recommended
+  dimensions: [
+    "query",
+    "page",
+    "country",
+    "device",
+  ], // valid values only
+
+  searchType: "web", // one of: web, image, video, news, googleNews, discover
+
+  rowLimit: 10,
+
+  startRow: 0, // Optional pagination
+
+  // Optional but valid
+  aggregationType: "auto", // or "byPage"
+  dataState: "final", // or "all"
+
+  // Optional filter (valid structure)
+  dimensionFilterGroups: [
+    {
+      groupType: "and", // "and" or "or"
+      filters: [
+        {
+          dimension: "query",
+          operator: "contains", // "equals", "contains", "notEquals", etc.
+          expression: "example",
+        },
+        {
+          dimension: "country",
+          operator: "equals",
+          expression: "USA",
+        },
+      ],
+    },
+  ],
+};
+
+const propsMeta = {
+
+  siteUrl: {
+    type: "string",
+    extendedType: "url",
+    label: "Verified Site URL",
+    description: "Including https:// is strongly advised",
+  },
+  startDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "Start Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  endDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "End Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  dimensions: {
+    type: "string[]",
+    label: "Dimensions",
+    optional: true,
+    description: "e.g. ['query', 'page', 'country', 'device']",
+    postBody: true,
+  },
+  searchType: {
+    type: "string",
+    label: "Search Type",
+    optional: true,
+    options: [
+      "web",
+      "image",
+      "video",
+      "news",
+      "googleNews",
+      "discover",
+    ],
+    default: "web",
+    postBody: true,
+  },
+  aggregationType: {
+    type: "string",
+    label: "Aggregation Type",
+    optional: true,
+    options: [
+      "auto",
+      "byPage",
+    ],
+    postBody: true,
+  },
+  rowLimit: {
+    type: "integer",
+    label: "Max rows to return",
+    default: 10,
+    postBody: true,
+  },
+  startRow: {
+    type: "integer",
+    label: "Start row (for pagination)",
+    optional: true,
+    postBody: true,
+  },
+  dimensionFilterGroups: {
+    type: "object",
+    label: "Dimension Filters",
+    optional: true,
+    description: "Follow Search Console API structure for filters",
+    postBody: true,
+  },
+  dataState: {
+    type: "string",
+    label: "Data State",
+    optional: true,
+    options: [
+      "all",
+      "final",
+    ],
+    default: "final",
+    postBody: true,
+  },
+
+};
+
+const testAction = {
+
+  ...mockeryData,
+
+  name: "Get Site Performance Data",
+  description: "Fetches search analytics data for a verified site.",
+  key: "get_search_console_analytics",
+  version: "0.0.47",
+  type: "action",
+  props: {
+    gsConsole,
+    // Remove custom prop metadata and spread only valid prop fields
+    ...removeCustomPropFields(propsMeta),
+  },
+
+  async run({ $ }) {
+
+    // body for POST request. Will be filled in the following loop.
+    const body = {};
+
+    // warnings accumulator
+    let warnings = [];
+
+    /* This loop performs the following tasks:
+   - Validates user input
+   - Populates the `body` object for the upcoming POST request
+   - Accumulates messages to display to the user at the end of the action
+*/
+    for (let propName in propsMeta) {
+
+      console.log("===VALUE", this[propName]);
+      const meta = propsMeta[propName];
+
+      // Trim the input if it's a string
+      this[propName] = trimIfString(this[propName]);
+
+      // If the optional prop is undefined, null, or a blank string — skip it
+      if (meta.optional === true && ((this[propName] ?? "") === "")) continue;
+
+      // Validate the input and throw an error if it's invalid.
+      // Also return an empty string or a warning message, if applicable
+      const validationResult = gsConsole.methods.validateUserInput(meta, this[propName]);
+
+      // Push the warnings into warnings accumulator if any.
+      if (validationResult.warnings) warnings.push(...validationResult.warnings);
+
+      // If the prop should be included in the POST request, add it to the body
+      if (meta.postBody === true) body[propName] = this[propName];
+
+      console.log(" SUCCESS");
+    };
+
+    // Trimmed in loop above
+    const url = this.siteUrl;
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: `https://searchconsole.googleapis.com/webmasters/v3/sites/${encodeURIComponent(url)}/searchAnalytics/query`,
+        headers: {
+          // TEMP: Local-only token for manual testing. Do not commit this to source control.
+          "Authorization": "Bearer  *HARDCODED TOKEN HERE*",
+          "Content-Type": "application/json",
+        },
+        data: body,
+      });
+
+    } catch (error) {
+      // Check who threw the error. Internal code or the request. To ease debugging.
+      const thrower = gsConsole.methods.checkWhoThrewError(error);
+
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. `  + warnings.join("\n- "));
+
+    };
+
+    $.export("$summary", ` Fetched ${response.rows?.length || 0} rows of data. ` + warnings.join("\n- "));
+    return response;
+  },
+};
+
+// await is just in case if node wants to finish its job before time =)
+async function runTest() {
+  await testAction.run(mockery$);
+}
+
+runTest();
diff --git a/components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs b/components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs
new file mode 100644
index 0000000000000..3c202f520ddc6
--- /dev/null
+++ b/components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs
@@ -0,0 +1,92 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects a mocked `$` object (`mockery$`)
+ * - Bypasses OAuth using a hardcoded access token (get-token.mjs)
+ * - Sends a URL submission request to the Google Indexing API
+ *
+ *   You MUST:
+ * - Replace the `Authorization` token with a valid one manually
+ * - Ensure the `siteUrl` is verified in your Search Console account
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs"; // TEST ONLY. Delete in production code
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import { trimIfString } from "../../../common/utils.mjs";
+
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+};
+
+const testAction = { // TEST ONLY. Replace to export in real code
+
+  ...mockeryData,  // TEST ONLY. Delete in production code
+
+  name: "Google test",
+  description: "This is a demo action",
+  key: "google_test",
+  version: "0.0.23",
+  type: "action",
+  props: {
+    gsConsole,
+    url: {
+      type: "string",
+      label: "URL for indexing",
+      description: "URL to be submitted for indexing",
+    },
+  },
+  async run({ $ }) {
+
+    this.url = trimIfString(this.url);
+
+    // warnings accumulator
+    let warnings = [];
+
+    const urlCheck = gsConsole.methods.checkIfUrlValid(this.url); // TEST ONLY. Replace to "this"
+
+    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: "https://indexing.googleapis.com/v3/urlNotifications:publish",
+        headers: {
+          // Tested with real hardcoded token that had required scopes.
+          "Authorization": "Bearer *HARDCODED TOKEN HERE*", // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing".
+          "Content-Type": "application/json",
+        },
+        data: {
+          url: this.url,
+          type: "URL_UPDATED",
+        },
+      });
+    } catch (error) {
+      // Check who threw the error. Internal code or the request. To ease debugging.
+      const thrower = gsConsole.methods.checkWhoThrewError(error);
+
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("\n- "));
+
+    };
+
+    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + warnings.join("\n- "));
+    return response;
+  },
+};
+
+// await is just in case if node wants to finish its job before time =)
+async function runTest() {
+  await testAction.run(mockery$);
+}
+
+runTest();
diff --git a/components/google_search_console/common/tests/get-token.mjs b/components/google_search_console/common/tests/get-token.mjs
new file mode 100644
index 0000000000000..c2fe5dabf56cb
--- /dev/null
+++ b/components/google_search_console/common/tests/get-token.mjs
@@ -0,0 +1,81 @@
+// Bare-bones OAuth script to fetch a token.
+// For testing purposes only — no timeouts, no browser response.
+// Manually terminate the script from the console when finished.
+
+import open from "open";
+import http from "http";
+
+// --- Google OAuth Config ---
+const clientId = "*CLIEN ID HERE*";
+const redirectUri = "http://localhost:3000";
+const scopes = [
+  "https://www.googleapis.com/auth/webmasters.readonly",
+  "https://www.googleapis.com/auth/indexing",
+].join(" ");
+
+const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
+authUrl.searchParams.set("client_id", clientId);
+authUrl.searchParams.set("redirect_uri", redirectUri);
+authUrl.searchParams.set("response_type", "code");
+authUrl.searchParams.set("scope", scopes);
+authUrl.searchParams.set("access_type", "offline");
+authUrl.searchParams.set("prompt", "consent");
+
+new Promise((resolve, reject) => {
+  // Start a local HTTP server to catch Google's OAuth redirect
+  http.createServer((incomingRequest) => {
+    const fullRequestUrl = new URL(incomingRequest.url, `http://${incomingRequest.headers.host}`);
+    const authorizationCode = fullRequestUrl.searchParams.get("code");
+
+    if (authorizationCode) {
+      console.log("AUTH CODE RECEIVED:", authorizationCode);
+      resolve(authorizationCode);
+    } else {
+      console.log("Unexpected request received — no code param found.");
+      reject("Unexpected request received — no code param.");
+    }
+  }).listen(3000, () => {
+    console.log("🌐 Listening on http://localhost:3000");
+    console.log("🔗 Opening browser for Google OAuth...");
+    open(authUrl.toString());
+  });
+})
+  .then((authorizationCode) => {
+    console.log("✅ Got the code! Exchanging for tokens...");
+
+    fetch("https://oauth2.googleapis.com/token", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/x-www-form-urlencoded",
+      },
+      body: new URLSearchParams({
+        code: authorizationCode,
+        client_id: clientId,
+        client_secret: "*CLIENT SECRET HERE",
+        redirect_uri: redirectUri,
+        grant_type: "authorization_code",
+      }),
+    })
+      .then((res) => res.json())
+      .then((data) => {
+        if (data.access_token) {
+          console.log("🎉 Tokens received!");
+          console.log("Access Token:", data.access_token);
+          console.log("Refresh Token:", data.refresh_token);
+          console.log("Expires In:", data.expires_in);
+          console.log("Token retrieval complete. Shutting down server...");
+          process.exit(0);
+        } else {
+          console.error(" Failed to get tokens:", data);
+          process.exit(1);
+        }
+      })
+      .catch((err) => {
+        console.error("Error while exchanging code:", err);
+        process.exit(1);
+      });
+  })
+  .catch((error) => {
+    console.error("Error during authorization:", error);
+    process.exit(1);
+  });
diff --git a/components/google_search_console/common/tests/methods/bogus-data/bogus-data-google-date.mjs b/components/google_search_console/common/tests/methods/bogus-data/bogus-data-google-date.mjs
new file mode 100644
index 0000000000000..e9ac872f1dbfe
--- /dev/null
+++ b/components/google_search_console/common/tests/methods/bogus-data/bogus-data-google-date.mjs
@@ -0,0 +1,128 @@
+export default {
+  aSingleWordString: {
+    value: "wrgwgwepkgprgprpwnwwrehwrh",
+    jsType: "string",
+    extendedType: "singleWordString",
+  },
+  aMultiWordString: {
+    value: "etheth etphmpethm ethpethm",
+    jsType: "string",
+    extendedType: "aMultiWordString",
+  },
+  aPositiveInteger: {
+    value: 15115,
+    jsType: "number",
+    extendedType: "positiveInteger",
+  },
+  aNegativeInteger: {
+    value: -15115,
+    jsType: "number",
+    extendedType: "negativeInteger",
+  },
+  aPositiveFloat: {
+    value: 12.155,
+    jsType: "number",
+    extendedType: "positiveFloat",
+  },
+  aNegativeFloat: {
+    value: -12.155,
+    jsType: "number",
+    extendedType: "negativeFloat",
+  },
+  aZero: {
+    value: 0,
+    jsType: "number",
+    extendedType: "zero",
+  },
+  anArray: {
+    value: [
+      13135,
+      35.15,
+      "3feqe",
+      [
+        1,
+        2,
+        3,
+      ],
+      {
+        some: "val",
+      },
+      0,
+    ],
+    jsType: "object",
+    extendedType: "trueArray",
+  },
+
+  anArrayOfStrings: {
+    value: [
+      "string1",
+      "string2",
+      "string3",
+    ],
+    jsType: "object",
+    extendedType: "arrayOfStrings",
+  },
+  anObject: {
+    value: {
+      some: "val",
+      some2: 23,
+      some3: [
+        1,
+        2,
+        3,
+      ],
+    },
+    jsType: "object",
+    extendedType: "trueObject",
+  },
+
+  anEmptyString: {
+    value: "",
+    jsType: "string",
+    extendedType: "emptyString",
+  },
+
+  aSpace: {
+    value: " ",
+    jsType: "string",
+    extendedType: "space",
+  },
+
+  aValidYMDDashDate: {
+    value: "2025-05-28",
+    jsType: "string",
+    extendedType: "ymdDashDate",
+  },
+
+  aCharinYMDDashDate: {
+    value: "20G5-h5-28",
+    jsType: "string",
+    extendedType: "ymdDashDate",
+  },
+
+  aShortYearYMDDashDate: {
+    value: "20-23-28",
+    jsType: "string",
+    extendedType: "ymdDashDate",
+  },
+
+  aMonthOusideValYMDDashDate: {
+    value: "2025-23-28",
+    jsType: "string",
+    extendedType: "ymdDashDate",
+  },
+
+  aDayOusideValYMDDashDate: {
+    value: "2025-12-56",
+    jsType: "string",
+    extendedType: "ymdDashDate",
+  },
+
+  anInvalidSeparatorYMDDashDate: {
+    value: "2025/12.56",
+    jsType: "string",
+    extendedType: "ymdDashDate",
+  },
+
+};
+
diff --git a/components/google_search_console/common/tests/methods/bogus-data/bogus-data-url.mjs b/components/google_search_console/common/tests/methods/bogus-data/bogus-data-url.mjs
new file mode 100644
index 0000000000000..b8933a29ffd31
--- /dev/null
+++ b/components/google_search_console/common/tests/methods/bogus-data/bogus-data-url.mjs
@@ -0,0 +1,133 @@
+export default {
+  aSingleWordString: {
+    value: "wrgwgwepkgprgprpwnwwrehwrh",
+    jsType: "string",
+    extendedType: "singleWordString",
+  },
+  aMultiWordString: {
+    value: "etheth etphmpethm ethpethm",
+    jsType: "string",
+    extendedType: "aMultiWordString",
+  },
+  aPositiveInteger: {
+    value: 15115,
+    jsType: "number",
+    extendedType: "positiveInteger",
+  },
+  aNegativeInteger: {
+    value: -15115,
+    jsType: "number",
+    extendedType: "negativeInteger",
+  },
+  aPositiveFloat: {
+    value: 12.155,
+    jsType: "number",
+    extendedType: "positiveFloat",
+  },
+  aNegativeFloat: {
+    value: -12.155,
+    jsType: "number",
+    extendedType: "negativeFloat",
+  },
+  aZero: {
+    value: 0,
+    jsType: "number",
+    extendedType: "zero",
+  },
+  anArray: {
+    value: [
+      13135,
+      35.15,
+      "3feqe",
+      [
+        1,
+        2,
+        3,
+      ],
+      {
+        some: "val",
+      },
+      0,
+    ],
+    jsType: "object",
+    extendedType: "trueArray",
+  },
+
+  anArrayOfStrings: {
+    value: [
+      "string1",
+      "string2",
+      "string3",
+    ],
+    jsType: "object",
+    extendedType: "arrayOfStrings",
+  },
+  anObject: {
+    value: {
+      some: "val",
+      some2: 23,
+      some3: [
+        1,
+        2,
+        3,
+      ],
+    },
+    jsType: "object",
+    extendedType: "trueObject",
+  },
+
+  anEmptyString: {
+    value: "",
+    jsType: "string",
+    extendedType: "emptyString",
+  },
+
+  aSpace: {
+    value: " ",
+    jsType: "string",
+    extendedType: "space",
+  },
+
+  aValidHttpUrl: {
+    value: "http://example.com",
+    jsType: "string",
+    extendedType: "url",
+  },
+
+  aValidHttpsUrl: {
+    value: "https://example.com",
+    jsType: "string",
+    extendedType: "url",
+  },
+
+  aValidUrlWithoutProtocol: {
+    value: "example.com",
+    jsType: "string",
+    extendedType: "url",
+  },
+  anUrlWithSpace: {
+    value: "https://exa mple.com",
+    jsType: "string",
+    extendedType: "url",
+  },
+
+  aReverseSlashUrl: {
+    value: "http://example.com\\path",
+    jsType: "string",
+    extendedType: "url",
+  },
+
+  aOneSlashUrl: {
+    value: "http:/example.com",
+    jsType: "string",
+    extendedType: "url",
+  },
+
+  aValidUrlWithDubiousCharacters: {
+    value: "http://example.com\\<script>{value}|\"stuff\"^^^\u200Bmiddle\u200Czone\u200D\u2060\uFEFF\u00A0end?param=\"weird\"",
+    jsType: "string",
+    extendedType: "url",
+  },
+
+};
+
diff --git a/components/google_search_console/common/tests/methods/test-checkIfUrlValid.mjs b/components/google_search_console/common/tests/methods/test-checkIfUrlValid.mjs
new file mode 100644
index 0000000000000..8b53a046c3cec
--- /dev/null
+++ b/components/google_search_console/common/tests/methods/test-checkIfUrlValid.mjs
@@ -0,0 +1,93 @@
+import methods from "../../../common/methods.mjs";
+import bogus from "./bogus-data/bogus-data-url.mjs";
+
+// Any other case from bogus data should lead to throw
+// Valid data : expected result;
+const validCases = {
+
+  aSingleWordString:
+    {
+      warnings: 1,
+      url: bogus.aSingleWordString.value,
+    },
+  aValidHttpsUrl:
+    {
+      warnings: 0,
+      url: bogus.aValidHttpsUrl.value,
+    },
+  aValidHttpUrl:
+    {
+      warnings: 0,
+      url: bogus.aValidHttpUrl.value,
+    },
+  aValidUrlWithoutProtocol:
+    {
+      warnings: 1,
+      url: bogus.aValidUrlWithoutProtocol.value,
+    },
+  aValidUrlWithDubiousCharacters:
+    {
+      warnings: 1,
+      url: bogus.aValidUrlWithDubiousCharacters.value,
+    },
+
+  aOneSlashUrl:
+    {
+      warnings: 1,
+      url: bogus.aOneSlashUrl.value,
+    },
+
+  aReverseSlashUrl:
+    {
+      warnings: 1,
+      url: bogus.aReverseSlashUrl.value,
+    },
+};
+
+console.log("Running checkUrl() tests...");
+
+let counterOfFails = 0;
+
+for (const bogusCase in bogus) {
+
+  console.log ("==============");
+
+  let result;
+  const testArg = bogus[bogusCase].value;
+  const extendedType = bogus[bogusCase].extendedType;
+
+  try {
+    console.log("ENTERED VALUE", testArg, "of extended type ", extendedType);
+    result = methods.checkIfUrlValid(testArg);
+
+  } catch (err) {
+    if (err.isCustom) {
+      console.log ("------- \x1b[32m EXPECTED THROW PASS!\x1b[0m", "Custom error was thrown as expected" );
+      console.log();
+    } else {
+      console.log ("--- \x1b[31m HARD FAIL! \x1b[0m", "UNEXPECTED ERROR WAS THROWN");
+      console.log (err);
+      counterOfFails++;
+    };
+    continue;
+  }
+
+  const pass =
+        (result.warnings.length === validCases[bogusCase].warnings ) &&
+        (result.url === validCases[bogusCase].url);
+
+  if (pass) {
+    console.log ("--- \x1b[32m  PASS! \x1b[0m", "Function returned expected values");
+    continue;
+  }
+  else {
+    console.log ("--- \x1b[31m FAIL! \x1b[0m", "ENTERED VALUE", testArg);
+    console.log ("Expected ", validCases[bogusCase].warnings, "warnings", "got ", result.warnings.length );
+    console.log ("Expected ", validCases[bogusCase].url, "got ", result.url );
+    counterOfFails++;
+  };
+};
+
+console.log ("OOOOOOOOOOOOOOOOOOOOOOOOOOOO");
+console.log ("TOTAL FAILS = ", counterOfFails);
+console.log ("OOOOOOOOOOOOOOOOOOOOOOOOOOOO");
diff --git a/components/google_search_console/common/tests/methods/test-throwIfNotYMDDashDate.mjs b/components/google_search_console/common/tests/methods/test-throwIfNotYMDDashDate.mjs
new file mode 100644
index 0000000000000..efaa7e10d33ca
--- /dev/null
+++ b/components/google_search_console/common/tests/methods/test-throwIfNotYMDDashDate.mjs
@@ -0,0 +1,56 @@
+import methods from "../../common/methods.mjs";
+import bogus from "./bogus-data/bogus-data-google-date.mjs";
+
+// Any other case from bogus data shoul lead to throw
+// Valid data : expected result;
+const validCases = {
+
+  aValidYMDDashDate: bogus.aValidYMDDashDate.value,
+};
+
+console.log("Running throwIfNotYMDDashDate() tests...");
+
+let counterOfFails = 0;
+
+for (const bogusCase in bogus) {
+
+  console.log ("==============");
+
+  let result;
+  const testArg = bogus[bogusCase].value;
+  const extendedType = bogus[bogusCase].extendedType;
+
+  try {
+    console.log("ENTERED VALUE", testArg, "of extended type ", extendedType);
+    // Call function
+    result = methods.throwIfNotYMDDashDate(testArg);
+
+  } catch (err) {
+    if (err.isCustom) {
+      console.log ("------- \x1b[32m EXPECTED THROW PASS!\x1b[0m", "Custom error was thrown as expected" );
+      console.log(err.message);
+    } else {
+      console.log ("!!!!!! HARD FAIL!", "UNEXPECTED ERROR WAS THROWN");
+      console.log (err);
+      counterOfFails++;
+    };
+    continue;
+  }
+
+  const pass =
+        (result === validCases[bogusCase]);
+
+  if (pass) {
+    console.log ("--- \x1b[32m  PASS! \x1b[0m", "Function returned expected values");
+    continue;
+  }
+  else {
+    console.log (" ****************************************** FAIL!", "ENTERED VALUE", testArg);
+    console.log ("Expected ", validCases[bogusCase], "got ", result);
+    counterOfFails++;
+  };
+};
+
+console.log ("OOOOOOOOOOOOOOOOOOOOOOOOOOOO");
+console.log ("TOTAL FAILS = ", counterOfFails);
+console.log ("OOOOOOOOOOOOOOOOOOOOOOOOOOOO");
diff --git a/components/google_search_console/common/tests/mockery-dollar.mjs b/components/google_search_console/common/tests/mockery-dollar.mjs
new file mode 100644
index 0000000000000..f254db0e55aa7
--- /dev/null
+++ b/components/google_search_console/common/tests/mockery-dollar.mjs
@@ -0,0 +1,6 @@
+// This is just mockery for test runs. Imitates $ runtime.
+export default {
+  $: {
+    export: (a, b) => console.log(a, b),
+  },
+};
diff --git a/components/google_search_console/common/utils.mjs b/components/google_search_console/common/utils.mjs
new file mode 100644
index 0000000000000..e2d561229ff4e
--- /dev/null
+++ b/components/google_search_console/common/utils.mjs
@@ -0,0 +1,32 @@
+// Returns a new object containing only standard prop fields, removing any custom keys.
+// Considered using JSON deep cloning, but opted for a manual approach to safely preserve
+// functions or complex types in future data structures.
+export function removeCustomPropFields(input) {
+  const blacklist = new Set([
+    "extendedType",
+    "postBody",
+  ]);
+  const clean = {};
+
+  for (const key of Object.keys(input)) {
+    const prop = input[key];
+    const cloned = {};
+
+    for (const field of Object.keys(prop)) {
+      if (!blacklist.has(field)) {
+        cloned[field] = prop[field];
+      }
+    }
+
+    clean[key] = cloned;
+  }
+
+  return clean;
+}
+
+// return the trimmed string or input as is if its not a string
+export function trimIfString(input) {
+  return (typeof input === "string")
+    ? input.trim()
+    : input;
+};
diff --git a/components/google_search_console/google_search_console.app.mjs b/components/google_search_console/google_search_console.app.mjs
index 3dfde0f8896a7..c9454577a6f99 100644
--- a/components/google_search_console/google_search_console.app.mjs
+++ b/components/google_search_console/google_search_console.app.mjs
@@ -1,8 +1,11 @@
+import methods from "./common/methods.mjs";
+
 export default {
   type: "app",
   app: "google_search_console",
   propDefinitions: {},
   methods: {
+    ...methods,
     // this.$auth contains connected account data
     authKeys() {
       console.log(Object.keys(this.$auth));
diff --git a/components/google_search_console/package.json b/components/google_search_console/package.json
index 21aa00a5f7282..0bc777df186cb 100644
--- a/components/google_search_console/package.json
+++ b/components/google_search_console/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@pipedream/google_search_console",
-  "version": "0.6.0",
+  "version": "0.7.0",
   "description": "Pipedream google_search_console Components",
   "main": "google_search_console.app.mjs",
   "keywords": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7305cc63d8637..d6bcb6fca43c8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10186,8 +10186,7 @@ importers:
 
   components/proposify: {}
 
-  components/proprofs_knowledge_base:
-    specifiers: {}
+  components/proprofs_knowledge_base: {}
 
   components/proprofs_quiz_maker: {}
 
@@ -10909,8 +10908,7 @@ importers:
         specifier: ^3.0.0
         version: 3.0.3
 
-  components/riku_ai:
-    specifiers: {}
+  components/riku_ai: {}
 
   components/ringba:
     dependencies:
@@ -27987,22 +27985,22 @@ packages:
   superagent@3.8.1:
     resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==}
     engines: {node: '>= 4.0'}
-    deprecated: Please upgrade to v7.0.2+ of superagent.  We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing.  See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>.
+    deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net
 
   superagent@4.1.0:
     resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==}
     engines: {node: '>= 6.0'}
-    deprecated: Please upgrade to v7.0.2+ of superagent.  We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing.  See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>.
+    deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net
 
   superagent@5.3.1:
     resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==}
     engines: {node: '>= 7.0.0'}
-    deprecated: Please upgrade to v7.0.2+ of superagent.  We have fixed numerous issues with streams, form-data, attach(), filesystem errors not bubbling up (ENOENT on attach()), and all tests are now passing.  See the releases tab for more information at <https://github.com/visionmedia/superagent/releases>.
+    deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net
 
   superagent@7.1.6:
     resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==}
     engines: {node: '>=6.4.0 <13 || >=14'}
-    deprecated: Please downgrade to v7.1.5 if you need IE/ActiveXObject support OR upgrade to v8.0.0 as we no longer support IE and published an incorrect patch version (see https://github.com/visionmedia/superagent/issues/1731)
+    deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net
 
   supports-color@2.0.0:
     resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 9**

<details>
<summary>🧹 Nitpick comments (32)</summary><blockquote>

<details>
<summary>components/google_search_console/tests/mockery-dollar.mjs (1)</summary><blockquote>

`1-1`: **Fix typo in comment.** The word "Immitiates" is misspelled; it should be "Imitates".

</blockquote></details>
<details>
<summary>components/google_search_console/common/utils.mjs (2)</summary><blockquote>

`23-23`: **Fix typo in comment.** Change "rimmed" to "trimmed" to accurately describe the behavior of `trimIfString`.

---

`4-21`: **Guard against inherited properties and optimize loops.**  
Using `for…in` can iterate over prototype properties; consider switching to `Object.keys()` and replace the blacklist array with a `Set` for better performance and clarity. 
```diff
-export function removeCustomPropFields(input) {
-  const blacklist = ["extendedType", "postBody"];
-  const clean = {};
-
-  for (const key in input) {
-    const prop = input[key];
-    const cloned = {};
-
-    for (const field in prop) {
-      if (!blacklist.includes(field)) {
-        cloned[field] = prop[field];
-      }
-    };
-
-    clean[key] = cloned;
-  };
-
-  return clean;
-}
+export function removeCustomPropFields(input) {
+  const blacklist = new Set(["extendedType", "postBody"]);
+  const clean = {};
+
+  for (const key of Object.keys(input)) {
+    const prop = input[key];
+    const cloned = {};
+
+    for (const field of Object.keys(prop)) {
+      if (!blacklist.has(field)) {
+        cloned[field] = prop[field];
+      }
+    }
+
+    clean[key] = cloned;
+  }
+
+  return clean;
+}
```

</blockquote></details>
<details>
<summary>components/google_search_console/actions/submit-url-for-indexing/README.md (1)</summary><blockquote>

`1-31`: **Clear and comprehensive documentation.**

The README is well-structured and provides excellent information about the action's purpose, use cases, internals, authentication requirements, and endpoint.


Consider formatting the bare URLs as proper markdown links for better adherence to markdown best practices:

```diff
-Requires OAuth 2.0 with this scope: https://www.googleapis.com/auth/indexing
+Requires OAuth 2.0 with this scope: [https://www.googleapis.com/auth/indexing](https://www.googleapis.com/auth/indexing)

-https://indexing.googleapis.com/v3/urlNotifications:publish
+[https://indexing.googleapis.com/v3/urlNotifications:publish](https://indexing.googleapis.com/v3/urlNotifications:publish)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

27-27: Bare URL used
null

(MD034, no-bare-urls)

---

31-31: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>

</blockquote></details>
<details>
<summary>components/google_search_console/tests/methods/test-checkIfUrlValid.mjs (2)</summary><blockquote>

`5-6`: **Fix typo in comment.**

There's a small typo in the comment.

```diff
-// Any other case from bogus data shoul lead to throw
+// Any other case from bogus data should lead to throw
```

---

`55-96`: **Robust test logic with minor enhancement opportunities.**

The test provides comprehensive coverage of various URL formats and edge cases, which aligns well with the PR's focus on robust input validation.


Consider adding a check to ensure `bogusCase` exists in `validCases` before trying to access it to prevent potential undefined property access:

```diff
        console.log ("==============");
    
        let result;
        const testArg = bogus[bogusCase].value;
        const extendedType = bogus[bogusCase].extendedType;

        try {
            console.log("ENTERED VALUE", testArg, "of extended type ", extendedType);
            result = methods.checkIfUrlValid(testArg);
            
        } catch(err) {
            if (err.isCustom) {
                console.log ("------- \x1b[32m EXPECTED THROW PASS!\x1b[0m", "Custom error was thrown as expected" );
                console.log();
            } else {
                console.log ("--- \x1b[31m HARD FAIL! \x1b[0m", "UNEXPECTED ERROR WAS THROWN");
                console.log (err);
                counterOfFails++;
            };
            continue;
        }
        
  
+       // Skip validation if this case isn't in our validCases mapping
+       if (!validCases[bogusCase]) {
+           console.log ("--- \x1b[33m SKIPPED! \x1b[0m", "No expected values defined for this case");
+           continue;
+       }

        const pass = 
        (result.warnings.length === validCases[bogusCase].warnings )&&
        (result.url === validCases[bogusCase].url);
```

</blockquote></details>
<details>
<summary>components/google_search_console/tests/README.md (1)</summary><blockquote>

`1-44`: **Well-organized testing documentation.**

This README provides clear information about the test suite structure and purpose. The explicit notes about the tests being for local development only and that they can be safely deleted are particularly helpful.


Fix the markdown linting issues by removing extra spaces after the hash marks in headings:

```diff
-#  Local Tests Directory
+# Local Tests Directory

-##  Folder Structure
+## Folder Structure
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

1-1: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

8-8: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

</details>

</details>

</blockquote></details>
<details>
<summary>components/google_search_console/tests/methods/test-throwIfNotYMDDashDate.mjs (3)</summary><blockquote>

`15-15`: **Incorrect console log message**

The console log message indicates "Running checkUrl() tests..." but this script is testing the `throwIfNotYMDDashDate` function, not `checkUrl()`.

```diff
-console.log("Running checkUrl() tests...");
+console.log("Running throwIfNotYMDDashDate() tests...");
```

---

`7-10`: **Improve test coverage with more valid test cases**

The test only defines one valid test case (`aValidYMDDashDate`). Consider adding more valid date formats to ensure comprehensive test coverage.

```diff
const validCases = {
    
    aValidYMDDashDate: bogus.aValidYMDDashDate.value,
+   // Add more valid test cases
+   // For example:
+   anotherValidDate: "2023-01-01",
+   validLeapYearDate: "2024-02-29",
};
```

---

`61-63`: **Add overall test pass/fail status**

The test reports the total number of failures but doesn't clearly indicate whether the overall test suite passed or failed. Add a clear pass/fail indicator based on the counter value.

```diff
console.log ("OOOOOOOOOOOOOOOOOOOOOOOOOOOO");
console.log ("TOTAL FAILS = ", counterOfFails);
+console.log (counterOfFails === 0 ? "\x1b[32mALL TESTS PASSED!\x1b[0m" : "\x1b[31mTEST SUITE FAILED!\x1b[0m");
console.log ("OOOOOOOOOOOOOOOOOOOOOOOOOOOO");
+process.exit(counterOfFails > 0 ? 1 : 0); // Exit with appropriate code for CI/CD integration
```

</blockquote></details>
<details>
<summary>components/google_search_console/tests/get-token.mjs (3)</summary><blockquote>

`9-9`: **Fix typo in placeholder text**

There's a typo in the client ID placeholder.

```diff
-const clientId = "*CLIEN ID HERE*";
+const clientId = "*CLIENT ID HERE*";
```

---

`56-56`: **Fix incomplete placeholder format**

The client secret placeholder is missing the closing asterisk.

```diff
-        client_secret: "*CLIENT SECRET HERE", 
+        client_secret: "*CLIENT SECRET HERE*", 
```

---

`4-23`: **Add token storage capability for reuse in tests**

The current script displays the tokens in the console, but doesn't save them for later use in tests. Consider adding an option to save tokens to a local file (excluded from git) for test reuse.

```diff
// Bare-bones OAuth script to fetch a token.
// For testing purposes only — no timeouts, no browser response.
// Manually terminate the script from the console when finished.

import open from "open";
import http from "http";
+import fs from "fs";
+import path from "path";

+// Optional: Save tokens to a local file for reuse in tests
+const SAVE_TOKENS = true;
+const TOKEN_FILE_PATH = path.join(process.cwd(), '.google_tokens.json');

// --- Google OAuth Config ---
const clientId = "*CLIENT ID HERE*";
const redirectUri = "http://localhost:3000";
const scopes = [
  "https://www.googleapis.com/auth/webmasters.readonly",
  "https://www.googleapis.com/auth/indexing",
].join(" ");

+// Function to save tokens to a local file
+function saveTokens(tokens) {
+  if (!SAVE_TOKENS) return;
+  try {
+    fs.writeFileSync(TOKEN_FILE_PATH, JSON.stringify(tokens, null, 2));
+    console.log(`Tokens saved to ${TOKEN_FILE_PATH}`);
+    console.log('IMPORTANT: This file contains sensitive credentials. Do not commit it to version control.');
+  } catch (err) {
+    console.error('Failed to save tokens:', err);
+  }
+}
```

And in the token response handler:

```diff
.then((data) => {
  if (data.access_token) {
    console.log("🎉 Tokens received!");
    console.log("Access Token:", data.access_token);
    console.log("Refresh Token:", data.refresh_token);
    console.log("Expires In:", data.expires_in);
+   saveTokens({
+     access_token: data.access_token,
+     refresh_token: data.refresh_token,
+     expires_in: data.expires_in,
+     created_at: Date.now()
+   });
    console.log("✅ Token retrieval complete. Shutting down server...");
    process.exit(0);
  } else {
```

</blockquote></details>
<details>
<summary>components/google_search_console/tests/action-tests/test-submit-url-for-indexing.mjs (4)</summary><blockquote>

`64-64`: **Replace hardcoded token placeholder with instructions**

The current placeholder for the token doesn't include clear instructions on how to replace it or where to get a valid token.

```diff
-          Authorization: `Bearer *HARDCODED TOKEN HERE*`,
+          Authorization: `Bearer YOUR_ACCESS_TOKEN`, // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing"
```

---

`76-76`: **Improve error message clarity**

The current error message concatenates warnings with an unusual delimiter "*!*" which could be confusing to users.

```diff
-      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("*!*"));
+      const warningStr = warnings.length > 0 ? ` Warnings: ${warnings.join(", ")}` : "";
+      throw new Error(`Failed to fetch data (${thrower.whoThrew} error): ${error.message}.${warningStr}`);
```

---

`80-80`: **Improve warning display in summary message**

Similar to the error message, the summary message concatenates warnings with an unusual delimiter.

```diff
-    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + warnings.join("*!*"));
+    const warningStr = warnings.length > 0 ? ` (Warnings: ${warnings.join(", ")})` : "";
+    $.export("$summary", `URL submitted to Google: ${this.siteUrl}${warningStr}`);
```

---

`50-52`: **Avoid modifying URL check result directly**

The code directly accesses the URL check result's warnings property and manipulates it. Consider using a more robust approach.

```diff
-    const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
-
-    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+    const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+    
+    // Safely extract warnings if they exist
+    if (urlCheck && urlCheck.warnings && Array.isArray(urlCheck.warnings)) {
+      warnings.push(...urlCheck.warnings);
+    }
```

</blockquote></details>
<details>
<summary>components/google_search_console/actions/retrieve-site-performance-data/README.md (4)</summary><blockquote>

`1-1`: **Fix markdown heading formatting**

There are multiple spaces after the hash symbol in the heading, which is flagged by markdown linting.

```diff
-#  Google Search Console – Site Performance (Analytics) Action
+# Google Search Console – Site Performance (Analytics) Action
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

1-1: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

</details>

</details>

---

`12-12`: **Fix multiple markdown heading formatting issues**

All section headings have multiple spaces after the hash, which is flagged by markdown linting.

```diff
-##  Use Cases
+## Use Cases

-##  Internals
+## Internals

-##  Auth
+## Auth

-##  Endpoint
+## Endpoint
```


Also applies to: 20-20, 31-31, 36-36

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

12-12: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

</details>

</details>

---

`33-33`: **Fix bare URLs in markdown**

The markdown linter flags bare URLs. Consider enclosing them in angle brackets or using markdown link syntax.

```diff
-Requires OAuth 2.0 with the following scope: https://www.googleapis.com/auth/webmasters.readonly
+Requires OAuth 2.0 with the following scope: `https://www.googleapis.com/auth/webmasters.readonly`

-https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query
+`https://searchconsole.googleapis.com/webmasters/v3/sites/{siteUrl}/searchAnalytics/query`

-  "siteUrl": "https://falc1.com/soda_can",
+  "siteUrl": "https://example.com/page",
```


Also applies to: 37-37, 48-48

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

33-33: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>

---

`40-109`: **Improve example payload formatting**

The example JSON payload lacks proper code block formatting and contains unnecessary comments within the JSON structure, which could confuse users trying to copy and modify it.


Consider reformatting the example as:

```diff
## 📦 Example Request Payload

+```json
{
-  // The site you want to query data for.
-  // Must be verified in your Google Search Console account.
  "siteUrl": "https://example.com/page",

-  // The start date of the reporting period (inclusive), in YYYY-MM-DD format.
  "startDate": "2025-12-22",

-  // The end date of the reporting period (inclusive), in YYYY-MM-DD format.
  "endDate": "2025-12-31",

-  // The dimensions you want to break down the data by.
-  // Valid values: "query", "page", "country", "device", "searchAppearance", "date".
-  // Order matters — it affects how rows are grouped in the response.
  "dimensions": ["query", "page", "country", "device"],

-  // The type of search data to include.
-  // Valid values: "web", "image", "video", "news", "googleNews", "discover"
  "searchType": "web",

-  // Maximum number of rows to return (1–25,000)
  "rowLimit": 10,

-  // Optional: Skips the first N rows — used for pagination.
  "startRow": 0,

-  // Optional: How to group data.
-  // "auto" = Google's default grouping.
-  // "byPage" = Group by page (useful for getting per-page breakdowns).
  "aggregationType": "auto",

-  // Optional: Data freshness filter.
-  // "final" = Only finalized data (more accurate).
-  // "all" = Includes fresh but possibly incomplete data.
  "dataState": "final",

-  // Optional filter group(s) to restrict which rows are returned.
-  // Each group applies logical AND/OR across its filters.
  "dimensionFilterGroups": [
    {
-      // Logical grouping operator for the filters inside this group.
-      // "and" = all filters must match
-      // "or" = any filter can match
      "groupType": "and",

-      // List of individual filters to apply within the group
      "filters": [
        {
-          // Which dimension to filter by (must match a dimension in your request)
          "dimension": "query",

-          // Filter operator — e.g., "equals", "contains", "notEquals", etc.
          "operator": "contains",

-          // Value to match against
          "expression": "example"
        },
        {
          "dimension": "country",
          "operator": "equals",
          "expression": "USA"
        }
      ]
    }
  ]
}
+```

+### Field Descriptions
+
+- **siteUrl**: The site you want to query data for. Must be verified in your Google Search Console account.
+- **startDate**: The start date of the reporting period (inclusive), in YYYY-MM-DD format.
+- **endDate**: The end date of the reporting period (inclusive), in YYYY-MM-DD format.
+- **dimensions**: The dimensions you want to break down the data by. Valid values: "query", "page", "country", "device", "searchAppearance", "date". Order matters — it affects how rows are grouped in the response.
+- **searchType**: The type of search data to include. Valid values: "web", "image", "video", "news", "googleNews", "discover"
+- **rowLimit**: Maximum number of rows to return (1–25,000)
+- **startRow**: Optional: Skips the first N rows — used for pagination.
+- **aggregationType**: Optional: How to group data. "auto" = Google's default grouping. "byPage" = Group by page (useful for getting per-page breakdowns).
+- **dataState**: Optional: Data freshness filter. "final" = Only finalized data (more accurate). "all" = Includes fresh but possibly incomplete data.
+- **dimensionFilterGroups**: Optional filter group(s) to restrict which rows are returned. Each group applies logical AND/OR across its filters.
+  - **groupType**: Logical grouping operator for the filters inside this group. "and" = all filters must match, "or" = any filter can match
+  - **filters**: List of individual filters to apply within the group
+    - **dimension**: Which dimension to filter by (must match a dimension in your request)
+    - **operator**: Filter operator — e.g., "equals", "contains", "notEquals", etc.
+    - **expression**: Value to match against
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~109-~109: Loose punctuation mark.
Context: ...ssion": "USA"         }       ]     }   ] }

(UNLIKELY_OPENING_PUNCTUATION)

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

48-48: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>

</blockquote></details>
<details>
<summary>components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs (4)</summary><blockquote>

`8-8`: **Import formatting and organization can be improved.**

The spacing after `trimIfString` import is inconsistent. Consider adding a comma after the import.

```diff
-import {trimIfString } from "../../common/utils.mjs"
+import { trimIfString } from "../../common/utils.mjs"
```

---

`60-60`: **Fix extra space in Authorization header.**

There's an extra space between "Bearer" and the token which could potentially cause authentication issues.

```diff
-          Authorization: `Bearer  ${this.gsConsole.$auth.oauth_access_token}`,
+          Authorization: `Bearer ${this.gsConsole.$auth.oauth_access_token}`,
```

---

`78-79`: **Improve error message readability.**

The warning separator might make error messages difficult to read. Consider adding a space before warnings are appended.

```diff
-      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("*!*"));
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + (warnings.length ? " " + warnings.join("*!*") : ""));
```

---

`82-82`: **Improve summary message readability.**

Similar to the error message, consider adding a space before appending warnings.

```diff
-    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + warnings.join("*!*"));
+    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + (warnings.length ? " " + warnings.join("*!*") : ""));
```

</blockquote></details>
<details>
<summary>components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs (2)</summary><blockquote>

`183-183`: **Consider improving error message readability.**

Similar to the submit-url action, the error message could be more readable with proper spacing before warnings.

```diff
-      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("*!*"));
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + (warnings.length ? " " + warnings.join("*!*") : ""));
```

---

`188-188`: **Improve summary message readability.**

The summary message could be more readable with proper spacing before warnings.

```diff
-    $.export("$summary", ` Fetched ${response.rows?.length || 0} rows of data. ` + warnings.join("*!*"));
+    $.export("$summary", ` Fetched ${response.rows?.length || 0} rows of data. ` + (warnings.length ? " " + warnings.join("*!*") : ""));
```

</blockquote></details>
<details>
<summary>components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs (3)</summary><blockquote>

`179-180`: **Remove debugging console.log statements.**

Production code should not contain debugging statements. Consider removing or using a conditional debug flag.

```diff
-      console.log("===VALUE", this[propName]); // TEST ONLY
```

---

`198-199`: **Remove another debugging console.log statement.**

Production code should not contain debugging statements.

```diff
-      console.log(" SUCCESS"); // TEST ONLY
```

---

`223-223`: **Add warnings to the error message.**

Unlike the actual implementation, the test code doesn't append warnings to the error message, which means the test doesn't fully match the production behavior.

```diff
-      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. `);
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + (warnings.length ? " " + warnings.join("*!*") : ""));
```

</blockquote></details>
<details>
<summary>components/google_search_console/common/methods.mjs (2)</summary><blockquote>

`171-237`: **URL validation is comprehensive but has inconsistent return format.**

The `checkIfUrlValid` function returns an object `{ warnings, url }` while other validators return the validated value directly. This inconsistency could lead to unexpected behavior when using these functions interchangeably.


Consider either:
1. Renaming to `validateUrl` and returning just the URL like other validators
2. Making all validators return the `{ warnings, value }` format

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 189-189: A character class cannot match a joined character sequence.

A zero width joiner composes several emojis into a new one. Replace the character class with an alternation.

(lint/suspicious/noMisleadingCharacterClass)

</details>

</details>

---

`303-305`: **Error handling in parseIfJsonString could be improved.**

Consider keeping the original error as the cause of the new error for better debugging.

```diff
-        this.throwCustomError( `Can not parse JSON. Error : ${err}`  +  this._reasonMsg(reason))
+        const error = new Error(`Can not parse JSON: ${err.message}` + this._reasonMsg(reason));
+        error.cause = err;
+        error.isCustom = true;
+        throw error;
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8c5072e7d7e1a5fbe3ed5ef9c977c12b03b394ac and 6b1562121517adf4f453da0680cc0f944a70ae50.

</details>

<details>
<summary>📒 Files selected for processing (17)</summary>

* `components/google_search_console/README.md` (1 hunks)
* `components/google_search_console/actions/retrieve-site-performance-data/README.md` (1 hunks)
* `components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs` (1 hunks)
* `components/google_search_console/actions/submit-url-for-indexing/README.md` (1 hunks)
* `components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs` (1 hunks)
* `components/google_search_console/common/methods.mjs` (1 hunks)
* `components/google_search_console/common/utils.mjs` (1 hunks)
* `components/google_search_console/google_search_console.app.mjs` (1 hunks)
* `components/google_search_console/tests/README.md` (1 hunks)
* `components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs` (1 hunks)
* `components/google_search_console/tests/action-tests/test-submit-url-for-indexing.mjs` (1 hunks)
* `components/google_search_console/tests/get-token.mjs` (1 hunks)
* `components/google_search_console/tests/methods/bogus-data/bogus-data-google-date.mjs` (1 hunks)
* `components/google_search_console/tests/methods/bogus-data/bogus-data-url.mjs` (1 hunks)
* `components/google_search_console/tests/methods/test-checkIfUrlValid.mjs` (1 hunks)
* `components/google_search_console/tests/methods/test-throwIfNotYMDDashDate.mjs` (1 hunks)
* `components/google_search_console/tests/mockery-dollar.mjs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>components/google_search_console/actions/submit-url-for-indexing/README.md</summary>

27-27: Bare URL used
null

(MD034, no-bare-urls)

---

31-31: Bare URL used
null

(MD034, no-bare-urls)

</details>
<details>
<summary>components/google_search_console/tests/README.md</summary>

1-1: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

8-8: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

</details>
<details>
<summary>components/google_search_console/actions/retrieve-site-performance-data/README.md</summary>

1-1: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

12-12: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

20-20: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

31-31: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

33-33: Bare URL used
null

(MD034, no-bare-urls)

---

36-36: Multiple spaces after hash on atx style heading
null

(MD019, no-multiple-space-atx)

---

37-37: Bare URL used
null

(MD034, no-bare-urls)

---

48-48: Bare URL used
null

(MD034, no-bare-urls)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>components/google_search_console/actions/retrieve-site-performance-data/README.md</summary>

[uncategorized] ~109-~109: Loose punctuation mark.
Context: ...ssion": "USA"         }       ]     }   ] }

(UNLIKELY_OPENING_PUNCTUATION)

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>components/google_search_console/common/methods.mjs</summary>

[error] 189-189: A character class cannot match a joined character sequence.

A zero width joiner composes several emojis into a new one. Replace the character class with an alternation.

(lint/suspicious/noMisleadingCharacterClass)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>components/google_search_console/google_search_console.app.mjs (1)</summary>

`1-2`: **Well-structured integration of shared methods.**

The approach of importing validation methods from a common module and integrating them with the spread operator is clean and maintainable. This pattern promotes code reuse and ensures consistent validation behavior across the component's actions.



Also applies to: 8-8

</details>
<details>
<summary>components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs (1)</summary>

`33-48`: **The validation and warning handling looks robust.**

The approach of accumulating warnings without blocking execution aligns well with the PR objective of identifying suspicious but not clearly invalid inputs.

</details>
<details>
<summary>components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs (3)</summary>

`27-99`: **Well-structured prop metadata with comprehensive validation.**

The approach of defining extended metadata separately from standard Pipedream props enables rich validation while maintaining UI compatibility. The `postBody` flag efficiently marks which props should be included in the API request.

---

`119-127`: **This approach to JSON validation is robust.**

Handling both object and string inputs for `dimensionFilterGroups` provides good flexibility for users, whether they're configuring through the UI or programmatically.

---

`141-160`: **The validation loop is elegant and handles optional fields correctly.**

The loop efficiently processes all props using their metadata, respecting optional fields and accumulating warnings without blocking execution.

</details>
<details>
<summary>components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs (1)</summary>

`1-18`: **Well-documented test file with clear instructions.**

The header comments clearly explain the purpose of this file and how to use it, which is excellent for maintainability.

</details>
<details>
<summary>components/google_search_console/common/methods.mjs (2)</summary>

`8-12`: **Good approach for custom error handling.**

Adding the `isCustom` flag to errors makes it easier to distinguish between internal validation errors and external API errors.

---

`255-284`: **The validateUserInput function is well designed.**

Using a mapping object to dispatch to the appropriate validator based on type is a clean approach and makes adding new validators easy.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-21T18:46:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (5)</summary><blockquote>

<details>
<summary>components/google_search_console/common/methods.mjs (4)</summary><blockquote>

`94-102`: _🛠️ Refactor suggestion_

**Method implementation is inverted from what its name suggests.**

Similar to the array validation method, the name suggests it throws when the input is not an object, but the implementation returns the input when it is an object and throws otherwise.


Rename the method to match its behavior:

```diff
-  throwIfNotObject(input, reason) {
+  validateObject(input, reason) {
    
    if( typeof input === "object" && input !== null && !Array.isArray(input)){
      return input;
    };
      
    this.throwCustomError(`Invalid argument type. Expected an object ` +  this._reasonMsg(reason));

  },
```

---

`189-189`: _⚠️ Potential issue_

**Fix misleading character class in regex.**

The character class contains zero-width joiners which can't be matched correctly in a character class.


Replace with alternation as recommended by the static analysis tool:

```diff
-    const dubiousCharRegex = /[\\<>{}|"`^]|[\u200B\u200C\u200D\u2060\uFEFF\u00A0]/g;
+    const dubiousCharRegex = /[\\<>{}|"`^]|\u200B|\u200C|\u200D|\u2060|\uFEFF|\u00A0/g;
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 189-189: A character class cannot match a joined character sequence.

A zero width joiner composes several emojis into a new one. Replace the character class with an alternation.

(lint/suspicious/noMisleadingCharacterClass)

</details>

</details>

---

`63-70`: _🛠️ Refactor suggestion_

**Method names don't match implementation logic.**

The method name `throwIfNotArray` suggests it throws when the condition isn't met, but it returns the input when the condition is met and throws otherwise. This creates confusion about the expected behavior.


Rename the method to better reflect its behavior:

```diff
-  throwIfNotArray(input, reason) {
+  validateArray(input, reason) {

     if (!Array.isArray(input)) {
       this.throwCustomError(`Invalid argument type. Expected an array ` +  this._reasonMsg(reason));
     };

     return input;
   },
```

---

`109-117`: _🛠️ Refactor suggestion_

**Method name contradicts its implementation.**

The naming pattern continues to be inconsistent with the implementation. The method returns the input when it is an object or array, rather than throwing when it's not.


Rename for clarity:

```diff
-throwIfNotObjectOrArray(input, reason) {
+validateObjectOrArray(input, reason) {
    
  if( typeof input === "object" && input !== null){
    return input;
  };
    
  this.throwCustomError(`Invalid argument type. Expected an object or array` +  this._reasonMsg(reason));

},
```

</blockquote></details>
<details>
<summary>components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs (1)</summary><blockquote>

`211-214`: _⚠️ Potential issue_

**Replace hardcoded token with environment variable.**

Hardcoded tokens—even placeholders—risk accidental commits of real credentials.



Load tokens from environment variables instead:

```diff
-            Authorization: `Bearer  *HARDCODED TOKEN HERE*`,
+            Authorization: `Bearer ${process.env.GSC_API_TOKEN}`,
```

Add a `.env.example` file with `GSC_API_TOKEN=your_token_here` and ensure `.env` is in `.gitignore`.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>components/google_search_console/common/methods.mjs (1)</summary><blockquote>

`232-235`: **Return trimmed URL for consistency.**

The method trims the input URL for validation but returns the original untrimmed URL, which could lead to inconsistencies.


Return the trimmed version for consistency:

```diff
  return {
    warnings : warnings,
-    url : input,
+    url : trimmedInput,
  };
```

</blockquote></details>
<details>
<summary>components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs (1)</summary><blockquote>

`179-179`: **Remove debug logging statement.**

This console log is useful for development but should be removed or commented out before submission.


```diff
-      console.log("===VALUE", this[propName]);
+      // console.log("===VALUE", this[propName]);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 173277aab05c6005e572617def1ecab4935c80bb and f4c36fba5e58c5f10f990e6670ed72b95a4e00a6.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `components/google_search_console/actions/retrieve-site-performance-data/README.md` (1 hunks)
* `components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs` (1 hunks)
* `components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs` (1 hunks)
* `components/google_search_console/common/methods.mjs` (1 hunks)
* `components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs` (1 hunks)
* `components/google_search_console/tests/action-tests/test-submit-url-for-indexing.mjs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (4)</summary>

* components/google_search_console/actions/retrieve-site-performance-data/README.md
* components/google_search_console/actions/submit-url-for-indexing/submit-url-for-indexing.mjs
* components/google_search_console/tests/action-tests/test-submit-url-for-indexing.mjs
* components/google_search_console/actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>components/google_search_console/common/methods.mjs</summary>

[error] 189-189: A character class cannot match a joined character sequence.

A zero width joiner composes several emojis into a new one. Replace the character class with an alternation.

(lint/suspicious/noMisleadingCharacterClass)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>components/google_search_console/common/methods.mjs (1)</summary>

`265-273`: **Good implementation of validator selection.**

The validator mapping approach makes the code maintainable and easy to extend. The use of extended types is a nice design that allows specialized validation beyond basic types.

</details>
<details>
<summary>components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs (2)</summary>

`31-69`: **Well-structured test data with comprehensive coverage.**

The test data covers all required fields and includes optional parameters with valid values, making it an excellent test case for the action.

---

`167-199`: **Good validation loop with warning accumulation.**

The validation approach is robust and user-friendly:
1. Trims string inputs
2. Skips optional empty fields
3. Validates inputs with appropriate validators
4. Accumulates warnings instead of failing immediately
5. Builds the request body dynamically

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T10:13:32Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>components/google_search_console/common/tests/methods/test-throwIfNotYMDDashDate.mjs (2)</summary><blockquote>

`6-9`: **Consider expanding test coverage with additional valid cases.**

The `validCases` object only contains a single test case. Adding more explicit valid test cases would improve test coverage and make the validation logic more robust.

```diff
const validCases = {
  aValidYMDDashDate: bogus.aValidYMDDashDate.value,
+  // Add more valid cases with expected results
+  aValidFutureDate: "2030-01-01", 
+  aValidPastDate: "2000-12-31",
};
```

---

`15-52`: **Consider using a standard testing framework.**

The custom test harness with console logs works but lacks the structure and reporting capabilities of standard testing frameworks. Consider migrating to Jest, Mocha, or another testing framework for more maintainable tests.


For example, with Jest:
```javascript
import methods from "../../common/methods.mjs";
import bogus from "./bogus-data/bogus-data-google-date.mjs";

describe('throwIfNotYMDDashDate', () => {
  // Test valid cases
  test.each([
    ['aValidYMDDashDate', bogus.aValidYMDDashDate.value],
  ])('valid date %s returns expected result', (testName, value) => {
    expect(methods.throwIfNotYMDDashDate(value)).toBe(value);
  });
  
  // Test invalid cases
  test.each(
    Object.entries(bogus)
      .filter(([key]) => key !== 'aValidYMDDashDate')
      .map(([key, data]) => [key, data.value, data.extendedType])
  )('invalid date %s throws custom error', (testName, value, type) => {
    expect(() => methods.throwIfNotYMDDashDate(value)).toThrow();
  });
});
```

</blockquote></details>
<details>
<summary>components/google_search_console/common/tests/methods/test-checkIfUrlValid.mjs (1)</summary><blockquote>

`51-89`: **Consider using a standard testing framework.**

Similar to the previous file, consider migrating this custom test harness to a standard testing framework like Jest or Mocha for better maintainability and reporting.


For example with Jest:
```javascript
import methods from "../../../common/methods.mjs";
import bogus from "./bogus-data/bogus-data-url.mjs";

describe('checkIfUrlValid', () => {
  // Test valid cases
  test.each([
    ['aValidHttpsUrl', bogus.aValidHttpsUrl.value, 0, bogus.aValidHttpsUrl.value],
    ['aValidUrlWithoutProtocol', bogus.aValidUrlWithoutProtocol.value, 1, bogus.aValidUrlWithoutProtocol.value],
    // Add more test cases...
  ])('valid URL %s returns expected result', (testName, url, warningCount, normalizedUrl) => {
    const result = methods.checkIfUrlValid(url);
    expect(result.warnings.length).toBe(warningCount);
    expect(result.url).toBe(normalizedUrl);
  });
  
  // Test invalid cases that should throw
  test.each(
    // Filter for cases that should throw
    // ...
  )('invalid URL %s throws custom error', (testName, url) => {
    expect(() => methods.checkIfUrlValid(url)).toThrow();
  });
});
```

</blockquote></details>
<details>
<summary>components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs (1)</summary><blockquote>

`35-39`: **Future test dates may cause issues with the API.**

The test data includes future dates (2025), which may be rejected by the Google Search Console API. Consider using dates in the past for more reliable testing.

```diff
  // Required
-  startDate: "2025-12-22",
-  endDate: "2025-12-31",
+  startDate: "2023-01-01",
+  endDate: "2023-01-31",
```

</blockquote></details>
<details>
<summary>components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs (1)</summary><blockquote>

`53-53`: **Confusing inline comment.**

The comment "TEST ONLY. Replace to 'this'" is ambiguous and doesn't clearly indicate what should be replaced in production code.

```diff
-  const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+  const urlCheck = gsConsole.methods.checkIfUrlValid(this.url); // In production code, use the correct property name
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between de82ab9a47dd4f152e36b75abced765cf49119ae and 127e8dba8675a73788b66bf6ac2d6edaa5539af8.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `components/google_search_console/common/tests/README.md` (1 hunks)
* `components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs` (1 hunks)
* `components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs` (1 hunks)
* `components/google_search_console/common/tests/get-token.mjs` (1 hunks)
* `components/google_search_console/common/tests/methods/bogus-data/bogus-data-google-date.mjs` (1 hunks)
* `components/google_search_console/common/tests/methods/bogus-data/bogus-data-url.mjs` (1 hunks)
* `components/google_search_console/common/tests/methods/test-checkIfUrlValid.mjs` (1 hunks)
* `components/google_search_console/common/tests/methods/test-throwIfNotYMDDashDate.mjs` (1 hunks)
* `components/google_search_console/common/tests/mockery-dollar.mjs` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (5)</summary>

* components/google_search_console/common/tests/mockery-dollar.mjs
* components/google_search_console/common/tests/README.md
* components/google_search_console/common/tests/methods/bogus-data/bogus-data-url.mjs
* components/google_search_console/common/tests/get-token.mjs
* components/google_search_console/common/tests/methods/bogus-data/bogus-data-google-date.mjs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (4)</summary>

* GitHub Check: pnpm publish
* GitHub Check: Verify TypeScript components
* GitHub Check: Lint Code Base
* GitHub Check: Publish TypeScript components

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>components/google_search_console/common/tests/methods/test-checkIfUrlValid.mjs (2)</summary>

`6-45`: **Well-defined test cases covering diverse URL formats.**

The test includes a comprehensive set of valid cases with expected warnings and normalized URLs for various URL formats including:
- Valid HTTP/HTTPS URLs
- URLs without protocols
- URLs with dubious characters
- Malformed slashes

This thorough approach ensures robust validation.

---

`75-78`: **Robust validation checking both warnings and URL normalization.**

The test correctly validates both aspects of the URL validation function:
1. The number of warnings generated
2. The normalized URL output

This dual validation ensures the function provides both proper feedback and correctly handles the URL.

</details>
<details>
<summary>components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs (3)</summary>

`1-18`: **Clear documentation for local test runner.**

The file header provides excellent documentation explaining the purpose, usage requirements, and limitations of the test runner. This helps developers understand how to use it safely for local debugging.

---

`189-213`: **Comprehensive validation and prop processing logic.**

The input processing loop effectively:
1. Validates user inputs with appropriate validation rules
2. Collects warnings without blocking execution for suspicious inputs
3. Populates the request body with validated values
4. Handles optional props appropriately

This robust approach ensures API requests are well-formed while providing clear feedback.

---

`233-239`: **Good error handling with detailed context.**

The error handling logic:
1. Distinguishes between internal validation errors and API errors
2. Includes accumulated warnings in the error message
3. Preserves the original error message for debugging

This helps developers quickly identify whether issues come from input validation or the external API.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T17:10:29Z
user: login: michelle0927
body: Thanks for your contribution! Ready for QA!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T17:32:53Z

Review comments:
diff_hunk: @@ -0,0 +1,111 @@
+export default {
+    aSingleWordString : {
+      value: "wrgwgwepkgprgprpwnwwrehwrh",
+      jsType : "string",
+      extendedType : "singleWordString",
+    },
+    aMultiWordString : {
+      value: "etheth etphmpethm ethpethm",
+      jsType : "string",
+      extendedType : "aMultiWordString",
+    },
path: components/google_search_console/tests/methods/bogus-data/bogus-data-url.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix inconsistent `extendedType` for `aMultiWordString`.**  
The entry’s `extendedType` currently reads `"aMultiWordString"`, but it should drop the leading "a" to match the pattern used elsewhere (`"multiWordString"`). 
```diff
-      extendedType : "aMultiWordString",
+      extendedType : "multiWordString",
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    aMultiWordString : {
      value: "etheth etphmpethm ethpethm",
      jsType : "string",
      extendedType : "multiWordString",
    },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T18:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,113 @@
+export default {
+    aSingleWordString : {
+      value: "wrgwgwepkgprgprpwnwwrehwrh",
+      jsType : "string",
+      extendedType : "singleWordString",
+    },
+    aMultiWordString : {
+      value: "etheth etphmpethm ethpethm",
+      jsType : "string",
+      extendedType : "aMultiWordString",
+    },
+    aPositiveInteger : {
+      value:15115,
+      jsType: "number",
+      extendedType : "positiveInteger",
+    } ,
+    aNegativeInteger : {
+      value:-15115,
+      jsType: "number",
+      extendedType : "negativeInteger",
+    } ,
+    aPositiveFloat : {
+      value:12.155,
+      jsType: "number",
+      extendedType : "positiveFloat",
+    } ,
+    aNegativeFloat : {
+      value:-12.155,
+      jsType: "number",
+      extendedType : "negativeFloat",
+    } ,
+    aZero : {
+      value:0,
+      jsType: "number",
+      extendedType : "zero",
+    } ,
+    anArray : {
+      value : [13135, 35.15, "3feqe", [1,2,3], {some:"val"}, 0],
+      jsType : "object",
+      extendedType : "trueArray"
+    },
+
+    anArrayOfStrings : {
+      value : ["string1", "string2", "string3"],
+      jsType : "object",
+      extendedType : "arrayOfStrings"
+    },
+    anObject : {
+      value : {some: "val", some2: 23, some3 : [1,2,3]},
+      jsType : "object",
+      extendedType : "trueObject"
+    },
+
+    anEmptyString : {
+      value : "",
+      jsType : "string",
+      extendedType : "emptyString"
+    },
+
+    aSpace : {
+      value : " ",
+      jsType : "string",
+      extendedType : "space"
+    },
+
+
+    aValidYMDDashDate : {
+      value :   `2025-05-28`,
+      jsType : "string",
+      extendedType : "ymlDashDate"
+    },
path: components/google_search_console/tests/methods/bogus-data/bogus-data-google-date.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Typo in `extendedType` for YMD dash date entries.**  
All date test cases use `"ymlDashDate"` (with an "l"), but they should use `"ymdDashDate"` (Year-Month-Day). This mismatch will break the date validation tests. 
```diff
-      extendedType : "ymlDashDate"
+      extendedType : "ymdDashDate"
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    aValidYMDDashDate : {
      value :   `2025-05-28`,
      jsType : "string",
-      extendedType : "ymlDashDate"
+      extendedType : "ymdDashDate"
    },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits e1c2238 to 173277a
created_at: 2025-04-21T18:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,3 +9,9 @@ The Google Search Console API opens a treasure trove of data and insights about
 - **Sync Search Results with Google Sheets**: Create a workflow that periodically pulls data from the Google Search Console API and adds it to a Google Sheet. This is useful for maintaining an evolving dataset for deeper analysis, historical reference, or sharing insights across teams without giving direct access to the Search Console.
 
 - **Automatic Sitemap Submission**: Set up a Pipedream workflow that triggers whenever a new sitemap is generated in your content management system (CMS). The workflow can then automatically submit the sitemap to Google Search Console via API, ensuring Google has the latest structure of your site for crawling and indexing.
+
+## Available Actions
+
+[Get Site Performance Data](components/google_search_console/actions/retrieve-site-performance-data/README.md)
+[Submit URL to Google Indexing API](components/google_search_console/actions/submit-url-for-indexing/README.md)
+
path: components/google_search_console/README.md
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Update action links and naming consistency.**  
The current links include the full `components/google_search_console/` path (which will result in broken relative URLs) and use action titles that diverge from the PR summary. Use relative paths and unify the link text:


```diff
- [Get Site Performance Data](components/google_search_console/actions/retrieve-site-performance-data/README.md)
- [Submit URL to Google Indexing API](components/google_search_console/actions/submit-url-for-indexing/README.md)
+ [Retrieve Site Performance Data](./actions/retrieve-site-performance-data/README.md)
+ [Submit URL for Indexing](./actions/submit-url-for-indexing/README.md)
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
[Retrieve Site Performance Data](./actions/retrieve-site-performance-data/README.md)
[Submit URL for Indexing](./actions/submit-url-for-indexing/README.md)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 5cbe03a
created_at: 2025-04-21T18:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,63 @@
+import methods from "../../common/methods.mjs";
+import bogus from "./bogus-data/bogus-data-google-date.mjs";
+
+
+// Any other case from bogus data shoul lead to throw
+// Valid data : expected result;
+const validCases = {
+    
+    aValidYMDDashDate: bogus.aValidYMDDashDate.value,
+};
+
+
+
+
+console.log("Running checkUrl() tests...");
+
+let counterOfFails = 0;
+
+for (const bogusCase in bogus) {
+
+        
+        console.log ("==============");
+    
+        let result;
+        const testArg = bogus[bogusCase].value;
+        const extendedType = bogus[bogusCase].extendedType;
+
+        try {
+            console.log("ENTERED VALUE", testArg, "of extended type ", extendedType);
+            // Call function
+            result = methods.throwIfNotYMDDashDate(testArg);
+            
+        } catch(err) {
+            if (err.isCustom) {
+                console.log ("------- \x1b[32m EXPECTED THROW PASS!\x1b[0m", "Custom error was thrown as expected" );
+                console.log(err.message);
+            } else {
+                console.log ("!!!!!! HARD FAIL!", "UNEXPECTED ERROR WAS THROWN");
+                console.log (err);
+                counterOfFails++;
+            };
+            continue;
+        }
+        
+  
+
+        const pass = 
+        (result === validCases[bogusCase]);
+
+        if (pass) {
+            console.log ("--- \x1b[32m  PASS! \x1b[0m", "Function returned expected values");
+            continue;
+        }
+        else {
+            console.log (" ****************************************** FAIL!", "ENTERED VALUE", testArg);
+            console.log ("Expected ", validCases[bogusCase] , "got ", result);
+            counterOfFails++;
+        };
path: components/google_search_console/tests/methods/test-throwIfNotYMDDashDate.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix potential undefined comparison in test validation**

The test is checking if `result === validCases[bogusCase]`, but `validCases` only contains one key (`aValidYMDDashDate`). When iterating through all keys in `bogus`, most comparisons will be against `undefined`, which could lead to false positives or negatives.

```diff
    const pass = 
-   (result === validCases[bogusCase]);
+   (bogusCase in validCases) ? (result === validCases[bogusCase]) : false;

    if (pass) {
        console.log ("--- \x1b[32m  PASS! \x1b[0m", "Function returned expected values");
        continue;
    }
    else {
+       // Only report as a fail if this was expected to be valid
+       if (!(bogusCase in validCases)) {
+           console.log ("------- \x1b[32m UNEXPECTED PASS\x1b[0m", "Function did not throw for invalid case");
+       } else {
            console.log (" ****************************************** FAIL!", "ENTERED VALUE", testArg);
            console.log ("Expected ", validCases[bogusCase] , "got ", result);
+       }
        counterOfFails++;
    };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        const pass = 
        (bogusCase in validCases) ? (result === validCases[bogusCase]) : false;

        if (pass) {
            console.log ("--- \x1b[32m  PASS! \x1b[0m", "Function returned expected values");
            continue;
        }
        else {
            // Only report as a fail if this was expected to be valid
            if (!(bogusCase in validCases)) {
                console.log ("------- \x1b[32m UNEXPECTED PASS\x1b[0m", "Function did not throw for invalid case");
            } else {
                console.log (" ****************************************** FAIL!", "ENTERED VALUE", testArg);
                console.log ("Expected ", validCases[bogusCase] , "got ", result);
            }
            counterOfFails++;
        };
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T18:46:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,78 @@
+// Bare-bones OAuth script to fetch a token.
+// For testing purposes only — no timeouts, no browser response.
+// Manually terminate the script from the console when finished.
+
+import open from "open";
+import http from "http";
+
+// --- Google OAuth Config ---
+const clientId = "*CLIEN ID HERE*";
+const redirectUri = "http://localhost:3000";
+const scopes = [
+  "https://www.googleapis.com/auth/webmasters.readonly",
+  "https://www.googleapis.com/auth/indexing",
+].join(" ");
+
+const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth");
+authUrl.searchParams.set("client_id", clientId);
+authUrl.searchParams.set("redirect_uri", redirectUri);
+authUrl.searchParams.set("response_type", "code");
+authUrl.searchParams.set("scope", scopes);
+authUrl.searchParams.set("access_type", "offline");
+authUrl.searchParams.set("prompt", "consent");
+
+
+new Promise((resolve, reject) => {
+    // Start a local HTTP server to catch Google's OAuth redirect
+    http.createServer((incomingRequest, outgoingResponse) => {
+      const fullRequestUrl = new URL(incomingRequest.url, `http://${incomingRequest.headers.host}`);
+      const authorizationCode = fullRequestUrl.searchParams.get("code");
+
+      if (authorizationCode) {
+        console.log("AUTH CODE RECEIVED:", authorizationCode);
+        resolve(authorizationCode);
+      } else {
+        console.log("Unexpected request received — no code param found.");
+        reject("Unexpected request received — no code param.");
+      }
+    }).listen(3000, () => {
+      console.log("🌐 Listening on http://localhost:3000");
+      console.log("🔗 Opening browser for Google OAuth...");
+      open(authUrl.toString());
+    });
+})
+.then((authorizationCode) => {
+    console.log("✅ Got the code! Exchanging for tokens...");
+  
+  
+    fetch("https://oauth2.googleapis.com/token", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/x-www-form-urlencoded",
+      },
+      body: new URLSearchParams({
+        code: authorizationCode,
+        client_id: clientId,
+        client_secret: "*CLIENT SECRET HERE", 
+        redirect_uri: redirectUri,
+        grant_type: "authorization_code",
+      }),
+    })
+      .then((res) => res.json())
+      .then((data) => {
+        if (data.access_token) {
+          console.log("🎉 Tokens received!");
+          console.log("Access Token:", data.access_token);
+          console.log("Refresh Token:", data.refresh_token);
+          console.log("Expires In:", data.expires_in);
+        } else {
+          console.error(" Failed to get tokens:", data);
+        }
+      })
+      .catch((err) => {
+        console.error("Error while exchanging code:", err);
+      });
+  })
+  .catch((error) => {
+    console.error("Error during authorization:", error);
+  });
path: components/google_search_console/tests/get-token.mjs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve token retrieval script with proper server shutdown**

The current script starts an HTTP server but never closes it, requiring manual termination. Consider adding proper cleanup after token retrieval.

```diff
.then((authorizationCode) => {
    console.log("✅ Got the code! Exchanging for tokens...");
  
  
    fetch("https://oauth2.googleapis.com/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      body: new URLSearchParams({
        code: authorizationCode,
        client_id: clientId,
        client_secret: "*CLIENT SECRET HERE*", 
        redirect_uri: redirectUri,
        grant_type: "authorization_code",
      }),
    })
      .then((res) => res.json())
      .then((data) => {
        if (data.access_token) {
          console.log("🎉 Tokens received!");
          console.log("Access Token:", data.access_token);
          console.log("Refresh Token:", data.refresh_token);
          console.log("Expires In:", data.expires_in);
+         console.log("✅ Token retrieval complete. Shutting down server...");
+         process.exit(0);
        } else {
          console.error(" Failed to get tokens:", data);
+         process.exit(1);
        }
      })
      .catch((err) => {
        console.error("Error while exchanging code:", err);
+       process.exit(1);
      });
  })
  .catch((error) => {
    console.error("Error during authorization:", error);
+   process.exit(1);
  });
```

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commits e1c2238 to 173277a
created_at: 2025-04-21T18:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,90 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects a mocked `$` object (`mockery$`)
+ * - Bypasses OAuth using a hardcoded access token (get-token.mjs)
+ * - Sends a URL submission request to the Google Indexing API
+ *
+ *   You MUST:
+ * - Replace the `Authorization` token with a valid one manually
+ * - Ensure the `siteUrl` is verified in your Search Console account
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs"; // TEST ONLY. Delete in production code
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../google_search_console.app.mjs";
+import {trimIfString } from "../../common/utils.mjs"
+
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+};
+
+ 
+const testAction = { // TEST ONLY. Replace to export in real code
+
+  ...mockeryData,  // TEST ONLY. Delete in production code
+
+  name: "Google test",
+  description: "This is a demo action",
+  key: "google_test",
+  version: "0.0.23",
+  type: "action",
+  props: {
+    gsConsole,
+    url : {type: "string", label: "URL for indexing", description: "URL to be submitted for indexing"},
+  },
+  async run({ $ }) {
+
+    this.siteUrl = trimIfString(this.siteUrl);
path: components/google_search_console/tests/action-tests/test-submit-url-for-indexing.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix inconsistency between prop definition and usage**

The action defines a `url` prop but then uses `this.siteUrl` in the implementation. This inconsistency could lead to confusion and bugs.

```diff
  props: {
    gsConsole,
-   url : {type: "string", label: "URL for indexing", description: "URL to be submitted for indexing"},
+   siteUrl : {type: "string", label: "URL for indexing", description: "URL to be submitted for indexing"},
  },
  async run({ $ }) {

    this.siteUrl = trimIfString(this.siteUrl);
```

Or alternatively:

```diff
  props: {
    gsConsole,
    url : {type: "string", label: "URL for indexing", description: "URL to be submitted for indexing"},
  },
  async run({ $ }) {

-   this.siteUrl = trimIfString(this.siteUrl);
+   this.url = trimIfString(this.url);
```

And update all subsequent occurrences of `this.siteUrl` to `this.url`.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  props: {
    gsConsole,
    siteUrl : {type: "string", label: "URL for indexing", description: "URL to be submitted for indexing"},
  },
  async run({ $ }) {

    this.siteUrl = trimIfString(this.siteUrl);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T18:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,240 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects mocked `$` object (logs, summary)
+ * - Bypasses OAuth by using hardcoded access token (get-token.mjs)
+ * - Validates, builds, and sends the Search Console request
+ *
+ *   You MUST:
+ * - Replace `Authorization` token with a valid one manually
+ * - Ensure `siteUrl` is verified in your Search Console
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs";
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../google_search_console.app.mjs";
+import {removeCustomPropFields, trimIfString } from "../../common/utils.mjs"
+
+// Define prop metadata separately and spread it into the props object.
+// Useful for accessing extended metadata during runtime — available because it stays in closure.
+
+// ====================== TEST PREPARATION =====================================================
+
+// TEST ONLY
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+
+  // Required
+  startDate: "2025-12-22",
+  endDate: "2025-12-31",
+
+  // Recommended
+  dimensions: ["query", "page", "country", "device"], // valid values only
+
+  searchType: "web", // one of: web, image, video, news, googleNews, discover
+
+  rowLimit: 10,
+  
+  startRow: 0, // Optional pagination
+
+  // Optional but valid
+  aggregationType: "auto", // or "byPage"
+  dataState: "final", // or "all"
+
+  // Optional filter (valid structure)
+  dimensionFilterGroups: [
+    {
+      groupType: "and", // "and" or "or"
+      filters: [
+        {
+          dimension: "query",
+          operator: "contains", // "equals", "contains", "notEquals", etc.
+          expression: "example",
+        },
+        {
+          dimension: "country",
+          operator: "equals",
+          expression: "USA",
+        },
+      ],
+    },
+  ],
+};
+
+
+
+const propsMeta = {
+
+  
+  siteUrl: {
+    type: "string",
+    extendedType: "url",
+    label: "Verified Site URL",
+    description: "Including https:// is strongly advised",
+  },
+  startDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "Start Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  endDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "End Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  dimensions: {
+    type: "string[]",
+    label: "Dimensions",
+    optional: true,
+    description: "e.g. ['query', 'page', 'country', 'device']",
+    postBody: true,
+  },
+  searchType: {
+    type: "string",
+    label: "Search Type",
+    optional: true,
+    options: ["web", "image", "video", "news", "googleNews", "discover"],
+    default: "web",
+    postBody: true,
+  },
+  aggregationType: {
+    type: "string",
+    label: "Aggregation Type",
+    optional: true,
+    options: ["auto", "byPage"],
+    postBody: true,
+  },
+  rowLimit: {
+    type: "integer",
+    label: "Max rows to return",
+    default: 10,
+    postBody: true,
+  },
+  startRow: {
+    type: "integer",
+    label: "Start row (for pagination)",
+    optional: true,
+    postBody: true,
+  },
+  dimensionFilterGroups: {
+    type: "object",
+    label: "Dimension Filters",
+    optional: true,
+    description: "Follow Search Console API structure for filters",
+    postBody: true,
+  },
+  dataState: {
+    type: "string",
+    label: "Data State",
+    optional: true,
+    options: ["all", "final"],
+    default: "final",
+    postBody: true,
+  },
+
+
+
+};
+
+const testAction = {
+
+  ...mockeryData,
+  
+  name: "Get Site Performance Data",
+  description: "Fetches search analytics data for a verified site.",
+  key: "get_search_console_analytics",
+  version: "0.0.47",
+  type: "action",
+  props: {
+    gsConsole,
+   // Remove custom prop metadata and spread only valid prop fields
+    ...removeCustomPropFields(propsMeta),
+  },
+
+
+  async run({$}) {
+
+    // body for POST request. Will be filled in the following loop.
+    const body = {};
+
+    // warnings accumulator 
+    let warnings = [];
+
+    /* This loop performs the following tasks:
+   - Validates user input
+   - Populates the `body` object for the upcoming POST request
+   - Accumulates messages to display to the user at the end of the action
+*/ 
+    for (let propName in propsMeta){
+
+      console.log("===VALUE", this[propName]); // TEST ONLY
+      const meta = propsMeta[propName];
+
+       // Trim the input if it's a string
+       this[propName] = trimIfString(this[propName]);
+
+       // If the optional prop is undefined, null, or a blank string — skip it
+      if (meta.optional === true && ((this[propName] ?? '') === '')) continue;
+      
+       // Validate the input and throw an error if it's invalid.
+      // Also return an empty string or a warning message, if applicable
+      const validationResult = gsConsole.methods.validateUserInput(meta, this[propName]);
+
+      // Push the warnings into warnings accumulator if any.
+      if (validationResult.warnings) warnings.push(...validationResult.warnings);
+
+      // If the prop should be included in the POST request, add it to the body
+      if (meta.postBody === true) body[propName] = this[propName];
+
+      console.log(" SUCCESS"); // TEST ONLY
+    };
+
+    // Trimmed in loop above
+    const url = this.siteUrl;
+
+    // Response of the POST request.
+    let response;
+
+    try {
+        response = await axios($, {
+          method: "POST",
+          url: `https://searchconsole.googleapis.com/webmasters/v3/sites/${encodeURIComponent(url)}/searchAnalytics/query`,
+          headers: {
+             // TEMP: Local-only token for manual testing. Do not commit this to source control.
+            Authorization: `Bearer  *HARDCODED TOKEN HERE*`,
+            "Content-Type": "application/json",
path: components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Hardcoded token poses a security risk.**

While this is a local test file, including placeholders for tokens in code could lead to accidental commits of actual tokens. Consider using environment variables or a local config file that's gitignored.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any tokens in the codebase
rg -i "(token|key|secret|password|credential)" --type js | grep -i "hardcoded\|placeholder\|here"
```

Length of output: 1226

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for hardcoded tokens in .mjs files
rg -n 'HARDCODED TOKEN' -g '*.mjs'
```

Length of output: 339

---



</details>

**Replace hardcoded tokens in test files with environment variables**

Hardcoded placeholder tokens—even in local tests—risk accidental commits of real credentials. Instead, load tokens from environment variables (e.g. via a .env file that’s gitignored) and provide a .env.example.

Please update the following files:
- components/google_search_console/tests/action-tests/test-retrieve-site-performance-data.mjs (line 213)  
- components/google_search_console/tests/action-tests/test-submit-url-for-indexing.mjs (line 64)

Suggested change:
```diff
- Authorization: `Bearer *HARDCODED TOKEN HERE*`,
+ Authorization: `Bearer ${process.env.GSC_API_TOKEN}`,
```

• Add a `.env.example` with GSC_API_TOKEN  
• Ensure `.env` is listed in `.gitignore`  
• Document in README/test setup how to supply the token in CI and local runs  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
          headers: {
             // TEMP: Local-only token for manual testing. Do not commit this to source control.
            Authorization: `Bearer ${process.env.GSC_API_TOKEN}`,
            "Content-Type": "application/json",
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T18:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,309 @@
+
+export default {
+
+/* ===================================================================================================
+    Throws a custom error and marks it with a flag (`isCustom`) for easier debugging and testing.
+====================================================================================================== */
+
+  throwCustomError(msg){
+    const err = new Error(msg);
+    err.isCustom = true;
+    throw err; 
+  },
+
+/* ===================================================================================================
+    Determines whether an error originated from your own validation code or from the API request.
+    Useful for debugging and crafting more helpful error messages.
+====================================================================================================== */
+
+  // ===================================================================== 
+  checkWhoThrewError(error) {   
+    return { whoThrew: error?.response?.status ? "API response" : "Internal Code", error, };
+  },
+
+/* ===================================================================================================
+    Throws if the input is not a string or is a blank string (only whitespace, tabs, newlines, etc.).
+====================================================================================================== */
+
+  throwIfBlankOrNotString(input, reason) {
+
+    if (typeof input !== "string") {
+      this.throwCustomError(
+        `Expected a string, but got ${typeof input}` + this._reasonMsg(reason)
+      );
+    }
+
+    if (input.trim() === "") {
+      this.throwCustomError(
+        "Expected a non-empty string with visible characters." + this._reasonMsg(reason)
+      );
+    }
+
+    return input;
+  },
+
+/* ===================================================================================================
+    Throws if the input is not a whole number (non-negative integer).
+====================================================================================================== */
+
+  throwIfNotWholeNumber(input, reason) {
+    if (!Number.isInteger(input) || input < 0) {
+      this.throwCustomError( `Expected a whole number (0 or positive integer), but got ${typeof input}` + this._reasonMsg(reason));
+
+    }
+
+    return input;
+  },
+
+
+/* ===================================================================================================
+    Throws if the input is not an array.
+====================================================================================================== */
+
+  throwIfNotArray(input, reason) {
+
+    if (!Array.isArray(input)) {
+      this.throwCustomError(`Invalid argument type. Expected an array ` +  this._reasonMsg(reason));
+    };
+
+    return input;
+  },
path: components/google_search_console/common/methods.mjs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Method names are potentially misleading.**

The names `throwIfNotArray`, `throwIfNotObject`, and `throwIfNotObjectOrArray` suggest they throw when the condition isn't met, but they actually throw when the condition *is* met. Consider renaming:

- `throwIfNotArray` → `validateArray`
- `throwIfNotObject` → `validateObject` 
- `throwIfNotObjectOrArray` → `validateObjectOrArray`

Or adjust the implementation to match the names.



Also applies to: 93-102, 109-117

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T18:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,309 @@
+
+export default {
+
+/* ===================================================================================================
+    Throws a custom error and marks it with a flag (`isCustom`) for easier debugging and testing.
+====================================================================================================== */
+
+  throwCustomError(msg){
+    const err = new Error(msg);
+    err.isCustom = true;
+    throw err; 
+  },
+
+/* ===================================================================================================
+    Determines whether an error originated from your own validation code or from the API request.
+    Useful for debugging and crafting more helpful error messages.
+====================================================================================================== */
+
+  // ===================================================================== 
+  checkWhoThrewError(error) {   
+    return { whoThrew: error?.response?.status ? "API response" : "Internal Code", error, };
+  },
+
+/* ===================================================================================================
+    Throws if the input is not a string or is a blank string (only whitespace, tabs, newlines, etc.).
+====================================================================================================== */
+
+  throwIfBlankOrNotString(input, reason) {
+
+    if (typeof input !== "string") {
+      this.throwCustomError(
+        `Expected a string, but got ${typeof input}` + this._reasonMsg(reason)
+      );
+    }
+
+    if (input.trim() === "") {
+      this.throwCustomError(
+        "Expected a non-empty string with visible characters." + this._reasonMsg(reason)
+      );
+    }
+
+    return input;
+  },
+
+/* ===================================================================================================
+    Throws if the input is not a whole number (non-negative integer).
+====================================================================================================== */
+
+  throwIfNotWholeNumber(input, reason) {
+    if (!Number.isInteger(input) || input < 0) {
+      this.throwCustomError( `Expected a whole number (0 or positive integer), but got ${typeof input}` + this._reasonMsg(reason));
+
+    }
+
+    return input;
+  },
+
+
+/* ===================================================================================================
+    Throws if the input is not an array.
+====================================================================================================== */
+
+  throwIfNotArray(input, reason) {
+
+    if (!Array.isArray(input)) {
+      this.throwCustomError(`Invalid argument type. Expected an array ` +  this._reasonMsg(reason));
+    };
+
+    return input;
+  },
+
+/* ===================================================================================================
+    Throws if the input is not an array of strings.
+====================================================================================================== */
+
+  throwIfNotArrayOfStrings(input, reason){
+
+    this.throwIfNotArray(input, reason);
+
+    for (let i = 0; i < input.length; i++) {
+      if (typeof input[i] !== "string"){
+         this.throwCustomError(`Expected an array of strings ` +  this._reasonMsg(reason));
+      }
+    }; 
+
+    return input;
+  },
+
+
+/* ===================================================================================================
+    Throws if the input is not a plain object (arrays are excluded).
+====================================================================================================== */
+
+  throwIfNotObject(input, reason) {
+    
+    if( typeof input === "object" && input !== null && !Array.isArray(input)){
+      return input;
+    };
+      
+    this.throwCustomError(`Invalid argument type. Expected an object ` +  this._reasonMsg(reason));
+
+  },
+
+
+/* ===================================================================================================
+    Throws if the input is neither an object nor an array.
+====================================================================================================== */
+
+throwIfNotObjectOrArray(input, reason) {
+    
+  if( typeof input === "object" && input !== null){
+    return input;
+  };
+    
+  this.throwCustomError(`Invalid argument type. Expected an object or array` +  this._reasonMsg(reason));
+
+},
+
+
+
+/* ===================================================================================================
+    Validates that a string follows the YYYY-MM-DD date format.
+    Throws if the format or any individual part (year, month, day) is invalid.
+====================================================================================================== */
+
+  throwIfNotYMDDashDate(input, reason) {
+
+    this.throwIfBlankOrNotString(input, reason);
+  
+    const parts = input.trim().split("-");
+  
+    if (parts.length !== 3) {
+      this.throwCustomError("Date must be in format YYYY-MM-DD"  +  this._reasonMsg(reason));
+    };
+  
+    let [year, month, day] = parts;
+  
+    // --- YEAR ---
+    year = +year;
+    if (!Number.isInteger(year) || year < 1990 || year > 2100) {
+      this.throwCustomError("Year must be between 1990 and 2100" +  this._reasonMsg(reason));
+    };
+  
+    const monthNum = +month;
+    if (month.length !== 2 || Number.isNaN(monthNum) || monthNum < 1 || monthNum > 12 ) {
+      this.throwCustomError(`Month must be between 01 and 12. Got: '${month}'` +  this._reasonMsg(reason));
+    };
+  
+    const dayNum = +day;
+    if (day.length !== 2 || Number.isNaN(dayNum) || dayNum < 1 || dayNum > 31 ) {
+      this.throwCustomError(`Day must be between 01 and 31. Got: '${day}'` +  this._reasonMsg(reason));
+    };
+  
+    return input;
+  },
+
+
+
+/* ===================================================================================================
+    Validates a URL string:
+    - Rejects blank strings, spaces, tabs, and newlines
+    - Warns about suspicious or unusual characters
+    - Adds a warning if protocol is missing or malformed
+    Returns:
+      {
+        warnings: string[],
+        url: string (trimmed original input)
+      }
+====================================================================================================== */
+
+  // =====================================================================
+  checkIfUrlValid(input, reason) {
+   
+    // Throws an error if the input is not a string or if its a blank string;
+    this.throwIfBlankOrNotString(input);
+
+    // Warnin accumulator
+    let warnings = [];
+;
+    // Trim the input (already checked for string);
+    const trimmedInput = input.trim();
+
+    // Reject if spaces, tabs, or newlines are present
+    if ((/[ \t\n]/.test(trimmedInput))) {
+      this.throwCustomError( `Url contains invalid characters like space, backslash etc., please check.`  +  this._reasonMsg(reason))
+    };
+  
+  // Warn about suspicious characters
+    const dubiousCharRegex = /[\\<>{}|"`^]|[\u200B\u200C\u200D\u2060\uFEFF\u00A0]/g;
+
path: components/google_search_console/common/methods.mjs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Fix misleading character class in regex.**

The static analysis tool correctly identified an issue with the character class that contains zero-width joiners.

```diff
-    const dubiousCharRegex = /[\\<>{}|"`^]|[\u200B\u200C\u200D\u2060\uFEFF\u00A0]/g;
+    const dubiousCharRegex = /[\\<>{}|"`^]|\u200B|\u200C|\u200D|\u2060|\uFEFF|\u00A0/g;
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const dubiousCharRegex = /[\\<>{}|"`^]|\u200B|\u200C|\u200D|\u2060|\uFEFF|\u00A0/g;
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 189-189: A character class cannot match a joined character sequence.

A zero width joiner composes several emojis into a new one. Replace the character class with an alternation.

(lint/suspicious/noMisleadingCharacterClass)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-21T18:46:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,56 @@
+import methods from "../../common/methods.mjs";
+import bogus from "./bogus-data/bogus-data-google-date.mjs";
+
+// Any other case from bogus data shoul lead to throw
+// Valid data : expected result;
+const validCases = {
+
+  aValidYMDDashDate: bogus.aValidYMDDashDate.value,
+};
+
+console.log("Running throwIfNotYMDDashDate() tests...");
+
+let counterOfFails = 0;
+
+for (const bogusCase in bogus) {
+
+  console.log ("==============");
+
+  let result;
+  const testArg = bogus[bogusCase].value;
+  const extendedType = bogus[bogusCase].extendedType;
+
+  try {
+    console.log("ENTERED VALUE", testArg, "of extended type ", extendedType);
+    // Call function
+    result = methods.throwIfNotYMDDashDate(testArg);
+
+  } catch (err) {
+    if (err.isCustom) {
+      console.log ("------- \x1b[32m EXPECTED THROW PASS!\x1b[0m", "Custom error was thrown as expected" );
+      console.log(err.message);
+    } else {
+      console.log ("!!!!!! HARD FAIL!", "UNEXPECTED ERROR WAS THROWN");
+      console.log (err);
+      counterOfFails++;
+    };
+    continue;
+  }
+
+  const pass =
+        (result === validCases[bogusCase]);
+
path: components/google_search_console/common/tests/methods/test-throwIfNotYMDDashDate.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Logic issue: comparing results with potentially undefined values.**

The comparison checks if `result === validCases[bogusCase]`, but for test cases not defined in `validCases`, this will compare against `undefined`, potentially causing false failures.

```diff
const pass =
-      (result === validCases[bogusCase]);
+      (validCases[bogusCase] !== undefined && result === validCases[bogusCase]);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const pass =
-        (result === validCases[bogusCase]);
+        (validCases[bogusCase] !== undefined && result === validCases[bogusCase]);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T17:10:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,251 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects mocked `$` object (logs, summary)
+ * - Bypasses OAuth by using hardcoded access token (get-token.mjs)
+ * - Validates, builds, and sends the Search Console request
+ *
+ *   You MUST:
+ * - Replace `Authorization` token with a valid one manually
+ * - Ensure `siteUrl` is verified in your Search Console
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs";
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import {
+  removeCustomPropFields, trimIfString,
+} from "../../../common/utils.mjs";
+
+// Define prop metadata separately and spread it into the props object.
+// Useful for accessing extended metadata during runtime — available because it stays in closure.
+
+// ====================== TEST PREPARATION =====================================================
+
+// TEST ONLY
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+
+  // Required
+  startDate: "2025-12-22",
+  endDate: "2025-12-31",
+
+  // Recommended
+  dimensions: [
+    "query",
+    "page",
+    "country",
+    "device",
+  ], // valid values only
+
+  searchType: "web", // one of: web, image, video, news, googleNews, discover
+
+  rowLimit: 10,
+
+  startRow: 0, // Optional pagination
+
+  // Optional but valid
+  aggregationType: "auto", // or "byPage"
+  dataState: "final", // or "all"
+
+  // Optional filter (valid structure)
+  dimensionFilterGroups: [
+    {
+      groupType: "and", // "and" or "or"
+      filters: [
+        {
+          dimension: "query",
+          operator: "contains", // "equals", "contains", "notEquals", etc.
+          expression: "example",
+        },
+        {
+          dimension: "country",
+          operator: "equals",
+          expression: "USA",
+        },
+      ],
+    },
+  ],
+};
+
+const propsMeta = {
+
+  siteUrl: {
+    type: "string",
+    extendedType: "url",
+    label: "Verified Site URL",
+    description: "Including https:// is strongly advised",
+  },
+  startDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "Start Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  endDate: {
+    type: "string",
+    extendedType: "YYYY-MM-DD",
+    label: "End Date (YYYY-MM-DD)",
+    postBody: true,
+  },
+  dimensions: {
+    type: "string[]",
+    label: "Dimensions",
+    optional: true,
+    description: "e.g. ['query', 'page', 'country', 'device']",
+    postBody: true,
+  },
+  searchType: {
+    type: "string",
+    label: "Search Type",
+    optional: true,
+    options: [
+      "web",
+      "image",
+      "video",
+      "news",
+      "googleNews",
+      "discover",
+    ],
+    default: "web",
+    postBody: true,
+  },
+  aggregationType: {
+    type: "string",
+    label: "Aggregation Type",
+    optional: true,
+    options: [
+      "auto",
+      "byPage",
+    ],
+    postBody: true,
+  },
+  rowLimit: {
+    type: "integer",
+    label: "Max rows to return",
+    default: 10,
+    postBody: true,
+  },
+  startRow: {
+    type: "integer",
+    label: "Start row (for pagination)",
+    optional: true,
+    postBody: true,
+  },
+  dimensionFilterGroups: {
+    type: "object",
+    label: "Dimension Filters",
+    optional: true,
+    description: "Follow Search Console API structure for filters",
+    postBody: true,
+  },
+  dataState: {
+    type: "string",
+    label: "Data State",
+    optional: true,
+    options: [
+      "all",
+      "final",
+    ],
+    default: "final",
+    postBody: true,
+  },
+
+};
+
+const testAction = {
+
+  ...mockeryData,
+
+  name: "Get Site Performance Data",
+  description: "Fetches search analytics data for a verified site.",
+  key: "get_search_console_analytics",
+  version: "0.0.47",
+  type: "action",
+  props: {
+    gsConsole,
+    // Remove custom prop metadata and spread only valid prop fields
+    ...removeCustomPropFields(propsMeta),
+  },
+
+  async run({ $ }) {
+
+    // body for POST request. Will be filled in the following loop.
+    const body = {};
+
+    // warnings accumulator
+    let warnings = [];
+
+    /* This loop performs the following tasks:
+   - Validates user input
+   - Populates the `body` object for the upcoming POST request
+   - Accumulates messages to display to the user at the end of the action
+*/
+    for (let propName in propsMeta) {
+
+      console.log("===VALUE", this[propName]);
+      const meta = propsMeta[propName];
+
+      // Trim the input if it's a string
+      this[propName] = trimIfString(this[propName]);
+
+      // If the optional prop is undefined, null, or a blank string — skip it
+      if (meta.optional === true && ((this[propName] ?? "") === "")) continue;
+
+      // Validate the input and throw an error if it's invalid.
+      // Also return an empty string or a warning message, if applicable
+      const validationResult = gsConsole.methods.validateUserInput(meta, this[propName]);
+
+      // Push the warnings into warnings accumulator if any.
+      if (validationResult.warnings) warnings.push(...validationResult.warnings);
+
+      // If the prop should be included in the POST request, add it to the body
+      if (meta.postBody === true) body[propName] = this[propName];
+
+      console.log(" SUCCESS");
+    };
+
+    // Trimmed in loop above
+    const url = this.siteUrl;
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: `https://searchconsole.googleapis.com/webmasters/v3/sites/${encodeURIComponent(url)}/searchAnalytics/query`,
+        headers: {
+          // TEMP: Local-only token for manual testing. Do not commit this to source control.
+          "Authorization": "Bearer  *HARDCODED TOKEN HERE*",
path: components/google_search_console/common/tests/action-tests/test-retrieve-site-performance-data.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Security risk: Replace token placeholder with a safer format.**

The placeholder for the hardcoded token could accidentally be replaced with a real token and committed to source control. Use a format that can't be mistaken for a valid token.

```diff
-          "Authorization": "Bearer  *HARDCODED TOKEN HERE*",
+          "Authorization": "Bearer  REPLACE_WITH_VALID_TOKEN_DO_NOT_COMMIT",
```

Consider using environment variables instead:
```javascript
"Authorization": `Bearer ${process.env.GOOGLE_API_TOKEN}`,
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T17:10:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects a mocked `$` object (`mockery$`)
+ * - Bypasses OAuth using a hardcoded access token (get-token.mjs)
+ * - Sends a URL submission request to the Google Indexing API
+ *
+ *   You MUST:
+ * - Replace the `Authorization` token with a valid one manually
+ * - Ensure the `siteUrl` is verified in your Search Console account
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs"; // TEST ONLY. Delete in production code
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import { trimIfString } from "../../../common/utils.mjs";
+
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+};
+
+const testAction = { // TEST ONLY. Replace to export in real code
+
+  ...mockeryData,  // TEST ONLY. Delete in production code
+
+  name: "Google test",
+  description: "This is a demo action",
+  key: "google_test",
+  version: "0.0.23",
+  type: "action",
+  props: {
+    gsConsole,
+    url: {
+      type: "string",
+      label: "URL for indexing",
+      description: "URL to be submitted for indexing",
+    },
+  },
+  async run({ $ }) {
+
+    this.siteUrl = trimIfString(this.siteUrl);
+
+    // warnings accumulator
+    let warnings = [];
+
+    const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+
+    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: "https://indexing.googleapis.com/v3/urlNotifications:publish",
+        headers: {
+          // Tested with real hardcoded token that had required scopes.
+          "Authorization": "Bearer *HARDCODED TOKEN HERE*", // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing".
path: components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Security risk: Replace token placeholder with a safer format.**

Similar to the other file, the hardcoded token placeholder should use a format that can't be mistaken for a real token to avoid accidental commitment of credentials.

```diff
-          "Authorization": "Bearer *HARDCODED TOKEN HERE*", // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing".
+          "Authorization": "Bearer REPLACE_WITH_VALID_TOKEN_DO_NOT_COMMIT", // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing".
```

Consider using environment variables instead:
```javascript
"Authorization": `Bearer ${process.env.GOOGLE_INDEXING_API_TOKEN}`,
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T17:10:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects a mocked `$` object (`mockery$`)
+ * - Bypasses OAuth using a hardcoded access token (get-token.mjs)
+ * - Sends a URL submission request to the Google Indexing API
+ *
+ *   You MUST:
+ * - Replace the `Authorization` token with a valid one manually
+ * - Ensure the `siteUrl` is verified in your Search Console account
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs"; // TEST ONLY. Delete in production code
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import { trimIfString } from "../../../common/utils.mjs";
+
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+};
+
+const testAction = { // TEST ONLY. Replace to export in real code
+
+  ...mockeryData,  // TEST ONLY. Delete in production code
+
+  name: "Google test",
+  description: "This is a demo action",
+  key: "google_test",
+  version: "0.0.23",
+  type: "action",
+  props: {
+    gsConsole,
+    url: {
+      type: "string",
+      label: "URL for indexing",
+      description: "URL to be submitted for indexing",
+    },
+  },
+  async run({ $ }) {
+
+    this.siteUrl = trimIfString(this.siteUrl);
+
+    // warnings accumulator
+    let warnings = [];
+
+    const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+
+    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: "https://indexing.googleapis.com/v3/urlNotifications:publish",
+        headers: {
+          // Tested with real hardcoded token that had required scopes.
+          "Authorization": "Bearer *HARDCODED TOKEN HERE*", // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing".
+          "Content-Type": "application/json",
+        },
+        data: {
+          url: this.siteUrl,
+          type: "URL_UPDATED",
+        },
+      });
+    } catch (error) {
+      // Check who threw the error. Internal code or the request. To ease debugging.
+      const thrower = gsConsole.methods.checkWhoThrewError(error);
+
+      throw new Error(`Failed to fetch data ( ${thrower.whoThrew} error ) : ${error.message}. ` + warnings.join("\n- "));
+
+    };
+
+    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + warnings.join("\n- "));
path: components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Property name mismatch in summary message.**

The URL in the summary message should match the property name used for validation.

```diff
-    $.export("$summary", ` URL submitted to Google: ${this.siteUrl}`  + warnings.join("\n- "));
+    $.export("$summary", ` URL submitted to Google: ${this.url}`  + warnings.join("\n- "));
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    $.export("$summary", ` URL submitted to Google: ${this.url}`  + warnings.join("\n- "));
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T17:10:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects a mocked `$` object (`mockery$`)
+ * - Bypasses OAuth using a hardcoded access token (get-token.mjs)
+ * - Sends a URL submission request to the Google Indexing API
+ *
+ *   You MUST:
+ * - Replace the `Authorization` token with a valid one manually
+ * - Ensure the `siteUrl` is verified in your Search Console account
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs"; // TEST ONLY. Delete in production code
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import { trimIfString } from "../../../common/utils.mjs";
+
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+};
+
+const testAction = { // TEST ONLY. Replace to export in real code
+
+  ...mockeryData,  // TEST ONLY. Delete in production code
+
+  name: "Google test",
+  description: "This is a demo action",
+  key: "google_test",
+  version: "0.0.23",
+  type: "action",
+  props: {
+    gsConsole,
+    url: {
+      type: "string",
+      label: "URL for indexing",
+      description: "URL to be submitted for indexing",
+    },
+  },
+  async run({ $ }) {
+
+    this.siteUrl = trimIfString(this.siteUrl);
+
+    // warnings accumulator
+    let warnings = [];
+
+    const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+
+    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+
+    // Response of the POST request.
+    let response;
+
+    try {
+      response = await axios($, {
+        method: "POST",
+        url: "https://indexing.googleapis.com/v3/urlNotifications:publish",
+        headers: {
+          // Tested with real hardcoded token that had required scopes.
+          "Authorization": "Bearer *HARDCODED TOKEN HERE*", // Replace with a token from get-token.mjs with scope: "https://www.googleapis.com/auth/indexing".
+          "Content-Type": "application/json",
+        },
+        data: {
+          url: this.siteUrl,
path: components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Property name mismatch in request body.**

The URL in the request body should match the property name used for validation.

```diff
        data: {
-          url: this.siteUrl,
+          url: this.url,
          type: "URL_UPDATED",
        },
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        data: {
          url: this.url,
          type: "URL_UPDATED",
        },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit fbf3961
created_at: 2025-04-24T17:10:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,92 @@
+/**
+ * ─────────────────────────────────────────────────────────────────────────────
+ *  LOCAL TEST RUNNER – DO NOT DEPLOY
+ *
+ * This file is used to run and debug a Pipedream action **locally** outside
+ * of the Pipedream platform. Safe to delete. It does **not** affect production.
+ *
+ *  It:
+ * - Injects a mocked `$` object (`mockery$`)
+ * - Bypasses OAuth using a hardcoded access token (get-token.mjs)
+ * - Sends a URL submission request to the Google Indexing API
+ *
+ *   You MUST:
+ * - Replace the `Authorization` token with a valid one manually
+ * - Ensure the `siteUrl` is verified in your Search Console account
+ *
+ * ─────────────────────────────────────────────────────────────────────────────
+ */
+
+import mockery$ from "../mockery-dollar.mjs"; // TEST ONLY. Delete in production code
+import { axios } from "@pipedream/platform";
+import gsConsole from "../../../google_search_console.app.mjs";
+import { trimIfString } from "../../../common/utils.mjs";
+
+const mockeryData = {
+  siteUrl: "https://falc1.com/", // Must be verified in your GSC account
+};
+
+const testAction = { // TEST ONLY. Replace to export in real code
+
+  ...mockeryData,  // TEST ONLY. Delete in production code
+
+  name: "Google test",
+  description: "This is a demo action",
+  key: "google_test",
+  version: "0.0.23",
+  type: "action",
+  props: {
+    gsConsole,
+    url: {
+      type: "string",
+      label: "URL for indexing",
+      description: "URL to be submitted for indexing",
+    },
+  },
+  async run({ $ }) {
+
+    this.siteUrl = trimIfString(this.siteUrl);
+
+    // warnings accumulator
+    let warnings = [];
+
+    const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+
+    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
+
path: components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Mismatch between input property name and usage.**

The test action defines a prop named `url` (line 40-44) but references `this.siteUrl` in the code. This mismatch will cause the action to fail.

```diff
-  this.siteUrl = trimIfString(this.siteUrl);
+  this.url = trimIfString(this.url);

  // warnings accumulator
  let warnings = [];

-  const urlCheck = gsConsole.methods.checkIfUrlValid(this.siteUrl); // TEST ONLY. Replace to "this"
+  const urlCheck = gsConsole.methods.checkIfUrlValid(this.url);
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    this.url = trimIfString(this.url);

    // warnings accumulator
    let warnings = [];

    const urlCheck = gsConsole.methods.checkIfUrlValid(this.url); // TEST ONLY. Replace to "this"

    if (urlCheck.warnings) warnings.push(...urlCheck.warnings);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit fbf3961
created_at: 2025-04-24T17:10:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-21T18:41:34Z
body: @SokolovskyiK is attempting to deploy a commit to the **Pipedreamers** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Pipedreamers&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%226b1562121517adf4f453da0680cc0f944a70ae50%22%7D%2C%22id%22%3A%22QmYxDV5uxUvrT8C8HzqBqxHN9rzkqkRAsnjRZBg2sc7P4k%22%2C%22org%22%3A%22PipedreamHQ%22%2C%22prId%22%3A16378%2C%22repo%22%3A%22pipedream%22%7D).


author_association: NONE
user: login: vercel[bot]
created_at: 2025-04-21T18:41:35Z
body: [vc]: #eGf04v1vQmyQFfPZR9rBcKGv9FjGUbrqiFc1MnniI3Q=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwaXBlZHJlYW0tZG9jcy1yZWRpcmVjdC1kby1ub3QtZWRpdCIsInJvb3REaXJlY3RvcnkiOiJkb2NzIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcGlwZWRyZWFtZXJzL3BpcGVkcmVhbS1kb2NzLXJlZGlyZWN0LWRvLW5vdC1lZGl0LzVnOHlrZ2NXb2NUbUVHRjJuNjlaNlZzc0FGalQiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **pipedream-docs-redirect-do-not-edit** | ⬜️ Ignored ([Inspect](https://vercel.com/pipedreamers/pipedream-docs-redirect-do-not-edit/5g8ykgcWocTmEGF2n69Z6VssAFjT)) |  |  | Apr 24, 2025 5:19pm |
</details>


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-21T18:41:39Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces two new actions for the Google Search Console component: one to submit URLs for indexing via the Google Indexing API, and another to retrieve site performance analytics data. Each action includes new implementation modules, detailed documentation, and input validation logic. Utility and validation methods are added to a shared module, and local test scripts with supporting data and mocks are provided for development and validation. The component's main app file is updated to incorporate the new methods, and the package version is incremented to 0.7.0. Documentation is expanded to describe the new actions and testing setup.

## Changes

| File(s)                                                                                      | Change Summary                                                                                                                                                                                                                  |
|---------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `components/google_search_console/README.md`, `actions/retrieve-site-performance-data/README.md`, `actions/submit-url-for-indexing/README.md` | Updated/added documentation for the component and both new actions, describing usage, authentication, endpoints, and examples.                                                          |
| `actions/retrieve-site-performance-data/retrieve-site-performance-data.mjs`, `actions/submit-url-for-indexing/submit-url-for-indexing.mjs`  | Added new action modules for retrieving site performance data and submitting URLs for indexing, with input validation, error handling, and API calls.                                    |
| `common/methods.mjs`, `common/utils.mjs`                                                     | Introduced shared validation, error handling, and utility functions for input processing and object manipulation.                                                                        |
| `google_search_console.app.mjs`                                                              | Extended the app's methods with the new validation and utility functions.                                                                                                               |
| `package.json`                                                                               | Updated the package version from 0.6.0 to 0.7.0.                                                                                                                                        |
| `common/tests/README.md`, `common/tests/action-tests/test-retrieve-site-performance-data.mjs`, `common/tests/action-tests/test-submit-url-for-indexing.mjs`, `common/tests/get-token.mjs`, `common/tests/mockery-dollar.mjs` | Added local test documentation, test runners for both actions, an OAuth token utility, and a mock `$` object for local testing.                                                         |
| `common/tests/methods/bogus-data/bogus-data-google-date.mjs`, `common/tests/methods/bogus-data/bogus-data-url.mjs` | Provided bogus data sets for edge-case and type validation testing.                                                                                                                     |
| `common/tests/methods/test-checkIfUrlValid.mjs`, `common/tests/methods/test-throwIfNotYMDDashDate.mjs` | Added unit test scripts for URL and date validation methods.                                                                                                                            |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Action
    participant GoogleAPI

    User->>Action: Provide input (siteUrl, etc.)
    Action->>Action: Validate and trim input
    Action->>GoogleAPI: Make authenticated API request
    GoogleAPI-->>Action: Return API response
    Action-->>User: Return summary and raw response
```

## Assessment against linked issues

| Objective                                                                                             | Addressed | Explanation                                                                                      |
|-------------------------------------------------------------------------------------------------------|-----------|--------------------------------------------------------------------------------------------------|
| Implement "submit-url-for-indexing" action per requirements (#15016)                                  | ✅        |                                                                                                  |
| Implement "retrieve-site-performance-data" action with configurable filters (#15016)                  | ✅        |                                                                                                  |
| Input validation, error handling, and documentation for both actions (#15016)                         | ✅        |                                                                                                  |

## Suggested labels

`ai-assisted`

## Suggested reviewers

- lcaresia

## Poem

> A bunny hopped in code’s domain,  
> With Google tools to entertain—  
> URLs indexed, data fetched,  
> Each input trimmed and warnings checked.  
> Now rabbits dance, the tests all pass,  
> Version bumped, we’re shipping fast!  
> 🐇✨

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 8.57.1
> 
> Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'jsonc-eslint-parser' imported from /eslint.config.mjs
>     at packageResolve (node:internal/modules/esm/resolve:839:9)
>     at moduleResolve (node:internal/modules/esm/resolve:908:18)
>     at defaultResolve (node:internal/modules/esm/resolve:1038:11)
>     at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:557:12)
>     at ModuleLoader.resolve (node:internal/modules/esm/loader:525:25)
>     at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:246:38)
>     at ModuleJob._link (node:internal/modules/esm/module_job:126:49)
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 127e8dba8675a73788b66bf6ac2d6edaa5539af8 and fbf39616ae39ffdaa5d6ffcb337206048f3df41f.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* components/google_search_console/common/tests/action-tests/test-submit-url-for-indexing.mjs

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: pnpm publish
* GitHub Check: Verify TypeScript components
* GitHub Check: Publish TypeScript components

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=PipedreamHQ/pipedream&utm_content=16378):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: pipedream-component-development
created_at: 2025-04-21T18:41:47Z
body: Thank you so much for submitting this! We've added it to our backlog to review, and our team has been notified.
author_association: COLLABORATOR
user: login: pipedream-component-development
created_at: 2025-04-21T18:41:48Z
body: Thanks for submitting this PR! When we review PRs, we follow the [Pipedream component guidelines](https://pipedream.com/docs/components/guidelines/). If you're not familiar, here's a quick checklist:
- [ ] [Create components to address specific use cases whenever possible](https://pipedream.com/docs/components/guidelines/#component-scope)
- [ ] [Component `key`s should follow the format `app_name_slug-slugified-component-name`](https://pipedream.com/docs/components/guidelines/#component-key-pattern)
- [ ] Components should follow the [standard directory structure](https://pipedream.com/docs/components/guidelines/#folder-structure)
- [ ] [Prefer Node.js client libraries to REST APIs](https://pipedream.com/docs/components/guidelines/#using-apis-vs-client-libraries)
- [ ] When making API requests, [handle pagination](https://pipedream.com/docs/components/guidelines/#pagination) to ensure all data / events are processed
- [ ] [Use `secret` props](https://pipedream.com/docs/components/guidelines/#capturing-sensitive-data) to capture sensitive data
- [ ] Props and methods should be defined in [app files](https://pipedream.com/docs/components/guidelines/#app-files) whenever possible
- [ ] Document methods with [JS Docs](https://pipedream.com/docs/components/guidelines/#methods)
- [ ] [Use `optional` props](https://pipedream.com/docs/components/guidelines/#optional-vs-required-props) whenever possible, and [set a `default` value](https://pipedream.com/docs/components/guidelines/#default-values) where you can
- [ ] Use [async options](https://pipedream.com/docs/components/guidelines/#async-options) to accept user input wherever possible
author_association: COLLABORATOR
user: login: SokolovskyiK
created_at: 2025-04-24T17:35:31Z
body: @michelle0927 Cool. Thanks =) 
author_association: CONTRIBUTOR