snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
2
internal/health-worker/.gitignore
vendored
Normal file
2
internal/health-worker/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
build
|
||||
.wrangler
|
||||
27
internal/health-worker/README.md
Normal file
27
internal/health-worker/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Health Worker
|
||||
|
||||
Accepts webhooks from [Updown](https://updown.io/), sends them to our Discord.
|
||||
|
||||
## License
|
||||
|
||||
The code in this folder is Copyright (c) 2024-present tldraw Inc. The tldraw SDK is provided under the [tldraw license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
|
||||
|
||||
## Trademarks
|
||||
|
||||
Copyright (c) 2024-present tldraw Inc. The tldraw name and logo are trademarks of tldraw. Please see our [trademark guidelines](https://github.com/tldraw/tldraw/blob/main/TRADEMARKS.md) for info on acceptable usage.
|
||||
|
||||
## Distributions
|
||||
|
||||
You can find tldraw on npm [here](https://www.npmjs.com/package/@tldraw/tldraw?activeTab=versions).
|
||||
|
||||
## Contribution
|
||||
|
||||
Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
|
||||
|
||||
## Community
|
||||
|
||||
Have questions, comments or feedback? [Join our discord](https://discord.tldraw.com/?utm_source=github&utm_medium=readme&utm_campaign=sociallink). For the latest news and release notes, visit [tldraw.dev](https://tldraw.dev).
|
||||
|
||||
## Contact
|
||||
|
||||
Find us on Twitter/X at [@tldraw](https://twitter.com/tldraw).
|
||||
23
internal/health-worker/package.json
Normal file
23
internal/health-worker/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "health-worker",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"deploy": "wrangler deploy",
|
||||
"dev": "wrangler dev",
|
||||
"start": "wrangler dev",
|
||||
"check-bundle-size": "yarn run -T tsx ../scripts/check-worker-bundle.ts --entry src/index.ts --size-limit-bytes 40000",
|
||||
"lint": "yarn run -T tsx ../scripts/lint.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tldraw/utils": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20260302.0",
|
||||
"@types/node": "^22.15.31",
|
||||
"discord-api-types": "^0.37.118",
|
||||
"esbuild": "^0.28.0",
|
||||
"typescript": "^7.0.2",
|
||||
"wrangler": "^4.75.0"
|
||||
}
|
||||
}
|
||||
96
internal/health-worker/src/discord.ts
Normal file
96
internal/health-worker/src/discord.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { exhaustiveSwitchError } from '@tldraw/utils'
|
||||
import { type APIEmbed } from 'discord-api-types/v10'
|
||||
import { Event as UpdownEvent } from './updown_types'
|
||||
|
||||
// discord wants decimal colours
|
||||
const GREEN = 4243543
|
||||
const RED = 14692657
|
||||
const ORANGE = 16213767
|
||||
|
||||
// docs: https://birdie0.github.io/discord-webhooks-guide/index.html
|
||||
export interface DiscordPayload {
|
||||
username: string
|
||||
content: string
|
||||
embeds: APIEmbed[]
|
||||
}
|
||||
|
||||
function formatUpdownEvent(event: UpdownEvent): {
|
||||
colour: number
|
||||
title: string
|
||||
description: string
|
||||
} | null {
|
||||
switch (event.event) {
|
||||
case 'check.down':
|
||||
return {
|
||||
colour: RED,
|
||||
title: `Check DOWN at <${event.check.url}>`,
|
||||
description: `<${event.check.url}> is down: "${event.downtime.error}"\n\nNext check in ${event.check.period} seconds`,
|
||||
}
|
||||
case 'check.still_down':
|
||||
return null
|
||||
case 'check.up': {
|
||||
return {
|
||||
colour: GREEN,
|
||||
title: `Check UP at <${event.check.url}>`,
|
||||
description: `<${event.check.url}> is up\n\nIt was down for ${event.downtime.duration} seconds`,
|
||||
}
|
||||
}
|
||||
case 'check.ssl_invalid': {
|
||||
return {
|
||||
colour: RED,
|
||||
title: `SSL INVALID at <${event.check.url}>`,
|
||||
description: `SSL certificate at <${event.check.url}> is invalid: "${event.ssl.error}"`,
|
||||
}
|
||||
}
|
||||
case 'check.ssl_valid': {
|
||||
return {
|
||||
colour: GREEN,
|
||||
title: `SSL VALID at <${event.check.url}>`,
|
||||
description: `SSL certificate at <${event.check.url}> is now valid`,
|
||||
}
|
||||
}
|
||||
case 'check.ssl_expiration': {
|
||||
return {
|
||||
colour: ORANGE,
|
||||
title: `SSL EXPIRATION at <${event.check.url}>`,
|
||||
description: `SSL certificate at <${event.check.url}> will expire in ${event.ssl.days_before_expiration} days`,
|
||||
}
|
||||
}
|
||||
|
||||
case 'check.ssl_renewed': {
|
||||
return {
|
||||
colour: GREEN,
|
||||
title: `SSL RENEWED at <${event.check.url}>`,
|
||||
description: `SSL certificate at <${event.check.url}> was renewed`,
|
||||
}
|
||||
}
|
||||
case 'check.performance_drop':
|
||||
return {
|
||||
colour: ORANGE,
|
||||
title: `PERFORMANCE DROP at <${event.check.url}>`,
|
||||
description: `Performance drop at <${event.check.url}>, apdex dropped ${event.apdex_dropped}`,
|
||||
}
|
||||
|
||||
default:
|
||||
exhaustiveSwitchError(event, 'event')
|
||||
}
|
||||
}
|
||||
|
||||
export function updownToDiscord(event: UpdownEvent): DiscordPayload | null {
|
||||
const formatted = formatUpdownEvent(event)
|
||||
if (!formatted) return null
|
||||
|
||||
const { colour, title, description } = formatted
|
||||
|
||||
return {
|
||||
username: 'Health Worker',
|
||||
content: `Updown: ${title}`,
|
||||
embeds: [
|
||||
{
|
||||
color: colour,
|
||||
description: description,
|
||||
timestamp: event.time,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
69
internal/health-worker/src/index.ts
Normal file
69
internal/health-worker/src/index.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { DiscordPayload, updownToDiscord } from './discord'
|
||||
import { Event as UpdownEvent } from './updown_types'
|
||||
|
||||
interface Env {
|
||||
DISCORD_HEALTH_WEBHOOK_URL: string | undefined
|
||||
// it needs to be passed in because it's effectively a secret, unless we want everyone to be able
|
||||
// to stress us out with spurious discord alerts
|
||||
HEALTH_WORKER_UPDOWN_WEBHOOK_PATH: string | undefined
|
||||
}
|
||||
|
||||
async function sendDiscordWebhook(url: string, discord: DiscordPayload): Promise<Response> {
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(discord),
|
||||
})
|
||||
}
|
||||
|
||||
async function handleUpdown(request: Request, discordUrl: string): Promise<Response> {
|
||||
const updownEvents = (await request.json()) as Array<UpdownEvent>
|
||||
|
||||
let status = 200
|
||||
for (const e of updownEvents) {
|
||||
const discordPayload = updownToDiscord(e)
|
||||
if (!discordPayload) {
|
||||
continue
|
||||
}
|
||||
const discordResult = await sendDiscordWebhook(discordUrl, discordPayload)
|
||||
|
||||
if (!discordResult.ok) {
|
||||
console.error(`Discord error ${discordResult.status}: ${discordResult.statusText}`)
|
||||
status = discordResult.status
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(null, { status })
|
||||
}
|
||||
|
||||
const handler: ExportedHandler<Env> = {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const discordUrl = env.DISCORD_HEALTH_WEBHOOK_URL
|
||||
if (!discordUrl) {
|
||||
console.error('missing DISCORD_HEALTH_WEBHOOK_URL')
|
||||
return new Response('Internal error', { status: 500 })
|
||||
}
|
||||
|
||||
const updownWebhookPath = env.HEALTH_WORKER_UPDOWN_WEBHOOK_PATH
|
||||
if (!updownWebhookPath) {
|
||||
console.error('missing HEALTH_WORKER_UPDOWN_WEBHOOK_PATH')
|
||||
return new Response('Internal error', { status: 500 })
|
||||
}
|
||||
|
||||
const url = new URL(request.url)
|
||||
|
||||
// timing safety COULD be an issue, but it seems that in practice it isn't:
|
||||
// https://github.com/scriptin/node-timing-attack
|
||||
// my own testing confirms those observations
|
||||
if (url.pathname === updownWebhookPath) {
|
||||
return handleUpdown(request, discordUrl)
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 })
|
||||
},
|
||||
}
|
||||
|
||||
export default handler
|
||||
152
internal/health-worker/src/updown_types.ts
Normal file
152
internal/health-worker/src/updown_types.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
// docs: https://updown.io/api#webhooks
|
||||
|
||||
interface BaseCheck {
|
||||
token: string
|
||||
url: string
|
||||
alias: null
|
||||
last_status: number
|
||||
uptime: number
|
||||
period: number
|
||||
apdex_t: number
|
||||
string_match: string
|
||||
enabled: boolean
|
||||
published: boolean
|
||||
disabled_locations: any[]
|
||||
recipients: any[]
|
||||
last_check_at: string
|
||||
next_check_at: string
|
||||
created_at: null
|
||||
mute_until: null | string
|
||||
favicon_url: string
|
||||
custom_headers: CustomHeaders
|
||||
http_verb: string
|
||||
http_body: string
|
||||
}
|
||||
|
||||
interface FailingCheck extends BaseCheck {
|
||||
down: true
|
||||
down_since: string
|
||||
up_since: null
|
||||
error: string
|
||||
}
|
||||
|
||||
interface SucceedingCheck extends BaseCheck {
|
||||
down: true
|
||||
down_since: null
|
||||
up_since: string
|
||||
error: null
|
||||
}
|
||||
|
||||
interface BaseDowntime {
|
||||
id: string
|
||||
error: string
|
||||
started_at: string
|
||||
partial: unknown
|
||||
}
|
||||
|
||||
interface OngoingDowntime extends BaseDowntime {
|
||||
ended_at: null
|
||||
duration: null
|
||||
}
|
||||
|
||||
interface FinishedDowntime extends BaseDowntime {
|
||||
ended_at: string
|
||||
// seconds
|
||||
duration: number
|
||||
}
|
||||
|
||||
type CustomHeaders = Record<string, string>
|
||||
|
||||
interface SslCert {
|
||||
subject: string
|
||||
issuer: string
|
||||
from: string
|
||||
to: string
|
||||
algorithm: string
|
||||
}
|
||||
|
||||
interface EventDown {
|
||||
event: 'check.down'
|
||||
time: string
|
||||
description: string
|
||||
check: FailingCheck
|
||||
downtime: OngoingDowntime
|
||||
}
|
||||
|
||||
interface EventStillDown {
|
||||
event: 'check.still_down'
|
||||
time: string
|
||||
description: string
|
||||
check: FailingCheck
|
||||
downtime: OngoingDowntime
|
||||
}
|
||||
|
||||
interface EventUp {
|
||||
event: 'check.up'
|
||||
time: string
|
||||
description: string
|
||||
check: SucceedingCheck
|
||||
downtime: FinishedDowntime
|
||||
}
|
||||
|
||||
interface EventSslInvalid {
|
||||
event: 'check.ssl_invalid'
|
||||
time: string
|
||||
description: string
|
||||
check: SucceedingCheck | FailingCheck
|
||||
ssl: {
|
||||
cert: SslCert
|
||||
error: string
|
||||
}
|
||||
}
|
||||
|
||||
interface EventSslValid {
|
||||
event: 'check.ssl_valid'
|
||||
time: string
|
||||
description: string
|
||||
check: SucceedingCheck | FailingCheck
|
||||
ssl: {
|
||||
cert: SslCert
|
||||
}
|
||||
}
|
||||
|
||||
interface EventSslExpiration {
|
||||
event: 'check.ssl_expiration'
|
||||
time: string
|
||||
description: string
|
||||
check: SucceedingCheck | FailingCheck
|
||||
ssl: {
|
||||
cert: SslCert
|
||||
days_before_expiration: number
|
||||
}
|
||||
}
|
||||
|
||||
interface EventSslRenewed {
|
||||
event: 'check.ssl_renewed'
|
||||
time: string
|
||||
description: string
|
||||
check: SucceedingCheck | FailingCheck
|
||||
ssl: {
|
||||
new_cert: SslCert
|
||||
old_cert: SslCert
|
||||
}
|
||||
}
|
||||
|
||||
interface EventPerformanceDrop {
|
||||
event: 'check.performance_drop'
|
||||
time: string
|
||||
description: string
|
||||
check: SucceedingCheck | FailingCheck
|
||||
apdex_dropped: string
|
||||
last_metrics: Record<string, { apdex: number }>
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventDown
|
||||
| EventStillDown
|
||||
| EventUp
|
||||
| EventSslInvalid
|
||||
| EventSslValid
|
||||
| EventSslExpiration
|
||||
| EventSslRenewed
|
||||
| EventPerformanceDrop
|
||||
11
internal/health-worker/tsconfig.json
Normal file
11
internal/health-worker/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../config/tsconfig.base.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", ".tsbuild*"],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"types": ["@cloudflare/workers-types", "@types/node"]
|
||||
},
|
||||
"references": [{ "path": "../../packages/utils" }]
|
||||
}
|
||||
17
internal/health-worker/wrangler.toml
Normal file
17
internal/health-worker/wrangler.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2023-12-18"
|
||||
|
||||
#################### Environment names ####################
|
||||
# dev should never actually get deployed anywhere
|
||||
[env.dev]
|
||||
name = "health-worker-dev"
|
||||
|
||||
# we don't have a hard-coded name for preview. we instead have to generate it at build time and append it to this file.
|
||||
|
||||
# staging is the same as a preview on main:
|
||||
[env.staging]
|
||||
name = "health-worker-staging"
|
||||
|
||||
# production gets the proper name
|
||||
[env.production]
|
||||
name = "health-worker-production"
|
||||
Reference in New Issue
Block a user