Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
06c5a7a
fix: use correct API endpoints for broadcasts - separate /fetch and /…
ziptied Jan 7, 2026
5e31e86
test: add URL verification tests for broadcasts endpoints and enhance…
ziptied Jan 7, 2026
4417e67
test: increase coverage for experimental, v1, and upsert modules to 100%
ziptied Jan 7, 2026
d33c57d
fix: convert non-string query parameter values to strings in URLSearc…
ziptied Jan 7, 2026
dc48d13
feat(client): Add request timeout and error handling
ziptied Jan 7, 2026
878735e
test(workflows): Add comprehensive test suite for getWorkflows method
ziptied Jan 7, 2026
b0bb910
test(upsert): Simplify upsert subscriber test cases
ziptied Jan 7, 2026
95700bc
test(sequences): Add comprehensive test suite for getSequences method
ziptied Jan 7, 2026
845e18b
test(email-templates): Add comprehensive test suite for email template
ziptied Jan 7, 2026
2c64780
feat(sdk): Improve subscriber upsert and add timeout option
ziptied Jan 7, 2026
c330f96
feat(config): Add client timeout configuration and update docs
ziptied Jan 7, 2026
b7c9fe8
Merge pull request #4 from ziptied/mcp_updates
ziptied Jan 7, 2026
fadbf3a
feat(client): Add optional timeout parameter to HTTP methods
ziptied Jan 9, 2026
d21cc95
test(mock): Enhance mock fetch and workflow tests
ziptied Jan 9, 2026
c060b5a
docs(upsertSubscriber): Update method description and example
ziptied Jan 9, 2026
5f10c58
Merge pull request #5 from ziptied/Z-007
ziptied Jan 9, 2026
c531e0f
test: Add afterEach cleanup for mock fetch tracking
ziptied Jan 9, 2026
e1dc060
fix(client): Improve timeout error message with dynamic timeout value
ziptied Jan 9, 2026
b7d333a
Merge pull request #6 from ziptied/Z-007
ziptied Jan 9, 2026
313c05c
fix(sdk): Correct typo in batch email documentation comment
ziptied Jan 9, 2026
43c7d40
Merge pull request #7 from ziptied/Z-007
ziptied Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ const bento = new Analytics({
secretKey: 'bento-secret-key',
},
siteUuid: 'bento-site-uuid',
// Optional: Configure request timeout (default: 30000ms)
clientOptions: {
timeout: 30000,
},
});

bento.V1.track({
Expand Down Expand Up @@ -179,10 +183,11 @@ bento.V1.removeSubscriber({

#### upsertSubscriber

Updates existing subscriber or creates a new one if they don't exist:
Creates or updates a subscriber. The SDK queues the import job and then attempts to fetch
the subscriber record once the job has been accepted.

```javascript
await analytics.V1.upsertSubscriber({
const subscriber = await analytics.V1.upsertSubscriber({
email: 'user@example.com',
fields: {
firstName: 'John',
Expand All @@ -194,6 +199,9 @@ await analytics.V1.upsertSubscriber({
});
```

> **Note:** Imports are processed asynchronously by Bento and may take 1-5 minutes to
> complete. If the subscriber is not yet available, the method will return `null`.

#### updateFields

Updates custom fields for a subscriber.
Expand Down Expand Up @@ -824,6 +832,32 @@ Note: The `S` and `E` generic types are used for TypeScript support. `S` represe
- The SDK supports TypeScript with generics for custom fields and events.
- Batch operations are available for importing subscribers and events efficiently.
- The SDK doesn't currently support anonymous events (coming soon).
- Requests have a default timeout of 30 seconds, configurable via `clientOptions.timeout`.

## Error Handling

The SDK exports several error types for specific error conditions:

```javascript
import {
NotAuthorizedError, // 401 - Invalid credentials
RateLimitedError, // 429 - Too many requests
AuthorNotAuthorizedError, // Author not permitted to send emails
RequestTimeoutError, // Request exceeded timeout
} from '@bentonow/bento-node-sdk';

try {
await bento.V1.Tags.getTags();
} catch (error) {
if (error instanceof RequestTimeoutError) {
// Handle timeout - maybe retry
} else if (error instanceof RateLimitedError) {
// Handle rate limiting - back off and retry
} else if (error instanceof NotAuthorizedError) {
// Handle auth error - check credentials
}
}
```

## Contributing

Expand Down
25 changes: 22 additions & 3 deletions __tests__/batch/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { expect, test, describe, beforeEach } from 'bun:test';
import { expect, test, describe, beforeEach, afterEach } from 'bun:test';
import { Analytics } from '../../src';
import { mockOptions } from '../helpers/mockClient';
import { setupMockFetch } from '../helpers/mockFetch';
import { setupMockFetch, lastFetchSignal, resetMockFetchTracking } from '../helpers/mockFetch';
import { BentoEvents } from '../../src/sdk/batch/enums';
describe('BentoBatch - importEvents', () => {
let analytics: Analytics;

beforeEach(() => {
analytics = new Analytics(mockOptions);
});
afterEach(() => {
resetMockFetchTracking();
});

test('successfully imports purchase event', async () => {
setupMockFetch({ results: 1 });
Expand Down Expand Up @@ -310,4 +313,20 @@ describe('BentoBatch - importEvents', () => {

expect(result).toBe(3);
});
});

test('uses unlimited timeout for imports', async () => {
setupMockFetch({ results: 1 });

await analytics.V1.Batch.importEvents({
events: [{
type: BentoEvents.TAG,
email: 'signal@example.com',
details: {
tag: 'VIP'
}
}]
});

expect(lastFetchSignal).toBeNull();
});
});
31 changes: 31 additions & 0 deletions __tests__/batch/subscribers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expect, test, describe, beforeEach, afterEach } from 'bun:test';
import { Analytics } from '../../src';
import { mockOptions } from '../helpers/mockClient';
import { setupMockFetch, lastFetchSignal, resetMockFetchTracking } from '../helpers/mockFetch';

describe('BentoBatch - importSubscribers', () => {
let analytics: Analytics;

beforeEach(() => {
analytics = new Analytics(mockOptions);
});
afterEach(() => {
resetMockFetchTracking();
});

test('successfully imports subscribers without timeout signal', async () => {
setupMockFetch({ results: 1 });

const result = await analytics.V1.Batch.importSubscribers({
subscribers: [
{
email: 'test@example.com',
firstName: 'Test',
},
],
});

expect(result).toBe(1);
expect(lastFetchSignal).toBeNull();
});
});
Loading