From bd7ca9a5bbd57e7a4452cfb496f6b6c3d0f448e4 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 19 Nov 2025 11:50:59 +0100 Subject: [PATCH 01/19] =?UTF-8?q?=F0=9F=91=B7=20replace=20monitor=20checks?= =?UTF-8?q?=20by=20logs=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy/check-monitors.ts | 72 +++++++++++++++++++------------- scripts/lib/datacenter.ts | 15 ------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/scripts/deploy/check-monitors.ts b/scripts/deploy/check-monitors.ts index f70e36afdf..356106e613 100644 --- a/scripts/deploy/check-monitors.ts +++ b/scripts/deploy/check-monitors.ts @@ -5,50 +5,66 @@ */ import { printLog, runMain, fetchHandlingError } from '../lib/executionUtils.ts' import { getTelemetryOrgApiKey, getTelemetryOrgApplicationKey } from '../lib/secrets.ts' -import { monitorIdsByDatacenter, siteByDatacenter } from '../lib/datacenter.ts' - -interface MonitorStatus { - id: number - name: string - overall_state: string -} +import { siteByDatacenter } from '../lib/datacenter.ts' +import { browserSdkVersion } from '../lib/browserSdkVersion.ts' const datacenters = process.argv[2].split(',') runMain(async () => { for (const datacenter of datacenters) { - if (!monitorIdsByDatacenter[datacenter]) { - printLog(`No monitors configured for datacenter ${datacenter}`) + const site = siteByDatacenter[datacenter] + const apiKey = getTelemetryOrgApiKey(site) + const applicationKey = getTelemetryOrgApplicationKey(site) + + if (!apiKey || !applicationKey) { + printLog(`No API key or application key found for ${site}, skipping...`) continue } - const monitorIds = monitorIdsByDatacenter[datacenter] - const site = siteByDatacenter[datacenter] - const monitorStatuses = await Promise.all(monitorIds.map((monitorId) => fetchMonitorStatus(site, monitorId))) - for (const monitorStatus of monitorStatuses) { - printLog(`${monitorStatus.overall_state} - ${monitorStatus.name}`) - if (monitorStatus.overall_state !== 'OK') { - throw new Error( - `Monitor ${monitorStatus.name} is in state ${monitorStatus.overall_state}, see ${computeMonitorLink(site, monitorStatus.id)}` - ) - } + + const errorLogsCount = await queryErrorLogsCount(site, apiKey, applicationKey) + + if (errorLogsCount > 0) { + throw new Error(`Errors found in the last 30 minutes, +see ${computeMonitorLink(site)}`) + } else { + printLog(`No errors found in the last 30 minutes for ${datacenter}`) } } }) -async function fetchMonitorStatus(site: string, monitorId: number): Promise { - const response = await fetchHandlingError(`https://api.${site}/api/v1/monitor/${monitorId}`, { - method: 'GET', +async function queryErrorLogsCount(site: string, apiKey: string, applicationKey: string): Promise { + const response = await fetchHandlingError(`https://api.${site}/api/v2/logs/events/search`, { + method: 'POST', headers: { - Accept: 'application/json', - 'DD-API-KEY': getTelemetryOrgApiKey(site), - 'DD-APPLICATION-KEY': getTelemetryOrgApplicationKey(site), + 'Content-Type': 'application/json', + 'DD-API-KEY': apiKey, + 'DD-APPLICATION-KEY': applicationKey, }, + body: JSON.stringify({ + filter: { + from: 'now-30m', + to: 'now', + query: `source:browser status:error version:${browserSdkVersion}`, + }, + }), }) - return response.json() as Promise + + const data = (await response.json()) as { data: unknown[] } + + return data.data.length } -function computeMonitorLink(site: string, monitorId: number): string { - return `https://${computeTelemetryOrgDomain(site)}/monitors/${monitorId}` +function computeMonitorLink(site: string): string { + const now = Date.now() + const thirtyMinutesAgo = now - 30 * 60 * 1000 + + const queryParams = new URLSearchParams({ + query: `source:browser status:error version:${browserSdkVersion}`, + from_ts: `${thirtyMinutesAgo}`, + to_ts: `${now}`, + }) + + return `https://${computeTelemetryOrgDomain(site)}/logs?${queryParams.toString()}` } function computeTelemetryOrgDomain(site: string): string { diff --git a/scripts/lib/datacenter.ts b/scripts/lib/datacenter.ts index 73a8692889..078e5d464f 100644 --- a/scripts/lib/datacenter.ts +++ b/scripts/lib/datacenter.ts @@ -7,18 +7,3 @@ export const siteByDatacenter: Record = { ap2: 'ap2.datadoghq.com', prtest00: 'prtest00.datad0g.com', } - -/** - * Each datacenter has 3 monitor IDs: - * - Telemetry errors - * - Telemetry errors on specific org - * - Telemetry errors on specific message - */ -export const monitorIdsByDatacenter: Record = { - us1: [72055549, 68975047, 110519972], - eu1: [5855803, 5663834, 9896387], - us3: [164368, 160677, 329066], - us5: [22388, 20646, 96049], - ap1: [858, 859, 2757030], - ap2: [1234, 1235, 1236], -} From 06a759e0b340126df6ddfe7e3971e7d7cca547ed Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 21 Nov 2025 07:26:40 +0100 Subject: [PATCH 02/19] =?UTF-8?q?=E2=9C=85=20add=20unit=20test=20for=20dep?= =?UTF-8?q?loy-prod-dc=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy/deploy-prod-dc.spec.ts | 69 +++++++++++++++++++++++++++ scripts/deploy/deploy-prod-dc.ts | 42 +++++++++------- 2 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 scripts/deploy/deploy-prod-dc.spec.ts diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts new file mode 100644 index 0000000000..346e3dd3a7 --- /dev/null +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import { beforeEach, before, describe, it, mock } from 'node:test' +import type { CommandDetail } from './lib/testHelpers.ts' +import { mockModule, mockCommandImplementation } from './lib/testHelpers.ts' + +// eslint-disable-next-line +describe.only('deploy-prod-dc', () => { + const commandMock = mock.fn() + + let commands: CommandDetail[] + + before(async () => { + await mockModule(path.resolve(import.meta.dirname, '../lib/command.ts'), { command: commandMock }) + await mockModule(path.resolve(import.meta.dirname, '../lib/executionUtils.ts'), { + timeout: () => Promise.resolve(), + }) + }) + + beforeEach(() => { + commands = mockCommandImplementation(commandMock) + }) + + it('should deploy a given datacenter', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'us1') + + assert.deepEqual(commands, [ + { command: 'node ./scripts/deploy/deploy.ts prod v6 us1' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 us1' }, + ]) + }) + + it('should deploy a given datacenter with check monitors', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'us1', '--check-monitors') + + assert.deepEqual(commands, [ + { command: 'node ./scripts/deploy/check-monitors.ts us1' }, + { command: 'node ./scripts/deploy/deploy.ts prod v6 us1' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 us1' }, + // 1 monitor check per minute for 30 minutes + ...Array.from({ length: 30 }, () => ({ command: 'node ./scripts/deploy/check-monitors.ts us1' })), + ]) + }) + + it('should only check monitors before deploying if the upload path is root', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'root', '--check-monitors') + + assert.deepEqual(commands, [ + { command: 'node ./scripts/deploy/check-monitors.ts root' }, + { command: 'node ./scripts/deploy/deploy.ts prod v6 root' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 root' }, + ]) + }) + + it('should deploy all minor datacenters', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'minor-dcs', '--no-check-monitors') + + assert.deepEqual(commands, [ + { command: 'node ./scripts/deploy/deploy.ts prod v6 us3,us5,ap1,ap2,prtest00' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 us3,us5,ap1,ap2,prtest00' }, + ]) + }) +}) + +async function runScript(scriptPath: string, ...args: string[]): Promise { + const { main } = (await import(scriptPath)) as { main: (...args: string[]) => Promise } + + return main(...args) +} diff --git a/scripts/deploy/deploy-prod-dc.ts b/scripts/deploy/deploy-prod-dc.ts index 56e83161c0..c192a1ce3f 100644 --- a/scripts/deploy/deploy-prod-dc.ts +++ b/scripts/deploy/deploy-prod-dc.ts @@ -21,27 +21,33 @@ function getAllMinorDcs(): string[] { return Object.keys(siteByDatacenter).filter((dc) => !MAJOR_DCS.includes(dc)) } -const { - values: { 'check-monitors': checkMonitors }, - positionals, -} = parseArgs({ - allowPositionals: true, - allowNegative: true, - options: { - 'check-monitors': { - type: 'boolean', +if (!process.env.NODE_TEST_CONTEXT) { + runMain(() => main(...process.argv.slice(2))) +} + +export async function main(...args: string[]): Promise { + const { + values: { 'check-monitors': checkMonitors }, + positionals, + } = parseArgs({ + args, + allowPositionals: true, + allowNegative: true, + options: { + 'check-monitors': { + type: 'boolean', + default: false, + }, }, - }, -}) + }) -const version = positionals[0] -const uploadPath = positionals[1] === 'minor-dcs' ? getAllMinorDcs().join(',') : positionals[1] + const version = positionals[0] + const uploadPath = positionals[1] === 'minor-dcs' ? getAllMinorDcs().join(',') : positionals[1] -if (!uploadPath) { - throw new Error('UPLOAD_PATH argument is required') -} + if (!uploadPath) { + throw new Error('UPLOAD_PATH argument is required') + } -runMain(async () => { if (checkMonitors) { command`node ./scripts/deploy/check-monitors.ts ${uploadPath}`.withLogs().run() } @@ -52,7 +58,7 @@ runMain(async () => { if (checkMonitors && uploadPath !== 'root') { await gateMonitors(uploadPath) } -}) +} async function gateMonitors(uploadPath: string): Promise { printLog(`Check monitors for ${uploadPath} during ${GATE_DURATION / ONE_MINUTE_IN_SECOND} minutes`) From 931ed9ef4e47e42aa905dbba2983fd2fe203c8f5 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 18 Dec 2025 09:28:47 +0100 Subject: [PATCH 03/19] =?UTF-8?q?fixup!=20=F0=9F=91=B7=20replace=20monitor?= =?UTF-8?q?=20checks=20by=20logs=20query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy/check-monitors.ts | 5 +++++ scripts/deploy/deploy-prod-dc.spec.ts | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/deploy/check-monitors.ts b/scripts/deploy/check-monitors.ts index 356106e613..b0e54afe18 100644 --- a/scripts/deploy/check-monitors.ts +++ b/scripts/deploy/check-monitors.ts @@ -13,6 +13,11 @@ const datacenters = process.argv[2].split(',') runMain(async () => { for (const datacenter of datacenters) { const site = siteByDatacenter[datacenter] + + if (!site) { + throw new Error(`No site found for datacenter ${datacenter}`) + } + const apiKey = getTelemetryOrgApiKey(site) const applicationKey = getTelemetryOrgApplicationKey(site) diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts index 346e3dd3a7..2dd5fe7d0a 100644 --- a/scripts/deploy/deploy-prod-dc.spec.ts +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -4,8 +4,7 @@ import { beforeEach, before, describe, it, mock } from 'node:test' import type { CommandDetail } from './lib/testHelpers.ts' import { mockModule, mockCommandImplementation } from './lib/testHelpers.ts' -// eslint-disable-next-line -describe.only('deploy-prod-dc', () => { +describe('deploy-prod-dc', () => { const commandMock = mock.fn() let commands: CommandDetail[] From 855e5464848c1c7a0c26193dfa512f04d93fe2dd Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 19 Dec 2025 14:52:06 +0100 Subject: [PATCH 04/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20check-mon?= =?UTF-8?q?itors=20scripts=20to=20query=20by=20orgs=20id=20and=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy/check-monitors.spec.ts | 125 ++++++++++++++++++++++++++ scripts/deploy/check-monitors.ts | 113 +++++++++++++++++++---- 2 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 scripts/deploy/check-monitors.spec.ts diff --git a/scripts/deploy/check-monitors.spec.ts b/scripts/deploy/check-monitors.spec.ts new file mode 100644 index 0000000000..200dd5507a --- /dev/null +++ b/scripts/deploy/check-monitors.spec.ts @@ -0,0 +1,125 @@ +import assert from 'node:assert/strict' +import path from 'node:path' +import type { Mock } from 'node:test' +import { afterEach, before, describe, it, mock } from 'node:test' +import type { fetchHandlingError } from 'scripts/lib/executionUtils.ts' +import { mockModule } from './lib/testHelpers.ts' +import type { QueryResultBucket } from './check-monitors.ts' + +const FAKE_API_KEY = 'FAKE_API_KEY' +const FAKE_APPLICATION_KEY = 'FAKE_APPLICATION_KEY' + +const NO_TELEMETRY_ERRORS_MOCK = [{ computes: { c0: 40 }, by: {} }] +const NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK = [ + { by: { '@org_id': 123456 }, computes: { c0: 22 } }, + { by: { '@org_id': 789012 }, computes: { c0: 3 } }, + { by: { '@org_id': 345678 }, computes: { c0: 3 } }, +] +const NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK = [ + { by: { 'issue.id': 'b4a4bf0c-e64a-11ef-aa16-da7ad0900002' }, computes: { c0: 16 } }, + { by: { 'issue.id': '0aedaf4a-da09-11ed-baef-da7ad0900002' }, computes: { c0: 9 } }, + { by: { 'issue.id': '118a6a40-1454-11f0-82c6-da7ad0900002' }, computes: { c0: 1 } }, + { by: { 'issue.id': '144e312a-919a-11ef-8519-da7ad0900002' }, computes: { c0: 1 } }, +] +const TELEMETRY_ERRORS_MOCK = [{ computes: { c0: 10000 }, by: {} }] +const TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK = [ + { by: { '@org_id': 123456 }, computes: { c0: 500 } }, + { by: { '@org_id': 789012 }, computes: { c0: 3 } }, + { by: { '@org_id': 345678 }, computes: { c0: 3 } }, +] +const TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK = [ + { by: { 'issue.id': 'b4a4bf0c-e64a-11ef-aa16-da7ad0900002' }, computes: { c0: 1600 } }, + { by: { 'issue.id': '0aedaf4a-da09-11ed-baef-da7ad0900002' }, computes: { c0: 9 } }, + { by: { 'issue.id': '118a6a40-1454-11f0-82c6-da7ad0900002' }, computes: { c0: 1 } }, + { by: { 'issue.id': '144e312a-919a-11ef-8519-da7ad0900002' }, computes: { c0: 1 } }, +] + +describe('check-monitors', () => { + const fetchHandlingErrorMock: Mock = mock.fn() + + function mockFetchHandlingError( + responseBuckets: [QueryResultBucket[], QueryResultBucket[], QueryResultBucket[]] + ): void { + for (let i = 0; i < 3; i++) { + fetchHandlingErrorMock.mock.mockImplementationOnce( + (_url: string, _options?: RequestInit) => + Promise.resolve({ + json: () => + Promise.resolve({ + data: { + buckets: responseBuckets[i], + }, + }), + } as unknown as Response), + i + ) + } + } + + before(async () => { + await mockModule(path.resolve(import.meta.dirname, '../lib/secrets.ts'), { + getTelemetryOrgApiKey: () => FAKE_API_KEY, + getTelemetryOrgApplicationKey: () => FAKE_APPLICATION_KEY, + }) + + await mockModule(path.resolve(import.meta.dirname, '../lib/executionUtils.ts'), { + fetchHandlingError: fetchHandlingErrorMock, + }) + }) + + afterEach(() => { + fetchHandlingErrorMock.mock.resetCalls() + }) + + it('should not throw an error if no telemetry errors are found for a given datacenter', async () => { + mockFetchHandlingError([ + NO_TELEMETRY_ERRORS_MOCK, + NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, + NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, + ]) + + await assert.doesNotReject(() => runScript('./check-monitors.ts', 'us1')) + }) + + it('should throw an error if telemetry errors are found for a given datacenter', async () => { + mockFetchHandlingError([ + TELEMETRY_ERRORS_MOCK, + NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, + NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, + ]) + + await assert.rejects(() => runScript('./check-monitors.ts', 'us1'), /Telemetry errors found in the last 5 minutes/) + }) + + it('should throw an error if telemetry errors on specific org are found for a given datacenter', async () => { + mockFetchHandlingError([ + NO_TELEMETRY_ERRORS_MOCK, + TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, + NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, + ]) + + await assert.rejects( + () => runScript('./check-monitors.ts', 'us1'), + /Telemetry errors on specific org found in the last 5 minutes/ + ) + }) + + it('should throw an error if telemetry errors on specific message are found for a given datacenter', async () => { + mockFetchHandlingError([ + NO_TELEMETRY_ERRORS_MOCK, + NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, + TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, + ]) + + await assert.rejects( + () => runScript('./check-monitors.ts', 'us1'), + /Telemetry error on specific message found in the last 5 minutes/ + ) + }) +}) + +async function runScript(scriptPath: string, ...args: string[]): Promise { + const { main } = (await import(scriptPath)) as { main: (...args: string[]) => Promise } + + return main(...args) +} diff --git a/scripts/deploy/check-monitors.ts b/scripts/deploy/check-monitors.ts index b0e54afe18..0b8b5284f0 100644 --- a/scripts/deploy/check-monitors.ts +++ b/scripts/deploy/check-monitors.ts @@ -8,9 +8,35 @@ import { getTelemetryOrgApiKey, getTelemetryOrgApplicationKey } from '../lib/sec import { siteByDatacenter } from '../lib/datacenter.ts' import { browserSdkVersion } from '../lib/browserSdkVersion.ts' -const datacenters = process.argv[2].split(',') +const TIME_WINDOW_IN_MINUTES = 5 +const BASE_QUERY = `source:browser status:error version:${browserSdkVersion}` +const QUERIES: Query[] = [ + { + name: 'Telemetry errors', + query: BASE_QUERY, + threshold: 300, + }, + { + name: 'Telemetry errors on specific org', + query: BASE_QUERY, + facet: '@org_id', + threshold: 100, + }, + { + name: 'Telemetry error on specific message', + query: BASE_QUERY, + facet: 'issue.id', + threshold: 100, + }, +] + +if (!process.env.NODE_TEST_CONTEXT) { + runMain(() => main(...process.argv.slice(2))) +} + +export async function main(...args: string[]): Promise { + const datacenters = args[0].split(',') -runMain(async () => { for (const datacenter of datacenters) { const site = siteByDatacenter[datacenter] @@ -26,19 +52,25 @@ runMain(async () => { continue } - const errorLogsCount = await queryErrorLogsCount(site, apiKey, applicationKey) + for (const query of QUERIES) { + const buckets = await queryLogsApi(site, apiKey, applicationKey, query) - if (errorLogsCount > 0) { - throw new Error(`Errors found in the last 30 minutes, -see ${computeMonitorLink(site)}`) - } else { - printLog(`No errors found in the last 30 minutes for ${datacenter}`) + // buckets are sorted by count, so we only need to check the first one + if (buckets[0]?.computes?.c0 > query.threshold) { + throw new Error(`${query.name} found in the last ${TIME_WINDOW_IN_MINUTES} minutes, +see ${computeLogsLink(site, query)}`) + } } } -}) +} -async function queryErrorLogsCount(site: string, apiKey: string, applicationKey: string): Promise { - const response = await fetchHandlingError(`https://api.${site}/api/v2/logs/events/search`, { +async function queryLogsApi( + site: string, + apiKey: string, + applicationKey: string, + query: Query +): Promise { + const response = await fetchHandlingError(`https://api.${site}/api/v2/logs/analytics/aggregate`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -46,26 +78,51 @@ async function queryErrorLogsCount(site: string, apiKey: string, applicationKey: 'DD-APPLICATION-KEY': applicationKey, }, body: JSON.stringify({ + compute: [ + { + aggregation: 'count', + }, + ], + ...(query.facet + ? { + group_by: [ + { + facet: query.facet, + sort: { + type: 'measure', + aggregation: 'count', + }, + }, + ], + } + : {}), filter: { - from: 'now-30m', + from: `now-${TIME_WINDOW_IN_MINUTES}m`, to: 'now', - query: `source:browser status:error version:${browserSdkVersion}`, + query: query.query, }, }), }) - const data = (await response.json()) as { data: unknown[] } + const data = (await response.json()) as QueryResult - return data.data.length + return data.data.buckets } -function computeMonitorLink(site: string): string { +function computeLogsLink(site: string, query: Query): string { const now = Date.now() - const thirtyMinutesAgo = now - 30 * 60 * 1000 + const timeWindowAgo = now - TIME_WINDOW_IN_MINUTES * 60 * 1000 const queryParams = new URLSearchParams({ - query: `source:browser status:error version:${browserSdkVersion}`, - from_ts: `${thirtyMinutesAgo}`, + query: query.query, + ...(query.facet + ? { + agg_q: query.facet, + agg_t: 'count', + viz: 'toplist', + } + : {}), + from_ts: `${timeWindowAgo}`, to_ts: `${now}`, }) @@ -81,3 +138,21 @@ function computeTelemetryOrgDomain(site: string): string { return site } } + +interface Query { + name: string + query: string + facet?: string + threshold: number +} + +interface QueryResult { + data: { + buckets: QueryResultBucket[] + } +} + +export interface QueryResultBucket { + by: { [key: string]: number | string } + computes: { c0: number } +} From 54cf67e4d2883f1edf6e6302c48d1f7cff7abae4 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Mon, 22 Dec 2025 09:11:57 +0100 Subject: [PATCH 05/19] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20update=20error=20?= =?UTF-8?q?handling=20in=20check-monitors=20script=20to=20log=20missing=20?= =?UTF-8?q?datacenter=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/deploy/check-monitors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/deploy/check-monitors.ts b/scripts/deploy/check-monitors.ts index 0b8b5284f0..bb7ce9fb8d 100644 --- a/scripts/deploy/check-monitors.ts +++ b/scripts/deploy/check-monitors.ts @@ -41,7 +41,8 @@ export async function main(...args: string[]): Promise { const site = siteByDatacenter[datacenter] if (!site) { - throw new Error(`No site found for datacenter ${datacenter}`) + printLog(`No site is configured for datacenter ${datacenter}. skipping...`) + continue } const apiKey = getTelemetryOrgApiKey(site) From 1f23c10dc4d4383cbea82932f985a9885ccc55f1 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 20 Nov 2025 09:12:21 +0100 Subject: [PATCH 06/19] =?UTF-8?q?=F0=9F=91=B7=20refactor=20deployment=20sc?= =?UTF-8?q?ripts=20to=20remove=20hardcoded=20list=20of=20DCs=20and=20sites?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 11 +++++ .gitlab/deploy-auto.yml | 10 ++-- .gitlab/deploy-manual.yml | 12 ++--- scripts/deploy/check-monitors.ts | 4 +- scripts/deploy/deploy-prod-dc.spec.ts | 35 +++++++++----- scripts/deploy/deploy-prod-dc.ts | 59 +++++++++++++++-------- scripts/deploy/lib/testHelpers.ts | 20 ++++++-- scripts/deploy/upload-source-maps.spec.ts | 18 +++++-- scripts/deploy/upload-source-maps.ts | 7 +-- scripts/lib/datacenter.ts | 58 +++++++++++++++++++--- 10 files changed, 169 insertions(+), 65 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index afb8956862..8ad4f22ff0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,6 +20,7 @@ cache: stages: - task - ci-image + - before-tests - test - after-tests - browserstack @@ -124,6 +125,16 @@ ci-image: - docker build --build-arg CHROME_PACKAGE_VERSION=$CHROME_PACKAGE_VERSION --tag $CI_IMAGE . - docker push $CI_IMAGE +list-datacenters: + stage: before-tests + extends: .base-configuration + script: + - | + TOKEN=$(curl --silent $VAULT_ADDR/v1/identity/oidc/token/runtime-metadata-service -H 'X-Vault-Request: true' | jq '.data.token' -r) + curl -X 'GET' 'https://runtime-metadata-service.us1.ddbuild.io/v2/datacenters' \ + -H "accept: application/json" \ + -H "Authorization: Bearer $TOKEN" + ######################################################################################################################## # Tests ######################################################################################################################## diff --git a/.gitlab/deploy-auto.yml b/.gitlab/deploy-auto.yml index 43b14a6cb3..c0713b3171 100644 --- a/.gitlab/deploy-auto.yml +++ b/.gitlab/deploy-auto.yml @@ -17,7 +17,7 @@ stages: - VERSION=$(node -p -e "require('./lerna.json').version") - yarn - yarn build:bundle - - node ./scripts/deploy/deploy-prod-dc.ts v${VERSION%%.*} $UPLOAD_PATH --check-monitors + - node ./scripts/deploy/deploy-prod-dc.ts v${VERSION%%.*} $DATACENTER --check-monitors step-1_deploy-prod-minor-dcs: when: manual @@ -26,7 +26,7 @@ step-1_deploy-prod-minor-dcs: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: minor-dcs + DATACENTER: minor-dcs step-2_deploy-prod-eu1: needs: @@ -35,7 +35,7 @@ step-2_deploy-prod-eu1: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: eu1 + DATACENTER: eu1 step-3_deploy-prod-us1: needs: @@ -44,7 +44,7 @@ step-3_deploy-prod-us1: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: us1 + DATACENTER: us1 step-4_deploy-prod-gov: needs: @@ -53,7 +53,7 @@ step-4_deploy-prod-gov: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: root + DATACENTER: gov step-5_publish-npm: needs: diff --git a/.gitlab/deploy-manual.yml b/.gitlab/deploy-manual.yml index b3f2849ccd..97b5656693 100644 --- a/.gitlab/deploy-manual.yml +++ b/.gitlab/deploy-manual.yml @@ -19,35 +19,35 @@ stages: - VERSION=$(node -p -e "require('./lerna.json').version") - yarn - yarn build:bundle - - node ./scripts/deploy/deploy-prod-dc.ts v${VERSION%%.*} $UPLOAD_PATH --no-check-monitors + - node ./scripts/deploy/deploy-prod-dc.ts v${VERSION%%.*} $DATACENTER --no-check-monitors step-1_deploy-prod-minor-dcs: extends: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: minor-dcs + DATACENTER: minor-dcs step-2_deploy-prod-eu1: extends: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: eu1 + DATACENTER: eu1 step-3_deploy-prod-us1: extends: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: us1 + DATACENTER: us1 step-4_deploy-prod-gov: extends: - .base-configuration - .deploy-prod variables: - UPLOAD_PATH: root + DATACENTER: root step-5_publish-npm: stage: deploy @@ -80,7 +80,7 @@ step-7_create-github-release: - node scripts/release/create-github-release.ts # This step is used to deploy the SDK to a new datacenter. -# the `UPLOAD_PATH` variable needs to be provided as an argument when starting the manual job +# the `DATACENTER` variable needs to be provided as an argument when starting the manual job optional_step-deploy-to-new-datacenter: extends: - .base-configuration diff --git a/scripts/deploy/check-monitors.ts b/scripts/deploy/check-monitors.ts index bb7ce9fb8d..4ff868d05e 100644 --- a/scripts/deploy/check-monitors.ts +++ b/scripts/deploy/check-monitors.ts @@ -5,7 +5,7 @@ */ import { printLog, runMain, fetchHandlingError } from '../lib/executionUtils.ts' import { getTelemetryOrgApiKey, getTelemetryOrgApplicationKey } from '../lib/secrets.ts' -import { siteByDatacenter } from '../lib/datacenter.ts' +import { getSite } from '../lib/datacenter.ts' import { browserSdkVersion } from '../lib/browserSdkVersion.ts' const TIME_WINDOW_IN_MINUTES = 5 @@ -38,7 +38,7 @@ export async function main(...args: string[]): Promise { const datacenters = args[0].split(',') for (const datacenter of datacenters) { - const site = siteByDatacenter[datacenter] + const site = getSite(datacenter) if (!site) { printLog(`No site is configured for datacenter ${datacenter}. skipping...`) diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts index 2dd5fe7d0a..b8ac24c022 100644 --- a/scripts/deploy/deploy-prod-dc.spec.ts +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -1,12 +1,11 @@ import assert from 'node:assert/strict' import path from 'node:path' -import { beforeEach, before, describe, it, mock } from 'node:test' +import { beforeEach, before, describe, it, mock, afterEach } from 'node:test' import type { CommandDetail } from './lib/testHelpers.ts' -import { mockModule, mockCommandImplementation } from './lib/testHelpers.ts' +import { mockCommandImplementation, mockModule } from './lib/testHelpers.ts' describe('deploy-prod-dc', () => { const commandMock = mock.fn() - let commands: CommandDetail[] before(async () => { @@ -20,6 +19,10 @@ describe('deploy-prod-dc', () => { commands = mockCommandImplementation(commandMock) }) + afterEach(() => { + mock.restoreAll() + }) + it('should deploy a given datacenter', async () => { await runScript('./deploy-prod-dc.ts', 'v6', 'us1') @@ -41,22 +44,30 @@ describe('deploy-prod-dc', () => { ]) }) - it('should only check monitors before deploying if the upload path is root', async () => { - await runScript('./deploy-prod-dc.ts', 'v6', 'root', '--check-monitors') + it('should deploy all minor datacenters', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'minor-dcs', '--no-check-monitors') assert.deepEqual(commands, [ - { command: 'node ./scripts/deploy/check-monitors.ts root' }, - { command: 'node ./scripts/deploy/deploy.ts prod v6 root' }, - { command: 'node ./scripts/deploy/upload-source-maps.ts v6 root' }, + { command: 'node ./scripts/deploy/deploy.ts prod v6 ap1,ap2,us3,us5' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 ap1,ap2,us3,us5' }, ]) }) - it('should deploy all minor datacenters', async () => { - await runScript('./deploy-prod-dc.ts', 'v6', 'minor-dcs', '--no-check-monitors') + it('should deploy all private regions', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'private-regions', '--no-check-monitors') + + assert.deepEqual(commands, [ + { command: 'node ./scripts/deploy/deploy.ts prod v6 prtest00,prtest01' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 prtest00,prtest01' }, + ]) + }) + + it('should deploy gov datacenters to the root upload path', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'gov', '--no-check-monitors') assert.deepEqual(commands, [ - { command: 'node ./scripts/deploy/deploy.ts prod v6 us3,us5,ap1,ap2,prtest00' }, - { command: 'node ./scripts/deploy/upload-source-maps.ts v6 us3,us5,ap1,ap2,prtest00' }, + { command: 'node ./scripts/deploy/deploy.ts prod v6 root' }, + { command: 'node ./scripts/deploy/upload-source-maps.ts v6 root' }, ]) }) }) diff --git a/scripts/deploy/deploy-prod-dc.ts b/scripts/deploy/deploy-prod-dc.ts index c192a1ce3f..a87700dd94 100644 --- a/scripts/deploy/deploy-prod-dc.ts +++ b/scripts/deploy/deploy-prod-dc.ts @@ -1,7 +1,7 @@ import { parseArgs } from 'node:util' import { printLog, runMain, timeout } from '../lib/executionUtils.ts' import { command } from '../lib/command.ts' -import { siteByDatacenter } from '../lib/datacenter.ts' +import { getAllMinorDcs, getAllPrivateDcs } from '../lib/datacenter.ts' /** * Orchestrate the deployments of the artifacts for specific DCs @@ -12,15 +12,6 @@ const ONE_MINUTE_IN_SECOND = 60 const GATE_DURATION = 30 * ONE_MINUTE_IN_SECOND const GATE_INTERVAL = ONE_MINUTE_IN_SECOND -// Major DCs are the ones that are deployed last. -// They have their own step jobs in `deploy-manual.yml` and `deploy-auto.yml`. -const MAJOR_DCS = ['root', 'us1', 'eu1'] - -// Minor DCs are all the DCs from `siteByDatacenter` that are not in `MAJOR_DCS`. -function getAllMinorDcs(): string[] { - return Object.keys(siteByDatacenter).filter((dc) => !MAJOR_DCS.includes(dc)) -} - if (!process.env.NODE_TEST_CONTEXT) { runMain(() => main(...process.argv.slice(2))) } @@ -42,30 +33,56 @@ export async function main(...args: string[]): Promise { }) const version = positionals[0] - const uploadPath = positionals[1] === 'minor-dcs' ? getAllMinorDcs().join(',') : positionals[1] + const datacenters = getDatacenters(positionals[1]) - if (!uploadPath) { - throw new Error('UPLOAD_PATH argument is required') + if (!datacenters) { + throw new Error('DATACENTER argument is required') } if (checkMonitors) { - command`node ./scripts/deploy/check-monitors.ts ${uploadPath}`.withLogs().run() + command`node ./scripts/deploy/check-monitors.ts ${datacenters.join(',')}`.withLogs().run() } - command`node ./scripts/deploy/deploy.ts prod ${version} ${uploadPath}`.withLogs().run() - command`node ./scripts/deploy/upload-source-maps.ts ${version} ${uploadPath}`.withLogs().run() + const uploadPathTypes = toDatacenterUploadPathType(datacenters).join(',') - if (checkMonitors && uploadPath !== 'root') { - await gateMonitors(uploadPath) + command`node ./scripts/deploy/deploy.ts prod ${version} ${uploadPathTypes}`.withLogs().run() + command`node ./scripts/deploy/upload-source-maps.ts ${version} ${uploadPathTypes}`.withLogs().run() + + if (checkMonitors) { + await gateMonitors(datacenters) } } -async function gateMonitors(uploadPath: string): Promise { - printLog(`Check monitors for ${uploadPath} during ${GATE_DURATION / ONE_MINUTE_IN_SECOND} minutes`) +async function gateMonitors(datacenters: string[]): Promise { + printLog(`Check monitors for ${datacenters.join(',')} during ${GATE_DURATION / ONE_MINUTE_IN_SECOND} minutes`) + for (let i = 0; i < GATE_DURATION; i += GATE_INTERVAL) { - command`node ./scripts/deploy/check-monitors.ts ${uploadPath}`.run() + command`node ./scripts/deploy/check-monitors.ts ${datacenters.join(',')}`.run() process.stdout.write('.') // progress indicator await timeout(GATE_INTERVAL * 1000) } + printLog() // new line } + +function getDatacenters(datacenterGroup: string): string[] { + if (datacenterGroup === 'minor-dcs') { + return getAllMinorDcs() + } + + if (datacenterGroup === 'private-regions') { + return getAllPrivateDcs() + } + + return datacenterGroup.split(',') +} + +function toDatacenterUploadPathType(datacenters: string[]): string[] { + return datacenters.map((datacenter) => { + if (datacenter === 'gov') { + return 'root' + } + + return datacenter + }) +} diff --git a/scripts/deploy/lib/testHelpers.ts b/scripts/deploy/lib/testHelpers.ts index e2c241ede4..32a16ef76d 100644 --- a/scripts/deploy/lib/testHelpers.ts +++ b/scripts/deploy/lib/testHelpers.ts @@ -62,9 +62,7 @@ export function mockCommandImplementation(mockFn: Mock<(...args: any[]) => void> withCurrentWorkingDirectory: () => result, withLogs: () => result, run(): string | undefined { - commands.push(commandDetail) - - if (command.includes('aws sts assume-role')) { + if (command.startsWith('aws sts assume-role')) { return JSON.stringify({ Credentials: { AccessKeyId: FAKE_AWS_ENV_CREDENTIALS.AWS_ACCESS_KEY_ID, @@ -73,6 +71,22 @@ export function mockCommandImplementation(mockFn: Mock<(...args: any[]) => void> }, }) } + + if (command.startsWith('ddtool datacenters list')) { + return JSON.stringify([ + { name: 'ap1.prod.dog', site: 'ap1.datadoghq.com' }, + { name: 'ap2.prod.dog', site: 'ap2.datadoghq.com' }, + { name: 'eu1.prod.dog', site: 'datadoghq.eu' }, + { name: 'us1.prod.dog', site: 'datadoghq.com' }, + { name: 'us3.prod.dog', site: 'us3.datadoghq.com' }, + { name: 'us5.prod.dog', site: 'us5.datadoghq.com' }, + { name: 'prtest00.prod.dog', site: 'prtest00.datadoghq.com' }, + { name: 'prtest01.prod.dog', site: 'prtest01.datadoghq.com' }, + ]) + } + + // don't push command details for the above mock commands + commands.push(commandDetail) }, } return result diff --git a/scripts/deploy/upload-source-maps.spec.ts b/scripts/deploy/upload-source-maps.spec.ts index cd67d62d2c..9be2535bf7 100644 --- a/scripts/deploy/upload-source-maps.spec.ts +++ b/scripts/deploy/upload-source-maps.spec.ts @@ -1,7 +1,6 @@ import assert from 'node:assert/strict' import path from 'node:path' -import { beforeEach, before, describe, it, mock } from 'node:test' -import { siteByDatacenter } from '../lib/datacenter.ts' +import { beforeEach, before, describe, it, mock, afterEach } from 'node:test' import { mockModule, mockCommandImplementation, replaceChunkHashes } from './lib/testHelpers.ts' const FAKE_API_KEY = 'FAKE_API_KEY' @@ -24,6 +23,8 @@ describe('upload-source-maps', () => { let commands: CommandDetail[] let uploadSourceMaps: (version: string, uploadPathTypes: string[]) => Promise + let getAllDatacenters: () => string[] + let getSite: (datacenter: string) => string function getSourceMapCommands(): CommandDetail[] { return commands.filter(({ command }) => command.includes('datadog-ci sourcemaps')) @@ -39,19 +40,26 @@ describe('upload-source-maps', () => { getTelemetryOrgApiKey: () => FAKE_API_KEY, }) - // This MUST be a dynamic import because that is the only way to ensure the + // These MUST be dynamic imports because that is the only way to ensure the // import starts after the mock has been set up. const uploadModule = await import('./upload-source-maps.ts') uploadSourceMaps = uploadModule.main + const datacenterModule = await import('../lib/datacenter.ts') + getAllDatacenters = datacenterModule.getAllDatacenters + getSite = datacenterModule.getSite }) beforeEach(() => { commands = mockCommandImplementation(commandMock) }) + afterEach(() => { + mock.restoreAll() + }) + function forEachDatacenter(callback: (site: string) => void): void { - for (const site of Object.values(siteByDatacenter)) { - callback(site) + for (const datacenter of getAllDatacenters()) { + callback(getSite(datacenter)) } } diff --git a/scripts/deploy/upload-source-maps.ts b/scripts/deploy/upload-source-maps.ts index b8137c7e34..4ebc262e35 100644 --- a/scripts/deploy/upload-source-maps.ts +++ b/scripts/deploy/upload-source-maps.ts @@ -3,7 +3,7 @@ import { printLog, runMain } from '../lib/executionUtils.ts' import { command } from '../lib/command.ts' import { getBuildEnvValue } from '../lib/buildEnv.ts' import { getTelemetryOrgApiKey } from '../lib/secrets.ts' -import { siteByDatacenter } from '../lib/datacenter.ts' +import { getSite, getAllDatacenters } from '../lib/datacenter.ts' import { forEachFile } from '../lib/filesUtils.ts' import { buildRootUploadPath, buildDatacenterUploadPath, buildBundleFolder, packages } from './lib/deploymentUtils.ts' @@ -20,7 +20,8 @@ function getSitesByVersion(version: string): string[] { case 'canary': return ['datadoghq.com'] default: - return Object.values(siteByDatacenter) + // TODO: do we upload to root for all DCs? + return getAllDatacenters().map(getSite) } } @@ -56,7 +57,7 @@ async function uploadSourceMaps( uploadPath = buildRootUploadPath(packageName, version) await renameFilesWithVersionSuffix(bundleFolder, version) } else { - sites = [siteByDatacenter[uploadPathType]] + sites = [getSite(uploadPathType)] uploadPath = buildDatacenterUploadPath(uploadPathType, packageName, version) } const prefix = path.dirname(`/${uploadPath}`) diff --git a/scripts/lib/datacenter.ts b/scripts/lib/datacenter.ts index 078e5d464f..80f9028cdd 100644 --- a/scripts/lib/datacenter.ts +++ b/scripts/lib/datacenter.ts @@ -1,9 +1,51 @@ -export const siteByDatacenter: Record = { - us1: 'datadoghq.com', - eu1: 'datadoghq.eu', - us3: 'us3.datadoghq.com', - us5: 'us5.datadoghq.com', - ap1: 'ap1.datadoghq.com', - ap2: 'ap2.datadoghq.com', - prtest00: 'prtest00.datad0g.com', +import { command } from './command.ts' + +// Major DCs are the ones that are deployed last. +// They have their own step jobs in `deploy-manual.yml` and `deploy-auto.yml`. +const MAJOR_DCS = ['gov', 'us1', 'eu1'] + +export function getSite(datacenter: string): string { + return getAllDatacentersMetadata()[datacenter].site +} + +export function getAllDatacenters(): string[] { + return Object.keys(getAllDatacentersMetadata()) +} + +export function getAllMinorDcs(): string[] { + return getAllDatacenters().filter((dc) => !MAJOR_DCS.includes(dc) && !dc.startsWith('pr')) +} + +export function getAllPrivateDcs(): string[] { + return getAllDatacenters().filter((dc) => dc.startsWith('pr')) +} + +interface Datacenter { + name: string + site: string +} + +let cachedDatacenters: Record | undefined + +function getAllDatacentersMetadata(): Record { + if (cachedDatacenters) { + return cachedDatacenters + } + + const selector = 'datacenter.environment == "prod" && datacenter.flavor == "site"' + const rawDatacenters = command`ddtool datacenters list --selector ${selector}`.run().trim() + const jsonDatacenters = JSON.parse(rawDatacenters) as Datacenter[] + + cachedDatacenters = {} + + for (const datacenter of jsonDatacenters) { + const shortName = datacenter.name.split('.')[0] + + cachedDatacenters[shortName] = { + name: datacenter.name, + site: datacenter.site, + } + } + + return cachedDatacenters } From b805742a372de89f86bb6b248100c6654fbeb8a0 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 11:07:39 +0100 Subject: [PATCH 07/19] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20replace=20ddtool=20w?= =?UTF-8?q?ith=20runtime-metadata-service=20API=20for=20datacenter=20fetch?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the ddtool command-line tool with direct fetch calls to the runtime-metadata-service API. This enables the scripts to work in CI environments where ddtool is not available. Key changes: - Refactored datacenter.ts to use fetchHandlingError with Vault token authentication instead of ddtool command - Made all datacenter functions async with lazy initialization - Added comprehensive test coverage for datacenter module --- scripts/deploy/check-monitors.spec.ts | 46 ++++++------ scripts/deploy/check-monitors.ts | 2 +- scripts/deploy/deploy-prod-dc.spec.ts | 9 ++- scripts/deploy/deploy-prod-dc.ts | 8 +-- scripts/deploy/lib/testHelpers.ts | 85 ++++++++++++++++++---- scripts/deploy/upload-source-maps.spec.ts | 16 +++-- scripts/deploy/upload-source-maps.ts | 12 ++-- scripts/lib/datacenter.spec.ts | 87 +++++++++++++++++++++++ scripts/lib/datacenter.ts | 54 ++++++++++---- 9 files changed, 251 insertions(+), 68 deletions(-) create mode 100644 scripts/lib/datacenter.spec.ts diff --git a/scripts/deploy/check-monitors.spec.ts b/scripts/deploy/check-monitors.spec.ts index 200dd5507a..11dac6f053 100644 --- a/scripts/deploy/check-monitors.spec.ts +++ b/scripts/deploy/check-monitors.spec.ts @@ -3,7 +3,7 @@ import path from 'node:path' import type { Mock } from 'node:test' import { afterEach, before, describe, it, mock } from 'node:test' import type { fetchHandlingError } from 'scripts/lib/executionUtils.ts' -import { mockModule } from './lib/testHelpers.ts' +import { mockModule, mockFetchHandlingError } from './lib/testHelpers.ts' import type { QueryResultBucket } from './check-monitors.ts' const FAKE_API_KEY = 'FAKE_API_KEY' @@ -37,23 +37,29 @@ const TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK = [ describe('check-monitors', () => { const fetchHandlingErrorMock: Mock = mock.fn() - function mockFetchHandlingError( + function setupTelemetryMocks( responseBuckets: [QueryResultBucket[], QueryResultBucket[], QueryResultBucket[]] ): void { - for (let i = 0; i < 3; i++) { - fetchHandlingErrorMock.mock.mockImplementationOnce( - (_url: string, _options?: RequestInit) => - Promise.resolve({ - json: () => - Promise.resolve({ - data: { - buckets: responseBuckets[i], - }, - }), - } as unknown as Response), - i - ) - } + let telemetryCallIndex = 0 + + // Use the shared helper with additional handler for telemetry API calls + mockFetchHandlingError(fetchHandlingErrorMock, (url) => { + // Handle telemetry API calls + if (url.includes('/api/v2/logs/analytics/aggregate')) { + const buckets = responseBuckets[telemetryCallIndex] + telemetryCallIndex++ + return Promise.resolve({ + json: () => + Promise.resolve({ + data: { + buckets, + }, + }), + } as unknown as Response) + } + // Return undefined to let default handlers try + return undefined + }) } before(async () => { @@ -72,7 +78,7 @@ describe('check-monitors', () => { }) it('should not throw an error if no telemetry errors are found for a given datacenter', async () => { - mockFetchHandlingError([ + setupTelemetryMocks([ NO_TELEMETRY_ERRORS_MOCK, NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, @@ -82,7 +88,7 @@ describe('check-monitors', () => { }) it('should throw an error if telemetry errors are found for a given datacenter', async () => { - mockFetchHandlingError([ + setupTelemetryMocks([ TELEMETRY_ERRORS_MOCK, NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, @@ -92,7 +98,7 @@ describe('check-monitors', () => { }) it('should throw an error if telemetry errors on specific org are found for a given datacenter', async () => { - mockFetchHandlingError([ + setupTelemetryMocks([ NO_TELEMETRY_ERRORS_MOCK, TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, @@ -105,7 +111,7 @@ describe('check-monitors', () => { }) it('should throw an error if telemetry errors on specific message are found for a given datacenter', async () => { - mockFetchHandlingError([ + setupTelemetryMocks([ NO_TELEMETRY_ERRORS_MOCK, NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, diff --git a/scripts/deploy/check-monitors.ts b/scripts/deploy/check-monitors.ts index 4ff868d05e..2d6fd9b087 100644 --- a/scripts/deploy/check-monitors.ts +++ b/scripts/deploy/check-monitors.ts @@ -38,7 +38,7 @@ export async function main(...args: string[]): Promise { const datacenters = args[0].split(',') for (const datacenter of datacenters) { - const site = getSite(datacenter) + const site = await getSite(datacenter) if (!site) { printLog(`No site is configured for datacenter ${datacenter}. skipping...`) diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts index b8ac24c022..cbe8cb00de 100644 --- a/scripts/deploy/deploy-prod-dc.spec.ts +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -2,15 +2,18 @@ import assert from 'node:assert/strict' import path from 'node:path' import { beforeEach, before, describe, it, mock, afterEach } from 'node:test' import type { CommandDetail } from './lib/testHelpers.ts' -import { mockCommandImplementation, mockModule } from './lib/testHelpers.ts' +import { mockCommandImplementation, mockModule, mockFetchHandlingError } from './lib/testHelpers.ts' describe('deploy-prod-dc', () => { const commandMock = mock.fn() + const fetchHandlingErrorMock = mock.fn() let commands: CommandDetail[] before(async () => { + mockFetchHandlingError(fetchHandlingErrorMock) await mockModule(path.resolve(import.meta.dirname, '../lib/command.ts'), { command: commandMock }) await mockModule(path.resolve(import.meta.dirname, '../lib/executionUtils.ts'), { + fetchHandlingError: fetchHandlingErrorMock, timeout: () => Promise.resolve(), }) }) @@ -19,10 +22,6 @@ describe('deploy-prod-dc', () => { commands = mockCommandImplementation(commandMock) }) - afterEach(() => { - mock.restoreAll() - }) - it('should deploy a given datacenter', async () => { await runScript('./deploy-prod-dc.ts', 'v6', 'us1') diff --git a/scripts/deploy/deploy-prod-dc.ts b/scripts/deploy/deploy-prod-dc.ts index a87700dd94..b016886444 100644 --- a/scripts/deploy/deploy-prod-dc.ts +++ b/scripts/deploy/deploy-prod-dc.ts @@ -33,7 +33,7 @@ export async function main(...args: string[]): Promise { }) const version = positionals[0] - const datacenters = getDatacenters(positionals[1]) + const datacenters = await getDatacenters(positionals[1]) if (!datacenters) { throw new Error('DATACENTER argument is required') @@ -65,13 +65,13 @@ async function gateMonitors(datacenters: string[]): Promise { printLog() // new line } -function getDatacenters(datacenterGroup: string): string[] { +async function getDatacenters(datacenterGroup: string): Promise { if (datacenterGroup === 'minor-dcs') { - return getAllMinorDcs() + return await getAllMinorDcs() } if (datacenterGroup === 'private-regions') { - return getAllPrivateDcs() + return await getAllPrivateDcs() } return datacenterGroup.split(',') diff --git a/scripts/deploy/lib/testHelpers.ts b/scripts/deploy/lib/testHelpers.ts index 32a16ef76d..949d9123b2 100644 --- a/scripts/deploy/lib/testHelpers.ts +++ b/scripts/deploy/lib/testHelpers.ts @@ -32,6 +32,7 @@ export const FAKE_AWS_ENV_CREDENTIALS = { } as const export const FAKE_CHUNK_HASH = 'FAKEHASHd7628536637b074ddc3b' +export const FAKE_RUNTIME_METADATA_SERVICE_TOKEN = 'FAKE_RUNTIME_METADATA_SERVICE_TOKEN' export interface CommandDetail { command: string @@ -72,19 +73,6 @@ export function mockCommandImplementation(mockFn: Mock<(...args: any[]) => void> }) } - if (command.startsWith('ddtool datacenters list')) { - return JSON.stringify([ - { name: 'ap1.prod.dog', site: 'ap1.datadoghq.com' }, - { name: 'ap2.prod.dog', site: 'ap2.datadoghq.com' }, - { name: 'eu1.prod.dog', site: 'datadoghq.eu' }, - { name: 'us1.prod.dog', site: 'datadoghq.com' }, - { name: 'us3.prod.dog', site: 'us3.datadoghq.com' }, - { name: 'us5.prod.dog', site: 'us5.datadoghq.com' }, - { name: 'prtest00.prod.dog', site: 'prtest00.datadoghq.com' }, - { name: 'prtest01.prod.dog', site: 'prtest01.datadoghq.com' }, - ]) - } - // don't push command details for the above mock commands commands.push(commandDetail) }, @@ -95,6 +83,77 @@ export function mockCommandImplementation(mockFn: Mock<(...args: any[]) => void> return commands } +export const MOCK_DATACENTERS = [ + { name: 'ap1.prod.dog', site: 'ap1.datadoghq.com' }, + { name: 'ap2.prod.dog', site: 'ap2.datadoghq.com' }, + { name: 'eu1.prod.dog', site: 'datadoghq.eu' }, + { name: 'us1.prod.dog', site: 'datadoghq.com' }, + { name: 'us3.prod.dog', site: 'us3.datadoghq.com' }, + { name: 'us5.prod.dog', site: 'us5.datadoghq.com' }, + { name: 'prtest00.prod.dog', site: 'prtest00.datadoghq.com' }, + { name: 'prtest01.prod.dog', site: 'prtest01.datadoghq.com' }, +] + +type FetchMockHandler = (url: string, options?: RequestInit) => Promise | undefined + +/** + * Configure a fetchHandlingError mock with datacenter API support. + * Can be extended with an additional handler for test-specific API calls. + * + * @param fetchHandlingErrorMock - The mock function to configure + * @param additionalHandler - Optional custom handler that runs before default handlers. + * Should return a Response promise if it handles the URL, or undefined to let default handlers try. + * @example + * // Simple usage with just datacenter mocks + * mockFetchHandlingError(fetchMock) + * @example + * // Extended with telemetry API mock + * mockFetchHandlingError(fetchMock, (url) => { + * if (url.includes('api.datadoghq.com')) { + * return Promise.resolve({ json: () => Promise.resolve({ data: [] }) } as Response) + * } + * }) + */ +export function mockFetchHandlingError( + fetchHandlingErrorMock: Mock<(...args: any[]) => any>, + additionalHandler?: FetchMockHandler +): void { + fetchHandlingErrorMock.mock.mockImplementation((url: string, options?: RequestInit) => { + // Try additional handler first (for test-specific mocks) + if (additionalHandler) { + const result = additionalHandler(url, options) + if (result) { + return result + } + } + + // Vault token request + if (url.includes('/v1/identity/oidc/token/runtime-metadata-service')) { + return Promise.resolve({ + json: () => + Promise.resolve({ + data: { + token: FAKE_RUNTIME_METADATA_SERVICE_TOKEN, + }, + }), + } as unknown as Response) + } + + // Datacenters request + if (url.includes('runtime-metadata-service')) { + return Promise.resolve({ + json: () => Promise.resolve(MOCK_DATACENTERS), + } as unknown as Response) + } + + // Default response + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), + } as unknown as Response) + }) +} + function rebuildStringTemplate(template: TemplateStringsArray, ...values: any[]): string { const combinedString = template.reduce((acc, part, i) => acc + part + (values[i] || ''), '') const normalizedString = combinedString.replace(/\s+/g, ' ').trim() diff --git a/scripts/deploy/upload-source-maps.spec.ts b/scripts/deploy/upload-source-maps.spec.ts index 9be2535bf7..3eac71a4c4 100644 --- a/scripts/deploy/upload-source-maps.spec.ts +++ b/scripts/deploy/upload-source-maps.spec.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import path from 'node:path' import { beforeEach, before, describe, it, mock, afterEach } from 'node:test' -import { mockModule, mockCommandImplementation, replaceChunkHashes } from './lib/testHelpers.ts' +import { mockModule, mockCommandImplementation, replaceChunkHashes, mockFetchHandlingError } from './lib/testHelpers.ts' const FAKE_API_KEY = 'FAKE_API_KEY' const ENV_STAGING = { @@ -20,6 +20,7 @@ interface CommandDetail { describe('upload-source-maps', () => { const commandMock = mock.fn() + const fetchHandlingErrorMock = mock.fn() let commands: CommandDetail[] let uploadSourceMaps: (version: string, uploadPathTypes: string[]) => Promise @@ -35,7 +36,11 @@ describe('upload-source-maps', () => { } before(async () => { + mockFetchHandlingError(fetchHandlingErrorMock) await mockModule(path.resolve(import.meta.dirname, '../lib/command.ts'), { command: commandMock }) + await mockModule(path.resolve(import.meta.dirname, '../lib/executionUtils.ts'), { + fetchHandlingError: fetchHandlingErrorMock, + }) await mockModule(path.resolve(import.meta.dirname, '../lib/secrets.ts'), { getTelemetryOrgApiKey: () => FAKE_API_KEY, }) @@ -57,16 +62,17 @@ describe('upload-source-maps', () => { mock.restoreAll() }) - function forEachDatacenter(callback: (site: string) => void): void { - for (const datacenter of getAllDatacenters()) { - callback(getSite(datacenter)) + async function forEachDatacenter(callback: (site: string) => void): Promise { + const datacenters = await getAllDatacenters() + for (const datacenter of datacenters) { + callback(await getSite(datacenter)) } } it('should upload root packages source maps', async () => { await uploadSourceMaps('v6', ['root']) - forEachDatacenter((site) => { + await forEachDatacenter((site) => { const commandsByDatacenter = commands.filter(({ env }) => env?.DATADOG_SITE === site) const env = { DATADOG_API_KEY: FAKE_API_KEY, DATADOG_SITE: site } diff --git a/scripts/deploy/upload-source-maps.ts b/scripts/deploy/upload-source-maps.ts index 4ebc262e35..7f541ab97e 100644 --- a/scripts/deploy/upload-source-maps.ts +++ b/scripts/deploy/upload-source-maps.ts @@ -13,15 +13,17 @@ import { buildRootUploadPath, buildDatacenterUploadPath, buildBundleFolder, pack * BUILD_MODE=canary|release node upload-source-maps.ts staging|canary|vXXX root,us1,eu1,... */ -function getSitesByVersion(version: string): string[] { +async function getSitesByVersion(version: string): Promise { switch (version) { case 'staging': return ['datad0g.com', 'datadoghq.com'] case 'canary': return ['datadoghq.com'] - default: + default: { // TODO: do we upload to root for all DCs? - return getAllDatacenters().map(getSite) + const datacenters = await getAllDatacenters() + return await Promise.all(datacenters.map((dc) => getSite(dc))) + } } } @@ -53,11 +55,11 @@ async function uploadSourceMaps( let sites: string[] let uploadPath: string if (uploadPathType === 'root') { - sites = getSitesByVersion(version) + sites = await getSitesByVersion(version) uploadPath = buildRootUploadPath(packageName, version) await renameFilesWithVersionSuffix(bundleFolder, version) } else { - sites = [getSite(uploadPathType)] + sites = [await getSite(uploadPathType)] uploadPath = buildDatacenterUploadPath(uploadPathType, packageName, version) } const prefix = path.dirname(`/${uploadPath}`) diff --git a/scripts/lib/datacenter.spec.ts b/scripts/lib/datacenter.spec.ts new file mode 100644 index 0000000000..b991cc1547 --- /dev/null +++ b/scripts/lib/datacenter.spec.ts @@ -0,0 +1,87 @@ +import { describe, it, before, mock, type Mock } from 'node:test' +import assert from 'node:assert' +import path from 'node:path' +import { mockModule, mockFetchHandlingError, FAKE_RUNTIME_METADATA_SERVICE_TOKEN } from '../deploy/lib/testHelpers.ts' +import type { fetchHandlingError } from './executionUtils.ts' + +describe('datacenter', () => { + const fetchHandlingErrorMock: Mock = mock.fn() + + before(async () => { + // Setup mock before importing the module + mockFetchHandlingError(fetchHandlingErrorMock) + + await mockModule(path.resolve(import.meta.dirname, './executionUtils.ts'), { + fetchHandlingError: fetchHandlingErrorMock, + }) + }) + + it('should fetch all datacenters from runtime-metadata-service', async () => { + const { getAllDatacenters } = await import('./datacenter.ts') + const datacenters = await getAllDatacenters() + + assert.deepStrictEqual(datacenters, ['ap1', 'ap2', 'eu1', 'us1', 'us3', 'us5', 'prtest00', 'prtest01']) + }) + + it('should return site for a given datacenter', async () => { + const { getSite } = await import('./datacenter.ts') + const site = await getSite('us1') + + assert.strictEqual(site, 'datadoghq.com') + }) + + it('should filter minor datacenters (excluding major and private)', async () => { + const { getAllMinorDcs } = await import('./datacenter.ts') + const minorDcs = await getAllMinorDcs() + + assert.deepStrictEqual(minorDcs, ['ap1', 'ap2', 'us3', 'us5']) + }) + + it('should filter private datacenters (starting with pr)', async () => { + const { getAllPrivateDcs } = await import('./datacenter.ts') + const privateDcs = await getAllPrivateDcs() + + assert.deepStrictEqual(privateDcs, ['prtest00', 'prtest01']) + }) + + it('should fetch vault token with correct headers', async () => { + await import('./datacenter.ts') + + const vaultCall = fetchHandlingErrorMock.mock.calls.find((call) => + call.arguments[0].includes('/v1/identity/oidc/token/runtime-metadata-service') + ) + assert.ok(vaultCall) + assert.deepStrictEqual(vaultCall.arguments[1]?.headers, { 'X-Vault-Request': 'true' }) + }) + + it('should fetch datacenters with vault token authorization', async () => { + await import('./datacenter.ts') + + const datacentersCall = fetchHandlingErrorMock.mock.calls.find( + (call) => call.arguments[0] === 'https://runtime-metadata-service.us1.ddbuild.io/v2/datacenters' + ) + assert.ok(datacentersCall) + assert.deepStrictEqual(datacentersCall.arguments[1]?.headers, { + accept: 'application/json', + Authorization: `Bearer ${FAKE_RUNTIME_METADATA_SERVICE_TOKEN}`, + }) + }) + + it('should cache datacenter metadata across multiple calls', async () => { + const { getAllDatacenters, getSite } = await import('./datacenter.ts') + + // Module may already be initialized from previous tests + // Make multiple calls and verify no additional API calls are made + const initialCallCount = fetchHandlingErrorMock.mock.calls.length + + await getAllDatacenters() + await getSite('us1') + await getAllDatacenters() + await getSite('eu1') + + const finalCallCount = fetchHandlingErrorMock.mock.calls.length + + // Should not make any new API calls since data is cached (whether cached from this test or previous tests) + assert.strictEqual(finalCallCount, initialCallCount, 'Should not make additional API calls when cached') + }) +}) diff --git a/scripts/lib/datacenter.ts b/scripts/lib/datacenter.ts index 80f9028cdd..52cab4a3ce 100644 --- a/scripts/lib/datacenter.ts +++ b/scripts/lib/datacenter.ts @@ -1,23 +1,26 @@ -import { command } from './command.ts' +import { fetchHandlingError } from './executionUtils.ts' // Major DCs are the ones that are deployed last. // They have their own step jobs in `deploy-manual.yml` and `deploy-auto.yml`. const MAJOR_DCS = ['gov', 'us1', 'eu1'] -export function getSite(datacenter: string): string { - return getAllDatacentersMetadata()[datacenter].site +const VAULT_ADDR = process.env.VAULT_ADDR || 'https://vault.us1.ddbuild.io' +const RUNTIME_METADATA_SERVICE_URL = 'https://runtime-metadata-service.us1.ddbuild.io/v2/datacenters' + +export async function getSite(datacenter: string): Promise { + return (await getAllDatacentersMetadata())[datacenter].site } -export function getAllDatacenters(): string[] { - return Object.keys(getAllDatacentersMetadata()) +export async function getAllDatacenters(): Promise { + return Object.keys(await getAllDatacentersMetadata()) } -export function getAllMinorDcs(): string[] { - return getAllDatacenters().filter((dc) => !MAJOR_DCS.includes(dc) && !dc.startsWith('pr')) +export async function getAllMinorDcs(): Promise { + return (await getAllDatacenters()).filter((dc) => !MAJOR_DCS.includes(dc) && !dc.startsWith('pr')) } -export function getAllPrivateDcs(): string[] { - return getAllDatacenters().filter((dc) => dc.startsWith('pr')) +export async function getAllPrivateDcs(): Promise { + return (await getAllDatacenters()).filter((dc) => dc.startsWith('pr')) } interface Datacenter { @@ -27,18 +30,15 @@ interface Datacenter { let cachedDatacenters: Record | undefined -function getAllDatacentersMetadata(): Record { +async function getAllDatacentersMetadata(): Promise> { if (cachedDatacenters) { return cachedDatacenters } - const selector = 'datacenter.environment == "prod" && datacenter.flavor == "site"' - const rawDatacenters = command`ddtool datacenters list --selector ${selector}`.run().trim() - const jsonDatacenters = JSON.parse(rawDatacenters) as Datacenter[] - + const datacenters = await fetchDatacentersFromRuntimeMetadataService() cachedDatacenters = {} - for (const datacenter of jsonDatacenters) { + for (const datacenter of datacenters) { const shortName = datacenter.name.split('.')[0] cachedDatacenters[shortName] = { @@ -49,3 +49,27 @@ function getAllDatacentersMetadata(): Record { return cachedDatacenters } + +async function fetchDatacentersFromRuntimeMetadataService(): Promise { + const token = await getVaultToken() + + const response = await fetchHandlingError(RUNTIME_METADATA_SERVICE_URL, { + headers: { + accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + }) + + return (await response.json()) as Datacenter[] +} + +async function getVaultToken(): Promise { + const response = await fetchHandlingError(`${VAULT_ADDR}/v1/identity/oidc/token/runtime-metadata-service`, { + headers: { + 'X-Vault-Request': 'true', + }, + }) + + const data = (await response.json()) as { data: { token: string } } + return data.data.token +} From db6bc46fbf3be0fb5c097bf6391f3867ff233e8b Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 11:09:09 +0100 Subject: [PATCH 08/19] =?UTF-8?q?=E2=9C=A8=20add=20test=20script=20for=20d?= =?UTF-8?q?atacenter=20API=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 6 +----- scripts/test-datacenter-api.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 scripts/test-datacenter-api.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8ad4f22ff0..58c324330d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -129,11 +129,7 @@ list-datacenters: stage: before-tests extends: .base-configuration script: - - | - TOKEN=$(curl --silent $VAULT_ADDR/v1/identity/oidc/token/runtime-metadata-service -H 'X-Vault-Request: true' | jq '.data.token' -r) - curl -X 'GET' 'https://runtime-metadata-service.us1.ddbuild.io/v2/datacenters' \ - -H "accept: application/json" \ - -H "Authorization: Bearer $TOKEN" + - node scripts/test-datacenter-api.ts ######################################################################################################################## # Tests diff --git a/scripts/test-datacenter-api.ts b/scripts/test-datacenter-api.ts new file mode 100644 index 0000000000..9b574a806d --- /dev/null +++ b/scripts/test-datacenter-api.ts @@ -0,0 +1,21 @@ +import { runMain, printLog } from './lib/executionUtils.ts' +import { getAllDatacenters, getSite } from './lib/datacenter.ts' + +/** + * Test script to verify datacenter API works in CI + * This is temporary and will be removed before merging + */ + +runMain(async () => { + printLog('Fetching datacenters from runtime-metadata-service...') + + const datacenters = await getAllDatacenters() + printLog(`Found ${datacenters.length} datacenters:`) + + for (const dc of datacenters) { + const site = await getSite(dc) + printLog(` - ${dc}: ${site}`) + } + + printLog('✅ Datacenter API test successful!') +}) From 6a534b9b06ab147575b2a628aef69edd284685c4 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 11:20:06 +0100 Subject: [PATCH 09/19] =?UTF-8?q?=F0=9F=94=8D=20add=20debug=20log=20for=20?= =?UTF-8?q?fetched=20datacenters=20in=20getAllDatacentersMetadata=20functi?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/lib/datacenter.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/lib/datacenter.ts b/scripts/lib/datacenter.ts index 52cab4a3ce..166c69df38 100644 --- a/scripts/lib/datacenter.ts +++ b/scripts/lib/datacenter.ts @@ -36,6 +36,7 @@ async function getAllDatacentersMetadata(): Promise> } const datacenters = await fetchDatacentersFromRuntimeMetadataService() + console.log('Fetched datacenters from runtime-metadata-service:', datacenters) cachedDatacenters = {} for (const datacenter of datacenters) { From 455dad789fe26a9febecee8861967c40bddab596 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 11:28:04 +0100 Subject: [PATCH 10/19] fix datacenter response types --- scripts/deploy/lib/testHelpers.ts | 2 +- scripts/lib/datacenter.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/deploy/lib/testHelpers.ts b/scripts/deploy/lib/testHelpers.ts index 949d9123b2..6794a2e7c8 100644 --- a/scripts/deploy/lib/testHelpers.ts +++ b/scripts/deploy/lib/testHelpers.ts @@ -142,7 +142,7 @@ export function mockFetchHandlingError( // Datacenters request if (url.includes('runtime-metadata-service')) { return Promise.resolve({ - json: () => Promise.resolve(MOCK_DATACENTERS), + json: () => Promise.resolve({ datacenters: MOCK_DATACENTERS }), } as unknown as Response) } diff --git a/scripts/lib/datacenter.ts b/scripts/lib/datacenter.ts index 166c69df38..9817416bd8 100644 --- a/scripts/lib/datacenter.ts +++ b/scripts/lib/datacenter.ts @@ -28,6 +28,10 @@ interface Datacenter { site: string } +interface DatacentersResponse { + datacenters: Datacenter[] +} + let cachedDatacenters: Record | undefined async function getAllDatacentersMetadata(): Promise> { @@ -36,7 +40,6 @@ async function getAllDatacentersMetadata(): Promise> } const datacenters = await fetchDatacentersFromRuntimeMetadataService() - console.log('Fetched datacenters from runtime-metadata-service:', datacenters) cachedDatacenters = {} for (const datacenter of datacenters) { @@ -61,7 +64,8 @@ async function fetchDatacentersFromRuntimeMetadataService(): Promise { From 9019c769d7fbc523fa038d4b1d6823a3aaf73981 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 11:40:33 +0100 Subject: [PATCH 11/19] fix: add selector to RMS call --- scripts/lib/datacenter.spec.ts | 10 ++++++++-- scripts/lib/datacenter.ts | 18 ++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/lib/datacenter.spec.ts b/scripts/lib/datacenter.spec.ts index b991cc1547..90d680a609 100644 --- a/scripts/lib/datacenter.spec.ts +++ b/scripts/lib/datacenter.spec.ts @@ -57,10 +57,16 @@ describe('datacenter', () => { it('should fetch datacenters with vault token authorization', async () => { await import('./datacenter.ts') - const datacentersCall = fetchHandlingErrorMock.mock.calls.find( - (call) => call.arguments[0] === 'https://runtime-metadata-service.us1.ddbuild.io/v2/datacenters' + const datacentersCall = fetchHandlingErrorMock.mock.calls.find((call) => + call.arguments[0].includes('runtime-metadata-service.us1.ddbuild.io/v2/datacenters') ) assert.ok(datacentersCall) + assert.ok( + datacentersCall.arguments[0].includes( + `selector=${encodeURIComponent('datacenter.environment == "prod" && datacenter.flavor == "site"')}` + ), + 'URL should include selector for prod environment and site flavor' + ) assert.deepStrictEqual(datacentersCall.arguments[1]?.headers, { accept: 'application/json', Authorization: `Bearer ${FAKE_RUNTIME_METADATA_SERVICE_TOKEN}`, diff --git a/scripts/lib/datacenter.ts b/scripts/lib/datacenter.ts index 9817416bd8..2ded8f7bd8 100644 --- a/scripts/lib/datacenter.ts +++ b/scripts/lib/datacenter.ts @@ -57,12 +57,18 @@ async function getAllDatacentersMetadata(): Promise> async function fetchDatacentersFromRuntimeMetadataService(): Promise { const token = await getVaultToken() - const response = await fetchHandlingError(RUNTIME_METADATA_SERVICE_URL, { - headers: { - accept: 'application/json', - Authorization: `Bearer ${token}`, - }, - }) + // Filter for production environment and site flavor only + const selector = 'datacenter.environment == "prod" && datacenter.flavor == "site"' + + const response = await fetchHandlingError( + `${RUNTIME_METADATA_SERVICE_URL}?selector=${encodeURIComponent(selector)}`, + { + headers: { + accept: 'application/json', + Authorization: `Bearer ${token}`, + }, + } + ) const data = (await response.json()) as DatacentersResponse return data.datacenters From eba40737f9e2d7a1d73362b98d72bf9a2a6ddbfb Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 12:56:56 +0100 Subject: [PATCH 12/19] refactor: update deploy-prod-dc script to skip monitor checks for gov datacenter deployments --- scripts/deploy/deploy-prod-dc.spec.ts | 8 +++++--- scripts/deploy/deploy-prod-dc.ts | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts index cbe8cb00de..32f3b09419 100644 --- a/scripts/deploy/deploy-prod-dc.spec.ts +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -1,6 +1,6 @@ import assert from 'node:assert/strict' import path from 'node:path' -import { beforeEach, before, describe, it, mock, afterEach } from 'node:test' +import { beforeEach, before, describe, it, mock } from 'node:test' import type { CommandDetail } from './lib/testHelpers.ts' import { mockCommandImplementation, mockModule, mockFetchHandlingError } from './lib/testHelpers.ts' @@ -61,12 +61,14 @@ describe('deploy-prod-dc', () => { ]) }) - it('should deploy gov datacenters to the root upload path', async () => { - await runScript('./deploy-prod-dc.ts', 'v6', 'gov', '--no-check-monitors') + it('should deploy gov datacenters to the root upload path and skip all monitor checks', async () => { + await runScript('./deploy-prod-dc.ts', 'v6', 'gov', '--check-monitors') + // Should not include any monitor checks since gov maps to root upload path assert.deepEqual(commands, [ { command: 'node ./scripts/deploy/deploy.ts prod v6 root' }, { command: 'node ./scripts/deploy/upload-source-maps.ts v6 root' }, + // No monitor checks should be present at all ]) }) }) diff --git a/scripts/deploy/deploy-prod-dc.ts b/scripts/deploy/deploy-prod-dc.ts index b016886444..90ddc38adb 100644 --- a/scripts/deploy/deploy-prod-dc.ts +++ b/scripts/deploy/deploy-prod-dc.ts @@ -39,7 +39,10 @@ export async function main(...args: string[]): Promise { throw new Error('DATACENTER argument is required') } - if (checkMonitors) { + // Skip all monitor checks for gov datacenter deployments + const shouldCheckMonitors = checkMonitors && !datacenters.every((path) => path === 'gov') + + if (shouldCheckMonitors) { command`node ./scripts/deploy/check-monitors.ts ${datacenters.join(',')}`.withLogs().run() } @@ -48,7 +51,7 @@ export async function main(...args: string[]): Promise { command`node ./scripts/deploy/deploy.ts prod ${version} ${uploadPathTypes}`.withLogs().run() command`node ./scripts/deploy/upload-source-maps.ts ${version} ${uploadPathTypes}`.withLogs().run() - if (checkMonitors) { + if (shouldCheckMonitors) { await gateMonitors(datacenters) } } From fca020951c3b5a3e58d086f6bc3e0a05a7e57f9b Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 13:25:54 +0100 Subject: [PATCH 13/19] fix: ts errors --- scripts/deploy/upload-source-maps.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/deploy/upload-source-maps.spec.ts b/scripts/deploy/upload-source-maps.spec.ts index 3eac71a4c4..7579274842 100644 --- a/scripts/deploy/upload-source-maps.spec.ts +++ b/scripts/deploy/upload-source-maps.spec.ts @@ -24,8 +24,8 @@ describe('upload-source-maps', () => { let commands: CommandDetail[] let uploadSourceMaps: (version: string, uploadPathTypes: string[]) => Promise - let getAllDatacenters: () => string[] - let getSite: (datacenter: string) => string + let getAllDatacenters: () => Promise + let getSite: (datacenter: string) => Promise function getSourceMapCommands(): CommandDetail[] { return commands.filter(({ command }) => command.includes('datadog-ci sourcemaps')) From 38ec673ee68aebf74dbafc86b842d8f6cb992786 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 13:31:08 +0100 Subject: [PATCH 14/19] fix formating --- scripts/deploy/check-monitors.spec.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/deploy/check-monitors.spec.ts b/scripts/deploy/check-monitors.spec.ts index 11dac6f053..13c3dd1402 100644 --- a/scripts/deploy/check-monitors.spec.ts +++ b/scripts/deploy/check-monitors.spec.ts @@ -37,9 +37,7 @@ const TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK = [ describe('check-monitors', () => { const fetchHandlingErrorMock: Mock = mock.fn() - function setupTelemetryMocks( - responseBuckets: [QueryResultBucket[], QueryResultBucket[], QueryResultBucket[]] - ): void { + function setupTelemetryMocks(responseBuckets: [QueryResultBucket[], QueryResultBucket[], QueryResultBucket[]]): void { let telemetryCallIndex = 0 // Use the shared helper with additional handler for telemetry API calls From eae7dc13312af96fe2a028c2c82aec6dfbda10a5 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 14:29:21 +0100 Subject: [PATCH 15/19] remove test script --- .gitlab-ci.yml | 7 ------- scripts/test-datacenter-api.ts | 21 --------------------- 2 files changed, 28 deletions(-) delete mode 100644 scripts/test-datacenter-api.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 58c324330d..afb8956862 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,6 @@ cache: stages: - task - ci-image - - before-tests - test - after-tests - browserstack @@ -125,12 +124,6 @@ ci-image: - docker build --build-arg CHROME_PACKAGE_VERSION=$CHROME_PACKAGE_VERSION --tag $CI_IMAGE . - docker push $CI_IMAGE -list-datacenters: - stage: before-tests - extends: .base-configuration - script: - - node scripts/test-datacenter-api.ts - ######################################################################################################################## # Tests ######################################################################################################################## diff --git a/scripts/test-datacenter-api.ts b/scripts/test-datacenter-api.ts deleted file mode 100644 index 9b574a806d..0000000000 --- a/scripts/test-datacenter-api.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { runMain, printLog } from './lib/executionUtils.ts' -import { getAllDatacenters, getSite } from './lib/datacenter.ts' - -/** - * Test script to verify datacenter API works in CI - * This is temporary and will be removed before merging - */ - -runMain(async () => { - printLog('Fetching datacenters from runtime-metadata-service...') - - const datacenters = await getAllDatacenters() - printLog(`Found ${datacenters.length} datacenters:`) - - for (const dc of datacenters) { - const site = await getSite(dc) - printLog(` - ${dc}: ${site}`) - } - - printLog('✅ Datacenter API test successful!') -}) From c142ae7874d7700dcd796b6ef32dc558032afcdc Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 14:30:39 +0100 Subject: [PATCH 16/19] fix: missing root -> gov renaming --- .gitlab/deploy-manual.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab/deploy-manual.yml b/.gitlab/deploy-manual.yml index 97b5656693..adadf861db 100644 --- a/.gitlab/deploy-manual.yml +++ b/.gitlab/deploy-manual.yml @@ -47,7 +47,7 @@ step-4_deploy-prod-gov: - .base-configuration - .deploy-prod variables: - DATACENTER: root + DATACENTER: gov step-5_publish-npm: stage: deploy From f710785da463cfca3fad8d0b9fdcb7ef556a4665 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 7 Jan 2026 14:37:44 +0100 Subject: [PATCH 17/19] feat: Deploy to private regions --- .gitlab/deploy-auto.yml | 31 ++++++++++++++++++++----------- .gitlab/deploy-manual.yml | 19 +++++++++++++------ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/.gitlab/deploy-auto.yml b/.gitlab/deploy-auto.yml index c0713b3171..32e78d678d 100644 --- a/.gitlab/deploy-auto.yml +++ b/.gitlab/deploy-auto.yml @@ -28,36 +28,45 @@ step-1_deploy-prod-minor-dcs: variables: DATACENTER: minor-dcs -step-2_deploy-prod-eu1: +step-2_deploy-prod-private-regions: needs: - step-1_deploy-prod-minor-dcs extends: - .base-configuration - .deploy-prod + variables: + DATACENTER: private-regions + +step-3_deploy-prod-eu1: + needs: + - step-2_deploy-prod-private-regions + extends: + - .base-configuration + - .deploy-prod variables: DATACENTER: eu1 -step-3_deploy-prod-us1: +step-4_deploy-prod-us1: needs: - - step-2_deploy-prod-eu1 + - step-3_deploy-prod-eu1 extends: - .base-configuration - .deploy-prod variables: DATACENTER: us1 -step-4_deploy-prod-gov: +step-5_deploy-prod-gov: needs: - - step-3_deploy-prod-us1 + - step-4_deploy-prod-us1 extends: - .base-configuration - .deploy-prod variables: DATACENTER: gov -step-5_publish-npm: +step-6_publish-npm: needs: - - step-4_deploy-prod-gov + - step-5_deploy-prod-gov stage: deploy extends: - .base-configuration @@ -66,9 +75,9 @@ step-5_publish-npm: - yarn - node ./scripts/deploy/publish-npm.ts -step-6_publish-developer-extension: +step-7_publish-developer-extension: needs: - - step-5_publish-npm + - step-6_publish-npm stage: deploy extends: - .base-configuration @@ -77,9 +86,9 @@ step-6_publish-developer-extension: - yarn - node ./scripts/deploy/publish-developer-extension.ts -step-7_create-github-release: +step-8_create-github-release: needs: - - step-6_publish-developer-extension + - step-7_publish-developer-extension stage: deploy extends: - .base-configuration diff --git a/.gitlab/deploy-manual.yml b/.gitlab/deploy-manual.yml index adadf861db..4ef3c84dc3 100644 --- a/.gitlab/deploy-manual.yml +++ b/.gitlab/deploy-manual.yml @@ -28,28 +28,35 @@ step-1_deploy-prod-minor-dcs: variables: DATACENTER: minor-dcs -step-2_deploy-prod-eu1: +step-2_deploy-prod-private-regions: + extends: + - .base-configuration + - .deploy-prod + variables: + DATACENTER: private-regions + +step-3_deploy-prod-eu1: extends: - .base-configuration - .deploy-prod variables: DATACENTER: eu1 -step-3_deploy-prod-us1: +step-4_deploy-prod-us1: extends: - .base-configuration - .deploy-prod variables: DATACENTER: us1 -step-4_deploy-prod-gov: +step-5_deploy-prod-gov: extends: - .base-configuration - .deploy-prod variables: DATACENTER: gov -step-5_publish-npm: +step-6_publish-npm: stage: deploy extends: - .base-configuration @@ -59,7 +66,7 @@ step-5_publish-npm: - yarn - node ./scripts/deploy/publish-npm.ts -step-6_publish-developer-extension: +step-7_publish-developer-extension: stage: deploy extends: - .base-configuration @@ -69,7 +76,7 @@ step-6_publish-developer-extension: - yarn - node ./scripts/deploy/publish-developer-extension.ts -step-7_create-github-release: +step-8_create-github-release: stage: deploy extends: - .base-configuration From 454dac667cf272567f43eba4d6c14e0844e6a5fb Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 8 Jan 2026 09:26:54 +0100 Subject: [PATCH 18/19] fix merge conflicts --- scripts/deploy/check-monitors.spec.ts | 129 ------------------ scripts/deploy/deploy-prod-dc.spec.ts | 5 +- scripts/deploy/deploy-prod-dc.ts | 8 +- .../deploy/lib/checkTelemetryErrors.spec.ts | 15 ++ 4 files changed, 23 insertions(+), 134 deletions(-) delete mode 100644 scripts/deploy/check-monitors.spec.ts diff --git a/scripts/deploy/check-monitors.spec.ts b/scripts/deploy/check-monitors.spec.ts deleted file mode 100644 index 13c3dd1402..0000000000 --- a/scripts/deploy/check-monitors.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import assert from 'node:assert/strict' -import path from 'node:path' -import type { Mock } from 'node:test' -import { afterEach, before, describe, it, mock } from 'node:test' -import type { fetchHandlingError } from 'scripts/lib/executionUtils.ts' -import { mockModule, mockFetchHandlingError } from './lib/testHelpers.ts' -import type { QueryResultBucket } from './check-monitors.ts' - -const FAKE_API_KEY = 'FAKE_API_KEY' -const FAKE_APPLICATION_KEY = 'FAKE_APPLICATION_KEY' - -const NO_TELEMETRY_ERRORS_MOCK = [{ computes: { c0: 40 }, by: {} }] -const NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK = [ - { by: { '@org_id': 123456 }, computes: { c0: 22 } }, - { by: { '@org_id': 789012 }, computes: { c0: 3 } }, - { by: { '@org_id': 345678 }, computes: { c0: 3 } }, -] -const NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK = [ - { by: { 'issue.id': 'b4a4bf0c-e64a-11ef-aa16-da7ad0900002' }, computes: { c0: 16 } }, - { by: { 'issue.id': '0aedaf4a-da09-11ed-baef-da7ad0900002' }, computes: { c0: 9 } }, - { by: { 'issue.id': '118a6a40-1454-11f0-82c6-da7ad0900002' }, computes: { c0: 1 } }, - { by: { 'issue.id': '144e312a-919a-11ef-8519-da7ad0900002' }, computes: { c0: 1 } }, -] -const TELEMETRY_ERRORS_MOCK = [{ computes: { c0: 10000 }, by: {} }] -const TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK = [ - { by: { '@org_id': 123456 }, computes: { c0: 500 } }, - { by: { '@org_id': 789012 }, computes: { c0: 3 } }, - { by: { '@org_id': 345678 }, computes: { c0: 3 } }, -] -const TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK = [ - { by: { 'issue.id': 'b4a4bf0c-e64a-11ef-aa16-da7ad0900002' }, computes: { c0: 1600 } }, - { by: { 'issue.id': '0aedaf4a-da09-11ed-baef-da7ad0900002' }, computes: { c0: 9 } }, - { by: { 'issue.id': '118a6a40-1454-11f0-82c6-da7ad0900002' }, computes: { c0: 1 } }, - { by: { 'issue.id': '144e312a-919a-11ef-8519-da7ad0900002' }, computes: { c0: 1 } }, -] - -describe('check-monitors', () => { - const fetchHandlingErrorMock: Mock = mock.fn() - - function setupTelemetryMocks(responseBuckets: [QueryResultBucket[], QueryResultBucket[], QueryResultBucket[]]): void { - let telemetryCallIndex = 0 - - // Use the shared helper with additional handler for telemetry API calls - mockFetchHandlingError(fetchHandlingErrorMock, (url) => { - // Handle telemetry API calls - if (url.includes('/api/v2/logs/analytics/aggregate')) { - const buckets = responseBuckets[telemetryCallIndex] - telemetryCallIndex++ - return Promise.resolve({ - json: () => - Promise.resolve({ - data: { - buckets, - }, - }), - } as unknown as Response) - } - // Return undefined to let default handlers try - return undefined - }) - } - - before(async () => { - await mockModule(path.resolve(import.meta.dirname, '../lib/secrets.ts'), { - getTelemetryOrgApiKey: () => FAKE_API_KEY, - getTelemetryOrgApplicationKey: () => FAKE_APPLICATION_KEY, - }) - - await mockModule(path.resolve(import.meta.dirname, '../lib/executionUtils.ts'), { - fetchHandlingError: fetchHandlingErrorMock, - }) - }) - - afterEach(() => { - fetchHandlingErrorMock.mock.resetCalls() - }) - - it('should not throw an error if no telemetry errors are found for a given datacenter', async () => { - setupTelemetryMocks([ - NO_TELEMETRY_ERRORS_MOCK, - NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, - NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, - ]) - - await assert.doesNotReject(() => runScript('./check-monitors.ts', 'us1')) - }) - - it('should throw an error if telemetry errors are found for a given datacenter', async () => { - setupTelemetryMocks([ - TELEMETRY_ERRORS_MOCK, - NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, - NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, - ]) - - await assert.rejects(() => runScript('./check-monitors.ts', 'us1'), /Telemetry errors found in the last 5 minutes/) - }) - - it('should throw an error if telemetry errors on specific org are found for a given datacenter', async () => { - setupTelemetryMocks([ - NO_TELEMETRY_ERRORS_MOCK, - TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, - NO_TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, - ]) - - await assert.rejects( - () => runScript('./check-monitors.ts', 'us1'), - /Telemetry errors on specific org found in the last 5 minutes/ - ) - }) - - it('should throw an error if telemetry errors on specific message are found for a given datacenter', async () => { - setupTelemetryMocks([ - NO_TELEMETRY_ERRORS_MOCK, - NO_TELEMETRY_ERRORS_ON_SPECIFIC_ORG_MOCK, - TELEMETRY_ERROR_ON_SPECIFIC_MESSAGE_MOCK, - ]) - - await assert.rejects( - () => runScript('./check-monitors.ts', 'us1'), - /Telemetry error on specific message found in the last 5 minutes/ - ) - }) -}) - -async function runScript(scriptPath: string, ...args: string[]): Promise { - const { main } = (await import(scriptPath)) as { main: (...args: string[]) => Promise } - - return main(...args) -} diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts index 99677e827c..229a353f81 100644 --- a/scripts/deploy/deploy-prod-dc.spec.ts +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -3,20 +3,23 @@ import path from 'node:path' import { beforeEach, before, describe, it, mock, type Mock } from 'node:test' import { browserSdkVersion } from '../lib/browserSdkVersion.ts' import type { CommandDetail } from './lib/testHelpers.ts' -import { mockModule, mockCommandImplementation } from './lib/testHelpers.ts' +import { mockModule, mockCommandImplementation, mockFetchHandlingError } from './lib/testHelpers.ts' const currentBrowserSdkVersionMajor = browserSdkVersion.split('.')[0] describe('deploy-prod-dc', () => { const commandMock = mock.fn() const checkTelemetryErrorsMock: Mock<(datacenters: string[], version: string) => Promise> = mock.fn() + const fetchHandlingErrorMock = mock.fn() let commands: CommandDetail[] let checkTelemetryErrorsCalls: Array<{ version: string; datacenters: string[] }> before(async () => { + mockFetchHandlingError(fetchHandlingErrorMock) await mockModule(path.resolve(import.meta.dirname, '../lib/command.ts'), { command: commandMock }) await mockModule(path.resolve(import.meta.dirname, '../lib/executionUtils.ts'), { + fetchHandlingError: fetchHandlingErrorMock, timeout: () => Promise.resolve(), }) await mockModule(path.resolve(import.meta.dirname, './lib/checkTelemetryErrors.ts'), { diff --git a/scripts/deploy/deploy-prod-dc.ts b/scripts/deploy/deploy-prod-dc.ts index 63c094f528..806e113590 100644 --- a/scripts/deploy/deploy-prod-dc.ts +++ b/scripts/deploy/deploy-prod-dc.ts @@ -20,7 +20,7 @@ if (!process.env.NODE_TEST_CONTEXT) { export async function main(...args: string[]): Promise { const { - values: { 'check-telemetry-errors': shouldCheckTelemetryErrors }, + values: { 'check-telemetry-errors': checkTelemetryErrorsFlag }, positionals, } = parseArgs({ args, @@ -42,9 +42,9 @@ export async function main(...args: string[]): Promise { } // Skip all telemetry error checks for gov datacenter deployments - const shouldCheckTelemetryErrorsActual = shouldCheckTelemetryErrors && !datacenters.every((dc) => dc === 'gov') + const shouldCheckTelemetryErrors = checkTelemetryErrorsFlag && !datacenters.every((dc) => dc === 'gov') - if (shouldCheckTelemetryErrorsActual) { + if (shouldCheckTelemetryErrors) { // Make sure system is in a good state before deploying const currentBrowserSdkVersionMajor = browserSdkVersion.split('.')[0] await checkTelemetryErrors(datacenters, `${currentBrowserSdkVersionMajor}.*`) @@ -55,7 +55,7 @@ export async function main(...args: string[]): Promise { command`node ./scripts/deploy/deploy.ts prod ${version} ${uploadPathTypes}`.withLogs().run() command`node ./scripts/deploy/upload-source-maps.ts ${version} ${uploadPathTypes}`.withLogs().run() - if (shouldCheckTelemetryErrorsActual) { + if (shouldCheckTelemetryErrors) { await gateTelemetryErrors(datacenters) } } diff --git a/scripts/deploy/lib/checkTelemetryErrors.spec.ts b/scripts/deploy/lib/checkTelemetryErrors.spec.ts index 483e599998..840563e66d 100644 --- a/scripts/deploy/lib/checkTelemetryErrors.spec.ts +++ b/scripts/deploy/lib/checkTelemetryErrors.spec.ts @@ -67,6 +67,21 @@ describe('check-telemetry-errors', () => { fetchHandlingError: fetchHandlingErrorMock, }) + await mockModule(path.resolve(import.meta.dirname, '../../lib/datacenter.ts'), { + getSite: (datacenter: string) => { + const siteByDatacenter: Record = { + us1: 'datadoghq.com', + eu1: 'datadoghq.eu', + us3: 'us3.datadoghq.com', + us5: 'us5.datadoghq.com', + ap1: 'ap1.datadoghq.com', + ap2: 'ap2.datadoghq.com', + prtest00: 'prtest00.datad0g.com', + } + return Promise.resolve(siteByDatacenter[datacenter]) + }, + }) + checkTelemetryErrors = (await import('./checkTelemetryErrors.ts')).checkTelemetryErrors }) From 2a815da01a1bf930b16577abea7dd476d7e4553f Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Thu, 8 Jan 2026 09:35:26 +0100 Subject: [PATCH 19/19] fix: tidy up --- scripts/deploy/deploy-prod-dc.spec.ts | 3 +-- scripts/deploy/upload-source-maps.ts | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/deploy/deploy-prod-dc.spec.ts b/scripts/deploy/deploy-prod-dc.spec.ts index 229a353f81..3da5c8531c 100644 --- a/scripts/deploy/deploy-prod-dc.spec.ts +++ b/scripts/deploy/deploy-prod-dc.spec.ts @@ -92,13 +92,12 @@ describe('deploy-prod-dc', () => { it('should deploy gov datacenters to the root upload path and skip all telemetry error checks', async () => { await runScript('./deploy-prod-dc.ts', 'v6', 'gov', '--check-telemetry-errors') - // Should not include any telemetry error checks since gov maps to root upload path + // gov datacenters should not be checked for telemetry errors assert.strictEqual(checkTelemetryErrorsCalls.length, 0) assert.deepEqual(commands, [ { command: 'node ./scripts/deploy/deploy.ts prod v6 root' }, { command: 'node ./scripts/deploy/upload-source-maps.ts v6 root' }, - // No telemetry error checks should be present at all ]) }) }) diff --git a/scripts/deploy/upload-source-maps.ts b/scripts/deploy/upload-source-maps.ts index 7f541ab97e..34d58df0be 100644 --- a/scripts/deploy/upload-source-maps.ts +++ b/scripts/deploy/upload-source-maps.ts @@ -20,7 +20,6 @@ async function getSitesByVersion(version: string): Promise { case 'canary': return ['datadoghq.com'] default: { - // TODO: do we upload to root for all DCs? const datacenters = await getAllDatacenters() return await Promise.all(datacenters.map((dc) => getSite(dc))) }