Openstatus www.openstatus.dev

Improve alerting (#1210)

* alert-enginer wip

* 🚀 improve alerting strategy

* fmt

* update pr

* move endpoint

* ci: apply automated fixes

* 🤦

* alert-enginer wip

* ci: apply automated fixes

* fix tests

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Thibault Le Ouay
autofix-ci[bot]
and committed by
GitHub
66b9f010 bde04635

+888 -427
+1
.gitignore
··· 65 65 openstatus-dev.db-shm 66 66 apps/ingest-worker/.production.vars 67 67 apps/ingest-worker/.dev.vars 68 + apps/alerting-engine/tmp/
+48 -84
apps/checker/handlers/checker.go
··· 183 183 // it's in error if not successful 184 184 if isSuccessfull { 185 185 data.Error = 0 186 + if req.DegradedAfter != 0 && res.Latency > req.DegradedAfter { 187 + data.Body = res.Body 188 + 189 + } else { 190 + data.Body = "" 191 + 192 + } 186 193 // Small trick to avoid sending the body at the moment to TB 187 - data.Body = "" 188 194 } else { 189 195 data.Error = 1 190 196 result.Error = "Error" ··· 192 198 193 199 data.Assertions = assertionAsString 194 200 195 - if req.Status == "active" { 196 - if !isSuccessfull { 197 - // Q: Why here we do not check if the status was previously active? 198 - checker.UpdateStatus(ctx, checker.UpdateData{ 199 - MonitorId: req.MonitorID, 200 - Status: "error", 201 - StatusCode: res.Status, 202 - Region: h.Region, 203 - Message: res.Error, 204 - CronTimestamp: req.CronTimestamp, 205 - Latency: res.Latency, 206 - }) 207 - } 208 - // Check if the status is degraded 209 - if isSuccessfull && req.DegradedAfter > 0 && res.Latency > req.DegradedAfter { 210 - checker.UpdateStatus(ctx, checker.UpdateData{ 211 - MonitorId: req.MonitorID, 212 - Status: "degraded", 213 - Region: h.Region, 214 - StatusCode: res.Status, 215 - CronTimestamp: req.CronTimestamp, 216 - Latency: res.Latency, 217 - }) 218 - } 201 + if !isSuccessfull && req.Status != "error" { 202 + // Q: Why here we do not check if the status was previously active? 203 + checker.UpdateStatus(ctx, checker.UpdateData{ 204 + MonitorId: req.MonitorID, 205 + Status: "error", 206 + StatusCode: res.Status, 207 + Region: h.Region, 208 + Message: res.Error, 209 + CronTimestamp: req.CronTimestamp, 210 + Latency: res.Latency, 211 + }) 219 212 } 220 - 221 - // We were in error 222 - if req.Status == "error" { 223 - if isSuccessfull && req.DegradedAfter > 0 && res.Latency > req.DegradedAfter { 224 - checker.UpdateStatus(ctx, checker.UpdateData{ 225 - MonitorId: req.MonitorID, 226 - Status: "degraded", 227 - Region: h.Region, 228 - StatusCode: res.Status, 229 - CronTimestamp: req.CronTimestamp, 230 - Latency: res.Latency, 231 - }) 232 - } 233 - 234 - if isSuccessfull && req.DegradedAfter > 0 && res.Latency < req.DegradedAfter { 235 - checker.UpdateStatus(ctx, checker.UpdateData{ 236 - MonitorId: req.MonitorID, 237 - Status: "active", 238 - Region: h.Region, 239 - StatusCode: res.Status, 240 - CronTimestamp: req.CronTimestamp, 241 - }) 242 - } 243 - 244 - // This happens when we don't have a degradedAfter 245 - if isSuccessfull && req.DegradedAfter == 0 { 246 - checker.UpdateStatus(ctx, checker.UpdateData{ 247 - MonitorId: req.MonitorID, 248 - Status: "active", 249 - Region: h.Region, 250 - StatusCode: res.Status, 251 - CronTimestamp: req.CronTimestamp, 252 - Latency: res.Latency, 253 - }) 254 - } 213 + // it's degraded 214 + if isSuccessfull && req.DegradedAfter > 0 && res.Latency > req.DegradedAfter && req.Status != "degraded" { 215 + checker.UpdateStatus(ctx, checker.UpdateData{ 216 + MonitorId: req.MonitorID, 217 + Status: "degraded", 218 + Region: h.Region, 219 + StatusCode: res.Status, 220 + CronTimestamp: req.CronTimestamp, 221 + Latency: res.Latency, 222 + }) 255 223 } 256 - 257 - if req.Status == "degraded" { 258 - // if we were in degraded and now we are successful, we should update the status to active 259 - if isSuccessfull && req.DegradedAfter > 0 && res.Latency <= req.DegradedAfter { 260 - checker.UpdateStatus(ctx, checker.UpdateData{ 261 - MonitorId: req.MonitorID, 262 - Status: "active", 263 - Region: h.Region, 264 - StatusCode: res.Status, 265 - CronTimestamp: req.CronTimestamp, 266 - Latency: res.Latency, 267 - }) 268 - } 269 - 270 - if !isSuccessfull { 271 - checker.UpdateStatus(ctx, checker.UpdateData{ 272 - MonitorId: req.MonitorID, 273 - Status: "error", 274 - Region: h.Region, 275 - StatusCode: res.Status, 276 - CronTimestamp: req.CronTimestamp, 277 - Latency: res.Latency, 278 - }) 279 - } 224 + // it's active 225 + if isSuccessfull && req.DegradedAfter == 0 && req.Status != "active" { 226 + checker.UpdateStatus(ctx, checker.UpdateData{ 227 + MonitorId: req.MonitorID, 228 + Status: "active", 229 + Region: h.Region, 230 + StatusCode: res.Status, 231 + CronTimestamp: req.CronTimestamp, 232 + Latency: res.Latency, 233 + }) 234 + } 235 + // it's active 236 + if isSuccessfull && res.Latency < req.DegradedAfter && req.DegradedAfter != 0 && req.Status != "active" { 237 + checker.UpdateStatus(ctx, checker.UpdateData{ 238 + MonitorId: req.MonitorID, 239 + Status: "active", 240 + Region: h.Region, 241 + StatusCode: res.Status, 242 + CronTimestamp: req.CronTimestamp, 243 + }) 280 244 } 281 245 282 246 if err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil {
+18 -33
apps/checker/handlers/tcp.go
··· 127 127 JobType: "tcp", 128 128 } 129 129 130 - if req.Status == "active" && req.DegradedAfter > 0 && latency > req.DegradedAfter { 130 + if req.DegradedAfter == 0 && req.Status != "active" { 131 131 checker.UpdateStatus(ctx, checker.UpdateData{ 132 132 MonitorId: req.MonitorID, 133 - Status: "degraded", 133 + Status: "active", 134 134 Region: h.Region, 135 135 CronTimestamp: req.CronTimestamp, 136 136 Latency: latency, 137 137 }) 138 138 } 139 139 140 - if req.Status == "degraded" && req.DegradedAfter > 0 && latency <= req.DegradedAfter { 140 + if (req.DegradedAfter > 0 && latency < req.DegradedAfter) && req.Status != "active" { 141 141 checker.UpdateStatus(ctx, checker.UpdateData{ 142 142 MonitorId: req.MonitorID, 143 143 Status: "active", ··· 147 147 }) 148 148 } 149 149 150 - if req.Status == "error" { 151 - if req.DegradedAfter == 0 || (req.DegradedAfter > 0 && latency < req.DegradedAfter) { 152 - checker.UpdateStatus(ctx, checker.UpdateData{ 153 - MonitorId: req.MonitorID, 154 - Status: "active", 155 - Region: h.Region, 156 - CronTimestamp: req.CronTimestamp, 157 - Latency: latency, 158 - }) 159 - } 160 - 161 - if req.DegradedAfter > 0 && latency > req.DegradedAfter { 162 - checker.UpdateStatus(ctx, checker.UpdateData{ 163 - MonitorId: req.MonitorID, 164 - Status: "degraded", 165 - Region: h.Region, 166 - CronTimestamp: req.CronTimestamp, 167 - Latency: latency, 168 - }) 169 - } 170 - 150 + if req.DegradedAfter > 0 && latency > req.DegradedAfter && req.Status != "degraded" { 151 + checker.UpdateStatus(ctx, checker.UpdateData{ 152 + MonitorId: req.MonitorID, 153 + Status: "degraded", 154 + Region: h.Region, 155 + CronTimestamp: req.CronTimestamp, 156 + Latency: latency, 157 + }) 171 158 } 172 159 173 160 if err := h.TbClient.SendEvent(ctx, data, dataSourceName); err != nil { ··· 190 177 }, dataSourceName); err != nil { 191 178 log.Ctx(ctx).Error().Err(err).Msg("failed to send event to tinybird") 192 179 } 180 + checker.UpdateStatus(ctx, checker.UpdateData{ 181 + MonitorId: req.MonitorID, 182 + Status: "error", 183 + Message: err.Error(), 184 + Region: h.Region, 185 + CronTimestamp: req.CronTimestamp, 186 + }) 193 187 194 - if req.Status != "error" { 195 - checker.UpdateStatus(ctx, checker.UpdateData{ 196 - MonitorId: req.MonitorID, 197 - Status: "error", 198 - Message: err.Error(), 199 - Region: h.Region, 200 - CronTimestamp: req.CronTimestamp, 201 - }) 202 - } 203 188 } 204 189 205 190 returnData := c.Query("data")
+137 -234
apps/server/src/routes/checker/index.ts
··· 2 2 import { Hono } from "hono"; 3 3 import { z } from "zod"; 4 4 5 - import { and, db, eq, isNull, schema } from "@openstatus/db"; 5 + import { and, count, db, eq, isNull, schema } from "@openstatus/db"; 6 6 import { incidentTable, workspace } from "@openstatus/db/src/schema"; 7 7 import { 8 8 monitorStatusSchema, 9 9 selectMonitorSchema, 10 10 } from "@openstatus/db/src/schema/monitors/validation"; 11 - import { Redis } from "@openstatus/upstash"; 12 11 13 12 import { env } from "@/env"; 14 13 import { checkerAudit } from "@/utils/audit-log"; 15 14 import { flyRegions } from "@openstatus/db/src/schema/constants"; 15 + import { Tinybird } from "@openstatus/tinybird"; 16 16 import { triggerNotifications, upsertMonitorStatus } from "./alerting"; 17 17 18 18 export const checkerRoute = new Hono(); 19 - const redis = Redis.fromEnv(); 19 + 20 + const tb = new Tinybird({ token: process.env.TINY_BIRD_API_KEY || "" }); 21 + 22 + const payloadSchema = z.object({ 23 + monitorId: z.string(), 24 + message: z.string().optional(), 25 + statusCode: z.number().optional(), 26 + region: z.enum(flyRegions), 27 + cronTimestamp: z.number(), 28 + status: monitorStatusSchema, 29 + latency: z.number().optional(), 30 + }); 31 + 32 + const publishStatus = tb.buildIngestEndpoint({ 33 + datasource: "alerts__v0", 34 + event: payloadSchema, 35 + }); 20 36 21 37 checkerRoute.post("/updateStatus", async (c) => { 22 38 const auth = c.req.header("Authorization"); ··· 26 42 } 27 43 28 44 const json = await c.req.json(); 29 - const payloadSchema = z.object({ 30 - monitorId: z.string(), 31 - message: z.string().optional(), 32 - statusCode: z.number().optional(), 33 - region: z.enum(flyRegions), 34 - cronTimestamp: z.number(), 35 - status: monitorStatusSchema, 36 - latency: z.number().optional(), 37 - }); 38 45 39 46 const result = payloadSchema.safeParse(json); 40 47 ··· 53 60 54 61 console.log(`📝 update monitor status ${JSON.stringify(result.data)}`); 55 62 56 - // we check if it's an error 57 - // If status not in 200> and <300 58 - // if there's no incident create one and notify 59 - // publish event to TB 63 + // First we upsert the monitor status 64 + await upsertMonitorStatus({ 65 + monitorId: monitorId, 66 + status, 67 + region: region, 68 + }); 69 + await publishStatus(result.data); 60 70 61 - // if status is ok checked if there's an open incident 62 - // if open incident publish incident recovered 63 - const incident = await db 71 + const currentMonitor = await db 64 72 .select() 65 - .from(incidentTable) 73 + .from(schema.monitor) 74 + .where(eq(schema.monitor.id, Number(monitorId))) 75 + .get(); 76 + 77 + const monitor = selectMonitorSchema.parse(currentMonitor); 78 + const numberOfRegions = monitor.regions.length; 79 + 80 + const affectedRegion = await db 81 + .select({ count: count() }) 82 + .from(schema.monitorStatusTable) 66 83 .where( 67 84 and( 68 - eq(incidentTable.monitorId, Number(monitorId)), 69 - isNull(incidentTable.resolvedAt), 70 - isNull(incidentTable.acknowledgedAt), 85 + eq(schema.monitorStatusTable.monitorId, monitor.id), 86 + eq(schema.monitorStatusTable.status, status), 71 87 ), 72 88 ) 73 89 .get(); 74 90 75 - if (status === "degraded") { 76 - // We upsert the status of the monitor 77 - await upsertMonitorStatus({ 78 - monitorId: monitorId, 79 - status: "degraded", 80 - region: region, 81 - }); 82 - await checkerAudit.publishAuditLog({ 83 - id: `monitor:${monitorId}`, 84 - action: "monitor.degraded", 85 - targets: [{ id: monitorId, type: "monitor" }], 86 - metadata: { region, statusCode: statusCode ?? -1 }, 87 - }); 88 - const currentMonitor = await db 89 - .select() 90 - .from(schema.monitor) 91 - .where(eq(schema.monitor.id, Number(monitorId))) 92 - .get(); 93 - if (currentMonitor?.status === "active") { 94 - const redisKey = `${monitorId}-${cronTimestamp}-degraded`; 95 - // We add the new region to the set 96 - await redis.sadd(redisKey, region); 97 - // let's add an expire to the set 98 - // We get the number of regions affected 99 - const nbAffectedRegion = await redis.scard(redisKey); 100 - await redis.expire(redisKey, 60 * 60 * 24); 101 - 102 - const monitor = selectMonitorSchema.parse(currentMonitor); 103 - 104 - const numberOfRegions = monitor.regions.length; 105 - 106 - if (nbAffectedRegion >= numberOfRegions / 2 || numberOfRegions === 1) { 107 - await triggerNotifications({ 108 - monitorId, 109 - statusCode, 110 - message, 111 - notifType: "degraded", 112 - cronTimestamp, 113 - incidentId: `${cronTimestamp}`, 114 - region, 115 - latency, 116 - }); 117 - } 118 - } 91 + if (!affectedRegion?.count) { 92 + return; 119 93 } 120 - // if we are in error 121 - if (status === "error") { 122 - // trigger alerting 123 - await checkerAudit.publishAuditLog({ 124 - id: `monitor:${monitorId}`, 125 - action: "monitor.failed", 126 - targets: [{ id: monitorId, type: "monitor" }], 127 - metadata: { region, statusCode, message }, 128 - }); 129 - // We upsert the status of the monitor 130 - await upsertMonitorStatus({ 131 - monitorId: monitorId, 132 - status: "error", 133 - region: region, 134 - }); 135 94 136 - if (incident === undefined) { 137 - const redisKey = `${monitorId}-${cronTimestamp}-error`; 138 - // We add the new region to the set 139 - await redis.sadd(redisKey, region); 140 - // let's add an expire to the set 141 - // We get the number of regions affected 142 - const nbAffectedRegion = await redis.scard(redisKey); 143 - await redis.expire(redisKey, 60 * 60 * 24); 144 - 145 - const currentMonitor = await db 146 - .select() 147 - .from(schema.monitor) 148 - .where(eq(schema.monitor.id, Number(monitorId))) 149 - .get(); 150 - 151 - const monitor = selectMonitorSchema.parse(currentMonitor); 152 - 153 - const numberOfRegions = monitor.regions.length; 154 - 155 - console.log( 156 - `🤓 MonitorID ${monitorId} incident current affected ${nbAffectedRegion} total region ${numberOfRegions}`, 157 - ); 158 - // If the number of affected regions is greater than half of the total region, we trigger the alerting 159 - // 4 of 6 monitor need to fail to trigger an alerting 160 - if (nbAffectedRegion >= numberOfRegions / 2 || numberOfRegions === 1) { 161 - // let's refetch the incident to avoid race condition 95 + if (affectedRegion.count >= numberOfRegions / 2 || numberOfRegions === 1) { 96 + switch (status) { 97 + case "active": { 162 98 const incident = await db 163 99 .select() 164 100 .from(incidentTable) ··· 167 103 eq(incidentTable.monitorId, Number(monitorId)), 168 104 isNull(incidentTable.resolvedAt), 169 105 isNull(incidentTable.acknowledgedAt), 170 - eq(incidentTable.startedAt, new Date(cronTimestamp)), 171 106 ), 172 107 ) 173 108 .get(); 174 109 175 - if (incident === undefined) { 110 + if (!incident) { 111 + // it was just a single failure not a proper incident 112 + return; 113 + } 114 + if (incident?.resolvedAt) { 115 + // incident is already resolved 116 + return; 117 + } 118 + 119 + console.log(`🤓 recovering incident ${incident.id}`); 120 + await db 121 + .update(incidentTable) 122 + .set({ 123 + resolvedAt: new Date(cronTimestamp), 124 + autoResolved: true, 125 + }) 126 + .where(eq(incidentTable.id, incident.id)) 127 + .run(); 128 + 129 + await db 130 + .update(schema.monitor) 131 + .set({ status: "active" }) 132 + .where(eq(schema.monitor.id, monitor.id)); 133 + 134 + await triggerNotifications({ 135 + monitorId, 136 + statusCode, 137 + message, 138 + notifType: "recovery", 139 + cronTimestamp, 140 + incidentId: `${cronTimestamp}`, 141 + }); 142 + 143 + await checkerAudit.publishAuditLog({ 144 + id: `monitor:${monitorId}`, 145 + action: "monitor.recovered", 146 + targets: [{ id: monitorId, type: "monitor" }], 147 + metadata: { region: region, statusCode: statusCode ?? -1 }, 148 + }); 149 + 150 + break; 151 + } 152 + case "degraded": 153 + if (monitor.status !== "degraded") { 154 + console.log( 155 + `🔄 update monitorStatus ${monitor.id} status: DEGRADED}`, 156 + ); 157 + await db 158 + .update(schema.monitor) 159 + .set({ status: "degraded" }) 160 + .where(eq(schema.monitor.id, monitor.id)); 161 + // figure how to send the notification once 162 + await triggerNotifications({ 163 + monitorId, 164 + statusCode, 165 + message, 166 + notifType: "degraded", 167 + cronTimestamp, 168 + latency, 169 + incidentId: `${cronTimestamp}`, 170 + }); 171 + } 172 + await checkerAudit.publishAuditLog({ 173 + id: `monitor:${monitorId}`, 174 + action: "monitor.degraded", 175 + targets: [{ id: monitorId, type: "monitor" }], 176 + metadata: { region, statusCode: statusCode ?? -1 }, 177 + }); 178 + break; 179 + case "error": 180 + try { 176 181 const newIncident = await db 177 182 .insert(incidentTable) 178 183 .values({ ··· 180 185 workspaceId: monitor.workspaceId, 181 186 startedAt: new Date(cronTimestamp), 182 187 }) 183 - .onConflictDoNothing() 184 188 .returning(); 185 189 190 + if (!newIncident[0].id) { 191 + return; 192 + } 186 193 await triggerNotifications({ 187 194 monitorId, 188 195 statusCode, 189 196 message, 190 197 notifType: "alert", 191 198 cronTimestamp, 192 - incidentId: newIncident.length 193 - ? String(newIncident[0]?.id) 194 - : `${cronTimestamp}`, 195 - region, 196 - latency, 199 + incidentId: String(newIncident[0].id), 197 200 }); 198 201 199 - if (newIncident.length > 0) { 200 - const monitor = await db 201 - .select({ 202 - url: schema.monitor.url, 203 - jobType: schema.monitor.jobType, 204 - workspaceId: schema.monitor.workspaceId, 205 - }) 206 - .from(schema.monitor) 207 - .where(eq(schema.monitor.id, Number(monitorId))) 208 - .get(); 209 - if (monitor && monitor.jobType === "http" && monitor.workspaceId) { 210 - const currentWorkspace = await db 211 - .select() 212 - .from(workspace) 213 - .where(eq(workspace.id, monitor.workspaceId)) 214 - .get(); 215 - if ( 216 - !!currentWorkspace?.plan && 217 - currentWorkspace?.plan !== "free" 218 - ) { 219 - await triggerScreenshot({ 220 - data: { 221 - url: monitor.url, 222 - incidentId: newIncident[0].id, 223 - kind: "incident", 224 - }, 225 - }); 226 - } 227 - } 228 - } 229 - } 230 - } 231 - } 232 - } 233 - // When the status is ok 234 - if (status === "active") { 235 - await upsertMonitorStatus({ 236 - monitorId: monitorId, 237 - status: "active", 238 - region: region, 239 - }); 240 - 241 - await checkerAudit.publishAuditLog({ 242 - id: `monitor:${monitorId}`, 243 - action: "monitor.recovered", 244 - targets: [{ id: monitorId, type: "monitor" }], 245 - metadata: { region: region, statusCode: statusCode ?? -1 }, 246 - }); 247 - 248 - if (incident) { 249 - const redisKey = `${monitorId}-${incident.id}-resolved`; 250 - // // We add the new region to the set 251 - await redis.sadd(redisKey, region); 252 - // // let's add an expire to the set 253 - // // We get the number of regions affected 254 - const nbAffectedRegion = await redis.scard(redisKey); 255 - await redis.expire(redisKey, 60 * 60 * 24); 256 - 257 - const currentMonitor = await db 258 - .select() 259 - .from(schema.monitor) 260 - .where(eq(schema.monitor.id, Number(monitorId))) 261 - .get(); 262 - 263 - const monitor = selectMonitorSchema.parse(currentMonitor); 264 - 265 - const numberOfRegions = monitor.regions.length; 266 - 267 - console.log( 268 - `🤓 MonitorId ${monitorId} recovering incident current ${nbAffectedRegion} total region ${numberOfRegions}`, 269 - ); 270 - // // If the number of affected regions is greater than half of the total region, we trigger the alerting 271 - // // 4 of 6 monitor need to fail to trigger an alerting 272 - if (nbAffectedRegion >= numberOfRegions / 2 || numberOfRegions === 1) { 273 - const incident = await db 274 - .select() 275 - .from(incidentTable) 276 - .where( 277 - and( 278 - eq(incidentTable.monitorId, Number(monitorId)), 279 - isNull(incidentTable.resolvedAt), 280 - isNull(incidentTable.acknowledgedAt), 281 - ), 282 - ) 283 - .get(); 284 - if (incident) { 285 - console.log(`🤓 recovering incident ${incident.id}`); 286 202 await db 287 - .update(incidentTable) 288 - .set({ 289 - resolvedAt: new Date(cronTimestamp), 290 - autoResolved: true, 291 - }) 292 - .where(eq(incidentTable.id, incident.id)) 293 - .run(); 203 + .update(schema.monitor) 204 + .set({ status: "error" }) 205 + .where(eq(schema.monitor.id, monitor.id)); 294 206 295 - await triggerNotifications({ 296 - monitorId, 297 - statusCode, 298 - message, 299 - notifType: "recovery", 300 - cronTimestamp, 301 - incidentId: String(incident.id), 302 - region, 303 - latency, 304 - }); 305 - 306 - const monitor = await db 307 - .select({ 308 - url: schema.monitor.url, 309 - jobType: schema.monitor.jobType, 310 - workspaceId: schema.monitor.workspaceId, 311 - }) 312 - .from(schema.monitor) 313 - .where(eq(schema.monitor.id, Number(monitorId))) 314 - .get(); 315 207 if (monitor && monitor.jobType === "http" && monitor.workspaceId) { 316 208 const currentWorkspace = await db 317 209 .select() 318 210 .from(workspace) 319 211 .where(eq(workspace.id, monitor.workspaceId)) 320 212 .get(); 321 - 322 213 if (!!currentWorkspace?.plan && currentWorkspace?.plan !== "free") { 323 214 await triggerScreenshot({ 324 215 data: { 325 216 url: monitor.url, 326 - incidentId: incident.id, 327 - kind: "recovery", 217 + incidentId: newIncident[0].id, 218 + kind: "incident", 328 219 }, 329 220 }); 330 221 } 331 222 } 223 + await checkerAudit.publishAuditLog({ 224 + id: `monitor:${monitorId}`, 225 + action: "monitor.failed", 226 + targets: [{ id: monitorId, type: "monitor" }], 227 + metadata: { region, statusCode, message }, 228 + }); 229 + } catch { 230 + console.log("incident was already created"); 332 231 } 333 - } 232 + break; 233 + default: 234 + console.log("should not happen"); 235 + break; 334 236 } 335 237 } 336 238 239 + // if we are in error 337 240 return c.text("Ok", 200); 338 241 }); 339 242
-2
apps/workflows/.dockerignore
··· 9 9 /packages/analytics 10 10 /packages/api 11 11 /packages/error 12 - /packages/notifications 13 - /packages/tinybird 14 12 /packages/tracker
+15
apps/workflows/.env.test
··· 1 + DATABASE_URL=http://127.0.0.1:8080 2 + DATABASE_AUTH_TOKEN= 3 + NODE_ENV=test 4 + UNKEY_TOKEN=test 5 + TINY_BIRD_API_KEY=test 6 + UPSTASH_REDIS_REST_URL=test 7 + UPSTASH_REDIS_REST_TOKEN=test 8 + QSTASH_CURRENT_SIGNING_KEY=test 9 + QSTASH_NEXT_SIGNING_KEY=test 10 + FLY_REGION=ams 11 + RESEND_API_KEY=test 12 + SQLD_HTTP_AUTH=basic:token 13 + SCREENSHOT_SERVICE_URL=http://your.endpoint 14 + OPENPANEL_CLIENT_ID=test 15 + OPENPANEL_CLIENT_SECRET=test
+7
apps/workflows/Dockerfile
··· 11 11 --mount=type=bind,target=packages/assertions/package.json,source=packages/assertions/package.json \ 12 12 --mount=type=bind,target=packages/db/package.json,source=packages/db/package.json \ 13 13 --mount=type=bind,target=packages/emails/package.json,source=packages/emails/package.json \ 14 + --mount=type=bind,target=packages/notifications/discord/package.json,source=packages/notifications/discord/package.json \ 15 + --mount=type=bind,target=packages/notifications/email/package.json,source=packages/notifications/email/package.json \ 16 + --mount=type=bind,target=packages/notifications/opsgenie/package.json,source=packages/notifications/opsgenie/package.json \ 17 + --mount=type=bind,target=packages/notifications/pagerduty/package.json,source=packages/notifications/pagerduty/package.json \ 18 + --mount=type=bind,target=packages/notifications/slack/package.json,source=packages/notifications/slack/package.json \ 19 + --mount=type=bind,target=packages/notifications/twillio-sms/package.json,source=packages/notifications/twillio-sms/package.json \ 14 20 --mount=type=bind,target=packages/utils/package.json,source=packages/utils/package.json \ 15 21 --mount=type=bind,target=packages/tsconfig/package.json,source=packages/tsconfig/package.json \ 22 + --mount=type=bind,target=packages/tinybird/package.json,source=packages/tinybird/package.json \ 16 23 --mount=type=bind,target=packages/upstash/package.json,source=packages/upstash/package.json \ 17 24 --mount=type=cache,target=/root/.bun/install/cache,sharing=locked \ 18 25 bun install --production --ignore-scripts --frozen-lockfile --verbose
+43 -26
apps/workflows/dofigen.lock
··· 8 8 - /packages/analytics 9 9 - /packages/api 10 10 - /packages/error 11 - - /packages/notifications 12 - - /packages/tinybird 13 11 - /packages/tracker 14 12 builders: 13 + build: 14 + fromImage: 15 + path: oven/bun 16 + digest: sha256:e9382fda475d1ff0a939e925db3ca5a91b3b26cd71f23410dc5363262384bbc2 17 + workdir: /app/apps/workflows 18 + env: 19 + NODE_ENV: production 20 + copy: 21 + - paths: 22 + - . 23 + target: /app/ 24 + - fromBuilder: install 25 + paths: 26 + - /app/node_modules 27 + target: /app/node_modules 28 + run: 29 + - bun build --compile --sourcemap --format=cjs src/index.ts --outfile=app 15 30 install: 16 31 fromImage: 17 32 path: oven/bun ··· 32 47 source: packages/db/package.json 33 48 - target: packages/emails/package.json 34 49 source: packages/emails/package.json 50 + - target: packages/notifications/discord/package.json 51 + source: packages/notifications/discord/package.json 52 + - target: packages/notifications/email/package.json 53 + source: packages/notifications/email/package.json 54 + - target: packages/notifications/opsgenie/package.json 55 + source: packages/notifications/opsgenie/package.json 56 + - target: packages/notifications/pagerduty/package.json 57 + source: packages/notifications/pagerduty/package.json 58 + - target: packages/notifications/slack/package.json 59 + source: packages/notifications/slack/package.json 60 + - target: packages/notifications/twillio-sms/package.json 61 + source: packages/notifications/twillio-sms/package.json 35 62 - target: packages/utils/package.json 36 63 source: packages/utils/package.json 37 64 - target: packages/tsconfig/package.json 38 65 source: packages/tsconfig/package.json 66 + - target: packages/tinybird/package.json 67 + source: packages/tinybird/package.json 39 68 - target: packages/upstash/package.json 40 69 source: packages/upstash/package.json 41 - build: 42 - fromImage: 43 - path: oven/bun 44 - digest: sha256:e9382fda475d1ff0a939e925db3ca5a91b3b26cd71f23410dc5363262384bbc2 45 - workdir: /app/apps/workflows 46 - env: 47 - NODE_ENV: production 48 - copy: 49 - - paths: 50 - - . 51 - target: /app/ 52 - - fromBuilder: install 53 - paths: 54 - - /app/node_modules 55 - target: /app/node_modules 56 - run: 57 - - bun build --compile --sourcemap --format=cjs src/index.ts --outfile=app 58 70 fromImage: 59 71 path: debian 60 72 digest: sha256:e831d9a884d63734fe3dd9c491ed9a5a3d4c6a6d32c5b14f2067357c49b0b7e1 ··· 70 82 - port: 3000 71 83 images: 72 84 registry.hub.docker.com:443: 73 - library: 74 - debian: 75 - bullseye-slim: 76 - digest: sha256:e831d9a884d63734fe3dd9c491ed9a5a3d4c6a6d32c5b14f2067357c49b0b7e1 77 85 oven: 78 86 bun: 79 87 latest: 80 88 digest: sha256:e9382fda475d1ff0a939e925db3ca5a91b3b26cd71f23410dc5363262384bbc2 89 + library: 90 + debian: 91 + bullseye-slim: 92 + digest: sha256:e831d9a884d63734fe3dd9c491ed9a5a3d4c6a6d32c5b14f2067357c49b0b7e1 81 93 resources: 82 94 dofigen.yml: 83 - hash: 48a9d086b070600575e9867af0c63f2bbcfd301c867121e671fc771ae50f34dc 95 + hash: 89efc794b70865f5718e42302021382f7ed0c0ea9459a7bd9a8047cdd28757ae 84 96 content: | 85 97 ignore: 86 98 - node_modules ··· 91 103 - /packages/analytics 92 104 - /packages/api 93 105 - /packages/error 94 - - /packages/notifications 95 - - /packages/tinybird 96 106 - /packages/tracker 97 107 builders: 98 108 install: ··· 105 115 - packages/assertions/package.json 106 116 - packages/db/package.json 107 117 - packages/emails/package.json 118 + - packages/notifications/discord/package.json 119 + - packages/notifications/email/package.json 120 + - packages/notifications/opsgenie/package.json 121 + - packages/notifications/pagerduty/package.json 122 + - packages/notifications/slack/package.json 123 + - packages/notifications/twillio-sms/package.json 108 124 - packages/utils/package.json 109 125 - packages/tsconfig/package.json 126 + - packages/tinybird/package.json 110 127 - packages/upstash/package.json 111 128 112 129 # Install dependencies
+7 -2
apps/workflows/dofigen.yml
··· 7 7 - /packages/analytics 8 8 - /packages/api 9 9 - /packages/error 10 - - /packages/notifications 11 - - /packages/tinybird 12 10 - /packages/tracker 13 11 builders: 14 12 install: ··· 21 19 - packages/assertions/package.json 22 20 - packages/db/package.json 23 21 - packages/emails/package.json 22 + - packages/notifications/discord/package.json 23 + - packages/notifications/email/package.json 24 + - packages/notifications/opsgenie/package.json 25 + - packages/notifications/pagerduty/package.json 26 + - packages/notifications/slack/package.json 27 + - packages/notifications/twillio-sms/package.json 24 28 - packages/utils/package.json 25 29 - packages/tsconfig/package.json 30 + - packages/tinybird/package.json 26 31 - packages/upstash/package.json 27 32 28 33 # Install dependencies
+8
apps/workflows/package.json
··· 9 9 "@google-cloud/tasks": "4.0.1", 10 10 "@openstatus/db": "workspace:*", 11 11 "@openstatus/emails": "workspace:*", 12 + "@openstatus/notification-discord": "workspace:*", 13 + "@openstatus/notification-emails": "workspace:*", 14 + "@openstatus/notification-opsgenie": "workspace:*", 15 + "@openstatus/notification-pagerduty": "workspace:*", 16 + "@openstatus/notification-slack": "workspace:*", 17 + "@openstatus/notification-twillio-sms": "workspace:*", 18 + "@openstatus/tinybird": "workspace:*", 12 19 "@openstatus/upstash": "workspace:*", 13 20 "@openstatus/utils": "workspace:*", 21 + "@upstash/qstash": "2.6.2", 14 22 "hono": "4.5.3", 15 23 "limiter": "^3.0.0", 16 24 "zod": "3.23.8"
+22
apps/workflows/src/checker/alerting.test.ts
··· 1 + import { expect, mock, test } from "bun:test"; 2 + 3 + import { triggerNotifications } from "./alerting"; 4 + 5 + test.todo("should send email notification", async () => { 6 + const fn = mock(() => {}); 7 + mock.module("./utils.ts", () => { 8 + return { 9 + providerToFunction: { 10 + email: fn, 11 + }, 12 + }; 13 + }); 14 + await triggerNotifications({ 15 + monitorId: "1", 16 + statusCode: 400, 17 + notifType: "alert", 18 + cronTimestamp: 123456, 19 + incidentId: "1", 20 + }); 21 + expect(fn).toHaveBeenCalled(); 22 + });
+123
apps/workflows/src/checker/alerting.ts
··· 1 + import { db, eq, schema } from "@openstatus/db"; 2 + import type { MonitorStatus } from "@openstatus/db/src/schema"; 3 + import { 4 + selectMonitorSchema, 5 + selectNotificationSchema, 6 + } from "@openstatus/db/src/schema"; 7 + 8 + import type { 9 + MonitorFlyRegion, 10 + Region, 11 + } from "@openstatus/db/src/schema/constants"; 12 + import { checkerAudit } from "../utils/audit-log"; 13 + import { providerToFunction } from "./utils"; 14 + 15 + export const triggerNotifications = async ({ 16 + monitorId, 17 + statusCode, 18 + message, 19 + notifType, 20 + cronTimestamp, 21 + incidentId, 22 + region, 23 + latency, 24 + }: { 25 + monitorId: string; 26 + statusCode?: number; 27 + message?: string; 28 + notifType: "alert" | "recovery" | "degraded"; 29 + cronTimestamp: number; 30 + incidentId: string; 31 + region?: Region; 32 + latency?: number; 33 + }) => { 34 + console.log(`💌 triggerAlerting for ${monitorId}`); 35 + const notifications = await db 36 + .select() 37 + .from(schema.notificationsToMonitors) 38 + .innerJoin( 39 + schema.notification, 40 + eq(schema.notification.id, schema.notificationsToMonitors.notificationId), 41 + ) 42 + .innerJoin( 43 + schema.monitor, 44 + eq(schema.monitor.id, schema.notificationsToMonitors.monitorId), 45 + ) 46 + .where(eq(schema.monitor.id, Number(monitorId))) 47 + .all(); 48 + for (const notif of notifications) { 49 + console.log( 50 + `💌 sending notification for ${monitorId} and chanel ${notif.notification.provider} for ${notifType}`, 51 + ); 52 + const monitor = selectMonitorSchema.parse(notif.monitor); 53 + switch (notifType) { 54 + case "alert": 55 + await providerToFunction[notif.notification.provider].sendAlert({ 56 + monitor, 57 + notification: selectNotificationSchema.parse(notif.notification), 58 + statusCode, 59 + message, 60 + incidentId, 61 + cronTimestamp, 62 + region, 63 + latency, 64 + }); 65 + break; 66 + case "recovery": 67 + await providerToFunction[notif.notification.provider].sendRecovery({ 68 + monitor, 69 + notification: selectNotificationSchema.parse(notif.notification), 70 + statusCode, 71 + message, 72 + incidentId, 73 + cronTimestamp, 74 + region, 75 + latency, 76 + }); 77 + break; 78 + case "degraded": 79 + await providerToFunction[notif.notification.provider].sendDegraded({ 80 + monitor, 81 + notification: selectNotificationSchema.parse(notif.notification), 82 + statusCode, 83 + message, 84 + cronTimestamp, 85 + region, 86 + latency, 87 + }); 88 + break; 89 + } 90 + // ALPHA 91 + await checkerAudit.publishAuditLog({ 92 + id: `monitor:${monitorId}`, 93 + action: "notification.sent", 94 + targets: [{ id: monitorId, type: "monitor" }], 95 + metadata: { provider: notif.notification.provider }, 96 + }); 97 + // 98 + } 99 + }; 100 + 101 + export const upsertMonitorStatus = async ({ 102 + monitorId, 103 + status, 104 + region, 105 + }: { 106 + monitorId: string; 107 + status: MonitorStatus; 108 + region: MonitorFlyRegion; 109 + }) => { 110 + const newData = await db 111 + .insert(schema.monitorStatusTable) 112 + .values({ status, region, monitorId: Number(monitorId) }) 113 + .onConflictDoUpdate({ 114 + target: [ 115 + schema.monitorStatusTable.monitorId, 116 + schema.monitorStatusTable.region, 117 + ], 118 + set: { status, updatedAt: new Date() }, 119 + }) 120 + .returning(); 121 + console.log(`📈 upsertMonitorStatus for ${monitorId} in region ${region}`); 122 + console.log(`🤔 upsert monitor ${JSON.stringify(newData)}`); 123 + };
+272
apps/workflows/src/checker/index.ts
··· 1 + import { Client } from "@upstash/qstash"; 2 + 3 + import { Hono } from "hono"; 4 + import { z } from "zod"; 5 + 6 + import { and, count, db, eq, isNull, schema } from "@openstatus/db"; 7 + import { incidentTable, workspace } from "@openstatus/db/src/schema"; 8 + import { 9 + monitorStatusSchema, 10 + selectMonitorSchema, 11 + } from "@openstatus/db/src/schema/monitors/validation"; 12 + 13 + import { flyRegions } from "@openstatus/db/src/schema/constants"; 14 + import { Tinybird } from "@openstatus/tinybird"; 15 + import { env } from "../env"; 16 + import { checkerAudit } from "../utils/audit-log"; 17 + import { triggerNotifications, upsertMonitorStatus } from "./alerting"; 18 + 19 + export const checkerRoute = new Hono(); 20 + 21 + const tb = new Tinybird({ token: env().TINY_BIRD_API_KEY }); 22 + 23 + const payloadSchema = z.object({ 24 + monitorId: z.string(), 25 + message: z.string().optional(), 26 + statusCode: z.number().optional(), 27 + region: z.enum(flyRegions), 28 + cronTimestamp: z.number(), 29 + status: monitorStatusSchema, 30 + latency: z.number().optional(), 31 + }); 32 + 33 + const publishStatus = tb.buildIngestEndpoint({ 34 + datasource: "alerts__v0", 35 + event: payloadSchema, 36 + }); 37 + 38 + checkerRoute.post("/updateStatus", async (c) => { 39 + const auth = c.req.header("Authorization"); 40 + if (auth !== `Basic ${env().CRON_SECRET}`) { 41 + console.error("Unauthorized"); 42 + return c.text("Unauthorized", 401); 43 + } 44 + 45 + const json = await c.req.json(); 46 + 47 + const result = payloadSchema.safeParse(json); 48 + 49 + if (!result.success) { 50 + return c.text("Unprocessable Entity", 422); 51 + } 52 + const { 53 + monitorId, 54 + message, 55 + region, 56 + statusCode, 57 + cronTimestamp, 58 + status, 59 + latency, 60 + } = result.data; 61 + 62 + console.log(`📝 update monitor status ${JSON.stringify(result.data)}`); 63 + 64 + // First we upsert the monitor status 65 + await upsertMonitorStatus({ 66 + monitorId: monitorId, 67 + status, 68 + region: region, 69 + }); 70 + await publishStatus(result.data); 71 + 72 + const currentMonitor = await db 73 + .select() 74 + .from(schema.monitor) 75 + .where(eq(schema.monitor.id, Number(monitorId))) 76 + .get(); 77 + 78 + const monitor = selectMonitorSchema.parse(currentMonitor); 79 + const numberOfRegions = monitor.regions.length; 80 + 81 + const affectedRegion = await db 82 + .select({ count: count() }) 83 + .from(schema.monitorStatusTable) 84 + .where( 85 + and( 86 + eq(schema.monitorStatusTable.monitorId, monitor.id), 87 + eq(schema.monitorStatusTable.status, status), 88 + ), 89 + ) 90 + .get(); 91 + 92 + if (!affectedRegion?.count) { 93 + return; 94 + } 95 + 96 + if (affectedRegion.count >= numberOfRegions / 2 || numberOfRegions === 1) { 97 + switch (status) { 98 + case "active": { 99 + const incident = await db 100 + .select() 101 + .from(incidentTable) 102 + .where( 103 + and( 104 + eq(incidentTable.monitorId, Number(monitorId)), 105 + isNull(incidentTable.resolvedAt), 106 + isNull(incidentTable.acknowledgedAt), 107 + ), 108 + ) 109 + .get(); 110 + 111 + if (!incident) { 112 + // it was just a single failure not a proper incident 113 + return; 114 + } 115 + if (incident?.resolvedAt) { 116 + // incident is already resolved 117 + return; 118 + } 119 + 120 + console.log(`🤓 recovering incident ${incident.id}`); 121 + await db 122 + .update(incidentTable) 123 + .set({ 124 + resolvedAt: new Date(cronTimestamp), 125 + autoResolved: true, 126 + }) 127 + .where(eq(incidentTable.id, incident.id)) 128 + .run(); 129 + 130 + await db 131 + .update(schema.monitor) 132 + .set({ status: "active" }) 133 + .where(eq(schema.monitor.id, monitor.id)); 134 + 135 + await triggerNotifications({ 136 + monitorId, 137 + statusCode, 138 + message, 139 + notifType: "recovery", 140 + cronTimestamp, 141 + incidentId: `${cronTimestamp}`, 142 + }); 143 + 144 + await checkerAudit.publishAuditLog({ 145 + id: `monitor:${monitorId}`, 146 + action: "monitor.recovered", 147 + targets: [{ id: monitorId, type: "monitor" }], 148 + metadata: { region: region, statusCode: statusCode ?? -1 }, 149 + }); 150 + 151 + break; 152 + } 153 + case "degraded": 154 + if (monitor.status !== "degraded") { 155 + console.log( 156 + `🔄 update monitorStatus ${monitor.id} status: DEGRADED}`, 157 + ); 158 + await db 159 + .update(schema.monitor) 160 + .set({ status: "degraded" }) 161 + .where(eq(schema.monitor.id, monitor.id)); 162 + // figure how to send the notification once 163 + await triggerNotifications({ 164 + monitorId, 165 + statusCode, 166 + message, 167 + notifType: "degraded", 168 + cronTimestamp, 169 + latency, 170 + incidentId: `${cronTimestamp}`, 171 + }); 172 + } 173 + await checkerAudit.publishAuditLog({ 174 + id: `monitor:${monitorId}`, 175 + action: "monitor.degraded", 176 + targets: [{ id: monitorId, type: "monitor" }], 177 + metadata: { region, statusCode: statusCode ?? -1 }, 178 + }); 179 + break; 180 + case "error": 181 + try { 182 + const newIncident = await db 183 + .insert(incidentTable) 184 + .values({ 185 + monitorId: Number(monitorId), 186 + workspaceId: monitor.workspaceId, 187 + startedAt: new Date(cronTimestamp), 188 + }) 189 + .returning(); 190 + 191 + if (!newIncident[0].id) { 192 + return; 193 + } 194 + await triggerNotifications({ 195 + monitorId, 196 + statusCode, 197 + message, 198 + notifType: "alert", 199 + cronTimestamp, 200 + incidentId: String(newIncident[0].id), 201 + }); 202 + 203 + await db 204 + .update(schema.monitor) 205 + .set({ status: "error" }) 206 + .where(eq(schema.monitor.id, monitor.id)); 207 + 208 + if (monitor && monitor.jobType === "http" && monitor.workspaceId) { 209 + const currentWorkspace = await db 210 + .select() 211 + .from(workspace) 212 + .where(eq(workspace.id, monitor.workspaceId)) 213 + .get(); 214 + if (!!currentWorkspace?.plan && currentWorkspace?.plan !== "free") { 215 + await triggerScreenshot({ 216 + data: { 217 + url: monitor.url, 218 + incidentId: newIncident[0].id, 219 + kind: "incident", 220 + }, 221 + }); 222 + } 223 + } 224 + await checkerAudit.publishAuditLog({ 225 + id: `monitor:${monitorId}`, 226 + action: "monitor.failed", 227 + targets: [{ id: monitorId, type: "monitor" }], 228 + metadata: { region, statusCode, message }, 229 + }); 230 + } catch { 231 + console.log("incident was already created"); 232 + } 233 + break; 234 + default: 235 + console.log("should not happen"); 236 + break; 237 + } 238 + } 239 + 240 + // if we are in error 241 + return c.text("Ok", 200); 242 + }); 243 + 244 + const payload = z.object({ 245 + url: z.string().url(), 246 + incidentId: z.number(), 247 + kind: z.enum(["incident", "recovery"]), 248 + }); 249 + 250 + const triggerScreenshot = async ({ 251 + data, 252 + }: { 253 + data: z.infer<typeof payload>; 254 + }) => { 255 + console.log(` 📸 taking screenshot for incident ${data.incidentId}`); 256 + 257 + const client = new Client({ token: env().QSTASH_TOKEN }); 258 + 259 + await client.publishJSON({ 260 + url: env().SCREENSHOT_SERVICE_URL, 261 + method: "POST", 262 + headers: { 263 + "Content-Type": "application/json", 264 + "api-key": `Basic ${env().CRON_SECRET}`, 265 + }, 266 + body: { 267 + url: data.url, 268 + incidentId: data.incidentId, 269 + kind: data.kind, 270 + }, 271 + }); 272 + };
+97
apps/workflows/src/checker/utils.ts
··· 1 + import type { 2 + Monitor, 3 + Notification, 4 + NotificationProvider, 5 + } from "@openstatus/db/src/schema"; 6 + import { 7 + sendAlert as sendDiscordAlert, 8 + sendDegraded as sendDiscordDegraded, 9 + sendRecovery as sendDiscordRecovery, 10 + } from "@openstatus/notification-discord"; 11 + import { 12 + sendAlert as sendEmailAlert, 13 + sendDegraded as sendEmailDegraded, 14 + sendRecovery as sendEmailRecovery, 15 + } from "@openstatus/notification-emails"; 16 + import { 17 + sendAlert as sendSlackAlert, 18 + sendDegraded as sendSlackDegraded, 19 + sendRecovery as sendSlackRecovery, 20 + } from "@openstatus/notification-slack"; 21 + import { 22 + sendAlert as sendSmsAlert, 23 + sendDegraded as sendSmsDegraded, 24 + sendRecovery as sendSmsRecovery, 25 + } from "@openstatus/notification-twillio-sms"; 26 + 27 + import { 28 + sendDegraded as sendPagerDutyDegraded, 29 + sendRecovery as sendPagerDutyRecovery, 30 + sendAlert as sendPagerdutyAlert, 31 + } from "@openstatus/notification-pagerduty"; 32 + 33 + import type { Region } from "@openstatus/db/src/schema/constants"; 34 + import { 35 + sendAlert as sendOpsGenieAlert, 36 + sendDegraded as sendOpsGenieDegraded, 37 + sendRecovery as sendOpsGenieRecovery, 38 + } from "@openstatus/notification-opsgenie"; 39 + 40 + type SendNotification = ({ 41 + monitor, 42 + notification, 43 + statusCode, 44 + message, 45 + incidentId, 46 + cronTimestamp, 47 + latency, 48 + region, 49 + }: { 50 + monitor: Monitor; 51 + notification: Notification; 52 + statusCode?: number; 53 + message?: string; 54 + incidentId?: string; 55 + cronTimestamp: number; 56 + latency?: number; 57 + region?: Region; 58 + }) => Promise<void>; 59 + 60 + type Notif = { 61 + sendAlert: SendNotification; 62 + sendRecovery: SendNotification; 63 + sendDegraded: SendNotification; 64 + }; 65 + 66 + export const providerToFunction = { 67 + email: { 68 + sendAlert: sendEmailAlert, 69 + sendRecovery: sendEmailRecovery, 70 + sendDegraded: sendEmailDegraded, 71 + }, 72 + slack: { 73 + sendAlert: sendSlackAlert, 74 + sendRecovery: sendSlackRecovery, 75 + sendDegraded: sendSlackDegraded, 76 + }, 77 + discord: { 78 + sendAlert: sendDiscordAlert, 79 + sendRecovery: sendDiscordRecovery, 80 + sendDegraded: sendDiscordDegraded, 81 + }, 82 + sms: { 83 + sendAlert: sendSmsAlert, 84 + sendRecovery: sendSmsRecovery, 85 + sendDegraded: sendSmsDegraded, 86 + }, 87 + opsgenie: { 88 + sendAlert: sendOpsGenieAlert, 89 + sendRecovery: sendOpsGenieRecovery, 90 + sendDegraded: sendOpsGenieDegraded, 91 + }, 92 + pagerduty: { 93 + sendAlert: sendPagerdutyAlert, 94 + sendRecovery: sendPagerDutyRecovery, 95 + sendDegraded: sendPagerDutyDegraded, 96 + }, 97 + } satisfies Record<NotificationProvider, Notif>;
+4
apps/workflows/src/env.ts
··· 15 15 DATABASE_AUTH_TOKEN: z.string().default(""), 16 16 RESEND_API_KEY: z.string().default(""), 17 17 TINY_BIRD_API_KEY: z.string().default(""), 18 + QSTASH_TOKEN: z.string().default(""), 19 + SCREENSHOT_SERVICE_URL: z.string().default(""), 20 + TWILLIO_AUTH_TOKEN: z.string().default(""), 21 + TWILLIO_ACCOUNT_ID: z.string().default(""), 18 22 }) 19 23 .parse(process.env);
+3
apps/workflows/src/index.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { showRoutes } from "hono/dev"; 3 3 import { logger } from "hono/logger"; 4 + import { checkerRoute } from "./checker"; 4 5 import { cronRouter } from "./cron"; 5 6 import { env } from "./env"; 6 7 ··· 25 26 if (NODE_ENV === "development") { 26 27 showRoutes(app, { verbose: true, colorize: true }); 27 28 } 29 + 30 + app.route("/", checkerRoute); 28 31 29 32 console.log(`Starting server on port ${PORT}`); 30 33
+7
apps/workflows/src/utils/audit-log.ts
··· 1 + import { AuditLog, Tinybird } from "@openstatus/tinybird"; 2 + 3 + import { env } from "../env"; 4 + 5 + const tb = new Tinybird({ token: env().TINY_BIRD_API_KEY }); 6 + 7 + export const checkerAudit = new AuditLog({ tb });
+76 -46
pnpm-lock.yaml
··· 95 95 version: 2.6.2 96 96 drizzle-orm: 97 97 specifier: 0.35.3 98 - version: 0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.7.0)(bun-types@1.2.4)(react@19.0.0) 98 + version: 0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.7.0)(bun-types@1.2.5)(react@19.0.0) 99 99 hono: 100 100 specifier: 4.5.3 101 101 version: 4.5.3 ··· 171 171 version: link:../../packages/utils 172 172 '@scalar/hono-api-reference': 173 173 specifier: 0.5.131 174 - version: 0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2) 174 + version: 0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2) 175 175 '@t3-oss/env-core': 176 176 specifier: 0.7.1 177 177 version: 0.7.1(typescript@5.7.2)(zod@3.23.8) ··· 489 489 '@openstatus/emails': 490 490 specifier: workspace:* 491 491 version: link:../../packages/emails 492 + '@openstatus/notification-discord': 493 + specifier: workspace:* 494 + version: link:../../packages/notifications/discord 495 + '@openstatus/notification-emails': 496 + specifier: workspace:* 497 + version: link:../../packages/notifications/email 498 + '@openstatus/notification-opsgenie': 499 + specifier: workspace:* 500 + version: link:../../packages/notifications/opsgenie 501 + '@openstatus/notification-pagerduty': 502 + specifier: workspace:* 503 + version: link:../../packages/notifications/pagerduty 504 + '@openstatus/notification-slack': 505 + specifier: workspace:* 506 + version: link:../../packages/notifications/slack 507 + '@openstatus/notification-twillio-sms': 508 + specifier: workspace:* 509 + version: link:../../packages/notifications/twillio-sms 510 + '@openstatus/tinybird': 511 + specifier: workspace:* 512 + version: link:../../packages/tinybird 492 513 '@openstatus/upstash': 493 514 specifier: workspace:* 494 515 version: link:../../packages/upstash 495 516 '@openstatus/utils': 496 517 specifier: workspace:* 497 518 version: link:../../packages/utils 519 + '@upstash/qstash': 520 + specifier: 2.6.2 521 + version: 2.6.2 498 522 hono: 499 523 specifier: 4.5.3 500 524 version: 4.5.3 ··· 510 534 version: link:../../packages/tsconfig 511 535 '@types/bun': 512 536 specifier: latest 513 - version: 1.2.4 537 + version: 1.2.5 514 538 515 539 packages/analytics: 516 540 dependencies: ··· 621 645 version: 0.7.0(typescript@5.6.2)(zod@3.23.8) 622 646 drizzle-orm: 623 647 specifier: 0.35.3 624 - version: 0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.4)(react@19.0.0) 648 + version: 0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.5)(react@19.0.0) 625 649 drizzle-zod: 626 650 specifier: 0.5.1 627 - version: 0.5.1(drizzle-orm@0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.4)(react@19.0.0))(zod@3.23.8) 651 + version: 0.5.1(drizzle-orm@0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.5)(react@19.0.0))(zod@3.23.8) 628 652 zod: 629 653 specifier: 3.23.8 630 654 version: 3.23.8 ··· 1712 1736 '@chronark/zod-bird@0.3.6': 1713 1737 resolution: {integrity: sha512-hE8kCGLJK5ncH8F7uPaqPiOOqo68vUI66nusg7HO5X9BcyN8lXfeQliu6Ou1kOSq95OYshf9nB2fk2+LEvF4ng==} 1714 1738 1715 - '@cloudflare/workers-types@4.20241230.0': 1716 - resolution: {integrity: sha512-dtLD4jY35Lb750cCVyO1i/eIfdZJg2Z0i+B1RYX6BVeRPlgaHx/H18ImKAkYmy0g09Ow8R2jZy3hIxMgXun0WQ==} 1739 + '@cloudflare/workers-types@4.20250303.0': 1740 + resolution: {integrity: sha512-O7F7nRT4bbmwHf3gkRBLfJ7R6vHIJ/oZzWdby6obOiw2yavUfp/AIwS7aO2POu5Cv8+h3TXS3oHs3kKCZLraUA==} 1717 1741 1718 1742 '@codemirror/autocomplete@6.17.0': 1719 1743 resolution: {integrity: sha512-fdfj6e6ZxZf8yrkMHUSJJir7OJkHkZKaOZGzLWIYp2PZ3jd+d+UjG8zVPqJF6d3bKxkhvXTPan/UZ1t7Bqm0gA==} ··· 4884 4908 '@types/babel__traverse@7.20.6': 4885 4909 resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} 4886 4910 4887 - '@types/bun@1.2.4': 4888 - resolution: {integrity: sha512-QtuV5OMR8/rdKJs213iwXDpfVvnskPXY/S0ZiFbsTjQZycuqPbMW8Gf/XhLfwE5njW8sxI2WjISURXPlHypMFA==} 4911 + '@types/bun@1.2.5': 4912 + resolution: {integrity: sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg==} 4889 4913 4890 4914 '@types/caseless@0.12.4': 4891 4915 resolution: {integrity: sha512-2in/lrHRNmDvHPgyormtEralhPcN3An1gLjJzj2Bw145VBxkQ75JEXW6CTdMAwShiHQcYsl2d10IjQSdJSJz4g==} ··· 5630 5654 bun-types@1.0.8: 5631 5655 resolution: {integrity: sha512-2dNB+dBwAcFW7RSd4y5vKycRjouKVklSwPk4EjBKWvcMYUBOqZGGNzV7+b2tfKBG3BeRXnozbnegVKR1azuATg==} 5632 5656 5633 - bun-types@1.2.4: 5634 - resolution: {integrity: sha512-nDPymR207ZZEoWD4AavvEaa/KZe/qlrbMSchqpQwovPZCKc7pwMoENjEtHgMKaAjJhy+x6vfqSBA1QU3bJgs0Q==} 5657 + bun-types@1.2.5: 5658 + resolution: {integrity: sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg==} 5635 5659 5636 5660 bundle-require@4.0.2: 5637 5661 resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} ··· 6966 6990 resolution: {integrity: sha512-r26WwwbKD3BAYdfB294knNnegNda7VfV1tVn66D9Kvl9WQTdrR+5eKdoeaQNHQcC3Gr0KBikzAtjd6VsRGVSaw==} 6967 6991 engines: {node: '>=16.0.0'} 6968 6992 6993 + hono@4.7.4: 6994 + resolution: {integrity: sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg==} 6995 + engines: {node: '>=16.9.0'} 6996 + 6969 6997 hookable@5.5.3: 6970 6998 resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} 6971 6999 ··· 11339 11367 dependencies: 11340 11368 zod: 3.23.8 11341 11369 11342 - '@cloudflare/workers-types@4.20241230.0': 11370 + '@cloudflare/workers-types@4.20250303.0': 11343 11371 optional: true 11344 11372 11345 11373 '@codemirror/autocomplete@6.17.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.28.4)(@lezer/common@1.2.1)': ··· 12022 12050 protobufjs: 7.2.5 12023 12051 yargs: 17.7.2 12024 12052 12025 - '@headlessui/tailwindcss@0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))': 12053 + '@headlessui/tailwindcss@0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))': 12026 12054 dependencies: 12027 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)) 12055 + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)) 12028 12056 12029 12057 '@headlessui/vue@1.7.22(vue@3.4.31(typescript@5.7.2))': 12030 12058 dependencies: ··· 13655 13683 '@rollup/rollup-win32-x64-msvc@4.34.8': 13656 13684 optional: true 13657 13685 13658 - '@scalar/api-client@2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2)': 13686 + '@scalar/api-client@2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2)': 13659 13687 dependencies: 13660 - '@headlessui/tailwindcss': 0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2))) 13688 + '@headlessui/tailwindcss': 0.2.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2))) 13661 13689 '@headlessui/vue': 1.7.22(vue@3.4.31(typescript@5.7.2)) 13662 13690 '@scalar/components': 0.12.28(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(typescript@5.7.2) 13663 13691 '@scalar/draggable': 0.1.4(typescript@5.7.2) ··· 13693 13721 - typescript 13694 13722 - vitest 13695 13723 13696 - '@scalar/api-reference@1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2)': 13724 + '@scalar/api-reference@1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2)': 13697 13725 dependencies: 13698 13726 '@floating-ui/vue': 1.1.1(vue@3.4.31(typescript@5.7.2)) 13699 13727 '@headlessui/vue': 1.7.22(vue@3.4.31(typescript@5.7.2)) 13700 - '@scalar/api-client': 2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2) 13728 + '@scalar/api-client': 2.0.45(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2) 13701 13729 '@scalar/components': 0.12.28(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(typescript@5.7.2) 13702 13730 '@scalar/oas-utils': 0.2.26(typescript@5.7.2) 13703 13731 '@scalar/openapi-parser': 0.7.2 ··· 13782 13810 transitivePeerDependencies: 13783 13811 - typescript 13784 13812 13785 - '@scalar/hono-api-reference@0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2)': 13813 + '@scalar/hono-api-reference@0.5.131(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2)': 13786 13814 dependencies: 13787 - '@scalar/api-reference': 1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)))(typescript@5.7.2) 13788 - hono: 4.5.3 13815 + '@scalar/api-reference': 1.24.70(postcss@8.5.2)(storybook@8.4.7(bufferutil@4.0.8)(prettier@3.4.2)(utf-8-validate@6.0.5))(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)))(typescript@5.7.2) 13816 + hono: 4.7.4 13789 13817 transitivePeerDependencies: 13790 13818 - '@jest/globals' 13791 13819 - '@types/bun' ··· 14740 14768 dependencies: 14741 14769 '@babel/types': 7.26.3 14742 14770 14743 - '@types/bun@1.2.4': 14771 + '@types/bun@1.2.5': 14744 14772 dependencies: 14745 - bun-types: 1.2.4 14773 + bun-types: 1.2.5 14746 14774 14747 14775 '@types/caseless@0.12.4': {} 14748 14776 ··· 15015 15043 '@upstash/qstash@2.6.2': 15016 15044 dependencies: 15017 15045 crypto-js: 4.2.0 15018 - jose: 5.2.4 15046 + jose: 5.9.4 15019 15047 15020 15048 '@upstash/ratelimit@0.4.4(encoding@0.1.13)': 15021 15049 dependencies: ··· 15732 15760 15733 15761 bun-types@1.0.8: {} 15734 15762 15735 - bun-types@1.2.4: 15763 + bun-types@1.2.5: 15736 15764 dependencies: 15737 15765 '@types/node': 22.10.2 15738 15766 '@types/ws': 8.5.13 ··· 16258 16286 transitivePeerDependencies: 16259 16287 - supports-color 16260 16288 16261 - drizzle-orm@0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.4)(react@19.0.0): 16289 + drizzle-orm@0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.5)(react@19.0.0): 16262 16290 dependencies: 16263 16291 '@libsql/client-wasm': 0.14.0 16264 16292 optionalDependencies: 16265 - '@cloudflare/workers-types': 4.20241230.0 16293 + '@cloudflare/workers-types': 4.20250303.0 16266 16294 '@libsql/client': 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) 16267 16295 '@opentelemetry/api': 1.9.0 16268 16296 '@types/pg': 8.11.10 16269 16297 '@types/react': 19.0.10 16270 16298 better-sqlite3: 11.4.0 16271 - bun-types: 1.2.4 16299 + bun-types: 1.2.5 16272 16300 react: 19.0.0 16273 16301 16274 - drizzle-orm@0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.7.0)(bun-types@1.2.4)(react@19.0.0): 16302 + drizzle-orm@0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.7.0)(bun-types@1.2.5)(react@19.0.0): 16275 16303 dependencies: 16276 16304 '@libsql/client-wasm': 0.14.0 16277 16305 optionalDependencies: 16278 - '@cloudflare/workers-types': 4.20241230.0 16306 + '@cloudflare/workers-types': 4.20250303.0 16279 16307 '@libsql/client': 0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.5) 16280 16308 '@opentelemetry/api': 1.9.0 16281 16309 '@types/pg': 8.11.10 16282 16310 '@types/react': 19.0.10 16283 16311 better-sqlite3: 11.7.0 16284 - bun-types: 1.2.4 16312 + bun-types: 1.2.5 16285 16313 react: 19.0.0 16286 16314 16287 - drizzle-zod@0.5.1(drizzle-orm@0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.4)(react@19.0.0))(zod@3.23.8): 16315 + drizzle-zod@0.5.1(drizzle-orm@0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.5)(react@19.0.0))(zod@3.23.8): 16288 16316 dependencies: 16289 - drizzle-orm: 0.35.3(@cloudflare/workers-types@4.20241230.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.4)(react@19.0.0) 16317 + drizzle-orm: 0.35.3(@cloudflare/workers-types@4.20250303.0)(@libsql/client-wasm@0.14.0)(@libsql/client@0.14.0(bufferutil@4.0.8)(utf-8-validate@6.0.3))(@opentelemetry/api@1.9.0)(@types/pg@8.11.10)(@types/react@19.0.10)(better-sqlite3@11.4.0)(bun-types@1.2.5)(react@19.0.0) 16290 16318 zod: 3.23.8 16291 16319 16292 16320 dset@3.1.4: {} ··· 17305 17333 react-is: 16.13.1 17306 17334 17307 17335 hono@4.5.3: {} 17336 + 17337 + hono@4.7.4: {} 17308 17338 17309 17339 hookable@5.5.3: {} 17310 17340 ··· 19152 19182 postcss: 8.4.38 19153 19183 ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.6.2) 19154 19184 19155 - postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)): 19185 + postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 19156 19186 dependencies: 19157 19187 lilconfig: 3.1.3 19158 19188 yaml: 2.6.1 19159 19189 optionalDependencies: 19160 19190 postcss: 8.5.2 19161 - ts-node: 10.9.2(@types/node@20.8.0)(typescript@5.7.2) 19191 + ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.6.2) 19162 19192 19163 - postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 19193 + postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)): 19164 19194 dependencies: 19165 19195 lilconfig: 3.1.3 19166 19196 yaml: 2.6.1 19167 19197 optionalDependencies: 19168 19198 postcss: 8.5.2 19169 - ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.6.2) 19199 + ts-node: 10.9.2(@types/node@22.10.2)(typescript@5.7.2) 19170 19200 19171 19201 postcss-nested@6.0.1(postcss@8.5.2): 19172 19202 dependencies: ··· 20379 20409 dependencies: 20380 20410 tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)) 20381 20411 20382 - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)): 20412 + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 20383 20413 dependencies: 20384 20414 '@alloc/quick-lru': 5.2.0 20385 20415 arg: 5.0.2 ··· 20398 20428 postcss: 8.5.2 20399 20429 postcss-import: 15.1.0(postcss@8.5.2) 20400 20430 postcss-js: 4.0.1(postcss@8.5.2) 20401 - postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2)) 20431 + postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)) 20402 20432 postcss-nested: 6.2.0(postcss@8.5.2) 20403 20433 postcss-selector-parser: 6.1.2 20404 20434 resolve: 1.22.9 ··· 20406 20436 transitivePeerDependencies: 20407 20437 - ts-node 20408 20438 20409 - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)): 20439 + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)): 20410 20440 dependencies: 20411 20441 '@alloc/quick-lru': 5.2.0 20412 20442 arg: 5.0.2 ··· 20425 20455 postcss: 8.5.2 20426 20456 postcss-import: 15.1.0(postcss@8.5.2) 20427 20457 postcss-js: 4.0.1(postcss@8.5.2) 20428 - postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2)) 20458 + postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2)) 20429 20459 postcss-nested: 6.2.0(postcss@8.5.2) 20430 20460 postcss-selector-parser: 6.1.2 20431 20461 resolve: 1.22.9 ··· 20679 20709 v8-compile-cache-lib: 3.0.1 20680 20710 yn: 3.1.1 20681 20711 20682 - ts-node@10.9.2(@types/node@20.8.0)(typescript@5.7.2): 20712 + ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2): 20683 20713 dependencies: 20684 20714 '@cspotcode/source-map-support': 0.8.1 20685 20715 '@tsconfig/node10': 1.0.11 20686 20716 '@tsconfig/node12': 1.0.11 20687 20717 '@tsconfig/node14': 1.0.3 20688 20718 '@tsconfig/node16': 1.0.4 20689 - '@types/node': 20.8.0 20719 + '@types/node': 22.10.2 20690 20720 acorn: 8.11.3 20691 20721 acorn-walk: 8.3.2 20692 20722 arg: 4.1.3 20693 20723 create-require: 1.1.1 20694 20724 diff: 4.0.2 20695 20725 make-error: 1.3.6 20696 - typescript: 5.7.2 20726 + typescript: 5.6.2 20697 20727 v8-compile-cache-lib: 3.0.1 20698 20728 yn: 3.1.1 20699 20729 optional: true 20700 20730 20701 - ts-node@10.9.2(@types/node@22.10.2)(typescript@5.6.2): 20731 + ts-node@10.9.2(@types/node@22.10.2)(typescript@5.7.2): 20702 20732 dependencies: 20703 20733 '@cspotcode/source-map-support': 0.8.1 20704 20734 '@tsconfig/node10': 1.0.11 ··· 20712 20742 create-require: 1.1.1 20713 20743 diff: 4.0.2 20714 20744 make-error: 1.3.6 20715 - typescript: 5.6.2 20745 + typescript: 5.7.2 20716 20746 v8-compile-cache-lib: 3.0.1 20717 20747 yn: 3.1.1 20718 20748 optional: true