dd-trace
Version:
Datadog APM tracing client for JavaScript
114 lines (95 loc) • 3.31 kB
JavaScript
// Control-plane HTTP client for LLM Obs Experiments. Uses the global `fetch`,
// so this module adds no new dependency; credentials and site come from config.
const API_BASE_PATH = '/api/v2/llm-obs/v1'
// Control-plane host for a Datadog site, e.g.
// datadoghq.com -> api.datadoghq.com
// us3.datadoghq.com -> api.us3.datadoghq.com
// datad0g.com (staging)-> api.datad0g.com
function apiHost (site) {
return `api.${site}`
}
// Web-app host for dashboard URLs. Single-level sites (datadoghq.com,
// ddog-gov.com) are served from the `app.` subdomain; regional sites
// (us3.datadoghq.com, ap1.datadoghq.com) are used as-is.
function appHost (site) {
return site.split('.').length === 2 ? `app.${site}` : site
}
class ExperimentsClient {
#apiKey
#appKey
#site
#projectName
#timeout
#cachedProjectId
constructor ({ apiKey, appKey, site, projectName, timeout = 30_000 } = {}) {
this.#apiKey = apiKey
this.#appKey = appKey
this.#site = site
this.#projectName = projectName
this.#timeout = timeout
this.#cachedProjectId = null
}
// Whether the client has everything it needs to talk to the control plane.
get configured () {
return Boolean(this.#apiKey && this.#appKey && this.#site)
}
get site () {
return this.#site
}
// Dashboard URL base for the configured site, e.g. https://app.datadoghq.com
get appBase () {
return `https://${appHost(this.#site)}`
}
// Resolve the configured project's id (get-or-create), cached.
ensureProjectId () {
return this.getOrCreateProject(this.#projectName)
}
// Low-level request. Builds https://api.<site><path>, attaches both keys, and
// returns the parsed JSON body. Throws with status + body on a non-2xx.
async request (method, path, body) {
const url = `https://${apiHost(this.#site)}${path}`
const headers = {
'DD-API-KEY': this.#apiKey,
'DD-APPLICATION-KEY': this.#appKey,
}
let payload
if (body !== undefined) {
payload = JSON.stringify(body)
headers['Content-Type'] = 'application/json'
}
let response
try {
response = await fetch(url, {
method,
headers,
body: payload,
signal: AbortSignal.timeout(this.#timeout),
})
} catch (err) {
throw new Error(`${method} ${path} failed: ${err.message}`)
}
const text = await response.text()
if (!response.ok) {
throw new Error(`${method} ${path} failed: HTTP ${response.status} ${text}`)
}
return text ? JSON.parse(text) : {}
}
// Resolve the project id for `name`, creating it if absent. The create
// endpoint is get-or-create on name, so repeated calls return the same id.
// Cached after the first resolution.
async getOrCreateProject (name) {
if (this.#cachedProjectId) return this.#cachedProjectId
let response
try {
response = await this.request('POST', `${API_BASE_PATH}/projects`, {
data: { type: 'projects', attributes: { name } },
})
} catch (err) {
throw new Error(`Failed to create or get project '${name}': ${err.message}`)
}
this.#cachedProjectId = response?.data?.id ?? null
return this.#cachedProjectId
}
}
module.exports = { ExperimentsClient, apiHost, appHost, API_BASE_PATH }