diff --git a/integrations/browser/integration.definition.ts b/integrations/browser/integration.definition.ts index 76c3d912337..e7a5749a6fc 100644 --- a/integrations/browser/integration.definition.ts +++ b/integrations/browser/integration.definition.ts @@ -3,7 +3,7 @@ import { IntegrationDefinition } from '@botpress/sdk' import { actionDefinitions } from 'src/definitions/actions' export const INTEGRATION_NAME = 'browser' -export const INTEGRATION_VERSION = '0.8.7' +export const INTEGRATION_VERSION = '0.8.8' export default new IntegrationDefinition({ name: INTEGRATION_NAME, @@ -22,10 +22,6 @@ export default new IntegrationDefinition({ FIRECRAWL_API_KEY: { description: 'FireCrawl key', }, - FIRECRAWL_CUSTOM_HEADERS: { - description: 'Custom HTTP headers to include in Firecrawl scrape requests (JSON object)', - optional: true, - }, LOGO_API_KEY: { description: 'Logo key', }, diff --git a/integrations/browser/src/actions/browse-pages.ts b/integrations/browser/src/actions/browse-pages.ts index 748b7e89504..1ce0c8c0e73 100644 --- a/integrations/browser/src/actions/browse-pages.ts +++ b/integrations/browser/src/actions/browse-pages.ts @@ -15,23 +15,6 @@ const fixOutput = (val: unknown): string => { return '' } -const getCustomHeaders = (): Record | undefined => { - const raw = bp.secrets.FIRECRAWL_CUSTOM_HEADERS - if (!raw) { - return undefined - } - - try { - const parsed = JSON.parse(raw) - if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { - return undefined - } - return parsed as Record - } catch { - return undefined - } -} - const getPageContent = async (props: { url: string logger: IntegrationLogger @@ -51,7 +34,7 @@ const getPageContent = async (props: { waitFor: props.waitFor, timeout: props.timeout, formats: ['markdown', 'rawHtml'], - headers: getCustomHeaders(), + headers: { 'X-Botpress-Crawler': 'botpress' }, storeInCache: true, }) diff --git a/integrations/chat/package.json b/integrations/chat/package.json index 64b0166f5ce..338965fe9fa 100644 --- a/integrations/chat/package.json +++ b/integrations/chat/package.json @@ -11,6 +11,14 @@ "dependencies": { "@aws-sdk/client-dynamodb": "^3.564.0", "@botpress/sdk": "workspace:*", + "@opentelemetry/api": "1.9.0", + "@opentelemetry/core": "1.30.0", + "@opentelemetry/exporter-trace-otlp-http": "0.54.2", + "@opentelemetry/instrumentation": "0.54.2", + "@opentelemetry/instrumentation-http": "0.54.2", + "@opentelemetry/resources": "1.27.0", + "@opentelemetry/sdk-trace-base": "1.27.0", + "@opentelemetry/sdk-trace-node": "1.27.0", "ajv": "^8.12.0", "axios": "1.2.5", "chalk": "^4.1.2", diff --git a/integrations/chat/src/api/operations/conversation.ts b/integrations/chat/src/api/operations/conversation.ts index f4eecd4bcb7..3a25ba21333 100644 --- a/integrations/chat/src/api/operations/conversation.ts +++ b/integrations/chat/src/api/operations/conversation.ts @@ -1,5 +1,6 @@ import * as errors from '../../gen/errors' import { validateFid } from '../../id-store' +import { setSpanAttributes, SPAN_ATTRS } from '../../tracing' import * as types from '../types' import * as fid from './fid' import * as model from './model' @@ -12,6 +13,8 @@ export const createConversation: types.AuthenticatedOperations['createConversati auth: { userId }, } = req + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: userId }) + const { conversation } = await props.client.createConversation({ channel: 'channel', tags: { @@ -20,6 +23,8 @@ export const createConversation: types.AuthenticatedOperations['createConversati }, }) + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversation.id }) + await props.client.addParticipant({ id: conversation.id, userId }) return fidHandler.mapResponse({ @@ -33,6 +38,8 @@ export const getConversation: types.AuthenticatedOperations['getConversation'] = const fidHandler = fid.handlers.getConversation(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: req.params.id, [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { conversation } = await props.client.getConversation({ id: req.params.id }) const { participant } = await props.apiUtils.findParticipant({ @@ -62,6 +69,8 @@ export const getOrCreateConversation: types.AuthenticatedOperations['getOrCreate const existingId = await props.convIdStore.byFid.find(conversationFid) if (existingId) { + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: existingId, [SPAN_ATTRS.USER_ID]: userId }) + const { conversation } = await props.client.getConversation({ id: existingId }) if (conversation.tags.owner !== userId) { throw new errors.ForbiddenError('You are not the owner of this conversation') @@ -96,6 +105,7 @@ export const getOrCreateConversation: types.AuthenticatedOperations['getOrCreate const { id: conversationId } = conversation + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: userId }) await props.client.addParticipant({ id: conversationId, userId }) await props.convIdStore.byFid.set(conversationFid, conversationId) @@ -118,6 +128,8 @@ export const deleteConversation: types.AuthenticatedOperations['deleteConversati const fidHandler = fid.handlers.deleteConversation(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: req.params.id, [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { conversation } = await props.client.getConversation({ id: req.params.id }) if (conversation.tags.owner !== req.auth.userId) { throw new errors.ForbiddenError('You are not the owner of this conversation') @@ -132,6 +144,8 @@ export const listConversations: types.AuthenticatedOperations['listConversations const fidHandler = fid.handlers.listConversations(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { conversations, meta } = await props.client.listConversations({ nextToken: req.query.nextToken, tags: { owner: req.auth.userId }, @@ -156,6 +170,8 @@ export const listMessages: types.AuthenticatedOperations['listMessages'] = async const { nextToken } = req.query const { conversationId } = req.params + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { participant } = await props.apiUtils.findParticipant({ id: conversationId, userId: req.auth.userId }) if (!participant) { throw new errors.ForbiddenError('You are not a participant in this conversation') @@ -178,6 +194,8 @@ export const listenConversation: types.AuthenticatedOperations['listenConversati const userId = req.auth.userId const conversationId = req.params.id + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: userId }) + const { participant } = await props.apiUtils.findParticipant({ id: conversationId, userId }) if (!participant) { throw new errors.ForbiddenError('You are not a participant in this conversation') @@ -211,6 +229,8 @@ export const addParticipant: types.AuthenticatedOperations['addParticipant'] = a const conversationId = req.params.conversationId const participantId = req.body.userId + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: userId }) + const { conversation: { tags: { owner }, @@ -244,6 +264,8 @@ export const getParticipant: types.AuthenticatedOperations['getParticipant'] = a const fidHandler = fid.handlers.getParticipant(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: req.params.conversationId, [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { conversation: { tags: { owner }, @@ -276,6 +298,8 @@ export const removeParticipant: types.AuthenticatedOperations['removeParticipant const conversationId = req.params.conversationId const participantId = req.params.userId + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: userId }) + const { conversation: { tags: { owner }, @@ -313,6 +337,8 @@ export const listParticipants: types.AuthenticatedOperations['listParticipants'] const fidHandler = fid.handlers.listParticipants(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: req.params.conversationId, [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { participant } = await props.apiUtils.findParticipant({ id: req.params.conversationId, userId: req.auth.userId, diff --git a/integrations/chat/src/api/operations/event.ts b/integrations/chat/src/api/operations/event.ts index 10609ebeb95..c18d71a8aa3 100644 --- a/integrations/chat/src/api/operations/event.ts +++ b/integrations/chat/src/api/operations/event.ts @@ -1,4 +1,5 @@ import * as errors from '../../gen/errors' +import { setSpanAttributes, SPAN_ATTRS } from '../../tracing' import * as types from '../types' import * as fid from './fid' import * as model from './model' @@ -10,6 +11,8 @@ export const createEvent: types.AuthenticatedOperations['createEvent'] = async ( const { conversationId, payload } = req.body const { userId } = req.auth + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: userId }) + const { participant } = await props.apiUtils.findParticipant({ id: conversationId, userId: req.auth.userId }) if (!participant) { throw new errors.ForbiddenError("You are not a participant in this event's conversation") @@ -44,8 +47,11 @@ export const getEvent: types.AuthenticatedOperations['getEvent'] = async (props, const fidHandler = fid.handlers.getEvent(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId }) + const { event } = await props.client.getEvent({ id: req.params.id }) const { conversationId } = event.payload + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId }) const { participant } = await props.apiUtils.findParticipant({ id: conversationId, userId: req.auth.userId }) if (!participant) { throw new errors.ForbiddenError("You are not a participant in this event's conversation") diff --git a/integrations/chat/src/api/operations/message.ts b/integrations/chat/src/api/operations/message.ts index 6db197525bc..6654cb245e4 100644 --- a/integrations/chat/src/api/operations/message.ts +++ b/integrations/chat/src/api/operations/message.ts @@ -1,4 +1,5 @@ import * as errors from '../../gen/errors' +import { setSpanAttributes, SPAN_ATTRS } from '../../tracing' import * as msgPayload from '../message-payload' import * as types from '../types' import * as fid from './fid' @@ -11,7 +12,10 @@ export const createMessage: types.AuthenticatedOperations['createMessage'] = asy const { conversationId, payload, metadata } = req.body const { userId } = req.auth + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId, [SPAN_ATTRS.USER_ID]: userId }) + const { participant } = await props.apiUtils.findParticipant({ id: conversationId, userId: req.auth.userId }) + if (!participant) { throw new errors.ForbiddenError("You are not a participant in this message's conversation") } @@ -25,6 +29,8 @@ export const createMessage: types.AuthenticatedOperations['createMessage'] = asy payload: mappedPayload, }) + setSpanAttributes({ [SPAN_ATTRS.MESSAGE_ID]: message.id }) + const res = await fidHandler.mapResponse({ body: { message: model.mapMessage(message), @@ -43,8 +49,11 @@ export const getMessage: types.AuthenticatedOperations['getMessage'] = async (pr const fidHandler = fid.handlers.getMessage(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId, [SPAN_ATTRS.MESSAGE_ID]: req.params.id }) + const { message } = await props.client.getMessage({ id: req.params.id }) const { conversationId } = message + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: conversationId }) const { participant } = await props.apiUtils.findParticipant({ id: conversationId, userId: req.auth.userId }) if (!participant) { throw new errors.ForbiddenError("You are not a participant in this message's conversation") @@ -63,7 +72,12 @@ export const deleteMessage: types.AuthenticatedOperations['deleteMessage'] = asy const { id } = req.params + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId, [SPAN_ATTRS.MESSAGE_ID]: id }) + const { message } = await props.client.getMessage({ id }) + + setSpanAttributes({ [SPAN_ATTRS.CONVERSATION_ID]: message.conversationId }) + if (message.userId !== req.auth.userId) { throw new errors.ForbiddenError('You are not the sender of this message') } diff --git a/integrations/chat/src/api/operations/user.ts b/integrations/chat/src/api/operations/user.ts index efc6a3d728b..9a5f467dc86 100644 --- a/integrations/chat/src/api/operations/user.ts +++ b/integrations/chat/src/api/operations/user.ts @@ -1,5 +1,6 @@ import * as errors from '../../gen/errors' import { validateFid } from '../../id-store' +import { setSpanAttributes, SPAN_ATTRS } from '../../tracing' import * as types from '../types' import * as fid from './fid' import * as model from './model' @@ -28,6 +29,7 @@ export const createUser: types.Operations['createUser'] = async (props, foreignR }) const userFid = foreignReq.body.id ?? user.id + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: user.id }) const userKey = props.auth.generateKey({ id: userFid }) return fidHandler.mapResponse({ body: { @@ -41,6 +43,7 @@ export const getUser: types.AuthenticatedOperations['getUser'] = async (props, f const fidHandler = fid.handlers.getUser(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId }) const { user } = await props.client.getUser({ id: req.auth.userId }) return fidHandler.mapResponse({ body: { @@ -60,6 +63,7 @@ export const getOrCreateUser: types.AuthenticatedOperations['getOrCreateUser'] = (await props.apiUtils.findUser({ id: userFid }).then((res) => res.user?.id)) if (existingId) { + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: existingId }) const { user: updatedUser } = await props.client.updateUser({ id: existingId, name, @@ -103,6 +107,7 @@ export const getOrCreateUser: types.AuthenticatedOperations['getOrCreateUser'] = }, } + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: newUser.id }) await props.userIdStore.byFid.set(userFid, newUser.id) return fid.merge(res, { body: { @@ -121,6 +126,7 @@ export const updateUser: types.AuthenticatedOperations['updateUser'] = async (pr body: { name, pictureUrl, profile }, } = req + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId }) const { user } = await props.client.updateUser({ id: req.auth.userId, name, pictureUrl, tags: { profile } }) return fidHandler.mapResponse({ @@ -134,6 +140,7 @@ export const deleteUser: types.AuthenticatedOperations['deleteUser'] = async (pr const fidHandler = fid.handlers.deleteUser(props, foreignReq) const req = await fidHandler.mapRequest() + setSpanAttributes({ [SPAN_ATTRS.USER_ID]: req.auth.userId }) await props.client.deleteUser({ id: req.auth.userId }) return fidHandler.mapResponse({ body: {} }) } diff --git a/integrations/chat/src/id-store/dynamo-db-store.ts b/integrations/chat/src/id-store/dynamo-db-store.ts index d785ba5b278..967a56a2d27 100644 --- a/integrations/chat/src/id-store/dynamo-db-store.ts +++ b/integrations/chat/src/id-store/dynamo-db-store.ts @@ -1,5 +1,6 @@ import * as dynamodb from '@aws-sdk/client-dynamodb' import { logger } from '../logger' +import { runWithSpan, SPAN_ATTRS } from '../tracing' import * as errors from './errors' import * as types from './types' @@ -23,17 +24,22 @@ class DynamoDbMap implements types.IdMap { public async find(src: string): Promise { const { botId, tableName, indexName, partitionKey, srcKeyName, destKeyName } = this._props - const { Items } = await this._client.send( - new dynamodb.QueryCommand({ - TableName: tableName, - IndexName: indexName, - ConsistentRead: true, - KeyConditionExpression: `${partitionKey} = :bot_id AND ${srcKeyName} = :src`, - ExpressionAttributeValues: { - ':bot_id': { S: botId }, - ':src': { S: src }, - }, - }) + const { Items } = await runWithSpan( + 'dynamodb.find', + () => + this._client.send( + new dynamodb.QueryCommand({ + TableName: tableName, + IndexName: indexName, + ConsistentRead: true, + KeyConditionExpression: `${partitionKey} = :bot_id AND ${srcKeyName} = :src`, + ExpressionAttributeValues: { + ':bot_id': { S: botId }, + ':src': { S: src }, + }, + }) + ), + { attributes: { [SPAN_ATTRS.DB_TABLE]: tableName, [SPAN_ATTRS.DB_KEY]: src } } ) const dest = Items?.[0]?.[destKeyName]?.S @@ -76,39 +82,49 @@ class IncomingDynamoDbMap extends DynamoDbMap implements types.IncomingIdMap { const { botId, tableName, partitionKey: partitionKeyName, sortKey, indexSortKey } = this._args const createdAt = String(Date.now()) - await this._client - .send( - new dynamodb.PutItemCommand({ - TableName: tableName, - Item: { - [partitionKeyName]: { S: botId }, - [sortKey]: { S: fid }, - [indexSortKey]: { S: id }, - created_at: { N: createdAt }, - }, - ConditionExpression: `attribute_not_exists(${partitionKeyName}) AND attribute_not_exists(${sortKey})`, - }) - ) - .catch((thrown) => { - if (thrown instanceof dynamodb.ConditionalCheckFailedException) { - throw new errors.IdAlreadyAssignedError(fid) - } - throw thrown - }) + await runWithSpan( + 'dynamodb.set', + () => + this._client + .send( + new dynamodb.PutItemCommand({ + TableName: tableName, + Item: { + [partitionKeyName]: { S: botId }, + [sortKey]: { S: fid }, + [indexSortKey]: { S: id }, + created_at: { N: createdAt }, + }, + ConditionExpression: `attribute_not_exists(${partitionKeyName}) AND attribute_not_exists(${sortKey})`, + }) + ) + .catch((thrown) => { + if (thrown instanceof dynamodb.ConditionalCheckFailedException) { + throw new errors.IdAlreadyAssignedError(fid) + } + throw thrown + }), + { attributes: { [SPAN_ATTRS.DB_TABLE]: tableName, [SPAN_ATTRS.DB_KEY]: fid } } + ) this._debug('set', fid, id) } public async delete(fid: string): Promise { const { botId, tableName, partitionKey: partitionKeyName, sortKey } = this._args - await this._client.send( - new dynamodb.DeleteItemCommand({ - TableName: tableName, - Key: { - [partitionKeyName]: { S: botId }, - [sortKey]: { S: fid }, - }, - }) + await runWithSpan( + 'dynamodb.delete', + () => + this._client.send( + new dynamodb.DeleteItemCommand({ + TableName: tableName, + Key: { + [partitionKeyName]: { S: botId }, + [sortKey]: { S: fid }, + }, + }) + ), + { attributes: { [SPAN_ATTRS.DB_TABLE]: tableName, [SPAN_ATTRS.DB_KEY]: fid } } ) } } @@ -142,13 +158,20 @@ class OutgoingDynamoDbMap extends DynamoDbMap implements types.OutoingIdMap { const { botId, tableName, indexName, partitionKey, indexSortKey, sortKey } = this._args const uniqueIds = Array.from(new Set(ids)) - const whereIn = uniqueIds.map((id) => `'${id}'`).join(', ') + const placeholders = uniqueIds.map(() => '?').join(', ') + const parameters = [{ S: botId }, ...uniqueIds.map((id) => ({ S: id }))] // cannot perform a batch get operation on a secondary index, so we use PartiQl instead - const { Items } = await this._client.send( - new dynamodb.ExecuteStatementCommand({ - Statement: `SELECT ${indexSortKey}, ${sortKey} FROM "${tableName}"."${indexName}" WHERE ${partitionKey} = '${botId}' AND ${indexSortKey} IN (${whereIn})`, - }) + const { Items } = await runWithSpan( + 'dynamodb.fetch', + () => + this._client.send( + new dynamodb.ExecuteStatementCommand({ + Statement: `SELECT ${indexSortKey}, ${sortKey} FROM "${tableName}"."${indexName}" WHERE ${partitionKey} = ? AND ${indexSortKey} IN (${placeholders})`, + Parameters: parameters, + }) + ), + { attributes: { [SPAN_ATTRS.DB_TABLE]: tableName, [SPAN_ATTRS.DB_COUNT]: String(uniqueIds.length) } } ) const entries: Record = {} diff --git a/integrations/chat/src/index.ts b/integrations/chat/src/index.ts index da8a63ee214..3f2756ecbcd 100644 --- a/integrations/chat/src/index.ts +++ b/integrations/chat/src/index.ts @@ -7,9 +7,15 @@ import { makeHandler } from './handler' import { MemorySpace, ChatIdStore, InMemoryChatIdStore, DynamoDbChatIdStore } from './id-store' import { Options, options } from './options' import { CompositeSignalEmiter, PushpinEmitter, SignalEmitter, WebhookEmitter } from './signal-emitter' +import { initTracing, normalizePath, runWithSpan, setSpanAttributes, SPAN_ATTRS } from './tracing' import { MessageArgs, ActionArgs } from './types' import * as bp from '.botpress' +const tracingProvider = initTracing() +if (tracingProvider) { + process.on('SIGTERM', () => void tracingProvider.shutdown().catch(console.error)) +} + const memSpace = new MemorySpace() type ChatIdStores = Record<'convIdStore' | 'userIdStore', ChatIdStore> @@ -85,60 +91,77 @@ const mapEventSignalFid = async (idStores: ChatIdStores, args: ActionArgs): Prom } const emitMessage = async (args: MessageArgs) => { - const opts = options(args) - const signalEmitter = makeEmitter(opts) - const idStores = makeIdStores(opts) - - const { - conversation: { id: channel }, - } = args - - args = await mapMessageSignalFid(idStores, args) - debug.debugSignal(args) - - const { metadata, payload } = mapBotpressMessageToChat(args) - await signalEmitter.emit(channel, { - type: 'message_created', - data: { - id: args.message.id, - conversationId: args.conversation.id, - userId: args.user.id, - createdAt: args.message.createdAt, - payload, - metadata, - isBot: true, - }, + await runWithSpan('emit.message', async () => { + const opts = options(args) + const signalEmitter = makeEmitter(opts) + const idStores = makeIdStores(opts) + + const { + conversation: { id: channel }, + } = args + + args = await mapMessageSignalFid(idStores, args) + debug.debugSignal(args) + + setSpanAttributes({ + [SPAN_ATTRS.CONVERSATION_ID]: args.conversation.id, + [SPAN_ATTRS.USER_ID]: args.user.id, + [SPAN_ATTRS.MESSAGE_ID]: args.message.id, + }) + + const { metadata, payload } = mapBotpressMessageToChat(args) + await signalEmitter.emit(channel, { + type: 'message_created', + data: { + id: args.message.id, + conversationId: args.conversation.id, + userId: args.user.id, + createdAt: args.message.createdAt, + payload, + metadata, + isBot: true, + }, + }) }) } const emitEvent = async (args: ActionArgs) => { - const opts = options(args) - const signalEmitter = makeEmitter(opts) - const idStores = makeIdStores(opts) - - const { - input: { conversationId: channel }, - } = args - - args = await mapEventSignalFid(idStores, args) - debug.debugSignal(args) - - await signalEmitter.emit(channel, { - type: 'event_created', - data: { - id: null, - createdAt: new Date().toISOString(), - conversationId: args.input.conversationId, - userId: args.ctx.botUserId, - payload: args.input.payload, - isBot: true, - }, + await runWithSpan('emit.event', async () => { + const opts = options(args) + const signalEmitter = makeEmitter(opts) + const idStores = makeIdStores(opts) + + const { + input: { conversationId: channel }, + } = args + + args = await mapEventSignalFid(idStores, args) + debug.debugSignal(args) + + setSpanAttributes({ + [SPAN_ATTRS.CONVERSATION_ID]: args.input.conversationId, + }) + + await signalEmitter.emit(channel, { + type: 'event_created', + data: { + id: null, + createdAt: new Date().toISOString(), + conversationId: args.input.conversationId, + userId: args.ctx.botUserId, + payload: args.input.payload, + isBot: true, + }, + }) }) } export default new bp.Integration({ register: async () => {}, unregister: async () => {}, + __advanced: { + managesOwnTracePropagation: !!tracingProvider, + }, actions: { sendEvent: async (props) => { await emitEvent(props) @@ -179,7 +202,20 @@ export default new bp.Integration({ }) const reqId = debug.debugRequest(props.req) - const res = await handler(props) + const res = await runWithSpan( + `${props.req.method} ${normalizePath(props.req.path)}`, + async () => { + setSpanAttributes({ + [SPAN_ATTRS.BOT_ID]: props.ctx.botId, + [SPAN_ATTRS.INTEGRATION_ID]: props.ctx.integrationId, + }) + return handler(props) + }, + { + // traceHeaders is only used for W3C context extraction — header values are not stored as span attributes + traceHeaders: props.req.headers, + } + ) debug.debugResponse(reqId, res) return res diff --git a/integrations/chat/src/tracing.ts b/integrations/chat/src/tracing.ts new file mode 100644 index 00000000000..3108b540ccd --- /dev/null +++ b/integrations/chat/src/tracing.ts @@ -0,0 +1,124 @@ +import { context, propagation, SpanStatusCode, trace } from '@opentelemetry/api' +import { W3CTraceContextPropagator } from '@opentelemetry/core' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' +import { registerInstrumentations } from '@opentelemetry/instrumentation' +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http' +import { Resource } from '@opentelemetry/resources' +import { BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base' +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node' + +const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i +const ULID_REGEX = /^[a-zA-Z]+_[0-9A-HJKMNP-TV-Z]{26}$/ + +export const normalizePath = (path: string): string => + path + .split('?')[0]! + .split('/') + .map((part) => (UUID_REGEX.test(part) || ULID_REGEX.test(part) ? ':id' : part)) + .join('/') + +export const SPAN_ATTRS = { + USER_ID: 'bp.userId', + CONVERSATION_ID: 'bp.conversationId', + MESSAGE_ID: 'bp.messageId', + BOT_ID: 'bp.botId', + INTEGRATION_ID: 'bp.integrationId', + DB_TABLE: 'db.table', + DB_KEY: 'db.key', + DB_COUNT: 'db.count', +} as const + +export const initTracing = (): NodeTracerProvider | null => { + if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT && process.env.OTEL_CONSOLE_EXPORTER !== 'true') { + return null + } + + const resourceAttrs: Record = { + 'service.name': process.env.OTEL_SERVICE_NAME ?? 'chat-integration', + } + if (process.env.OTEL_SERVICE_VERSION) { + resourceAttrs['service.version'] = process.env.OTEL_SERVICE_VERSION + } + if (process.env.OTEL_DEPLOYMENT_ENVIRONMENT) { + resourceAttrs['deployment.environment'] = process.env.OTEL_DEPLOYMENT_ENVIRONMENT + } + + const provider = new NodeTracerProvider({ + resource: new Resource(resourceAttrs), + }) + + if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { + provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter())) + } + if (process.env.OTEL_CONSOLE_EXPORTER === 'true') { + provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())) + } + provider.register({ propagator: new W3CTraceContextPropagator() }) + + registerInstrumentations({ + tracerProvider: provider, + instrumentations: [ + new HttpInstrumentation({ + // Rename spans for clarity + requestHook: (span, request) => { + if ('complete' in request) { + // IncomingMessage: use x-bp-operation header for a descriptive name + const op = request.headers['x-bp-operation'] + if (typeof op === 'string') { + span.updateName(`bp:${op}`) + } + } else if ('path' in request && request.path) { + // ClientRequest (outgoing): rename from "METHOD" to "-> METHOD /normalized-path" + span.updateName(`-> ${request.method ?? 'HTTP'} ${normalizePath(request.path)}`) + } + }, + }), + ], + }) + + return provider +} + +export const runWithSpan = async ( + spanName: string, + fn: () => Promise, + opts?: { + attributes?: Record + traceHeaders?: Record + } +): Promise => { + const tracer = trace.getTracer('chat-integration') + const parentCtx = opts?.traceHeaders ? propagation.extract(context.active(), opts.traceHeaders) : context.active() + + return context.with(parentCtx, async () => + tracer.startActiveSpan(spanName, { attributes: opts?.attributes }, async (span) => { + try { + const result = await fn() + span.setStatus({ code: SpanStatusCode.OK }) + return result + } catch (err: unknown) { + if (err instanceof Error) { + span.recordException(err) + span.setStatus({ code: SpanStatusCode.ERROR, message: err.message }) + } else { + span.setStatus({ code: SpanStatusCode.ERROR, message: String(err) }) + } + throw err + } finally { + span.end() + } + }) + ) +} + +export const setSpanAttributes = (attrs: Record): void => { + const span = trace.getActiveSpan() + if (!span) { + return + } + for (const [key, value] of Object.entries(attrs)) { + if (value !== undefined) { + span.setAttribute(key, value) + } + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 438e8153ded..13d341ce84c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/cli", - "version": "6.3.4", + "version": "6.3.5", "description": "Botpress CLI", "scripts": { "build": "pnpm run build:types && pnpm run bundle && pnpm run template:gen", @@ -28,7 +28,7 @@ "@apidevtools/json-schema-ref-parser": "^11.7.0", "@botpress/chat": "0.5.5", "@botpress/client": "1.40.0", - "@botpress/sdk": "6.4.5", + "@botpress/sdk": "6.5.0", "@bpinternal/const": "^0.1.0", "@bpinternal/tunnel": "^0.1.1", "@bpinternal/verel": "^0.2.0", diff --git a/packages/cli/templates/empty-bot/package.json b/packages/cli/templates/empty-bot/package.json index 387ab643078..720b4fdb418 100644 --- a/packages/cli/templates/empty-bot/package.json +++ b/packages/cli/templates/empty-bot/package.json @@ -6,7 +6,7 @@ "private": true, "dependencies": { "@botpress/client": "1.40.0", - "@botpress/sdk": "6.4.5" + "@botpress/sdk": "6.5.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-integration/package.json b/packages/cli/templates/empty-integration/package.json index ffb159c9835..5e36fe36a66 100644 --- a/packages/cli/templates/empty-integration/package.json +++ b/packages/cli/templates/empty-integration/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.40.0", - "@botpress/sdk": "6.4.5" + "@botpress/sdk": "6.5.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/empty-plugin/package.json b/packages/cli/templates/empty-plugin/package.json index e57832f8603..f7e02a797b5 100644 --- a/packages/cli/templates/empty-plugin/package.json +++ b/packages/cli/templates/empty-plugin/package.json @@ -6,7 +6,7 @@ }, "private": true, "dependencies": { - "@botpress/sdk": "6.4.5" + "@botpress/sdk": "6.5.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/hello-world/package.json b/packages/cli/templates/hello-world/package.json index 01954a75cb1..619a328a4da 100644 --- a/packages/cli/templates/hello-world/package.json +++ b/packages/cli/templates/hello-world/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.40.0", - "@botpress/sdk": "6.4.5" + "@botpress/sdk": "6.5.0" }, "devDependencies": { "@types/node": "^22.16.4", diff --git a/packages/cli/templates/webhook-message/package.json b/packages/cli/templates/webhook-message/package.json index 5a051e6b54d..726ef8639eb 100644 --- a/packages/cli/templates/webhook-message/package.json +++ b/packages/cli/templates/webhook-message/package.json @@ -7,7 +7,7 @@ "private": true, "dependencies": { "@botpress/client": "1.40.0", - "@botpress/sdk": "6.4.5", + "@botpress/sdk": "6.5.0", "axios": "^1.6.8" }, "devDependencies": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8371bc55afc..661a9c9b1c5 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@botpress/sdk", - "version": "6.4.5", + "version": "6.5.0", "description": "Botpress SDK", "main": "./dist/index.cjs", "module": "./dist/index.mjs", diff --git a/packages/sdk/src/integration/implementation.ts b/packages/sdk/src/integration/implementation.ts index 32a85b854d8..0e3f8d4c239 100644 --- a/packages/sdk/src/integration/implementation.ts +++ b/packages/sdk/src/integration/implementation.ts @@ -29,6 +29,7 @@ export type IntegrationImplementationProps __advanced?: { unknownOperationHandler?: UnknownOperationFunction + managesOwnTracePropagation?: boolean } } @@ -43,6 +44,9 @@ export class IntegrationImplementation['__advanced'] >['unknownOperationHandler'] + public readonly managesOwnTracePropagation: NonNullable< + IntegrationImplementationProps['__advanced'] + >['managesOwnTracePropagation'] public constructor(public readonly props: IntegrationImplementationProps) { this.actions = props.actions @@ -53,6 +57,7 @@ export class IntegrationImplementation) diff --git a/packages/sdk/src/integration/server/index.ts b/packages/sdk/src/integration/server/index.ts index b5b08144369..cb81bb573c2 100644 --- a/packages/sdk/src/integration/server/index.ts +++ b/packages/sdk/src/integration/server/index.ts @@ -48,7 +48,7 @@ const getServerProps = ( integrationId: ctx.integrationId, integrationAlias: ctx.integrationAlias, retry: retryConfig, - headers: extractTracingHeaders(req.headers), + headers: instance.managesOwnTracePropagation ? {} : extractTracingHeaders(req.headers), }) const client = new IntegrationSpecificClient(vanillaClient) const logger = new IntegrationLogger({ traceId }) diff --git a/packages/sdk/src/integration/server/types.ts b/packages/sdk/src/integration/server/types.ts index d23f9578b0e..86057e72e5b 100644 --- a/packages/sdk/src/integration/server/types.ts +++ b/packages/sdk/src/integration/server/types.ts @@ -151,4 +151,5 @@ export type IntegrationHandlers = { actions: ActionHandlers channels: ChannelHandlers unknownOperationHandler?: UnknownOperationHandler + managesOwnTracePropagation?: boolean } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a0f65c3d12..a65806792d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -706,6 +706,30 @@ importers: '@botpress/sdk': specifier: workspace:* version: link:../../packages/sdk + '@opentelemetry/api': + specifier: 1.9.0 + version: 1.9.0 + '@opentelemetry/core': + specifier: 1.30.0 + version: 1.30.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: 0.54.2 + version: 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': + specifier: 0.54.2 + version: 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': + specifier: 0.54.2 + version: 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: 1.27.0 + version: 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: 1.27.0 + version: 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: 1.27.0 + version: 1.27.0(@opentelemetry/api@1.9.0) ajv: specifier: ^8.12.0 version: 8.17.1 @@ -2602,7 +2626,7 @@ importers: specifier: 1.40.0 version: link:../client '@botpress/sdk': - specifier: 6.4.5 + specifier: 6.5.0 version: link:../sdk '@bpinternal/const': specifier: ^0.1.0 @@ -2726,7 +2750,7 @@ importers: specifier: 1.40.0 version: link:../../../client '@botpress/sdk': - specifier: 6.4.5 + specifier: 6.5.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2742,7 +2766,7 @@ importers: specifier: 1.40.0 version: link:../../../client '@botpress/sdk': - specifier: 6.4.5 + specifier: 6.5.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2755,7 +2779,7 @@ importers: packages/cli/templates/empty-plugin: dependencies: '@botpress/sdk': - specifier: 6.4.5 + specifier: 6.5.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2771,7 +2795,7 @@ importers: specifier: 1.40.0 version: link:../../../client '@botpress/sdk': - specifier: 6.4.5 + specifier: 6.5.0 version: link:../../../sdk devDependencies: '@types/node': @@ -2787,7 +2811,7 @@ importers: specifier: 1.40.0 version: link:../../../client '@botpress/sdk': - specifier: 6.4.5 + specifier: 6.5.0 version: link:../../../sdk axios: specifier: ^1.6.8 @@ -5305,6 +5329,112 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.54.2': + resolution: {integrity: sha512-4MTVwwmLgUh5QrJnZpYo6YRO5IBLAggf2h8gWDblwRagDStY13aEvt7gGk3jewrMaPlHiF83fENhIx0HO97/cQ==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.27.0': + resolution: {integrity: sha512-CdZ3qmHCwNhFAzjTgHqrDQ44Qxcpz43cVxZRhOs+Ns/79ug+Mr84Bkb626bkJLkA3+BLimA5YAEVRlJC6pFb7g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.27.0': + resolution: {integrity: sha512-yQPKnK5e+76XuiqUH/gKyS8wv/7qITd5ln56QkBTf3uggr0VkXOXfcaAuG330UfdYu83wsyoBwqwxigpIG+Jkg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.0': + resolution: {integrity: sha512-Q/3u/K73KUjTCnFUP97ZY+pBjQ1kPEgjOfXj/bJl8zW7GbXdkw6cwuyZk6ZTXkVgCBsYRYUzx4fvYK1jxdb9MA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.54.2': + resolution: {integrity: sha512-BgWKKyD/h2zpISdmYHN/sapwTjvt1P4p5yx4xeBV8XAEqh4OQUhOtSGFG80+nPQ1F8of3mKOT1DDoDbJp1u25w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.54.2': + resolution: {integrity: sha512-mABjJ34UcU32pg8g18L9xBh0U3JON/2F6/57BYYy8AZJp2a71lZjcKr0T00pICoic50TW5HvcTrmyfMil+AiXQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.54.2': + resolution: {integrity: sha512-go6zpOVoZVztT9r1aPd79Fr3OWiD4N24bCPJsIKkBses8oyFo12F/Ew3UBTdIu6hsW4HC4MVEJygG6TEyJI/lg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.54.2': + resolution: {integrity: sha512-NrNyxu6R/bGAwanhz1HI0aJWKR6xUED4TjCH4iWMlAfyRukGbI9Kt/Akd2sYLwRKNhfS+sKetKGCUQPMDyYYMA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.54.2': + resolution: {integrity: sha512-2tIjahJlMRRUz0A2SeE+qBkeBXBFkSjR0wqJ08kuOqaL8HNGan5iZf+A8cfrfmZzPUuMKCyY9I+okzFuFs6gKQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@1.27.0': + resolution: {integrity: sha512-pTsko3gnMioe3FeWcwTQR3omo5C35tYsKKwjgTCTVCgd3EOWL9BZrMfgLBmszrwXABDfUrlAEFN/0W0FfQGynQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.27.0': + resolution: {integrity: sha512-EI1bbK0wn0yIuKlc2Qv2LKBRw6LiUWevrjCF80fn/rlaB+7StAi8Y5s8DBqAYNpY7v1q86+NjU18v7hj2ejU3A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.27.0': + resolution: {integrity: sha512-jOwt2VJ/lUD5BLc+PMNymDrUCpm5PKi1E9oSVYAvz01U/VdndGmrtV3DU1pG4AwlYhJRHbHfOUIlpBeXCPw6QQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.54.2': + resolution: {integrity: sha512-yIbYqDLS/AtBbPjCjh6eSToGNRMqW2VR8RrKEy+G+J7dFG7pKoptTH5T+XlKPleP9NY8JZYIpgJBlI+Osi0rFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.27.0': + resolution: {integrity: sha512-JzWgzlutoXCydhHWIbLg+r76m+m3ncqvkCcsswXAQ4gqKS+LOHKhq+t6fx1zNytvLuaOUBur7EvWxECc4jPQKg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.27.0': + resolution: {integrity: sha512-btz6XTQzwsyJjombpeqCX6LhiMQYpzt2pIYNPnw0IPO/3AhT6yjnf8Mnv3ZC2A4eRYOjqrg+bfaXg9XHDRJDWQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.27.0': + resolution: {integrity: sha512-dWZp/dVGdUEfRBjBq2BgNuBlFqHCxyyMc8FsN0NX15X07mxSUO0SZRLyK/fdAVrde8nqFI/FEdMH4rgU9fqJfQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.27.0': + resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + '@oxlint/binding-android-arm-eabi@1.58.0': resolution: {integrity: sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5434,6 +5564,36 @@ packages: '@posthog/core@1.6.0': resolution: {integrity: sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@react-email/body@0.0.10': resolution: {integrity: sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==} peerDependencies: @@ -6486,6 +6646,9 @@ packages: '@types/serve-static@1.15.1': resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/stack-utils@2.0.1': resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} @@ -6686,6 +6849,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -8389,6 +8557,9 @@ packages: resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} deprecated: 'ACTION REQUIRED: SWITCH TO v3 - v1 and v2 are VULNERABLE! v1 is DEPRECATED FOR OVER 2 YEARS! Use formidable@latest or try formidable-mini for fresh projects' + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -8839,6 +9010,9 @@ packages: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} + import-in-the-middle@1.15.0: + resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} + import-lazy@4.0.0: resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} engines: {node: '>=8'} @@ -9620,6 +9794,9 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@2.0.4: resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} @@ -10034,6 +10211,9 @@ packages: mnemonist@0.38.3: resolution: {integrity: sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==} + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + moment@2.29.4: resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==} @@ -10672,6 +10852,10 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.5.5: + resolution: {integrity: sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -10871,6 +11055,10 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -11120,6 +11308,9 @@ packages: resolution: {integrity: sha512-1zXt6XQHT3d7L2dMhmlAoWpPhQhqvxdjrYSOoGwnbbZA8nX4jrGrUPpryOe96XBSaG/d+DJtoDujujjydXICSg==} hasBin: true + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -14914,6 +15105,125 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.54.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/core@1.30.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-trace-otlp-http@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/instrumentation-http@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + forwarded-parse: 2.1.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.15.0 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.54.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.54.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.5 + + '@opentelemetry/propagator-b3@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/sdk-logs@0.54.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.54.2 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 + + '@opentelemetry/sdk-trace-node@1.27.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.27.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.27.0(@opentelemetry/api@1.9.0) + semver: 7.7.2 + + '@opentelemetry/semantic-conventions@1.27.0': {} + + '@opentelemetry/semantic-conventions@1.28.0': {} + '@oxlint/binding-android-arm-eabi@1.58.0': optional: true @@ -14987,6 +15297,29 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@react-email/body@0.0.10(react@18.3.1)': dependencies: react: 18.3.1 @@ -16348,6 +16681,8 @@ snapshots: '@types/mime': 3.0.1 '@types/node': 22.16.4 + '@types/shimmer@1.2.0': {} + '@types/stack-utils@2.0.1': {} '@types/statuses@2.0.6': {} @@ -16425,7 +16760,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.6.3) '@typescript-eslint/types': 8.42.0 - debug: 4.4.1 + debug: 4.4.3 typescript: 5.6.3 transitivePeerDependencies: - supports-color @@ -16600,6 +16935,10 @@ snapshots: negotiator: 1.0.0 optional: true + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -18831,6 +19170,8 @@ snapshots: once: 1.4.0 qs: 6.15.0 + forwarded-parse@2.1.2: {} + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -19383,7 +19724,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -19424,6 +19765,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.15.0: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.2.2 + module-details-from-path: 1.0.4 + import-lazy@4.0.0: {} import-local@3.1.0: @@ -19715,7 +20063,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.1 + debug: 4.4.3 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -20426,6 +20774,8 @@ snapshots: loglevel@1.9.2: {} + long@5.3.2: {} + longest-streak@2.0.4: {} longest-streak@3.1.0: {} @@ -20980,7 +21330,7 @@ snapshots: micromark@2.11.4: dependencies: - debug: 4.4.1 + debug: 4.4.3 parse-entities: 2.0.0 transitivePeerDependencies: - supports-color @@ -20988,7 +21338,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 decode-named-character-reference: 1.2.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -21077,6 +21427,8 @@ snapshots: dependencies: obliterator: 1.6.1 + module-details-from-path@1.0.4: {} + moment@2.29.4: {} moment@2.30.1: {} @@ -21704,6 +22056,21 @@ snapshots: proto-list@1.2.4: {} + protobufjs@7.5.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.16.4 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -21988,6 +22355,14 @@ snapshots: require-from-string@2.0.2: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + resolve: 1.22.8 + transitivePeerDependencies: + - supports-color + requires-port@1.0.0: {} resend@4.6.0(react-dom@18.3.1(react@19.2.3))(react@19.2.3): @@ -22281,6 +22656,8 @@ snapshots: sherif-windows-arm64: 1.0.1 sherif-windows-x64: 1.0.1 + shimmer@1.2.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0