@@ -3,6 +3,7 @@ import { mockLogger } from './helpers/mock-logger'
33import * as globals from '../utils/globals'
44import { document , window } from '../utils/globals'
55import { uuidv7 } from '../uuidv7'
6+ import { isUndefined } from '@posthog/core'
67import { ENABLE_PERSON_PROCESSING , USER_STATE } from '../constants'
78import { createPosthogInstance , defaultPostHog } from './helpers/posthog-instance'
89import { PostHogConfig , RemoteConfig } from '../types'
@@ -794,6 +795,7 @@ describe('posthog core', () => {
794795 it ( 'sets the right distinctID' , ( ) => {
795796 const posthog = posthogWith (
796797 {
798+ token : 'bootstrap-distinctid-' + uuidv7 ( ) ,
797799 bootstrap : {
798800 distinctID : 'abcd' ,
799801 } ,
@@ -820,6 +822,7 @@ describe('posthog core', () => {
820822 it ( 'treats identified distinctIDs appropriately' , ( ) => {
821823 const posthog = posthogWith (
822824 {
825+ token : 'bootstrap-identified-' + uuidv7 ( ) ,
823826 bootstrap : {
824827 distinctID : 'abcd' ,
825828 isIdentifiedID : true ,
@@ -942,6 +945,141 @@ describe('posthog core', () => {
942945 posthog . featureFlags . onFeatureFlags ( ( ) => ( called = true ) )
943946 expect ( called ) . toEqual ( false )
944947 } )
948+
949+ describe ( 'auto-identify on bootstrap' , ( ) => {
950+ afterEach ( ( ) => {
951+ jest . restoreAllMocks ( )
952+ } )
953+
954+ it ( 'calls identify when bootstrap has identified distinctID that differs from persisted anonymous ID' , ( ) => {
955+ const token = 'auto-identify-test-' + uuidv7 ( )
956+
957+ // First instance creates an anonymous user in persistence
958+ const first = posthogWith ( { token } )
959+ expect ( first . get_distinct_id ( ) ) . toBeTruthy ( )
960+ expect ( first . persistence . get_property ( USER_STATE ) ) . toBe ( 'anonymous' )
961+
962+ const identifySpy = jest . spyOn ( PostHog . prototype , 'identify' )
963+ const captureSpy = jest . spyOn ( PostHog . prototype , 'capture' )
964+
965+ // Second instance bootstraps with an identified user
966+ const second = posthogWith ( {
967+ token,
968+ bootstrap : {
969+ distinctID : 'user-123' ,
970+ isIdentifiedID : true ,
971+ } ,
972+ } )
973+
974+ expect ( identifySpy ) . toHaveBeenCalledWith ( 'user-123' )
975+ expect ( second . get_distinct_id ( ) ) . toBe ( 'user-123' )
976+ expect ( second . persistence . get_property ( USER_STATE ) ) . toBe ( 'identified' )
977+
978+ // Verify the $identify event includes the anonymous-to-identified mapping
979+ const captureCall = captureSpy . mock . calls . find ( ( call ) => call [ 0 ] === '$identify' )
980+ expect ( captureCall ) . toBeDefined ( )
981+ expect ( captureCall ! [ 1 ] ) . toMatchObject ( {
982+ distinct_id : 'user-123' ,
983+ $anon_distinct_id : expect . any ( String ) ,
984+ } )
985+
986+ // Subsequent identify with the same ID should be a no-op
987+ identifySpy . mockClear ( )
988+ second . identify ( 'user-123' )
989+ // identify is called but since distinct_id matches, no $identify event fires
990+ expect ( second . get_distinct_id ( ) ) . toBe ( 'user-123' )
991+ } )
992+
993+ it ( 'does not call identify when bootstrap distinctID matches persisted ID' , ( ) => {
994+ const token = 'auto-identify-same-' + uuidv7 ( )
995+
996+ // First instance creates an anonymous user
997+ const first = posthogWith ( { token } )
998+ const anonId = first . get_distinct_id ( )
999+
1000+ const identifySpy = jest . spyOn ( PostHog . prototype , 'identify' )
1001+
1002+ // Second instance bootstraps with the same anonymous ID
1003+ posthogWith ( {
1004+ token,
1005+ bootstrap : {
1006+ distinctID : anonId ,
1007+ isIdentifiedID : true ,
1008+ } ,
1009+ } )
1010+
1011+ expect ( identifySpy ) . not . toHaveBeenCalled ( )
1012+ } )
1013+
1014+ it . each ( [
1015+ { isIdentifiedID : false , description : 'false' } ,
1016+ { isIdentifiedID : undefined , description : 'omitted' } ,
1017+ ] ) ( 'does not call identify when isIdentifiedID is $description' , ( { isIdentifiedID } ) => {
1018+ const token = 'auto-identify-non-true-' + uuidv7 ( )
1019+
1020+ // First instance creates an anonymous user
1021+ posthogWith ( { token } )
1022+
1023+ const identifySpy = jest . spyOn ( PostHog . prototype , 'identify' )
1024+
1025+ // Second instance bootstraps with isIdentifiedID that is not true
1026+ posthogWith ( {
1027+ token,
1028+ bootstrap : {
1029+ distinctID : 'user-456' ,
1030+ ...( ! isUndefined ( isIdentifiedID ) && { isIdentifiedID } ) ,
1031+ } ,
1032+ } )
1033+
1034+ expect ( identifySpy ) . not . toHaveBeenCalled ( )
1035+ } )
1036+
1037+ it ( 'does not call identify when there is no existing persisted ID (first visit)' , ( ) => {
1038+ const token = 'auto-identify-first-visit-' + uuidv7 ( )
1039+
1040+ const identifySpy = jest . spyOn ( PostHog . prototype , 'identify' )
1041+
1042+ // First visit with bootstrap - no prior persistence
1043+ const posthog = posthogWith ( {
1044+ token,
1045+ bootstrap : {
1046+ distinctID : 'user-789' ,
1047+ isIdentifiedID : true ,
1048+ } ,
1049+ } )
1050+
1051+ expect ( identifySpy ) . not . toHaveBeenCalled ( )
1052+ expect ( posthog . get_distinct_id ( ) ) . toBe ( 'user-789' )
1053+ expect ( posthog . persistence . get_property ( USER_STATE ) ) . toBe ( 'identified' )
1054+ } )
1055+
1056+ it ( 'does not call identify when existing user is already identified' , ( ) => {
1057+ const token = 'auto-identify-already-id-' + uuidv7 ( )
1058+
1059+ // First instance: create and identify a user
1060+ const first = posthogWith ( { token } , { capture : jest . fn ( ) } )
1061+ first . identify ( 'existing-user' )
1062+ expect ( first . persistence . get_property ( USER_STATE ) ) . toBe ( 'identified' )
1063+
1064+ const identifySpy = jest . spyOn ( PostHog . prototype , 'identify' )
1065+
1066+ // Second instance bootstraps with a different identified user
1067+ const second = posthogWith ( {
1068+ token,
1069+ bootstrap : {
1070+ distinctID : 'new-user' ,
1071+ isIdentifiedID : true ,
1072+ } ,
1073+ } )
1074+
1075+ // Should NOT auto-identify because user was already identified
1076+ expect ( identifySpy ) . not . toHaveBeenCalled ( )
1077+
1078+ // Existing identity should be preserved (bootstrap should NOT silently switch identities)
1079+ expect ( second . get_distinct_id ( ) ) . toBe ( 'existing-user' )
1080+ expect ( second . persistence . get_property ( USER_STATE ) ) . toBe ( 'identified' )
1081+ } )
1082+ } )
9451083 } )
9461084
9471085 describe ( 'init()' , ( ) => {
0 commit comments