Skip to content

Add Google Search Console plugin#63

Open
Daniel-Hodgson-SquaredUp wants to merge 3 commits into
mainfrom
add-gsc-plugin
Open

Add Google Search Console plugin#63
Daniel-Hodgson-SquaredUp wants to merge 3 commits into
mainfrom
add-gsc-plugin

Conversation

@Daniel-Hodgson-SquaredUp

Copy link
Copy Markdown

What does this change do?

Adds a new Google Search Console plugin that enables users to connect a GSC property via OAuth 2.0 and visualize search performance data directly within SquaredUp.

The plugin includes the following data streams. along with default dashboards for both site-level and page-level SEO analysis:

  • Country breakdown
  • Device breakdown
  • Page distribution
  • Page performance over time
  • Page performance summary
  • Page URLs
  • Pages
  • Queries
  • Queries by page
  • Site performance over time
  • Site performance summary

Why is it useful?

Google Search Console contains valuable SEO data and is one of the most widely used tools in the industry. It is also currently a gap in the SquaredUp plugin catalog.

This plugin allows users to:

  • Monitor clicks, impressions, CTR, and average position over time
  • Identify top-performing and under-performing pages
  • Analyse search queries driving traffic to specific pages
  • Break down performance by country and device
  • Scope dashboards to indexed GSC page objects
  • Compare current performance against previous periods

The included dashboards provide an out-of-the-box SEO reporting experience with minimal configuration required.

Testing performed

Validation

  • Plugin successfully validated when deployed via the CLI
  • Confirmed all plugin components were bundled correctly

Manual testing

Tested against a live Google Search Console property. A Google Pages test site was created and populated with content, with impressions and clicks generated over several weeks to produce representative Search Console data:

https://danielchodgson.github.io/gsc-lcp-test-site/index.html

Verified:

  • Authentication and configuration flow
  • All data streams return expected data
  • Page indexing returns all discovered pages
  • Dashboard variable integration using indexed GSC page objects
  • Default Site Overview dashboard loads successfully
  • Default Page Overview dashboard loads successfully
  • Timeframe handling functions correctly
  • Page scoping functions correctly

Screenshots

image image image image

Known limitations / follow-ups

  • Search Console API data is subject to Google's normal reporting delays and sampling behavior
  • Testing has only been performed against a relatively small dataset; additional validation against larger Search Console properties would be beneficial

@Daniel-Hodgson-SquaredUp Daniel-Hodgson-SquaredUp requested a review from a team June 15, 2026 12:53
@clarkd

clarkd commented Jun 17, 2026

Copy link
Copy Markdown
Member

@claude review

@clarkd clarkd added the new-plugin Used to PR newly added plugins label Jun 17, 2026
Country: row.keys?.[0],
Clicks: row.clicks,
Impressions: row.impressions,
CTR: Number((row.ctr * 100).toFixed(2)),

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.

Could you clarify the intent here? You're rounding/stringifying and then converting back to a number which might lose some precision.

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.

GSC returns ctr as a decimal number between 0 and 1, intent was to show CTR as a percentage and to round it - but I see what you're saying! I can do it as CTR: Math.round(row.ctr * 10000) / 100 if that's cleaner

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.

I would lave all the formatting to SquaredUp - if you just return this as, you can then set the metadata to:

  • shape percentage
  • range between 0 and 1: true
  • decimal places: 2

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.

Sounds good, will do! :)

Date: new Date(row.keys?.[0]),
Clicks: row.clicks,
Impressions: row.impressions,
CTR: Number((row.ctr * 100).toFixed(2)),

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.

As above

Page: row.keys?.[0],
Clicks: row.clicks,
Impressions: row.impressions,
CTR: Number((row.ctr * 100).toFixed(2)),

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.

As above

{
"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?

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

"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)

@@ -0,0 +1,59 @@
{
"name": "previousPeriodQueriesByPage",

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.

Given we have data/time selection in SquaredUp - do we need these 'previous period' streams, or can the user effectively create them by setting the date appropriately on two tiles?

@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.

For sure they could have tiles with fixed timeframes, but the intent was to enable fully timeframe-responsive comparison dashboards, it felt a bit of a letdown having the entire dashboards refresh and then have two tiles need re-configuring

E.g. if a user changes the dashboard from Last 30 days to Last quarter, the Organic Search Performance tiles will automatically update, but as fixed comparison tiles they would remain fixed until reconfigured

It's definitely not a necessity, but it makes the pre-built dashboards feel very smooth!

(Side note, I should probably name the tiles Period performance comparison or something to make it clearer)

Let me know what you think! Happy to go with fixed if needed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a cool feature request tbf, being able to offset a tile from the dashboard timeframe.

Smart way to achieve it

"rowLimit": 25000,
"dataState": "final"
},
"postRequestScript": "postRequest/script2.js",

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.

Worth renaming this script name (and script1) to be more identifiable

@clarkd

clarkd commented Jun 17, 2026

Copy link
Copy Markdown
Member

@claude review

Comment thread plugins/GoogleSearchConsole/v1/defaultContent/pageOverview.dash.json Outdated
Comment thread plugins/GoogleSearchConsole/v1/defaultContent/siteOverview.dash.json Outdated
Comment on lines +5 to +8
"author": {
"name": "Daniel.Hodgson@squaredup.com",
"type": "community"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 metadata.json doesn't follow the repo's metadata conventions: author.name is set to a raw email (Daniel.Hodgson@squaredup.com) rather than a GitHub handle (e.g. @Daniel-Hodgson-SquaredUp) or an organisation name, and the file is missing the links array (the repo guidelines expect two links — one with category: source pointing at the GitHub repo and one with category: documentation pointing at the markdown docs). Compare with plugins/GoogleSheets/v1/metadata.json for the expected shape — without these, in-product source/docs links won't render and a personal email gets published in the plugin catalogue.

Extended reasoning...

The repo review guidelines (which apply to every plugin metadata.json) call out two things this file does not do:

author.name - Should typically be a GitHub username, prefixed with @ OR an organisation name. For example @username1 or Contoso Inc.

links - Should typically contain two links, one link with category: source linking to the GitHub repository, and another link with category: documentation linking to the markdown documentation in the repository.

(1) author.name is an email, not a GitHub handle. plugins/GoogleSearchConsole/v1/metadata.json line 6 has "name": "Daniel.Hodgson@squaredup.com". Every other community-authored plugin in this repo follows the @handle convention (e.g. @clarkd, @kieranlangton, @TimWheeler-SQUP, @kmichalski0, @blackgrouse). This plugin is the only outlier and the only one shipping a personal email address publicly in the plugin catalogue — minor privacy concern beyond the convention violation. The PR comment author shows the GitHub handle is Daniel-Hodgson-SquaredUp, so @Daniel-Hodgson-SquaredUp would conform.

(2) The links array is missing. The file's keys end at base.config.oauth2Scope — there is no links field at all. Nearly every other plugin in the repo (GoogleSheets, Phare, NinjaOne, Snowflake, UptimeRobot, UniFi, etc.) ships a links array with both a source and a documentation entry. A README does exist at plugins/GoogleSearchConsole/v1/docs/README.md, so the in-product surface that consumes metadata.links.documentation will have nothing to render, and the source-repo link will likewise be absent — a material UX regression vs the rest of the catalogue.

Step-by-step proof using a peer plugin as reference:

  1. Open plugins/GoogleSheets/v1/metadata.json (or any peer). You'll see author.name like "@kieranlangton" and a links array like:
    "links": [
      { "category": "source", "url": "https://github.com/squaredup/plugins/tree/main/plugins/GoogleSheets/v1" },
      { "category": "documentation", "url": "https://github.com/squaredup/plugins/blob/main/plugins/GoogleSheets/v1/docs/README.md" }
    ]
  2. Open the file in this PR: author.name is an email, no links key.
  3. When the product renders the plugin catalogue it pulls these fields straight from metadata.json — so the GSC plugin will show a raw email as the author and have no source/docs links available next to it.

How to fix:

"author": {
    "name": "@Daniel-Hodgson-SquaredUp",
    "type": "community"
},

and add a top-level links array (placement matches other plugins):

"links": [
    {
        "category": "source",
        "url": "https://github.com/squaredup/plugins/tree/main/plugins/GoogleSearchConsole/v1"
    },
    {
        "category": "documentation",
        "url": "https://github.com/squaredup/plugins/blob/main/plugins/GoogleSearchConsole/v1/docs/README.md"
    }
]

Severity is nit — purely metadata/convention, no functional impact — but it is explicitly called out by the repo review guidelines and trivially fixable before merge.

Dan H added 2 commits June 19, 2026 14:40
…he "Biggest Ranking Changes" tile of the "Page Overview" dashboard
…he "Organic Search Performance " tile of the "Site Overview" dashboard
@github-actions

Copy link
Copy Markdown

🧩 Plugin PR Summary

📦 Modified Plugins

  • plugins/GoogleSearchConsole/v1

📋 Results

Step Status
Validation ✅ Passed
Deployment 🚀 Deployed

🔍 Validation Details

google-search-console
{
  "valid": true,
  "pluginName": "google-search-console",
  "pluginType": "hybrid",
  "summary": {
    "Data Streams": 14,
    "Import Definitions": 1,
    "UI Configuration": true,
    "Has Icon": true,
    "Has Default Content": true,
    "Config Validation": false,
    "Custom Types": false
  }
}

Comment on lines +158 to +170
"Clicks Change %",
"Impressions Change %",
"CTR Change %",
"Position Change"
],
"resizedColumns": {
"columnWidths": {
"Position Change": 135,
"Impressions Change %": 177,
"CTR Change %": 122,
"Clicks Change %": 135
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Five tiles across the default pageOverview.dash.json (4 SQL tiles) and siteOverview.dash.json (1 SQL tile) ship a tile-level scope block whose gremlin query and queryDetail.ids are hardcoded to node-1dwkxV4HX7tME4WtZkj2r04TirpbysXMf3ej9-a1txyPzIR6b4Z1ctJ0Ks — a node id from your test tenant that won't exist in any customer's tenant. This is leftover dashboard-editor state from the dev environment, and it overrides the (correctly variable-scoped) inner dataStream.dataSourceConfig.scope, so customers will get empty or mis-scoped tiles out of the box. Fix: delete the outer scope block on each affected tile (peer plugins like NinjaOne, UniFi, and Huntress ship object-scoped dashboards with no tile-level scope and rely entirely on the inner variable resolution).

Extended reasoning...

What the bug is

Searching plugins/GoogleSearchConsole/v1/defaultContent/ for node- finds 5 tiles with the exact same hardcoded id node-1dwkxV4HX7tME4WtZkj2r04TirpbysXMf3ej9-a1txyPzIR6b4Z1ctJ0Ks:

  • pageOverview.dash.jsonOrganic search performance tile (line ~158)
  • pageOverview.dash.jsonBiggest Ranking Changes tile (line ~270)
  • pageOverview.dash.jsonRanking distribution tile (line ~366)
  • pageOverview.dash.jsonQuery opportunities tile (line ~462)
  • siteOverview.dash.jsonOrganic Search Performance tile (line ~178)

Each occurrence has the shape:

"scope": {
  "query": "g.V().has('id', within(ids_<random>))",
  "bindings": {
    "ids_<random>": ["node-1dwkxV4HX7tME4WtZkj2r04TirpbysXMf3ej9-a1txyPzIR6b4Z1ctJ0Ks"]
  },
  "queryDetail": {
    "ids": ["node-1dwkxV4HX7tME4WtZkj2r04TirpbysXMf3ej9-a1txyPzIR6b4Z1ctJ0Ks"]
  }
}

That node-... id is a SquaredUp-generated tenant node id from the dev environment that was used to author the dashboards. It is not a portable reference and will not resolve in any other tenant.

Why it ships broken

The id is bound by literal value, not by template ({{...}}) or by variable. When the dashboard is installed in a customer tenant the gremlin query g.V().has('id', within('node-1dwkxV4HX7tME4WtZkj2r04TirpbysXMf3ej9-...')) matches zero vertices because that exact id doesn't exist there. The inner dataStream.dataSourceConfig.scope.variable on each tile is set correctly (it references {{variables.[Google Search Console Page]}} — except the siteOverview tile, which doesn't use a page variable at all), but the tile-level scope block takes precedence as an explicit override. So either tiles render empty, or — worse — they appear to load but with the wrong scope.

Step-by-step proof

  1. A customer installs the GSC plugin and opens the default Page Overview dashboard with a real GSC page selected for [Google Search Console Page].
  2. The Organic search performance tile is asked to resolve its scope. It looks at config.scope and runs g.V().has('id', within('node-1dwkxV4HX7tME4WtZkj2r04TirpbysXMf3ej9-a1txyPzIR6b4Z1ctJ0Ks')).
  3. No vertex with that id exists in the customer's graph (it's the dev's tenant node id), so the gremlin query returns [].
  4. With an empty scope, the SQL/data-stream query is run against zero objects, and the tile renders empty — or with a 'No data' state — on a freshly-installed default dashboard.
  5. Repeat for the other 4 tiles. Five out of the two dashboards' headline tiles will be broken on first open.

Cross-check against peer plugins

I ran grep -l '"node-"' plugins/*/v*/defaultContent/*.dash.json and only MicrosoftDefender (already under review) and this PR ship raw node-* ids. Other object-scoped default dashboards in the repo — NinjaOne's deviceDetail.dash.json, UniFi's networkDevice.dash.json, Huntress's agentsByOrganization.dash.json — ship no tile-level scope block at all. They rely solely on the inner dataStream.dataSourceConfig.scope.variable which the dashboard variable resolves at render time per-tenant. DattoRMM's dashboards use template-parameterised gremlin queries ({{configId}}) when they need a tile-level scope.

The canonical pattern when an outer scope IS needed is variable-based, not raw-id, e.g. { variable: {{variables...}}, scope: {{scopes...}}, workspace: {{workspaceId}} }. None of the GSC tiles need an outer scope at all — the inner scope.variable is already there.

Impact

Every customer who installs this plugin will see broken / empty tiles on the two default dashboards (Page Overview and Site Overview). These are the out-of-the-box experience the plugin is sold on (see the PR description: 'pre-built dashboards feel very smooth'), so the first impression is the worst-case one. Beyond correctness, this also leaks the developer's test-tenant node id into the public plugin catalogue.

How to fix

Delete the outer scope block on each of the 5 tiles. Concretely, in pageOverview.dash.json remove the block at lines ~158-170, ~270-282, ~366-378, ~462-474, and in siteOverview.dash.json remove the block at lines ~178-190. The inner dataStream.dataSourceConfig.scope is already correctly variable-based on the page-scoped tiles and the site-summary tile doesn't need any scope override at all. No other changes are needed.

Comment on lines +15 to +22
"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

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

Comment on lines +29 to +30
"timeframes": true,
"tags": []

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).

{
"name": "pageUrLs",
"displayName": "Page URLs",
"description": "Returns a list off URLs with impressions in the last year",

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-plugin Used to PR newly added plugins

Development

Successfully merging this pull request may close these issues.

3 participants