Skip to content

Commit c636cb7

Browse files
committed
chore: add tests
1 parent c94e5d8 commit c636cb7

File tree

4 files changed

+318
-0
lines changed

4 files changed

+318
-0
lines changed

src/__tests__/personProcessing.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,4 +813,120 @@ describe('person processing', () => {
813813
expect(beforeSendMock.mock.calls[1][0].properties.$process_person_profile).toEqual(false)
814814
})
815815
})
816+
817+
describe('property calls deduplication', () => {
818+
it('should dedupe identical consecutive calls to setPersonProperties', async () => {
819+
const { posthog, beforeSendMock } = await setup('always')
820+
821+
posthog.setPersonProperties({ email: '[email protected]' })
822+
posthog.setPersonProperties({ email: '[email protected]' })
823+
824+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
825+
expect(beforeSendMock).toHaveBeenCalledWith(
826+
expect.objectContaining({
827+
event: '$set',
828+
properties: expect.objectContaining({
829+
$set: { email: '[email protected]' },
830+
$set_once: {},
831+
}),
832+
})
833+
)
834+
})
835+
836+
it('should not dedupe when properties are different', async () => {
837+
const { posthog, beforeSendMock } = await setup('always')
838+
839+
posthog.setPersonProperties({ email: '[email protected]' })
840+
posthog.setPersonProperties({ email: '[email protected]' })
841+
842+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
843+
})
844+
845+
it('should not dedupe when set_once properties are different', async () => {
846+
const { posthog, beforeSendMock } = await setup('always')
847+
848+
posthog.setPersonProperties({ email: '[email protected]' }, { first_seen: 'today' })
849+
posthog.setPersonProperties({ email: '[email protected]' }, { first_seen: 'yesterday' })
850+
851+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
852+
})
853+
854+
it('does not dedupe when properties are in different order but identical', async () => {
855+
const { posthog, beforeSendMock } = await setup('always')
856+
857+
posthog.setPersonProperties({ name: 'John', email: '[email protected]' })
858+
posthog.setPersonProperties({ email: '[email protected]', name: 'John' })
859+
860+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
861+
})
862+
863+
it('should log a message when deduping properties', async () => {
864+
const { posthog } = await setup('always')
865+
mockLogger.info = jest.fn()
866+
867+
posthog.setPersonProperties({ email: '[email protected]' })
868+
posthog.setPersonProperties({ email: '[email protected]' })
869+
870+
expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('duplicate'))
871+
})
872+
873+
it('should not dedupe after distinct_id changes', async () => {
874+
const { posthog, beforeSendMock } = await setup('always')
875+
876+
posthog.setPersonProperties({ email: '[email protected]' })
877+
878+
posthog.identify('new-id')
879+
880+
posthog.setPersonProperties({ email: '[email protected]' })
881+
882+
// 1 for setPersonProperties, 1 for identify, 1 for second setPersonProperties
883+
expect(beforeSendMock).toHaveBeenCalledTimes(3)
884+
})
885+
886+
it('should dedupe when using people.set with identical properties', async () => {
887+
const { posthog, beforeSendMock } = await setup('always')
888+
889+
posthog.people.set({ email: '[email protected]' })
890+
posthog.people.set({ email: '[email protected]' })
891+
892+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
893+
})
894+
895+
it('should dedupe when mixing people.set and setPersonProperties with identical properties', async () => {
896+
const { posthog, beforeSendMock } = await setup('always')
897+
898+
posthog.people.set({ email: '[email protected]' })
899+
posthog.setPersonProperties({ email: '[email protected]' })
900+
901+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
902+
})
903+
904+
it('should dedupe when using people.set_once with identical properties', async () => {
905+
const { posthog, beforeSendMock } = await setup('always')
906+
907+
posthog.people.set_once({ first_seen: 'today' })
908+
posthog.people.set_once({ first_seen: 'today' })
909+
910+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
911+
})
912+
913+
it('should not dedupe when mixing set and set_once with same properties', async () => {
914+
const { posthog, beforeSendMock } = await setup('always')
915+
916+
posthog.people.set({ email: '[email protected]' })
917+
posthog.people.set_once({ email: '[email protected]' })
918+
919+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
920+
})
921+
922+
it('should reset deduplication cache after reset()', async () => {
923+
const { posthog, beforeSendMock } = await setup('always')
924+
925+
posthog.setPersonProperties({ email: '[email protected]' })
926+
posthog.reset()
927+
posthog.setPersonProperties({ email: '[email protected]' })
928+
929+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
930+
})
931+
})
816932
})

src/__tests__/posthog-core.identify.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,4 +344,100 @@ describe('identify()', () => {
344344
expect(instance.featureFlags.setAnonymousDistinctId).not.toHaveBeenCalled()
345345
})
346346
})
347+
348+
describe('identify deduplication', () => {
349+
beforeEach(() => {
350+
instance.persistence!.props['distinct_id'] = 'oldIdentity'
351+
})
352+
353+
it('dedupes identical consecutive property updates with same identity', () => {
354+
instance.identify('new-user', { email: '[email protected]' })
355+
356+
instance.identify('new-user', { email: '[email protected]' })
357+
358+
// Should only send one $identify and no additional $set
359+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
360+
expect(beforeSendMock).toHaveBeenCalledWith(expect.objectContaining({ event: '$identify' }))
361+
})
362+
363+
it('does not dedupe when properties are different', () => {
364+
instance.identify('new-user', { email: '[email protected]' })
365+
instance.identify('new-user', { email: '[email protected]' })
366+
367+
// Should send one $identify and one $set
368+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
369+
expect(beforeSendMock.mock.calls[0][0].event).toBe('$identify')
370+
expect(beforeSendMock.mock.calls[1][0].event).toBe('$set')
371+
})
372+
373+
it('does not dedupe when set_once properties are different', () => {
374+
instance.identify('new-user', { email: '[email protected]' }, { first_seen: 'today' })
375+
instance.identify('new-user', { email: '[email protected]' }, { first_seen: 'yesterday' })
376+
377+
// Should send one $identify and one $set
378+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
379+
expect(beforeSendMock.mock.calls[0][0].event).toBe('$identify')
380+
expect(beforeSendMock.mock.calls[1][0].event).toBe('$set')
381+
})
382+
383+
it('does not dedupe when properties are identical but in different order', () => {
384+
instance.identify('new-user', { name: 'John', email: '[email protected]' })
385+
instance.identify('new-user', { email: '[email protected]', name: 'John' })
386+
387+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
388+
})
389+
390+
it('does not dedupe when identity changes', () => {
391+
instance.identify('user-1', { email: '[email protected]' })
392+
instance.identify('user-2', { email: '[email protected]' })
393+
394+
// Should send two $identify events
395+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
396+
expect(beforeSendMock.mock.calls[0][0].event).toBe('$identify')
397+
expect(beforeSendMock.mock.calls[1][0].event).toBe('$identify')
398+
})
399+
400+
it('logs a message when deduping properties', () => {
401+
console.info = jest.fn()
402+
403+
instance.identify('new-user', { email: '[email protected]' })
404+
instance.identify('new-user', { email: '[email protected]' })
405+
406+
expect(console.info).toHaveBeenCalledWith(expect.stringContaining('duplicate'))
407+
})
408+
409+
it('dedupes when mixing identify and setPersonProperties with identical properties', () => {
410+
instance.identify('new-user', { email: '[email protected]' })
411+
instance.setPersonProperties({ email: '[email protected]' })
412+
413+
console.log(beforeSendMock.mock.calls)
414+
415+
// Should only send one $identify
416+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
417+
expect(beforeSendMock.mock.calls[0][0].event).toBe('$identify')
418+
})
419+
420+
it('does not dedupe after reset()', () => {
421+
instance.identify('new-user', { email: '[email protected]' })
422+
423+
instance.reset()
424+
instance.persistence!.props['distinct_id'] = 'oldIdentity' // Reset to trigger a new identify
425+
426+
instance.identify('new-user', { email: '[email protected]' })
427+
428+
// Should send two $identify events
429+
expect(beforeSendMock).toHaveBeenCalledTimes(2)
430+
expect(beforeSendMock.mock.calls[0][0].event).toBe('$identify')
431+
expect(beforeSendMock.mock.calls[1][0].event).toBe('$identify')
432+
})
433+
434+
it('dedupes when no properties are passed in consecutive calls', () => {
435+
instance.identify('new-user')
436+
instance.identify('new-user')
437+
438+
// Should only send one $identify
439+
expect(beforeSendMock).toHaveBeenCalledTimes(1)
440+
expect(beforeSendMock.mock.calls[0][0].event).toBe('$identify')
441+
})
442+
})
347443
})

src/__tests__/utils.test.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { isLikelyBot, DEFAULT_BLOCKED_UA_STRS, isBlockedUA, NavigatorUAData } fr
1313
import { expect } from '@jest/globals'
1414

1515
import { _base64Encode } from '../utils/encode-utils'
16+
import { getPersonPropertiesHash } from '../utils/identify-utils'
1617

1718
function userAgentFor(botString: string) {
1819
const randOne = (Math.random() + 1).toString(36).substring(7)
@@ -314,4 +315,101 @@ describe('utils', () => {
314315
expect(_base64Encode(input)).toBe(expectedOutput)
315316
})
316317
})
318+
319+
describe('getPersonPropertiesHash', () => {
320+
it('returns a string hash with only distinct_id', () => {
321+
const hash = getPersonPropertiesHash('user123')
322+
expect(typeof hash).toBe('string')
323+
expect(hash).toContain('user123')
324+
})
325+
326+
it('returns the same hash for the same inputs', () => {
327+
const distinct_id = 'user123'
328+
const userPropertiesToSet = { name: 'John Doe', email: '[email protected]' }
329+
const userPropertiesToSetOnce = { first_seen: '2023-01-01' }
330+
331+
const hash1 = getPersonPropertiesHash(distinct_id, userPropertiesToSet, userPropertiesToSetOnce)
332+
const hash2 = getPersonPropertiesHash(distinct_id, userPropertiesToSet, userPropertiesToSetOnce)
333+
334+
expect(hash1).toBe(hash2)
335+
})
336+
337+
it('returns different hashes for different distinct_ids', () => {
338+
const props = { name: 'John Doe' }
339+
const hash1 = getPersonPropertiesHash('user1', props)
340+
const hash2 = getPersonPropertiesHash('user2', props)
341+
342+
expect(hash1).not.toBe(hash2)
343+
})
344+
345+
it('returns different hashes for different userPropertiesToSet', () => {
346+
const distinct_id = 'user123'
347+
const hash1 = getPersonPropertiesHash(distinct_id, { name: 'John' })
348+
const hash2 = getPersonPropertiesHash(distinct_id, { name: 'Jane' })
349+
350+
expect(hash1).not.toBe(hash2)
351+
})
352+
353+
it('returns different hashes for different userPropertiesToSetOnce', () => {
354+
const distinct_id = 'user123'
355+
const hash1 = getPersonPropertiesHash(distinct_id, undefined, { first_seen: '2023-01-01' })
356+
const hash2 = getPersonPropertiesHash(distinct_id, undefined, { first_seen: '2023-02-01' })
357+
358+
expect(hash1).not.toBe(hash2)
359+
})
360+
361+
it('includes all parameters in the hash', () => {
362+
const distinct_id = 'user123'
363+
const userPropertiesToSet = { name: 'John Doe' }
364+
const userPropertiesToSetOnce = { first_seen: '2023-01-01' }
365+
366+
const hash = getPersonPropertiesHash(distinct_id, userPropertiesToSet, userPropertiesToSetOnce)
367+
368+
expect(hash).toContain('user123')
369+
expect(hash).toContain('John Doe')
370+
expect(hash).toContain('2023-01-01')
371+
})
372+
373+
it('handles undefined userPropertiesToSet', () => {
374+
const distinct_id = 'user123'
375+
const userPropertiesToSetOnce = { first_seen: '2023-01-01' }
376+
377+
const hash = getPersonPropertiesHash(distinct_id, undefined, userPropertiesToSetOnce)
378+
379+
expect(hash).toContain('user123')
380+
expect(hash).toContain('2023-01-01')
381+
expect(hash).not.toContain('undefined')
382+
})
383+
384+
it('handles undefined userPropertiesToSetOnce', () => {
385+
const distinct_id = 'user123'
386+
const userPropertiesToSet = { name: 'John Doe' }
387+
388+
const hash = getPersonPropertiesHash(distinct_id, userPropertiesToSet)
389+
390+
expect(hash).toContain('user123')
391+
expect(hash).toContain('John Doe')
392+
})
393+
394+
it('handles complex nested properties', () => {
395+
const distinct_id = 'user123'
396+
const userPropertiesToSet = {
397+
profile: {
398+
name: 'John Doe',
399+
contacts: ['email', 'phone'],
400+
details: {
401+
age: 30,
402+
location: 'New York',
403+
},
404+
},
405+
}
406+
407+
const hash = getPersonPropertiesHash(distinct_id, userPropertiesToSet)
408+
409+
expect(typeof hash).toBe('string')
410+
expect(hash).toContain('user123')
411+
expect(hash).toContain('John Doe')
412+
expect(hash).toContain('New York')
413+
})
414+
})
317415
})

src/posthog-core.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1469,6 +1469,14 @@ export class PostHog {
14691469
},
14701470
{ $set: userPropertiesToSet || {}, $set_once: userPropertiesToSetOnce || {} }
14711471
)
1472+
1473+
this._cachedPersonProperties = getPersonPropertiesHash(
1474+
new_distinct_id,
1475+
userPropertiesToSet,
1476+
userPropertiesToSetOnce
1477+
)
1478+
1479+
logger.info('this._cachedPersonProperties', this._cachedPersonProperties)
14721480
// let the reload feature flag request know to send this previous distinct id
14731481
// for flag consistency
14741482
this.featureFlags.setAnonymousDistinctId(previous_distinct_id)

0 commit comments

Comments
 (0)