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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/vercel-observability-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@executor-js/plugin-openapi": patch
---

The Vercel preset adds `logs.queryObservabilityEvents` (`POST /v1/observability/events`), the endpoint behind `vercel logs`. It searches runtime logs or CDN request records over a past time range, with filters and cursor paging. Vercel's published spec omits it, and its only runtime-log operation follows a live stream.
51 changes: 50 additions & 1 deletion packages/plugins/openapi/src/sdk/presets.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Option, Schema } from "effect";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

import { extract } from "./extract";
import { parse } from "./parse";
import { openApiPlugin } from "./plugin";
import { FIGMA_SPEC_OVERRIDES, FIGMA_SUPPORTED_OAUTH_SCOPES, openApiPresets } from "./presets";
import {
FIGMA_SPEC_OVERRIDES,
FIGMA_SUPPORTED_OAUTH_SCOPES,
VERCEL_SPEC_OVERRIDES,
openApiPresets,
} from "./presets";
import { applySpecOverrides } from "./spec-overrides";

const unsupportedFigmaOAuthScopes = [
"file_variables:read",
Expand All @@ -10,6 +21,12 @@ const unsupportedFigmaOAuthScopes = [
"library_analytics:read",
] as const;

const decodeJsonText = Schema.decodeUnknownSync(Schema.UnknownFromJsonString);
const encodeJsonText = Schema.encodeUnknownSync(Schema.UnknownFromJsonString);
const vercelSpec = decodeJsonText(
readFileSync(resolve(__dirname, "../../fixtures/vercel.json"), "utf-8"),
);

describe("OpenAPI presets", () => {
it("narrows Figma OAuth to the supported scope set", () => {
expect(FIGMA_SUPPORTED_OAUTH_SCOPES).toHaveLength(15);
Expand All @@ -34,4 +51,36 @@ describe("OpenAPI presets", () => {
expect(sdkPreset?.specOverrides).toEqual(FIGMA_SPEC_OVERRIDES);
expect(catalogPreset?.specOverrides).toEqual(FIGMA_SPEC_OVERRIDES);
});

it("projects Vercel spec overrides through the plugin catalog", () => {
const catalogPreset = openApiPlugin().integrationPresets?.find(
(preset) => preset.id === "vercel",
);

expect(catalogPreset?.specOverrides).toEqual(VERCEL_SPEC_OVERRIDES);
});

it.effect("adds the Vercel Observability events operation to the published spec", () =>
Effect.gen(function* () {
const patched = yield* applySpecOverrides(vercelSpec, VERCEL_SPEC_OVERRIDES);
const result = yield* extract(yield* parse(encodeJsonText(patched)));
const operation = result.operations.find(
(candidate) => candidate.operationId === "queryObservabilityEvents",
);

expect(operation?.method).toBe("post");
expect(operation?.pathTemplate).toBe("/v1/observability/events");
expect(
operation?.parameters.map((parameter) => [parameter.name, parameter.location]),
).toEqual([
["teamId", "query"],
["x-vercel-observability-query-reason", "header"],
]);
expect(operation && Option.isSome(operation.requestBody)).toBe(true);
expect(operation && Option.isSome(operation.outputSchema)).toBe(true);
expect(
result.operations.some((candidate) => candidate.operationId === "getRuntimeLogs"),
).toBe(true);
}),
);
});
134 changes: 134 additions & 0 deletions packages/plugins/openapi/src/sdk/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,139 @@ export const FIGMA_SPEC_OVERRIDES: SpecOverrides = [
},
];

/** Vercel's published spec lacks the Observability events endpoint the Vercel
* CLI uses for `vercel logs` (runtime logs and CDN requests over a past time
* range). Its only runtime-log operation, `getRuntimeLogs`, follows a live
* stream, so past logs are otherwise unreachable. The request and response
* shapes mirror the CLI's own client. */
export const VERCEL_SPEC_OVERRIDES: SpecOverrides = [
{
op: "add",
path: "/paths/~1v1~1observability~1events",
value: {
post: {
operationId: "queryObservabilityEvents",
summary: "Query observability events",
description:
"Returns runtime log entries (event `vercel.log`) or CDN request records (event `vercel.proxy_response`) for a time range, newest first by default. Page with `pagination.nextCursor`. This is the endpoint behind `vercel logs`.",
tags: ["logs"],
security: [{ bearerToken: [] }],
parameters: [
{
name: "teamId",
in: "query",
required: true,
description: "The Team identifier that owns the events. Must match `scope.ownerId`.",
schema: { type: "string", example: "team_1a2b3c4d5e6f7g8h9i0j1k2l" },
},
{
name: "x-vercel-observability-query-reason",
in: "header",
required: false,
description: "Free-form label for why the query runs, e.g. `agent/logs-runtime`.",
schema: { type: "string" },
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["event", "scope", "timeRange", "select"],
properties: {
event: {
type: "string",
enum: ["vercel.log", "vercel.proxy_response"],
description:
"`vercel.log` for runtime log entries, `vercel.proxy_response` for CDN request records.",
},
scope: {
type: "object",
required: ["ownerId"],
properties: {
ownerId: { type: "string", description: "The Team identifier." },
projectIds: { type: "array", items: { type: "string" } },
},
},
timeRange: {
type: "object",
required: ["start", "end"],
properties: {
start: { type: "string", format: "date-time" },
end: { type: "string", format: "date-time" },
},
},
select: {
type: "array",
items: { type: "string" },
description:
"Fields to return, e.g. `level`, `message`, `source`, `environment`, `deploymentId`, `requestId`, `requestPath`.",
},
filter: {
type: "string",
description:
'KQL-style filter, e.g. `level:warning AND environment:production AND "some text"`.',
},
order: { type: "string", enum: ["asc", "desc"], default: "desc" },
limit: { type: "integer", minimum: 1, maximum: 1000 },
cursor: {
type: "string",
description: "`pagination.nextCursor` from the previous page.",
},
},
},
},
},
},
responses: {
"200": {
description: "One page of events.",
content: {
"application/json": {
schema: {
type: "object",
required: ["events", "pagination"],
properties: {
events: {
type: "array",
items: { type: "object", additionalProperties: true },
},
pagination: {
type: "object",
required: ["hasMore", "nextCursor"],
properties: {
hasMore: { type: "boolean" },
nextCursor: { type: "string", nullable: true },
},
},
meta: {
type: "object",
additionalProperties: true,
properties: {
excludedIntervals: {
type: "array",
items: {
type: "object",
properties: {
start: { type: "string" },
end: { type: "string" },
},
},
},
},
},
},
},
},
},
},
},
},
},
},
];

const openApiOnlyPresets: readonly OpenApiPreset[] = [
{
id: "figma",
Expand Down Expand Up @@ -92,6 +225,7 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [
url: "https://openapi.vercel.sh",
icon: "https://integrations.sh/logo/vercel.com",
featured: true,
specOverrides: VERCEL_SPEC_OVERRIDES,
},
{
id: "neon",
Expand Down
Loading