diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9690e211..4d1c5edc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -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 diff --git a/plugins/Vercel/v1/configValidation.json b/plugins/Vercel/v1/configValidation.json new file mode 100644 index 00000000..d5dc3a2a --- /dev/null +++ b/plugins/Vercel/v1/configValidation.json @@ -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." + } + ] +} diff --git a/plugins/Vercel/v1/custom_types.json b/plugins/Vercel/v1/custom_types.json new file mode 100644 index 00000000..9e98e362 --- /dev/null +++ b/plugins/Vercel/v1/custom_types.json @@ -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" + } +] diff --git a/plugins/Vercel/v1/dataStreams/activity.json b/plugins/Vercel/v1/dataStreams/activity.json new file mode 100644 index 00000000..8a8f8cd4 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/activity.json @@ -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}}" }, + { "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 +} diff --git a/plugins/Vercel/v1/dataStreams/currentUser.json b/plugins/Vercel/v1/dataStreams/currentUser.json new file mode 100644 index 00000000..4c2c42d1 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/currentUser.json @@ -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" } +} diff --git a/plugins/Vercel/v1/dataStreams/deployments.json b/plugins/Vercel/v1/dataStreams/deployments.json new file mode 100644 index 00000000..153fa2d7 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/deployments.json @@ -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 +} diff --git a/plugins/Vercel/v1/dataStreams/domainConfig.json b/plugins/Vercel/v1/dataStreams/domainConfig.json new file mode 100644 index 00000000..74902aa0 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/domainConfig.json @@ -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 +} diff --git a/plugins/Vercel/v1/dataStreams/domains.json b/plugins/Vercel/v1/dataStreams/domains.json new file mode 100644 index 00000000..fb5daf3c --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/domains.json @@ -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 +} diff --git a/plugins/Vercel/v1/dataStreams/firewallEvents.json b/plugins/Vercel/v1/dataStreams/firewallEvents.json new file mode 100644 index 00000000..4c9cff55 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/firewallEvents.json @@ -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 +} diff --git a/plugins/Vercel/v1/dataStreams/projectInfo.json b/plugins/Vercel/v1/dataStreams/projectInfo.json new file mode 100644 index 00000000..aa69f7ec --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/projectInfo.json @@ -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 +} diff --git a/plugins/Vercel/v1/dataStreams/projects.json b/plugins/Vercel/v1/dataStreams/projects.json new file mode 100644 index 00000000..44ba6496 --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/projects.json @@ -0,0 +1,37 @@ +{ + "name": "projects", + "displayName": "Projects", + "description": "Lists Vercel projects in the configured account or team", + "tags": ["Project"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v10/projects", + "pathToData": "projects", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "from" } + } + }, + "matches": "none", + "metadata": [ + { + "name": "id", + "displayName": "ID", + "role": "value", + "visible": false + }, + { "name": "name", "displayName": "Name", "role": "label" }, + { "name": "framework", "displayName": "Framework" }, + { "name": "nodeVersion", "displayName": "Node Version" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" }, + { "name": "updatedAt", "displayName": "Updated", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/scripts/currentUser.js b/plugins/Vercel/v1/dataStreams/scripts/currentUser.js new file mode 100644 index 00000000..d294752f --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/scripts/currentUser.js @@ -0,0 +1,2 @@ +// /v2/user returns a single { user: {...} } object — wrap it as one row. +result = data && data.user ? [data.user] : []; diff --git a/plugins/Vercel/v1/dataStreams/scripts/deployments.js b/plugins/Vercel/v1/dataStreams/scripts/deployments.js new file mode 100644 index 00000000..3313455a --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/scripts/deployments.js @@ -0,0 +1,33 @@ +// /v7/deployments returns { deployments: [...], pagination: {...} }. +// One row per deployment. Optional `project` objects-picker (stream ui name +// "project") arrives at context.config.project as an array (multi-select), each +const deployments = (data && data.deployments) || []; + +const selected = (context.config && context.config.project) || []; +const projectIds = new Set(selected.map((o) => o.rawId).filter(Boolean)); + +// Token paging walks `until` from pagination.next back to the `since` floor, so +// the first page starts at "now". For a historical timeframe (e.g. lastMonth) +// that over-fetches rows newer than the window end — bound the top here. +// (We can't set an `until` getArg: it collides with the paging `out=until`.) +const tf = context.timeframe || {}; +const untilMs = tf.unixEnd ? tf.unixEnd * 1000 : null; + +const scoped = deployments.filter( + (d) => + (!projectIds.size || projectIds.has(d.projectId)) && + (untilMs === null || d.created <= untilMs), +); + +result = scoped.map((d) => ({ + ...d, + uid: d.uid, + name: d.name, + state: d.readyState || d.state, + target: d.target || "preview", + projectId: d.projectId, + created: d.created, + inspectorUrl: d.inspectorUrl, + creator: (d.creator && (d.creator.username || d.creator.uid)) || null, +})); + diff --git a/plugins/Vercel/v1/dataStreams/teamMembers.json b/plugins/Vercel/v1/dataStreams/teamMembers.json new file mode 100644 index 00000000..6342de2b --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/teamMembers.json @@ -0,0 +1,43 @@ +{ + "name": "teamMembers", + "displayName": "Team Members", + "description": "Lists members of the team", + "tags": ["Team"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "v3/teams/{{object.rawId}}/members", + "pathToData": "members", + "paging": { + "mode": "token", + "pageSize": { + "realm": "queryArg", + "path": "limit", + "value": "100" + }, + "in": { "realm": "payload", "path": "pagination.next" }, + "out": { "realm": "queryArg", "path": "until" } + } + }, + "matches": { "sourceType": { "type": "equals", "value": "Team" } }, + "metadata": [ + { + "name": "uid", + "displayName": "ID", + "shape": "string", + "visible": false + }, + { "name": "name", "displayName": "Name", "shape": "string" }, + { + "name": "username", + "displayName": "Username", + "shape": "string", + "role": "label" + }, + { "name": "email", "displayName": "Email", "shape": "string" }, + { "name": "role", "displayName": "Role", "shape": "string" }, + { "name": "confirmed", "displayName": "Confirmed", "shape": "boolean" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/dataStreams/teams.json b/plugins/Vercel/v1/dataStreams/teams.json new file mode 100644 index 00000000..6acafe5f --- /dev/null +++ b/plugins/Vercel/v1/dataStreams/teams.json @@ -0,0 +1,28 @@ +{ + "name": "teams", + "displayName": "Teams", + "description": "Lists the Vercel teams the configured token can access", + "tags": ["Teams"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "v2/teams", + "pathToData": "teams", + "expandInnerObjects": true, + "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": "Name", "role": "label" }, + { "name": "slug", "displayName": "Slug" }, + { "name": "membership.role", "displayName": "Your Role" }, + { "name": "createdAt", "displayName": "Created", "shape": "date" } + ], + "timeframes": false +} diff --git a/plugins/Vercel/v1/defaultContent/activity.dash.json b/plugins/Vercel/v1/defaultContent/activity.dash.json new file mode 100644 index 00000000..d69c2ab1 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/activity.dash.json @@ -0,0 +1,182 @@ +{ + "name": "Activity & Team", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "version": 115, + "contents": [ + { + "static": false, + "w": 3, + "moved": false, + "h": 3, + "x": 0, + "y": 0, + "i": "4c0a11bb-6c6b-481b-951c-fe4586313ea4", + "z": 0, + "config": { + "dataStream": { + "name": "activity", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[activity]}}", + "sort": { + "by": [["createdAt", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "Account events", + "activePluginConfigIds": ["{{configId}}"], + "title": "Recent Activity", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "createdAt", + "type", + "text", + "actor" + ], + "hiddenColumns": [ + "id", + "user.username", + "user.email", + "userId" + ], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 1, + "moved": false, + "h": 3, + "x": 3, + "y": 0, + "i": "9838709b-83d2-4542-85a7-6458352f4d93", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "teamMembers", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[teamMembers]}}", + "sort": { + "by": [["name", "asc"]] + } + }, + "scope": { + "query": "g.V().order().by('__name').hasNot('__canonicalType').has(\"__configId\", within(\"{{configId}}\")).or(__.has(\"sourceType\", \"Team\")).limit(500)", + "bindings": {}, + "queryDetail": {} + }, + "_type": "tile/data-stream", + "description": "Members of the configured team", + "activePluginConfigIds": ["{{configId}}"], + "title": "Team Roster", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": ["username", "role"], + "hiddenColumns": [ + "uid", + "createdAt", + "email", + "name", + "confirmed" + ], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 4, + "moved": false, + "h": 1, + "x": 0, + "y": 3, + "i": "fada223a-5939-4b55-bc61-3ef63deca7d2", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "teams", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[teams]}}", + "sort": { + "by": [["name", "asc"]] + } + }, + "_type": "tile/data-stream", + "description": "Teams accessible to the configured token", + "activePluginConfigIds": ["{{configId}}"], + "title": "Teams", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "slug", + "membership.role", + "createdAt" + ], + "hiddenColumns": ["id"], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 4, + "moved": false, + "h": 3, + "x": 0, + "y": 4, + "i": "34bbffe0-fdcd-479a-8c54-7757030d354a", + "z": 0, + "config": { + "dataStream": { + "name": "activity", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[activity]}}", + "sort": {}, + "group": { + "by": [["createdAt", "byDay"]], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Daily Activity", + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "seriesColumn": "none", + "xAxisColumn": "createdAt_byDay", + "yAxisColumn": ["count"] + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/deployments.dash.json b/plugins/Vercel/v1/defaultContent/deployments.dash.json new file mode 100644 index 00000000..d1f0ba19 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/deployments.dash.json @@ -0,0 +1,289 @@ +{ + "name": "Deployments", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "version": 79, + "contents": [ + { + "w": 2, + "h": 3, + "x": 0, + "y": 0, + "z": 0, + "i": "8880ebf2-9570-488b-ab5f-6cfeed0c1727", + "moved": false, + "static": false, + "config": { + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [] + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "top": 10, + "by": [["created", "desc"]] + }, + "group": { + "by": [], + "aggregate": [] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Latest Builds (Top 10)", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "state", + "name", + "url", + "created", + "source", + "readyState", + "readySubstate", + "type", + "creator", + "inspectorUrl", + "attribution.commitMeta.name", + "attribution.commitMeta.email", + "attribution.commitMeta.isVerified", + "attribution.gitUser.id", + "attribution.gitUser.login", + "attribution.gitUser.type", + "attribution.gitUser.provider", + "attribution.vercelUser.id", + "attribution.vercelUser.username", + "attribution.vercelUser.teamRoles.0", + "meta.githubCommitAuthorName", + "meta.githubCommitAuthorEmail", + "meta.githubCommitMessage", + "meta.githubCommitOrg", + "meta.githubCommitRef", + "meta.githubCommitRepo", + "meta.githubCommitSha", + "meta.githubDeployment", + "meta.githubOrg", + "meta.githubRepo", + "meta.githubRepoOwnerType", + "meta.githubCommitRepoId", + "meta.githubRepoId", + "meta.githubRepoVisibility", + "meta.githubHost", + "meta.githubCommitAuthorLogin", + "meta.githubCommitVerification", + "meta.repoPushedAt", + "meta.branchAlias", + "meta.lambdaRuntimeStats", + "target", + "aliasError", + "aliasAssigned", + "isRollbackCandidate", + "createdAt", + "buildingAt", + "ready", + "projectSettings.commandForIgnoringBuildStep", + "meta.githubPrId", + "state[Expanded].rawState" + ], + "hiddenColumns": ["uid", "projectId"] + } + } + } + } + }, + { + "w": 2, + "h": 3, + "x": 2, + "y": 0, + "z": 0, + "i": "69617b87-c49a-43a7-a4ec-4b60e2cdd16e", + "moved": false, + "static": false, + "config": { + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "state", + "operation": "equals", + "value": "error" + } + ] + }, + "id": "{{dataStreams.[deployments]}}", + "group": { + "by": [], + "aggregate": [] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Latest Failed Builds", + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "stateColumn": "state", + "linkColumn": "inspectorUrl", + "sublabel": "url", + "labelColumn": "name" + } + } + } + } + }, + { + "w": 2, + "h": 3, + "x": 0, + "y": 3, + "z": 0, + "i": "ece1c11a-be71-4a6f-a1f2-ed56c15081d7", + "moved": false, + "static": false, + "config": { + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[deployments]}}", + "sort": { + "by": [["created_byDay", "asc"]] + }, + "group": { + "by": [ + ["created", "byDay"], + ["state", "uniqueValues"] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Deployments per day, split by state", + "activePluginConfigIds": ["{{configId}}"], + "title": "Deployments per Day", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "color": { + "type": "custom", + "customColors": [ + { + "color": "#2BB660", + "expression": "series == \"Success\"" + }, + { + "color": "#F2164D", + "expression": "series == \"Error\"" + } + ] + }, + "xAxisGroup": "state_uniqueValues", + "showLegend": true, + "range": { + "type": "auto" + }, + "showGrid": true, + "grouping": false, + "xAxisData": "created_byDay", + "displayMode": "actual", + "showTotals": true, + "yAxisLabel": "Deployments", + "horizontalLayout": "vertical", + "showValue": false, + "yAxisData": ["count"], + "showYAxisLabel": true, + "xAxisLabel": "", + "legendPosition": "bottom", + "showXAxisLabel": false + } + } + } + } + }, + { + "w": 2, + "h": 3, + "x": 2, + "y": 3, + "z": 0, + "i": "d2eaaf90-d384-478a-b14d-dc955bbdeb3e", + "moved": false, + "static": false, + "config": { + "title": "Average Build Times", + "description": "", + "_type": "tile/data-stream", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "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 \"ready\" - \"buildingAt\"\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\"", + "tables": [ + { + "tableName": "dataset1", + "config": { + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[deployments]}}", + "name": "deployments" + } + } + } + ] + }, + "metadata": [ + { + "shape": [ + "seconds", + { + "thousandsSeparator": true, + "formatDuration": true + } + ], + "name": "avg_build_time" + }, + { + "pattern": ".*" + } + ] + }, + "visualisation": { + "type": "data-stream-line-graph", + "config": { + "data-stream-line-graph": { + "xAxisColumn": "date", + "yAxisColumn": ["avg_build_time"], + "seriesColumn": "name", + "showLegend": true, + "legendPosition": "bottom", + "dataPoints": true + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/domain.dash.json b/plugins/Vercel/v1/defaultContent/domain.dash.json new file mode 100644 index 00000000..5e8cb0a8 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/domain.dash.json @@ -0,0 +1,98 @@ +{ + "name": "Domain", + "schemaVersion": "1.5", + "timeframe": "none", + "variables": ["{{variables.[Vercel Domain]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "2d1a775f-5d77-4246-a727-47b4b22c0cc7", + "x": 0, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Configuration Health", + "description": "DNS / nameserver configuration for this domain", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "{{dataStreams.[domainConfig]}}", + "name": "domainConfig", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Vercel Domains]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Vercel Domain]}}" + }, + "variables": ["{{variables.[Vercel Domain]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "columnOrder": ["misconfigured", "serviceType", "configuredBy"] + } + } + }, + "monitorOld": { + "_type": "simple", + "monitorType": "threshold", + "aggregation": "count", + "groupBy": "__group_by_none__", + "frequency": 15, + "tileRollsUp": true, + "condition": { + "columns": ["misconfigured"], + "logic": { "if": [{ "==": [{ "var": "misconfigured" }, true] }, "error"] } + } + } + } + }, + { + "i": "da5c6f6d-e225-4de1-90c8-873e444e2fb2", + "x": 2, + "y": 0, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Domain Details", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "timeframe": "none", + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Vercel Domains]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Vercel Domain]}}" + }, + "variables": ["{{variables.[Vercel Domain]}}"], + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true, + "hiddenColumns": ["id", "link", "links", "label"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Vercel/v1/defaultContent/manifest.json b/plugins/Vercel/v1/defaultContent/manifest.json new file mode 100644 index 00000000..f7f81e36 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/manifest.json @@ -0,0 +1,9 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "deployments", "type": "dashboard" }, + { "name": "activity", "type": "dashboard" }, + { "name": "project", "type": "dashboard" }, + { "name": "domain", "type": "dashboard" } + ] +} diff --git a/plugins/Vercel/v1/defaultContent/overview.dash.json b/plugins/Vercel/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..05694b48 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/overview.dash.json @@ -0,0 +1,198 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last7days", + "dashboard": { + "_type": "layout/grid", + "version": 95, + "contents": [ + { + "static": false, + "w": 1, + "moved": false, + "h": 3, + "x": 0, + "y": 0, + "i": "fa9e69dc-7d70-450b-b973-1a215df3a398", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "projects", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[projects]}}", + "sort": { + "by": [["count", "desc"]] + }, + "group": { + "by": [["framework", "uniqueValues"]], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Projects by Framework", + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "hideCenterValue": false, + "showValuesAsPercentage": false, + "legendPosition": "auto", + "legendMode": "table", + "labelColumn": "framework_uniqueValues" + } + } + } + } + }, + { + "static": false, + "w": 3, + "moved": false, + "h": 3, + "x": 1, + "y": 0, + "i": "3850d290-308d-44a1-81de-aeff5200d0ba", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "projects", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[projects]}}", + "sort": { + "by": [["updatedAt", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Projects", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "framework", + "nodeVersion", + "updatedAt" + ], + "hiddenColumns": [ + "id", + "sourceType", + "createdAt" + ], + "transpose": false + } + } + } + } + }, + { + "static": false, + "w": 1, + "moved": false, + "h": 2, + "x": 0, + "y": 3, + "i": "60fe71cb-be1f-4627-9eb7-c4aa7fe10feb", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "domains", + "pluginConfigId": "{{configId}}", + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "verified", + "operation": "equals", + "value": "false" + } + ] + }, + "id": "{{dataStreams.[domains]}}", + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Domains that are not verified", + "activePluginConfigIds": ["{{configId}}"], + "title": "Unverified Domains", + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Unverified" + } + } + } + } + }, + { + "static": false, + "w": 3, + "moved": false, + "h": 2, + "x": 1, + "y": 3, + "i": "f2843ac7-c436-4ad4-a6c8-5e5cb570420b", + "z": 0, + "config": { + "timeframe": "none", + "dataStream": { + "name": "domains", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[domains]}}", + "sort": { + "by": [["expiresAt", "asc"]] + } + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Domain Inventory", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "verified", + "serviceType", + "renew", + "expiresAt" + ], + "hiddenColumns": [ + "id", + "sourceType", + "boughtAt", + "createdAt" + ], + "transpose": false + } + } + } + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/project.dash.json b/plugins/Vercel/v1/defaultContent/project.dash.json new file mode 100644 index 00000000..d85a05ba --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/project.dash.json @@ -0,0 +1,356 @@ +{ + "name": "Project", + "schemaVersion": "1.5", + "timeframe": "last7days", + "variables": ["{{variables.[Vercel Project]}}"], + "dashboard": { + "_type": "layout/grid", + "version": 29, + "contents": [ + { + "w": 1, + "h": 2, + "x": 0, + "y": 0, + "z": 0, + "i": "05a07731-9c30-4908-95b4-94f9f345d54c", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "top": 1, + "by": [["created", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "Most recent deployment for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Latest Deployment State", + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "stateColumn": "state", + "linkColumn": "inspectorUrl", + "sublabel": "target", + "columns": 1, + "labelColumn": "name" + } + } + } + } + }, + { + "w": 1, + "h": 2, + "x": 1, + "y": 0, + "z": 0, + "i": "d04fd358-ff9f-41f2-a7ea-5398a4cd34b9", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "filter": { + "filters": [ + { + "column": "state", + "operation": "equals", + "value": "error" + } + ], + "multiOperation": "or" + }, + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "group": { + "by": [], + "aggregate": [] + }, + "sort": {} + }, + "_type": "tile/data-stream", + "description": "Deployments in an error state for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Failed Deployments", + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": { + "type": "count" + }, + "comparisonColumn": "none", + "label": "Failed" + } + } + } + } + }, + { + "w": 2, + "h": 2, + "x": 2, + "y": 0, + "z": 0, + "i": "508189d0-5357-4722-8701-09b082e20c35", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "by": [["created_byDay", "asc"]] + }, + "group": { + "by": [ + ["created", "byDay"], + ["state", "uniqueValues"] + ], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "_type": "tile/data-stream", + "description": "Deployments per day for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Deployment Volume", + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisGroup": "state_uniqueValues", + "showLegend": false, + "range": { + "type": "auto" + }, + "showGrid": true, + "grouping": false, + "xAxisData": "created_byDay", + "displayMode": "actual", + "yAxisLabel": "Deployments", + "horizontalLayout": "vertical", + "yAxisData": ["count"], + "showYAxisLabel": true, + "legendPosition": "bottom", + "showXAxisLabel": false, + "showValue": false, + "xAxisLabel": "", + "color": { + "type": "default" + }, + "showTotals": true + } + } + } + } + }, + { + "w": 4, + "h": 3, + "x": 0, + "y": 2, + "z": 0, + "i": "b2a0e5d2-746d-48e2-b294-0cd82105f86c", + "moved": false, + "static": false, + "config": { + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "deployments", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "project": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + } + }, + "id": "{{dataStreams.[deployments]}}", + "sort": { + "by": [["created", "desc"]] + } + }, + "_type": "tile/data-stream", + "description": "Deployments for this project", + "activePluginConfigIds": ["{{configId}}"], + "title": "Deployment History", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "created", + "name", + "state", + "target", + "creator", + "inspectorUrl" + ], + "hiddenColumns": ["uid", "projectId"], + "transpose": false + } + } + } + } + }, + { + "w": 1, + "h": 3, + "x": 0, + "y": 5, + "z": 0, + "i": "378b1b08-9b44-4564-a83f-f59f2d02b9f8", + "moved": false, + "static": false, + "config": { + "timeframe": "none", + "variables": ["{{variables.[Vercel Project]}}"], + "dataStream": { + "name": "projectInfo", + "pluginConfigId": "{{configId}}", + "id": "{{dataStreams.[projectInfo]}}" + }, + "scope": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + }, + "_type": "tile/data-stream", + "description": "", + "activePluginConfigIds": ["{{configId}}"], + "title": "Project Info", + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "columnOrder": [ + "name", + "framework", + "nodeVersion", + "link.repo", + "link.type", + "createdAt", + "updatedAt" + ], + "transpose": true + } + } + } + } + }, + { + "w": 1, + "h": 3, + "x": 1, + "y": 5, + "z": 0, + "i": "6a4aa9a9-0870-452b-a66b-83917f0641da", + "moved": false, + "static": false, + "config": { + "title": "Firewall Event Actions", + "description": "", + "_type": "tile/data-stream", + "dataStream": { + "id": "{{dataStreams.[firewallEvents]}}", + "name": "firewallEvents" + }, + "scope": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "action" + } + } + }, + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Vercel Project]}}"] + } + }, + { + "w": 2, + "h": 3, + "x": 2, + "y": 5, + "z": 0, + "i": "97508e56-5d5b-4716-9223-8a3903c5d3da", + "moved": false, + "static": false, + "config": { + "title": "Firewall Events", + "description": "", + "_type": "tile/data-stream", + "dataStream": { + "id": "{{dataStreams.[firewallEvents]}}", + "name": "firewallEvents" + }, + "scope": { + "variable": "{{variables.[Vercel Project]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Vercel Projects]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "hiddenColumns": ["isActive", "action_type"], + "columnOrder": [ + "action", + "count", + "host", + "startTime", + "endTime", + "public_ip" + ] + } + } + }, + "activePluginConfigIds": ["{{configId}}"], + "variables": ["{{variables.[Vercel Project]}}"] + } + } + ], + "columns": 4 + } +} diff --git a/plugins/Vercel/v1/defaultContent/scopes.json b/plugins/Vercel/v1/defaultContent/scopes.json new file mode 100644 index 00000000..da9b2912 --- /dev/null +++ b/plugins/Vercel/v1/defaultContent/scopes.json @@ -0,0 +1,26 @@ +[ + { + "name": "Vercel Projects", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Project"] } + }, + "variable": { + "name": "Vercel Project", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Vercel Domains", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Domain"] } + }, + "variable": { + "name": "Vercel Domain", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Vercel/v1/docs/README.md b/plugins/Vercel/v1/docs/README.md new file mode 100644 index 00000000..3a86a590 --- /dev/null +++ b/plugins/Vercel/v1/docs/README.md @@ -0,0 +1,47 @@ +## What this plugin monitors + +Monitor your [Vercel](https://vercel.com) projects, deployments, and domains in SquaredUp. This plugin connects to the Vercel REST API to import your projects and domains as objects you can scope dashboards to, and provides data streams for deployment health and history, domain verification and expiry, team membership and activity. + +- **Projects** — your Vercel projects, including framework, git repository, and latest production deployment status. Imported as objects. +- **Domains** — custom domains, including verification status, expiry/renewal dates, and configuration health. Imported as objects. +- **Deployments** — deployment volume, success/failure rates, and history over time, account-wide or drilled down per project. (Deployments are _not_ indexed as objects, because they change too frequently — they are available as data streams instead.) +- **Team & activity** — team membership roster and recent account activity (events). +- **Firewall / security** — per-project [Vercel Firewall (WAF)](https://vercel.com/docs/vercel-firewall) event activity: counts of firewall events broken down by action (e.g. allow / deny / challenge) over time. Available as data streams on the **Project** perspective's **Security** section. + +## Prerequisites + +1. Sign in to Vercel and open **Account Settings → Tokens** (). +2. Click **Create Token**. +3. Give it a name (e.g. `SquaredUp`). +4. **Scope** — choose the scope the token can access: + - To monitor a **Team**, set the scope to that team. + - To monitor your **personal account**, scope it to your personal account. +5. **Expiration** — choose an expiration (or no expiration). If the token expires, the plugin's data streams will stop returning data until you supply a new token. +6. Click **Create** and copy the token value immediately — Vercel only shows it once. + +For the **team members** streams, the token must belong to a Team and carry a role with billing/member visibility (Owner, Member, Developer, Security, or Billing). On personal/Hobby accounts these streams may return no data. + +## Configuration fields + +| Field | Required | Description | Where to find it | +| ------------- | -------- | -------------------------------------------------------------------------------------- | -------------------------------------- | +| **API token** | Yes | The Vercel Access Token used to authenticate. Sent as a bearer token on every request. | Account Settings → Tokens (see above). | + +## What gets indexed + +The plugin imports three object types into the SquaredUp graph: + +- **Vercel Project** — one object per project in the configured account/team. Carries the project name, framework, git repository link, and identifiers. +- **Vercel Domain** — one object per custom domain. Carries the domain name, verification status, and expiry information. +- **Vercel Team** - one object per team in the configured account, that the token has access to. + +Deployments, teams, members, and activity events are provided as **data streams** (not indexed objects) — query them on dashboards and scope deployment streams to a project. + +## 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. +- **Team members require a Team** and a token with adequate role/plan. On personal/Hobby accounts these streams may be empty. +- **Firewall events only appear for projects with the WAF configured.** The Firewall events stream returns rows only for projects that have the Vercel Firewall set up and that recorded events within the selected timeframe; a project with no firewall activity in that window shows an empty result (not an error). +- **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. diff --git a/plugins/Vercel/v1/icon.svg b/plugins/Vercel/v1/icon.svg new file mode 100644 index 00000000..73961ba7 --- /dev/null +++ b/plugins/Vercel/v1/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/Vercel/v1/indexDefinitions/default.json b/plugins/Vercel/v1/indexDefinitions/default.json new file mode 100644 index 00000000..8dbe16a8 --- /dev/null +++ b/plugins/Vercel/v1/indexDefinitions/default.json @@ -0,0 +1,54 @@ +{ + "steps": [ + { + "name": "projects", + "dataStream": { "name": "projects" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { + "value": "Project" + }, + "properties": [ + "framework", + "nodeVersion", + "createdAt", + "updatedAt" + ] + } + }, + { + "name": "domains", + "dataStream": { "name": "domains" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { + "value": "Domain" + }, + "properties": [ + "verified", + "boughtAt", + "expiresAt", + "renew", + "createdAt" + ] + } + }, + { + "name": "teams", + "dataStream": { "name": "teams" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { + "value": "Team" + }, + "properties": ["slug", "createdAt"] + } + } + ] +} diff --git a/plugins/Vercel/v1/metadata.json b/plugins/Vercel/v1/metadata.json new file mode 100644 index 00000000..bfeda2b8 --- /dev/null +++ b/plugins/Vercel/v1/metadata.json @@ -0,0 +1,47 @@ +{ + "name": "vercel", + "displayName": "Vercel", + "version": "1.0.0", + "author": { "name": "@andrewmumblebee", "type": "community" }, + "description": "Monitor your Vercel projects, deployments, and domains in SquaredUp - track deployment health and history, domains, team activity, and firewall (WAF) event activity.", + "category": "Cloud Platforms", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "vercel", + "deployments", + "hosting", + "frontend", + "projects", + "domains", + "security", + "firewall", + "waf" + ], + "objectTypes": ["Project", "Domain", "Team"], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Vercel/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Vercel", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.vercel.com", + "authMode": "none", + "headers": [ + { "key": "Authorization", "value": "Bearer {{accessToken}}" } + ] + } + } +} diff --git a/plugins/Vercel/v1/ui.json b/plugins/Vercel/v1/ui.json new file mode 100644 index 00000000..7ee8461a --- /dev/null +++ b/plugins/Vercel/v1/ui.json @@ -0,0 +1,9 @@ +[ + { + "type": "password", + "name": "accessToken", + "label": "API token", + "help": "A Vercel Access Token. Sent as a bearer token on every request. Create one at [Account Settings → Tokens](https://vercel.com/account/tokens).", + "validation": { "required": true } + } +]