Openstatus www.openstatus.dev

🧹 some cleaning and improvments (#633)

authored by

Thibault Le Ouay and committed by
GitHub
6aa605e6 324339d2

+12 -1230
+3 -1
.github/workflows/lint.yml
··· 3 3 on: 4 4 push: 5 5 branches: 6 - - "*" 6 + - "main" 7 + pull_request: 8 + branches: [main] 7 9 8 10 jobs: 9 11 lint:
+3 -1
.github/workflows/test.yml
··· 3 3 on: 4 4 push: 5 5 branches: 6 - - "*" 6 + - "main" 7 + pull_request: 8 + branches: [main] 7 9 8 10 jobs: 9 11 tests:
+3
apps/server/src/checker/alerting.ts
··· 38 38 .where(eq(schema.monitor.id, Number(monitorId))) 39 39 .all(); 40 40 for (const notif of notifications) { 41 + console.log( 42 + `💌 sending notification for ${monitorId} and chanel ${notif.notification.provider}`, 43 + ); 41 44 const monitor = selectMonitorSchema.parse(notif.monitor); 42 45 await providerToFunction[notif.notification.provider]({ 43 46 monitor,
+1 -10
apps/web/.env.example
··· 51 51 52 52 NEXT_PUBLIC_SENTRY_DSN='' 53 53 54 - # Vercel Log Drains 55 - INTEGRATION_SECRET= 56 - LOG_DRAIN_SECRET= 57 - 58 - # Marketplace Integration 59 - VERCEL_CLIENT_ID= 60 - VERCEL_CLIENT_SECRET= 61 - VERCEL_REDIRECT_URI= 62 - AES_KEY= 63 54 # Unkey 64 55 UNKEY_API_ID= 65 56 UNKEY_TOKEN= ··· 73 64 74 65 EXTERNAL_API_URL= 75 66 76 - PLAYGROUND_UNKEY_API_KEY= 67 + PLAYGROUND_UNKEY_API_KEY=
-1
apps/web/package.json
··· 27 27 "@openstatus/ui": "workspace:*", 28 28 "@openstatus/upstash": "workspace:*", 29 29 "@openstatus/utils": "workspace:*", 30 - "@openstatus/vercel": "workspace:*", 31 30 "@sentry/integrations": "7.73.0", 32 31 "@sentry/nextjs": "7.73.0", 33 32 "@stripe/stripe-js": "2.1.6",
-1
apps/web/src/env.ts
··· 3 3 4 4 import "@openstatus/db/env.mjs"; 5 5 import "@openstatus/analytics/env"; 6 - import "@openstatus/vercel/env"; 7 6 8 7 export const env = createEnv({ 9 8 server: {
-12
packages/integrations/vercel/.env.example
··· 1 - # Log Drains 2 - # add from the first form - 'x-vercel-verify' 3 - INTEGRATION_SECRET= 4 - # after verify, in the modal box - 'x-vercel-signature' 5 - LOG_DRAIN_SECRET= 6 - 7 - 8 - # Marketplace Integration 9 - VERCEL_CLIENT_ID= 10 - VERCEL_CLIENT_SECRET= 11 - VERCEL_REDIRECT_URI= 12 - AES_KEY=
-60
packages/integrations/vercel/README.md
··· 1 - # Log drain integration for Vercel 2 - 3 - Seamless installement into Vercel's log drains for your project. 4 - 5 - > Inspired by 6 - > [@valeriangalliat/vercel-custom-log-drain](https://github.com/valeriangalliat/vercel-custom-log-drain). 7 - 8 - ## Description 9 - 10 - When integrating the Vercel App into your project. Every request to 11 - [openstatus.dev](https://openstatus.dev) will include the key inside of the 12 - `Headers`. 13 - 14 - Once we have the `access_token` token we will be able to `createLogDrain` as 15 - authentificated user. The token is only expires after 30 minutes and is required 16 - for the user to access the integration within vercel. 17 - 18 - When creating the log drain, we should include a custom header to it, with an 19 - `OpenStatus-Vercel-TOKEN: "os_xxx-yyy"` token from Unkey. That way, it is easy 20 - to revoke tokens. We can test without but should include it. Check with Andreas 21 - how a good use case an look like. 22 - 23 - The new created log drain will point towards our `/api/integrations/vercel`. We 24 - will then be able to filter them and ingest Tinybird the the logs we want to 25 - keep. Maybe, we can start first by ingesting only errors. 26 - 27 - 1. We can start simple an only create the log drain for the user. nothing else 28 - 2. Once clear we will have to create a little `/configure` page where the user, 29 - authentificated within vercel, can update the log drain integration easily. 30 - 31 - The UI for the Integration should be the bare minimum. The UX should be in the 32 - focus. Default shadcn/ui components will do it. 33 - 34 - - white background 35 - - `DataTable` filled with `getLogDrains` data: 36 - - createdAt 37 - - name 38 - - projectId 39 - - dropdown menu: data can be deleted 40 - - 'Connect'-button to add log drains to different projet 41 - 42 - <!-- Redirect URL aka callback explaination --> 43 - 44 - ### Files 45 - 46 - - `webhook.ts`: ingests Tinybird with the log drains 47 - - `callback.ts`: callback for getting an access token 48 - - `configure.ts`: simple page to configure your vercel integration authorized as 49 - vercel user through access token 50 - - `client.ts`: includes all important REST API calls to vercel, updating the 51 - users project 52 - 53 - ### API Scopes 54 - 55 - The following scopes are necessary: 56 - 57 - - Log Drains (Read/Write): be able to send a request with the payload 58 - - Projects (Read): connect log drains with projects 59 - 60 - > We might require more scopes.
-13
packages/integrations/vercel/env.ts
··· 1 - import { createEnv } from "@t3-oss/env-core"; 2 - import { z } from "zod"; 3 - 4 - export const env = createEnv({ 5 - server: { 6 - INTEGRATION_SECRET: z.string().min(1), 7 - TINY_BIRD_API_KEY: z.string().min(1), 8 - }, 9 - runtimeEnv: { 10 - INTEGRATION_SECRET: process.env.INTEGRATION_SECRET, 11 - TINY_BIRD_API_KEY: process.env.TINY_BIRD_API_KEY, 12 - }, 13 - });
-21
packages/integrations/vercel/package.json
··· 1 - { 2 - "name": "@openstatus/vercel", 3 - "version": "0.0.0", 4 - "main": "src/index.ts", 5 - "description": "Log drains Vercel integration.", 6 - "dependencies": { 7 - "@openstatus/tinybird": "workspace:*", 8 - "@t3-oss/env-core": "0.7.0", 9 - "react": "18.2.0", 10 - "react-dom": "18.2.0", 11 - "zod": "^3.22.2" 12 - }, 13 - "devDependencies": { 14 - "@openstatus/tsconfig": "workspace:*", 15 - "@types/node": "20.8.0", 16 - "@types/react": "18.2.24", 17 - "@types/react-dom": "18.2.8", 18 - "next": "13.5.3", 19 - "typescript": "5.2.2" 20 - } 21 - }
-3
packages/integrations/vercel/src/components/integration-card.tsx
··· 1 - export const VercelIntegrationCard = () => { 2 - return <></>; 3 - };
-22
packages/integrations/vercel/src/components/submit-button.tsx
··· 1 - "use client"; 2 - 3 - import { experimental_useFormStatus as useFormStatus } from "react-dom"; 4 - 5 - export function SubmitButton({ 6 - disabled, 7 - children, 8 - }: { 9 - disabled?: boolean; 10 - children: React.ReactNode; 11 - }) { 12 - const { pending } = useFormStatus(); 13 - return ( 14 - <button 15 - type="submit" 16 - disabled={pending || disabled} 17 - className="bg-foreground text-background rounded-md px-2 py-1 disabled:cursor-not-allowed disabled:opacity-60" 18 - > 19 - {pending ? "Loading" : children} 20 - </button> 21 - ); 22 - }
-4
packages/integrations/vercel/src/index.ts
··· 1 - export * from "./libs/schema"; 2 - export * from "./routes/webhook"; 3 - export * from "./routes/callback"; 4 - export * from "./pages/configure";
-157
packages/integrations/vercel/src/libs/client.ts
··· 1 - // CREDITS: https://github.com/valeriangalliat/vercel-custom-log-drain/blob/43043c095475c9fac279e5fec8976497ee1ea9b6/clients/vercel.ts 2 - 3 - import { projectsSchema } from "./schema"; 4 - 5 - async function fetchOk( 6 - url: string, 7 - init?: RequestInit | undefined, 8 - ): Promise<Response> { 9 - const res = await fetch(url, init); 10 - 11 - if (!res.ok) { 12 - const contentType = res.headers.get("content-type"); 13 - let body: string | object | null; 14 - 15 - try { 16 - const isJson = contentType && contentType.includes("json"); 17 - body = await (isJson ? res.json() : res.text()); 18 - } catch (err) { 19 - body = null; 20 - } 21 - 22 - console.log({ body }); 23 - 24 - throw Object.assign(new Error(`Failed to fetch: ${url}`), { 25 - res, 26 - body, 27 - }); 28 - } 29 - 30 - return res; 31 - } 32 - 33 - const base = "https://api.vercel.com"; 34 - 35 - export async function getToken(code: string): Promise<string> { 36 - const url = `${base}/v2/oauth/access_token`; 37 - 38 - const res = await fetchOk(url, { 39 - method: "POST", 40 - headers: { 41 - "Content-Type": "application/x-www-form-urlencoded", 42 - }, 43 - body: new URLSearchParams({ 44 - client_id: process.env.VERCEL_CLIENT_ID || "", 45 - client_secret: process.env.VERCEL_CLIENT_SECRET || "", 46 - code, 47 - redirect_uri: process.env.VERCEL_REDIRECT_URI || "", 48 - }), 49 - }); 50 - 51 - const json = await res.json(); 52 - 53 - return json.access_token; 54 - } 55 - 56 - // Type from <https://vercel.com/docs/log-drains> 57 - export type LogDrain = { 58 - /** The oauth2 client application id that created this log drain */ 59 - clientId: string; 60 - /** The client configuration this log drain was created with */ 61 - configurationId: string; 62 - /** A timestamp that tells you when the log drain was created */ 63 - createdAt: number; 64 - /** The unique identifier of the log drain. Always prefixed with `ld_` */ 65 - id: string; 66 - /** The type of log format */ 67 - deliveryFormat: "json" | "ndjson" | "syslog"; 68 - /** The name of the log drain */ 69 - name: string; 70 - /** The identifier of the team or user whose events will trigger the log drain */ 71 - ownerId: string; 72 - /** The identifier of the project this log drain is associated with */ 73 - projectId?: string | null; 74 - /** The URL to call when logs are generated */ 75 - url: string; 76 - /** TODO: add correct description and check if correct */ 77 - headers?: Record<string, string>; 78 - /** The sources from which logs are currently being delivered to this log drain */ 79 - sources?: ( 80 - | "static" 81 - | "lambda" 82 - | "build" 83 - | "edge" 84 - | "external" 85 - | "deployment" 86 - )[]; // REMINDER: creating a log drain with "deployment" source won't work 87 - }; 88 - 89 - function getQuery(teamId?: string) { 90 - return teamId ? `?teamId=${encodeURIComponent(teamId)}` : ""; 91 - } 92 - 93 - export async function getLogDrains( 94 - token: string, 95 - teamId?: string, 96 - ): Promise<LogDrain[]> { 97 - const url = `${base}/v2/integrations/log-drains${getQuery(teamId)}`; 98 - 99 - const res = await fetchOk(url, { 100 - headers: { 101 - Authorization: `Bearer ${token}`, 102 - }, 103 - }); 104 - 105 - return await res.json(); 106 - } 107 - 108 - export async function getProjects(token: string, teamId?: string) { 109 - const url = `${base}/v9/projects${getQuery(teamId)}`; 110 - 111 - const res = await fetchOk(url, { 112 - headers: { 113 - Authorization: `Bearer ${token}`, 114 - }, 115 - }); 116 - 117 - const json = await res.json(); 118 - console.log(json); 119 - return projectsSchema.parse(json); 120 - } 121 - 122 - export async function createLogDrain( 123 - token: string, 124 - logDrain: LogDrain, 125 - teamId?: string, 126 - ): Promise<LogDrain> { 127 - const url = `${base}/v2/integrations/log-drains${getQuery(teamId)}`; 128 - 129 - console.log({ token, logDrain, teamId }); 130 - 131 - const res = await fetchOk(url, { 132 - method: "post", 133 - headers: { 134 - Authorization: `Bearer ${token}`, 135 - }, 136 - body: JSON.stringify(logDrain), 137 - }); 138 - 139 - return await res.json(); 140 - } 141 - 142 - export async function deleteLogDrain( 143 - token: string, 144 - id: string, 145 - teamId?: string, 146 - ): Promise<void> { 147 - const url = `${base}/v1/integrations/log-drains/${encodeURIComponent( 148 - id, 149 - )}${getQuery(teamId)}`; 150 - 151 - await fetchOk(url, { 152 - method: "delete", 153 - headers: { 154 - Authorization: `Bearer ${token}`, 155 - }, 156 - }); 157 - }
-16
packages/integrations/vercel/src/libs/crypto.ts
··· 1 - // CREDITS: https://github.com/valeriangalliat/vercel-custom-log-drain/blob/master/utils/crypto.ts 2 - 3 - import crypto from "crypto"; 4 - 5 - /** generate key via `node -p "crypto.randomBytes(32).toString('base64url')"` */ 6 - const key = Buffer.from(process.env.AES_KEY || "", "base64url"); 7 - 8 - export function encrypt(iv: Buffer, data: Buffer): Buffer { 9 - const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); 10 - return Buffer.concat([cipher.update(data), cipher.final()]); 11 - } 12 - 13 - export function decrypt(iv: Buffer, encrypted: Buffer): Buffer { 14 - const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); 15 - return Buffer.concat([decipher.update(encrypted), decipher.final()]); 16 - }
-613
packages/integrations/vercel/src/libs/schema.ts
··· 1 - import * as z from "zod"; 2 - 3 - /** 4 - * If the response of the request returns an HTTP statusCode with a value of -1, 5 - * that means there was no response returned and the lambda crashed. 6 - * In the same response, if the value of proxy.statusCode is returned with -1, 7 - * that means the revalidation occurred in the background. 8 - */ 9 - 10 - // https://vercel.com/docs/observability/log-drains-overview/log-drains-reference#json-log-drains 11 - export const logDrainSchema = z.object({ 12 - id: z.string().optional(), 13 - timestamp: z.number().optional(), 14 - type: z 15 - .enum([ 16 - "middleware-invocation", 17 - "stdout", 18 - "stderr", 19 - "edge-function-invocation", 20 - "fatal", 21 - ]) 22 - .optional(), 23 - edgeType: z.enum(["edge-function", "middleware"]).optional(), 24 - requestId: z.string().optional(), 25 - statusCode: z.number().optional(), 26 - message: z.string().optional(), 27 - projectId: z.string().optional(), 28 - deploymentId: z.string().optional(), 29 - buildId: z.string().optional(), 30 - source: z.enum(["external", "lambda", "edge", "static", "build"]), 31 - host: z.string().optional(), 32 - environment: z.string().optional(), 33 - branch: z.string().optional(), 34 - destination: z.string().optional(), 35 - path: z.string().optional(), 36 - entrypoint: z.string().optional(), 37 - proxy: z 38 - .object({ 39 - timestamp: z.number().optional(), 40 - region: z.string().optional(), // TODO: use regions enum? 41 - method: z.string().optional(), // TODO: use methods enum? 42 - vercelCache: z.string().optional(), // TODO: use "HIT" / "MISS" enum? 43 - statusCode: z.number().optional(), 44 - path: z.string().optional(), 45 - host: z.string().optional(), 46 - scheme: z.string().optional(), 47 - clientIp: z.string().optional(), 48 - userAgent: z.array(z.string()).optional(), 49 - }) 50 - .optional(), 51 - }); 52 - 53 - export const logDrainSchemaArray = z.array(logDrainSchema); 54 - 55 - export const projectSchema = z.object({ 56 - accountId: z.string(), 57 - analytics: z 58 - .object({ 59 - id: z.string(), 60 - canceledAt: z.number().nullable(), 61 - disabledAt: z.number(), 62 - enabledAt: z.number(), 63 - paidAt: z.number().optional(), 64 - sampleRatePercent: z.number().optional().nullable(), 65 - spendLimitInDollars: z.number().optional().nullable(), 66 - }) 67 - .optional(), 68 - autoExposeSystemEnvs: z.boolean().optional(), 69 - autoAssignCustomDomains: z.boolean().optional(), 70 - autoAssignCustomDomainsUpdatedBy: z.string().optional(), 71 - buildCommand: z.string().optional().nullable(), 72 - commandForIgnoringBuildStep: z.string().optional().nullable(), 73 - connectConfigurationId: z.string().optional().nullable(), 74 - connectBuildsEnabled: z.boolean().optional(), 75 - createdAt: z.number().optional(), 76 - customerSupportCodeVisibility: z.boolean().optional(), 77 - crons: z 78 - .object({ 79 - enabledAt: z.number(), 80 - disabledAt: z.number().nullable(), 81 - updatedAt: z.number(), 82 - deploymentId: z.string().nullable(), 83 - definitions: z.array( 84 - z.object({ 85 - host: z.string(), 86 - path: z.string(), 87 - schedule: z.string(), 88 - }), 89 - ), 90 - }) 91 - .optional(), 92 - dataCache: z 93 - .object({ 94 - userDisabled: z.boolean(), 95 - storageSizeBytes: z.number().optional().nullable(), 96 - unlimited: z.boolean().optional(), 97 - }) 98 - .optional(), 99 - devCommand: z.string().optional().nullable(), 100 - directoryListing: z.boolean(), 101 - installCommand: z.string().optional().nullable(), 102 - env: z 103 - .array( 104 - z.object({ 105 - target: z 106 - .union([ 107 - z.array( 108 - z.union([ 109 - z.literal("production"), 110 - z.literal("preview"), 111 - z.literal("development"), 112 - z.literal("preview"), 113 - z.literal("development"), 114 - ]), 115 - ), 116 - z.union([ 117 - z.literal("production"), 118 - z.literal("preview"), 119 - z.literal("development"), 120 - z.literal("preview"), 121 - z.literal("development"), 122 - ]), 123 - ]) 124 - .optional(), 125 - type: z.union([ 126 - z.literal("secret"), 127 - z.literal("system"), 128 - z.literal("encrypted"), 129 - z.literal("plain"), 130 - z.literal("sensitive"), 131 - ]), 132 - id: z.string().optional(), 133 - key: z.string(), 134 - value: z.string(), 135 - configurationId: z.string().optional().nullable(), 136 - createdAt: z.number().optional(), 137 - updatedAt: z.number().optional(), 138 - createdBy: z.string().optional().nullable(), 139 - updatedBy: z.string().optional().nullable(), 140 - gitBranch: z.string().optional(), 141 - edgeConfigId: z.string().optional().nullable(), 142 - edgeConfigTokenId: z.string().optional().nullable(), 143 - contentHint: z 144 - .union([ 145 - z.object({ 146 - type: z.literal("redis-url"), 147 - storeId: z.string(), 148 - }), 149 - z.object({ 150 - type: z.literal("redis-rest-api-url"), 151 - storeId: z.string(), 152 - }), 153 - z.object({ 154 - type: z.literal("redis-rest-api-token"), 155 - storeId: z.string(), 156 - }), 157 - z.object({ 158 - type: z.literal("redis-rest-api-read-only-token"), 159 - storeId: z.string(), 160 - }), 161 - z.object({ 162 - type: z.literal("blob-read-write-token"), 163 - storeId: z.string(), 164 - }), 165 - z.object({ 166 - type: z.literal("postgres-url"), 167 - storeId: z.string(), 168 - }), 169 - z.object({ 170 - type: z.literal("postgres-url-non-pooling"), 171 - storeId: z.string(), 172 - }), 173 - z.object({ 174 - type: z.literal("postgres-prisma-url"), 175 - storeId: z.string(), 176 - }), 177 - z.object({ 178 - type: z.literal("postgres-user"), 179 - storeId: z.string(), 180 - }), 181 - z.object({ 182 - type: z.literal("postgres-host"), 183 - storeId: z.string(), 184 - }), 185 - z.object({ 186 - type: z.literal("postgres-password"), 187 - storeId: z.string(), 188 - }), 189 - z.object({ 190 - type: z.literal("postgres-database"), 191 - storeId: z.string(), 192 - }), 193 - ]) 194 - .optional(), 195 - decrypted: z.boolean().optional(), 196 - }), 197 - ) 198 - .optional(), 199 - framework: z 200 - .union([ 201 - z.literal("blitzjs"), 202 - z.literal("nextjs"), 203 - z.literal("gatsby"), 204 - z.literal("remix"), 205 - z.literal("astro"), 206 - z.literal("hexo"), 207 - z.literal("eleventy"), 208 - z.literal("docusaurus-2"), 209 - z.literal("docusaurus"), 210 - z.literal("preact"), 211 - z.literal("solidstart"), 212 - z.literal("dojo"), 213 - z.literal("ember"), 214 - z.literal("vue"), 215 - z.literal("scully"), 216 - z.literal("ionic-angular"), 217 - z.literal("angular"), 218 - z.literal("polymer"), 219 - z.literal("svelte"), 220 - z.literal("sveltekit"), 221 - z.literal("sveltekit-1"), 222 - z.literal("ionic-react"), 223 - z.literal("create-react-app"), 224 - z.literal("gridsome"), 225 - z.literal("umijs"), 226 - z.literal("sapper"), 227 - z.literal("saber"), 228 - z.literal("stencil"), 229 - z.literal("nuxtjs"), 230 - z.literal("redwoodjs"), 231 - z.literal("hugo"), 232 - z.literal("jekyll"), 233 - z.literal("brunch"), 234 - z.literal("middleman"), 235 - z.literal("zola"), 236 - z.literal("hydrogen"), 237 - z.literal("vite"), 238 - z.literal("vitepress"), 239 - z.literal("vuepress"), 240 - z.literal("parcel"), 241 - z.literal("sanity"), 242 - z.literal("storybook"), 243 - ]) 244 - .optional(), 245 - gitForkProtection: z.boolean().optional(), 246 - gitLFS: z.boolean().optional(), 247 - id: z.string(), 248 - latestDeployments: z 249 - .array( 250 - z.object({ 251 - alias: z.array(z.string()).optional(), 252 - aliasAssigned: z.union([z.number(), z.boolean(), z.null()]).nullish(), 253 - aliasError: z 254 - .object({ 255 - code: z.string(), 256 - message: z.string(), 257 - }) 258 - .optional() 259 - .nullable(), 260 - aliasFinal: z.string().optional().nullable(), 261 - automaticAliases: z.array(z.string()).optional(), 262 - builds: z 263 - .array( 264 - z.object({ 265 - use: z.string(), 266 - src: z.string().optional(), 267 - dest: z.string().optional(), 268 - }), 269 - ) 270 - .optional(), 271 - connectBuildsEnabled: z.boolean().optional(), 272 - connectConfigurationId: z.string().optional(), 273 - createdAt: z.number(), 274 - createdIn: z.string(), 275 - creator: z 276 - .object({ 277 - email: z.string(), 278 - githubLogin: z.string().optional(), 279 - gitlabLogin: z.string().optional(), 280 - uid: z.string(), 281 - username: z.string(), 282 - }) 283 - .nullable(), 284 - deploymentHostname: z.string(), 285 - name: z.string(), 286 - forced: z.boolean().optional(), 287 - id: z.string(), 288 - meta: z.record(z.string()).optional(), 289 - monorepoManager: z.string().optional().nullable(), 290 - plan: z.union([ 291 - z.literal("pro"), 292 - z.literal("enterprise"), 293 - z.literal("hobby"), 294 - z.literal("oss"), 295 - ]), 296 - private: z.boolean(), 297 - readyState: z.union([ 298 - z.literal("BUILDING"), 299 - z.literal("ERROR"), 300 - z.literal("INITIALIZING"), 301 - z.literal("QUEUED"), 302 - z.literal("READY"), 303 - z.literal("CANCELED"), 304 - ]), 305 - readySubstate: z 306 - .union([z.literal("STAGED"), z.literal("PROMOTED")]) 307 - .optional(), 308 - requestedAt: z.number().optional(), 309 - target: z.string().optional().nullable(), 310 - teamId: z.string().optional().nullable(), 311 - type: z.literal("LAMBDAS"), 312 - url: z.string(), 313 - userId: z.string(), 314 - withCache: z.boolean().optional(), 315 - checksConclusion: z 316 - .union([ 317 - z.literal("succeeded"), 318 - z.literal("failed"), 319 - z.literal("skipped"), 320 - z.literal("canceled"), 321 - ]) 322 - .optional(), 323 - checksState: z 324 - .union([ 325 - z.literal("registered"), 326 - z.literal("running"), 327 - z.literal("completed"), 328 - ]) 329 - .optional(), 330 - readyAt: z.number().optional(), 331 - buildingAt: z.number().optional(), 332 - previewCommentsEnabled: z.boolean().optional(), 333 - }), 334 - ) 335 - .optional(), 336 - link: z 337 - .union([ 338 - z.object({ 339 - org: z.string().optional(), 340 - repo: z.string().optional(), 341 - repoId: z.number().optional(), 342 - type: z.literal("github").optional(), 343 - createdAt: z.number().optional(), 344 - deployHooks: z.array( 345 - z.object({ 346 - createdAt: z.number().optional(), 347 - id: z.string(), 348 - name: z.string(), 349 - ref: z.string(), 350 - url: z.string(), 351 - }), 352 - ), 353 - gitCredentialId: z.string().optional(), 354 - updatedAt: z.number().optional(), 355 - sourceless: z.boolean().optional(), 356 - productionBranch: z.string().optional(), 357 - }), 358 - z.object({ 359 - projectId: z.string().optional(), 360 - projectName: z.string().optional(), 361 - projectNameWithNamespace: z.string().optional(), 362 - projectNamespace: z.string().optional(), 363 - projectUrl: z.string().optional(), 364 - type: z.literal("gitlab").optional(), 365 - createdAt: z.number().optional(), 366 - deployHooks: z.array( 367 - z.object({ 368 - createdAt: z.number().optional(), 369 - id: z.string(), 370 - name: z.string(), 371 - ref: z.string(), 372 - url: z.string(), 373 - }), 374 - ), 375 - gitCredentialId: z.string().optional(), 376 - updatedAt: z.number().optional(), 377 - sourceless: z.boolean().optional(), 378 - productionBranch: z.string().optional(), 379 - }), 380 - z.object({ 381 - name: z.string().optional(), 382 - slug: z.string().optional(), 383 - owner: z.string().optional(), 384 - type: z.literal("bitbucket").optional(), 385 - uuid: z.string().optional(), 386 - workspaceUuid: z.string().optional(), 387 - createdAt: z.number().optional(), 388 - deployHooks: z.array( 389 - z.object({ 390 - createdAt: z.number().optional(), 391 - id: z.string(), 392 - name: z.string(), 393 - ref: z.string(), 394 - url: z.string(), 395 - }), 396 - ), 397 - gitCredentialId: z.string().optional(), 398 - updatedAt: z.number().optional(), 399 - sourceless: z.boolean().optional(), 400 - productionBranch: z.string().optional(), 401 - }), 402 - ]) 403 - .optional(), 404 - name: z.string(), 405 - nodeVersion: z.union([ 406 - z.literal("18.x"), 407 - z.literal("16.x"), 408 - z.literal("14.x"), 409 - z.literal("12.x"), 410 - z.literal("10.x"), 411 - ]), 412 - outputDirectory: z.string().optional().nullable(), 413 - passwordProtection: z.record(z.unknown()).optional().nullable(), 414 - productionDeploymentsFastLane: z.boolean().optional(), 415 - publicSource: z.boolean().optional().nullable(), 416 - rootDirectory: z.string().optional().nullable(), 417 - serverlessFunctionRegion: z.string().optional().nullable(), 418 - skipGitConnectDuringLink: z.boolean().optional(), 419 - sourceFilesOutsideRootDirectory: z.boolean().optional(), 420 - ssoProtection: z 421 - .object({ 422 - deploymentType: z.union([ 423 - z.literal("all"), 424 - z.literal("preview"), 425 - z.literal("prod_deployment_urls_and_all_previews"), 426 - ]), 427 - }) 428 - .optional() 429 - .nullable(), 430 - targets: z 431 - .record( 432 - z 433 - .object({ 434 - alias: z.array(z.string()).optional(), 435 - aliasAssigned: z.union([z.number(), z.boolean(), z.null()]).nullish(), 436 - aliasError: z 437 - .object({ 438 - code: z.string(), 439 - message: z.string(), 440 - }) 441 - .optional() 442 - .nullable(), 443 - aliasFinal: z.string().optional().nullable(), 444 - automaticAliases: z.array(z.string()).optional(), 445 - builds: z 446 - .array( 447 - z.object({ 448 - use: z.string(), 449 - src: z.string().optional(), 450 - dest: z.string().optional(), 451 - }), 452 - ) 453 - .optional(), 454 - connectBuildsEnabled: z.boolean().optional(), 455 - connectConfigurationId: z.string().optional(), 456 - createdAt: z.number(), 457 - createdIn: z.string(), 458 - creator: z 459 - .object({ 460 - email: z.string(), 461 - githubLogin: z.string().optional(), 462 - gitlabLogin: z.string().optional(), 463 - uid: z.string(), 464 - username: z.string(), 465 - }) 466 - .nullable(), 467 - deploymentHostname: z.string(), 468 - name: z.string(), 469 - forced: z.boolean().optional(), 470 - id: z.string(), 471 - meta: z.record(z.string()).optional(), 472 - monorepoManager: z.string().optional().nullable(), 473 - plan: z.union([ 474 - z.literal("pro"), 475 - z.literal("enterprise"), 476 - z.literal("hobby"), 477 - z.literal("oss"), 478 - ]), 479 - private: z.boolean(), 480 - readyState: z.union([ 481 - z.literal("BUILDING"), 482 - z.literal("ERROR"), 483 - z.literal("INITIALIZING"), 484 - z.literal("QUEUED"), 485 - z.literal("READY"), 486 - z.literal("CANCELED"), 487 - ]), 488 - readySubstate: z 489 - .union([z.literal("STAGED"), z.literal("PROMOTED")]) 490 - .optional(), 491 - requestedAt: z.number().optional(), 492 - target: z.string().optional().nullable(), 493 - teamId: z.string().optional().nullable(), 494 - type: z.literal("LAMBDAS"), 495 - url: z.string(), 496 - userId: z.string(), 497 - withCache: z.boolean().optional(), 498 - checksConclusion: z 499 - .union([ 500 - z.literal("succeeded"), 501 - z.literal("failed"), 502 - z.literal("skipped"), 503 - z.literal("canceled"), 504 - ]) 505 - .optional(), 506 - checksState: z 507 - .union([ 508 - z.literal("registered"), 509 - z.literal("running"), 510 - z.literal("completed"), 511 - ]) 512 - .optional(), 513 - readyAt: z.number().optional(), 514 - buildingAt: z.number().optional(), 515 - previewCommentsEnabled: z.boolean().optional(), 516 - }) 517 - .nullable(), 518 - ) 519 - .optional(), 520 - transferCompletedAt: z.number().optional(), 521 - transferStartedAt: z.number().optional(), 522 - transferToAccountId: z.string().optional(), 523 - transferredFromAccountId: z.string().optional(), 524 - updatedAt: z.number().optional(), 525 - live: z.boolean().optional(), 526 - enablePreviewFeedback: z.boolean().optional().nullable(), 527 - permissions: z.object({}).optional(), 528 - lastRollbackTarget: z.record(z.unknown()).optional().nullable(), 529 - lastAliasRequest: z 530 - .object({ 531 - fromDeploymentId: z.string(), 532 - toDeploymentId: z.string(), 533 - jobStatus: z.union([ 534 - z.literal("succeeded"), 535 - z.literal("failed"), 536 - z.literal("skipped"), 537 - z.literal("pending"), 538 - z.literal("in-progress"), 539 - ]), 540 - requestedAt: z.number(), 541 - type: z.union([z.literal("promote"), z.literal("rollback")]), 542 - }) 543 - .optional() 544 - .nullable(), 545 - hasFloatingAliases: z.boolean().optional(), 546 - protectionBypass: z 547 - .record( 548 - z.union([ 549 - z.object({ 550 - createdAt: z.number(), 551 - createdBy: z.string(), 552 - scope: z.union([ 553 - z.literal("shareable-link"), 554 - z.literal("automation-bypass"), 555 - ]), 556 - }), 557 - z.object({ 558 - createdAt: z.number(), 559 - lastUpdatedAt: z.number(), 560 - lastUpdatedBy: z.string(), 561 - access: z.union([z.literal("requested"), z.literal("granted")]), 562 - scope: z.literal("user"), 563 - }), 564 - ]), 565 - ) 566 - .optional(), 567 - hasActiveBranches: z.boolean().optional(), 568 - trustedIps: z 569 - .union([ 570 - z.object({ 571 - deploymentType: z.union([ 572 - z.literal("all"), 573 - z.literal("preview"), 574 - z.literal("prod_deployment_urls_and_all_previews"), 575 - z.literal("production"), 576 - ]), 577 - addresses: z.array( 578 - z.object({ 579 - value: z.string(), 580 - note: z.string().optional(), 581 - }), 582 - ), 583 - protectionMode: z.union([ 584 - z.literal("additional"), 585 - z.literal("exclusive"), 586 - ]), 587 - }), 588 - z.object({ 589 - deploymentType: z.union([ 590 - z.literal("all"), 591 - z.literal("preview"), 592 - z.literal("prod_deployment_urls_and_all_previews"), 593 - z.literal("production"), 594 - ]), 595 - }), 596 - ]) 597 - .optional(), 598 - gitComments: z 599 - .object({ 600 - onPullRequest: z.boolean(), 601 - onCommit: z.boolean(), 602 - }) 603 - .optional(), 604 - }); 605 - 606 - export const projectsSchema = z.object({ 607 - projects: z.array(projectSchema), 608 - pagination: z.object({ 609 - count: z.number(), 610 - next: z.number().nullable(), 611 - prev: z.number().nullable(), 612 - }), 613 - });
-12
packages/integrations/vercel/src/libs/tinybird.ts
··· 1 - import { Tinybird } from "@openstatus/tinybird"; 2 - 3 - import { logDrainSchema } from "./schema"; 4 - 5 - const tb = new Tinybird({ token: process.env.TINY_BIRD_API_KEY || "" }); // should we use t3-env? 6 - 7 - export function publishVercelLogDrain() { 8 - return tb.buildIngestEndpoint({ 9 - datasource: "vercel_log_drain__v0", 10 - event: logDrainSchema, 11 - }); 12 - }
-107
packages/integrations/vercel/src/pages/configure.tsx
··· 1 - import { revalidatePath } from "next/cache"; 2 - import { cookies } from "next/headers"; 3 - import { redirect } from "next/navigation"; 4 - 5 - import { SubmitButton } from "../components/submit-button"; 6 - import { 7 - createLogDrain, 8 - deleteLogDrain, 9 - getLogDrains, 10 - getProjects, 11 - } from "../libs/client"; 12 - import { decrypt } from "../libs/crypto"; 13 - 14 - export async function Configure() { 15 - const iv = cookies().get("iv")?.value; 16 - const encryptedToken = cookies().get("token")?.value; 17 - const teamId = cookies().get("teamId")?.value; 18 - 19 - if (!iv || !encryptedToken) { 20 - /** Redirect to access new token */ 21 - return redirect("/app"); 22 - } 23 - 24 - const token = decrypt( 25 - Buffer.from(iv || "", "base64url"), 26 - Buffer.from(encryptedToken || "", "base64url"), 27 - ).toString(); 28 - 29 - let logDrains = await getLogDrains(token, teamId); 30 - 31 - const projects = await getProjects(token, teamId); 32 - 33 - for (const project of projects.projects) { 34 - console.log({ project }); 35 - console.log("create integration"); 36 - } 37 - // Create integration project if it doesn't exist 38 - 39 - if (logDrains.length === 0) { 40 - logDrains = [ 41 - await createLogDrain( 42 - token, 43 - // @ts-expect-error We need more data - but this is a demo 44 - { 45 - deliveryFormat: "json", 46 - name: "OpenStatus Log Drain", 47 - // TODO: update with correct url 48 - url: "https://wwww.openstatus.dev/api/integrations/vercel", 49 - sources: ["static", "lambda", "edge", "external"], 50 - // headers: { "key": "value"} 51 - }, 52 - teamId, 53 - ), 54 - ]; 55 - } 56 - 57 - // TODO: automatically create log drain on installation 58 - async function create(formData: FormData) { 59 - "use server"; 60 - await createLogDrain( 61 - token, 62 - // @ts-expect-error We need more data - but this is a demo 63 - { 64 - deliveryFormat: "json", 65 - name: "OpenStatus Log Drain", 66 - // TODO: update with correct url 67 - url: "https://wwww.openstatus.dev/api/integrations/vercel", 68 - sources: ["static", "lambda", "edge", "external"], 69 - // headers: { "key": "value"} 70 - }, 71 - teamId, 72 - ); 73 - revalidatePath("/"); 74 - } 75 - 76 - async function _delete(formData: FormData) { 77 - "use server"; 78 - const id = formData.get("id")?.toString(); 79 - console.log({ id }); 80 - if (id) { 81 - await deleteLogDrain(token, id, String(teamId)); 82 - revalidatePath("/"); 83 - } 84 - } 85 - 86 - return ( 87 - <div className="flex w-full flex-col items-center justify-center"> 88 - <div className="border-border m-3 grid w-full max-w-xl gap-3 rounded-lg border p-6 backdrop-blur-[2px]"> 89 - <h1 className="font-cal text-2xl">Configure Vercel Integration</h1> 90 - <ul> 91 - {logDrains.map((item) => ( 92 - <li 93 - key={item.id} 94 - className="flex items-center justify-between gap-2" 95 - > 96 - <p>{item.name}</p> 97 - <form action={_delete}> 98 - <input name="id" value={item.id} className="hidden" /> 99 - <SubmitButton>Remove integration</SubmitButton> 100 - </form> 101 - </li> 102 - ))} 103 - </ul> 104 - </div> 105 - </div> 106 - ); 107 - }
-54
packages/integrations/vercel/src/routes/callback.ts
··· 1 - import crypto from "crypto"; 2 - import { NextResponse } from "next/server"; 3 - import type { NextRequest } from "next/server"; 4 - 5 - import { getToken } from "../libs/client"; 6 - import { encrypt } from "../libs/crypto"; 7 - 8 - export async function GET(req: NextRequest) { 9 - const { searchParams } = new URL(req.url); 10 - const code = searchParams.get("code"); 11 - const next = searchParams.get("next"); 12 - const teamId = searchParams.get("teamId"); 13 - console.log({ code, next, teamId }); 14 - if (!code || !next) { 15 - throw new Error("Missing `code` or `next` param"); 16 - } 17 - 18 - /** Get access token from Vercel */ 19 - const token = await getToken(code); 20 - 21 - // TODO: automatically install log drains to the allowed projects 22 - 23 - const iv = crypto.randomBytes(16); 24 - const encryptedToken = encrypt(iv, Buffer.from(token)); 25 - 26 - // redirect to vercel's integration page after installation 27 - const res = NextResponse.redirect( 28 - `https://wwww.openstatus.dev/app/integrations/vercel/configure`, 29 - ); 30 - 31 - console.log("Setting cookies"); 32 - /** Encrypt access token and add to Cookies */ 33 - res.cookies.set("token", encryptedToken.toString("base64url")); 34 - res.cookies.set("iv", iv.toString("base64url")); 35 - 36 - if (teamId) { 37 - res.cookies.set("teamId", teamId); 38 - } 39 - 40 - return res; 41 - } 42 - 43 - /** 44 - * The code below is only needed if we allow the user 45 - * to send the secret link to a team mate e.g. 46 - */ 47 - function getSecretUrl(iv: Buffer, encryptedToken: Buffer) { 48 - const baseUrl = new URL(process.env.VRCL_REDIRECT_URI!); 49 - baseUrl.pathname = "/configure"; 50 - baseUrl.searchParams.set("token", encryptedToken.toString("base64url")); 51 - baseUrl.searchParams.set("iv", iv.toString("base64url")); 52 - const secretUrl = baseUrl.toString(); 53 - return secretUrl; 54 - }
-76
packages/integrations/vercel/src/routes/webhook.ts
··· 1 - import { log } from "console"; 2 - import crypto from "crypto"; 3 - import { NextResponse } from "next/server"; 4 - import * as z from "zod"; 5 - 6 - import { env } from "../../env"; 7 - import { logDrainSchemaArray } from "../libs/schema"; 8 - import { publishVercelLogDrain } from "../libs/tinybird"; 9 - 10 - export const config = { 11 - api: { 12 - bodyParser: false, 13 - }, 14 - }; 15 - 16 - /** 17 - * Ingest log drains from Vercel. 18 - */ 19 - export async function POST(request: Request) { 20 - const rawBody = await request.text(); 21 - // const rawBodyBuffer = Buffer.from(rawBody, "utf-8"); 22 - // // const bodySignature = sha1(rawBodyBuffer, "LOG_DRAIN_SECRET"); 23 - 24 - // // /** 25 - // // * Validates the signature of the request 26 - // // * Uncomment for 'Test Log Drain' in Vercel 27 - // // */ 28 - // // if (bodySignature !== request.headers.get("x-vercel-signature")) { 29 - // // return NextResponse.json( 30 - // // { 31 - // // code: "invalid_signature", 32 - // // error: "signature didn't match", 33 - // // }, 34 - // // { status: 401 }, 35 - // // ); 36 - // // } 37 - 38 - /** 39 - * Returns a header verification to Vercel, so the route can be added as a log drain 40 - */ 41 - if (z.object({}).safeParse(JSON.parse(rawBody)).success) { 42 - return NextResponse.json( 43 - { code: "ok" }, 44 - { status: 200, headers: { "x-vercel-verify": env.INTEGRATION_SECRET } }, 45 - ); 46 - } 47 - 48 - /** 49 - * Validates the body of the request 50 - */ 51 - const logDrains = logDrainSchemaArray.safeParse(JSON.parse(rawBody)); 52 - 53 - if (logDrains.success) { 54 - // We are only pushing the logs that are not stdout or stderr 55 - const data = logDrains.data.filter( 56 - (log) => log.type !== "stdout" && log.type !== "stderr", 57 - ); 58 - 59 - for (const event of data) { 60 - // FIXME: Zod-bird is broken 61 - await publishVercelLogDrain()(event); 62 - } 63 - 64 - return NextResponse.json({ code: "ok" }, { status: 200 }); 65 - } else { 66 - console.error("Error parsing logDrains", logDrains.error); 67 - } 68 - return NextResponse.json({ error: logDrains.error }, { status: 500 }); 69 - } 70 - 71 - /** 72 - * Creates a sha1 hash from a buffer and a secret 73 - */ 74 - function sha1(data: Buffer, secret: string) { 75 - return crypto.createHmac("sha1", secret).update(data).digest("hex"); 76 - }
-4
packages/integrations/vercel/tsconfig.json
··· 1 - { 2 - "extends": "@openstatus/tsconfig/nextjs.json", 3 - "include": ["src", "*.ts"] 4 - }
+2 -2
packages/notifications/discord/src/index.ts
··· 18 18 export const sendDiscordMessage = async ({ 19 19 monitor, 20 20 notification, 21 - region, 22 21 statusCode, 23 22 message, 24 23 }: { ··· 36 35 await postToWebhook( 37 36 `Your monitor ${name} is down 🚨 38 37 39 - Your monitor with url ${monitor.url} is down in ${region} with ${ 38 + Your monitor with url ${monitor.url} is down with ${ 40 39 statusCode ? `status code ${statusCode}` : `error message ${message}` 41 40 }.`, 42 41 webhookUrl, 43 42 ); 44 43 } catch (err) { 44 + console.error(err); 45 45 // Do something 46 46 } 47 47 };
-40
pnpm-lock.yaml
··· 155 155 '@openstatus/utils': 156 156 specifier: workspace:* 157 157 version: link:../../packages/utils 158 - '@openstatus/vercel': 159 - specifier: workspace:* 160 - version: link:../../packages/integrations/vercel 161 158 '@sentry/integrations': 162 159 specifier: 7.73.0 163 160 version: 7.73.0 ··· 539 536 react: 540 537 specifier: 18.2.0 541 538 version: 18.2.0 542 - typescript: 543 - specifier: 5.2.2 544 - version: 5.2.2 545 - 546 - packages/integrations/vercel: 547 - dependencies: 548 - '@openstatus/tinybird': 549 - specifier: workspace:* 550 - version: link:../../tinybird 551 - '@t3-oss/env-core': 552 - specifier: 0.7.0 553 - version: 0.7.0(typescript@5.2.2)(zod@3.22.2) 554 - react: 555 - specifier: 18.2.0 556 - version: 18.2.0 557 - react-dom: 558 - specifier: 18.2.0 559 - version: 18.2.0(react@18.2.0) 560 - zod: 561 - specifier: ^3.22.2 562 - version: 3.22.2 563 - devDependencies: 564 - '@openstatus/tsconfig': 565 - specifier: workspace:* 566 - version: link:../../tsconfig 567 - '@types/node': 568 - specifier: 20.8.0 569 - version: 20.8.0 570 - '@types/react': 571 - specifier: 18.2.24 572 - version: 18.2.24 573 - '@types/react-dom': 574 - specifier: 18.2.8 575 - version: 18.2.8 576 - next: 577 - specifier: 13.5.3 578 - version: 13.5.3(@babel/core@7.23.2)(@opentelemetry/api@1.4.1)(react-dom@18.2.0)(react@18.2.0) 579 539 typescript: 580 540 specifier: 5.2.2 581 541 version: 5.2.2