Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0326602
Add initial Vercel plugin dataStreams and dashboards
andrewmumblebee Jun 16, 2026
138fe02
Remove unused listDeployments dataStream
andrewmumblebee Jun 16, 2026
fc8a29a
Vercel plugin review comments
andrewmumblebee Jun 18, 2026
5b2e47a
update plugin author
andrewmumblebee Jun 18, 2026
c70fa6e
Ensure scopes and object types match new sourceTypes
andrewmumblebee Jun 18, 2026
79b05d5
Remove Vercel cost data stream
andrewmumblebee Jun 19, 2026
703ded5
shift overview tiles up
andrewmumblebee Jun 19, 2026
5c4a222
Add team error handling
andrewmumblebee Jun 19, 2026
85dd9d3
slight text tweak
andrewmumblebee Jun 19, 2026
c3b9d55
Add codeowners for vercel
andrewmumblebee Jun 19, 2026
d38b7ce
Update Vercel readme
andrewmumblebee Jun 19, 2026
9d4343d
Remove need for user to add teamId
andrewmumblebee Jun 19, 2026
e590528
Token could belong to multiple teams
andrewmumblebee Jun 19, 2026
987c70d
Add status colors to bar chart on deployment oob dashboard
andrewmumblebee Jun 19, 2026
5c2d51a
Clean up Vercel tags and descriptions
andrewmumblebee Jun 19, 2026
82e2378
change deployments ordering
andrewmumblebee Jun 19, 2026
8a11ba2
title case dashboards
andrewmumblebee Jun 19, 2026
360104d
remove orphaned cost script
andrewmumblebee Jun 19, 2026
bd90231
remove cost from description
andrewmumblebee Jun 19, 2026
9f8473e
Claude review fixes
andrewmumblebee Jun 19, 2026
88f9c72
Vercel claude review tweaks
andrewmumblebee Jun 19, 2026
04dc94c
remove unwrap as not needed
andrewmumblebee Jun 19, 2026
0d45644
By -> by
andrewmumblebee Jun 19, 2026
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ plugins/TransportForLondon/* @clarkd
plugins/UniFi/* @adamkinniburgh
plugins/UptimeRobot/* @kieranlangton
plugins/WorldCup2026/* @TimWheeler-SQUP
plugins/Vercel/* @andrewmumblebee


# Fallback – if a plugin has no specified author
Expand Down
11 changes: 11 additions & 0 deletions plugins/Vercel/v1/configValidation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"steps": [
{
"displayName": "Authenticate",
"dataStream": { "name": "currentUser" },
"required": true,
"error": "Could not authenticate with Vercel. Check that your API token is valid and has not expired.",
"success": "Connected to Vercel successfully."
}
]
}
23 changes: 23 additions & 0 deletions plugins/Vercel/v1/custom_types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"name": "Project",
"sourceType": "Project",
"icon": "rocket",
"singular": "Project",
"plural": "Projects"
},
{
"name": "Domain",
"sourceType": "Domain",
"icon": "globe",
"singular": "Domain",
"plural": "Domains"
},
{
"name": "Team",
"sourceType": "Team",
"icon": "people-group",
"singular": "Team",
"plural": "Teams"
}
]
Comment thread
claude[bot] marked this conversation as resolved.
41 changes: 41 additions & 0 deletions plugins/Vercel/v1/dataStreams/activity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "activity",
"displayName": "Activity",
"description": "Vercel account or team activity feed, one row per audit-style event",
"tags": ["Activity"],
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"endpointPath": "v3/events",
"pathToData": "events",
"expandInnerObjects": true,
"getArgs": [
{ "key": "since", "value": "{{timeframe.start}}" },
{ "key": "until", "value": "{{timeframe.end}}" },
Comment on lines +12 to +14

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 activity stream passes {{timeframe.start}} and {{timeframe.end}} (which template to ISO 8601 strings) to Vercel's /v3/events since/until params, but Vercel documents these as JavaScript millisecond timestamps — confirmed by the other two timeframe-aware Vercel streams in this same PR (deployments.json:13 and firewallEvents.json:14,16) which correctly use {{timeframe.unixStart * 1000}} / {{timeframe.unixEnd * 1000}}. As written, Vercel will reject the ISO string as non-numeric or silently ignore it, so the Activity dashboard's timeframe selector is a no-op — every request returns the API's default window regardless of the selected timeframe. Fix: change lines 13-14 to {{timeframe.unixStart * 1000}} / {{timeframe.unixEnd * 1000}} to match the other Vercel streams.

Extended reasoning...

What the bug is

At plugins/Vercel/v1/dataStreams/activity.json:12-14, the activity stream sends:

"getArgs": [
    { "key": "since", "value": "{{timeframe.start}}" },
    { "key": "until", "value": "{{timeframe.end}}" },
    { "key": "limit", "value": "100" }
]

Per SquaredUp's build-plugin reference (.claude/skills/build-plugin/references/data-streams.md), {{timeframe.start}} / {{timeframe.end}} template to ISO 8601 strings (e.g. 2026-06-19T00:00:00.000Z), not unix epoch values. This is confirmed by usage in other plugins — SumoLogic's metricsQuery.json maps {{timeframe.start}} to iso8601Time, and SendGrid's stats streams use {{timeframe.start.substring(0,10)}} to extract the date portion.

Vercel's /v3/events endpoint documents since and until as integer Unix timestamps in milliseconds. The strongest evidence is internal to this same PR — three Vercel timeframe-aware endpoints, two correctly using unix ms, one (this one) using ISO strings:

  • deployments.json:13"since" = {{timeframe.unixStart * 1000}} for /v7/deployments
  • firewallEvents.json:14,16startTimestamp / endTimestamp = {{timeframe.unixStart * 1000}} / {{timeframe.unixEnd * 1000}} for /v1/security/firewall/events
  • activity.json:13-14since/until = {{timeframe.start}}/{{timeframe.end}} (ISO strings) — the outlier

The deployments.js post-request script also explicitly notes the Vercel time convention is unix ms (untilMs = tf.unixEnd * 1000), reinforcing that this is the established convention for this plugin.

Why existing code does not prevent it

The plugin validator (visible in the github-actions[bot] PR summary, ✅ Passed) checks structural/schema correctness only — it does not validate that timeframe template values match the format the upstream API expects. The mismatch between the templated ISO 8601 string and Vercel's expected ms integer slips through validation cleanly.

Impact

The activity stream's paging.mode is none with limit=100, so even if Vercel silently ignores the malformed since/until params, the request still succeeds and returns Vercel's default recent-events window — capped at 100 events regardless of what timeframe the user selected. The Activity dashboard's timeframe selector (last24hours, last7days, etc., set at activity.dash.json:4 via last7days) becomes a no-op on the OOB Activity dashboard. The "Recent Activity" table, the "Daily Activity" line graph, and any user-authored activity tiles all show whatever Vercel's default window happens to be — not the timeframe the dashboard claims to be filtering by.

This is a visible behavioural regression on the OOB Activity dashboard, hence normal severity.

How to fix

Two-line edit on plugins/Vercel/v1/dataStreams/activity.json:

  "getArgs": [
-     { "key": "since", "value": "{{timeframe.start}}" },
-     { "key": "until", "value": "{{timeframe.end}}" },
+     { "key": "since", "value": "{{timeframe.unixStart * 1000}}" },
+     { "key": "until", "value": "{{timeframe.unixEnd * 1000}}" },
      { "key": "limit", "value": "100" }
  ]

This matches the convention used by the other two timeframe-aware Vercel streams in this PR.

Step-by-step proof

Concrete example with the dashboard's last7days timeframe selected on 2026-06-19:

  1. Dashboard timeframe last7days is resolved → timeframe.start = "2026-06-12T00:00:00.000Z", timeframe.end = "2026-06-19T00:00:00.000Z" (ISO 8601 strings, per the template docs).
  2. Activity stream substitutes them into the request: GET /v3/events?since=2026-06-12T00:00:00.000Z&until=2026-06-19T00:00:00.000Z&limit=100.
  3. Vercel's /v3/events parses since/until as integers. parseInt("2026-06-12T00:00:00.000Z") yields either 2026 (truncated parse) or NaN (strict parse) — either way, not the intended ms timestamp 1749686400000.
  4. Vercel either returns 400 (strict numeric validation), returns events newer than the year 2026 ms (i.e. events newer than ~1970-01-01T00:00:02Z — effectively everything), or silently ignores the malformed param and returns the default recent-events window.
  5. In all three failure modes, the request does not honour the dashboard's 7-day window. The user changes the timeframe to last24hours — same broken behaviour, no change in the rendered tile.
  6. For contrast, with the fix applied: GET /v3/events?since=1749686400000&until=1750291200000&limit=100 → Vercel honours both bounds → the tile renders activity events from the selected 7-day window.

{ "key": "limit", "value": "100" }
],
"paging": { "mode": "none" }
},
"matches": "none",
"metadata": [
{ "name": "id", "displayName": "ID", "role": "id", "visible": false },
{ "name": "type", "displayName": "Type" },
{ "name": "text", "displayName": "Summary", "role": "label" },
{
"name": "actor",
"displayName": "Actor",
"computed": true,
"valueExpression": "{{ $['user.username'] || $['user.email'] || $['userId'] }}"
},
{ "name": "user.username", "displayName": "User", "visible": false },
{ "name": "user.email", "displayName": "Email", "visible": false },
{ "name": "userId", "displayName": "User ID", "visible": false },
{
"name": "createdAt",
"displayName": "Created",
"shape": "date",
"role": "timestamp"
}
],
"timeframes": true
}
21 changes: 21 additions & 0 deletions plugins/Vercel/v1/dataStreams/currentUser.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "currentUser",
"displayName": "Current User",
"description": "Returns the authenticated Vercel user. Used to validate the connection",
"tags": ["User"],
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"endpointPath": "v2/user",
"postRequestScript": "currentUser.js"
},
"matches": "none",
"metadata": [
{ "name": "id", "displayName": "ID", "visible": false },
{ "name": "username", "displayName": "Username", "role": "label" },
{ "name": "name", "displayName": "Name" },
{ "name": "email", "displayName": "Email" }
],
"timeframes": false,
"visibility": { "type": "hidden" }
}
95 changes: 95 additions & 0 deletions plugins/Vercel/v1/dataStreams/deployments.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"name": "deployments",
"displayName": "Deployments",
"description": "Vercel deployments across the account or a selected project, one row per deployment",
"tags": ["Deployments"],
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"endpointPath": "v7/deployments",
"postRequestScript": "deployments.js",
"getArgs": [
{ "key": "since", "value": "{{timeframe.unixStart * 1000}}" }
],
"paging": {
"mode": "token",
"pageSize": {
"realm": "queryArg",
"path": "limit",
"value": "100"
},
"in": { "realm": "payload", "path": "pagination.next" },
"out": { "realm": "queryArg", "path": "until" }
}
},
"matches": "none",
"ui": [
{
"type": "objects",
"name": "project",
"label": "Project (optional)",
"matches": {
"sourceType": { "type": "equals", "value": "Project" }
}
}
],
"metadata": [
{
"name": "uid",
"displayName": "ID",
"role": "value",
"visible": false
},
{ "name": "name", "displayName": "Name", "role": "label" },
{
"name": "state",
"displayName": "State",
"shape": [
"state",
{
"map": {
"success": ["READY"],
"error": ["ERROR", "CANCELED"],
"warning": ["BUILDING", "QUEUED", "INITIALIZING"],
"unknown": ["DELETED"]
}
}
]
},
{ "name": "target", "displayName": "Target" },
{ "name": "projectId", "displayName": "Project ID", "visible": false },
{
"name": "created",
"displayName": "Created",
"shape": "date",
"role": "timestamp"
},
{ "name": "url", "displayName": "URL", "shape": "url" },
{
"name": "inspectorUrl",
"displayName": "Inspector URL",
"shape": "url"
},
{ "name": "creator", "displayName": "Creator" },
{
"name": "ready",
"displayName": "Ready",
"shape": "date",
"role": "timestamp"
},
{
"name": "buildingAt",
"displayName": "Building At",
"shape": "date",
"role": "timestamp"
},
{
"name": "createdAt",
"displayName": "Created At",
"shape": "date",
"role": "timestamp"
},
{ "pattern": ".*" }
],
"timeframes": true
}
30 changes: 30 additions & 0 deletions plugins/Vercel/v1/dataStreams/domainConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "domainConfig",
"displayName": "Domain Config",
"description": "Configuration health for a single Vercel domain",
"tags": ["Domain"],
"baseDataSourceName": "httpRequestScopedSingle",
"config": {
"httpMethod": "get",
"endpointPath": "v6/domains/{{object.name}}/config"
},
"matches": { "sourceType": { "type": "equals", "value": "Domain" } },
"metadata": [
{
"name": "misconfigured",
"displayName": "Misconfigured",
"shape": "boolean"
},
{
"name": "serviceType",
"displayName": "Service Type",
"shape": "string"
},
{
"name": "configuredBy",
"displayName": "Configured By",
"shape": "string"
}
],
"timeframes": false
}
34 changes: 34 additions & 0 deletions plugins/Vercel/v1/dataStreams/domains.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "domains",
"displayName": "Domains",
"description": "Lists Vercel custom domains in the configured account or team",
"tags": ["Domain"],
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"endpointPath": "v5/domains",
"pathToData": "domains",
"paging": {
"mode": "token",
"pageSize": {
"realm": "queryArg",
"path": "limit",
"value": "100"
},
"in": { "realm": "payload", "path": "pagination.next" },
"out": { "realm": "queryArg", "path": "until" }
}
},
"matches": "none",
"metadata": [
{ "name": "id", "displayName": "ID", "visible": false },
{ "name": "name", "displayName": "Domain", "role": "label" },
{ "name": "verified", "displayName": "Verified" },
{ "name": "serviceType", "displayName": "Service Type" },
{ "name": "expiresAt", "displayName": "Expires", "shape": "date" },
{ "name": "boughtAt", "displayName": "Bought", "shape": "date" },
{ "name": "renew", "displayName": "Auto-renew" },
{ "name": "createdAt", "displayName": "Created", "shape": "date" }
],
"timeframes": false
}
69 changes: 69 additions & 0 deletions plugins/Vercel/v1/dataStreams/firewallEvents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"name": "firewallEvents",
"displayName": "Firewall Events",
"description": "Per-action firewall event counts over the timeframe for a single Vercel project",
"tags": ["Security", "Firewall"],
"baseDataSourceName": "httpRequestScopedSingle",
"config": {
"httpMethod": "get",
"endpointPath": "v1/security/firewall/events",
"getArgs": [
{ "key": "projectId", "value": "{{object.rawId}}" },
{
"key": "startTimestamp",
"value": "{{timeframe.unixStart * 1000}}"
},
{ "key": "endTimestamp", "value": "{{timeframe.unixEnd * 1000}}" }
],
"pathToData": "actions"
},
"matches": {
"sourceType": { "type": "equals", "value": "Project" }
},
"metadata": [
{
"name": "startTime",
"displayName": "Time",
"shape": "date",
"role": "timestamp"
},
{
"name": "action",
"displayName": "Action",
"shape": "string",
"role": "label"
},
{
"name": "count",
"displayName": "Count",
"shape": "number",
"role": "value"
},
{
"name": "host",
"displayName": "Host",
"shape": "string"
},
{
"name": "public_ip",
"displayName": "Public IP",
"shape": "string"
},
{
"name": "action_type",
"displayName": "Action Category",
"shape": "string",
"visible": false
},
{
"name": "isActive",
"displayName": "Active",
"shape": "boolean",
"visible": false
},
{
"pattern": ".*"
}
],
"timeframes": true
}
36 changes: 36 additions & 0 deletions plugins/Vercel/v1/dataStreams/projectInfo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "projectInfo",
"displayName": "Project Info",
"description": "Per-project detail (framework, Node version, timestamps, git repo) for a single Vercel project",
"tags": ["Project"],
"baseDataSourceName": "httpRequestScopedSingle",
"config": {
"httpMethod": "get",
"endpointPath": "v9/projects/{{object.rawId}}",
"expandInnerObjects": true
},
"matches": { "sourceType": { "type": "equals", "value": "Project" } },
"metadata": [
{
"name": "name",
"displayName": "Name",
"shape": "string",
"role": "label"
},
{ "name": "framework", "displayName": "Framework", "shape": "string" },
{
"name": "nodeVersion",
"displayName": "Node Version",
"shape": "string"
},
{ "name": "createdAt", "displayName": "Created", "shape": "date" },
{ "name": "updatedAt", "displayName": "Updated", "shape": "date" },
{ "name": "link.repo", "displayName": "Git Repo", "shape": "string" },
{
"name": "link.type",
"displayName": "Git Provider",
"shape": "string"
}
],
"timeframes": false
}
Loading
Loading