From f6b1b2b12e254c6ed35d2f32ae5940eb07ce3dcd Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 15 Aug 2025 19:01:38 +0100 Subject: [PATCH 1/3] updating eventbridge code --- .../__tests__/index.test.ts | 9 - .../amazon-eventbridge/functions.ts | 93 ----- .../amazon-eventbridge/functionsv2.ts | 103 ------ .../amazon-eventbridge/generated-types.ts | 14 +- .../destinations/amazon-eventbridge/index.ts | 41 +-- .../send/__tests__/index.test.ts | 67 ++-- .../amazon-eventbridge/send/constants.ts | 31 ++ .../send/custom-http-handler-types.ts | 17 + .../send/custom-http-handler.ts | 34 ++ .../amazon-eventbridge/send/functions.ts | 146 ++++++++ .../send/generated-types.ts | 4 +- .../amazon-eventbridge/send/index.ts | 33 +- .../amazon-eventbridge/send/types.ts | 12 + .../sendV2/__tests__/index.test.ts | 328 ------------------ .../sendV2/generated-types.ts | 38 -- .../amazon-eventbridge/sendV2/index.ts | 92 ----- 16 files changed, 297 insertions(+), 765 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/functions.ts delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/functionsv2.ts create mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/constants.ts create mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts create mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts create mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts create mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/__tests__/index.test.ts delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/generated-types.ts delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/index.ts diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/__tests__/index.test.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/__tests__/index.test.ts index 494a3c4bae9..5a1d4a63e70 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/__tests__/index.test.ts @@ -30,15 +30,6 @@ test('authentication should contain valid AWS region choices', () => { ]) }) -test('authentication should contain valid partnerEventSourceName choices', () => { - const partnerEventSourceField = testDestination.authentication?.fields?.partnerEventSourceName - expect(partnerEventSourceField).toBeDefined() - expect(partnerEventSourceField?.choices).toEqual([ - { label: 'segment.com', value: 'aws.partner/segment.com' }, - { label: 'segment.com.test', value: 'aws.partner/segment.com.test' } - ]) -}) - test('createPartnerEventSource should default to false', () => { const createPartnerEventSourceField = testDestination.authentication?.fields?.createPartnerEventSource expect(createPartnerEventSourceField).toBeDefined() diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/functions.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/functions.ts deleted file mode 100644 index 52596e792ec..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/functions.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Payload } from './send/generated-types' -import { Settings } from './generated-types' -import { IntegrationError } from '@segment/actions-core' -import { - EventBridgeClient, - PutPartnerEventsCommand, - CreatePartnerEventSourceCommand, - ListPartnerEventSourcesCommand -} from '@aws-sdk/client-eventbridge' - -export async function send(payloads: Payload[], settings: Settings): Promise { - await process_data(payloads, settings) -} - -async function process_data(events: Payload[], settings: Settings) { - const client = new EventBridgeClient({ region: settings.awsRegion }) - const awsAccountId = settings.awsAccountId - - // Ensure the Partner Event Source exists before sending events - await ensurePartnerSourceExists( - client, - awsAccountId, - events[0].sourceId, - settings.createPartnerEventSource || false, - settings.partnerEventSourceName - ) - - const eb_payload = { - Entries: events.map((event) => ({ - EventBusName: event.sourceId, - Source: `${settings.partnerEventSourceName}/${event.sourceId}`, - DetailType: String(event.detailType), - Detail: JSON.stringify(event.data), - Resources: event.resources ? [event.resources] : [] - })) - } - - const command = new PutPartnerEventsCommand(eb_payload) - const response = await client.send(command) - // Check for errors in the response - if (response?.FailedEntryCount && response.FailedEntryCount > 0) { - const errors = response.Entries?.filter((entry) => entry.ErrorCode || entry.ErrorMessage) - const errorMessage = errors?.map((err) => `Error: ${err.ErrorCode}, Message: ${err.ErrorMessage}`).join('; ') - throw new IntegrationError( - `EventBridge failed with ${response.FailedEntryCount} errors: ${errorMessage}`, - 'EVENTBRIDGE_ERROR', - 400 - ) - } - - return response -} - -async function ensurePartnerSourceExists( - client: EventBridgeClient, - awsAccountId: string | undefined, - sourceId: unknown, - createPartnerEventSource: boolean, - partnerEventSourceName: string | undefined -) { - const namePrefix = `${partnerEventSourceName}/${sourceId}` - - const listCommand = new ListPartnerEventSourcesCommand({ NamePrefix: namePrefix }) - const listResponse = await client.send(listCommand) - - if (listResponse?.PartnerEventSources && listResponse.PartnerEventSources.length > 0) { - return true // Source exists - } - - // If we reach here, the source does not exist - if (createPartnerEventSource) { - await create_partner_source(client, awsAccountId, namePrefix) - } else { - throw new IntegrationError( - `Partner Event Source ${namePrefix} does not exist.`, - 'PARTNER_EVENT_SOURCE_NOT_FOUND', - 400 - ) - } -} - -async function create_partner_source( - client: EventBridgeClient, - aws_account_id: string | undefined, - partnerEventSourceName: string -) { - const command = new CreatePartnerEventSourceCommand({ - Account: aws_account_id, - Name: partnerEventSourceName - }) - const response = await client.send(command) - return response -} diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/functionsv2.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/functionsv2.ts deleted file mode 100644 index 723040cd692..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/functionsv2.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Payload } from './send/generated-types' -import { Settings } from './generated-types' -import { IntegrationError, MultiStatusResponse } from '@segment/actions-core' -import { - EventBridgeClient, - PutPartnerEventsCommand, - CreatePartnerEventSourceCommand, - ListPartnerEventSourcesCommand, - PutPartnerEventsCommandOutput -} from '@aws-sdk/client-eventbridge' - -export async function send(payloads: Payload[], settings: Settings): Promise { - return await process_data(payloads, settings) -} - -async function process_data(events: Payload[], settings: Settings): Promise { - const client = new EventBridgeClient({ region: settings.awsRegion }) - const awsAccountId = settings.awsAccountId - - // Ensure the Partner Event Source exists before sending events - await ensurePartnerSourceExists( - client, - awsAccountId, - events[0].sourceId, - settings.createPartnerEventSource || false, - settings.partnerEventSourceName - ) - - const ebPayload = { - Entries: events.map((event) => ({ - EventBusName: event.sourceId, - Source: `${settings.partnerEventSourceName}/${event.sourceId}`, - DetailType: String(event.detailType), - Detail: JSON.stringify(event.data), - Resources: event.resources ? [event.resources] : [] - })) - } - - const command = new PutPartnerEventsCommand(ebPayload) - const response: PutPartnerEventsCommandOutput = await client.send(command) - - const entries = response.Entries ?? [] - // Initialize MultiStatusResponse - const multiStatusResponse = new MultiStatusResponse() - events.forEach((event, index) => { - const entry = entries[index] ?? {} - if (entry.ErrorCode || entry.ErrorMessage) { - multiStatusResponse.setErrorResponseAtIndex(index, { - status: 400, - errormessage: entry.ErrorMessage ?? 'Unknown Error', - sent: JSON.stringify(event), - body: JSON.stringify(entry) - }) - } else { - multiStatusResponse.setSuccessResponseAtIndex(index, { - status: 200, - body: 'Event sent successfully', - sent: JSON.stringify(event) - }) - } - }) - return multiStatusResponse -} - -async function ensurePartnerSourceExists( - client: EventBridgeClient, - awsAccountId: string | undefined, - sourceId: unknown, - createPartnerEventSource: boolean, - partnerEventSourceName: string | undefined -) { - const namePrefix = `${partnerEventSourceName}/${sourceId}` - - const listCommand = new ListPartnerEventSourcesCommand({ NamePrefix: namePrefix }) - const listResponse = await client.send(listCommand) - - if (listResponse?.PartnerEventSources && listResponse.PartnerEventSources.length > 0) { - return true - } - - if (createPartnerEventSource) { - await create_partner_source(client, awsAccountId, namePrefix) - } else { - throw new IntegrationError( - `Partner Event Source ${namePrefix} does not exist.`, - 'PARTNER_EVENT_SOURCE_NOT_FOUND', - 400 - ) - } -} - -async function create_partner_source( - client: EventBridgeClient, - aws_account_id: string | undefined, - partnerEventSourceName: string -) { - const command = new CreatePartnerEventSourceCommand({ - Account: aws_account_id, - Name: partnerEventSourceName - }) - const response = await client.send(command) - return response -} diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts index 5afc2d8d2e5..f8410e3c5ec 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts @@ -5,19 +5,9 @@ export interface Settings { * The AWS Account ID that the event bus belongs to. * This is used to generate the ARN for the event bus. */ - awsAccountId: string + accountId: string /** * The AWS region that the event bus belongs to. */ - awsRegion: string - /** - * The name of the partner event source to use for the event bus. - */ - partnerEventSourceName: string - /** - * If enabled, Segment will check whether Partner Source identified by Segment source ID - * exists in EventBridge. - * If Partner Source does not exist, Segment will create a new Partner Source. - */ - createPartnerEventSource?: boolean + region: string } diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/index.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/index.ts index 2358e2312e7..299a15b254c 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/index.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/index.ts @@ -1,32 +1,26 @@ import type { DestinationDefinition } from '@segment/actions-core' import type { Settings } from './generated-types' import { DEFAULT_REQUEST_TIMEOUT } from '@segment/actions-core' - import send from './send' -import sendV2 from './sendV2' - const destination: DestinationDefinition = { name: 'Amazon Eventbridge', slug: 'actions-amazon-eventbridge', mode: 'cloud', - description: - 'Amazon EventBridge is a serverless event bus service that makes it easy to connect your applications with data from a variety of sources.', + description: 'Amazon EventBridge is a serverless event bus service that makes it easy to connect your applications with data from a variety of sources.', authentication: { scheme: 'custom', fields: { - awsAccountId: { + accountId: { type: 'string', label: 'AWS Account ID', - description: `The AWS Account ID that the event bus belongs to. - This is used to generate the ARN for the event bus.`, + description: `The AWS Account ID that the event bus belongs to. This is used to generate the ARN for the event bus.`, required: true }, - awsRegion: { + region: { type: 'string', label: 'AWS Region', description: 'The AWS region that the event bus belongs to.', - disabledInputMethods: ['enrichment', 'function', 'variable'], required: true, choices: [ { label: 'us-east-1', value: 'us-east-1' }, @@ -45,30 +39,6 @@ const destination: DestinationDefinition = { { label: 'ca-central-1', value: 'ca-central-1' }, { label: 'eu-central-1', value: 'eu-central-1' } ] - }, - partnerEventSourceName: { - type: 'string', - label: 'Partner Event Source Name', - description: 'The name of the partner event source to use for the event bus.', - required: true, - default: 'segment.com', - choices: [ - { label: 'segment.com', value: 'aws.partner/segment.com' }, - { label: 'segment.com.test', value: 'aws.partner/segment.com.test' } - ] - }, - createPartnerEventSource: { - type: 'boolean', - label: 'Create Partner Event Source', - description: `If enabled, Segment will check whether Partner Source identified by Segment source ID - exists in EventBridge. - If Partner Source does not exist, Segment will create a new Partner Source.`, - default: false - } - }, - testAuthentication: async (_request, { settings }) => { - if (!settings.awsAccountId || !settings.awsRegion) { - throw new Error('AWS Account ID and Region are required.') } } }, @@ -78,8 +48,7 @@ const destination: DestinationDefinition = { } }, actions: { - send, - sendV2 + send } } diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/__tests__/index.test.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/__tests__/index.test.ts index b3ce0e0f6f6..4b763d7a853 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/__tests__/index.test.ts @@ -1,11 +1,11 @@ -import { send } from '../../functions' // Adjust path as needed - +import { send } from '../functions' // Adjust path as needed import { PutPartnerEventsCommand, ListPartnerEventSourcesCommand, CreatePartnerEventSourceCommand } from '@aws-sdk/client-eventbridge' -import { Payload } from '../../send/generated-types' +import { SEGMENT_PARTNER_NAME } from '../constants' // Adjust path as needed +import { Payload } from '../generated-types' import { Settings } from '../../generated-types' // Mock AWS SDK @@ -28,8 +28,7 @@ describe('AWS EventBridge Integration', () => { const settings: Settings = { awsRegion: 'us-west-2', awsAccountId: '123456789012', - createPartnerEventSource: true, - partnerEventSourceName: 'your-partner-event-source-name' + createPartnerEventSource: true } afterEach(() => { @@ -54,7 +53,7 @@ describe('AWS EventBridge Integration', () => { // Mocking the listPartnerEventSources response to simulate source existence check mockSend.mockResolvedValueOnce({ - PartnerEventSources: [{ Name: `${settings.partnerEventSourceName}/test-source` }] + PartnerEventSources: [{ Name: `${SEGMENT_PARTNER_NAME}/test-source` }] }) // Mocking a successful PutPartnerEventsCommand response @@ -82,7 +81,7 @@ describe('AWS EventBridge Integration', () => { ] mockSend.mockResolvedValueOnce({ - PartnerEventSources: [{ Name: `${settings.partnerEventSourceName}/test-source` }] + PartnerEventSources: [{ Name: `${SEGMENT_PARTNER_NAME}/test-source` }] }) mockSend.mockResolvedValueOnce({ @@ -90,11 +89,12 @@ describe('AWS EventBridge Integration', () => { Entries: [{ ErrorCode: 'EventBridgeError', ErrorMessage: 'Invalid event' }] }) - await expect(send(payloads, settings)).rejects.toThrow('Invalid event') - - expect(mockSend).toHaveBeenCalledWith(expect.any(ListPartnerEventSourcesCommand)) - expect(mockSend).toHaveBeenCalledWith(expect.any(PutPartnerEventsCommand)) - expect(mockSend).toHaveBeenCalledTimes(2) + const result = await send(payloads, settings) + const responses = result.getAllResponses() + // Check if the first response has an error + if ('data' in responses[0]) { + expect(responses[0].data.status).toBe(400) + } }) test('ensurePartnerSourceExists should create source if it does not exist', async () => { @@ -110,7 +110,6 @@ describe('AWS EventBridge Integration', () => { const updatedSettings: Settings = { ...settings, - partnerEventSourceName: 'aws.partner/segment.com.test', createPartnerEventSource: true } @@ -158,7 +157,6 @@ describe('AWS EventBridge Integration', () => { const updatedSettings: Settings = { ...settings, - partnerEventSourceName: 'your-partner-event-source-name', createPartnerEventSource: false } @@ -221,8 +219,7 @@ describe('AWS EventBridge Integration', () => { await expect( send(payloads, { ...settings, - createPartnerEventSource: false, - partnerEventSourceName: 'aws.partner/segment.com.test' + createPartnerEventSource: false }) ).rejects.toThrow('Partner Event Source aws.partner/segment.com.test/test-source does not exist.') }) @@ -268,9 +265,7 @@ describe('AWS EventBridge Integration', () => { const settings = { awsRegion: 'us-west-2', - awsAccountId: '123456789012', - partnerEventSourceName: 'test-source' - // Other settings here + awsAccountId: '123456789012' } // Mock a failed response @@ -284,9 +279,15 @@ describe('AWS EventBridge Integration', () => { }) // Call the function and assert that it throws an error - await expect(send(payloads, settings)).rejects.toThrow( - 'EventBridge failed with 1 errors: Error: Error, Message: Failed' - ) + // await expect(send(payloads, settings)).rejects.toThrow( + // 'EventBridge failed with 1 errors: Error: Error, Message: Failed' + // ) + const result = await send(payloads, settings) + + const response = result.getAllResponses()[0].data + + expect(response.status).toBe(400) + expect(response.errormessage).toMatch(/Failed/) }) test('process_data should send event to EventBridge', async () => { @@ -300,14 +301,22 @@ describe('AWS EventBridge Integration', () => { } ] - // Proper mock setup for List and Put commands - mockSend - .mockResolvedValueOnce({ PartnerEventSources: [] }) // Simulate "List" finding no source - .mockResolvedValueOnce({}) // Simulate successful event send + const settings = { + awsRegion: 'us-west-2', + awsAccountId: '123456789012', + createPartnerEventSource: true + // Other settings here + } - await send(payloads, settings) + mockSend + .mockResolvedValueOnce({ PartnerEventSources: [{ Name: 'aws.partner/segment.com.test/test-source' }] }) // ListPartnerEventSourcesCommand + .mockResolvedValueOnce({ + FailedEntryCount: 0, + Entries: [{}] + }) - expect(mockSend).toHaveBeenCalledWith(expect.any(Object)) - expect(mockSend).toHaveBeenCalledTimes(3) // One for List, one for Put + const result = await send(payloads, settings) + const response = result.getAllResponses()[0].data + expect(response.status).toBe(200) }) }) diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/constants.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/constants.ts new file mode 100644 index 00000000000..9ce73c19081 --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/constants.ts @@ -0,0 +1,31 @@ + +export const SEGMENT_PARTNER_NAME = 'aws.partner/segment.com' + +export const EBNotRetryableErrors = { + LimitExceededException: 'NON_RETRYABLE', + OperationDisabledException: 'NON_RETRYABLE', + AccessDeniedException: 'NON_RETRYABLE', + NotAuthorized: 'NON_RETRYABLE', + IncompleteSignature: 'NON_RETRYABLE', + InvalidAction: 'NON_RETRYABLE', + InvalidClientTokenId: 'NON_RETRYABLE', + OptInRequired: 'NON_RETRYABLE', + RequestExpired: 'NON_RETRYABLE', + ValidationError: 'NON_RETRYABLE' +} as const + +export const EBRetryableErrors = { + ConcurrentModificationException: 'RETRYABLE', + InternalException: 'RETRYABLE', + InternalFailure: 'RETRYABLE', + ThrottlingException: 'RETRYABLE', + ServiceUnavailable: 'RETRYABLE' +} as const + +export const EBNotErrors = { + ResourceAlreadyExistsException: 'NOT_AN_ERROR' +} as const + +export type RetryableErrorType = keyof typeof EBRetryableErrors +export type NonRetryableErrorType = keyof typeof EBNotRetryableErrors +export type NotAnErrorType = keyof typeof EBNotErrors \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts new file mode 100644 index 00000000000..c506787132f --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts @@ -0,0 +1,17 @@ +export interface HttpRequest { + method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | undefined + hostname: string + path: string + headers: Record + body?: unknown +} + +export interface HttpResponse { + statusCode: number + headers: Record + body?: unknown +} + +export interface HttpHandler { + handle(request: HttpRequest, options?: unknown): Promise<{ response: HttpResponse }> +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts new file mode 100644 index 00000000000..61cb43d0f75 --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts @@ -0,0 +1,34 @@ +import { RequestClient } from '@segment/actions-core' +import { HttpHandler, HttpResponse, HttpRequest } from './custom-http-handler-types' + +export const createCustomHandler = (requestClient: RequestClient): HttpHandler => ({ + handle: async (request: HttpRequest, _options?: unknown): Promise<{ response: HttpResponse }> => { + const url = `https://${request.hostname}${request.path}` + + let body: BodyInit | null = null + if (request.body !== undefined) { + body = typeof request.body === "string" ? request.body : JSON.stringify(request.body) + } + + const result = await requestClient(url, { + method: request.method, + headers: request.headers, + body + }) + + let headers: Record = {} + if (typeof result.headers.toJSON === 'function') { + headers = result.headers.toJSON() as unknown as Record + } else if (result.headers && typeof result.headers === 'object') { + headers = result.headers as unknown as Record + } + + return { + response: { + statusCode: result.status, + headers, + body: result.body + } + } + } +}) \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts new file mode 100644 index 00000000000..4c1cf152537 --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts @@ -0,0 +1,146 @@ +import { Payload } from './generated-types' +import { Settings } from '../generated-types' +import { PayloadValidationError, MultiStatusResponse, RequestClient, RetryableError, IntegrationError } from '@segment/actions-core' +import { SEGMENT_PARTNER_NAME, EBNotErrors, EBRetryableErrors, EBNotRetryableErrors } from './constants' +import { EventBridgeClient, PutPartnerEventsCommand, CreatePartnerEventSourceCommand, ListPartnerEventSourcesCommand, PutPartnerEventsCommandOutput } from '@aws-sdk/client-eventbridge' +import { PutPartnerEventsCommandJSON } from './types' +import { createCustomHandler } from './custom-http-handler' + +export async function send(request: RequestClient, payloads: Payload[], settings: Settings): Promise { + + const sourceId = payloads[0].sourceId + + if (!sourceId) { + throw new PayloadValidationError('Source ID is required. It should be present at $.context.protocols.sourceId or $.projectId in the payload.') + } + + const { accountId, region } = settings + + const client = new EventBridgeClient({ region, requestHandler: createCustomHandler(request) }) + + await ensurePartnerSource(client, accountId, sourceId) + + const commandJSON = createCommandJSON(payloads, sourceId) + + const command = new PutPartnerEventsCommand(commandJSON) + + let response: PutPartnerEventsCommandOutput + + try { + response = await client.send(command) + } catch (error) { + throwError(error, `client.send`) + } + + return buildMultiStatusResponse(response, payloads) +} + +function buildMultiStatusResponse(response: PutPartnerEventsCommandOutput, payloads: Payload[]): MultiStatusResponse { + const entries: PutPartnerEventsResultEntryList = response.Entries + const multiStatusResponse = new MultiStatusResponse() + payloads.forEach((event, index) => { + const entry = entries[index] + if (entry.ErrorCode || entry.ErrorMessage) { + multiStatusResponse.setErrorResponseAtIndex(index, { + status: 400, + errormessage: entry.ErrorMessage ?? 'Unknown Error', + sent: JSON.stringify(event), + body: JSON.stringify(entry) + }) + } else { + multiStatusResponse.setSuccessResponseAtIndex(index, { + status: 200, + body: 'Event sent successfully', + sent: JSON.stringify(event) + }) + } + }) + + return multiStatusResponse +} + +function createCommandJSON(payloads: Payload[], sourceId: string): PutPartnerEventsCommandJSON { + return { + Entries: payloads.map((event) => ({ + EventBusName: sourceId, + Source: `${SEGMENT_PARTNER_NAME}/${sourceId}`, + DetailType: event.detailType, + Detail: JSON.stringify(event.data), + Resources: Array.isArray(event.resources) ? event.resources : typeof event.resources === 'string' ? [event.resources] : [], + Time: event.time ? new Date(event.time): new Date() + })) + } +} + +async function ensurePartnerSource(client: EventBridgeClient, awsAccountId: string, sourceId: string) { + const sourceExists = await findSource(client, sourceId) + if (!sourceExists) { + await createSource(client, awsAccountId, sourceId) + } +} + +async function findSource( client: EventBridgeClient, sourceId: string): Promise { + try { + const command = new ListPartnerEventSourcesCommand({ NamePrefix: getFullSourceName(sourceId)}) + const response = await client.send(command) + return (response.PartnerEventSources?.length ?? 0) > 0 + } + catch (error) { + throwError(error, 'findSource') + } +} + +async function createSource(client: EventBridgeClient, accountId: string, sourceId: string) { + const fullSourceName = getFullSourceName(sourceId) + const command = new CreatePartnerEventSourceCommand({ Account: accountId, Name: fullSourceName}) + try { + await client.send(command) + } + catch (error) { + if(isAnError(error)) { + throwError(error, `createSource(${fullSourceName})`) + } + } +} + +function getFullSourceName(sourceId: string): string { + return `${SEGMENT_PARTNER_NAME}/${sourceId}` +} + +function isAnError(error: unknown): boolean { + if (typeof error === 'object' && error !== null && 'name' in error) { + const err = error as { name: string } + return !(err.name in EBNotErrors) + } + return true +} + +function isRetryableError(error: unknown): boolean { + if (typeof error === 'object' && error !== null && 'name' in error) { + const err = error as { name: string } + return !(err.name in EBRetryableErrors) + } + return true +} + +function isNotRetryableError(error: unknown): boolean { + if (typeof error === 'object' && error !== null && 'name' in error) { + const err = error as { name: string } + return !(err.name in EBNotRetryableErrors) + } + return true +} + +function throwError(error: unknown, context: string): never { + if(isRetryableError(error)) { + const err = error as { name: string; message?: string } + const message = err.message ?? 'No error message returned' + throw new RetryableError(`Retryable error ${err.name} in ${context}. Message: ${message}`); + } else if(isNotRetryableError(error)) { + const err = error as { name: string; message?: string } + const message = err.message ?? 'No error message returned' + throw new IntegrationError(`Non-retryable error ${err.name} in ${context}. Message: ${message}`, err.name, 400) + } else { + throw new IntegrationError(`Unknown error in ${context}: ${JSON.stringify(error)}`, 'UnknownError', 500) + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts index d2fc4a61681..0acbd44c711 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts @@ -21,7 +21,7 @@ export interface Payload { * which the event primarily concerns. Any number, * including zero, may be present. */ - resources?: string + resources?: string[] /** * The timestamp the event occurred. */ @@ -35,4 +35,4 @@ export interface Payload { * Actual batch sizes may be lower. */ batch_size?: number -} +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts index 3736d6b72c9..593ea3f8930 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts @@ -1,12 +1,11 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { send } from '../functions' -import { send as sendV2 } from '../functionsv2' +import { send } from './functions' const action: ActionDefinition = { title: 'Send', - description: 'Send an event to Amazon EventBridge.', + description: 'Send event data to Amazon EventBridge.', fields: { data: { label: 'Detail', @@ -40,23 +39,16 @@ const action: ActionDefinition = { }, resources: { label: 'Resources', - description: `AWS resources, identified by Amazon Resource Name (ARN), - which the event primarily concerns. Any number, - including zero, may be present.`, + description: `AWS resources, identified by Amazon Resource Name (ARN), which the event primarily concerns. Any number, including zero, may be present.`, type: 'string', - default: { - '@if': { - exists: { '@path': '$.userId' }, - then: { '@path': '$.userId' }, - else: { '@path': '$.anonymousId' } - } - }, + multiple: true, required: false }, time: { label: 'Time', - description: 'The timestamp the event occurred.', + description: 'The timestamp the event occurred. Accepts a date in ISO 8601 format or a date in YYYY-MM-DD format.', type: 'string', + format: 'date-time', default: { '@path': '$.timestamp' }, required: false }, @@ -80,16 +72,11 @@ const action: ActionDefinition = { maximum: 20 } }, - perform: (_, data) => { - const { payload, settings } = data - return send([payload], settings) + perform: (request, { payload, settings}) => { + return send(request, [payload], settings) }, - performBatch: (_, data) => { - const { payload, settings, features } = data - if (features?.['amazon-eventbridge-v2']) { - return sendV2(payload, settings) - } - return send(payload, settings) + performBatch: (request, { payload, settings }) => { + return send(request, payload, settings) } } diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts new file mode 100644 index 00000000000..f5677244a73 --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts @@ -0,0 +1,12 @@ +export interface PutPartnerEventsCommandJSON{ + Entries: Array +} + +export interface EntryItem { + Time: Date + Source: string + Resources: string[] + DetailType: string + Detail: string + EventBusName: string +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/__tests__/index.test.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/__tests__/index.test.ts deleted file mode 100644 index 28299478967..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/__tests__/index.test.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { send } from '../../functionsv2' // Adjust path as needed -import { - PutPartnerEventsCommand, - ListPartnerEventSourcesCommand, - CreatePartnerEventSourceCommand -} from '@aws-sdk/client-eventbridge' -import { Payload } from '../../send/generated-types' -import { Settings } from '../../generated-types' - -// Mock AWS SDK -jest.mock('@aws-sdk/client-eventbridge', () => { - const mockSend = jest.fn() - return { - EventBridgeClient: jest.fn(() => ({ - send: mockSend - })), - PutPartnerEventsCommand: jest.fn(), - ListPartnerEventSourcesCommand: jest.fn(), - CreatePartnerEventSourceCommand: jest.fn(), - mockSend - } -}) - -const { mockSend } = jest.requireMock('@aws-sdk/client-eventbridge') - -describe('AWS EventBridge Integration', () => { - const settings: Settings = { - awsRegion: 'us-west-2', - awsAccountId: '123456789012', - createPartnerEventSource: true, - partnerEventSourceName: 'your-partner-event-source-name' - } - - afterEach(() => { - jest.clearAllMocks() - jest.restoreAllMocks() - }) - - beforeEach(() => { - jest.clearAllMocks() - }) - - test('send should call process_data with correct parameters', async () => { - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - // Mocking the listPartnerEventSources response to simulate source existence check - mockSend.mockResolvedValueOnce({ - PartnerEventSources: [{ Name: `${settings.partnerEventSourceName}/test-source` }] - }) - - // Mocking a successful PutPartnerEventsCommand response - mockSend.mockResolvedValueOnce({ - FailedEntryCount: 0, - Entries: [{ EventId: '12345' }] - }) - - await send(payloads, settings) - - expect(mockSend).toHaveBeenCalledWith(expect.any(ListPartnerEventSourcesCommand)) - expect(mockSend).toHaveBeenCalledWith(expect.any(PutPartnerEventsCommand)) - expect(mockSend).toHaveBeenCalledTimes(2) - }) - - test('send should throw an error if FailedEntryCount > 0', async () => { - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - mockSend.mockResolvedValueOnce({ - PartnerEventSources: [{ Name: `${settings.partnerEventSourceName}/test-source` }] - }) - - mockSend.mockResolvedValueOnce({ - FailedEntryCount: 1, - Entries: [{ ErrorCode: 'EventBridgeError', ErrorMessage: 'Invalid event' }] - }) - - const result = await send(payloads, settings) - const responses = result.getAllResponses() - // Check if the first response has an error - if ('data' in responses[0]) { - expect(responses[0].data.status).toBe(400) - } - }) - - test('ensurePartnerSourceExists should create source if it does not exist', async () => { - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - const updatedSettings: Settings = { - ...settings, - partnerEventSourceName: 'aws.partner/segment.com.test', - createPartnerEventSource: true - } - - // Simulate "List" finding no source and success on creation - mockSend - .mockResolvedValueOnce({ PartnerEventSources: [] }) // No source exists - .mockResolvedValueOnce({}) // CreatePartnerEventSourceCommand success - .mockResolvedValueOnce({ FailedEntryCount: 0 }) // Event sent successfully - - await send(payloads, updatedSettings) - - // Ensure the List command was called - expect(mockSend).toHaveBeenCalledWith(expect.any(ListPartnerEventSourcesCommand)) - - // Ensure the Create command was called - expect(mockSend).toHaveBeenCalledWith(expect.any(CreatePartnerEventSourceCommand)) - - // Ensure the event is sent - expect(mockSend).toHaveBeenCalledWith(expect.any(PutPartnerEventsCommand)) - - // Ensure all three commands are called - expect(mockSend).toHaveBeenCalledTimes(3) - }) - - test('ensurePartnerSourceExists should not create source if it already exists', async () => { - // Mock ListPartnerEventSourcesCommand to simulate existing source - mockSend - .mockResolvedValueOnce({ - PartnerEventSources: [{ Name: 'aws.partner/segment.com.test/test-source' }] - }) // ListPartnerEventSourcesCommand - .mockResolvedValueOnce({ - FailedEntryCount: 0, // Mock success for PutPartnerEventsCommand - Entries: [{ EventId: '12345' }] - }) - - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - const updatedSettings: Settings = { - ...settings, - partnerEventSourceName: 'your-partner-event-source-name', - createPartnerEventSource: false - } - - await send(payloads, updatedSettings) - - // Ensure it only calls ListPartnerEventSources and PutPartnerEventsCommand - expect(mockSend).toHaveBeenCalledTimes(2) - - // Ensure it does NOT call CreatePartnerEventSourceCommand - expect(mockSend).not.toHaveBeenCalledWith( - expect.objectContaining({ input: expect.objectContaining({ Account: '123456789012' }) }) - ) - }) - - test('ensurePartnerSourceExists should create source if missing', async () => { - // Simulate "ListPartnerEventSources" - No source found - mockSend.mockResolvedValueOnce({ PartnerEventSources: [] }) - - // Simulate "CreatePartnerEventSource" - Source created - mockSend.mockResolvedValueOnce({}) - - // Simulate "PutEventsCommand" - Event sent successfully - mockSend.mockResolvedValueOnce({ - FailedEntryCount: 0, - Entries: [{ EventId: '12345' }] - }) - - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - await send(payloads, settings) - - // Ensure all three calls are made: List, Create, and Put - expect(mockSend).toHaveBeenCalledTimes(3) - - // Check if CreatePartnerEventSourceCommand is called - expect(mockSend).toHaveBeenCalledWith(expect.any(CreatePartnerEventSourceCommand)) - }) - - test('should throw error if partner source is missing and createPartnerEventSource is false', async () => { - mockSend.mockResolvedValueOnce({ PartnerEventSources: [] }) - - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true // Add the enable_batching property - } - ] - - await expect( - send(payloads, { - ...settings, - createPartnerEventSource: false, - partnerEventSourceName: 'aws.partner/segment.com.test' - }) - ).rejects.toThrow('Partner Event Source aws.partner/segment.com.test/test-source does not exist.') - }) - - test('create_partner_source should send correct request', async () => { - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - // Ensure correct mock responses for all EventBridge calls - mockSend - .mockResolvedValueOnce({ PartnerEventSources: [] }) // ListPartnerEventSourcesCommand - .mockResolvedValueOnce({}) // CreatePartnerEventSourceCommand - .mockResolvedValueOnce({ FailedEntryCount: 0, Entries: [{ EventId: '12345' }] }) // PutEventsCommand - - await send(payloads, settings) - - expect(mockSend).toHaveBeenCalledTimes(3) - - // Ensure CreatePartnerEventSourceCommand is called - expect(mockSend).toHaveBeenCalledWith(expect.any(CreatePartnerEventSourceCommand)) - - // Ensure PutEventsCommand is called with expected arguments - expect(mockSend).toHaveBeenCalledWith(expect.any(PutPartnerEventsCommand)) - }) - - test('process_data should throw error if event send fails', async () => { - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - const settings = { - awsRegion: 'us-west-2', - awsAccountId: '123456789012', - partnerEventSourceName: 'test-source' - // Other settings here - } - - // Mock a failed response - mockSend - .mockResolvedValueOnce({ - PartnerEventSources: [{ Name: 'aws.partner/segment.com.test/test-source' }] - }) // ListPartnerEventSourcesCommand - .mockResolvedValueOnce({ - FailedEntryCount: 1, - Entries: [{ ErrorCode: 'Error', ErrorMessage: 'Failed' }] - }) - - // Call the function and assert that it throws an error - // await expect(send(payloads, settings)).rejects.toThrow( - // 'EventBridge failed with 1 errors: Error: Error, Message: Failed' - // ) - const result = await send(payloads, settings) - - const response = result.getAllResponses()[0].data - - expect(response.status).toBe(400) - expect(response.errormessage).toMatch(/Failed/) - }) - - test('process_data should send event to EventBridge', async () => { - const payloads: Payload[] = [ - { - sourceId: 'test-source', - detailType: 'UserSignup', - data: { user: '123', event: 'signed_up' }, - resources: 'test-resource', - enable_batching: true - } - ] - - const settings = { - awsRegion: 'us-west-2', - awsAccountId: '123456789012', - partnerEventSourceName: 'test-source', - createPartnerEventSource: true - // Other settings here - } - - mockSend - .mockResolvedValueOnce({ PartnerEventSources: [{ Name: 'aws.partner/segment.com.test/test-source' }] }) // ListPartnerEventSourcesCommand - .mockResolvedValueOnce({ - FailedEntryCount: 0, - Entries: [{}] - }) - - const result = await send(payloads, settings) - const response = result.getAllResponses()[0].data - expect(response.status).toBe(200) - }) -}) diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/generated-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/generated-types.ts deleted file mode 100644 index d2fc4a61681..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/generated-types.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Generated file. DO NOT MODIFY IT BY HAND. - -export interface Payload { - /** - * The event data to send to Amazon EventBridge. - */ - data: { - [k: string]: unknown - } - /** - * Detail Type of the event. Used to determine what fields to expect in the event Detail. - * Value cannot be longer than 128 characters. - */ - detailType: string - /** - * The source ID for the event. HIDDEN FIELD - */ - sourceId: string - /** - * AWS resources, identified by Amazon Resource Name (ARN), - * which the event primarily concerns. Any number, - * including zero, may be present. - */ - resources?: string - /** - * The timestamp the event occurred. - */ - time?: string - /** - * Enable Batching - */ - enable_batching: boolean - /** - * Maximum number of events to include in each batch. - * Actual batch sizes may be lower. - */ - batch_size?: number -} diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/index.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/index.ts deleted file mode 100644 index 3e954d2c408..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/sendV2/index.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { ActionDefinition } from '@segment/actions-core' -import type { Settings } from '../generated-types' -import type { Payload } from './generated-types' -import { send } from '../functionsv2' - -const action: ActionDefinition = { - title: 'SendV2', - description: 'Send an event to Amazon EventBridge.', - fields: { - data: { - label: 'Detail', - description: 'The event data to send to Amazon EventBridge.', - type: 'object', - default: { '@path': '$.' }, - required: true - }, - detailType: { - label: 'Detail Type', - description: `Detail Type of the event. Used to determine what fields to expect in the event Detail. - Value cannot be longer than 128 characters.`, - type: 'string', - maximum: 128, - default: { '@path': '$.type' }, - required: true - }, - sourceId: { - label: 'Source ID', - description: 'The source ID for the event. HIDDEN FIELD', - type: 'string', - unsafe_hidden: true, - default: { - '@if': { - exists: { '@path': '$.context.protocols.sourceId' }, - then: { '@path': '$.context.protocols.sourceId' }, - else: { '@path': '$.projectId' } - } - }, - required: true - }, - resources: { - label: 'Resources', - description: `AWS resources, identified by Amazon Resource Name (ARN), - which the event primarily concerns. Any number, - including zero, may be present.`, - type: 'string', - default: { - '@if': { - exists: { '@path': '$.userId' }, - then: { '@path': '$.userId' }, - else: { '@path': '$.anonymousId' } - } - }, - required: false - }, - time: { - label: 'Time', - description: 'The timestamp the event occurred.', - type: 'string', - default: { '@path': '$.timestamp' }, - required: false - }, - enable_batching: { - type: 'boolean', - label: 'Enable Batching', - description: 'Enable Batching', - unsafe_hidden: false, - required: true, - default: true - }, - batch_size: { - label: 'Batch Size', - description: `Maximum number of events to include in each batch. - Actual batch sizes may be lower.`, - type: 'number', - unsafe_hidden: true, - required: false, - default: 20, - minimum: 1, - maximum: 20 - } - }, - perform: (_, data) => { - const { payload, settings } = data - return send([payload], settings) - }, - performBatch: (_, data) => { - const { payload, settings } = data - return send(payload, settings) - } -} - -export default action From 0943e3f1db510ce34ed03997a300c6836af289a8 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 15 Aug 2025 19:26:07 +0100 Subject: [PATCH 2/3] yarn types --- .../destinations/amazon-eventbridge/generated-types.ts | 3 +-- .../amazon-eventbridge/send/custom-http-handler.ts | 3 ++- .../amazon-eventbridge/send/generated-types.ts | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts index f8410e3c5ec..d596d543650 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/generated-types.ts @@ -2,8 +2,7 @@ export interface Settings { /** - * The AWS Account ID that the event bus belongs to. - * This is used to generate the ARN for the event bus. + * The AWS Account ID that the event bus belongs to. This is used to generate the ARN for the event bus. */ accountId: string /** diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts index 61cb43d0f75..b1ad60d0d24 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts @@ -13,7 +13,8 @@ export const createCustomHandler = (requestClient: RequestClient): HttpHandler = const result = await requestClient(url, { method: request.method, headers: request.headers, - body + body, + throwHttpErrors: false }) let headers: Record = {} diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts index 0acbd44c711..acfa82aec18 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts @@ -17,13 +17,11 @@ export interface Payload { */ sourceId: string /** - * AWS resources, identified by Amazon Resource Name (ARN), - * which the event primarily concerns. Any number, - * including zero, may be present. + * AWS resources, identified by Amazon Resource Name (ARN), which the event primarily concerns. Any number, including zero, may be present. */ resources?: string[] /** - * The timestamp the event occurred. + * The timestamp the event occurred. Accepts a date in ISO 8601 format or a date in YYYY-MM-DD format. */ time?: string /** @@ -35,4 +33,4 @@ export interface Payload { * Actual batch sizes may be lower. */ batch_size?: number -} \ No newline at end of file +} From de7fca00484306ffeca1e5764a1773bb73c6a346 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Mon, 18 Aug 2025 18:15:30 +0100 Subject: [PATCH 3/3] refactor after talking to Varada --- .../send/custom-http-handler-types.ts | 17 ---- .../send/custom-http-handler.ts | 35 -------- .../amazon-eventbridge/send/functions.ts | 89 ++++++------------- .../send/generated-types.ts | 22 +++++ .../amazon-eventbridge/send/index.ts | 17 +++- .../amazon-eventbridge/send/types.ts | 25 +++++- 6 files changed, 87 insertions(+), 118 deletions(-) delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts delete mode 100644 packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts deleted file mode 100644 index c506787132f..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler-types.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface HttpRequest { - method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'head' | 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | undefined - hostname: string - path: string - headers: Record - body?: unknown -} - -export interface HttpResponse { - statusCode: number - headers: Record - body?: unknown -} - -export interface HttpHandler { - handle(request: HttpRequest, options?: unknown): Promise<{ response: HttpResponse }> -} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts deleted file mode 100644 index b1ad60d0d24..00000000000 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/custom-http-handler.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { RequestClient } from '@segment/actions-core' -import { HttpHandler, HttpResponse, HttpRequest } from './custom-http-handler-types' - -export const createCustomHandler = (requestClient: RequestClient): HttpHandler => ({ - handle: async (request: HttpRequest, _options?: unknown): Promise<{ response: HttpResponse }> => { - const url = `https://${request.hostname}${request.path}` - - let body: BodyInit | null = null - if (request.body !== undefined) { - body = typeof request.body === "string" ? request.body : JSON.stringify(request.body) - } - - const result = await requestClient(url, { - method: request.method, - headers: request.headers, - body, - throwHttpErrors: false - }) - - let headers: Record = {} - if (typeof result.headers.toJSON === 'function') { - headers = result.headers.toJSON() as unknown as Record - } else if (result.headers && typeof result.headers === 'object') { - headers = result.headers as unknown as Record - } - - return { - response: { - statusCode: result.status, - headers, - body: result.body - } - } - } -}) \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts index 4c1cf152537..db81809049e 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts @@ -1,24 +1,17 @@ import { Payload } from './generated-types' import { Settings } from '../generated-types' -import { PayloadValidationError, MultiStatusResponse, RequestClient, RetryableError, IntegrationError } from '@segment/actions-core' -import { SEGMENT_PARTNER_NAME, EBNotErrors, EBRetryableErrors, EBNotRetryableErrors } from './constants' -import { EventBridgeClient, PutPartnerEventsCommand, CreatePartnerEventSourceCommand, ListPartnerEventSourcesCommand, PutPartnerEventsCommandOutput } from '@aws-sdk/client-eventbridge' -import { PutPartnerEventsCommandJSON } from './types' -import { createCustomHandler } from './custom-http-handler' - -export async function send(request: RequestClient, payloads: Payload[], settings: Settings): Promise { - - const sourceId = payloads[0].sourceId - - if (!sourceId) { - throw new PayloadValidationError('Source ID is required. It should be present at $.context.protocols.sourceId or $.projectId in the payload.') - } +import { PayloadValidationError, MultiStatusResponse, RetryableError, IntegrationError } from '@segment/actions-core' +import { SEGMENT_PARTNER_NAME, EBRetryableErrors, EBNotRetryableErrors } from './constants' +import { PutPartnerEventsResultEntry, EventBridgeClient, PutPartnerEventsCommand, PutPartnerEventsCommandOutput } from '@aws-sdk/client-eventbridge' +import { PutPartnerEventsCommandJSON, HookOutputs } from './types' + +export async function send(payloads: Payload[], settings: Settings, hookOutputs?: HookOutputs): Promise { - const { accountId, region } = settings + const sourceId = getSourceId(payloads, hookOutputs) - const client = new EventBridgeClient({ region, requestHandler: createCustomHandler(request) }) + const { region } = settings - await ensurePartnerSource(client, accountId, sourceId) + const client = new EventBridgeClient({ region }) const commandJSON = createCommandJSON(payloads, sourceId) @@ -35,8 +28,27 @@ export async function send(request: RequestClient, payloads: Payload[], settings return buildMultiStatusResponse(response, payloads) } +function getSourceId(payloads: Payload[], hookOutputs?: HookOutputs): string { + const payloadSourceId = payloads[0].sourceId + const hookSourceId = hookOutputs?.onMappingSave?.sourceId ?? hookOutputs?.retlOnMappingSave?.sourceId + + if (!payloadSourceId) { + throw new PayloadValidationError('Source ID is required. Source ID not found in payload. It should be present at $.context.protocols.sourceId or $.projectId in the payload.') + } + + if (!hookSourceId) { + throw new PayloadValidationError('Source ID is required. Source ID not found in hook outputs.') + } + + if(hookSourceId !== payloadSourceId) { + throw new PayloadValidationError('Mismatch between payload and hook source ID values.') + } + + return payloadSourceId +} + function buildMultiStatusResponse(response: PutPartnerEventsCommandOutput, payloads: Payload[]): MultiStatusResponse { - const entries: PutPartnerEventsResultEntryList = response.Entries + const entries: PutPartnerEventsResultEntry[] = response.Entries ?? [] const multiStatusResponse = new MultiStatusResponse() payloads.forEach((event, index) => { const entry = entries[index] @@ -72,49 +84,6 @@ function createCommandJSON(payloads: Payload[], sourceId: string): PutPartnerEve } } -async function ensurePartnerSource(client: EventBridgeClient, awsAccountId: string, sourceId: string) { - const sourceExists = await findSource(client, sourceId) - if (!sourceExists) { - await createSource(client, awsAccountId, sourceId) - } -} - -async function findSource( client: EventBridgeClient, sourceId: string): Promise { - try { - const command = new ListPartnerEventSourcesCommand({ NamePrefix: getFullSourceName(sourceId)}) - const response = await client.send(command) - return (response.PartnerEventSources?.length ?? 0) > 0 - } - catch (error) { - throwError(error, 'findSource') - } -} - -async function createSource(client: EventBridgeClient, accountId: string, sourceId: string) { - const fullSourceName = getFullSourceName(sourceId) - const command = new CreatePartnerEventSourceCommand({ Account: accountId, Name: fullSourceName}) - try { - await client.send(command) - } - catch (error) { - if(isAnError(error)) { - throwError(error, `createSource(${fullSourceName})`) - } - } -} - -function getFullSourceName(sourceId: string): string { - return `${SEGMENT_PARTNER_NAME}/${sourceId}` -} - -function isAnError(error: unknown): boolean { - if (typeof error === 'object' && error !== null && 'name' in error) { - const err = error as { name: string } - return !(err.name in EBNotErrors) - } - return true -} - function isRetryableError(error: unknown): boolean { if (typeof error === 'object' && error !== null && 'name' in error) { const err = error as { name: string } diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts index acfa82aec18..99b770b457a 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/generated-types.ts @@ -34,3 +34,25 @@ export interface Payload { */ batch_size?: number } +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface RetlOnMappingSaveInputs {} +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface RetlOnMappingSaveOutputs { + /** + * The identifier for the source. + */ + sourceId: string +} +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface OnMappingSaveInputs {} +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface OnMappingSaveOutputs { + /** + * The identifier for the source. + */ + sourceId: string +} diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts index 593ea3f8930..aa0c42443d0 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/index.ts @@ -2,6 +2,7 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { send } from './functions' +import { ensureSourceIdHook } from './hooks' const action: ActionDefinition = { title: 'Send', @@ -72,11 +73,19 @@ const action: ActionDefinition = { maximum: 20 } }, - perform: (request, { payload, settings}) => { - return send(request, [payload], settings) + hooks: { + retlOnMappingSave: { + ...ensureSourceIdHook + }, + onMappingSave: { + ...ensureSourceIdHook + } + }, + perform: (_, { payload, settings, hookOutputs}) => { + return send([payload], settings, hookOutputs) }, - performBatch: (request, { payload, settings }) => { - return send(request, payload, settings) + performBatch: (_, { payload, settings, hookOutputs }) => { + return send(payload, settings, hookOutputs) } } diff --git a/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts index f5677244a73..1b6c918055f 100644 --- a/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts @@ -1,4 +1,6 @@ -export interface PutPartnerEventsCommandJSON{ +import { OnMappingSaveOutputs, RetlOnMappingSaveOutputs} from './generated-types' + +export interface PutPartnerEventsCommandJSON { Entries: Array } @@ -9,4 +11,23 @@ export interface EntryItem { DetailType: string Detail: string EventBusName: string -} \ No newline at end of file +} + +export interface HookError { + error: { + message: string + code: string + } +} + +export interface HookSuccess { + successMessage: string, + savedData: { + sourceId: string + } +} + +export interface HookOutputs { + onMappingSave?: OnMappingSaveOutputs + retlOnMappingSave?: RetlOnMappingSaveOutputs +}