Skip to content

Commit 95fd2fd

Browse files
sileskydrake-888
andauthored
Retain messageId if exists (allow overriding) (#1043)
Co-authored-by: H.Drake <hd@activeops.io>
1 parent 9b1540f commit 95fd2fd

File tree

9 files changed

+176
-15
lines changed

9 files changed

+176
-15
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@segment/analytics-core': patch
3+
'@segment/analytics-node': minor
4+
---
5+
6+
This ensures backward compatibility with analytics-node by modifying '@segment/analytics-core'. Specifically, the changes prevent the generation of a messageId if it is already set. This adjustment aligns with the behavior outlined in analytics-node's source code [here](https://github.com/segmentio/analytics-node/blob/master/index.js#L195-L201).
7+
8+
While this is a core release, only the node library is affected, as the browser has its own EventFactory atm.

packages/core/src/events/__tests__/index.test.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,27 @@ describe('Event Factory', () => {
350350
innerProp: '👻',
351351
})
352352
})
353+
354+
test('accepts a messageId', () => {
355+
const messageId = 'business-id-123'
356+
const track = factory.track('Order Completed', shoes, {
357+
messageId,
358+
})
359+
360+
expect(track.context).toEqual({})
361+
expect(track.messageId).toEqual(messageId)
362+
})
363+
364+
it('should ignore undefined options', () => {
365+
const event = factory.track(
366+
'Order Completed',
367+
{ ...shoes },
368+
{ timestamp: undefined, traits: { foo: 123 } }
369+
)
370+
371+
expect(typeof event.timestamp).toBe('object')
372+
expect(isDate(event.timestamp)).toBeTruthy()
373+
})
353374
})
354375

355376
describe('normalize', () => {
@@ -380,15 +401,4 @@ describe('Event Factory', () => {
380401
})
381402
})
382403
})
383-
384-
it('should ignore undefined options', () => {
385-
const event = factory.track(
386-
'Order Completed',
387-
{ ...shoes },
388-
{ timestamp: undefined, traits: { foo: 123 } }
389-
)
390-
391-
expect(typeof event.timestamp).toBe('object')
392-
expect(isDate(event.timestamp)).toBeTruthy()
393-
})
394404
})

packages/core/src/events/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ interface EventFactorySettings {
1919
user?: User
2020
}
2121

22+
/**
23+
* This is currently only used by node.js, but the original idea was to have something that could be shared between browser and node.
24+
* Unfortunately, there are some differences in the way the two environments handle events, so this is not currently shared.
25+
*/
2226
export class EventFactory {
2327
createMessageId: EventFactorySettings['createMessageId']
2428
user?: User
@@ -201,6 +205,7 @@ export class EventFactory {
201205
'userId',
202206
'anonymousId',
203207
'timestamp',
208+
'messageId',
204209
]
205210

206211
delete options['integrations']
@@ -271,7 +276,7 @@ export class EventFactory {
271276

272277
const evt: CoreSegmentEvent = {
273278
...body,
274-
messageId: this.createMessageId(),
279+
messageId: options.messageId || this.createMessageId(),
275280
}
276281

277282
validateEvent(evt)

packages/core/src/events/interfaces.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ export interface CoreOptions {
3333
anonymousId?: string
3434
userId?: string
3535
traits?: Traits
36+
/**
37+
* Override the messageId. Under normal circumstances, this is not recommended -- but neccessary for deduping events.
38+
*
39+
* **Currently, This option only works in `@segment/analytics-node`.**
40+
*/
41+
messageId?: string
3642
// ugh, this is ugly, but we allow literally any property to be passed to options (which get spread onto the event)
3743
[key: string]: any
3844
}

packages/core/src/validation/__tests__/assertions.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { validateEvent } from '../assertions'
44
const baseEvent: Partial<CoreSegmentEvent> = {
55
userId: 'foo',
66
event: 'Test Event',
7+
messageId: 'foo',
78
}
9+
810
describe(validateEvent, () => {
911
test('should be capable of working with empty properties and traits', () => {
1012
expect(() => validateEvent(undefined)).toThrowError()
@@ -40,6 +42,7 @@ describe(validateEvent, () => {
4042
test('identify: traits should be an object', () => {
4143
expect(() =>
4244
validateEvent({
45+
...baseEvent,
4346
type: 'identify',
4447
traits: undefined,
4548
})
@@ -151,5 +154,29 @@ describe(validateEvent, () => {
151154
})
152155
).toThrowError(/nil/i)
153156
})
157+
158+
test('should fail if messageId is _not_ string', () => {
159+
expect(() =>
160+
validateEvent({
161+
...baseEvent,
162+
type: 'track',
163+
properties: {},
164+
userId: undefined,
165+
anonymousId: 'foo',
166+
messageId: 'bar',
167+
})
168+
).not.toThrow()
169+
170+
expect(() =>
171+
validateEvent({
172+
...baseEvent,
173+
type: 'track',
174+
properties: {},
175+
userId: undefined,
176+
anonymousId: 'foo',
177+
messageId: 123 as any,
178+
})
179+
).toThrow(/messageId/)
180+
})
154181
})
155182
})

packages/core/src/validation/assertions.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,16 @@ export function assertTraits(event: CoreSegmentEvent): void {
5555
}
5656
}
5757

58+
export function assertMessageId(event: CoreSegmentEvent): void {
59+
if (!isString(event.messageId)) {
60+
throw new ValidationError('.messageId', stringError)
61+
}
62+
}
63+
5864
export function validateEvent(event?: CoreSegmentEvent | null) {
5965
assertEventExists(event)
6066
assertEventType(event)
67+
assertMessageId(event)
6168

6269
if (event.type === 'track') {
6370
assertTrackEventName(event)

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ describe('alias', () => {
8888
expect(ctx.event.userId).toEqual('chris radek')
8989
expect(ctx.event.previousId).toEqual('chris')
9090
expect(ctx.event.timestamp).toEqual(timestamp)
91+
expect(ctx.event.messageId).toEqual(expect.any(String))
92+
})
93+
it('allows messageId to be overridden', async () => {
94+
const analytics = createTestAnalytics({
95+
httpClient: testClient,
96+
})
97+
const messageId = 'overridden'
98+
analytics.alias({ userId: 'foo', previousId: 'bar', messageId })
99+
const ctx = await resolveCtx(analytics, 'alias')
100+
expect(ctx.event.messageId).toBe(messageId)
91101
})
92102
})
93103

@@ -107,6 +117,7 @@ describe('group', () => {
107117
expect(ctx.event.userId).toEqual('foo')
108118
expect(ctx.event.anonymousId).toBe('bar')
109119
expect(ctx.event.timestamp).toEqual(timestamp)
120+
expect(ctx.event.messageId).toEqual(expect.any(String))
110121
})
111122

112123
it('invocations are isolated', async () => {
@@ -134,6 +145,16 @@ describe('group', () => {
134145
expect(ctx2.event.anonymousId).toBeUndefined()
135146
expect(ctx2.event.userId).toEqual('me')
136147
})
148+
149+
it('allows messageId to be overridden', async () => {
150+
const analytics = createTestAnalytics({
151+
httpClient: testClient,
152+
})
153+
const messageId = 'overridden'
154+
analytics.group({ groupId: 'foo', userId: 'sup', messageId })
155+
const ctx = await resolveCtx(analytics, 'group')
156+
expect(ctx.event.messageId).toBe(messageId)
157+
})
137158
})
138159

139160
describe('identify', () => {
@@ -181,6 +202,7 @@ describe('page', () => {
181202
expect(ctx1.event.userId).toBeUndefined()
182203
expect(ctx1.event.properties).toEqual({ category })
183204
expect(ctx1.event.timestamp).toEqual(timestamp)
205+
expect(ctx1.event.messageId).toEqual(expect.any(String))
184206

185207
analytics.page({ name, properties: { title: 'wip' }, userId: 'user-id' })
186208

@@ -192,6 +214,7 @@ describe('page', () => {
192214
expect(ctx2.event.userId).toEqual('user-id')
193215
expect(ctx2.event.properties).toEqual({ title: 'wip' })
194216
expect(ctx2.event.timestamp).toEqual(expect.any(Date))
217+
expect(ctx2.event.messageId).toEqual(expect.any(String))
195218

196219
analytics.page({ properties: { title: 'invisible' }, userId: 'user-id' })
197220
const ctx3 = await resolveCtx(analytics, 'page')
@@ -201,6 +224,17 @@ describe('page', () => {
201224
expect(ctx3.event.anonymousId).toBeUndefined()
202225
expect(ctx3.event.userId).toEqual('user-id')
203226
expect(ctx3.event.properties).toEqual({ title: 'invisible' })
227+
expect(ctx3.event.messageId).toEqual(expect.any(String))
228+
})
229+
230+
it('allows messageId to be overridden', async () => {
231+
const analytics = createTestAnalytics({
232+
httpClient: testClient,
233+
})
234+
const messageId = 'overridden'
235+
analytics.page({ name: 'foo', userId: 'sup', messageId })
236+
const ctx = await resolveCtx(analytics, 'page')
237+
expect(ctx.event.messageId).toBe(messageId)
204238
})
205239
})
206240

@@ -224,6 +258,7 @@ describe('screen', () => {
224258
expect(ctx1.event.userId).toEqual('user-id')
225259
expect(ctx1.event.properties).toEqual({ title: 'wip' })
226260
expect(ctx1.event.timestamp).toEqual(timestamp)
261+
expect(ctx1.event.messageId).toEqual(expect.any(String))
227262

228263
analytics.screen({
229264
properties: { title: 'invisible' },
@@ -238,6 +273,16 @@ describe('screen', () => {
238273
expect(ctx2.event.userId).toEqual('user-id')
239274
expect(ctx2.event.properties).toEqual({ title: 'invisible' })
240275
expect(ctx2.event.timestamp).toEqual(expect.any(Date))
276+
expect(ctx2.event.messageId).toEqual(expect.any(String))
277+
})
278+
it('allows messageId to be overridden', async () => {
279+
const analytics = createTestAnalytics({
280+
httpClient: testClient,
281+
})
282+
const messageId = 'overridden'
283+
analytics.screen({ name: 'foo', userId: 'sup', messageId })
284+
const ctx = await resolveCtx(analytics, 'screen')
285+
expect(ctx.event.messageId).toBe(messageId)
241286
})
242287
})
243288

@@ -272,6 +317,7 @@ describe('track', () => {
272317
expect(ctx1.event.anonymousId).toEqual('unknown')
273318
expect(ctx1.event.userId).toEqual('known')
274319
expect(ctx1.event.timestamp).toEqual(timestamp)
320+
expect(ctx1.event.messageId).toEqual(expect.any(String))
275321

276322
analytics.track({
277323
event: eventName,
@@ -286,6 +332,17 @@ describe('track', () => {
286332
expect(ctx2.event.anonymousId).toBeUndefined()
287333
expect(ctx2.event.userId).toEqual('known')
288334
expect(ctx2.event.timestamp).toEqual(expect.any(Date))
335+
expect(ctx2.event.messageId).toEqual(expect.any(String))
336+
})
337+
338+
it('allows messageId to be overridden', async () => {
339+
const analytics = createTestAnalytics({
340+
httpClient: testClient,
341+
})
342+
const messageId = 'overridden'
343+
analytics.track({ event: 'foo', userId: 'sup', messageId })
344+
const ctx = await resolveCtx(analytics, 'track')
345+
expect(ctx.event.messageId).toBe(messageId)
289346
})
290347
})
291348

packages/node/src/app/analytics-node.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,21 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
148148
* @link https://segment.com/docs/connections/sources/catalog/libraries/server/node/#alias
149149
*/
150150
alias(
151-
{ userId, previousId, context, timestamp, integrations }: AliasParams,
151+
{
152+
userId,
153+
previousId,
154+
context,
155+
timestamp,
156+
integrations,
157+
messageId,
158+
}: AliasParams,
152159
callback?: Callback
153160
): void {
154161
const segmentEvent = this._eventFactory.alias(userId, previousId, {
155162
context,
156163
integrations,
157164
timestamp,
165+
messageId,
158166
})
159167
this._dispatch(segmentEvent, callback)
160168
}
@@ -172,6 +180,7 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
172180
traits = {},
173181
context,
174182
integrations,
183+
messageId,
175184
}: GroupParams,
176185
callback?: Callback
177186
): void {
@@ -181,6 +190,7 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
181190
userId,
182191
timestamp,
183192
integrations,
193+
messageId,
184194
})
185195

186196
this._dispatch(segmentEvent, callback)
@@ -198,6 +208,7 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
198208
context,
199209
timestamp,
200210
integrations,
211+
messageId,
201212
}: IdentifyParams,
202213
callback?: Callback
203214
): void {
@@ -207,6 +218,7 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
207218
userId,
208219
timestamp,
209220
integrations,
221+
messageId,
210222
})
211223
this._dispatch(segmentEvent, callback)
212224
}
@@ -225,14 +237,15 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
225237
context,
226238
timestamp,
227239
integrations,
240+
messageId,
228241
}: PageParams,
229242
callback?: Callback
230243
): void {
231244
const segmentEvent = this._eventFactory.page(
232245
category ?? null,
233246
name ?? null,
234247
properties,
235-
{ context, anonymousId, userId, timestamp, integrations }
248+
{ context, anonymousId, userId, timestamp, integrations, messageId }
236249
)
237250
this._dispatch(segmentEvent, callback)
238251
}
@@ -253,14 +266,15 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
253266
context,
254267
timestamp,
255268
integrations,
269+
messageId,
256270
}: PageParams,
257271
callback?: Callback
258272
): void {
259273
const segmentEvent = this._eventFactory.screen(
260274
category ?? null,
261275
name ?? null,
262276
properties,
263-
{ context, anonymousId, userId, timestamp, integrations }
277+
{ context, anonymousId, userId, timestamp, integrations, messageId }
264278
)
265279

266280
this._dispatch(segmentEvent, callback)
@@ -279,6 +293,7 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
279293
context,
280294
timestamp,
281295
integrations,
296+
messageId,
282297
}: TrackParams,
283298
callback?: Callback
284299
): void {
@@ -288,6 +303,7 @@ export class Analytics extends NodeEmitter implements CoreAnalytics {
288303
anonymousId,
289304
timestamp,
290305
integrations,
306+
messageId,
291307
})
292308

293309
this._dispatch(segmentEvent, callback)

0 commit comments

Comments
 (0)