Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions plugins/GoogleSearchConsole/v1/dataStreams/countryBreakdown.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "countryBreakdown",
"displayName": "Country breakdown",
"description": "Returns search performance metrics grouped by country",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"postBody": {
"startDate": "{{new Date(timeframe.start).toISOString().split('T')[0]}}",
"endDate": "{{new Date(timeframe.end).toISOString().split('T')[0]}}",
"dimensions": [
"country"
],
"rowLimit": 25000,
"dataState": "final"
},
"postRequestScript": "postRequest/countryBreakdown.js",
"getArgs": [],
"headers": []
},
"providesPluginDiagnostics": true,
"timeframes": true,
"tags": []
Comment on lines +29 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 All 14 new data stream JSON files in this PR ship with "tags": [] (e.g. countryBreakdown.json:30, pages.json:29, queries.json, sitePerformanceOverTime.json, etc.). The repo review guidelines explicitly state tags are mandatory and should reuse an existing category from other plugins where possible — peer plugins like GoogleSheets, Huntress, and AutoTask all populate them. Consider something like ["SEO", "Analytics"] across all 14 streams to aid discoverability.

Extended reasoning...

What the bug is

The repo review guidelines for plugin authors (see the REVIEW.md / contribution guidance applied across this repo) are explicit:

tagsMandatory. Reuse an existing category from other plugins where possible.

Every one of the 14 new data stream JSON files added in this PR ships with an empty tags array:

  • plugins/GoogleSearchConsole/v1/dataStreams/countryBreakdown.json:30"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/deviceBreakdown.json:30"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/pageDistribution.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/pagePerformanceOverTime.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/pagePerformanceSummary.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/pageUrLs.json:29"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/pages.json:29"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/previousPeriodPagePerformance.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/previousPeriodPerformanceOverTime.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/previousPeriodQueriesByPage.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/queries.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/queriesByPage.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/sitePerformanceOverTime.json"tags": []
  • plugins/GoogleSearchConsole/v1/dataStreams/sitePerformanceSummary.json"tags": []

Comparison to peer plugins

A quick comparison against other plugins in the repo confirms the convention is widely followed:

  • plugins/Huntress/v1/dataStreams/*.json populates tags such as ["Security", "Incidents"].
  • plugins/AutoTask/v1/dataStreams/*.json uses tags like ["Tickets"], ["Resources"].
  • plugins/Checkly, plugins/GoogleSheets, plugins/NinjaOne, and others all populate tags with descriptive categories such as ["Monitoring"], ["Security"], ["Backup"], ["Ticketing"].

The GSC plugin is essentially the only PR shipping uniformly empty tag arrays across every data stream.

Step-by-step proof of impact

  1. A user installs the GSC plugin and opens the data stream picker in SquaredUp.
  2. They filter or browse by tag (e.g. searching for an analytics or SEO-related stream).
  3. Because every GSC data stream has "tags": [], none of them show up under any category — they are only discoverable by exact name.
  4. Contrast with a peer plugin like Huntress: filtering by Security surfaces all of its data streams immediately.

Why existing code does not prevent it

The schema does not reject empty arrays — MicrosoftDefender (added in commit efb12f9) also ships with empty tags, so prior precedent shows the validator does not enforce this. The guideline is documented in the review process rather than the schema, which is why it slipped through.

Impact

No functional impact — data streams still execute correctly and dashboards still render. The cost is purely discoverability: users browsing the plugin catalogue by category will not find these streams under any SEO/Analytics tag, and the plugin will look inconsistent next to peers that follow the convention.

How to fix

A one-line edit in each of the 14 files. Suggested values (reusing categories used elsewhere in the repo):

"tags": ["SEO", "Analytics"]

or, if SEO is too narrow as a single tag, ["Analytics"] alone (the plugin already uses "category": "Analytics" in metadata.json, so reusing that here is consistent with the existing self-categorisation).

}
31 changes: 31 additions & 0 deletions plugins/GoogleSearchConsole/v1/dataStreams/deviceBreakdown.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "deviceBreakdown",
"displayName": "Device breakdown",
"description": "Returns search performance metrics grouped by device type",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"postBody": {
"startDate": "{{new Date(timeframe.start).toISOString().split('T')[0]}}",
"endDate": "{{new Date(timeframe.end).toISOString().split('T')[0]}}",
"dimensions": [
"device"
],
"rowLimit": 25000,
"dataState": "final"
},
"postRequestScript": "postRequest/deviceBreakdown.js",
"getArgs": [],
"headers": []
},
"providesPluginDiagnostics": true,
"timeframes": true,
"tags": []
}
59 changes: 59 additions & 0 deletions plugins/GoogleSearchConsole/v1/dataStreams/pageDistribution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "pageDistribution",
"displayName": "Page distribution",
"description": "Returns a distribution of queries by search ranking position for the selected page",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"getArgs": [],
"headers": [],
"postBody": {
"startDate": "{{new Date(timeframe.start).toISOString().split('T')[0]}}",
"endDate": "{{new Date(timeframe.end).toISOString().split('T')[0]}}",
"dimensions": [
"query"
],
"dimensionFilterGroups": [
{
"filters": [
{
"dimension": "page",
"operator": "equals",
"expression": "{{scope?.[0]?.name}}"
}
]
}
],
"rowLimit": 25000
},
"postRequestScript": "postRequest/pageDistribution.js"
},
"providesPluginDiagnostics": true,
"timeframes": true,
"tags": [],

"ui": [
{
"name": "scope",
"objectLimit": 1,
"label": "Scope",
"type": "objects",
"matches": {
"sourceType": {
"type": "equals",
"value": "gsc-page"
}
},
"validation": {
"required": true
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "pagePerformanceOverTime",
"displayName": "Page performance over time",
"description": "Returns daily clicks, impressions and CTR trends for the selected page",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"postBody": {
"startDate": "{{new Date(timeframe.start).toISOString().split('T')[0]}}",
"endDate": "{{new Date(timeframe.end).toISOString().split('T')[0]}}",
"dimensions": [
"date"
],
"dimensionFilterGroups": [
{
"filters": [
{
"dimension": "page",
"operator": "equals",
"expression": "{{scope?.[0]?.name}}"
}
]
}
],
"rowLimit": 25000
},
"postRequestScript": "postRequest/pagePerformanceOverTime.js",
"getArgs": [],
"headers": []
},
"providesPluginDiagnostics": true,
"timeframes": true,
"tags": [],

"ui": [
{
"name": "scope",
"objectLimit": 1,
"label": "Scope",
"type": "objects",
"matches": {
"sourceType": {
"type": "equals",
"value": "gsc-page"
}
},
"validation": {
"required": true
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "pagePerformanceSummary",
"displayName": "Page performance summary",
"description": "Returns performance metrics for a given page",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"getArgs": [],
"headers": [],
"postBody": {
"startDate": "{{new Date(timeframe.start).toISOString().split('T')[0]}}",
"endDate": "{{new Date(timeframe.end).toISOString().split('T')[0]}}",
"dimensions": [
"page"
],
"dimensionFilterGroups": [
{
"filters": [
{
"dimension": "page",
"operator": "equals",
"expression": "{{scope?.[0]?.name}}"
}
]
}
],
"rowLimit": 25000
},
"postRequestScript": "postRequest/script1.js"
},
"providesPluginDiagnostics": true,
"timeframes": true,
"tags": [],

"ui": [
{
"name": "scope",
"objectLimit": 1,
"label": "Scope",
"type": "objects",
"matches": {
"sourceType": {
"type": "equals",
"value": "gsc-page"
}
},
"validation": {
"required": true
}
}
]
}
30 changes: 30 additions & 0 deletions plugins/GoogleSearchConsole/v1/dataStreams/pageUrLs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "pageUrLs",
"displayName": "Page URLs",
"description": "Returns a list off URLs with impressions in the last year",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look quite right - the timeframe is 'now -> whatever end date selected in SquaredUp'

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stream is used for object discovery / indexing where I wanted to pull pages seen in the last year regardless of the dashboard timeframe (the API always requires a startDate and endDate)

You reckon I need to update it somehow or is it okay for that?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Typo in the user-facing description: line 4 reads "description": "Returns a list off URLs with impressions in the last year" — "off" should be "of". This text appears in the data stream picker, so users will see it.

Extended reasoning...

What the bug is

plugins/GoogleSearchConsole/v1/dataStreams/pageUrLs.json line 4:

"description": "Returns a list off URLs with impressions in the last year",

"a list off URLs" should be "a list of URLs". A small but plainly wrong typo.

Why it matters

The description field on a data stream is user-facing — it is surfaced in the SquaredUp data stream picker when users browse and select streams when building tiles. Every customer who installs this plugin and goes to add a tile will see this typo next to the "Page URLs" stream name.

How it manifests

  1. User installs Google Search Console plugin from the catalogue.
  2. User adds a tile and opens the data stream picker.
  3. Picker lists "Page URLs" with the subtitle "Returns a list off URLs with impressions in the last year".

There's no code path that filters or sanitises this string — it is rendered as-is from the JSON.

Why existing code doesn't prevent it

Plugin validation (google-search-console plugin validation passed per the PR-summary bot output) checks structural validity of description (must be a string), not spelling. No automated linter is going to catch a real-word typo where both spellings are valid English words.

How to fix

One-character edit:

-    "description": "Returns a list off URLs with impressions in the last year",
+    "description": "Returns a list of URLs with impressions in the last year",

Severity

Pure cosmetic typo, no functional impact — nit. But it ships visibly to every user of the plugin and is trivial to fix before merge.

"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"postBody": {
"startDate": "{{new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]}}",
"endDate": "{{new Date().toISOString().split('T')[0]}}",
"dimensions": [
"page"
],
"rowLimit": 25

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do this or other streams need paging configured?

@Daniel-Hodgson-SquaredUp Daniel-Hodgson-SquaredUp Jun 17, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't need explicit paging but I definitively need to check the rowLimit configs, for one this stream should be way higher than 25!

},
Comment on lines +15 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The pageUrLs data stream sets rowLimit: 25 (line 21), but this stream is the sole source for object indexing in indexDefinitions/default.json (populating the gsc-page source type used by the Page Overview dashboard, the "Google Search Console Pages" scope variable, and every {{scope?.[0]?.name}} filter). With a 25-row cap, any site with more than 25 indexed pages will silently miss the rest from object discovery and page selection. You already flagged this in the PR thread ("this stream should be way higher than 25!") — raising it to 25000 to match the other streams would fix it.

Extended reasoning...

What the bug is

plugins/GoogleSearchConsole/v1/dataStreams/pageUrLs.json sets rowLimit: 25 on line 21. Every other data stream in this plugin (countryBreakdown, deviceBreakdown, pageDistribution, pagePerformanceOverTime, pagePerformanceSummary, pages, previousPeriodPagePerformance, previousPeriodPerformanceOverTime, previousPeriodQueriesByPage, queries, queriesByPage, sitePerformanceOverTime) uses rowLimit: 25000. The 25 looks like a debug value left in by mistake.

Why this matters more than for any other stream

indexDefinitions/default.json uses only pageUrLs as the source for object indexing. Its single step imports rows from pageUrLs and maps each row's value to an object id with type: "gsc-page". That gsc-page source type is then referenced by:

  • defaultContent/scopes.json — the "Google Search Console Pages" scope and its associated "Google Search Console Page" variable, which is the only way users pick a page on the page-level dashboard.
  • defaultContent/pageOverview.dash.json — every tile binds via scope.[Google Search Console Pages] and the {{variables.[Google Search Console Page]}} substitution.
  • The scope?.[0]?.name / scope?.[0]?.rawId?.[0] filter expressions in pageDistribution.json, pagePerformanceOverTime.json, pagePerformanceSummary.json, previousPeriodPagePerformance.json, previousPeriodQueriesByPage.json, and queriesByPage.json — these all rely on a page object existing in the index in order to be selectable.

So pageUrLs is the bottleneck for the entire page-level surface of the plugin.

Step-by-step proof for a realistic site (say 200 indexed pages)

  1. The indexer issues the pageUrLs request with startDate = one year ago, endDate = today, dimensions ["page"], and rowLimit: 25.
  2. GSC returns the top 25 pages by clicks (its default ordering) for the requested period.
  3. The post-request script (pageUrLs.js) maps those 25 rows to {label, value} pairs.
  4. The index step in indexDefinitions/default.json creates exactly 25 gsc-page objects.
  5. The "Google Search Console Page" scope variable is populated from those 25 objects only — the other 175 pages on the site simply don't exist as selectable objects.
  6. A user trying to view, say, their TEST PR #50 most-clicked page in the Page Overview dashboard cannot select it; it isn't in the dropdown. Even if they paste the URL into a tile manually, indexDefinitions/default.json will still treat it as an unknown object.

Why existing code doesn't prevent this

There's no auto-paging on this stream (paging.mode: "none"), no fallback path that fetches more rows, and no validation that the page set is complete. The stream simply returns the first 25 results and the index is built from exactly that.

Author has already confirmed

In inline comment 3428036489 the author wrote: "Doesn't need explicit paging but I definitively need to check the rowLimit configs, for one this stream should be way higher than 25!" — so this is already acknowledged as unintended, but the diff still shows 25 and no follow-up commit has changed it.

How to fix

Change line 21 of plugins/GoogleSearchConsole/v1/dataStreams/pageUrLs.json from "rowLimit": 25 to "rowLimit": 25000, matching every other stream in this plugin. (25000 is the GSC API maximum per request, so this is also the largest single-request fix.) If a site has more than 25 000 indexed pages, real paging would be needed — but the immediate user-visible bug is the 25-vs-25000 typo.

🔬 also observed by verify-runtime

"postRequestScript": "postRequest/pageUrLs.js",
"getArgs": [],
"headers": []
},
"timeframes": false,
"providesPluginDiagnostics": true,
"tags": []
}
30 changes: 30 additions & 0 deletions plugins/GoogleSearchConsole/v1/dataStreams/pages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "pages",
"displayName": "Pages",
"description": "Returns a complete list of pages with performance metrics",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"postBody": {
"startDate": "{{new Date(timeframe.start).toISOString().split('T')[0]}}",
"endDate": "{{new Date(timeframe.end).toISOString().split('T')[0]}}",
"dimensions": [
"page"
],
"rowLimit": 25000
},
"postRequestScript": "postRequest/pages.js",
"getArgs": [],
"headers": []
},
"providesPluginDiagnostics": true,
"timeframes": true,
"tags": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"name": "previousPeriodPagePerformance",
"displayName": "Previous period page performance",
"description": "Returns a summary of performance for the previous period of a given timeframe for the selected page",
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "post",
"errorHandling": {
"type": "default"
},
"paging": {
"mode": "none"
},
"expandInnerObjects": true,
"getArgs": [],
"headers": [],
"postBody": {
"startDate": "{{new Date(new Date(timeframe.start).getTime() - (new Date(timeframe.end).getTime() - new Date(timeframe.start).getTime()) - 86400000).toISOString().split('T')[0]}}",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't quite get my head around what happens if selected yearly ranges or something here - do we need to restrict the available timeframes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we need to restrict timeframes, as the calculation should work within 1 day error - I guess the main consideration is performance but I've not been able to test with a large dataset (and GSC data has a max query timeframe of 16 months)

"endDate": "{{new Date(new Date(timeframe.start).getTime() - 86400000).toISOString().split('T')[0]}}",
"dimensions": ["page"],
"dimensionFilterGroups": [
{
"filters": [
{
"dimension": "page",
"operator": "equals",
"expression": "{{scope?.[0]?.name}}"
}
]
}
],
"rowLimit": 25000
},
"postRequestScript": "postRequest/script1.js"
},
"timeframes": true,
"providesPluginDiagnostics": true,
"tags": [],
"visibility": {
"type": "hidden"
},
"ui": [
{
"name": "scope",
"objectLimit": 1,
"label": "Scope",
"type": "objects",
"matches": {
"sourceType": {
"type": "equals",
"value": "gsc-page"
}
},
"validation": {
"required": true
}
}
]
}
Loading
Loading