(request, env, ctx)
| 186 | |
| 187 | export default { |
| 188 | async fetch(request, env, ctx): Promise<Response> { |
| 189 | try { |
| 190 | const url = new URL(request.url); |
| 191 | |
| 192 | if (request.method === 'GET' && url.pathname === '/') { |
| 193 | return new Response(INFO_TEXT, { headers: { 'content-type': 'text/plain; charset=utf-8' } }); |
| 194 | } |
| 195 | if (url.pathname !== '/v1/events') { |
| 196 | return new Response('not found\n', { status: 404 }); |
| 197 | } |
| 198 | if (request.method !== 'POST') { |
| 199 | return new Response('method not allowed\n', { status: 405, headers: { allow: 'POST' } }); |
| 200 | } |
| 201 | |
| 202 | const contentLength = Number(request.headers.get('content-length')); |
| 203 | if (!Number.isFinite(contentLength) || contentLength <= 0) { |
| 204 | return new Response('length required\n', { status: 411 }); |
| 205 | } |
| 206 | if (contentLength > MAX_BODY_BYTES) { |
| 207 | return new Response('payload too large\n', { status: 413 }); |
| 208 | } |
| 209 | |
| 210 | let body: JsonObject; |
| 211 | try { |
| 212 | const text = await request.text(); |
| 213 | if (text.length > MAX_BODY_BYTES) return new Response('payload too large\n', { status: 413 }); |
| 214 | const parsed: unknown = JSON.parse(text); |
| 215 | if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { |
| 216 | return new Response('bad request\n', { status: 400 }); |
| 217 | } |
| 218 | body = parsed as JsonObject; |
| 219 | } catch { |
| 220 | return new Response('bad request\n', { status: 400 }); |
| 221 | } |
| 222 | |
| 223 | const machineId = body.machine_id; |
| 224 | if (typeof machineId !== 'string' || !UUID_RE.test(machineId)) { |
| 225 | return new Response('bad request\n', { status: 400 }); |
| 226 | } |
| 227 | |
| 228 | // Best-effort rate limit; fails open — losing a data point beats losing availability. |
| 229 | try { |
| 230 | const { success } = await env.MACHINE_RATE_LIMITER.limit({ key: machineId }); |
| 231 | if (!success) return new Response('rate limited\n', { status: 429 }); |
| 232 | } catch (err) { |
| 233 | console.error(JSON.stringify({ msg: 'rate limiter unavailable', err: String(err) })); |
| 234 | } |
| 235 | |
| 236 | const common: JsonObject = {}; |
| 237 | for (const [key, sanitize] of Object.entries(ENVELOPE_PROPS)) { |
| 238 | const val = sanitize(body[key]); |
| 239 | if (val !== undefined) common[key] = val; |
| 240 | } |
| 241 | |
| 242 | const rawEvents = Array.isArray(body.events) ? body.events.slice(0, MAX_EVENTS_PER_BATCH) : []; |
| 243 | const batch: PostHogEvent[] = []; |
| 244 | for (const raw of rawEvents) { |
| 245 | const sanitized = sanitizeEvent(raw, machineId, common); |
no test coverage detected