From 28d014c8ae68dd8e2f3f834e88c4dc3d8da5624e Mon Sep 17 00:00:00 2001 From: Maximilian Kaske Date: Thu, 15 Oct 2026 14:32:29 +0200 Subject: [PATCH] feat(tinybird): bucket status at 15 minutes so every timezone regroups into days Every current IANA offset is a multiple of 15 minutes, so 15m buckets fold into local days for India, Nepal, Adelaide, Chatham and Newfoundland too. Drops the whole-hour validation on both the client and the endpoints; the pipes, datasources and getters are renamed *_status_15m. --- .../src/libs/test/doubles/tinybird.mock.ts | 10 +- ...urce => mv__dns_status_15m__v0.datasource} | 2 +- ...rce => mv__grpc_status_15m__v0.datasource} | 2 +- ...rce => mv__http_status_15m__v0.datasource} | 2 +- ...rce => mv__icmp_status_15m__v0.datasource} | 2 +- ...urce => mv__tcp_status_15m__v0.datasource} | 2 +- .../endpoint__dns_status_15m__v0.pipe | 18 ++++ .../endpoint__dns_status_60m__v0.pipe | 28 ------ .../endpoint__grpc_status_15m__v0.pipe | 18 ++++ .../endpoint__grpc_status_60m__v0.pipe | 28 ------ .../endpoint__http_status_15m__v0.pipe | 18 ++++ .../endpoint__http_status_60m__v0.pipe | 28 ------ .../endpoint__icmp_status_15m__v0.pipe | 18 ++++ .../endpoint__icmp_status_60m__v0.pipe | 28 ------ .../endpoint__tcp_status_15m__v0.pipe | 18 ++++ .../endpoint__tcp_status_60m__v0.pipe | 28 ------ ...ipe => aggregate__dns_status_15m__v0.pipe} | 4 +- ...pe => aggregate__grpc_status_15m__v0.pipe} | 4 +- ...pe => aggregate__http_status_15m__v0.pipe} | 4 +- ...pe => aggregate__icmp_status_15m__v0.pipe} | 4 +- ...ipe => aggregate__tcp_status_15m__v0.pipe} | 4 +- packages/tinybird/src/client.test.ts | 89 ++++++------------ packages/tinybird/src/client.ts | 94 ++++++------------- 23 files changed, 167 insertions(+), 286 deletions(-) rename packages/tinybird/datasources/{mv__dns_status_60m__v0.datasource => mv__dns_status_15m__v0.datasource} (86%) rename packages/tinybird/datasources/{mv__grpc_status_60m__v0.datasource => mv__grpc_status_15m__v0.datasource} (86%) rename packages/tinybird/datasources/{mv__http_status_60m__v0.datasource => mv__http_status_15m__v0.datasource} (86%) rename packages/tinybird/datasources/{mv__icmp_status_60m__v0.datasource => mv__icmp_status_15m__v0.datasource} (86%) rename packages/tinybird/datasources/{mv__tcp_status_60m__v0.datasource => mv__tcp_status_15m__v0.datasource} (86%) create mode 100644 packages/tinybird/endpoints/endpoint__dns_status_15m__v0.pipe delete mode 100644 packages/tinybird/endpoints/endpoint__dns_status_60m__v0.pipe create mode 100644 packages/tinybird/endpoints/endpoint__grpc_status_15m__v0.pipe delete mode 100644 packages/tinybird/endpoints/endpoint__grpc_status_60m__v0.pipe create mode 100644 packages/tinybird/endpoints/endpoint__http_status_15m__v0.pipe delete mode 100644 packages/tinybird/endpoints/endpoint__http_status_60m__v0.pipe create mode 100644 packages/tinybird/endpoints/endpoint__icmp_status_15m__v0.pipe delete mode 100644 packages/tinybird/endpoints/endpoint__icmp_status_60m__v0.pipe create mode 100644 packages/tinybird/endpoints/endpoint__tcp_status_15m__v0.pipe delete mode 100644 packages/tinybird/endpoints/endpoint__tcp_status_60m__v0.pipe rename packages/tinybird/materializations/{aggregate__dns_status_60m__v0.pipe => aggregate__dns_status_15m__v0.pipe} (73%) rename packages/tinybird/materializations/{aggregate__grpc_status_60m__v0.pipe => aggregate__grpc_status_15m__v0.pipe} (73%) rename packages/tinybird/materializations/{aggregate__http_status_60m__v0.pipe => aggregate__http_status_15m__v0.pipe} (73%) rename packages/tinybird/materializations/{aggregate__icmp_status_60m__v0.pipe => aggregate__icmp_status_15m__v0.pipe} (73%) rename packages/tinybird/materializations/{aggregate__tcp_status_60m__v0.pipe => aggregate__tcp_status_15m__v0.pipe} (73%) diff --git a/apps/server/src/libs/test/doubles/tinybird.mock.ts b/apps/server/src/libs/test/doubles/tinybird.mock.ts index a0aa672ad..c3c47e39a 100644 --- a/apps/server/src/libs/test/doubles/tinybird.mock.ts +++ b/apps/server/src/libs/test/doubles/tinybird.mock.ts @@ -17,19 +17,19 @@ export class OSTinybird { get legacy_tcpStatus45d() { return empty; } - get httpStatus60m() { + get httpStatus15m() { return empty; } - get tcpStatus60m() { + get tcpStatus15m() { return empty; } - get dnsStatus60m() { + get dnsStatus15m() { return empty; } - get icmpStatus60m() { + get icmpStatus15m() { return empty; } - get grpcStatus60m() { + get grpcStatus15m() { return empty; } get httpListBiweekly() { diff --git a/packages/tinybird/datasources/mv__dns_status_60m__v0.datasource b/packages/tinybird/datasources/mv__dns_status_15m__v0.datasource similarity index 86% rename from packages/tinybird/datasources/mv__dns_status_60m__v0.datasource rename to packages/tinybird/datasources/mv__dns_status_15m__v0.datasource index d9cb8d762..bf020ffb8 100644 --- a/packages/tinybird/datasources/mv__dns_status_60m__v0.datasource +++ b/packages/tinybird/datasources/mv__dns_status_15m__v0.datasource @@ -1,4 +1,4 @@ -# Data Source created from Pipe 'aggregate__dns_status_60m__v0' +# Data Source created from Pipe 'aggregate__dns_status_15m__v0' SCHEMA > `time` DateTime('UTC'), diff --git a/packages/tinybird/datasources/mv__grpc_status_60m__v0.datasource b/packages/tinybird/datasources/mv__grpc_status_15m__v0.datasource similarity index 86% rename from packages/tinybird/datasources/mv__grpc_status_60m__v0.datasource rename to packages/tinybird/datasources/mv__grpc_status_15m__v0.datasource index be38d18a1..5b20b8b93 100644 --- a/packages/tinybird/datasources/mv__grpc_status_60m__v0.datasource +++ b/packages/tinybird/datasources/mv__grpc_status_15m__v0.datasource @@ -1,4 +1,4 @@ -# Data Source created from Pipe 'aggregate__grpc_status_60m__v0' +# Data Source created from Pipe 'aggregate__grpc_status_15m__v0' SCHEMA > `time` DateTime('UTC'), diff --git a/packages/tinybird/datasources/mv__http_status_60m__v0.datasource b/packages/tinybird/datasources/mv__http_status_15m__v0.datasource similarity index 86% rename from packages/tinybird/datasources/mv__http_status_60m__v0.datasource rename to packages/tinybird/datasources/mv__http_status_15m__v0.datasource index d3571779c..b9c5643f9 100644 --- a/packages/tinybird/datasources/mv__http_status_60m__v0.datasource +++ b/packages/tinybird/datasources/mv__http_status_15m__v0.datasource @@ -1,4 +1,4 @@ -# Data Source created from Pipe 'aggregate__http_status_60m__v0' +# Data Source created from Pipe 'aggregate__http_status_15m__v0' SCHEMA > `time` DateTime('UTC'), diff --git a/packages/tinybird/datasources/mv__icmp_status_60m__v0.datasource b/packages/tinybird/datasources/mv__icmp_status_15m__v0.datasource similarity index 86% rename from packages/tinybird/datasources/mv__icmp_status_60m__v0.datasource rename to packages/tinybird/datasources/mv__icmp_status_15m__v0.datasource index 6881189f5..685e2743b 100644 --- a/packages/tinybird/datasources/mv__icmp_status_60m__v0.datasource +++ b/packages/tinybird/datasources/mv__icmp_status_15m__v0.datasource @@ -1,4 +1,4 @@ -# Data Source created from Pipe 'aggregate__icmp_status_60m__v0' +# Data Source created from Pipe 'aggregate__icmp_status_15m__v0' SCHEMA > `time` DateTime('UTC'), diff --git a/packages/tinybird/datasources/mv__tcp_status_60m__v0.datasource b/packages/tinybird/datasources/mv__tcp_status_15m__v0.datasource similarity index 86% rename from packages/tinybird/datasources/mv__tcp_status_60m__v0.datasource rename to packages/tinybird/datasources/mv__tcp_status_15m__v0.datasource index c169c777e..76e73bd95 100644 --- a/packages/tinybird/datasources/mv__tcp_status_60m__v0.datasource +++ b/packages/tinybird/datasources/mv__tcp_status_15m__v0.datasource @@ -1,4 +1,4 @@ -# Data Source created from Pipe 'aggregate__tcp_status_60m__v0' +# Data Source created from Pipe 'aggregate__tcp_status_15m__v0' SCHEMA > `time` DateTime('UTC'), diff --git a/packages/tinybird/endpoints/endpoint__dns_status_15m__v0.pipe b/packages/tinybird/endpoints/endpoint__dns_status_15m__v0.pipe new file mode 100644 index 000000000..af35a85b7 --- /dev/null +++ b/packages/tinybird/endpoints/endpoint__dns_status_15m__v0.pipe @@ -0,0 +1,18 @@ +NODE endpoint +SQL > + +% +SELECT + -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes + toDate(toTimeZone(time, {{ String(timezone, 'UTC') }})) AS day, + monitorId, + countMerge(count) AS count, + countMerge(success) AS ok, + countMerge(error) AS error, + countMerge(degraded) AS degraded +FROM mv__dns_status_15m__v0 +WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} +GROUP BY day, monitorId +ORDER BY day DESC + +TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__dns_status_60m__v0.pipe b/packages/tinybird/endpoints/endpoint__dns_status_60m__v0.pipe deleted file mode 100644 index a7673fc10..000000000 --- a/packages/tinybird/endpoints/endpoint__dns_status_60m__v0.pipe +++ /dev/null @@ -1,28 +0,0 @@ -NODE endpoint -SQL > - -% -WITH {{ String(timezone, 'UTC') }} AS tz -SELECT - -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes - toDate(toTimeZone(time, tz)) AS day, - monitorId, - countMerge(count) AS count, - countMerge(success) AS ok, - countMerge(error) AS error, - countMerge(degraded) AS degraded -FROM mv__dns_status_60m__v0 -WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} - -- hourly buckets only regroup cleanly into days when every offset the zone - -- uses is a whole hour; January and July cover both sides of DST - AND ( - SELECT throwIf( - timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 1, 1, 0, 0, 0), tz)) % 3600 != 0 - OR timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 7, 1, 0, 0, 0), tz)) % 3600 != 0, - 'timezone must be an IANA name whose UTC offset is a whole hour' - ) - ) = 0 -GROUP BY day, monitorId -ORDER BY day DESC - -TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__grpc_status_15m__v0.pipe b/packages/tinybird/endpoints/endpoint__grpc_status_15m__v0.pipe new file mode 100644 index 000000000..cb1b0332f --- /dev/null +++ b/packages/tinybird/endpoints/endpoint__grpc_status_15m__v0.pipe @@ -0,0 +1,18 @@ +NODE endpoint +SQL > + +% +SELECT + -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes + toDate(toTimeZone(time, {{ String(timezone, 'UTC') }})) AS day, + monitorId, + countMerge(count) AS count, + countMerge(success) AS ok, + countMerge(error) AS error, + countMerge(degraded) AS degraded +FROM mv__grpc_status_15m__v0 +WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} +GROUP BY day, monitorId +ORDER BY day DESC + +TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__grpc_status_60m__v0.pipe b/packages/tinybird/endpoints/endpoint__grpc_status_60m__v0.pipe deleted file mode 100644 index b14797a31..000000000 --- a/packages/tinybird/endpoints/endpoint__grpc_status_60m__v0.pipe +++ /dev/null @@ -1,28 +0,0 @@ -NODE endpoint -SQL > - -% -WITH {{ String(timezone, 'UTC') }} AS tz -SELECT - -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes - toDate(toTimeZone(time, tz)) AS day, - monitorId, - countMerge(count) AS count, - countMerge(success) AS ok, - countMerge(error) AS error, - countMerge(degraded) AS degraded -FROM mv__grpc_status_60m__v0 -WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} - -- hourly buckets only regroup cleanly into days when every offset the zone - -- uses is a whole hour; January and July cover both sides of DST - AND ( - SELECT throwIf( - timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 1, 1, 0, 0, 0), tz)) % 3600 != 0 - OR timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 7, 1, 0, 0, 0), tz)) % 3600 != 0, - 'timezone must be an IANA name whose UTC offset is a whole hour' - ) - ) = 0 -GROUP BY day, monitorId -ORDER BY day DESC - -TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__http_status_15m__v0.pipe b/packages/tinybird/endpoints/endpoint__http_status_15m__v0.pipe new file mode 100644 index 000000000..18a10cd0b --- /dev/null +++ b/packages/tinybird/endpoints/endpoint__http_status_15m__v0.pipe @@ -0,0 +1,18 @@ +NODE endpoint +SQL > + +% +SELECT + -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes + toDate(toTimeZone(time, {{ String(timezone, 'UTC') }})) AS day, + monitorId, + countMerge(count) AS count, + countMerge(success) AS ok, + countMerge(error) AS error, + countMerge(degraded) AS degraded +FROM mv__http_status_15m__v0 +WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} +GROUP BY day, monitorId +ORDER BY day DESC + +TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__http_status_60m__v0.pipe b/packages/tinybird/endpoints/endpoint__http_status_60m__v0.pipe deleted file mode 100644 index 06c566d5f..000000000 --- a/packages/tinybird/endpoints/endpoint__http_status_60m__v0.pipe +++ /dev/null @@ -1,28 +0,0 @@ -NODE endpoint -SQL > - -% -WITH {{ String(timezone, 'UTC') }} AS tz -SELECT - -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes - toDate(toTimeZone(time, tz)) AS day, - monitorId, - countMerge(count) AS count, - countMerge(success) AS ok, - countMerge(error) AS error, - countMerge(degraded) AS degraded -FROM mv__http_status_60m__v0 -WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} - -- hourly buckets only regroup cleanly into days when every offset the zone - -- uses is a whole hour; January and July cover both sides of DST - AND ( - SELECT throwIf( - timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 1, 1, 0, 0, 0), tz)) % 3600 != 0 - OR timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 7, 1, 0, 0, 0), tz)) % 3600 != 0, - 'timezone must be an IANA name whose UTC offset is a whole hour' - ) - ) = 0 -GROUP BY day, monitorId -ORDER BY day DESC - -TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__icmp_status_15m__v0.pipe b/packages/tinybird/endpoints/endpoint__icmp_status_15m__v0.pipe new file mode 100644 index 000000000..91efe72cd --- /dev/null +++ b/packages/tinybird/endpoints/endpoint__icmp_status_15m__v0.pipe @@ -0,0 +1,18 @@ +NODE endpoint +SQL > + +% +SELECT + -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes + toDate(toTimeZone(time, {{ String(timezone, 'UTC') }})) AS day, + monitorId, + countMerge(count) AS count, + countMerge(success) AS ok, + countMerge(error) AS error, + countMerge(degraded) AS degraded +FROM mv__icmp_status_15m__v0 +WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} +GROUP BY day, monitorId +ORDER BY day DESC + +TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__icmp_status_60m__v0.pipe b/packages/tinybird/endpoints/endpoint__icmp_status_60m__v0.pipe deleted file mode 100644 index 61e454ee0..000000000 --- a/packages/tinybird/endpoints/endpoint__icmp_status_60m__v0.pipe +++ /dev/null @@ -1,28 +0,0 @@ -NODE endpoint -SQL > - -% -WITH {{ String(timezone, 'UTC') }} AS tz -SELECT - -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes - toDate(toTimeZone(time, tz)) AS day, - monitorId, - countMerge(count) AS count, - countMerge(success) AS ok, - countMerge(error) AS error, - countMerge(degraded) AS degraded -FROM mv__icmp_status_60m__v0 -WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} - -- hourly buckets only regroup cleanly into days when every offset the zone - -- uses is a whole hour; January and July cover both sides of DST - AND ( - SELECT throwIf( - timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 1, 1, 0, 0, 0), tz)) % 3600 != 0 - OR timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 7, 1, 0, 0, 0), tz)) % 3600 != 0, - 'timezone must be an IANA name whose UTC offset is a whole hour' - ) - ) = 0 -GROUP BY day, monitorId -ORDER BY day DESC - -TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__tcp_status_15m__v0.pipe b/packages/tinybird/endpoints/endpoint__tcp_status_15m__v0.pipe new file mode 100644 index 000000000..43275fe0f --- /dev/null +++ b/packages/tinybird/endpoints/endpoint__tcp_status_15m__v0.pipe @@ -0,0 +1,18 @@ +NODE endpoint +SQL > + +% +SELECT + -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes + toDate(toTimeZone(time, {{ String(timezone, 'UTC') }})) AS day, + monitorId, + countMerge(count) AS count, + countMerge(success) AS ok, + countMerge(error) AS error, + countMerge(degraded) AS degraded +FROM mv__tcp_status_15m__v0 +WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} +GROUP BY day, monitorId +ORDER BY day DESC + +TYPE endpoint diff --git a/packages/tinybird/endpoints/endpoint__tcp_status_60m__v0.pipe b/packages/tinybird/endpoints/endpoint__tcp_status_60m__v0.pipe deleted file mode 100644 index df0bfe047..000000000 --- a/packages/tinybird/endpoints/endpoint__tcp_status_60m__v0.pipe +++ /dev/null @@ -1,28 +0,0 @@ -NODE endpoint -SQL > - -% -WITH {{ String(timezone, 'UTC') }} AS tz -SELECT - -- calendar date of the local day; the client re-keys it as UTC midnight like the 45d pipes - toDate(toTimeZone(time, tz)) AS day, - monitorId, - countMerge(count) AS count, - countMerge(success) AS ok, - countMerge(error) AS error, - countMerge(degraded) AS degraded -FROM mv__tcp_status_60m__v0 -WHERE monitorId IN {{ Array(monitorIds, 'String', '1,666') }} - -- hourly buckets only regroup cleanly into days when every offset the zone - -- uses is a whole hour; January and July cover both sides of DST - AND ( - SELECT throwIf( - timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 1, 1, 0, 0, 0), tz)) % 3600 != 0 - OR timeZoneOffset(toTimeZone(makeDateTime(toYear(now()), 7, 1, 0, 0, 0), tz)) % 3600 != 0, - 'timezone must be an IANA name whose UTC offset is a whole hour' - ) - ) = 0 -GROUP BY day, monitorId -ORDER BY day DESC - -TYPE endpoint diff --git a/packages/tinybird/materializations/aggregate__dns_status_60m__v0.pipe b/packages/tinybird/materializations/aggregate__dns_status_15m__v0.pipe similarity index 73% rename from packages/tinybird/materializations/aggregate__dns_status_60m__v0.pipe rename to packages/tinybird/materializations/aggregate__dns_status_15m__v0.pipe index 4443d0dd9..30b8977cb 100644 --- a/packages/tinybird/materializations/aggregate__dns_status_60m__v0.pipe +++ b/packages/tinybird/materializations/aggregate__dns_status_15m__v0.pipe @@ -2,7 +2,7 @@ NODE aggregate SQL > SELECT - toStartOfHour(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, + toStartOfFifteenMinutes(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, monitorId, countState() AS count, countState(if(requestStatus = 'success', 1, NULL)) AS success, @@ -14,4 +14,4 @@ SQL > monitorId TYPE materialized -DATASOURCE mv__dns_status_60m__v0 +DATASOURCE mv__dns_status_15m__v0 diff --git a/packages/tinybird/materializations/aggregate__grpc_status_60m__v0.pipe b/packages/tinybird/materializations/aggregate__grpc_status_15m__v0.pipe similarity index 73% rename from packages/tinybird/materializations/aggregate__grpc_status_60m__v0.pipe rename to packages/tinybird/materializations/aggregate__grpc_status_15m__v0.pipe index bb3a1dd55..e09ad94a7 100644 --- a/packages/tinybird/materializations/aggregate__grpc_status_60m__v0.pipe +++ b/packages/tinybird/materializations/aggregate__grpc_status_15m__v0.pipe @@ -2,7 +2,7 @@ NODE aggregate SQL > SELECT - toStartOfHour(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, + toStartOfFifteenMinutes(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, monitorId, countState() AS count, countState(if(requestStatus = 'success', 1, NULL)) AS success, @@ -14,4 +14,4 @@ SQL > monitorId TYPE materialized -DATASOURCE mv__grpc_status_60m__v0 +DATASOURCE mv__grpc_status_15m__v0 diff --git a/packages/tinybird/materializations/aggregate__http_status_60m__v0.pipe b/packages/tinybird/materializations/aggregate__http_status_15m__v0.pipe similarity index 73% rename from packages/tinybird/materializations/aggregate__http_status_60m__v0.pipe rename to packages/tinybird/materializations/aggregate__http_status_15m__v0.pipe index 618974ad2..e27b3f29d 100644 --- a/packages/tinybird/materializations/aggregate__http_status_60m__v0.pipe +++ b/packages/tinybird/materializations/aggregate__http_status_15m__v0.pipe @@ -2,7 +2,7 @@ NODE aggregate SQL > SELECT - toStartOfHour(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, + toStartOfFifteenMinutes(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, monitorId, countState() AS count, countState(if(requestStatus = 'success', 1, NULL)) AS success, @@ -14,4 +14,4 @@ SQL > monitorId TYPE materialized -DATASOURCE mv__http_status_60m__v0 +DATASOURCE mv__http_status_15m__v0 diff --git a/packages/tinybird/materializations/aggregate__icmp_status_60m__v0.pipe b/packages/tinybird/materializations/aggregate__icmp_status_15m__v0.pipe similarity index 73% rename from packages/tinybird/materializations/aggregate__icmp_status_60m__v0.pipe rename to packages/tinybird/materializations/aggregate__icmp_status_15m__v0.pipe index 762926ab5..96a25b801 100644 --- a/packages/tinybird/materializations/aggregate__icmp_status_60m__v0.pipe +++ b/packages/tinybird/materializations/aggregate__icmp_status_15m__v0.pipe @@ -2,7 +2,7 @@ NODE aggregate SQL > SELECT - toStartOfHour(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, + toStartOfFifteenMinutes(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, monitorId, countState() AS count, countState(if(requestStatus = 'success', 1, NULL)) AS success, @@ -14,4 +14,4 @@ SQL > monitorId TYPE materialized -DATASOURCE mv__icmp_status_60m__v0 +DATASOURCE mv__icmp_status_15m__v0 diff --git a/packages/tinybird/materializations/aggregate__tcp_status_60m__v0.pipe b/packages/tinybird/materializations/aggregate__tcp_status_15m__v0.pipe similarity index 73% rename from packages/tinybird/materializations/aggregate__tcp_status_60m__v0.pipe rename to packages/tinybird/materializations/aggregate__tcp_status_15m__v0.pipe index 3629ff41b..794c2928e 100644 --- a/packages/tinybird/materializations/aggregate__tcp_status_60m__v0.pipe +++ b/packages/tinybird/materializations/aggregate__tcp_status_15m__v0.pipe @@ -2,7 +2,7 @@ NODE aggregate SQL > SELECT - toStartOfHour(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, + toStartOfFifteenMinutes(toTimeZone(fromUnixTimestamp64Milli(cronTimestamp), 'UTC')) AS time, monitorId, countState() AS count, countState(if(requestStatus = 'success', 1, NULL)) AS success, @@ -14,4 +14,4 @@ SQL > monitorId TYPE materialized -DATASOURCE mv__tcp_status_60m__v0 +DATASOURCE mv__tcp_status_15m__v0 diff --git a/packages/tinybird/src/client.test.ts b/packages/tinybird/src/client.test.ts index f6f156c23..1bf010db5 100644 --- a/packages/tinybird/src/client.test.ts +++ b/packages/tinybird/src/client.test.ts @@ -3,11 +3,10 @@ import { describe, test } from "@std/testing/bdd"; import { z } from "zod"; import { - isWholeHourTimeZone, noopFlagSchema, OSTinybird, - status60mParameters, - status60mShape, + status15mParameters, + status15mShape, } from "./client"; describe("noopFlagSchema", () => { @@ -164,66 +163,38 @@ describe("response-log facet pipes", () => { } }); -const status60mGetters = [ - "httpStatus60m", - "tcpStatus60m", - "dnsStatus60m", - "icmpStatus60m", - "grpcStatus60m", +const status15mGetters = [ + "httpStatus15m", + "tcpStatus15m", + "dnsStatus15m", + "icmpStatus15m", + "grpcStatus15m", ] as const; -describe("isWholeHourTimeZone", () => { - test("accepts zones whose standard and DST offsets are both whole hours", () => { - for (const tz of ["UTC", "Etc/GMT+1", "Asia/Tokyo", "Europe/Berlin"]) { - expect(isWholeHourTimeZone(tz)).toBe(true); - } - expect(isWholeHourTimeZone("America/New_York")).toBe(true); - expect(isWholeHourTimeZone("Australia/Sydney")).toBe(true); - }); - - test("rejects half- and quarter-hour offsets, on either side of DST", () => { - expect(isWholeHourTimeZone("Asia/Kolkata")).toBe(false); - expect(isWholeHourTimeZone("Asia/Kathmandu")).toBe(false); - expect(isWholeHourTimeZone("Pacific/Chatham")).toBe(false); - // +10:30 standard, +11:00 daylight: whole-hour for half the year only - expect(isWholeHourTimeZone("Australia/Lord_Howe")).toBe(false); - }); - - test("rejects unknown zones and fixed offsets instead of throwing", () => { - expect(isWholeHourTimeZone("Mars/Olympus")).toBe(false); - expect(isWholeHourTimeZone("")).toBe(false); - }); -}); - -describe("status60mParameters", () => { +describe("status15mParameters", () => { test("defaults timezone to UTC", () => { - expect(status60mParameters.parse({ monitorIds: ["1"] })).toEqual({ + expect(status15mParameters.parse({ monitorIds: ["1"] })).toEqual({ monitorIds: ["1"], timezone: "UTC", }); }); - test("forwards a whole-hour timezone untouched", () => { - expect( - status60mParameters.parse({ - monitorIds: ["1", "2"], - timezone: "Europe/Berlin", - }).timezone, - ).toBe("Europe/Berlin"); - }); - - test("names the whole-hour requirement when rejecting", () => { - const result = status60mParameters.safeParse({ - monitorIds: ["1"], - timezone: "Asia/Kolkata", - }); - expect(result.success).toBe(false); - expect(JSON.stringify(result.error?.issues)).toContain("whole hour"); + test("forwards any zone untouched, fractional offsets included", () => { + for (const timezone of [ + "Europe/Berlin", + "Asia/Kolkata", + "Asia/Kathmandu", + ]) { + expect( + status15mParameters.parse({ monitorIds: ["1", "2"], timezone }) + .timezone, + ).toBe(timezone); + } }); }); -describe("status60mShape", () => { - const shape = status60mShape(z.coerce.string()); +describe("status15mShape", () => { + const shape = status15mShape(z.coerce.string()); test("re-keys the local calendar date as UTC midnight, like the 45d pipes", () => { const row = shape.parse({ @@ -269,19 +240,13 @@ describe("status60mShape", () => { }); }); -describe("hourly status pipes", () => { +describe("15m status pipes", () => { const tb = new OSTinybird({ token: "a-token" }); - for (const name of status60mGetters) { - test(`${name} validates timezone before the request`, async () => { - await expect( - tb[name]({ monitorIds: ["1"], timezone: "Asia/Kolkata" }), - ).rejects.toThrow("whole hour"); - }); - - test(`${name} resolves for whole-hour zones and when timezone is omitted`, async () => { + for (const name of status15mGetters) { + test(`${name} resolves for any zone and when timezone is omitted`, async () => { expect( - await tb[name]({ monitorIds: ["1"], timezone: "Europe/Berlin" }), + await tb[name]({ monitorIds: ["1"], timezone: "Asia/Kolkata" }), ).toEqual({ meta: [], data: [] }); expect(await tb[name]({ monitorIds: ["1"] })).toEqual({ meta: [], diff --git a/packages/tinybird/src/client.ts b/packages/tinybird/src/client.ts index bce249a11..8443a1069 100644 --- a/packages/tinybird/src/client.ts +++ b/packages/tinybird/src/client.ts @@ -14,45 +14,11 @@ const PUBLIC_CACHE = 300; // 5 * 60 = 300s = 5m const DEV_CACHE = 10 * 60; // 10m const REVALIDATE = env.NODE_ENV === "development" ? DEV_CACHE : 0; -// The 60m pipes group UTC-hour buckets into local days, which only lines up -// when every offset the zone uses is a whole number of hours. January and -// July cover both sides of DST. The endpoints enforce the same rule server-side. -const wholeHourTimeZoneCache = new Map(); - -export function isWholeHourTimeZone(timeZone: string): boolean { - const cached = wholeHourTimeZoneCache.get(timeZone); - if (cached !== undefined) return cached; - - let format: Intl.DateTimeFormat; - try { - format = new Intl.DateTimeFormat("en-US", { - timeZone, - timeZoneName: "longOffset", - }); - } catch (error) { - if (error instanceof RangeError) return false; - throw error; - } - const year = new Date().getUTCFullYear(); - const result = [0, 6].every((month) => { - const offset = format - .formatToParts(new Date(Date.UTC(year, month, 1))) - .find((p) => p.type === "timeZoneName")?.value; - return offset === "GMT" || /^GMT[+-]\d{2}:00$/.test(offset ?? ""); - }); - wholeHourTimeZoneCache.set(timeZone, result); - return result; -} - -export const status60mParameters = z.object({ +// 15-minute buckets regroup into local days for every IANA zone: all current +// offsets are multiples of 15 minutes. ClickHouse rejects unknown zone names. +export const status15mParameters = z.object({ monitorIds: z.string().array(), - timezone: z - .string() - .refine( - isWholeHourTimeZone, - "timezone must be an IANA name whose UTC offset is a whole hour", - ) - .prefault("UTC"), + timezone: z.string().prefault("UTC"), }); /** @@ -60,7 +26,7 @@ export const status60mParameters = z.object({ * UTC midnight, the convention day-keyed consumers already read — a Berlin * 2026-10-14 stays "2026-10-14", not the 22:00Z instant of local midnight. */ -export function status60mShape(monitorId: z.ZodType) { +export function status15mShape(monitorId: z.ZodType) { return z.object({ day: z .string() @@ -867,12 +833,12 @@ export class OSTinybird { }); } - /** Hourly buckets regrouped into days of `timezone`; see {@link status60mShape}. */ - public get httpStatus60m() { + /** 15m buckets regrouped into days of `timezone`; see {@link status15mShape}. */ + public get httpStatus15m() { return this.tb.buildPipe({ - pipe: "endpoint__http_status_60m__v0", - parameters: status60mParameters, - data: status60mShape(z.string()), + pipe: "endpoint__http_status_15m__v0", + parameters: status15mParameters, + data: status15mShape(z.string()), opts: { next: { revalidate: REVALIDATE } }, }); } @@ -1444,12 +1410,12 @@ export class OSTinybird { }); } - /** Hourly buckets regrouped into days of `timezone`; see {@link status60mShape}. */ - public get tcpStatus60m() { + /** 15m buckets regrouped into days of `timezone`; see {@link status15mShape}. */ + public get tcpStatus15m() { return this.tb.buildPipe({ - pipe: "endpoint__tcp_status_60m__v0", - parameters: status60mParameters, - data: status60mShape(z.coerce.string()), + pipe: "endpoint__tcp_status_15m__v0", + parameters: status15mParameters, + data: status15mShape(z.coerce.string()), opts: { next: { revalidate: REVALIDATE } }, }); } @@ -1854,12 +1820,12 @@ export class OSTinybird { }); } - /** Hourly buckets regrouped into days of `timezone`; see {@link status60mShape}. */ - public get icmpStatus60m() { + /** 15m buckets regrouped into days of `timezone`; see {@link status15mShape}. */ + public get icmpStatus15m() { return this.tb.buildPipe({ - pipe: "endpoint__icmp_status_60m__v0", - parameters: status60mParameters, - data: status60mShape(z.coerce.string()), + pipe: "endpoint__icmp_status_15m__v0", + parameters: status15mParameters, + data: status15mShape(z.coerce.string()), opts: { next: { revalidate: REVALIDATE } }, }); } @@ -2335,12 +2301,12 @@ export class OSTinybird { }); } - /** Hourly buckets regrouped into days of `timezone`; see {@link status60mShape}. */ - public get grpcStatus60m() { + /** 15m buckets regrouped into days of `timezone`; see {@link status15mShape}. */ + public get grpcStatus15m() { return this.tb.buildPipe({ - pipe: "endpoint__grpc_status_60m__v0", - parameters: status60mParameters, - data: status60mShape(z.coerce.string()), + pipe: "endpoint__grpc_status_15m__v0", + parameters: status15mParameters, + data: status15mShape(z.coerce.string()), opts: { next: { revalidate: REVALIDATE } }, }); } @@ -3631,12 +3597,12 @@ export class OSTinybird { }); } - /** Hourly buckets regrouped into days of `timezone`; see {@link status60mShape}. */ - public get dnsStatus60m() { + /** 15m buckets regrouped into days of `timezone`; see {@link status15mShape}. */ + public get dnsStatus15m() { return this.tb.buildPipe({ - pipe: "endpoint__dns_status_60m__v0", - parameters: status60mParameters, - data: status60mShape(z.coerce.string()), + pipe: "endpoint__dns_status_15m__v0", + parameters: status15mParameters, + data: status15mShape(z.coerce.string()), opts: { next: { revalidate: REVALIDATE } }, }); } -- 2.51.2