Files
simstudioai--sim/apps/sim/tools/new_relic/create_deployment_event.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

385 lines
11 KiB
TypeScript

import type {
NewRelicChangeTrackingEvent,
NewRelicCreateDeploymentEventParams,
NewRelicCreateDeploymentEventResponse,
NewRelicDeploymentType,
} from '@/tools/new_relic/types'
import {
cleanOptionalString,
getNerdGraphEndpoint,
gqlString,
newRelicHeaders,
parseNerdGraphResponse,
} from '@/tools/new_relic/utils'
import type { ToolConfig } from '@/tools/types'
interface CreateDeploymentEventData {
changeTrackingCreateEvent?: {
changeTrackingEvent?: NewRelicChangeTrackingEvent | null
messages?: string[]
} | null
}
const DEPLOYMENT_TYPES: NewRelicDeploymentType[] = [
'basic',
'blue green',
'canary',
'rolling',
'shadow',
]
const CUSTOM_ATTRIBUTE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
const RESTRICTED_CUSTOM_ATTRIBUTE_NAMES = new Set([
'accountId',
'ago',
'and',
'appID',
'as',
'auto',
'begin',
'begintime',
'category',
'categoryType',
'changeTrackingId',
'compare',
'customAttributes',
'customType',
'day',
'days',
'description',
'end',
'endtime',
'explain',
'entityGuid',
'entityName',
'eventType',
'facet',
'from',
'groupId',
'hostname',
'hour',
'hours',
'in',
'is',
'like',
'limit',
'log',
'minute',
'minutes',
'month',
'months',
'not',
'null',
'offset',
'or',
'raw',
'second',
'seconds',
'select',
'since',
'timeseries',
'timestamp',
'type',
'until',
'user',
'week',
'weeks',
'where',
'with',
])
const getDeploymentType = (value?: string): NewRelicDeploymentType => {
const normalized = value?.toLowerCase().replace(/[_-]/g, ' ')
return DEPLOYMENT_TYPES.includes(normalized as NewRelicDeploymentType)
? (normalized as NewRelicDeploymentType)
: 'basic'
}
const graphqlLiteral = (value: string | number | boolean): string => {
if (typeof value === 'string') return gqlString(value)
return String(value)
}
const buildCustomAttributes = (
customAttributes?: NewRelicCreateDeploymentEventParams['customAttributes']
): string | undefined => {
if (!customAttributes) return undefined
const entries = Object.entries(customAttributes)
if (!entries.length) return undefined
for (const [key, value] of entries) {
if (!CUSTOM_ATTRIBUTE_NAME_PATTERN.test(key)) {
throw new Error(
`Invalid New Relic custom attribute name "${key}". Use letters, numbers, and underscores, and do not start with a number.`
)
}
if (RESTRICTED_CUSTOM_ATTRIBUTE_NAMES.has(key) || key.includes('.')) {
throw new Error(`New Relic custom attribute name "${key}" is restricted`)
}
if (!['string', 'number', 'boolean'].includes(typeof value)) {
throw new Error(
`Invalid value for New Relic custom attribute "${key}". Use a string, number, or boolean.`
)
}
if (typeof value === 'number' && !Number.isFinite(value)) {
throw new Error(`Invalid numeric value for New Relic custom attribute "${key}"`)
}
}
const fields = entries.map(([key, value]) => `${key}: ${graphqlLiteral(value)}`).join(', ')
return `customAttributes: { ${fields} }`
}
const buildDeploymentMutation = (params: NewRelicCreateDeploymentEventParams): string => {
const deploymentType = getDeploymentType(params.deploymentType)
const entityGuid = params.entityGuid.trim()
if (!entityGuid || entityGuid.includes("'")) {
throw new Error('Invalid entity GUID: value must not be empty or contain single quotes')
}
const version = params.version.trim()
const shortDescription = cleanOptionalString(params.shortDescription)
const description = cleanOptionalString(params.description)
const changelog = cleanOptionalString(params.changelog)
const commit = cleanOptionalString(params.commit)
const deepLink = cleanOptionalString(params.deepLink)
const user = cleanOptionalString(params.user)
const groupId = cleanOptionalString(params.groupId)
const customAttributes = buildCustomAttributes(params.customAttributes)
const deploymentFields = [
`version: ${gqlString(version)}`,
changelog ? `changelog: ${gqlString(changelog)}` : undefined,
commit ? `commit: ${gqlString(commit)}` : undefined,
deepLink ? `deepLink: ${gqlString(deepLink)}` : undefined,
]
.filter((field): field is string => Boolean(field))
.join(', ')
const optionalFields = [
shortDescription ? `shortDescription: ${gqlString(shortDescription)}` : undefined,
description ? `description: ${gqlString(description)}` : undefined,
user ? `user: ${gqlString(user)}` : undefined,
groupId ? `groupId: ${gqlString(groupId)}` : undefined,
customAttributes,
params.timestamp ? `timestamp: ${Math.trunc(Number(params.timestamp))}` : undefined,
]
.filter((field): field is string => Boolean(field))
.join('\n ')
return `mutation {
changeTrackingCreateEvent(
changeTrackingEvent: {
categoryAndTypeData: {
categoryFields: { deployment: { ${deploymentFields} } }
kind: { category: "deployment", type: ${gqlString(deploymentType)} }
}
entitySearch: { query: ${gqlString(`id = '${entityGuid}'`)} }
${optionalFields}
}
) {
changeTrackingEvent {
category
categoryAndType
changeTrackingId
customAttributes
description
entity {
guid
name
}
groupId
shortDescription
timestamp
type
user
}
messages
}
}`
}
export const newRelicCreateDeploymentEventTool: ToolConfig<
NewRelicCreateDeploymentEventParams,
NewRelicCreateDeploymentEventResponse
> = {
id: 'new_relic_create_deployment_event',
name: 'New Relic Create Deployment Event',
description: 'Record a deployment change event in New Relic change tracking.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'New Relic user API key for NerdGraph',
},
region: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'New Relic data center region: us or eu',
},
entityGuid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'GUID of the entity associated with the deployment',
},
version: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Deployment version, release name, or commit SHA',
},
shortDescription: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Short description of the deployment',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Longer deployment description',
},
changelog: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Deployment changelog text or URL',
},
commit: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Commit SHA or identifier associated with the deployment',
},
deepLink: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'URL to the deployment, build, or release details',
},
user: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'User or automation that performed the deployment',
},
groupId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional group ID to correlate related changes',
},
customAttributes: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Custom change event metadata as key-value pairs with string, number, or boolean values',
},
deploymentType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Deployment type: basic, blue green, canary, rolling, or shadow',
},
timestamp: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Event timestamp in milliseconds since Unix epoch',
},
},
request: {
url: (params) => getNerdGraphEndpoint(params.region),
method: 'POST',
headers: (params) => newRelicHeaders(params.apiKey),
body: (params) => ({
query: buildDeploymentMutation(params),
}),
},
transformResponse: async (response) => {
const payload = await parseNerdGraphResponse<CreateDeploymentEventData>(response)
const result = payload.data?.changeTrackingCreateEvent
if (!result) {
throw new Error('New Relic did not return a deployment change tracking result')
}
if (!result.changeTrackingEvent) {
const message = result.messages?.length
? result.messages.join('; ')
: 'New Relic did not create a deployment change tracking event'
throw new Error(message)
}
return {
success: true,
output: {
event: result.changeTrackingEvent,
messages: result?.messages ?? [],
},
}
},
outputs: {
event: {
type: 'object',
description: 'Created New Relic change tracking event',
properties: {
changeTrackingId: {
type: 'string',
description: 'New Relic change tracking ID',
nullable: true,
},
customAttributes: {
type: 'json',
description: 'Custom attributes on the change tracking event',
optional: true,
nullable: true,
},
category: { type: 'string', description: 'Change category', nullable: true },
categoryAndType: {
type: 'string',
description: 'Combined category and type',
nullable: true,
},
type: { type: 'string', description: 'Change type', nullable: true },
shortDescription: {
type: 'string',
description: 'Short change description',
nullable: true,
},
description: { type: 'string', description: 'Change description', nullable: true },
timestamp: {
type: 'number',
description: 'Change timestamp in milliseconds',
nullable: true,
},
user: { type: 'string', description: 'User associated with the change', nullable: true },
groupId: { type: 'string', description: 'Change group ID', nullable: true },
entity: {
type: 'object',
description: 'Entity associated with the change',
nullable: true,
properties: {
guid: { type: 'string', description: 'Entity GUID', nullable: true },
name: { type: 'string', description: 'Entity name', nullable: true },
},
},
},
},
messages: {
type: 'array',
description: 'Messages returned by New Relic for the created change event',
items: {
type: 'string',
description: 'New Relic message',
},
},
},
}