diff --git a/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts b/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts index d52bf70e..1d3546a0 100644 --- a/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts +++ b/apps/server/src/routes/rpc/handlers/monitor/validators.test.ts @@ -165,10 +165,10 @@ describe("validateCommonMonitorFields", () => { test("drops an unspecified region instead of rejecting it", () => { // Documents current behaviour, which is weaker than it looks: // `regionsToStrings` filters unmapped enum values to "", so the - // "Invalid regions" error below can never fire on the RPC path — - // every proto Region that maps to a non-empty string is in - // AVAILABLE_REGIONS. An unknown region is silently dropped and the - // monitor is created with fewer regions than the caller asked for. + // "Invalid regions" error can never fire on the RPC path — every proto + // Region maps either to AVAILABLE_REGIONS or to a deprecated region, + // which the deprecation check rejects first. An unknown region is + // silently dropped and the monitor is created with fewer regions. expect(() => validateCommonMonitorFields({ regions: [Region.UNSPECIFIED] }), ).not.toThrow(); diff --git a/packages/db/script/region-migration.test.ts b/packages/db/script/region-migration.test.ts index 29981071..38c6bc26 100644 --- a/packages/db/script/region-migration.test.ts +++ b/packages/db/script/region-migration.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, test } from "@std/testing/bdd"; import type { z } from "zod"; import type { monitorRegionSchema } from "../src/schema/constants"; -import { updateRegion } from "./region-migration"; +import { applyRegionRemaps, updateRegion } from "./region-migration"; // Import the types we need @@ -59,6 +59,17 @@ describe("updateRegion", () => { expect(regions).toEqual(["ams", "sin"]); }); + test("should replace every bom when it is listed twice", () => { + const regions: z.infer[] = [ + "bom", + "ams", + "bom", + ]; + applyRegionRemaps(regions); + + expect(regions).toEqual(["sin", "ams"]); + }); + test("should drop bom when sin is already selected", () => { const regions: z.infer[] = [ "bom", @@ -181,38 +192,14 @@ describe("updateRegion", () => { "yul", "yyz", ] as z.infer[]; - // Asia Pacific - updateRegion("hkg", "sin", newRegions); - - // North America - updateRegion("atl", "dfw", newRegions); - updateRegion("mia", "dfw", newRegions); - updateRegion("gdl", "dfw", newRegions); - updateRegion("qro", "dfw", newRegions); - updateRegion("bos", "ewr", newRegions); - updateRegion("phx", "lax", newRegions); - updateRegion("sea", "sjc", newRegions); - updateRegion("yul", "yyz", newRegions); - - // Europe - updateRegion("waw", "ams", newRegions); - updateRegion("mad", "cdg", newRegions); - updateRegion("otp", "fra", newRegions); - - // South America - updateRegion("bog", "gru", newRegions); - updateRegion("gig", "gru", newRegions); - updateRegion("scl", "gru", newRegions); - updateRegion("eze", "gru", newRegions); + applyRegionRemaps(newRegions); // Should be the same reference (mutated) expect(newRegions).toEqual([ "ams", "arn", - "bom", "cdg", - "den", "dfw", "ewr", "fra", diff --git a/packages/db/script/region-migration.ts b/packages/db/script/region-migration.ts index 0e96cfbd..b4521bf2 100644 --- a/packages/db/script/region-migration.ts +++ b/packages/db/script/region-migration.ts @@ -4,6 +4,42 @@ import { db, eq, schema } from "../src"; import { selectMonitorSchema } from "../src/schema"; import type { monitorRegionSchema } from "../src/schema/constants"; +type MonitorRegion = z.infer; + +// Every deprecated region and its replacement. Earlier entries already ran; +// they stay because re-applying them is a no-op. +export const REGION_REMAPS: ReadonlyArray<[MonitorRegion, MonitorRegion]> = [ + // Asia Pacific + ["hkg", "sin"], + ["bom", "sin"], + // North America + ["atl", "dfw"], + ["mia", "dfw"], + ["gdl", "dfw"], + ["qro", "dfw"], + ["bos", "ewr"], + ["phx", "lax"], + ["sea", "sjc"], + ["yul", "yyz"], + ["den", "dfw"], + // Europe + ["waw", "ams"], + ["mad", "cdg"], + ["otp", "fra"], + // South America + ["bog", "gru"], + ["gig", "gru"], + ["scl", "gru"], + ["eze", "gru"], +]; + +export function applyRegionRemaps(regions: MonitorRegion[]) { + for (const [from, to] of REGION_REMAPS) { + // `updateRegion` handles one occurrence; a region can be listed twice. + while (regions.includes(from)) updateRegion(from, to, regions); + } +} + // Only run against the DB when executed directly, not when imported by tests. if (import.meta.main) { const rawMonitors = await db.select().from(schema.monitor); @@ -11,8 +47,7 @@ if (import.meta.main) { const monitors = z.array(selectMonitorSchema).parse(rawMonitors); for (const monitor of monitors) { const regions = monitor.regions.slice(); - // Asia Pacific - updateRegion("bom", "sin", regions); + applyRegionRemaps(regions); const newRegions = regions.join(","); if (newRegions === monitor.regions.join(",")) continue; await db diff --git a/packages/db/src/seed/limits.ts b/packages/db/src/seed/limits.ts index 245a66fe..8bf74b21 100644 --- a/packages/db/src/seed/limits.ts +++ b/packages/db/src/seed/limits.ts @@ -29,7 +29,6 @@ export const TEAM_WORKSPACE_LIMITS = JSON.stringify({ "arn", "atl", "bog", - "bom", "bos", "cdg", "den", diff --git a/packages/importers/package.json b/packages/importers/package.json index 6d960986..fc2ce10a 100644 --- a/packages/importers/package.json +++ b/packages/importers/package.json @@ -25,6 +25,7 @@ "zod": "catalog:" }, "devDependencies": { + "@openstatus/regions": "workspace:*", "@openstatus/tsconfig": "workspace:*", "@std/expect": "jsr:^1.0.19", "@std/testing": "jsr:^1.0.19", diff --git a/packages/importers/src/providers/betterstack/mapper.test.ts b/packages/importers/src/providers/betterstack/mapper.test.ts index 1d805155..028d01bc 100644 --- a/packages/importers/src/providers/betterstack/mapper.test.ts +++ b/packages/importers/src/providers/betterstack/mapper.test.ts @@ -1,3 +1,4 @@ +import { regionDict } from "@openstatus/regions"; import { expect } from "@std/expect"; import { describe, test } from "@std/testing/bdd"; @@ -28,6 +29,7 @@ import { mapResource, mapSection, mapStatusPage, + REGION_MAP, } from "./mapper"; describe("mapFrequency", () => { @@ -62,6 +64,17 @@ describe("mapRegions", () => { expect(mapRegions(["us", "eu", "as", "au"])).toBe("iad,fra,sin,syd"); }); + test("only targets regions that are still available", () => { + for (const [location, region] of Object.entries(REGION_MAP)) { + const info = regionDict[region as keyof typeof regionDict]; + expect({ location, region, deprecated: info?.deprecated }).toEqual({ + location, + region, + deprecated: false, + }); + } + }); + test("returns default for unknown regions", () => { expect(mapRegions([])).toBe("iad"); expect(mapRegions(["unknown"])).toBe("iad"); diff --git a/packages/importers/src/providers/betterstack/mapper.ts b/packages/importers/src/providers/betterstack/mapper.ts index 4701bdb0..4c14bfad 100644 --- a/packages/importers/src/providers/betterstack/mapper.ts +++ b/packages/importers/src/providers/betterstack/mapper.ts @@ -27,7 +27,8 @@ const FREQUENCY_MAP: Record = { 3600: "1h", }; -const REGION_MAP: Record = { +// Imports write regions unvalidated, so every target must be non-deprecated. +export const REGION_MAP: Record = { us: "iad", eu: "fra", as: "sin", diff --git a/packages/importers/src/providers/checkly/mapper.test.ts b/packages/importers/src/providers/checkly/mapper.test.ts index a73f4ce4..f7123a6b 100644 --- a/packages/importers/src/providers/checkly/mapper.test.ts +++ b/packages/importers/src/providers/checkly/mapper.test.ts @@ -1,3 +1,4 @@ +import { regionDict } from "@openstatus/regions"; import { expect } from "@std/expect"; import { describe, test } from "@std/testing/bdd"; @@ -18,6 +19,7 @@ import { mapPage, mapRegions, mapService, + REGION_MAP, } from "./mapper"; describe("mapCheckType", () => { @@ -57,6 +59,23 @@ describe("mapRegions", () => { expect(mapRegions(["us-east-1", "eu-west-1"])).toBe("iad,lhr"); }); + test("only targets regions that are still available", () => { + for (const [location, region] of Object.entries(REGION_MAP)) { + const info = regionDict[region as keyof typeof regionDict]; + expect({ location, region, deprecated: info?.deprecated }).toEqual({ + location, + region, + deprecated: false, + }); + } + }); + + test("maps deprecated targets to their replacements", () => { + expect(mapRegions(["ap-south-1", "us-west-2", "ca-central-1"])).toBe( + "sin,sjc,yyz", + ); + }); + test("dedupes collapsed regions", () => { expect(mapRegions(["us-east-1", "us-east-2"])).toBe("iad"); }); diff --git a/packages/importers/src/providers/checkly/mapper.ts b/packages/importers/src/providers/checkly/mapper.ts index 58cfbfe1..46da5d97 100644 --- a/packages/importers/src/providers/checkly/mapper.ts +++ b/packages/importers/src/providers/checkly/mapper.ts @@ -53,19 +53,20 @@ export function mapFrequency(minutes: number): string { return FREQUENCY_MAP[closest] ?? "10m"; } -// AWS region codes (Checkly locations) → Fly regions (OpenStatus). -const REGION_MAP: Record = { +// AWS region codes (Checkly locations) → Fly regions (OpenStatus). Imports +// write regions unvalidated, so every target must be a non-deprecated region. +export const REGION_MAP: Record = { "us-east-1": "iad", "us-east-2": "iad", "us-west-1": "sjc", - "us-west-2": "sea", - "ca-central-1": "yul", + "us-west-2": "sjc", + "ca-central-1": "yyz", "eu-west-1": "lhr", "eu-west-2": "lhr", "eu-west-3": "cdg", "eu-central-1": "fra", "eu-north-1": "arn", - "ap-south-1": "bom", + "ap-south-1": "sin", "ap-southeast-1": "sin", "ap-southeast-2": "syd", "ap-northeast-1": "nrt", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2753dcee..98948c25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2320,6 +2320,9 @@ importers: specifier: 'catalog:' version: 4.6.5 devDependencies: + '@openstatus/regions': + specifier: workspace:* + version: link:../regions '@openstatus/tsconfig': specifier: workspace:* version: link:../tsconfig