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..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,22 +2,11 @@ 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. */ - 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/functions.ts b/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts new file mode 100644 index 00000000000..db81809049e --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/functions.ts @@ -0,0 +1,115 @@ +import { Payload } from './generated-types' +import { Settings } from '../generated-types' +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 sourceId = getSourceId(payloads, hookOutputs) + + const { region } = settings + + const client = new EventBridgeClient({ region }) + + 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 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: PutPartnerEventsResultEntry[] = 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() + })) + } +} + +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..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 @@ -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 + 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 /** @@ -36,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 3736d6b72c9..aa0c42443d0 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,12 @@ 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' +import { ensureSourceIdHook } from './hooks' 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 +40,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 +73,19 @@ const action: ActionDefinition = { maximum: 20 } }, - perform: (_, data) => { - const { payload, settings } = data - return send([payload], settings) + hooks: { + retlOnMappingSave: { + ...ensureSourceIdHook + }, + onMappingSave: { + ...ensureSourceIdHook + } }, - performBatch: (_, data) => { - const { payload, settings, features } = data - if (features?.['amazon-eventbridge-v2']) { - return sendV2(payload, settings) - } - return send(payload, settings) + perform: (_, { payload, settings, hookOutputs}) => { + return send([payload], settings, hookOutputs) + }, + 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 new file mode 100644 index 00000000000..1b6c918055f --- /dev/null +++ b/packages/destination-actions/src/destinations/amazon-eventbridge/send/types.ts @@ -0,0 +1,33 @@ +import { OnMappingSaveOutputs, RetlOnMappingSaveOutputs} from './generated-types' + +export interface PutPartnerEventsCommandJSON { + Entries: Array +} + +export interface EntryItem { + Time: Date + Source: string + Resources: string[] + DetailType: string + Detail: string + EventBusName: string +} + +export interface HookError { + error: { + message: string + code: string + } +} + +export interface HookSuccess { + successMessage: string, + savedData: { + sourceId: string + } +} + +export interface HookOutputs { + onMappingSave?: OnMappingSaveOutputs + retlOnMappingSave?: RetlOnMappingSaveOutputs +} 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