Openstatus www.openstatus.dev

fix: timestamp and timing (#1009)

* fix: timestamp and timing

* ci: apply automated fixes

* fix: test

* ci: apply automated fixes

* fix: struct

---------

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

authored by

Maximilian Kaske
autofix-ci[bot]
and committed by
GitHub
218fa617 b1d3d9c7

+98 -114
+17 -12
apps/checker/handlers/ping.go
··· 14 14 ) 15 15 16 16 type PingResponse struct { 17 - Body string `json:"body,omitempty"` 18 - Headers string `json:"headers,omitempty"` 19 - Region string `json:"region"` 20 - RequestId int64 `json:"requestId,omitempty"` 21 - WorkspaceId int64 `json:"workspaceId,omitempty"` 22 - Latency int64 `json:"latency"` 23 - Time int64 `json:"time"` 24 - Status int `json:"status,omitempty"` 25 - Timing checker.Timing `json:"timing"` 17 + Body string `json:"body,omitempty"` 18 + Headers string `json:"headers,omitempty"` 19 + Region string `json:"region"` 20 + RequestId int64 `json:"requestId,omitempty"` 21 + WorkspaceId int64 `json:"workspaceId,omitempty"` 22 + Latency int64 `json:"latency"` 23 + Timestamp int64 `json:"timestamp"` 24 + Status int `json:"status,omitempty"` 25 + Timing string `json:"timing,omitempty"` 26 26 } 27 27 28 28 type Response struct { ··· 34 34 RequestId int64 `json:"requestId,omitempty"` 35 35 WorkspaceId int64 `json:"workspaceId,omitempty"` 36 36 Latency int64 `json:"latency"` 37 - Time int64 `json:"time"` 37 + Timestamp int64 `json:"timestamp"` 38 38 Timing checker.Timing `json:"timing"` 39 39 Status int `json:"status,omitempty"` 40 40 } ··· 112 112 return fmt.Errorf("unable to ping: %w", err) 113 113 } 114 114 115 + timingAsString, err := json.Marshal(r.Timing) 116 + if err != nil { 117 + return fmt.Errorf("error while parsing timing data %s: %w", req.URL, err) 118 + } 119 + 115 120 headersAsString, err := json.Marshal(r.Headers) 116 121 if err != nil { 117 122 return nil ··· 124 129 Latency: r.Latency, 125 130 Body: r.Body, 126 131 Headers: string(headersAsString), 127 - Time: r.Timestamp, 128 - Timing: r.Timing, 132 + Timestamp: r.Timestamp, 133 + Timing: string(timingAsString), 129 134 Region: h.Region, 130 135 } 131 136
+4 -4
apps/server/src/v1/check/post.test.ts
··· 24 24 mockFetch.mockReturnValue( 25 25 Promise.resolve( 26 26 new Response( 27 - '{"status":200,"latency":100,"body":"Hello World","headers":{"Content-Type":"application/json"},"time":1234567890,"timing":{"dnsStart":1,"dnsDone":2,"connectStart":3,"connectDone":4,"tlsHandshakeStart":5,"tlsHandshakeDone":6,"firstByteStart":7,"firstByteDone":8,"transferStart":9,"transferDone":10},"region":"ams"}', 27 + '{"status":200,"latency":100,"body":"Hello World","headers":{"Content-Type":"application/json"},"timestamp":1234567890,"timing":{"dnsStart":1,"dnsDone":2,"connectStart":3,"connectDone":4,"tlsHandshakeStart":5,"tlsHandshakeDone":6,"firstByteStart":7,"firstByteDone":8,"transferStart":9,"transferDone":10},"region":"ams"}', 28 28 { status: 200, headers: { "content-type": "application/json" } }, 29 29 ), 30 30 ), ··· 65 65 latency: 100, 66 66 region: "ams", 67 67 status: 200, 68 - time: 1234567890, 68 + timestamp: 1234567890, 69 69 timing: { 70 70 connectDone: 4, 71 71 connectStart: 3, ··· 93 93 mockFetch.mockReturnValue( 94 94 Promise.resolve( 95 95 new Response( 96 - '{"status":200,"latency":100,"body":"Hello World","headers":{"Content-Type":"application/json"},"time":1234567890,"timing":{"dnsStart":1,"dnsDone":2,"connectStart":3,"connectDone":4,"tlsHandshakeStart":5,"tlsHandshakeDone":6,"firstByteStart":7,"firstByteDone":8,"transferStart":9,"transferDone":10},"region":"ams"}', 96 + '{"status":200,"latency":100,"body":"Hello World","headers":{"Content-Type":"application/json"},"timestamp":1234567890,"timing":{"dnsStart":1,"dnsDone":2,"connectStart":3,"connectDone":4,"tlsHandshakeStart":5,"tlsHandshakeDone":6,"firstByteStart":7,"firstByteDone":8,"transferStart":9,"transferDone":10},"region":"ams"}', 97 97 { status: 200, headers: { "content-type": "application/json" } }, 98 98 ), 99 99 ), ··· 134 134 latency: 100, 135 135 region: "ams", 136 136 status: 200, 137 - time: 1234567890, 137 + timestamp: 1234567890, 138 138 timing: { 139 139 connectDone: 4, 140 140 connectStart: 3,
+76 -97
apps/server/src/v1/check/post.ts
··· 1 - import { createRoute, z } from "@hono/zod-openapi"; 1 + import { createRoute, type z } from "@hono/zod-openapi"; 2 2 3 3 import { db } from "@openstatus/db"; 4 4 import { check } from "@openstatus/db/src/schema/check"; ··· 60 60 }) 61 61 .returning() 62 62 .get(); 63 + 63 64 const result = []; 65 + 64 66 for (let count = 0; count < input.runCount; count++) { 65 67 const currentFetch = []; 66 68 for (const region of input.regions) { ··· 94 96 const allResults = await Promise.allSettled(currentFetch); 95 97 result.push(...allResults); 96 98 } 99 + 100 + const fulfilledRequest: z.infer<typeof ResponseSchema>[] = []; 101 + 97 102 const filteredResult = result.filter((r) => r.status === "fulfilled"); 98 - const fulfilledRequest = []; 99 103 for await (const r of filteredResult) { 100 104 if (r.status !== "fulfilled") throw new Error("No value"); 101 105 102 106 const json = await r.value.json(); 103 - fulfilledRequest.push(ResponseSchema.parse(json)); 107 + const parsed = ResponseSchema.safeParse(json); 108 + 109 + if (!parsed.success) { 110 + console.error(parsed.error.errors); 111 + throw new Error(`Failed to parse response: ${parsed.error.errors}`); 112 + } 113 + 114 + fulfilledRequest.push(parsed.data); 104 115 } 105 116 106 117 let aggregatedResponse = null; 118 + 107 119 if (aggregated) { 108 - // This is ugly 109 - const dnsArray = fulfilledRequest.map( 110 - (r) => r.timing.dnsDone - r.timing.dnsStart, 111 - ); 112 - const connectArray = fulfilledRequest.map( 113 - (r) => r.timing.connectDone - r.timing.connectStart, 114 - ); 115 - const tlsArray = fulfilledRequest.map( 116 - (r) => r.timing.tlsHandshakeDone - r.timing.tlsHandshakeStart, 117 - ); 118 - const firstArray = fulfilledRequest.map( 119 - (r) => r.timing.firstByteDone - r.timing.firstByteStart, 120 - ); 121 - const transferArray = fulfilledRequest.map( 122 - (r) => r.timing.transferDone - r.timing.transferStart, 123 - ); 124 - const latencyArray = fulfilledRequest.map((r) => r.latency); 125 - 126 - const dnsPercentile = percentile([50, 75, 95, 99], dnsArray) as number[]; 127 - const connectPercentile = percentile( 128 - [50, 75, 95, 99], 129 - connectArray, 130 - ) as number[]; 131 - const tlsPercentile = percentile([50, 75, 95, 99], tlsArray) as number[]; 132 - const firstPercentile = percentile( 133 - [50, 75, 95, 99], 134 - firstArray, 135 - ) as number[]; 136 - 137 - const transferPercentile = percentile( 138 - [50, 75, 95, 99], 139 - transferArray, 140 - ) as number[]; 141 - const latencyPercentile = percentile( 142 - [50, 75, 95, 99], 143 - latencyArray, 144 - ) as number[]; 145 - 146 - const aggregatedDNS = AggregatedResponseSchema.parse({ 147 - p50: dnsPercentile[0], 148 - p75: dnsPercentile[1], 149 - p95: dnsPercentile[2], 150 - p99: dnsPercentile[3], 151 - min: Math.min(...dnsArray), 152 - max: Math.max(...dnsArray), 153 - }); 154 - const aggregatedConnect = AggregatedResponseSchema.parse({ 155 - p50: connectPercentile[0], 156 - p75: connectPercentile[1], 157 - p95: connectPercentile[2], 158 - p99: connectPercentile[3], 159 - min: Math.min(...connectArray), 160 - max: Math.max(...connectArray), 161 - }); 162 - const aggregatedTls = AggregatedResponseSchema.parse({ 163 - p50: tlsPercentile[0], 164 - p75: tlsPercentile[1], 165 - p95: tlsPercentile[2], 166 - p99: tlsPercentile[3], 167 - min: Math.min(...tlsArray), 168 - max: Math.max(...tlsArray), 169 - }); 170 - const aggregatedFirst = AggregatedResponseSchema.parse({ 171 - p50: firstPercentile[0], 172 - p75: firstPercentile[1], 173 - p95: firstPercentile[2], 174 - p99: firstPercentile[3], 175 - min: Math.min(...firstArray), 176 - max: Math.max(...firstArray), 177 - }); 178 - const aggregatedTransfer = AggregatedResponseSchema.parse({ 179 - p50: transferPercentile[0], 180 - p75: transferPercentile[1], 181 - p95: transferPercentile[2], 182 - p99: transferPercentile[3], 183 - min: Math.min(...transferArray), 184 - max: Math.max(...transferArray), 185 - }); 186 - 187 - const aggregatedLatency = AggregatedResponseSchema.parse({ 188 - p50: latencyPercentile[0], 189 - p75: latencyPercentile[1], 190 - p95: latencyPercentile[2], 191 - p99: latencyPercentile[3], 192 - min: Math.min(...latencyArray), 193 - max: Math.max(...latencyArray), 194 - }); 120 + const { dns, connect, tls, firstByte, transfer, latency } = 121 + getTiming(fulfilledRequest); 195 122 196 123 aggregatedResponse = AggregatedResult.parse({ 197 - dns: aggregatedDNS, 198 - connect: aggregatedConnect, 199 - tls: aggregatedTls, 200 - firstByte: aggregatedFirst, 201 - transfer: aggregatedTransfer, 202 - latency: aggregatedLatency, 124 + dns: getAggregate(dns), 125 + connect: getAggregate(connect), 126 + tls: getAggregate(tls), 127 + firstByte: getAggregate(firstByte), 128 + transfer: getAggregate(transfer), 129 + latency: getAggregate(latency), 203 130 }); 204 131 } 132 + 205 133 const allTimings = fulfilledRequest.map((r) => r.timing); 206 134 207 135 const lastResponse = fulfilledRequest[fulfilledRequest.length - 1]; 208 136 const responseResult = CheckPostResponseSchema.parse({ 209 137 id: newCheck.id, 210 - raw: allTimings, 138 + raw: allTimings, // TODO: we should return the region here as well! 211 139 response: lastResponse, 212 140 aggregated: aggregatedResponse ? aggregatedResponse : undefined, 213 141 }); ··· 215 143 return c.json(responseResult, 200); 216 144 }); 217 145 } 146 + 147 + // This is a helper function to get the timing of the request 148 + 149 + type ReturnGetTiming = Record< 150 + "dns" | "connect" | "tls" | "firstByte" | "transfer" | "latency", 151 + number[] 152 + >; 153 + 154 + function getTiming(data: z.infer<typeof ResponseSchema>[]): ReturnGetTiming { 155 + return data.reduce( 156 + (prev, curr) => { 157 + prev.dns.push(curr.timing.dnsDone - curr.timing.dnsStart); 158 + prev.connect.push(curr.timing.connectDone - curr.timing.connectStart); 159 + prev.tls.push( 160 + curr.timing.tlsHandshakeDone - curr.timing.tlsHandshakeStart, 161 + ); 162 + prev.firstByte.push( 163 + curr.timing.firstByteDone - curr.timing.firstByteStart, 164 + ); 165 + prev.transfer.push(curr.timing.transferDone - curr.timing.transferStart); 166 + prev.latency.push(curr.latency); 167 + return prev; 168 + }, 169 + { 170 + dns: [], 171 + connect: [], 172 + tls: [], 173 + firstByte: [], 174 + transfer: [], 175 + latency: [], 176 + } as ReturnGetTiming, 177 + ); 178 + } 179 + 180 + function getAggregate(data: number[]) { 181 + const parsed = AggregatedResponseSchema.safeParse({ 182 + p50: percentile(50, data), 183 + p75: percentile(75, data), 184 + p95: percentile(95, data), 185 + p99: percentile(99, data), 186 + min: Math.min(...data), 187 + max: Math.max(...data), 188 + }); 189 + 190 + if (!parsed.success) { 191 + console.error(parsed.error.errors); 192 + throw new Error(`Failed to parse response: ${parsed.error.errors}`); 193 + } 194 + 195 + return parsed.data; 196 + }
+1 -1
apps/server/src/v1/check/schema.ts
··· 75 75 }); 76 76 77 77 export const ResponseSchema = z.object({ 78 - time: z 78 + timestamp: z 79 79 .number() 80 80 .openapi({ description: "The timestamp of the response in UTC" }), 81 81 status: z