diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b9785..dbd0717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added comprehensive tests and example usage - Integrates with new SMASHSEND backend endpoint `/v1/contacts/by-email/{email}` +- **Events API Improvements**: New clearer method names for better developer experience + - šŸŽÆ **New methods**: `events.send()` and `events.sendBatch()` - much clearer than "track"! + - šŸ“š **Better documentation**: Updated examples and documentation to use the new method names + - šŸ“ **Added Events API section**: Comprehensive guide in README with practical examples + - šŸ”§ **New example file**: `examples/events-usage.ts` showing real-world usage patterns + ## [1.16.0] - 2025-01-21 ### Changed diff --git a/README.md b/README.md index 2cee9c4..4fa85f8 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,68 @@ const property = await smashsend.contacts.createProperty({ - `STRING` for email addresses, URLs, phone numbers, and any text - `NUMBER` for both integers and decimals +## Events API + +Send events to trigger automations, track user behavior, or sync data with your SMASHSEND workspace. + +**Send a single event:** + +```typescript +const response = await smashsend.events.send({ + event: 'user.signup', + identify: { + email: 'user@example.com', + traits: { + firstName: 'John', + lastName: 'Doe', + plan: 'premium' + } + }, + properties: { + source: 'website', + campaign: 'summer-sale' + } +}); + +console.log(`Event sent with ID: ${response.messageId}`); +``` + +**Send multiple events in a batch:** + +```typescript +const events = [ + { + event: 'page.view', + identify: { email: 'user1@example.com' }, + properties: { page: '/home' } + }, + { + event: 'button.click', + identify: { email: 'user2@example.com' }, + properties: { button: 'signup' } + } +]; + +const result = await smashsend.events.sendBatch(events); +console.log(`Accepted: ${result.accepted}, Failed: ${result.failed}`); + +// Handle failed events +if (result.errors?.length > 0) { + result.errors.forEach(error => { + console.log(`Event ${error.index} failed:`, error.errors); + }); +} +``` + +**Event structure:** + +- `event`: Event name (e.g., 'user.signup', 'purchase.completed') +- `identify.email`: User email (required) +- `identify.traits`: User attributes to sync with contact record (optional) +- `properties`: Event-specific data (optional) +- `timestamp`: Event timestamp (optional, defaults to current time) +- `messageId`: Custom ID for deduplication (optional, auto-generated if not provided) + ## TypeScript Support - Built **in TypeScript** diff --git a/examples/events-usage.ts b/examples/events-usage.ts new file mode 100644 index 0000000..09b4c3f --- /dev/null +++ b/examples/events-usage.ts @@ -0,0 +1,151 @@ +/** + * SMASHSEND Events API Examples + * + * This example demonstrates how to use the new events.send() and events.sendBatch() methods + * to track user events and trigger automations in your SMASHSEND workspace. + */ + +import { SmashSend } from '@smashsend/node'; + +const smashsend = new SmashSend(process.env.SMASHSEND_API_KEY!); + +async function sendSingleEvent() { + try { + const response = await smashsend.events.send({ + event: 'user.signup', + identify: { + email: 'user@example.com', + traits: { + firstName: 'John', + lastName: 'Doe', + plan: 'premium', + signupSource: 'website' + } + }, + properties: { + campaign: 'summer-sale', + referrer: 'google', + utm_source: 'social' + } + }); + + console.log(`āœ… Event sent successfully with ID: ${response.messageId}`); + } catch (error) { + console.error('āŒ Failed to send event:', error); + } +} + +async function sendBatchEvents() { + const events = [ + { + event: 'page.view', + identify: { + email: 'user1@example.com', + traits: { firstName: 'Alice' } + }, + properties: { + page: '/pricing', + duration: 45000 + } + }, + { + event: 'button.click', + identify: { + email: 'user2@example.com', + traits: { firstName: 'Bob' } + }, + properties: { + button: 'signup', + location: 'header' + } + }, + { + event: 'purchase.completed', + identify: { + email: 'user3@example.com', + traits: { firstName: 'Carol' } + }, + properties: { + orderId: 'order_123', + amount: 99.99, + currency: 'USD' + } + } + ]; + + try { + const result = await smashsend.events.sendBatch(events); + + console.log(`šŸ“Š Batch results:`); + console.log(` āœ… Accepted: ${result.accepted}`); + console.log(` āŒ Failed: ${result.failed}`); + console.log(` šŸ”„ Duplicated: ${result.duplicated}`); + + // Handle failed events + if (result.errors?.length > 0) { + console.log('\nāš ļø Failed events:'); + result.errors.forEach(error => { + console.log(` Event ${error.index}:`, error.errors.map(e => e.message).join(', ')); + }); + } + + // Show successful events + if (result.events?.length > 0) { + console.log('\nāœ… Successful events:'); + result.events.forEach(event => { + console.log(` Index ${event.index}: ${event.messageId} (${event.status})`); + }); + } + } catch (error) { + console.error('āŒ Failed to send batch events:', error); + } +} + +async function sendEventWithCustomDeduplication() { + try { + const response = await smashsend.events.send({ + event: 'order.created', + identify: { + email: 'customer@example.com', + traits: { + customerId: 'cust_12345' + } + }, + properties: { + orderId: 'order_67890', + amount: 149.99 + }, + // Custom messageId prevents duplicate processing if this exact event is sent again + messageId: 'order_67890_created' + }); + + console.log(`āœ… Order event sent with ID: ${response.messageId}`); + } catch (error) { + console.error('āŒ Failed to send order event:', error); + } +} + +// Run examples +async function main() { + console.log('šŸš€ SMASHSEND Events API Examples\n'); + + console.log('1. Sending single event...'); + await sendSingleEvent(); + + console.log('\n2. Sending batch events...'); + await sendBatchEvents(); + + console.log('\n3. Sending event with custom deduplication...'); + await sendEventWithCustomDeduplication(); +} + +// Only run if this file is executed directly +if (require.main === module) { + main().catch(console.error); +} + +export { + sendSingleEvent, + sendBatchEvents, + sendEventWithCustomDeduplication +}; diff --git a/src/__tests__/events.test.ts b/src/__tests__/events.test.ts new file mode 100644 index 0000000..a8e6e97 --- /dev/null +++ b/src/__tests__/events.test.ts @@ -0,0 +1,123 @@ +import { SmashSend } from '../index'; +import { HttpClient } from '../utils/http-client'; + +// Mock the HttpClient +jest.mock('../utils/http-client'); +const MockedHttpClient = HttpClient as jest.MockedClass; + +describe('Events API', () => { + let smashsend: SmashSend; + let mockHttpClient: jest.Mocked; + + beforeEach(() => { + // Clear all mocks + jest.clearAllMocks(); + + // Create a new SmashSend instance + smashsend = new SmashSend('test-api-key'); + + // Get the mocked http client instance + mockHttpClient = MockedHttpClient.mock.instances[0] as jest.Mocked; + }); + + describe('send() method', () => { + it('should send a single event successfully', async () => { + const mockResponse = { + success: true, + messageId: 'msg_123', + }; + + mockHttpClient.post.mockResolvedValue(mockResponse); + + const eventPayload = { + event: 'user.signup', + identify: { + email: 'user@example.com', + traits: { + firstName: 'John', + lastName: 'Doe', + }, + }, + properties: { + plan: 'premium', + source: 'website', + }, + }; + + const result = await smashsend.events.send(eventPayload); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/events', + eventPayload, + { headers: {} } + ); + expect(result).toEqual(mockResponse); + }); + + it('should send event with options', async () => { + const mockResponse = { + success: true, + messageId: 'msg_456', + }; + + mockHttpClient.post.mockResolvedValue(mockResponse); + + const eventPayload = { + event: 'purchase.completed', + identify: { email: 'user@example.com' }, + properties: { orderId: 'order_123' }, + }; + + const options = { + headers: { 'X-Custom': 'header' }, + timeout: 5000, + }; + + await smashsend.events.send(eventPayload, options); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/events', + eventPayload, + { headers: { 'X-Custom': 'header' }, timeout: 5000 } + ); + }); + }); + + describe('sendBatch() method', () => { + it('should send multiple events successfully', async () => { + const mockResponse = { + accepted: 2, + failed: 0, + duplicated: 0, + events: [ + { index: 0, messageId: 'msg_123', status: 'accepted' as const }, + { index: 1, messageId: 'msg_456', status: 'accepted' as const }, + ], + }; + + mockHttpClient.post.mockResolvedValue(mockResponse); + + const events = [ + { + event: 'page.view', + identify: { email: 'user1@example.com' }, + properties: { page: '/home' }, + }, + { + event: 'button.click', + identify: { email: 'user2@example.com' }, + properties: { button: 'signup' }, + }, + ]; + + const result = await smashsend.events.sendBatch(events); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/events/batch', + { events }, + { headers: {} } + ); + expect(result).toEqual(mockResponse); + }); + }); +}); diff --git a/src/api/events.ts b/src/api/events.ts index 881439f..af5fdf6 100644 --- a/src/api/events.ts +++ b/src/api/events.ts @@ -15,15 +15,15 @@ export class Events { } /** - * Track a single event - * @param event The event payload to track + * Send a single event + * @param event The event payload to send * @param options Optional tracking options - * @returns The tracking response + * @returns The event response * * @example * ```typescript - * // Basic event tracking (SMASHSEND generates messageId automatically) - * const response = await smashsend.events.track({ + * // Basic event sending (SMASHSEND generates messageId automatically) + * const response = await smashsend.events.send({ * event: 'user.signup', * identify: { * email: 'user@example.com', @@ -35,10 +35,10 @@ export class Events { * } * }); * - * console.log(`Event tracked with ID: ${response.messageId}`); + * console.log(`Event sent with ID: ${response.messageId}`); * * // With custom messageId for deduplication - * const customResponse = await smashsend.events.track({ + * const customResponse = await smashsend.events.send({ * event: 'purchase.completed', * identify: { email: 'user@example.com' }, * properties: { orderId: 'order_123' }, @@ -46,7 +46,7 @@ export class Events { * }); * ``` */ - async track( + async send( event: EventPayload, options: EventTrackingOptions = {} ): Promise { @@ -69,10 +69,10 @@ export class Events { } /** - * Track multiple events in a single batch operation - * @param events Array of event payloads to track + * Send multiple events in a single batch operation + * @param events Array of event payloads to send * @param options Optional tracking options - * @returns The batch tracking response + * @returns The batch event response * * @example * ```typescript @@ -89,7 +89,7 @@ export class Events { * } * ]; * - * const result = await smashsend.events.trackBatch(events); + * const result = await smashsend.events.sendBatch(events); * console.log(`Accepted: ${result.accepted}, Failed: ${result.failed}`); * * // Handle failed events @@ -100,7 +100,7 @@ export class Events { * } * ``` */ - async trackBatch( + async sendBatch( events: EventPayload[], options: EventTrackingOptions = {} ): Promise {