Skip to content

Add Vercel Plugin#66

Open
andrewmumblebee wants to merge 5 commits into
mainfrom
work/ah/vercel
Open

Add Vercel Plugin#66
andrewmumblebee wants to merge 5 commits into
mainfrom
work/ah/vercel

Conversation

@andrewmumblebee

@andrewmumblebee andrewmumblebee commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🔌 Plugin overview

  • Plugin name: Vercel
  • Purpose / problem solved: Monitoring of Vercel platform analytics, i.e. deployments, cost, firewall events
  • Primary audience (e.g. platform teams, SREs, product teams): DevOps
  • Authentication method(s) (e.g. OAuth, Username/Password, API Key): API Key

🖼️ Plugin screenshots

Plugin configuration

image image

Default dashboards

image image image image

🧪 Testing


⚠️ Known limitations

  • Deployments are not indexed. They are available only as data streams, because deployment volume and churn make them unsuitable as long-lived graph objects.
  • No real-time analytics or metrics via REST. Vercel does not expose Web Analytics (pageviews/visitors), Speed Insights (Core Web Vitals), or real-time Observability metrics (edge requests, function invocations, bandwidth) through its public REST API. Operational usage is available only as daily billed quantities via the cost stream — not real-time, per-request telemetry.
  • Cost data is daily granularity and the billing endpoint returns very large datasets, so the cost stream is restricted to the Last 24 hours and Last 7 days timeframes, even then we may get time outs or lambda response size errors
  • Team members and cost require a Team and a token with adequate role/plan. On personal/Hobby accounts these streams may be empty.
    • One connection = one scope. Each configured plugin instance monitors either your personal account or a single team. To monitor multiple teams, add the plugin once per team.
  • Rate limits. Vercel enforces per-action rate limits (HTTP 429). Very large accounts may occasionally see throttling during imports.

📚 Checklist

  • Plugin, datastream and UI naming follow SquaredUp guidelines
  • Logo added
  • One or more dashboards added
  • README added including configuration guidance
  • No secrets or credentials included
  • I agree to the Code of Conduct

@andrewmumblebee andrewmumblebee marked this pull request as ready for review June 16, 2026 14:28
@andrewmumblebee andrewmumblebee requested a review from a team June 16, 2026 14:28
@andrewmumblebee

Copy link
Copy Markdown
Contributor Author

schemaVersion is breaking validation, as fix for it not live in prod yet.

Waiting on Vercel's APIs to be back up to share Cost dashboard, as currently failing due to platform issues on their end

@clarkd

clarkd commented Jun 17, 2026

Copy link
Copy Markdown
Member

@claude review

@clarkd

clarkd commented Jun 17, 2026

Copy link
Copy Markdown
Member

@claude review once

Comment thread plugins/Vercel/v1/custom_types.json
Comment thread plugins/Vercel/v1/metadata.json
Comment thread plugins/Vercel/v1/defaultContent/overview.dash.json Outdated
Comment thread plugins/Vercel/v1/defaultContent/deployments.dash.json Outdated
Comment on lines +1 to +26
[
{
"name": "Vercel Projects",
"matches": {
"sourceType": { "type": "oneOf", "values": ["Vercel Project"] }
},
"variable": {
"name": "Vercel Project",
"allowMultipleSelection": false,
"default": "none",
"type": "object"
}
},
{
"name": "Vercel Domains",
"matches": {
"sourceType": { "type": "oneOf", "values": ["Vercel Domain"] }
},
"variable": {
"name": "Vercel Domain",
"allowMultipleSelection": false,
"default": "none",
"type": "object"
}
}
]

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 previous review's prefix-removal fix was applied incompletely. custom_types.json and indexDefinitions/default.json now use unprefixed Project/Domain, but four files still carry the old Vercel Project/Vercel Domain strings — and the miss here in scopes.json (lines 5, 17) is a hard breakage: both scopes match zero imported objects, so the Vercel Project and Vercel Domain variables backing the perspective dashboards have no selectable values. Also remaining: metadata.json:23 objectTypes, dataStreams/projects.json:30-35 computed sourceType, and dataStreams/domains.json:21 computed sourceType — those are cosmetic but should be cleaned up in the same sweep. Fix: change every remaining prefixed reference to Project/Domain.

Extended reasoning...

What is broken

The previous review removed the Vercel prefix from sourceType identifiers, but the sweep was applied to only two files. Currently:

Updated (unprefixed):

  • custom_types.jsonsourceType: "Project" / "Domain" (lines 4, 11)
  • indexDefinitions/default.jsontype.value: "Project" / "Domain" (lines 11, 23)
  • dataStreams/projectInfo.json:12, dataStreams/domainConfig.json:11, dataStreams/firewallEvents.json:21, dataStreams/deployments.json:32 — all matches.sourceType correctly use the unprefixed names.

NOT updated (still Vercel Project/Vercel Domain):

  • defaultContent/scopes.json:5,17matches.sourceType.values: ["Vercel Project"] / ["Vercel Domain"]
  • metadata.json:23objectTypes: ["Vercel Project", "Vercel Domain"]
  • dataStreams/projects.json:30-35 — computed sourceType valueExpression: "Vercel Project"
  • dataStreams/domains.json:21 — computed sourceType valueExpression: "Vercel Domain"

Why scopes.json is the hard breakage

indexDefinitions/default.json assigns the graph object's sourceType from the literal type.value"Project" / "Domain" — not from any column in the data stream. So every imported Project carries sourceType="Project" and every Domain carries sourceType="Domain". A scope keyed on oneOf ["Vercel Project"] therefore matches zero objects.

Step-by-step proof — Project perspective dashboard rendering

  1. On install, indexDefinitions runs the projects data stream and imports each row as a graph object with literal type.value="Project" → graph contains {sourceType: "Project", name: ...} objects.
  2. defaultContent/scopes.json registers the Vercel Projects scope: matches.sourceType.values=["Vercel Project"] → looks for objects with sourceType="Vercel Project".
  3. No imported object has sourceType="Vercel Project" (they all have "Project") → scope is empty.
  4. The Vercel Project variable defined in that scope (variable.type="object", sourced from the scope) has no selectable values → variable picker is empty.
  5. project.dash.json declares variables: ["{{variables.[Vercel Project]}}"] and references {{scopes.[Vercel Projects]}} and {{variables.[Vercel Project]}} throughout (lines 5, 188-192, 217-221, 252-256, etc.). With no variable value, none of the per-project tiles can resolve their scope → perspective dashboard is non-functional out of the box.
  6. Same story for Domain perspective via scopes.json:17 / domain.dash.json.

Impact

Every user who installs the Vercel plugin will find that both perspective dashboards (Project and Domain) are unusable on first install — the variable picker is empty, so nothing can be selected. This is a visible regression on two of the three OOB dashboards, introduced by the partial application of the previous review's fix.

Fix

Change in defaultContent/scopes.json:

  • Line 5: "values": ["Vercel Project"]"values": ["Project"]
  • Line 17: "values": ["Vercel Domain"]"values": ["Domain"]

Also fix in the same sweep (cosmetic but inconsistent):

  • metadata.json:23"objectTypes": ["Vercel Project", "Vercel Domain"]["Project", "Domain"] (validation currently passes, but objectTypes is documented to list the identifiers declared in custom_types.json)
  • dataStreams/projects.json:33"valueExpression": "Vercel Project""Project" (or remove the computed column — the actual graph type comes from indexDefinitions's literal type.value, so this column is currently dead, but visible to anyone querying the data stream)
  • dataStreams/domains.json:21"valueExpression": "Vercel Domain""Domain" (same reasoning)

Local dashboard variable/scope display names like {{variables.[Vercel Project]}} and {{scopes.[Vercel Projects]}} are friendly labels and can stay — only the sourceType identifier needs to be unprefixed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks — the hard breakage in scopes.json and the metadata.json objectTypes are now fixed. Two prefixed references still remain from the original sweep list:

  • dataStreams/projects.json:33"valueExpression": "Vercel Project""Project"
  • dataStreams/domains.json:21"valueExpression": "Vercel Domain""Domain"

These are cosmetic only (as noted previously, the actual graph type comes from indexDefinitions's literal type.value, so the computed column is dead), but they're worth cleaning up in the same sweep for consistency — anyone querying the projects or domains data stream directly will still see a sourceType column with the prefixed values that don't match what's actually in the graph.

{
"name": "projects",
"displayName": "Projects",
"description": "Lists Vercel projects in the configured account or team. Backs the Vercel Project import and project inventory tiles.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Per REVIEW.md ('Data streams' → description: 'Typically one sentence only. No full stop at the end ... Never use two sentences.'), five data stream descriptions in this PR are two sentences, and three also end with a full stop: projects.json:4, domains.json:4 (both two sentences + trailing period), activity.json:4, deployments.json:4 (both two sentences, the second being 'Backs ... tiles'), and currentUser.json:4 (two sentences + trailing period). Fix by condensing each to a single sentence with no trailing period, putting the secondary clarification in brackets — e.g. "Vercel projects in the configured account or team (backs the project import and inventory tiles)".

Extended reasoning...

What the bug is

REVIEW.md is explicit under the Data streams section about the description field:

Typically one sentence only. No full stop at the end. Add relevant clarifications in brackets. Never use two sentences.

Five of the eleven data streams in this PR violate that rule. The other six (cost.json, domainConfig.json, firewallEvents.json, projectInfo.json, teamMembers.json, teams.json) follow it correctly — so the convention is clearly understood; these five just slipped through.

The specific violations

Verified against the current files:

  • plugins/Vercel/v1/dataStreams/projects.json:4"Lists Vercel projects in the configured account or team. Backs the Vercel Project import and project inventory tiles." — two sentences + trailing period.
  • plugins/Vercel/v1/dataStreams/domains.json:4"Lists Vercel custom domains in the configured account or team. Backs the Vercel Domain import and domain inventory tiles." — two sentences + trailing period.
  • plugins/Vercel/v1/dataStreams/activity.json:4"Vercel account or team activity feed, one row per audit-style event. Backs activity and audit log tiles" — two sentences (no trailing period, but the second sentence is the violation).
  • plugins/Vercel/v1/dataStreams/deployments.json:4"Vercel deployments across the account or a selected project, one row per deployment. Backs deployment health and history tiles" — two sentences.
  • plugins/Vercel/v1/dataStreams/currentUser.json:4"Returns the authenticated Vercel user. Used to validate the connection." — two sentences + trailing period (hidden stream, lower priority but still part of the plugin surface).

Why existing tooling doesn't catch it

The plugin validator (visible in the github-actions[bot] PR summary) checks schema/structural correctness but not stylistic prose rules from REVIEW.md. There is no lint step that scans data-stream descriptions for sentence count or trailing periods, so these slip through validation even though they're explicitly enumerated as a rule in the repo prompt.

Impact

Cosmetic / consistency — these descriptions surface in the SquaredUp UI when users browse data streams. The rule exists to keep that surface uniform across plugins. Nothing functionally breaks, hence nit severity, but REVIEW.md treats it as a rule rather than a suggestion (the language is 'Never use two sentences'), and the contrast with the six correct streams in the same PR makes it worth a quick fixup pass before merge.

How to fix

For each of the five, condense to a single sentence with no trailing period. Put any secondary clarification in brackets per the REVIEW.md guidance. Suggested rewrites:

  • projects.json: "Vercel projects in the configured account or team (backs the project import and inventory tiles)"
  • domains.json: "Vercel custom domains in the configured account or team (backs the domain import and inventory tiles)"
  • activity.json: "Vercel account or team activity feed, one row per audit-style event (backs activity and audit log tiles)"
  • deployments.json: "Vercel deployments across the account or a selected project, one row per deployment (backs deployment health and history tiles)"
  • currentUser.json: "Authenticated Vercel user (used to validate the connection)"

Step-by-step proof

  1. Open REVIEW.md and locate the Data streams section. The description field's guidance reads verbatim: 'Typically one sentence only. No full stop at the end. Add relevant clarifications in brackets. Never use two sentences.'
  2. Open plugins/Vercel/v1/dataStreams/projects.json at line 4. The string contains . mid-string (between 'team' and 'Backs') and a trailing . before the closing quote — that's two sentences plus a full stop, two rule violations in one description.
  3. Repeat for domains.json:4 (same pattern), activity.json:4 and deployments.json:4 (two sentences, no trailing period), and currentUser.json:4 (two sentences + trailing period).
  4. For contrast, open plugins/Vercel/v1/dataStreams/cost.json:4: "Vercel usage cost and consumption from the FOCUS billing endpoint, one row per daily charge" — one sentence, no trailing period. This (and the five other compliant streams) demonstrates the author knows the rule, so the five violations are unintentional slippage rather than an intentional style deviation.
  5. The fix is a five-line edit and changes nothing else.

"id": "datastream-sql",
"dataSourceConfig": {
"version": "2.0",
"sql": "WITH\r\n \"build_times\" AS (\r\n SELECT\r\n STRFTIME(\r\n DATE_TRUNC('day', \"createdAt\"),\r\n '%Y-%m-%dT%H:%M:%SZ'\r\n ) AS \"date\",\r\n \"name\",\r\n EXTRACT (\r\n EPOCH\r\n FROM\r\n \"buildingAt\" - \"createdAt\"\r\n ) AS \"build_time\"\r\n FROM\r\n \"dataset1\"\r\n )\r\nSELECT\r\n \"date\",\r\n \"name\",\r\n AVG(\"build_time\") AS \"avg_build_time\"\r\nFROM\r\n \"build_times\"\r\nGROUP BY\r\n \"date\",\r\n \"name\"",

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 'Average build times' tile (lines 215-272) computes EXTRACT(EPOCH FROM "buildingAt" - "createdAt") as build_time, but per Vercel's deployment lifecycle (createdAt → buildingAt → ready) that difference is the queue/wait time before the build started, not the build duration. The tile's title and seconds/formatDuration formatting will lead users to read the (typically much smaller) queue-latency numbers as actual build time. Fix: change the expression to EXTRACT(EPOCH FROM "ready" - "buildingAt") — the deployments stream already surfaces ready as a date column (deployments.json:74-78).

Extended reasoning...

What the bug is

In plugins/Vercel/v1/defaultContent/deployments.dash.json at line 229, the headline "Average build times" tile contains this SQL:

WITH "build_times" AS (
    SELECT
        STRFTIME(DATE_TRUNC(day, "createdAt"), ...) AS "date",
        "name",
        EXTRACT(EPOCH FROM "buildingAt" - "createdAt") AS "build_time"
    FROM "dataset1"
)
SELECT "date", "name", AVG("build_time") AS "avg_build_time"
FROM "build_times"
GROUP BY "date", "name"

Per Vercels REST API for /v7/deployments, the deployment lifecycle has three timestamps:

  • createdAt — when the deployment was created/queued
  • buildingAt — when the build process actually started (transition to the BUILDING state)
  • ready — when the deployment finished and is READY

So buildingAt - createdAt is the time the deployment spent waiting in the queue before the build began — not the build duration. The build duration is ready - buildingAt.

How it manifests

The tile is titled "Average build times" and the avg_build_time column carries the metadata shape ["seconds", { "formatDuration": true }] (line 251-260), so the number is rendered as a human-readable duration. Users looking at this tile will inevitably interpret it as "how long my builds take", but the value actually shown is "how long my deployments waited to start building".

Queue times are typically seconds; build times are typically minutes. The magnitude mismatch makes the reading actively misleading — users investigating slow builds will see suspiciously fast numbers and conclude their builds are fine when they may not be.

Why existing code does not prevent it

This is a semantic correctness bug in user-authored SQL — schema/structural validators have no way to know that buildingAt - createdAt does not match the tiles title. The deployments stream metadata correctly exposes all three lifecycle timestamps (ready, buildingAt, createdAtdeployments.json lines 74-90), so the data needed for the correct expression is already available; the SQL just references the wrong pair.

Impact

Headline tile on the out-of-the-box Deployments dashboard reports the wrong metric. Every user that installs the Vercel plugin sees a "build times" chart that actually measures queue latency. Normal severity — visible, misleading analytics on an OOB dashboard.

Step-by-step proof

Concrete example with realistic Vercel timestamps for a single deployment:

  1. createdAt = 2026-06-18T10:00:00Z — deployment queued
  2. buildingAt = 2026-06-18T10:00:08Z — build started 8 seconds later
  3. ready = 2026-06-18T10:03:32Z — build finished 3m 24s after starting

The current SQL computes buildingAt - createdAt = 8 seconds and labels it "build_time". With formatDuration: true, the tile shows "8s" as the average build time — but the actual build took 3m 32s. The tile is reporting queue latency (8s) instead of build duration (212s), an order-of-magnitude understatement of the metric users believe they are seeing.

How to fix

Change the SQL expression on line 229 from:

EXTRACT(EPOCH FROM "buildingAt" - "createdAt") AS "build_time"

to:

EXTRACT(EPOCH FROM "ready" - "buildingAt") AS "build_time"

No other changes are needed — ready is already surfaced as a date column on the deployments stream (deployments.json:74-78). The DATE_TRUNC grouping on createdAt for the daily bucket is fine to keep (bucketing builds by the day they were created reads naturally).

"role": "timestamp"
}
],
"timeframes": ["last24hours", "last7days", "last30days", "thisMonth"]

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 cost data stream declares "timeframes": ["last24hours", "last7days", "last30days", "thisMonth"] at cost.json:70, but docs/README.md:53 and the PR description's Known limitations explicitly state the cost stream is restricted to Last 24 hours and Last 7 days, noting that even within that window users "may get time outs or lambda response size errors". A user picking last30days or thisMonth from the timeframe picker will silently walk into the documented timeout/lambda response-size failures with no warning. Fix by narrowing the array to ["last24hours", "last7days"] to match the README (preferred — the docs explain the underlying constraint), or update the README/known-limitations to reflect the wider allowed set.

Extended reasoning...

What the bug is

plugins/Vercel/v1/dataStreams/cost.json:70 declares:

"timeframes": ["last24hours", "last7days", "last30days", "thisMonth"]

— four selectable timeframes. But plugins/Vercel/v1/docs/README.md:53 explicitly tells users:

Cost data is daily granularity and the billing endpoint returns very large datasets, so the cost stream is restricted to the Last 24 hours and Last 7 days timeframes.

And this PR's own Known limitations section in the description goes further:

Cost data is daily granularity and the billing endpoint returns very large datasets, so the cost stream is restricted to the Last 24 hours and Last 7 days timeframes, even then we may get time outs or lambda response size errors.

The config and the documentation contradict each other. The four-entry list is what the UI will actually show in the timeframe picker; the README is what the user will read before deciding what to pick.

How it manifests

  1. User opens a tile bound to the Cost stream (e.g. one of the Cost dashboard tiles, or a new tile they author).
  2. The SquaredUp UI renders the timeframe picker from the stream's timeframes array — last24hours, last7days, last30days, and thisMonth are all selectable.
  3. User selects last30days (or thisMonth) because it's available — there is no in-product hint that it's off-limits.
  4. The cost stream calls GET /v1/billing/charges?from={timeframe.start}&to={timeframe.end} — a 30-day FOCUS billing window for a non-trivial account.
  5. Per the author's own documented experience, the request returns either a Lambda timeout or a Lambda response-size error (or both). The tile fails to render; the user has no idea this is an expected limitation.

Why existing code does not prevent it

There is no runtime guard in dataStreams/scripts/cost.js to detect or short-circuit oversized requests — the script only parses the JSONL response after it returns. The timeframes array in the data stream JSON is the only user-facing gate, and it currently allows the two timeframes the author has explicitly flagged as broken. The OOB Cost dashboard tiles all pin timeframe: "last7days", so OOB rendering is safe — but the moment any user customizes the timeframe or browses the data stream, the footgun is exposed.

Step-by-step proof of the inconsistency

  1. Open plugins/Vercel/v1/dataStreams/cost.json line 70 → array has 4 entries: last24hours, last7days, last30days, thisMonth.
  2. Open plugins/Vercel/v1/docs/README.md line 53 → "the cost stream is restricted to the Last 24 hours and Last 7 days timeframes." Two values, explicitly.
  3. Read the PR description's Known limitations bullet for cost → same two values, with the additional admission that even those can time out.
  4. Compare: 4 ≠ 2. The config offers two timeframes the docs say are not supported.
  5. Concrete trigger: in the SquaredUp UI, edit any cost-bound tile, open the timeframe dropdown, pick Last 30 days or This Month. The request fires; on any account with non-trivial billing volume, the documented timeout/response-size failure occurs.

How to fix

Preferred fix (matches the author's documented intent):

-    "timeframes": ["last24hours", "last7days", "last30days", "thisMonth"]
+    "timeframes": ["last24hours", "last7days"]

Alternative: if last30days / thisMonth were intentionally exposed despite the documented risk, update docs/README.md:53 and the PR description's Known limitations bullet to reflect that those timeframes are available but may time out, so users are not surprised. The first option is cleaner — it removes a known footgun rather than documenting it.

Comment thread plugins/Vercel/v1/dataStreams/projects.json
"_type": "tile/data-stream",
"description": "",
"activePluginConfigIds": ["{{configId}}"],
"title": "Daily activity",

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 OOB dashboard tile titles aren't title-cased, while ~20 other tiles in this PR (and REVIEW.md's 'Tile names — Use title case' rule) follow the convention. Affected: activity.dash.json:161 'Daily activity' → 'Daily Activity'; cost.dash.json:141 'Cost by day' → 'Cost by Day'; deployments.dash.json:180 'Deployments per day' → 'Deployments per Day'; deployments.dash.json:221 'Average build times' → 'Average Build Times'; project.dash.json:321 'Firewall events' → 'Firewall Events'. (Short prepositions 'by'/'per' stay lowercase per standard title-case rules.) Cosmetic — nit-level consistency fix.

Extended reasoning...

What the bug is

REVIEW.md, under Out-of-the-box dashboards, states a clear rule for Tile names — Use title case. Five tile titles in this PR violate it while ~20 others in the same PR follow it, so the convention is established and these five are slips rather than an intentional style choice.

The specific violations

Verified directly against the diff:

  • plugins/Vercel/v1/defaultContent/activity.dash.json:161"title": "Daily activity" → 'Daily Activity'
  • plugins/Vercel/v1/defaultContent/cost.dash.json:141"title": "Cost by day" → 'Cost by Day'
  • plugins/Vercel/v1/defaultContent/deployments.dash.json:180"title": "Deployments per day" → 'Deployments per Day'
  • plugins/Vercel/v1/defaultContent/deployments.dash.json:221"title": "Average build times" → 'Average Build Times'
  • plugins/Vercel/v1/defaultContent/project.dash.json:321"title": "Firewall events" → 'Firewall Events'

Short prepositions like 'by' and 'per' remain lowercase under standard title-case rules — the actual issue in 'Cost by day' / 'Deployments per day' is the uncapitalised noun Day after the preposition, not the preposition itself.

Why existing code doesn't catch it

The plugin validator (visible in the github-actions[bot] PR summary) checks structural/schema correctness only. There is no lint step that scans dashboard tile title strings for casing rules from REVIEW.md, so stylistic slips like this pass validation cleanly.

Step-by-step proof

  1. Open REVIEW.mdOut-of-the-box dashboards section → the rule reads verbatim: 'Tile names — Use title case'.
  2. Open activity.dash.json at line 161 → "title": "Daily activity". The leading 'Daily' is capitalised; 'activity' is not.
  3. For contrast, look at sibling titles in the same files that DO follow the rule: 'Recent Activity' (activity.dash.json:35), 'Team Roster' (activity.dash.json:75), 'Cost by Service' / 'Cost by Project' (cost.dash.json), 'Latest Failed Builds' / 'Deployment Volume' / 'Deployment History' / 'Latest Deployment State' (deployments.dash.json, project.dash.json), 'Project Info' / 'Firewall Event Actions' (project.dash.json). The author clearly knows the convention.
  4. Repeat for the other four citations — same pattern: capital first word, uncapitalised noun/verb later in the title.

Impact

Cosmetic only — these tiles will render fine. The inconsistency is visible to users browsing the OOB dashboards (one title-cased tile sitting next to a sentence-cased one in the same dashboard reads as untidy), and REVIEW.md treats this as a rule rather than a suggestion. Hence nit severity.

How to fix

A five-line edit, one per file:

activity.dash.json:161      "Daily activity"          → "Daily Activity"
cost.dash.json:141          "Cost by day"             → "Cost by Day"
deployments.dash.json:180   "Deployments per day"     → "Deployments per Day"
deployments.dash.json:221   "Average build times"     → "Average Build Times"
project.dash.json:321       "Firewall events"         → "Firewall Events"

Comment thread plugins/Vercel/v1/ui.json
{
"type": "password",
"name": "accessToken",
"label": "API Token",

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 'API Token' field label at line 10 capitalizes the second word, but REVIEW.md's UI Configuration section requires displayName to follow 'First word uppercase, then lowercase (e.g. "Table name", or "API key")'. Nine other plugins (Phare, Rootly, MetOffice, UptimeRobot, Checkly, Postcoder, SendGrid, DigiCert, DattoRMM) use the correct 'API key' form. Change to 'API token' to match the documented rule and repo precedent.

Extended reasoning...

What the bug is

plugins/Vercel/v1/ui.json:10 declares:

{
    "type": "password",
    "name": "accessToken",
    "label": "API Token",
    ...
}

The label value "API Token" capitalizes the second word. REVIEW.md is explicit under the UI Configuration section that displayName/label values must follow: 'First word uppercase, then lowercase (e.g. "Table name", or "API key")'. Token is a common noun (not an initialism/abbreviation like ID or URL that the rule allows to remain uppercase), so it must be lowercase here.

Why this matters

The rule is not stylistic preference — it is enumerated as an explicit rule in the repo's review guide, and it is overwhelmingly followed by the existing plugin catalog. A repo-wide grep for the same field shape across plugins confirms the convention:

  • Follow the rule (lowercase second word): Phare, Rootly, MetOffice, UptimeRobot, Checkly, Postcoder, SendGrid, DigiCert, DattoRMM — all use "API key".
  • Deviate: UniFi and AutoTask use "API Key" — a small minority that violates the same rule. Vercel's "API Token" joins that minority.

These labels surface directly in the SquaredUp plugin configuration UI, so inconsistent capitalization is visible to every user adding a connection. Keeping the casing uniform across plugins is the point of the rule.

Why existing validation doesn't catch it

The plugin validator (visible in the github-actions[bot] PR summary on this PR) checks structural correctness — schema, required fields, file presence — but does not lint prose-level rules from REVIEW.md such as displayName casing. There is no CI step that flags this; it relies on review.

Impact

Cosmetic / consistency — nothing functionally breaks. Severity is nit, in line with all four verifier confirmations. But the rule is explicit and the fix is one character, so it is worth correcting before merge to avoid permanent inconsistency in the published catalog (the label is user-visible and would require a plugin update to change later).

Step-by-step proof

  1. Open REVIEW.md and locate the UI Configuration section. The displayName field's guidance reads verbatim: 'First word uppercase, then lowercase (e.g. "Table name", or "API key")'.
  2. Open plugins/Vercel/v1/ui.json at line 10: "label": "API Token". The second word Token is capitalized. Token is a normal noun, not an abbreviation — so the rule applies and is violated.
  3. Compare with any of the nine peer plugins listed above — e.g. plugins/Phare/v1/ui.json or plugins/Rootly/v1/ui.json — which use "API key". The convention is well-established and intentional.
  4. Note that the configValidation.json error message at line 7 ("Could not authenticate with Vercel. Check that your API Token is valid and has not expired.") also uses API Token. The rule applies specifically to the displayName/label field, but the author may want to align that user-facing string for the same reason.

How to fix

Change line 10 of plugins/Vercel/v1/ui.json from "label": "API Token" to "label": "API token". Optionally align the error message in configValidation.json:7 to match ("...your API token is valid...") for consistency, though that string is not governed by the displayName rule.

@TimWheeler-SQUP TimWheeler-SQUP added the new-plugin Used to PR newly added plugins label Jun 18, 2026
@github-actions

Copy link
Copy Markdown

🧩 Plugin PR Summary

📦 Modified Plugins

  • plugins/Vercel/v1

📋 Results

Step Status
Validation ❌ Failed
Deployment ⏭️ Skipped

🔍 Validation Details

Comment on lines +144 to +154
"config": {
"data-stream-line-graph": {
"seriesColumn": "none",
"dataPoints": true,
"showTrendLine": true,
"cumulative": false,
"xAxisColumn": "service_uniqueValues",
"yAxisColumn": ["billedCost_sum"]
}
}
}

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 "Cost by day" line graph tile at cost.dash.json:144-154 sets xAxisColumn to "service_uniqueValues", but the tile groups by [["periodStart", "byDay"]] and aggregates billedCost — so the grouped output only contains the columns periodStart_byDay and billedCost_sum. The service_uniqueValues column only exists on the sibling "Cost by Service" donut tile (a clear copy-paste artifact), so the x-axis cannot bind and the headline daily-cost trend line on the OOB Cost dashboard will fail to render data over time. Fix: change "xAxisColumn": "service_uniqueValues" to "xAxisColumn": "periodStart_byDay" on line 150.

Extended reasoning...

What the bug is

In plugins/Vercel/v1/defaultContent/cost.dash.json lines 118-155, the third tile ("Cost by day") is a data-stream-line-graph bound to the cost data stream. Its data stream config is:

"dataStream": {
    "name": "cost",
    "id": "{{dataStreams.[cost]}}",
    "group": {
        "by": [["periodStart", "byDay"]],
        "aggregate": [
            { "type": "sum", "names": ["billedCost"] }
        ]
    }
}

After SquaredUp applies this group+aggregate, the post-grouping dataset has exactly two columns:

  • periodStart_byDay — the day bucket from the group-by
  • billedCost_sum — the summed billed cost from the aggregate

There is no service column in this tiles grouped result, because the tile does not group by service at all.

But the visualisation block on line 150 sets:

"visualisation": {
    "type": "data-stream-line-graph",
    "config": {
        "data-stream-line-graph": {
            "xAxisColumn": "service_uniqueValues",
            "yAxisColumn": ["billedCost_sum"]
        }
    }
}

service_uniqueValues is the unique-values column produced by the sibling "Cost by Service" donut tile above it (lines 18-66), which legitimately groups by [["service", "uniqueValues"]]. The xAxisColumn value here was copy-pasted from that donut and never updated when the line graph was switched to a daily breakdown.

Why existing code does not prevent it

Dashboard tile JSON is structurally validated for schema correctness, but the plugin validator does not check that xAxisColumn / yAxisColumn strings actually correspond to columns produced by the tiles group/aggregate configuration. The visualisation column references are validated only at render time against the live data, so a mismatch like this passes the PRs validation pipeline (the github-actions[bot] summary reports ✅ Passed) but breaks at runtime on the users dashboard.

Impact

The "Cost by day" tile is the third headline tile on the out-of-the-box Cost dashboard — directly under "Cost by Service" and "Cost by Project". Every user who installs the Vercel plugin will see this tile fail to render its x-axis correctly (most likely an empty/broken axis), so the daily cost trend over the last 7 days — the entire point of this tile — will not display. Visible regression on an OOB dashboard. Normal severity.

Step-by-step proof

Concrete example with three days of cost rows in the cost data stream:

service billedCost periodStart
Bandwidth 0.40 2026-06-16T00:00:00Z
Function Invocations 1.20 2026-06-16T00:00:00Z
Bandwidth 0.55 2026-06-17T00:00:00Z
Function Invocations 0.90 2026-06-17T00:00:00Z
Bandwidth 0.30 2026-06-18T00:00:00Z
Function Invocations 1.10 2026-06-18T00:00:00Z
  1. The tile applies group: { by: [["periodStart", "byDay"]], aggregate: [{ type: "sum", names: ["billedCost"] }] }.

  2. The grouped dataset is:

    periodStart_byDay billedCost_sum
    2026-06-16 1.60
    2026-06-17 1.45
    2026-06-18 1.40
  3. The renderer looks up xAxisColumn = "service_uniqueValues" in this dataset → no such column exists → x-axis cannot bind to any data → the line graph cannot plot points along a time axis.

  4. For contrast, with xAxisColumn = "periodStart_byDay", the renderer finds the column and plots three points: (2026-06-16, 1.60), (2026-06-17, 1.45), (2026-06-18, 1.40) — the daily cost trend the tile title promises.

How to fix

Change line 150 of plugins/Vercel/v1/defaultContent/cost.dash.json from:

"xAxisColumn": "service_uniqueValues",

to:

"xAxisColumn": "periodStart_byDay",

No other changes are needed — yAxisColumn: ["billedCost_sum"] is already correct, and periodStart_byDay is what the group-by emits.

Comment on lines +27 to +30
"sort": {
"top": 10,
"by": [["created", "asc"]]
},

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 'Latest Builds (Top 10)' tile sorts by [["created", "asc"]] with top: 10, which returns the 10 oldest builds in the timeframe rather than the latest. The sibling 'Latest Deployment State' tile in project.dash.json correctly uses desc with top: 1. Fix: change to [["created", "desc"]] at deployments.dash.json:29.

Extended reasoning...

What the bug is

At plugins/Vercel/v1/defaultContent/deployments.dash.json:27-30, the "Latest Builds (Top 10)" tile declares:

"sort": {
    "top": 10,
    "by": [["created", "asc"]]
}

The created column on the deployments stream is a date timestamp (see dataStreams/deployments.json:74-78). Ascending order on a timestamp puts the oldest rows first, so combined with top: 10 the tile selects the 10 oldest builds in the dashboard's last30days timeframe (declared at line 4) — the opposite of the tile's title and intent.

Code path that triggers it

  1. Dashboard loads with timeframe: "last30days" (deployments.dash.json:4).
  2. The deployments data stream returns all deployments within the window (the deployments.js post-request script enforces the upper bound via untilMs and the stream's since query arg enforces the lower bound).
  3. The tile applies the configured sort[["created", "asc"]] with top: 10 — which slices to the 10 rows with the smallest created timestamp.
  4. The user sees the 10 oldest deployments in the past 30 days rendered as "Latest Builds".

Why existing code doesn't prevent it

This is a semantic correctness bug in the dashboard JSON. The schema/structural validator (visible in the github-actions[bot] PR summary, which reports ✅ Passed) has no way to know that "Latest Builds" + asc is contradictory. The peer "Latest Deployment State" tile in project.dash.json:42-45 correctly pairs the "Latest" label with [["created", "desc"]] and top: 1, confirming the intended convention.

Impact

Visible regression on the headline tile of the out-of-the-box Deployments dashboard — every user that installs the Vercel plugin sees the 10 oldest builds (potentially up to 30 days stale) on the most prominent tile of the deployments page, instead of the most recent builds they are looking for. Severity: normal.

Step-by-step proof

Concrete example. Suppose the account had 50 deployments in the last 30 days. With the dashboard's last30days timeframe and the tile's current sort:

  1. Stream returns all 50 deployments, each with a created epoch timestamp (e.g. ranging from 2026-05-19T08:00:00Z to 2026-06-18T11:30:00Z).
  2. Sort [["created", "asc"]] orders them oldest-first → row 1 is the deployment from 2026-05-19, row 50 is today's.
  3. top: 10 keeps rows 1–10 → the ten oldest deployments (May 19 → ~May 22 range), not the latest.
  4. With the dashboard rendering this as "Latest Builds (Top 10)", the user sees deployments that may be nearly a month old labelled as "latest". Today's production deploy is nowhere on the headline tile.

How to fix

Change line 29 of plugins/Vercel/v1/defaultContent/deployments.dash.json from:

"by": [["created", "asc"]]

to:

"by": [["created", "desc"]]

No other changes required — top: 10 is correct, and created is the correct column.

@@ -0,0 +1,55 @@
## What this plugin monitors

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 🟡 docs/README.md starts with ## What this plugin monitors (h2, no h1 anywhere) — REVIEW.md's Documentation section says docs/README.md 'Should typically start headings from level 1' and to 'Avoid headings that repeat the plugin name or use "Overview". A good heading might be something like # Before you start or # Prerequisites.' 13 of 15 peer plugin READMEs lead with # Before you start (h1); only Vercel and SumoLogic start with h2. The README is rendered in-product on the plugin configuration page, so the leading heading is user-visible. Suggested fix: lead with # Before you start (h1) and put the Prerequisites/Configuration content directly under it, or promote the existing Prerequisites section to h1 and move it to the top — the current "What this plugin monitors" section is the Overview-style heading the rule discourages.

Extended reasoning...

What the bug is

plugins/Vercel/v1/docs/README.md opens with ## What this plugin monitors (h2). Searching the file, no h1 (#) heading appears anywhere — every section is h2: ## What this plugin monitors, ## Prerequisites, ## Configuration fields, ## What gets indexed, ## Known limitations. This violates two explicit rules from REVIEW.md's Documentation section:

  1. 'Should typically start headings from level 1. When embedded in SquaredUp, the headings will be sized appropriately.'
  2. 'Avoid headings that repeat the plugin name or use "Overview". A good heading might be something like # Before you start or # Prerequisites.'

## What this plugin monitors is precisely the Overview-style heading the second rule discourages — it sits at the top of the doc and frames the rest as a product overview, which is exactly what the rule is telling authors to drop in favour of leading with actionable setup content.

Repo precedent

A repo-wide grep of plugins/*/v1/docs/README.md for the first heading shows the convention is overwhelming: 13 of 15 peer plugin READMEs lead with # Before you start (h1). Only Vercel (this PR) and SumoLogic begin with an h2 overview-style heading. The author of this PR has clearly modelled the doc after the SumoLogic style rather than the majority pattern; the rule and the precedent both point the other way.

Why it matters

The README is rendered in-product when a user is configuring the plugin (the 'Need help?' panel on the connection setup page). The first heading the user sees is therefore the headline of that help panel — and in this case it is a smaller h2 with no anchoring h1 above it, followed by a marketing-style intro paragraph rather than the prerequisites or setup steps the user is actively looking for. REVIEW.md also says elsewhere that 'The Setup or Configuration section should appear near the top' — the current ordering pushes Prerequisites below a 'What this plugin monitors' block.

The bug is purely cosmetic — no data flow breaks — hence the verifier consensus on nit severity. But it is one of the rare nits where the rule is explicit, the precedent is overwhelming, and the fix is a 1–2 line edit, so it is cheap to land before merge and impossible to change post-publication without a plugin version bump.

Step-by-step proof

  1. Open plugins/Vercel/v1/docs/README.md line 1. The first non-blank line is ## What this plugin monitors.
  2. Grep the file for ^# (h1 heading) — zero matches. There is no h1 in the document at all.
  3. Open any of the peer READMEs — e.g. plugins/DigiCert/v1/docs/README.md, plugins/Huntress/v1/docs/README.md, plugins/UptimeRobot/v1/docs/README.md, plugins/NinjaOne/v1/docs/README.md, plugins/Spotify/v1/docs/README.md. Each begins with # Before you start. The convention is well-established.
  4. Open REVIEW.md and locate the Documentation - (docs/README.md) section. Both rules quoted above appear verbatim.
  5. In-product: when a user adds the Vercel plugin and lands on the configuration page, the 'Need help?' panel renders this README, and the first heading they see is What this plugin monitors — the Overview-style framing the rule discourages.

How to fix

Two equivalent options, both small edits:

Option A (matches majority precedent): Rename the first heading to # Before you start (h1) and replace the current intro paragraph + bullet list with a brief one-line context sentence, then jump straight into prerequisites. The bullet list of capabilities can move to a smaller ## What gets monitored section further down, or merge with the existing ## What gets indexed section.

Option B (minimal change): Promote the existing ## Prerequisites section to # Prerequisites (h1), move it to the top of the file, and either drop the current ## What this plugin monitors block or demote it to a single short intro line above the h1.

Option A matches the 13/15 precedent and is the strongly preferred fix; option B is the minimum acceptable change. Either way, ensure the file contains at least one h1 and that the first heading is not an Overview-style block.

"name": "vercel",
"displayName": "Vercel",
"version": "1.0.0",
"author": { "name": "@andrewmumblebee", "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.

🟡 This PR adds the Vercel plugin but does not add a matching entry to .github/CODEOWNERS. Every other community plugin (AutoTask, DattoRMM, DigiCert, Huntress, MetOffice, Phare, Rootly, SendGrid, Spotify, etc.) has an explicit plugins/<Name>/* @<author> line, so @andrewmumblebee should add plugins/Vercel/* @andrewmumblebee to get auto-pinged on future changes to this plugin. Nit — the fallback rule plugins/* @squaredup/community-moderators catches reviews for now, but the per-plugin convention is documented in REVIEW.md ("If the user is adding a new plugin, encourage them to update the .github/CODEOWNERS file so they can help review future contributions").

Extended reasoning...

What is missing

The PR introduces a new community plugin under plugins/Vercel/v1/ with metadata.json declaring "author": { "name": "@andrewmumblebee", "type": "community" } (line 5). However, the PR diff does not touch .github/CODEOWNERS, so no per-plugin ownership entry exists for the new plugins/Vercel/ directory.

Why it matters / how the convention is established

REVIEW.md includes a dedicated CODEOWNERS section stating: "If the user is adding a new plugin, encourage them to update the .github/CODEOWNERS file so they can help review future contributions." Every other community plugin in this repo follows the convention with an explicit plugins/<PluginName>/* @<author> line — AutoTask, DattoRMM, DigiCert, FantasyPremierLeague, GoogleSheets, Huntress, MetOffice, Phare, Postcoder, RDAP, RSS, Rootly, SendGrid, Spotify, Steam, SumoLogic, TransportForLondon, UniFi, UptimeRobot, WorldCup2026, and others all have their own line. Vercel is the only community plugin in this PR's tree without one.

Why this isn't a blocker

The catch-all rule plugins/* @squaredup/community-moderators already exists in .github/CODEOWNERS, so review requests on any future Vercel-plugin PRs will still route to the community moderators group. Nothing functionally breaks. That is why this is a nit rather than a normal-severity bug.

Why it's still worth fixing

Without a per-plugin entry, the author themselves (@andrewmumblebee) is not automatically requested on future Vercel-plugin changes. The whole point of the established convention — and the explicit REVIEW.md guidance — is that the original plugin author opts in to receive review pings on their own plugin, so they can help maintain it. Skipping this leaves the plugin worse positioned for follow-up contributions and breaks the consistency every other community plugin in the repo has.

Step-by-step proof

  1. Open plugins/Vercel/v1/metadata.json:5"author": { "name": "@andrewmumblebee", "type": "community" }. New plugin, new community author.
  2. Open .github/CODEOWNERS and grep for Vercel → no match. Grep for any other community plugin (e.g. Rootly, Phare, SendGrid) → each has its own plugins/<Name>/* @<author> line.
  3. Open REVIEW.mdCODEOWNERS section: "If the user is adding a new plugin, encourage them to update the .github/CODEOWNERS file so they can help review future contributions."
  4. Consequence: future PRs touching plugins/Vercel/ will only auto-request @squaredup/community-moderators via the catch-all, not @andrewmumblebee.

How to fix

Add a single line to .github/CODEOWNERS alongside the other per-plugin entries:

plugins/Vercel/* @andrewmumblebee

One-line change, matches the convention used by all 19+ existing community plugin entries.

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