From 1f029d2f4e5e5f563d4fcb98002eafb4dd5340d0 Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 04:51:42 -0700 Subject: [PATCH 1/5] feat: replace track by send events --- CHANGELOG.md | 14 +++ README.md | 62 ++++++++++++ examples/events-usage.ts | 151 ++++++++++++++++++++++++++++ src/__tests__/events.test.ts | 188 +++++++++++++++++++++++++++++++++++ src/api/events.ts | 56 ++++++++--- 5 files changed, 458 insertions(+), 13 deletions(-) create mode 100644 examples/events-usage.ts create mode 100644 src/__tests__/events.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b9785..02cdf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,20 @@ 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 + +### Deprecated + +- **Events API**: Deprecated confusing method names in favor of clearer alternatives + - āš ļø `events.track()` is deprecated - use `events.send()` instead + - āš ļø `events.trackBatch()` is deprecated - use `events.sendBatch()` instead + - šŸ”„ **Backward compatible**: Old methods still work but show deprecation warnings + - šŸ—‘ļø **Future removal**: Deprecated methods will be removed in v2.0 + ## [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..351629a --- /dev/null +++ b/src/__tests__/events.test.ts @@ -0,0 +1,188 @@ +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); + }); + }); + + describe('deprecated methods', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + it('should show deprecation warning for track() method', async () => { + const mockResponse = { + success: true, + messageId: 'msg_123', + }; + + mockHttpClient.post.mockResolvedValue(mockResponse); + + const eventPayload = { + event: 'user.signup', + identify: { email: 'user@example.com' }, + }; + + await smashsend.events.track(eventPayload); + + expect(consoleSpy).toHaveBeenCalledWith( + 'āš ļø events.track() is deprecated. Use events.send() instead.' + ); + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/events', + eventPayload, + { headers: {} } + ); + }); + + it('should show deprecation warning for trackBatch() method', async () => { + const mockResponse = { + accepted: 1, + failed: 0, + duplicated: 0, + }; + + mockHttpClient.post.mockResolvedValue(mockResponse); + + const events = [ + { + event: 'page.view', + identify: { email: 'user@example.com' }, + }, + ]; + + await smashsend.events.trackBatch(events); + + expect(consoleSpy).toHaveBeenCalledWith( + 'āš ļø events.trackBatch() is deprecated. Use events.sendBatch() instead.' + ); + expect(mockHttpClient.post).toHaveBeenCalledWith( + '/events/batch', + { events }, + { headers: {} } + ); + }); + }); +}); diff --git a/src/api/events.ts b/src/api/events.ts index 881439f..9cdb7e1 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 { @@ -123,4 +123,34 @@ export class Events { requestOptions ); } + + /** + * @deprecated Use `send()` instead. This method will be removed in a future version. + * Track a single event + * @param event The event payload to track + * @param options Optional tracking options + * @returns The tracking response + */ + async track( + event: EventPayload, + options: EventTrackingOptions = {} + ): Promise { + console.warn('āš ļø events.track() is deprecated. Use events.send() instead.'); + return this.send(event, options); + } + + /** + * @deprecated Use `sendBatch()` instead. This method will be removed in a future version. + * Track multiple events in a single batch operation + * @param events Array of event payloads to track + * @param options Optional tracking options + * @returns The batch tracking response + */ + async trackBatch( + events: EventPayload[], + options: EventTrackingOptions = {} + ): Promise { + console.warn('āš ļø events.trackBatch() is deprecated. Use events.sendBatch() instead.'); + return this.sendBatch(events, options); + } } \ No newline at end of file From f457ad094abd411a4ec4be7c683d397cedd02355 Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 04:52:51 -0700 Subject: [PATCH 2/5] chore --- src/api/events.ts | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/api/events.ts b/src/api/events.ts index 9cdb7e1..af5fdf6 100644 --- a/src/api/events.ts +++ b/src/api/events.ts @@ -123,34 +123,4 @@ export class Events { requestOptions ); } - - /** - * @deprecated Use `send()` instead. This method will be removed in a future version. - * Track a single event - * @param event The event payload to track - * @param options Optional tracking options - * @returns The tracking response - */ - async track( - event: EventPayload, - options: EventTrackingOptions = {} - ): Promise { - console.warn('āš ļø events.track() is deprecated. Use events.send() instead.'); - return this.send(event, options); - } - - /** - * @deprecated Use `sendBatch()` instead. This method will be removed in a future version. - * Track multiple events in a single batch operation - * @param events Array of event payloads to track - * @param options Optional tracking options - * @returns The batch tracking response - */ - async trackBatch( - events: EventPayload[], - options: EventTrackingOptions = {} - ): Promise { - console.warn('āš ļø events.trackBatch() is deprecated. Use events.sendBatch() instead.'); - return this.sendBatch(events, options); - } } \ No newline at end of file From 6dbd3141451f1a8826aa665bb5dfb752730e2f48 Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 05:04:51 -0700 Subject: [PATCH 3/5] fix: test --- src/__tests__/events.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/events.test.ts b/src/__tests__/events.test.ts index 351629a..59d2fc9 100644 --- a/src/__tests__/events.test.ts +++ b/src/__tests__/events.test.ts @@ -145,7 +145,7 @@ describe('Events API', () => { identify: { email: 'user@example.com' }, }; - await smashsend.events.track(eventPayload); + await smashsend.events.send(eventPayload); expect(consoleSpy).toHaveBeenCalledWith( 'āš ļø events.track() is deprecated. Use events.send() instead.' @@ -173,7 +173,7 @@ describe('Events API', () => { }, ]; - await smashsend.events.trackBatch(events); + await smashsend.events.sendBatch(events); expect(consoleSpy).toHaveBeenCalledWith( 'āš ļø events.trackBatch() is deprecated. Use events.sendBatch() instead.' From ee1f55743ccc71ceb82441d594d63315b74a2f1b Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 05:06:04 -0700 Subject: [PATCH 4/5] chore: update tests --- src/__tests__/events.test.ts | 65 ------------------------------------ 1 file changed, 65 deletions(-) diff --git a/src/__tests__/events.test.ts b/src/__tests__/events.test.ts index 59d2fc9..a8e6e97 100644 --- a/src/__tests__/events.test.ts +++ b/src/__tests__/events.test.ts @@ -120,69 +120,4 @@ describe('Events API', () => { expect(result).toEqual(mockResponse); }); }); - - describe('deprecated methods', () => { - let consoleSpy: jest.SpyInstance; - - beforeEach(() => { - consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); - }); - - afterEach(() => { - consoleSpy.mockRestore(); - }); - - it('should show deprecation warning for track() method', async () => { - const mockResponse = { - success: true, - messageId: 'msg_123', - }; - - mockHttpClient.post.mockResolvedValue(mockResponse); - - const eventPayload = { - event: 'user.signup', - identify: { email: 'user@example.com' }, - }; - - await smashsend.events.send(eventPayload); - - expect(consoleSpy).toHaveBeenCalledWith( - 'āš ļø events.track() is deprecated. Use events.send() instead.' - ); - expect(mockHttpClient.post).toHaveBeenCalledWith( - '/events', - eventPayload, - { headers: {} } - ); - }); - - it('should show deprecation warning for trackBatch() method', async () => { - const mockResponse = { - accepted: 1, - failed: 0, - duplicated: 0, - }; - - mockHttpClient.post.mockResolvedValue(mockResponse); - - const events = [ - { - event: 'page.view', - identify: { email: 'user@example.com' }, - }, - ]; - - await smashsend.events.sendBatch(events); - - expect(consoleSpy).toHaveBeenCalledWith( - 'āš ļø events.trackBatch() is deprecated. Use events.sendBatch() instead.' - ); - expect(mockHttpClient.post).toHaveBeenCalledWith( - '/events/batch', - { events }, - { headers: {} } - ); - }); - }); }); From 35a0a96eb0613a7121ad05073137e05a372c04db Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 05:06:37 -0700 Subject: [PATCH 5/5] chore: update tests --- CHANGELOG.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cdf13..dbd0717 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,14 +22,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - šŸ“ **Added Events API section**: Comprehensive guide in README with practical examples - šŸ”§ **New example file**: `examples/events-usage.ts` showing real-world usage patterns -### Deprecated - -- **Events API**: Deprecated confusing method names in favor of clearer alternatives - - āš ļø `events.track()` is deprecated - use `events.send()` instead - - āš ļø `events.trackBatch()` is deprecated - use `events.sendBatch()` instead - - šŸ”„ **Backward compatible**: Old methods still work but show deprecation warnings - - šŸ—‘ļø **Future removal**: Deprecated methods will be removed in v2.0 - ## [1.16.0] - 2025-01-21 ### Changed