diff --git a/event_factory.js b/event_factory.js index 8f87d48..ccd713f 100644 --- a/event_factory.js +++ b/event_factory.js @@ -19,6 +19,10 @@ function EventFactory(withReasons) { return false; } + function userContextKind(user) { + return user.anonymous ? 'anonymousUser' : 'user'; + } + ef.newEvalEvent = (flag, user, detail, defaultVal, prereqOfFlag) => { const addExperimentData = isExperiment(flag, detail.reason); const e = { @@ -44,6 +48,9 @@ function EventFactory(withReasons) { if (addExperimentData || withReasons) { e.reason = detail.reason; } + if (user && user.anonymous) { + e.contextKind = userContextKind(user); + } return e; }; @@ -67,6 +74,9 @@ function EventFactory(withReasons) { if (withReasons) { e.reason = detail.reason; } + if (user && user.anonymous) { + e.contextKind = userContextKind(user); + } return e; }; @@ -82,6 +92,9 @@ function EventFactory(withReasons) { if (withReasons) { e.reason = detail.reason; } + if (user && user.anonymous) { + e.contextKind = userContextKind(user); + } return e; }; @@ -102,12 +115,24 @@ function EventFactory(withReasons) { if (data !== null && data !== undefined) { e.data = data; } + if (user && user.anonymous) { + e.contextKind = userContextKind(user); + } if (metricValue !== null && metricValue !== undefined) { e.metricValue = metricValue; } return e; }; + ef.newAliasEvent = (user, previousUser) => ({ + kind: 'alias', + key: user.key, + contextKind: userContextKind(user), + previousKey: previousUser.key, + previousContextKind: userContextKind(previousUser), + creationDate: new Date().getTime(), + }); + return ef; } diff --git a/index.d.ts b/index.d.ts index 6d1bd9a..265f6c3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -981,6 +981,21 @@ declare module 'launchdarkly-node-server-sdk' { * as part of the custom event for Data Export. */ track(key: string, user: LDUser, data?: any, metricValue?: number): void; + + /** + * Associates two users for analytics purposes. + * + * This can be helpful in the situation where a person is represented by multiple + * LaunchDarkly users. This may happen, for example, when a person initially logs into + * an application-- the person might be represented by an anonymous user prior to logging + * in and a different user after logging in, as denoted by a different user key. + * + * @param user + * The newly identified user. + * @param previousUser + * The previously identified user. + */ + alias(user: LDUser, previousUser: LDUser): void; /** * Identifies a user to LaunchDarkly. diff --git a/index.js b/index.js index 83e352a..1a935fe 100644 --- a/index.js +++ b/index.js @@ -367,6 +367,14 @@ const newClient = function(sdkKey, originalConfig) { client.isOffline = () => config.offline; + client.alias = (user, previousUser) => { + if (!user || !previousUser) { + return; + } + + eventProcessor.sendEvent(eventFactoryDefault.newAliasEvent(user, previousUser)); + }; + client.track = (eventName, user, data, metricValue) => { if (!userExistsAndHasKey(user)) { config.logger.warn(messages.missingUserKeyNoEvent()); diff --git a/test-types.ts b/test-types.ts index 401ecef..b475cf0 100644 --- a/test-types.ts +++ b/test-types.ts @@ -41,6 +41,7 @@ var allOptions: ld.LDOptions = { wrapperVersion: 'y' }; var userWithKeyOnly: ld.LDUser = { key: 'user' }; +var anonymousUser: ld.LDUser = { key: 'anon-user', anonymous: true }; var user: ld.LDUser = { key: 'user', name: 'name', @@ -69,6 +70,8 @@ client.track('key', user); client.track('key', user, { ok: 1 }); client.track('key', user, null, 1.5); +client.alias(user, anonymousUser); + // evaluation methods with callbacks client.variation('key', user, false, (value: ld.LDFlagValue) => { }); client.variation('key', user, 2, (value: ld.LDFlagValue) => { }); diff --git a/test/LDClient-events-test.js b/test/LDClient-events-test.js index 62a09ad..61594cc 100644 --- a/test/LDClient-events-test.js +++ b/test/LDClient-events-test.js @@ -4,6 +4,7 @@ describe('LDClient - analytics events', () => { var eventProcessor; var defaultUser = { key: 'user' }; + var anonymousUser = { key: 'anon-user', anonymous: true }; var userWithNoKey = { name: 'Keyless Joe' }; var userWithEmptyKey = { key: '' }; @@ -40,6 +41,35 @@ describe('LDClient - analytics events', () => { }); }); + it('generates event for existing feature when user is anonymous', async () => { + var flag = { + key: 'flagkey', + version: 1, + on: true, + targets: [], + fallthrough: { variation: 1 }, + variations: ['a', 'b'], + trackEvents: true + }; + var client = stubs.createClient({ eventProcessor: eventProcessor }, { flagkey: flag }); + await client.waitForInitialization(); + await client.variation(flag.key, anonymousUser, 'c'); + + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'feature', + key: 'flagkey', + version: 1, + user: anonymousUser, + contextKind: 'anonymousUser', + variation: 1, + value: 'b', + default: 'c', + trackEvents: true + }); + }); + it('generates event for existing feature with reason', async () => { var flag = { key: 'flagkey', @@ -215,6 +245,23 @@ describe('LDClient - analytics events', () => { }); }); + it('generates event for unknown feature when user is anonymous', async () => { + var client = stubs.createClient({ eventProcessor: eventProcessor }, {}); + await client.waitForInitialization(); + await client.variation('flagkey', anonymousUser, 'c'); + + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'feature', + key: 'flagkey', + user: anonymousUser, + contextKind: 'anonymousUser', + value: 'c', + default: 'c' + }); + }); + it('generates event for existing feature when user key is missing', async () => { var flag = { key: 'flagkey', @@ -376,6 +423,25 @@ describe('LDClient - analytics events', () => { expect(logger.warn).not.toHaveBeenCalled(); }); + it('generates an event for an anonymous user', async () => { + var data = { thing: 'stuff' }; + var logger = stubs.stubLogger(); + var client = stubs.createClient({ eventProcessor: eventProcessor, logger: logger }, {}); + await client.waitForInitialization(); + + client.track('eventkey', anonymousUser, data); + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'custom', + key: 'eventkey', + user: anonymousUser, + contextKind: 'anonymousUser', + data: data + }); + expect(logger.warn).not.toHaveBeenCalled(); + }); + it('does not generate an event, and logs a warning, if user is missing', async () => { var logger = stubs.stubLogger(); var client = stubs.createClient({ eventProcessor: eventProcessor, logger: logger }, {}); @@ -406,4 +472,72 @@ describe('LDClient - analytics events', () => { expect(logger.warn).toHaveBeenCalledTimes(1); }); }); + + describe('alias', () => { + it('generates an event for aliasing anonymous to known', async () => { + var client = stubs.createClient({ eventProcessor: eventProcessor }, {}); + await client.waitForInitialization(); + + client.alias(defaultUser, anonymousUser); + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'alias', + key: 'user', + previousKey: 'anon-user', + contextKind: 'user', + previousContextKind: 'anonymousUser' + }); + }); + + it('generates an event for aliasing known to known', async () => { + var anotherUser = { key: 'another-user' }; + var client = stubs.createClient({ eventProcessor: eventProcessor }, {}); + await client.waitForInitialization(); + + client.alias(defaultUser, anotherUser); + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'alias', + key: 'user', + previousKey: 'another-user', + contextKind: 'user', + previousContextKind: 'user' + }); + }); + + it('generates an event for aliasing anonymous to anonymous', async () => { + var anotherAnonymousUser = { key: 'another-anon-user', anonymous: true }; + var client = stubs.createClient({ eventProcessor: eventProcessor }, {}); + await client.waitForInitialization(); + + client.alias(anonymousUser, anotherAnonymousUser); + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'alias', + key: 'anon-user', + previousKey: 'another-anon-user', + contextKind: 'anonymousUser', + previousContextKind: 'anonymousUser' + }); + }); + + it('generates an event for aliasing known to anonymous', async () => { + var client = stubs.createClient({ eventProcessor: eventProcessor }, {}); + await client.waitForInitialization(); + + client.alias(anonymousUser, defaultUser); + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'alias', + key: 'anon-user', + previousKey: 'user', + contextKind: 'anonymousUser', + previousContextKind: 'user' + }); + }); + }); });