diff --git a/.changeset/fix-telemetry-endpoint.md b/.changeset/fix-telemetry-endpoint.md new file mode 100644 index 000000000..190a6ae06 --- /dev/null +++ b/.changeset/fix-telemetry-endpoint.md @@ -0,0 +1,6 @@ +--- +"@pgflow/core": patch +"pgflow": patch +--- + +Fix the telemetry endpoint: send CLI install events and daily database reports to `https://telemetry.pgflow.dev` instead of the unresolvable `pgflow-telemetry.workers.dev` hostname. Existing 0.17.1 installations get the corrected `pgflow_telemetry.report()` function through a new additive migration; opted-out databases stay opted out. Patch-level behavior: opt-out flags, local/CI suppression, timeouts, and payload limits are unchanged. diff --git a/apps/telemetry-worker/src/index.test.ts b/apps/telemetry-worker/src/index.test.ts index be87c9bc2..60eb5428c 100644 --- a/apps/telemetry-worker/src/index.test.ts +++ b/apps/telemetry-worker/src/index.test.ts @@ -24,7 +24,7 @@ function post( contentType = 'application/json', ): Promise { return worker.fetch( - new Request('https://pgflow-telemetry.workers.dev/', { + new Request('https://telemetry.pgflow.dev/', { method: 'POST', headers: { 'content-type': contentType }, body: raw ? (body as string) : JSON.stringify(body), @@ -92,7 +92,7 @@ describe('telemetry ingest', () => { it('rejects GET with 405', async () => { const { env } = makeEnv(); const res = await worker.fetch( - new Request('https://pgflow-telemetry.workers.dev/', { method: 'GET' }), + new Request('https://telemetry.pgflow.dev/', { method: 'GET' }), env, ); expect(res.status).toBe(405); diff --git a/pkgs/cli/__tests__/commands/install/report-install-telemetry.test.ts b/pkgs/cli/__tests__/commands/install/report-install-telemetry.test.ts index 88185dbef..4d3f675f9 100644 --- a/pkgs/cli/__tests__/commands/install/report-install-telemetry.test.ts +++ b/pkgs/cli/__tests__/commands/install/report-install-telemetry.test.ts @@ -18,7 +18,7 @@ describe('reportInstallTelemetry', () => { expect(send).toHaveBeenCalledOnce(); const [url, request] = send.mock.calls[0] as [string, RequestInit]; - expect(url).toBe('https://pgflow-telemetry.workers.dev/'); + expect(url).toBe('https://telemetry.pgflow.dev/'); expect(request.method).toBe('POST'); expect(JSON.parse(request.body as string)).toEqual({ schema: 1, diff --git a/pkgs/cli/src/commands/install/report-install-telemetry.ts b/pkgs/cli/src/commands/install/report-install-telemetry.ts index 473a3e42a..3d80dcace 100644 --- a/pkgs/cli/src/commands/install/report-install-telemetry.ts +++ b/pkgs/cli/src/commands/install/report-install-telemetry.ts @@ -1,7 +1,7 @@ import { getVersion } from '../../utils/get-version.js'; import type { MigrationInstallResult } from './copy-migrations.js'; -const ENDPOINT = 'https://pgflow-telemetry.workers.dev/'; +const ENDPOINT = 'https://telemetry.pgflow.dev/'; const SEMVER_RE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/; type Environment = Record; diff --git a/pkgs/core/schemas/0135_function_report.sql b/pkgs/core/schemas/0135_function_report.sql index 2d94b7afa..d87726e14 100644 --- a/pkgs/core/schemas/0135_function_report.sql +++ b/pkgs/core/schemas/0135_function_report.sql @@ -66,7 +66,7 @@ begin -- prune rolls the queued request back with everything else in this block. begin v_request_id := net.http_post( - url => 'https://pgflow-telemetry.workers.dev', + url => 'https://telemetry.pgflow.dev', body => v_payload, headers => jsonb_build_object('Content-Type', 'application/json'), timeout_milliseconds => 5000 diff --git a/pkgs/core/supabase/migrations/20261001194538_pgflow_telemetry_endpoint_fix.sql b/pkgs/core/supabase/migrations/20261001194538_pgflow_telemetry_endpoint_fix.sql new file mode 100644 index 000000000..b28c4c4d4 --- /dev/null +++ b/pkgs/core/supabase/migrations/20261001194538_pgflow_telemetry_endpoint_fix.sql @@ -0,0 +1,74 @@ +-- Modify "report" function +CREATE OR REPLACE FUNCTION "pgflow_telemetry"."report" () RETURNS text LANGUAGE plpgsql SET "search_path" = '' AS $$ +declare + v_day date := current_date - 1; + v_payload jsonb; + v_request_id bigint; +begin + -- Every failure path, including the gates below, returns a status: + -- query cancellation (SQLSTATE 57014) or any gate failure must not + -- escape this function (regression-tested in report.test.sql). + begin + if not exists ( + select 1 from pgflow.runs r + where r.started_at >= v_day and r.started_at < v_day + 1 + ) then + return 'skipped: inactive day'; + end if; + + if exists ( + select 1 from pgflow_telemetry.sent_reports s where s.day = v_day + ) then + return 'skipped: already reported'; + end if; + + -- Local CLI stack (developer machines, CI on supabase start) never sends. + if pgflow.is_local() then + return 'skipped: local'; + end if; + exception + when query_canceled then + return 'error: canceled'; + when others then + return 'error: gate failed'; + end; + + begin + v_payload := pgflow_telemetry.build_payload(v_day); + -- build_payload enforces the receiver's limits; this guard keeps a + -- future regression from queueing a body the receiver would reject. + if jsonb_array_length(v_payload->'contributions') > 64 + or octet_length(v_payload::text) > 2048 then + return 'error: build failed'; + end if; + exception + when query_canceled then + return 'error: canceled'; + when others then + return 'error: build failed'; + end; + + -- pg_net queues transactionally: a failure in the audit insert or the + -- prune rolls the queued request back with everything else in this block. + begin + v_request_id := net.http_post( + url => 'https://telemetry.pgflow.dev', + body => v_payload, + headers => jsonb_build_object('Content-Type', 'application/json'), + timeout_milliseconds => 5000 + ); + insert into pgflow_telemetry.sent_reports (day, payload, request_id) + values (v_day, v_payload, v_request_id); + + delete from pgflow_telemetry.sent_reports + where day < current_date - 90; + exception + when query_canceled then + return 'error: canceled'; + when others then + return 'error: send failed'; + end; + + return 'sent: ' || v_request_id; +end +$$; diff --git a/pkgs/core/supabase/migrations/atlas.sum b/pkgs/core/supabase/migrations/atlas.sum index 9480fdac3..771681f6d 100644 --- a/pkgs/core/supabase/migrations/atlas.sum +++ b/pkgs/core/supabase/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:QaPHwmoNhloJfusgYJ5s9h5QS7cwqav//Qak1RSWtDw= +h1:sOlj3XB6+Tss5pInYvuwe/yxDhipAVGK/ZF49e+gLAo= 20250429164909_pgflow_initial.sql h1:I3n/tQIg5Q5nLg7RDoU3BzqHvFVjmumQxVNbXTPG15s= 20250517072017_pgflow_fix_poll_for_tasks_to_use_separate_statement_for_polling.sql h1:wTuXuwMxVniCr3ONCpodpVWJcHktoQZIbqMZ3sUHKMY= 20250609105135_pgflow_add_start_tasks_and_started_status.sql h1:ggGanW4Wyt8Kv6TWjnZ00/qVb3sm+/eFVDjGfT8qyPg= @@ -25,3 +25,4 @@ h1:QaPHwmoNhloJfusgYJ5s9h5QS7cwqav//Qak1RSWtDw= 20260915074120_pgflow_private_step_queues.sql h1:+vsfsOyDaM8WISO/jxp4UBlTzPuQhg6RwBCiOAk5YAE= 20260919152659_pgflow_fix_force_skip_multi_queue.sql h1:DC7uQRwjpOm3A4NtDYb3SjbSSqQix4MlQ4+BnhGQ/Q8= 20260920093533_pgflow_telemetry.sql h1:dp3gNRSUpTNoPH9w4n9P7CBhXFoJspQ+hP9MfQ1u3Fg= +20261001194538_pgflow_telemetry_endpoint_fix.sql h1:/ROJiUnHzOt6xsw9f877wY6KlrVOFxpjGBuFuweX6Is= diff --git a/pkgs/core/supabase/tests/telemetry/report.test.sql b/pkgs/core/supabase/tests/telemetry/report.test.sql index f3ce69b1a..0a1c2ac32 100644 --- a/pkgs/core/supabase/tests/telemetry/report.test.sql +++ b/pkgs/core/supabase/tests/telemetry/report.test.sql @@ -78,7 +78,7 @@ select is( select is( ( select count(*) from net.http_request_queue q - where q.url like 'https://pgflow-telemetry.workers.dev%' + where q.url like 'https://telemetry.pgflow.dev%' and convert_from(q.body, 'UTF8')::jsonb = pgflow_telemetry.preview(current_date - 1) ), 1::bigint, @@ -117,7 +117,7 @@ select is( select is( ( select count(*) from net.http_request_queue - where url like 'https://pgflow-telemetry.workers.dev%' + where url like 'https://telemetry.pgflow.dev%' ), 0::bigint, 'failed send rolls the pg_net queue insert back' @@ -266,7 +266,7 @@ select is( select is( ( select count(*) from net.http_request_queue - where url like 'https://pgflow-telemetry.workers.dev%' + where url like 'https://telemetry.pgflow.dev%' ), 0::bigint, 'ordinary gate failure queues no HTTP request' diff --git a/pkgs/website/src/content/docs/reference/telemetry.mdx b/pkgs/website/src/content/docs/reference/telemetry.mdx index 9517b9954..b10b37f5e 100644 --- a/pkgs/website/src/content/docs/reference/telemetry.mdx +++ b/pkgs/website/src/content/docs/reference/telemetry.mdx @@ -115,9 +115,17 @@ select pgflow_telemetry.enable(); -- resume daily database reports Local development databases (`supabase start`) never send telemetry, and days without runs send nothing. The scheduled job bounds itself with a 5-second statement timeout. If you call `pgflow_telemetry.report()` manually, set your own `statement_timeout` first: PostgreSQL cannot arm a timeout that changes inside a running function. +## Upgrading from 0.17.1 + +pgflow 0.17.1 pointed both senders at `https://pgflow-telemetry.workers.dev`, a hostname that does not resolve. The corrected endpoint is `https://telemetry.pgflow.dev`. + +- Databases get the corrected daily sender through a new pgflow migration that replaces `pgflow_telemetry.report()`. Update pgflow, rerun `pgflow install` to copy the migration, and apply it with your normal Supabase migration workflow. Databases that disabled telemetry stay disabled: the migration replaces only the function and never reschedules the reporting job. +- The corrected installation sender ships in the pgflow CLI patch release; update the CLI you invoke (`npx pgflow@latest`). +- Deploying the collector endpoint alone does not repair 0.17.1: its senders keep using the unresolvable hostname until you upgrade. + ## Privacy -- Both signals go to `https://pgflow-telemetry.workers.dev`, a [Cloudflare Worker](https://workers.cloudflare.com/) backed by [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/). +- Both signals go to `https://telemetry.pgflow.dev`, a [Cloudflare Worker](https://workers.cloudflare.com/) backed by [Workers Analytics Engine](https://developers.cloudflare.com/analytics/analytics-engine/). - Stored data expires automatically after three months. - The `sent_reports` audit table in your database is separate: its 90-day prune runs only as part of a successful daily report. Disabled databases and databases with inactive days keep their audit rows until you delete them yourself. - pgflow sends no identifiers and the telemetry application stores no transport metadata, including IP addresses or user agents. Cloudflare necessarily processes source IPs to carry the network request.