Openstatus
www.openstatus.dev
1import { writeFileSync } from "node:fs";
2import { resolve } from "node:path";
3import { db, eq } from "@openstatus/db";
4import { monitor, selectMonitorSchema } from "@openstatus/db/src/schema";
5import { OSTinybird } from "@openstatus/tinybird";
6
7// WARNING: make sure to enable the Tinybird client in the env you are running this script in
8
9// Configuration
10const MONITOR_ID = "7002";
11const PERIOD = "7d" as const;
12const INTERVAL = 60;
13const TYPE = "http" as const;
14const OUTPUT_FILE = "blog-post-metrics.json";
15const PERCENTILE = "p50"; // p50, p75, p90, p95, p99
16
17async function main() {
18 // Get Tinybird API key from environment
19 const tinybirdApiKey = process.env.TINY_BIRD_API_KEY;
20 if (!tinybirdApiKey) {
21 throw new Error("TINY_BIRD_API_KEY environment variable is required");
22 }
23
24 const tb = new OSTinybird(tinybirdApiKey);
25
26 console.log(`Fetching data for monitor ID: ${MONITOR_ID}`);
27
28 // 1. Fetch monitor from database with private locations
29 const monitorDataRaw = await db.query.monitor.findFirst({
30 where: eq(monitor.id, Number.parseInt(MONITOR_ID)),
31 with: {
32 privateLocationToMonitors: {
33 with: {
34 privateLocation: true,
35 },
36 },
37 },
38 });
39
40 if (!monitorDataRaw) {
41 throw new Error(`Monitor with ID ${MONITOR_ID} not found`);
42 }
43
44 // Parse the monitor data using the schema to convert regions string to array
45 const monitorData = selectMonitorSchema.parse(monitorDataRaw);
46
47 // Get private location names
48 const privateLocationNames =
49 monitorDataRaw.privateLocationToMonitors
50 ?.map((pl) => pl.privateLocation?.name)
51 .filter((name): name is string => Boolean(name)) || [];
52
53 // Combine regular regions with private locations
54 const allRegions = [...monitorData.regions, ...privateLocationNames];
55
56 console.log(`\nMonitor Details:`);
57 console.log(` ID: ${MONITOR_ID}`);
58 console.log(` Name: ${monitorData.name || "Unnamed"}`);
59 console.log(` Type: ${monitorData.jobType}`);
60 console.log(` Active: ${monitorData.active}`);
61 console.log(` Created: ${monitorData.createdAt}`);
62 console.log(` Regular regions: ${monitorData.regions.join(", ")}`);
63 console.log(
64 ` Private locations: ${privateLocationNames.join(", ") || "None"}`
65 );
66 console.log(` Total regions: ${allRegions.length}`);
67 console.log(`\nQuery Parameters:`);
68 console.log(` Period: ${PERIOD}`);
69 console.log(` Interval: ${INTERVAL} minutes`);
70
71 // Use the monitor's actual type, or fall back to the configured TYPE
72 const monitorType = (monitorData.jobType || TYPE) as "http" | "tcp";
73
74 // 2. Fetch metricsRegions (timeline data with region, timestamp, and quantiles)
75 const metricsRegionsResult =
76 monitorType === "http"
77 ? PERIOD === "7d"
78 ? await tb.httpMetricsRegionsWeekly({
79 monitorId: MONITOR_ID,
80 interval: INTERVAL,
81 })
82 : await tb.httpMetricsRegionsDaily({
83 monitorId: MONITOR_ID,
84 interval: INTERVAL,
85 })
86 : PERIOD === "7d"
87 ? await tb.tcpMetricsByIntervalWeekly({
88 monitorId: MONITOR_ID,
89 interval: INTERVAL,
90 })
91 : await tb.tcpMetricsByIntervalDaily({
92 monitorId: MONITOR_ID,
93 interval: INTERVAL,
94 });
95
96 console.log(
97 `\nFetched ${metricsRegionsResult.data.length} metrics regions data points`
98 );
99 if (metricsRegionsResult.data.length > 0) {
100 console.log(
101 ` First data point:`,
102 JSON.stringify(metricsRegionsResult.data[0], null, 2)
103 );
104 console.log(
105 ` Last data point:`,
106 JSON.stringify(
107 metricsRegionsResult.data[metricsRegionsResult.data.length - 1],
108 null,
109 2
110 )
111 );
112 } else {
113 console.log(` ⚠️ No data returned. This could mean:`);
114 console.log(` - The monitor hasn't collected any data yet`);
115 console.log(` - The monitor is inactive or was just created`);
116 console.log(
117 ` - There's no data in the selected time period (${PERIOD})`
118 );
119 console.log(
120 `\n 💡 Tip: Try querying without the interval parameter or using PERIOD="1d"`
121 );
122
123 // Try without interval to see if that helps
124 console.log(`\n Trying without interval parameter...`);
125 const retryResult =
126 monitorType === "http"
127 ? PERIOD === "7d"
128 ? await tb.httpMetricsRegionsWeekly({
129 monitorId: MONITOR_ID,
130 })
131 : await tb.httpMetricsRegionsDaily({
132 monitorId: MONITOR_ID,
133 })
134 : PERIOD === "7d"
135 ? await tb.tcpMetricsByIntervalWeekly({
136 monitorId: MONITOR_ID,
137 })
138 : await tb.tcpMetricsByIntervalDaily({
139 monitorId: MONITOR_ID,
140 });
141 console.log(` Retry returned ${retryResult.data.length} data points`);
142 if (retryResult.data.length > 0) {
143 console.log(` ✅ Success! The interval parameter might be the issue.`);
144 console.log(
145 ` First data point:`,
146 JSON.stringify(retryResult.data[0], null, 2)
147 );
148 }
149 }
150
151 // 3. Fetch metricsByRegion (summary data by region)
152 const metricsByRegionProcedure =
153 monitorType === "http"
154 ? PERIOD === "7d"
155 ? tb.httpMetricsByRegionWeekly
156 : tb.httpMetricsByRegionDaily
157 : PERIOD === "7d"
158 ? tb.tcpMetricsByRegionWeekly
159 : tb.tcpMetricsByRegionDaily;
160
161 const metricsByRegionsResult = await metricsByRegionProcedure({
162 monitorId: MONITOR_ID,
163 });
164
165 console.log(
166 `\nFetched ${metricsByRegionsResult.data.length} metrics by region data points`
167 );
168 if (metricsByRegionsResult.data.length > 0) {
169 console.log(
170 ` Sample:`,
171 JSON.stringify(metricsByRegionsResult.data.slice(0, 3), null, 2)
172 );
173 }
174
175 // 4. Transform metricsRegions data to match expected format
176 // Group by timestamp and pivot regions as columns
177 const timelineMap = new Map<number, Record<string, number | string>>();
178
179 for (const row of metricsRegionsResult.data) {
180 const timestamp = row.timestamp;
181 const region = row.region;
182 const latency = row[`${PERCENTILE}Latency`] ?? 0;
183
184 if (!timelineMap.has(timestamp)) {
185 timelineMap.set(timestamp, {
186 timestamp: new Date(timestamp).toISOString(),
187 });
188 }
189
190 const entry = timelineMap.get(timestamp)!;
191 entry[region] = latency;
192 }
193
194 // Convert map to sorted array
195 const timelineData = Array.from(timelineMap.values()).sort((a, b) => {
196 const timeA = new Date(a.timestamp as string).getTime();
197 const timeB = new Date(b.timestamp as string).getTime();
198 return timeA - timeB;
199 });
200
201 // 5. Build final output structure
202 const output = {
203 regions: allRegions,
204 data: {
205 regions: allRegions,
206 data: timelineData,
207 },
208 metricsByRegions: metricsByRegionsResult.data,
209 };
210
211 // 6. Write to file
212 const outputPath = resolve(process.cwd(), OUTPUT_FILE);
213 writeFileSync(outputPath, JSON.stringify(output, null, 2));
214
215 console.log(`\n✅ Data exported successfully to: ${outputPath}`);
216 console.log(`Total timeline entries: ${timelineData.length}`);
217 console.log(
218 `Total regions (including private locations): ${allRegions.length}`
219 );
220}
221
222// Run the script
223main().catch((error) => {
224 console.error("Error:", error);
225 process.exit(1);
226});