Skip to content

Commit ecb4b8d

Browse files
authored
Update cb behavior (#692)
1 parent 804dd06 commit ecb4b8d

File tree

11 files changed

+262
-222
lines changed

11 files changed

+262
-222
lines changed

.changeset/cyan-rocks-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@segment/analytics-core': patch
3+
---
4+
5+
Move code out of core and into analytics-node. Tweak emitter error contract.

packages/core/src/analytics/dispatch-emit.ts

Lines changed: 0 additions & 34 deletions
This file was deleted.
Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,23 @@
11
import { CoreContext } from '../context'
22

3-
export type EmittedUnknownError<Ctx extends CoreContext> = {
4-
code: 'unknown'
5-
message: string
6-
ctx?: Ctx
7-
err?: any
8-
}
9-
10-
export type EmittedDeliveryFailureError<Ctx extends CoreContext> = {
11-
code: 'delivery_failure'
12-
message: string
13-
ctx: Ctx
14-
data?: any
15-
}
16-
173
/**
18-
* Discriminated of all errors with a discriminant key of "code"
4+
* This is the base contract for all emitted errors. This interface may be extended.
195
*/
20-
export type CoreEmittedError<Ctx extends CoreContext> =
21-
| EmittedUnknownError<Ctx>
22-
| EmittedDeliveryFailureError<Ctx>
6+
export interface CoreEmittedError<Ctx extends CoreContext> {
7+
/**
8+
* e.g. 'delivery_failure'
9+
*/
10+
code: string
11+
/**
12+
* Why the error occurred. This can be an actual error object or a just a message.
13+
*/
14+
reason?: unknown
15+
ctx?: Ctx
16+
}
2317

2418
export type CoreEmitterContract<
2519
Ctx extends CoreContext,
26-
AdditionalErrors = CoreEmittedError<Ctx>
20+
Err extends CoreEmittedError<Ctx> = CoreEmittedError<Ctx>
2721
> = {
2822
alias: [ctx: Ctx]
2923
track: [ctx: Ctx]
@@ -33,5 +27,5 @@ export type CoreEmitterContract<
3327
group: [ctx: Ctx]
3428
register: [pluginNames: string[]]
3529
deregister: [pluginNames: string[]]
36-
error: [CoreEmittedError<Ctx> | AdditionalErrors]
30+
error: [error: Err]
3731
}

packages/core/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export * from './context'
1010
export * from './queue/event-queue'
1111
export * from './analytics'
1212
export * from './analytics/dispatch'
13-
export * from './analytics/dispatch-emit'
1413
export * from './validation/helpers'
1514
export * from './validation/assertions'
1615
export * from './utils/bind-all'

packages/node/README.md

Lines changed: 39 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ app.post('/cart', (req, res) => {
3636
event: 'Add to cart',
3737
properties: { productId: '123456' }
3838
})
39-
res.sendStatus(200)
39+
res.sendStatus(201)
4040
});
4141
```
4242
## Regional configuration
@@ -48,8 +48,9 @@ Dublin — events.eu1.segmentapis.com
4848
An example of setting the host to the EU endpoint using the Node library would be:
4949

5050
```ts
51-
const analytics = new Analytics('YOUR_WRITE_KEY', {
52-
host: "https://events.eu1.segmentapis.com"
51+
const analytics = new Analytics({
52+
...
53+
host: "https://events.eu1.segmentapis.com"
5354
});
5455
```
5556

@@ -66,7 +67,6 @@ const analytics = new Analytics({
6667
flushInterval: 10000,
6768
// ... and more!
6869
})
69-
7070
```
7171

7272
## Batching
@@ -84,16 +84,22 @@ There is a maximum of 500KB per batch request and 32KB per call.
8484

8585
If you don’t want to batch messages, you can turn batching off by setting the `maxEventsInBatch` setting to 1, like so:
8686
```ts
87-
const analytics = new Analytics({ '<MY_WRITE_KEY>', { maxEventsInBatch: 1 });
87+
const analytics = new Analytics({
88+
...
89+
maxEventsInBatch: 1
90+
});
8891
```
8992
Batching means that your message might not get sent right away. But every method call takes an optional callback, which you can use to know when a particular message is flushed from the queue, like so:
9093

9194
```ts
9295
analytics.track({
93-
userId: '019mr8mf4r',
94-
event: 'Ultimate Played'
95-
callback: (ctx) => console.log(ctx)
96-
})
96+
userId: '019mr8mf4r',
97+
event: 'Ultimate Played',
98+
},
99+
(err, ctx) => {
100+
...
101+
}
102+
)
97103
```
98104
## Error Handling
99105
Subscribe and log all event delivery errors.
@@ -143,7 +149,6 @@ const onExit = async () => {
143149
}
144150

145151
['SIGINT', 'SIGTERM'].forEach((code) => process.on(code, onExit))
146-
147152
```
148153

149154
#### Collecting unflushed events
@@ -175,16 +180,16 @@ Different parts of your application may require different types of batching, or
175180
```ts
176181
import { Analytics } from '@segment/analytics-node'
177182

178-
const marketingAnalytics = new Analytics('MARKETING_WRITE_KEY');
179-
const appAnalytics = new Analytics('APP_WRITE_KEY');
183+
const marketingAnalytics = new Analytics({ writeKey: 'MARKETING_WRITE_KEY' });
184+
const appAnalytics = new Analytics({ writeKey: 'APP_WRITE_KEY' });
180185
```
181186

182187
## Troubleshooting
183188
1. Double check that you’ve followed all the steps in the Quick Start.
184189

185190
2. Make sure that you’re calling a Segment API method once the library is successfully installed: identify, track, etc.
186191

187-
3. Log events and errors the event emitter:
192+
3. Log events.
188193
```js
189194
['initialize', 'call_after_close',
190195
'screen', 'identify', 'group',
@@ -194,6 +199,20 @@ const appAnalytics = new Analytics('APP_WRITE_KEY');
194199
```
195200
196201
202+
## Development: Disabling Analytics for Tests
203+
- If you want to intercept / disable analytics for integration tests, you can use something like [nock](https://github.com/nock/nock)
204+
205+
```ts
206+
// Note: nock will _not_ work if polyfill fetch with something like undici, as nock uses the http module. Undici has its own interception method.
207+
import nock from 'nock'
208+
209+
nock('https://api.segment.io')
210+
.post('/v1/batch')
211+
.reply(201)
212+
.persist()
213+
```
214+
215+
197216
## Differences from legacy analytics-node / Migration Guide
198217
199218
@@ -206,15 +225,13 @@ import Analytics from 'analytics-node'
206225
import { Analytics } from '@segment/analytics-next'
207226
```
208227
209-
- Instantiation requires an object
228+
- Instantiation now requires an _object_ as the first argument.
210229
```ts
211230
// old
231+
var analytics = new Analytics('YOUR_WRITE_KEY'); // not supported
212232

213-
var analytics = new Analytics('YOUR_WRITE_KEY');
214-
215-
// new
216-
const analytics = new Analytics({ writeKey: 'YOUR_WRITE_KEY' });
217-
233+
// new!
234+
const analytics = new Analytics({ writeKey: '<MY_WRITE_KEY>' })
218235
```
219236
- Graceful shutdown (See Graceful Shutdown section)
220237
```ts
@@ -232,49 +249,10 @@ Other Differences:
232249
- The `enable` configuration option has been removed-- see "Disabling Analytics" section
233250
- the `errorHandler` configuration option has been remove -- see "Error Handling" section
234251
- `flushAt` configuration option -> `maxEventsInBatch`.
235-
- `callback` option is moved to configuration
252+
- `callback` call signature is different
236253
```ts
237254
// old
238-
analytics.track({
239-
userId: '019mr8mf4r',
240-
event: 'Ultimate Played'
241-
}), function(err, batch){
242-
if (err) {
243-
console.error(err)
244-
}
245-
});
246-
255+
(err, batch) => void
247256
// new
248-
analytics.track({
249-
userId: '019mr8mf4r',
250-
event: 'Ultimate Played',
251-
callback: (ctx) => {
252-
if (ctx.failedDelivery()) {
253-
console.error(ctx)
254-
}
255-
}
256-
})
257-
258-
```
259-
260-
261-
## Development / Disabling Analytics
262-
- If you want to disable analytics for unit tests, you can use something like [nock](https://github.com/nock/nock) or [jest mocks](https://jestjs.io/docs/manual-mocks).
263-
264-
You should prefer mocking. However, if you need to intercept the request, you can do:
265-
266-
```ts
267-
// Note: nock will _not_ work if polyfill fetch with something like undici, as nock uses the http module. Undici has its own interception method.
268-
import nock from 'nock'
269-
270-
const mockApiHost = 'https://foo.bar'
271-
const mockPath = '/foo'
272-
273-
nock(mockApiHost) // using regex matching in nock changes the perf profile quite a bit
274-
.post(mockPath, (body) => true)
275-
.reply(201)
276-
.persist()
277-
278-
const analytics = new Analytics({ host: mockApiHost, path: mockPath })
279-
257+
(err, ctx) => void
280258
```
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const fetcher = jest.fn()
2+
jest.mock('../lib/fetch', () => ({ fetch: fetcher }))
3+
4+
import { createError, createSuccess } from './test-helpers/factories'
5+
import { createTestAnalytics } from './test-helpers/create-test-analytics'
6+
import { Context } from '../app/analytics-node'
7+
8+
describe('Callback behavior', () => {
9+
beforeEach(() => {
10+
fetcher.mockReturnValue(createSuccess())
11+
})
12+
13+
it('should handle success', async () => {
14+
const ajs = createTestAnalytics({ maxEventsInBatch: 1 })
15+
const ctx = await new Promise<Context>((resolve, reject) =>
16+
ajs.track(
17+
{
18+
anonymousId: 'bar',
19+
event: 'event name',
20+
},
21+
(err, ctx) => {
22+
if (err) reject('test fail')
23+
resolve(ctx!)
24+
}
25+
)
26+
)
27+
expect(ctx.event.event).toBe('event name')
28+
expect(ctx.event.anonymousId).toBe('bar')
29+
})
30+
31+
it('should handle errors', async () => {
32+
fetcher.mockReturnValue(createError())
33+
const ajs = createTestAnalytics({ maxEventsInBatch: 1 })
34+
const [err, ctx] = await new Promise<[any, Context]>((resolve) =>
35+
ajs.track(
36+
{
37+
anonymousId: 'bar',
38+
event: 'event name',
39+
},
40+
(err, ctx) => {
41+
resolve([err!, ctx!])
42+
}
43+
)
44+
)
45+
expect(ctx.event.event).toBe('event name')
46+
expect(ctx.event.anonymousId).toBe('bar')
47+
expect(err).toEqual(new Error('[404] Not Found'))
48+
})
49+
})

packages/node/src/__tests__/graceful-shutdown-integration.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('Ability for users to exit without losing events', () => {
2626
})
2727
const _helpers = {
2828
makeTrackCall: (analytics = ajs, cb?: (...args: any[]) => void) => {
29-
analytics.track({ userId: 'foo', event: 'Thing Updated', callback: cb })
29+
analytics.track({ userId: 'foo', event: 'Thing Updated' }, cb)
3030
},
3131
}
3232

@@ -52,22 +52,23 @@ describe('Ability for users to exit without losing events', () => {
5252

5353
test('all callbacks should be called ', async () => {
5454
const cb = jest.fn()
55-
ajs.track({ userId: 'foo', event: 'bar', callback: cb })
55+
ajs.track({ userId: 'foo', event: 'bar' }, cb)
5656
expect(cb).not.toHaveBeenCalled()
5757
await ajs.closeAndFlush()
5858
expect(cb).toBeCalled()
5959
})
6060

6161
test('all async callbacks should be called', async () => {
62-
const trackCall = new Promise<CoreContext>((resolve) =>
63-
ajs.track({
64-
userId: 'abc',
65-
event: 'def',
66-
callback: (ctx) => {
67-
return sleep(100).then(() => resolve(ctx))
62+
const trackCall = new Promise<CoreContext>((resolve) => {
63+
ajs.track(
64+
{
65+
userId: 'abc',
66+
event: 'def',
6867
},
69-
})
70-
)
68+
(_, ctx) => sleep(200).then(() => resolve(ctx!))
69+
)
70+
})
71+
7172
const res = await Promise.race([ajs.closeAndFlush(), trackCall])
7273
expect(res instanceof CoreContext).toBe(true)
7374
})

packages/node/src/__tests__/integration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('Error handling', () => {
6262
await promise
6363
throw new Error('fail')
6464
} catch (err: any) {
65-
expect(err.message).toMatch(/fail/)
65+
expect(err.reason).toEqual(new Error('[503] Service Unavailable'))
6666
expect(err.code).toMatch(/delivery_failure/)
6767
}
6868
})

0 commit comments

Comments
 (0)